> ## Documentation Index
> Fetch the complete documentation index at: https://checkfu.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Multiagent threads

> Run a published coordinator with persistent, independently observable agent threads.

A coordinator Agent can start persistent child threads from the exact roster frozen in its published AgentVersion. Every multiagent run is one Session with a primary `SessionThread` and zero or more child `SessionThread` resources. Threads have stable `sthr_…` identities, isolated agent context and event history, and their own status, usage, and statistics.

The published roster is the address book; live authority is still required. The coordinator needs a current `use_agent` PermissionAssignment and an allowing ActionPolicy decision for the selected member. Revocation blocks the next spawn or follow-up without rewriting the immutable roster snapshot, while adding a PermissionAssignment cannot make an unlisted agent addressable.

## Thread resource

`GET /v1/sessions/{session_id}/threads` returns the primary thread first and child threads in spawn order. A thread contains:

* `id`, `type: "session_thread"`, `session_id`, and nullable `parent_thread_id`;
* the exact resolved `agent` snapshot used by that thread;
* `running | idle | rescheduling | terminated` status;
* `created_at`, `updated_at`, and nullable `archived_at` timestamps;
* nullable cumulative `usage` and `stats` projections.

The primary has `parent_thread_id: null`. Every child points to that primary thread—not to another child—because multiagent depth is one. A successful child turn becomes `idle` and remains addressable for a follow-up; it does not disappear when a turn ends.

## Public API

| Operation                                                    | Purpose                                                               |
| ------------------------------------------------------------ | --------------------------------------------------------------------- |
| `GET /v1/sessions/{session_id}/threads`                      | Page the complete primary-first roster                                |
| `GET /v1/sessions/{session_id}/threads/{thread_id}`          | Retrieve one membership-fenced thread                                 |
| `POST /v1/sessions/{session_id}/threads/{thread_id}/archive` | Archive an idle child thread                                          |
| `GET /v1/sessions/{session_id}/threads/{thread_id}/events`   | Page the primary's condensed history or a child's complete history    |
| `GET /v1/sessions/{session_id}/threads/{thread_id}/stream`   | Replay and then tail that thread's events with `Last-Event-ID` resume |

The SDK mirrors this under `client.sessions.threads`: `list(sessionId)`, `retrieve(threadId, { session_id })`, `archive(threadId, { session_id })`, `events.list(threadId, { session_id })`, and `events.stream(threadId, { session_id })`. The CLI exposes the same hierarchy under `checkfu session threads`; `list`, `retrieve`, `archive`, `events list`, and `events stream` all take explicit `--session-id` and thread-scoped commands also take `--thread-id`. `checkfu session follow <session-id> --thread <thread-id>` remains a convenient tailing shortcut.

There is deliberately no customer operation to create a thread or send a message between threads. Those are coordinator actions constrained by the frozen roster and live governance, not a second public mutation authority.

## Asynchronous messaging

A spawn creates one persistent child thread and durably queues its initial message. The primary continues its own turn immediately; it never parks to join children. Spawning the same roster member twice creates two independent threads, while a follow-up names one existing `sthr_…` id and preserves that thread's prior context.

The primary may send only to one of its direct children. A child may report, ask a question, or otherwise send only to the primary. Delivery wakes an idle receiver; when the receiver is already running, messages enter its bounded queue in accepted order. A terminal child failure changes lifecycle state but does not fabricate a result message.

Both sides record durable direction-relative events:

| Event                           | Exact payload direction                                         |
| ------------------------------- | --------------------------------------------------------------- |
| `agent.thread_message_sent`     | `content`, `to_session_thread_id`, optional `to_agent_name`     |
| `agent.thread_message_received` | `content`, `from_session_thread_id`, optional `from_agent_name` |

The optional agent name is present when the other endpoint is a child and
omitted when the other endpoint is the primary. `content` uses the same bounded
text, image, and document blocks—and the same URL/file source-admission
policy—as an ordinary message. A successful private control acknowledgement
means the sender event and the receiver's durable inbox custody are
established; it does not mean the receiver has already completed a turn.

## Child tool waits

When a child needs a Custom-tool result or tool confirmation, its complete wait
stays on the child event resource. The primary Session stream receives the
corresponding `agent.tool_use` with `session_thread_id`, followed by
`session.thread_status_idle` with `stop_reason.type: "requires_action"`; the
stop reason's `event_ids` point to the cross-posted primary event, not to a
private child event id.

Reply through the ordinary primary Session event endpoint. Copy both
`tool_use_id` and `session_thread_id` from the cross-post into
`user.custom_tool_result` or `user.tool_confirmation`. Checkfu resolves the
unique current wait from its durable index and refuses an omitted ambiguous
origin, a wrong thread, a stale generation, or a conflicting replay. An exact
duplicate replays the first event. Agent Questions are ordinary inter-thread
messages and do not use this action-routing protocol.

Allowing a governed Custom tool is the first of two phases: the child records
`run.action_authorized` and remains parked on the same tool call until your app
posts `user.custom_tool_result` with the same thread and tool-use ids. Denying
resumes the child with `policy.denied` guidance and needs no result.

## Resources and credentials

The coordinator Session owns one secret-free resource snapshot for the whole
thread roster. It contains the exact Project, File, and Memory mount
declarations and resolved revisions, plus the eligible Connection identities
selected from the coordinator and all frozen roster members. A child receives that same snapshot;
it does not resolve a private replacement set when it starts or wakes again.

Shared eligibility does not widen an agent. Each thread exposes only the tools
declared by its own frozen AgentVersion, every invocation still passes live
PermissionAssignment and ActionPolicy checks, and provider tokens remain in Checkfu's trusted host
rather than the sandbox, event log, or thread resource. Revoking a Connection
therefore blocks later use without rewriting the Session snapshot.

Every compatible coordinator Session also owns one physical
`SessionSandbox`. The first active thread materializes its mounts and runs
`post_materialize`; later threads open isolated harness processes in that same
sandbox and see file changes immediately. Agent context, process streams, tools,
and MCP configuration remain per-thread even though the filesystem is shared.

One fenced Runner generation owns the complete SessionSandbox. When every active
thread process is idle, one winner runs `pre_writeback`, captures shared
Project/Memory changes, and seals one aggregate checkpoint. Owner loss moves
all active threads through `rescheduling` and restores that checkpoint as one
unit; Checkfu never falls back to one sandbox per thread.

`sandbox_expires_at` is fixed when the SessionSandbox is first materialized:
the earlier of Session sandbox-state retention or 30 days. Activity and
checkpoints never move it. A turn after expiry starts a fresh filesystem—so
scratch files and installed packages are absent—while retained thread
conversations and admitted resource truth remain.

## Lifecycle and interruption

The primary Session stream carries a condensed view of child lifecycle changes:

| Event                               | Meaning                                                             |
| ----------------------------------- | ------------------------------------------------------------------- |
| `session.thread_created`            | A child received its stable thread identity                         |
| `session.thread_status_running`     | The child began or resumed execution                                |
| `session.thread_status_idle`        | The child yielded and remains available; `stop_reason` explains why |
| `session.thread_status_rescheduled` | A retry is pending without creating another thread                  |
| `session.thread_status_terminated`  | The child was archived or failed terminally                         |

The child's event endpoint contains its complete history. Creation is recorded only on the primary view; child status changes are stored on the child and cross-posted as distinct correlated events to the primary. The primary itself uses ordinary `session.status_*` events.

`user.interrupt` accepts an optional `session_thread_id`. Supplying it targets one thread; omitting it interrupts every non-archived thread, including the primary. Interrupting an idle thread is a no-op. A running child must be interrupted before it can be archived.

Only an idle child can be archived. A child parked on an ActionApproval or Custom-tool action must first be targeted by `user.interrupt`; its public idle projection does not make the underlying action wait disposable. Repeating a successful archive is idempotent, the terminated resource remains readable, and archive frees one capacity slot. The primary cannot be archived while its Session exists. A Session admits at most 25 capacity-holding threads including the primary; idle, unarchived threads still hold capacity.

## Streams, previews, and webhooks

Thread streams use the same durable envelope and cursor rules as the [Session event stream](/reference/events). `event_deltas=agent.message|agent.thinking` opts into best-effort live preview frames. Previews are never persisted, never advance the cursor, and stay on the originating thread; a child's preview is not copied into the primary stream.

Webhook delivery exposes the child-only lifecycle names `session.thread_created`, `session.thread_idled`, and `session.thread_terminated`. The signed thin envelope includes `data.session_thread_id` beside the Session and Workspace identities; fetch the resource or event for complete details. A successful child turn emits `session.thread_idled`, while the primary continues to use Session-status webhooks.

<Note>
  A harness may also run its own internal subagents. Those remain observational
  `agent.subagent_started` and `agent.subagent_completed` events on one thread.
  They are not governed SessionThreads and never enter the thread roster.
</Note>

## Current execution boundary

Thread identity, independent persistent Runs and contexts, asynchronous
messaging, child action routing, the complete public lifecycle, one Session
event log, one resource/credential-eligibility snapshot, and one physical
SessionSandbox are authoritative now. There is no child Session, private
execution locator, or per-thread sandbox fallback. A coordinator whose pinned
harness cannot host isolated processes in that shared environment is rejected
with `runtime.harness_incompatible`.

## Next steps

<CardGroup cols={2}>
  <Card title="Multiagent coordinators" icon="users" href="/concepts/multiagent-coordinators">
    Publish the immutable roster that makes child agents addressable.
  </Card>

  <Card title="Events reference" icon="list-timeline" href="/reference/events">
    Read the common durable event envelope and resume rules.
  </Card>
</CardGroup>
