Complex tasks often require delegated subtasks — computing results, searching codebases, auditing for security issues. Doing everything in the parent agent's own context causes several problems:
- Context pollution: exploratory searches, tool outputs, and intermediate steps fill the context window, crowding out what matters.
- Tool sprawl: the parent carries every tool all the time, even though most subtasks only need a subset.
- No role isolation: the parent has one system prompt; it cannot adopt a different persona or constraints per subtask.
- No outside perspective: an agent reviewing its own work is inherently biased — it's unlikely to spot its own mistakes. A fresh context acts as a second pair of eyes, auditing code, challenging a design, or verifying a claim without the parent's assumptions and reasoning shortcuts.
A short-lived sub-agent is a natural fit for these problems: a fresh context per delegation, only the tools it needs, a dedicated system prompt. It runs, returns its result, and is destroyed — keeping the parent conversation clean and focused.
Spawned Sub-Agents give the parent agent two tools for creating short-lived sub-agents at run time:
-
SpawnSubAgentTool— choose from a pre-defined catalog of standardized specialists. Instruction, tool set, and model are locked by the archetype at construction time.Use this when you have a fixed set of expert roles (security auditor, code explorer, planner) and want the LLM to pick the right one per task. The parent LLM selects via
subagent_typeand writes a task-specificprompt, but cannot alter the sub-agent's instruction or tools. -
DynamicSubAgentTool— the LLM invents the specialist on the fly, writing the instruction at call time. No pre-registration needed.Use this when you cannot predict all the specialist types you'll need ahead of time. Every call can define a different role — the LLM decides what expertise, constraints, and tool subset each task requires.
The difference is who defines the role: the developer (Spawn) or the LLM (Dynamic).
The framework already offers several ways to compose agents (see Multi Agents). Spawned Sub-Agents solve a different problem:
| Mechanism | Agents involved | Who decides when to invoke | Context | Typical use |
|---|---|---|---|---|
| Chain / Parallel / Cycle Agent | Pre-built fixed agent instances | Deterministic orchestration — run in list order / in parallel / in a loop, regardless of input | Each agent independent | Fixed multi-step workflows |
| Sub Agents (transfer) | Pre-registered agents | Parent transfers control at runtime; the sub-agent then takes over the conversation | Shared session | Hand off the whole conversation to a better-suited agent |
| AgentTool | Wraps an existing agent instance as a tool | Parent LLM calls it on demand | Shares/syncs state & artifacts back to parent | Reuse a specific, already-built agent |
| Spawned Sub-Agents | Created on the fly per call, destroyed after | Parent LLM calls it on demand | Strictly isolated: fresh ephemeral session, history/state not shared by default | Delegate a one-off subtask while keeping the parent context clean |
In one line: Chain/Parallel/Cycle deterministically orchestrate a fixed set of agents; transfer hands the conversation off; AgentTool reuses one existing agent as a tool; while Spawned Sub-Agents create an isolated, short-lived sub-agent on the spot for a single task and discard it afterward — the emphasis is on on-demand runtime creation and context isolation, not reusing an existing agent or transferring control.
from trpc_agent_sdk.agents import LlmAgent
from trpc_agent_sdk.tools import SpawnSubAgentTool, DynamicSubAgentTool
# Spawn: pick from a catalog of pre-defined specialists
agent_with_spawn = LlmAgent(
name="orchestrator",
tools=[SpawnSubAgentTool()], # built-in `default` archetype
)
# Dynamic: LLM writes the specialist's role at call time
agent_with_dynamic = LlmAgent(
name="orchestrator",
tools=[DynamicSubAgentTool()], # sub-agent inherits all parent tools
)SpawnSubAgentTool |
DynamicSubAgentTool |
|
|---|---|---|
| Pattern | Pick from a pre-defined catalog | LLM invents role at call time |
| Who defines the role | Developer, two modes: ① SubAgentArchetype in code② Markdown file (YAML frontmatter + body) |
LLM (via instruction parameter) |
| Best for | Standardized, repeatable specialists | Roles you can't pre-register |
| Role flexibility | Locked — only prompt varies |
Full — every call can be different |
| Tool surface | Locked by archetype | Inherits parent tools; LLM can narrow via tools |
Dispatches tasks to pre-registered archetypes. The parent LLM picks the right specialist via subagent_type; its instruction and tools are fixed.
class SpawnSubAgentTool(BaseTool):
def __init__(
self,
agents: list[SubAgentArchetype] | None = None,
agent_paths: list[str | os.PathLike] | None = None,
tool_mapping: dict[str, Any] | None = None,
with_default: bool = True,
agent_config: SubAgentConfig | None = None,
skip_summarization: bool = False,
filters_name: list[str] | None = None,
filters: list[BaseFilter] | None = None,
) -> None: ...| parameter | meaning |
|---|---|
agents |
Additional archetypes to register. |
agent_paths |
Directories of *.md files to load archetypes from disk. |
tool_mapping |
Custom tool name → tool class mapping for resolving MD frontmatter. |
with_default |
Whether to register the built-in default archetype. Default True. |
agent_config |
SubAgentConfig applied to every spawned sub-agent. |
skip_summarization |
When True, skip the parent's summarization turn after the sub-agent returns. |
Three ways to configure:
# Zero config — only the built-in `default` archetype
SpawnSubAgentTool()
# Code-defined archetypes
SpawnSubAgentTool(agents=[security_auditor, EXPLORE_AGENT, PLAN_AGENT])
# Load from Markdown files
SpawnSubAgentTool(agent_paths=[".trpc_agents/"])A frozen template that describes one kind of sub-agent the parent is allowed to spawn. It locks down the dangerous knobs (instruction, tools, model) so prompt-injected calls cannot reshape the sub-agent.
@dataclass(frozen=True)
class SubAgentArchetype:
name: str # registry key + the value LLM passes as `subagent_type`
description: str # what the LLM reads to pick this archetype
instruction: str | InstructionProvider
tools: tuple | None = None # None = inherit all parent tools
model: Any = None # None = inherit via SubAgentConfig or parent's modeldescription— read by the parent LLM when selecting which archetype to spawn. Third-person, selection-focused.instruction— the sub-agent's system prompt. Second-person, execution-focused. Supports both strings andInstructionProvidercallables.
| name | tools | typical use |
|---|---|---|
default |
None (inherits all parent tools) |
Neutral task executor. Does not impose a specific role. Auto-registered. |
general-purpose |
None (inherits all parent tools) |
Researcher / explorer with soft "NEVER create files" constraints. Opt-in only. |
Explore |
Read / Glob / Grep / WebFetch |
Read-only search: locate files, grep symbols. |
Plan |
Read / Glob / Grep |
Design implementation plans without modifying code. |
Only default is auto-registered. general-purpose, Explore, and Plan must be explicitly added via the agents parameter.
The LLM writes the sub-agent's instruction at call time, creating any specialist on the fly. By default the sub-agent inherits all parent tools.
class DynamicSubAgentTool(BaseTool):
def __init__(
self,
name: str = "dynamic_subagent",
description: str | None = None,
tools: tuple | None = None,
expose_tool_selection: bool = True,
agent_config: SubAgentConfig | None = None,
skip_summarization: bool = False,
filters_name: list[str] | None = None,
filters: list[BaseFilter] | None = None,
) -> None: ...| parameter | meaning |
|---|---|
name |
Tool name. Default "dynamic_subagent". |
description |
Tool description. |
tools |
Fixed tool set for the sub-agent. None (default) = inherit all parent tools. |
expose_tool_selection |
When True (default), the tools field is exposed so the LLM can narrow the tool surface per call. |
agent_config |
SubAgentConfig applied to every spawned sub-agent. |
skip_summarization |
When True, skip the parent's summarization turn after the sub-agent returns. |
Unified construction-time defaults for every spawned sub-agent. None means "inherit from the parent agent".
@dataclass(frozen=True)
class SubAgentConfig:
model: LLMModel | None = None
"""Model for the sub-agent. None inherits the parent's model."""
generate_content_config: GenerateContentConfig | None = None
"""Generation config (temperature, top_p, etc.). None inherits from parent."""
parallel_tool_calls: bool | None = None
"""Whether the sub-agent may issue parallel tool calls. None inherits from parent."""
include_parent_history: bool = False
"""Whether to inject parent conversation history into the sub-agent's session."""
max_parent_history_turns: int | None = None
"""Max parent turns to inject. None = unlimited. Only used when include_parent_history=True."""
max_turns: int | None = None
"""Max LLM calls the sub-agent may make. None = unlimited."""
forward_events: bool = False
"""Whether to forward the sub-agent's execution events to the parent
runner's consumer as progress updates.
True: the orchestrator can display the sub-agent's execution live (model
output, tool calls, tool results); the parent agent's LLM still receives
only the sub-agent's final result. False (default): the sub-agent runs
silently and only its final result is returned."""Forwarded events reach the consumer as progress events; they are not written to the parent session and never enter the parent agent's LLM context. Consumers identify them via tool_progress=True on event.custom_metadata and read the execution from payload (author / partial / content, plus optional error / usage).
Zero config — only the built-in default archetype:
from trpc_agent_sdk.agents import LlmAgent
from trpc_agent_sdk.tools import SpawnSubAgentTool
orchestrator = LlmAgent(
name="main",
model=opus_model,
instruction="When a task benefits from isolated context, spawn a sub-agent via spawn_subagent.",
tools=[SpawnSubAgentTool()],
)Code-defined archetypes:
from trpc_agent_sdk.agents.sub_agent import SubAgentArchetype
from trpc_agent_sdk.tools import SpawnSubAgentTool
security_auditor = SubAgentArchetype(
name="security-auditor",
description="Use for security code audit. **IMPORTANT:** This agent is read-only.",
instruction="You are a security auditor...",
tools=(ReadTool, GrepTool, GlobTool),
)
orchestrator = LlmAgent(
tools=[SpawnSubAgentTool(agents=[security_auditor])],
)Loading archetypes from Markdown files:
Place .md files in a directory with YAML frontmatter:
---
name: security-auditor
description: Use for security code audit.
tools:
- Read
- Glob
- Grep
---
You are a security auditor...tools=[SpawnSubAgentTool(agent_paths=[".trpc_agents/"])]Unbounded (default) — the sub-agent inherits all parent tools. The LLM narrows the tool set per call via tools:
from trpc_agent_sdk.agents import LlmAgent
from trpc_agent_sdk.tools import DynamicSubAgentTool
orchestrator = LlmAgent(
name="main",
model=opus_model,
instruction="When you need a specialist, create one via dynamic_subagent. Narrow tools as needed.",
tools=[DynamicSubAgentTool()],
)Bounded — the sub-agent uses a fixed tool set. The parent agent has no direct access to those tools; every task must be delegated. This is useful for keeping dangerous tools behind the sub-agent boundary:
orchestrator = LlmAgent(
name="main",
model=opus_model,
instruction="You can only use tools by delegating via dynamic_subagent. Do not attempt direct calls.",
tools=[
DynamicSubAgentTool(
tools=(calculator, word_count),
expose_tool_selection=False,
),
],
)- Tool inheritance:
DynamicSubAgentTool()inherits all parent tools by default; passtools=(...)to give the sub-agent a fixed set instead. ForSpawnSubAgentTool, the archetype'stoolsfield decides —Nonemeans inherit,(ReadTool, ...)means that exact set. In all cases, spawn tools are stripped from the sub-agent to prevent recursion. - Session isolation: sub-agents run in a fresh ephemeral session. Parent history is not shared by default; opt in via
include_parent_history=True. - Nesting: 1-level hard cap. Sub-agents cannot spawn further sub-agents.
- Result shape: the sub-agent's final text is returned as the tool result string.
- Live execution (
forward_events): setSubAgentConfig(forward_events=True)to stream the sub-agent's execution to the parent runner's consumer for display. Forwarded events are progress events — they never enter the parent LLM's context, which still receives only the final result. Consumers detect them viatool_progress=Trueonevent.custom_metadataand readpayload. Seeexamples/dynamic_subagentandexamples/spawn_subagentfor a working consumer.