Recipes / Spawn a long-running agent

Spawn a long-running agent

Most MCP servers expose CRUD. Cognethics also exposes agentic primitives — the same ones that orchestrate work inside the platform. This recipe walks the full round-trip: mission defines scope and budget, spawn creates a worker under a persona, check_budget enforces the ceiling at dispatch time, wait_for_messages blocks on fold-back instead of polling, complete_task folds the result up the hierarchy, and audit returns a hash-chained record of every step.

Prerequisites. A sandbox ck_ key and a tenant subdomain (see Quickstart). Every example below is a real MCP JSON-RPC tools/call against https://{tenant}.cognethics.com/api/mcp/mcp. Replace ck_REPLACE_ME with your key.

The pieces

  • Mission — goal-bounded operational context. Carries the budget ceiling, the module scope, and the max-agents cap. Created in planning, transitions to active on launch.
  • Persona — role template. Authority level, allowed tools, memory scope. Same permissions whether the calling actor is a human or an agent.
  • Instance — a running agent under a persona, inside a mission. Spawned from a persona template; has its own session, its own budget slice, and its own audit lineage.
  • Fold-back — when a child instance finishes, it folds its result upward to its parent. The parent blocks on pj_wait_for_messages rather than polling.

Step 1 — Create the mission

agent

The mission is the unit of accounting. budget_usd is enforced on every tool call inside the mission — once the ceiling is reached the dispatcher refuses to spawn further work and refuses paid tool calls.

Request:

POST https://{tenant}.cognethics.com/api/mcp/mcp
Authorization: Bearer ck_REPLACE_ME
Content-Type: application/json
Accept: application/json, text/event-stream

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "pj_create_mission",
    "arguments": {
      "name": "Spare-parts audit",
      "goal": "Audit spare-parts inventory: identify items with no service history in 12+ months and items with stock below reorder threshold.",
      "description": "Cross-reference inventory records, maintenance-task completions, and reorder thresholds. Emit a report grouped by category and a CSV of action items.",
      "initial_modules": ["spare-parts", "finance"],
      "budget_usd": 5.0,
      "max_agents": 4,
      "allow_dynamic_personas": false
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"mission_id\": \"a7b3c2d1-1234-4567-89ab-cdef01234567\", \"name\": \"Spare-parts audit\", \"status\": \"planning\", \"budget_usd\": 5.0, \"max_agents\": 4, \"organization\": \"<org-uuid>\", \"created_at\": \"2026-05-16T18:42:00Z\"}"
    }]
  }
}

The mission is in planning — created, scoped, but not yet running. Hang on to mission_id.

Step 2 — Launch the mission

agent

Launch transitions the mission from planning to active and creates the lead instance — the root of the agent hierarchy. Pass the persona that plays the lead role; it inherits the mission's budget and module scope.

Request:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "pj_launch_mission",
    "arguments": {
      "mission_id": "a7b3c2d1-1234-4567-89ab-cdef01234567",
      "persona_id": "<lead-persona-uuid>",
      "initial_task": "Plan the audit. Delegate inventory queries to a worker child; synthesize results when it folds back.",
      "model": "claude-sonnet-4-6",
      "effort": "high"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"mission_id\": \"a7b3c2d1-...\", \"status\": \"active\", \"lead_instance_id\": \"11111111-2222-3333-4444-555555555555\", \"persona_id\": \"<lead-persona-uuid>\", \"terminal_command_id\": \"...\"}"
    }]
  }
}

lead_instance_id is the parent for every child the lead delegates to. The lead is already running on the platform; the rest of this recipe is what the lead itself would call.

Step 3 — Spawn a worker child

agent

The lead delegates an inventory query to a worker instance under a worker persona. parent_instance_id wires the child into the hierarchy — the child inherits the mission, the budget envelope, and the lineage that the audit chain tracks. mcp_endpoint=/api/mcp/sse gives the worker the full toolset; coordinators run on the restricted /api/mcp/pjv3/sse/ endpoint by default.

Request:

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "pj_v3_spawn_agent",
    "arguments": {
      "persona_id": "<worker-persona-uuid>",
      "spawn_reason": "Worker child for spare-parts inventory queries — needs full toolset.",
      "parent_instance_id": "11111111-2222-3333-4444-555555555555",
      "browser_mode": "terminal",
      "layout": "tab",
      "initial_task": "Query inventory: list items with reorder_threshold > current_stock, joined to last maintenance-task completion. Fold result back to parent when done.",
      "mcp_endpoint": "/api/mcp/sse",
      "allow_overage": false
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"instance_id\": \"66666666-7777-8888-9999-aaaaaaaaaaaa\", \"persona_id\": \"<worker-persona-uuid>\", \"parent_instance_id\": \"11111111-...\", \"browser_mode\": \"terminal\", \"session\": {\"tab_id\": \"...\", \"layout\": \"tab\"}, \"authority_level\": \"worker\"}"
    }]
  }
}

Step 4 — Enforce the budget

agent

Budgets are enforced inside the dispatcher — every paid tool call checks within_budget before executing — but you can introspect at any time. Two calls work here: pj_check_budget for the headline flag and pj_get_instance_cost for the token-level breakdown (including cache-read tokens, which dominate cost on long-running agents).

Budget check:

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "pj_check_budget",
    "arguments": {
      "instance_id": "66666666-7777-8888-9999-aaaaaaaaaaaa"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"within_budget\": true, \"budget_usd\": 5.00, \"spent_usd\": 0.42, \"remaining_usd\": 4.58, \"percent_used\": 8.4}"
    }]
  }
}

Token-level cost detail:

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "pj_get_instance_cost",
    "arguments": {
      "instance_id": "66666666-7777-8888-9999-aaaaaaaaaaaa"
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"instance_id\": \"66666666-...\", \"input_tokens\": 38420, \"output_tokens\": 4112, \"cache_read_tokens\": 162400, \"cost_usd\": 0.42, \"over_budget\": false}"
    }]
  }
}

over_budget on the instance and within_budget on the mission are both wired to the same ceiling. Set allow_overage: true on spawn or on a tool call to knowingly cross the line; the audit entry records the override.

Step 5 — Wait for the child to fold back

agent

Long-running agents don't return a blocking HTTP response — they return a mission ID and run asynchronously. pj_wait_for_messages blocks the caller until a message arrives in the lead's inbox, or until timeout_seconds elapses. Far cheaper than polling.

Request:

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "pj_wait_for_messages",
    "arguments": {
      "instance_id": "11111111-2222-3333-4444-555555555555",
      "timeout_seconds": 300,
      "mark_read": true,
      "limit": 10
    }
  }
}

Response when the child folds back:

{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"messages\": [{\"id\": \"...\", \"from_instance_id\": \"66666666-...\", \"message_semantic\": \"fold_completion\", \"body\": \"Found 12 items below reorder threshold; 4 with no service history in 18+ months.\", \"artifacts\": [{\"type\": \"csv\", \"name\": \"action_items.csv\"}], \"received_at\": \"2026-05-16T18:55:12Z\"}], \"timed_out\": false}"
    }]
  }
}

message_semantic: "fold_completion" is the canonical fold marker. Other values you'll see: delegation (parent to child), escalation (child to parent, authority insufficient), approval_request (child to human, policy gate).

Step 6 — Complete and fold up

agent

From the child's point of view: when its work is done, it calls pj_v3_complete_task with a result summary and any artifacts. The dispatcher validates that the child's own descendants have all folded (otherwise it would orphan their results), synthesizes the result payload, and emits an AgentMessage with message_semantic="fold_completion" to the parent — the message the lead is blocked on in Step 5.

Request (called by the child):

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "pj_v3_complete_task",
    "arguments": {
      "persona_id": "<worker-persona-uuid>",
      "result_summary": "Spare-parts audit complete: 12 items below reorder threshold, 4 overdue for service. Report and CSV attached.",
      "artifacts": [
        { "type": "report", "name": "spare_parts_audit_2026-05-16.md", "content": "..." },
        { "type": "csv", "name": "action_items.csv", "content": "..." }
      ]
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"folded\": true, \"to_instance_id\": \"11111111-...\", \"message_id\": \"...\", \"message_semantic\": \"fold_completion\", \"child_count\": 0}"
    }]
  }
}

Use force: true only when known-retired children will never fold back. Every override is captured in the audit chain.

Step 7 — Verify the audit chain

agent

Every governance-relevant event — mission created, mission launched, agent spawned, budget refused, task completed, authority overridden — is written to a hash-chained audit ledger. pj_get_audit_entries returns the rows in chain order; pass verify_chain: true to have the platform self-verify the chain before returning.

Request:

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tools/call",
  "params": {
    "name": "pj_get_audit_entries",
    "arguments": {
      "mission_id": "a7b3c2d1-1234-4567-89ab-cdef01234567",
      "limit": 20,
      "verify_chain": true
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"chain_verified\": true, \"entries\": [{\"created_at\": \"2026-05-16T18:42:00Z\", \"event\": \"mission.created\", \"prev_hash\": null, \"hash\": \"sha256:01a2...\"}, {\"created_at\": \"2026-05-16T18:42:14Z\", \"event\": \"mission.launched\", \"prev_hash\": \"sha256:01a2...\", \"hash\": \"sha256:9b3f...\"}, {\"created_at\": \"2026-05-16T18:43:02Z\", \"event\": \"agent.spawned\", \"prev_hash\": \"sha256:9b3f...\", \"hash\": \"sha256:7c1e...\"}, {\"created_at\": \"2026-05-16T18:55:18Z\", \"event\": \"task.completed\", \"prev_hash\": \"sha256:7c1e...\", \"hash\": \"sha256:d402...\"}]}"
    }]
  }
}

Each row carries prev_hash (the previous row's hash) and hash (a SHA-256 of this row plus prev_hash). Tampering with any row invalidates every subsequent row. The same auditors who already accept your existing logs accept this one — it's a flat hash-linked list, not a proprietary format.

Verify the governance gate (RBAC + agent authority)

The same permission gate every tool call passes through is enforced two ways you can observe directly: at the REST surface (a non-admin key is refused the audit trail) and in the agent hierarchy (a low-authority child cannot act beyond its level — it must escalate, and the escalation lands in the audit chain). This section reproduces both.

Gate A — the REST audit endpoint is permission-scoped

REST

Reading the audit trail requires the compliance.view_audit_logs or admin.view_audit_logs permission. A key whose user has neither is refused; an admin key (or a user holding the Compliance Officer role) is allowed. Mint two keys for the same tenant — one for a plain user, one for an admin — and call /api/audit/ with each.

Non-admin key:

# Non-admin key — audit trail is gated
curl -i -H "Authorization: Bearer ck_NON_ADMIN_KEY" \
  https://{tenant}.cognethics.com/api/audit/?limit=10

HTTP/1.1 403 Forbidden
{"detail": "You do not have permission to view audit logs."}

Admin key:

# Admin key (or a Compliance Officer role) — 200 OK
curl -i -H "Authorization: Bearer ck_ADMIN_KEY" \
  https://{tenant}.cognethics.com/api/audit/?limit=10

HTTP/1.1 200 OK
{"count": 184, "results": [ /* audit rows in chain order */ ]}

Grant the non-admin user the Compliance Officer role and the same call flips to 200 — the gate is role-based, not key-based, so your ck_ scopes don't widen it.

Gate B — agent authority is bounded and audited

agent

A child persona can only ever restrict what its parent allows — never expand it. Create a low-authority_level Specialist under your lead persona, spawn it, set its budget, then hand it a task that exceeds its authority. The platform refuses the autonomous action and records an escalation back to the parent in the same hash-chained ledger from Step 7.

Create the restricted child persona with pj_create_persona. A lower authority_level than the parent, and requires_approval_for naming the actions it cannot take alone:

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "pj_create_persona",
    "arguments": {
      "name": "Specialist-Governance-Demo",
      "role": "Specialist",
      "authority_level": 4,
      "parent_persona_id": "<lead-persona-uuid>",
      "allows_spawn": false,
      "requires_approval_for": ["code_merge", "financial"]
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 10,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"persona_id\": \"<specialist-persona-uuid>\", \"name\": \"Specialist-Governance-Demo\", \"authority_level\": 4, \"parent_persona_id\": \"<lead-persona-uuid>\", \"requires_approval_for\": [\"code_merge\", \"financial\"]}"
    }]
  }
}

Spawn it with pj_v3_spawn_agent (Step 3), passing the new persona_id and the lead's parent_instance_id, then cap its spend with pj_set_budget:

{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "tools/call",
  "params": {
    "name": "pj_set_budget",
    "arguments": {
      "instance_id": "<specialist-instance-uuid>",
      "budget_usd": 2.00
    }
  }
}

Delegate a task it cannot do alone. When the lead delegates code_merge to the Specialist (via pj_v3_delegate_task), the policy engine evaluates the child's authority, refuses the autonomous action, and the child emits a fold_escalation back to the parent (pj_v3_escalate_task). Nothing is lost — the request is recorded for the higher-authority parent to approve.

Read the governance audit chain scoped to the child persona with pj_get_audit_entries — the creation, spawn, budget, delegation, and escalation all appear in chain order:

{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "pj_get_audit_entries",
    "arguments": {
      "persona_id": "<specialist-persona-uuid>",
      "limit": 20,
      "verify_chain": true
    }
  }
}
{
  "jsonrpc": "2.0",
  "id": 12,
  "result": {
    "content": [{
      "type": "text",
      "text": "{\"chain_verified\": true, \"entries\": [{\"event\": \"persona.created\", \"persona\": \"Specialist-Governance-Demo\", \"authority_level\": 4}, {\"event\": \"agent.spawned\", \"persona\": \"Specialist-Governance-Demo\", \"parent\": \"<lead-persona-uuid>\"}, {\"event\": \"budget.set\", \"budget_usd\": 2.00}, {\"event\": \"task.delegated\", \"from\": \"<lead-persona-uuid>\", \"to\": \"Specialist-Governance-Demo\"}, {\"event\": \"task.escalated\", \"from\": \"Specialist-Governance-Demo\", \"to\": \"<lead-persona-uuid>\", \"action\": \"code_merge\", \"reason\": \"authority insufficient\"}]}"
    }]
  }
}

The task.escalated row is the gate working: a low-authority instance asked to act above its level, the platform refused, and the refusal-plus-escalation is in the tamper-evident ledger — not buried in a log nobody reads.

What you've just used

  • pj_create_mission — define scope, budget, max-agents.
  • pj_launch_mission — start the lead instance.
  • pj_v3_spawn_agent — delegate to a child under a persona.
  • pj_check_budget + pj_get_instance_cost — introspect the dispatcher's enforcement of the budget ceiling.
  • pj_wait_for_messages — block on fold-back, no polling.
  • pj_v3_complete_task — fold a child's result up the hierarchy.
  • pj_get_audit_entries — self-verify the tamper-evident ledger.

Where to go next

  • Agents — the conceptual model behind missions, personas, authority, and fold/unfold.
  • Reference: PJ Agent Orchestration System tools — the full catalog of agent-OS primitives, including delegation, escalation, approvals, telemetry, and handoff.
  • MCP Quickstart — wire any MCP-aware client (Claude Desktop, claude.ai, Claude Code) into your tenant.

Back to all recipes.