Subagents
Subagents let agents delegate work to other agents through tool calling. The calling agent's LLM decides when and what to delegate, and each sub-task runs on its own isolated stream with independent history.
Why Use Subagents#
Breaking work across multiple agents gives you:
- Separation of concerns — each agent has a focused prompt, tools, and config tuned for its role.
- Independent context — sub-tasks run on fresh streams, so history doesn't grow unboundedly.
- LLM-driven orchestration — the calling agent decides when to delegate, what context to pass, and how to use the result.
Note
When the LLM returns multiple tool calls in a single response, the framework dispatches them concurrently. Each concurrent sub-task gets its own copy of variables, so they don't interfere with each other.
Tip
For lightweight self-delegation where the parent doesn't need a named delegate, opt in to the auto-injected run_subtask / run_subtasks tools by passing tasks=TaskConfig(...) — see tasks= in The Agent Harness. Use Agent.as_tool() (below) when you want a distinct, purpose-named tool exposed to the LLM.
Subagents API#
Use Agent.as_tool() to make one agent available as a tool for another.
The coordinator's LLM sees two tools — task_researcher and task_writer — and calls them as needed. Each call spawns the target agent on a fresh stream, runs it to completion, and returns the result.
The calling agent's LLM sees a tool named task_{agent.name} with objective (required) and context (optional) parameters.
The context tool parameter is how the calling LLM shares relevant information with the sub-task:
as_tool() accepts these parameters:
| Parameter | Type | Description |
|---|---|---|
description | str | Tool description shown to the LLM (required) |
name | str | None | Override the default task_{agent.name} tool name |
stream | StreamFactory | None | Factory to create custom streams for sub-tasks (see Sub-Task Streams) |
middleware | Iterable[ToolMiddleware] | Tool middleware applied to the delegate tool (e.g., approval_required) |
You can also use subagent_tool() directly for more control:
Background Subagents#
as_tool() and subagent_tool() are blocking: the tool call doesn't return until the sub-task finishes, so the calling LLM waits for the result before doing anything else. background_agent_tool() is fire-and-forget — it starts the sub-task, returns a task id immediately, and lets the parent LLM keep working in the same turn. The result is delivered later as a follow-up message to the parent.
The calling LLM sees a tool named background_task_{agent.name} with the same objective (required) and context (optional) parameters as as_tool(). Calling it returns "Background task started: {task_id}" right away, so the LLM can issue other tool calls or continue reasoning while the sub-task runs.
How it behaves:
- The sub-task runs concurrently via
run_taskon its own stream (a freshMemoryStreamby default, or one built by thestreamfactory). - The parent
Agent.askloop keeps running and will not return until the background task finishes. Once it does, its result is pushed back into the parent's inbox as a follow-up turn (viacontext.enqueue), so the parent LLM can react to it. - On success the follow-up message reports the task result; on failure it reports the error — the exception does not propagate to the parent.
background_agent_tool() accepts these parameters:
| Parameter | Type | Description |
|---|---|---|
agent | Agent | The agent to run as a background sub-task (positional) |
description | str | Tool description shown to the LLM (required) |
name | str | None | Override the default background_task_{agent.name} tool name |
stream | StreamFactory | None | Factory to create custom streams for sub-tasks (see Sub-Task Streams) |
middleware | Iterable[ToolMiddleware] | Tool middleware applied to the delegate tool (e.g., approval_required) |
Note
Background sub-tasks are for work the parent can fire off and revisit later within the same turn — the ask call still awaits their completion before returning. They are not detached jobs that outlive the turn.
Self-Delegation#
An agent can delegate to itself to break complex work into independent sub-tasks. Each sub-task runs as a fresh copy of the agent with its own stream and history.
The analyst's LLM may call sub_task multiple times — one per aspect — then synthesise the results.
Dynamic Agents#
dynamic_agent() lets the calling LLM construct an ephemeral agent at runtime — picking a name, system prompt, and a subset of available tools per objective — instead of pre-defining each delegate as a named as_tool(). Use it when the orchestrator should compose a focused worker on demand for each sub-task.
The orchestrator's LLM sees one tool, create_and_run_agent(spec, objective). Each call spawns an ephemeral child Agent on a fresh stream, runs the objective via run_task, and returns the reply string.
dynamic_agent() accepts these parameters:
| Parameter | Type | Description |
|---|---|---|
available_tools | Iterable[Tool | Callable[..., Any]] | Pool of tools the spawned child may pick from by name |
config | ModelConfig | Model configuration used for every spawned child |
middleware | Iterable[ToolMiddleware] | Tool middleware applied to create_and_run_agent |
The AgentSpec the LLM constructs#
The LLM builds an AgentSpec on every call. It is JSON-serializable and captures the declarative parts of the child:
| Field | Type | Purpose |
|---|---|---|
name | str | Display name of the spawned child |
prompt | list[str] | System prompt for the child |
tool_names | list[str] | Subset of available_tools names to give the child |
response_schema | ResponseSchemaSpec | None | Optional structured output schema |
How the LLM discovers available tool names#
The pool's names and descriptions are rendered into the create_and_run_agent tool description automatically when the factory is built. The calling agent's system prompt does not need to enumerate them — the LLM discovers the valid names from the tool schema.
Tip
If the LLM picks a name not in the pool, the framework returns Error: unknown tools [...]. Available: [...] as a recoverable string. The LLM reads the hint and retries with a corrected spec — no exception propagates to the caller. The auto-rendered menu prevents this in the common case.
Warning
Spawned children are themselves constructed without dynamic_agent, so they cannot recursively spawn further dynamic agents. Recursion is structurally impossible — no depth limit needed.
Sub-Task Streams#
Default Behavior#
By default, each sub-task creates a fresh MemoryStream. The sub-task's history is isolated — it doesn't carry over between invocations.
It means that subagent has no information about previous calls or results. It just sees the current call and the context.
| What | Behavior | Why |
|---|---|---|
| Dependencies | Copied | Isolated — child mutations don't affect parent |
| Variables | Copied; synced back on success | Concurrent-safe — user variable mutations propagate back |
| History | Fresh stream | Clean context — the LLM passes relevant info via context parameter |
| Depth counter | Incremented in child; excluded from sync-back | Internal bookkeeping — never leaks to parent |
| Agent prompt, tools, config | Inherited | The sub-agent brings its own capabilities |
Persistent Stream#
persistent_stream() gives the same agent a consistent stream across multiple invocations within a context. The sub-task's history accumulates across calls rather than starting fresh each time:
It stores the stream ID in context.dependencies keyed by f"ag:{agent.name}:stream" and reuses the parent stream's storage backend. This is useful when the sub-agent benefits from seeing its own prior work — for example, a researcher that should avoid repeating searches.
Custom Factory#
For full control, pass any callable matching StreamFactory = Callable[[Agent, Context], Stream]: