Skip to main content
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

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: 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: 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. 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.
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.

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

Multiagent coordinators

Publish the immutable roster that makes child agents addressable.

Events reference

Read the common durable event envelope and resume rules.