Subagent¶
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
When satisfied, SubagentsHook (priority=80) is registered and exposes:
agent_spawn/agent_send/agent_listtask_output/task_cancel/task_list
On every PreReasoningEvent turn, SubagentsHook injects into SYSTEM:
Subagent usage rules
List of currently available
agent_idsSummary of async tasks in the current session (up to 10)
Declaration Sources¶
buildSubagentEntries(...) merges four sources:
Built-in
general-purposeProgrammatic declarations:
builder.subagent(SubagentDeclaration)File declarations:
workspace/subagents/*.md(loaded non-recursively byAgentSpecLoader)Custom factories:
builder.subagentFactory(name, factory)
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
Mutual exclusion constraints (validated on build()):
urlcannot coexist withworkspaceor non-emptyinlineAgentsBodyworkspaceand non-emptyinlineAgentsBodycannot coexist
Runtime Workspace Five-Row Decision Table¶
WorkspaceMode determines the runtime workspace root:
Case |
sysPrompt base source |
Runtime workspace |
|---|---|---|
Built-in |
No additional base (only Subagent Context appended; |
|
|
|
|
|
|
|
No |
|
|
No |
|
|
Notes:
toolsis an allowlist for inherited tools: it only filters the parent toolkit, and does not affect tools the subagent auto-registers locallyMultiple 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.
---
description: Code review expert
workspace:
mode: isolated # isolated | shared, defaults to isolated
path: ./defs/reviewer # optional; relative to mainWorkspace or absolute
model: openai:gpt-4o-mini # optional
maxIters: 8 # optional, default 10
tools: [read_file, grep_files] # optional
---
You are a subagent focused on code review.
Parsing rules (AgentSpecLoader):
Required:
descriptionOnly 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 ignoredMarkdown declarations currently do not parse
url/headers(remote declarations should use the programmatic API)
Programmatic Configuration¶
HarnessAgent.builder()
.name("orchestrator")
.model(model)
.workspace(workspace)
.subagent(SubagentDeclaration.builder()
.name("reviewer")
.description("Code review expert")
.workspace(Path.of("./defs/reviewer"))
.workspaceMode(WorkspaceMode.ISOLATED)
.model("qwen3-max")
.maxIters(8)
.tools(List.of("read_file", "grep_files"))
.build())
.subagent(SubagentDeclaration.builder()
.name("remote-researcher")
.description("Remote research subagent")
.url("http://agent-task-server:8080")
.headers(Map.of("Authorization", "Bearer xxx"))
.build())
.build();
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 registerSubagentsHookAgentSpawnToolalso has a dynamic depth cap:MAX_SPAWN_DEPTH = 3
RuntimeContext Propagation¶
When agent_spawn / agent_send invokes a subagent:
The sub-session
session_idis a new value (sub-<uuid>)userIdis propagated from the parentRuntimeContextto the childRuntimeContext
This maintains consistent USER-dimension isolation keys (e.g., scenarios where namespace/sandbox isolation depends on userId).
Tool Reference and Key Parameters¶
Tool |
Purpose |
Key Parameters |
|---|---|---|
|
Create a subagent; optionally execute the first task sync or async |
|
|
Send a follow-up message to an existing subagent |
|
|
List currently active subagents |
none |
|
Query / wait for background task result |
|
|
Cancel a task |
|
|
List tasks in the current session |
|
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 uses WorkspaceTaskRepository (unless overridden with taskRepository(...)).
Lifecycle (simplified):
putTask(...)writesTaskRecord(PENDING)to workspaceSubmits local execution future (local or remote)
Updates to
RUNNINGduring executionWrites
COMPLETED / FAILED / CANCELLEDon completion
Storage layering:
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 abortsThe orphan sweeper marks local tasks with no heartbeat for a long time as
FAILED(remote transport tasks are not subject to this check)
Relationship to filesystem mode:
Mode |
|
|---|---|
|
Routed to shared remote storage, visible to multiple nodes |
|
Goes through sandbox filesystem and sandbox state persistence |
|
Visible on local machine only |
Remote Subagent Behavior¶
When a declaration has url(...) configured:
The factory returns a
RemoteSubagentStub(placeholder, performs no local reasoning)Actual execution is delegated to a remote task HTTP service via
TaskRunSpec.RemoteTaskRunSpec+AgentProtocolTaskClientSupports 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 delegateSubagent
maxItersis typically set smaller than the parent agent to avoid sub-threads consuming excessive tokensAfter session compaction or recovery, first use
task_list()to restore full task state before making single-task queries