Skip to main content

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 called
  • model is configured
When satisfied, SubagentsHook (priority=80) is registered and exposes:
  • agent_spawn / agent_send / agent_list
  • task_output / task_cancel / task_list
On every 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:
  1. Built-in general-purpose
  2. Programmatic declarations: builder.subagent(SubagentDeclaration)
  3. File declarations: workspace/subagents/*.md (loaded non-recursively by AgentSpecLoader)
  4. Custom factories: builder.subagentFactory(name, factory) or builder.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:
  1. Definition workspace mode
    • workspace(path) points to a definition directory (typically containing AGENTS.md)
  2. Inline mode
    • inlineAgentsBody(...) is used directly as the system prompt base
  3. Remote HTTP mode
    • url(...) + optional headers(...), execution delegated remotely via the task protocol
Mutual exclusion constraints (validated on build()):
  • url cannot coexist with workspace or non-empty inlineAgentsBody
  • workspace and non-empty inlineAgentsBody cannot coexist

Runtime Workspace Five-Row Decision Table

WorkspaceMode determines the runtime workspace root: Notes:
  • tools is 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.path relative paths are resolved via mainWorkspace.resolve(...).normalize()

Declaration Files (workspace/subagents/<id>.md)

The filename (without .md) is the agent_id; name is not read from front matter.
Parsing rules (AgentSpecLoader):
  • Required: description
  • Only scans the first level of subagents/ directory for .md files (non-recursive)
  • If workspace.path is 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 (SHARED semantics)
  • 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:
  1. All subagents generated via declarations or built-in are given asLeafSubagent() — leaf agents do not register SubagentsHook
  2. AgentSpawnTool also has a dynamic depth cap: MAX_SPAWN_DEPTH = 3

RuntimeContext Propagation

When agent_spawn / agent_send invokes a subagent:
  • The sub-session session_id is a new value (sub-<uuid>)
  • userId is propagated from the parent RuntimeContext to the child RuntimeContext
This maintains consistent USER-dimension isolation keys (e.g., scenarios where namespace/sandbox isolation depends on userId).

Tool Reference and Key Parameters

Notes:
  • The agent_key for agent_send must be the complete agent_key: ... from the agent_spawn return value (not agent_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) or task_list to check latest status

Async Task Lifecycle and Storage

By default, the parent agent uses WorkspaceTaskRepository (unless overridden with taskRepository(...)). Lifecycle (simplified):
  1. putTask(...) writes TaskRecord(PENDING) to workspace
  2. Submits local execution future (local or remote)
  3. Updates to RUNNING during execution
  4. Writes COMPLETED / FAILED / CANCELLED on 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 indefinitely
  • task_cancel persists cancelRequested=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)
Relationship to filesystem mode:

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 + AgentProtocolTaskClient
  • Supports both synchronous (timeout_seconds > 0) and asynchronous (timeout_seconds = 0) modes

Practical Advice

  1. Write description clearly — “when to use / output format / prohibited actions” — this is the key signal the parent model uses to decide whether to delegate
  2. Subagent maxIters is typically set smaller than the parent agent to avoid sub-threads consuming excessive tokens
  3. After session compaction or recovery, first use task_list() to restore full task state before making single-task queries