Purpose
Subagents let the parent agent delegate tasks that are “independently handled, context-heavy, or parallelizable”, preventing the main thread from bloating.Each subagent is a temporary
HarnessAgent (or remote stub) instance with its own sub-session; results are returned via tool output.
When Subagents Are Enabled
HarnessAgent.build() only loads subagent capability when all of the following are true:
- The current agent is not a leaf subagent
disableSubagents()has not been calledmodelis configured
SubagentsHook (priority=80) is registered and exposes:
agent_spawn/agent_send/agent_listtask_output/task_cancel/task_list
PreReasoningEvent turn, SubagentsHook injects into SYSTEM:
- Subagent usage rules
- List of currently available
agent_ids - Summary of async tasks in the current session (up to 10)
Declaration Sources
buildSubagentEntries(...) merges four sources:
- Built-in
general-purpose - Programmatic declarations:
builder.subagent(SubagentDeclaration) - File declarations:
workspace/subagents/*.md(loaded non-recursively byAgentSpecLoader) - Custom factories:
builder.subagentFactory(name, factory)orbuilder.subagentFactory(name, description, factory)to give the orchestrator a meaningful description instead of the bare name (falls back to the name when omitted or blank)
Declaration Model (SubagentDeclaration)
SubagentDeclaration supports 3 mutually exclusive source modes:
- Definition workspace mode
workspace(path)points to a definition directory (typically containingAGENTS.md)
- Inline mode
inlineAgentsBody(...)is used directly as the system prompt base
- Remote HTTP mode
url(...)+ optionalheaders(...), execution delegated remotely via the task protocol
build()):
urlcannot coexist withworkspaceor non-emptyinlineAgentsBodyworkspaceand non-emptyinlineAgentsBodycannot coexist
Runtime Workspace Five-Row Decision Table
WorkspaceMode determines the runtime workspace root:
Notes:
toolsis an allowlist for inherited tools: it only filters the parent toolkit, and does not affect tools the subagent auto-registers locally- Multiple declarations can reuse the same definition workspace
workspace.pathrelative paths are resolved viamainWorkspace.resolve(...).normalize()
Declaration Files (workspace/subagents/<id>.md)
The filename (without .md) is the agent_id; name is not read from front matter.
AgentSpecLoader):
- Required:
description - Only scans the first level of
subagents/directory for.mdfiles (non-recursive) - If
workspace.pathis set and body is non-empty: logs a warning; body is ignored - Markdown declarations currently do not parse
url/headers(remote declarations should use the programmatic API)
Programmatic Configuration
Built-in general-purpose
The built-in general-purpose requires no declaration file and is always included in the entry list.Its goal is to “mirror the parent agent’s capabilities”:
- Shares the main workspace (
SHAREDsemantics) - Inherits and mirrors the parent agent’s:
- toolkit (parent tools)
- hooks
- execution config
- compaction / toolResultEviction
- additional context files / maxContextTokens
- various disable flags
- Always a leaf subagent (cannot further spawn subagents)
Recursion Prevention and Depth Safety
Two safety mechanisms:- All subagents generated via declarations or built-in are given
asLeafSubagent()— leaf agents do not registerSubagentsHook AgentSpawnToolalso has a dynamic depth cap:MAX_SPAWN_DEPTH = 3
RuntimeContext Propagation
Whenagent_spawn / agent_send invokes a subagent:
- The sub-session
session_idis a new value (sub-<uuid>) userIdis propagated from the parentRuntimeContextto the childRuntimeContext
userId).
Tool Reference and Key Parameters
Notes:
- The
agent_keyforagent_sendmust be the completeagent_key: ...from theagent_spawnreturn value (notagent_id/session_id/task_id) - Do not immediately poll after creating an async task; prefer returning to the user first, then using
task_output(block=false)ortask_listto check latest status
Async Task Lifecycle and Storage
By default, the parent agent usesWorkspaceTaskRepository (unless overridden with taskRepository(...)).
Lifecycle (simplified):
putTask(...)writesTaskRecord(PENDING)to workspace- Submits local execution future (local or remote)
- Updates to
RUNNINGduring execution - Writes
COMPLETED / FAILED / CANCELLEDon completion
- In-memory layer:
localTasks(node-local acceleration handle, lost on restart) - Persistent layer:
agents/<parentAgentId>/tasks/<sessionId>.json(source of truth)
Distributed Semantics
- Task execution is sticky to the creating node, but any node can read state through the workspace
task_output(block=true)degrades gracefully in cross-node scenarios and does not block indefinitelytask_cancelpersistscancelRequested=true; the executing node polls this flag and aborts- The orphan sweeper marks local tasks with no heartbeat for a long time as
FAILED(remote transport tasks are not subject to this check)
Remote Subagent Behavior
When a declaration hasurl(...) configured:
- The factory returns a
RemoteSubagentStub(placeholder, performs no local reasoning) - Actual execution is delegated to a remote task HTTP service via
TaskRunSpec.RemoteTaskRunSpec+AgentProtocolTaskClient - Supports both synchronous (
timeout_seconds > 0) and asynchronous (timeout_seconds = 0) modes
Practical Advice
- Write
descriptionclearly — “when to use / output format / prohibited actions” — this is the key signal the parent model uses to decide whether to delegate - Subagent
maxItersis typically set smaller than the parent agent to avoid sub-threads consuming excessive tokens - After session compaction or recovery, first use
task_list()to restore full task state before making single-task queries