|
| 1 | +# Spawned Sub-Agents |
| 2 | + |
| 3 | +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: |
| 4 | + |
| 5 | +- **Context pollution**: exploratory searches, tool outputs, and intermediate steps fill the context window, crowding out what matters. |
| 6 | +- **Tool sprawl**: the parent carries every tool all the time, even though most subtasks only need a subset. |
| 7 | +- **No role isolation**: the parent has one system prompt; it cannot adopt a different persona or constraints per subtask. |
| 8 | +- **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. |
| 9 | + |
| 10 | +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. |
| 11 | + |
| 12 | +**Spawned Sub-Agents** give the parent agent two tools for creating short-lived sub-agents at run time: |
| 13 | + |
| 14 | +- **`SpawnSubAgentTool`** — choose from a **pre-defined catalog** of standardized specialists. Instruction, tool set, and model are locked by the archetype at construction time. |
| 15 | + |
| 16 | + 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_type` and writes a task-specific `prompt`, but cannot alter the sub-agent's instruction or tools. |
| 17 | + |
| 18 | +- **`DynamicSubAgentTool`** — the LLM **invents the specialist on the fly**, writing the instruction at call time. No pre-registration needed. |
| 19 | + |
| 20 | + 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. |
| 21 | + |
| 22 | +The difference is *who defines the role*: the developer (Spawn) or the LLM (Dynamic). |
| 23 | + |
| 24 | +## Quick Start |
| 25 | + |
| 26 | +```python |
| 27 | +from trpc_agent_sdk.agents import LlmAgent |
| 28 | +from trpc_agent_sdk.tools import SpawnSubAgentTool, DynamicSubAgentTool |
| 29 | + |
| 30 | +# Spawn: pick from a catalog of pre-defined specialists |
| 31 | +agent_with_spawn = LlmAgent( |
| 32 | + name="orchestrator", |
| 33 | + tools=[SpawnSubAgentTool()], # built-in `default` archetype |
| 34 | +) |
| 35 | + |
| 36 | +# Dynamic: LLM writes the specialist's role at call time |
| 37 | +agent_with_dynamic = LlmAgent( |
| 38 | + name="orchestrator", |
| 39 | + tools=[DynamicSubAgentTool()], # sub-agent inherits all parent tools |
| 40 | +) |
| 41 | +``` |
| 42 | + |
| 43 | +## Two Tools |
| 44 | + |
| 45 | +| | `SpawnSubAgentTool` | `DynamicSubAgentTool` | |
| 46 | +| --- | --- | --- | |
| 47 | +| **Pattern** | Pick from a pre-defined catalog | LLM invents role at call time | |
| 48 | +| **Who defines the role** | Developer, two modes:<br>① `SubAgentArchetype` in code<br>② Markdown file (YAML frontmatter + body) | LLM (via `instruction` parameter) | |
| 49 | +| **Best for** | Standardized, repeatable specialists | Roles you can't pre-register | |
| 50 | +| **Role flexibility** | Locked — only `prompt` varies | Full — every call can be different | |
| 51 | +| **Tool surface** | Locked by archetype | Inherits parent tools; LLM can narrow via `tools` | |
| 52 | + |
| 53 | +### `SpawnSubAgentTool` |
| 54 | + |
| 55 | +Dispatches tasks to pre-registered archetypes. The parent LLM picks the right specialist via `subagent_type`; its instruction and tools are fixed. |
| 56 | + |
| 57 | +```python |
| 58 | +class SpawnSubAgentTool(BaseTool): |
| 59 | + def __init__( |
| 60 | + self, |
| 61 | + agents: list[SubAgentArchetype] | None = None, |
| 62 | + agent_paths: list[str | os.PathLike] | None = None, |
| 63 | + tool_mapping: dict[str, Any] | None = None, |
| 64 | + with_default: bool = True, |
| 65 | + agent_config: SubAgentConfig | None = None, |
| 66 | + skip_summarization: bool = False, |
| 67 | + filters_name: list[str] | None = None, |
| 68 | + filters: list[BaseFilter] | None = None, |
| 69 | + ) -> None: ... |
| 70 | +``` |
| 71 | + |
| 72 | +| parameter | meaning | |
| 73 | +| --- | --- | |
| 74 | +| `agents` | Additional archetypes to register. | |
| 75 | +| `agent_paths` | Directories of `*.md` files to load archetypes from disk. | |
| 76 | +| `tool_mapping` | Custom tool name → tool class mapping for resolving MD frontmatter. | |
| 77 | +| `with_default` | Whether to register the built-in `default` archetype. Default `True`. | |
| 78 | +| `agent_config` | `SubAgentConfig` applied to every spawned sub-agent. | |
| 79 | +| `skip_summarization` | When `True`, skip the parent's summarization turn after the sub-agent returns. | |
| 80 | + |
| 81 | +**Three ways to configure:** |
| 82 | + |
| 83 | +```python |
| 84 | +# Zero config — only the built-in `default` archetype |
| 85 | +SpawnSubAgentTool() |
| 86 | + |
| 87 | +# Code-defined archetypes |
| 88 | +SpawnSubAgentTool(agents=[security_auditor, EXPLORE_AGENT, PLAN_AGENT]) |
| 89 | + |
| 90 | +# Load from Markdown files |
| 91 | +SpawnSubAgentTool(agent_paths=[".trpc_agents/"]) |
| 92 | +``` |
| 93 | + |
| 94 | +#### `SubAgentArchetype` |
| 95 | + |
| 96 | +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. |
| 97 | + |
| 98 | +```python |
| 99 | +@dataclass(frozen=True) |
| 100 | +class SubAgentArchetype: |
| 101 | + name: str # registry key + the value LLM passes as `subagent_type` |
| 102 | + description: str # what the LLM reads to pick this archetype |
| 103 | + instruction: str | InstructionProvider |
| 104 | + tools: tuple | None = None # None = inherit all parent tools |
| 105 | + model: Any = None # None = inherit via SubAgentConfig or parent's model |
| 106 | +``` |
| 107 | + |
| 108 | +- **`description`** — read by the **parent LLM** when selecting which archetype to spawn. Third-person, selection-focused. |
| 109 | +- **`instruction`** — the **sub-agent's** system prompt. Second-person, execution-focused. Supports both strings and `InstructionProvider` callables. |
| 110 | + |
| 111 | +#### Built-in Archetypes |
| 112 | + |
| 113 | +| name | tools | typical use | |
| 114 | +| --- | --- | --- | |
| 115 | +| `default` | `None` (inherits all parent tools) | **Neutral task executor.** Does not impose a specific role. **Auto-registered.** | |
| 116 | +| `general-purpose` | `None` (inherits all parent tools) | **Researcher / explorer** with soft "NEVER create files" constraints. Opt-in only. | |
| 117 | +| `Explore` | `Read` / `Glob` / `Grep` / `WebFetch` | Read-only search: locate files, grep symbols. | |
| 118 | +| `Plan` | `Read` / `Glob` / `Grep` | Design implementation plans without modifying code. | |
| 119 | + |
| 120 | +Only `default` is auto-registered. `general-purpose`, `Explore`, and `Plan` must be explicitly added via the `agents` parameter. |
| 121 | + |
| 122 | +### `DynamicSubAgentTool` |
| 123 | + |
| 124 | +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. |
| 125 | + |
| 126 | +```python |
| 127 | +class DynamicSubAgentTool(BaseTool): |
| 128 | + def __init__( |
| 129 | + self, |
| 130 | + name: str = "dynamic_subagent", |
| 131 | + description: str | None = None, |
| 132 | + tools: tuple | None = None, |
| 133 | + expose_tool_selection: bool = True, |
| 134 | + agent_config: SubAgentConfig | None = None, |
| 135 | + skip_summarization: bool = False, |
| 136 | + filters_name: list[str] | None = None, |
| 137 | + filters: list[BaseFilter] | None = None, |
| 138 | + ) -> None: ... |
| 139 | +``` |
| 140 | + |
| 141 | +| parameter | meaning | |
| 142 | +| --- | --- | |
| 143 | +| `name` | Tool name. Default `"dynamic_subagent"`. | |
| 144 | +| `description` | Tool description. | |
| 145 | +| `tools` | Fixed tool set for the sub-agent. `None` (default) = inherit all parent tools. | |
| 146 | +| `expose_tool_selection` | When `True` (default), the `tools` field is exposed so the LLM can narrow the tool surface per call. | |
| 147 | +| `agent_config` | `SubAgentConfig` applied to every spawned sub-agent. | |
| 148 | +| `skip_summarization` | When `True`, skip the parent's summarization turn after the sub-agent returns. | |
| 149 | + |
| 150 | +## Shared Configuration |
| 151 | + |
| 152 | +### `SubAgentConfig` |
| 153 | + |
| 154 | +Unified construction-time defaults for every spawned sub-agent. `None` means "inherit from the parent agent". |
| 155 | + |
| 156 | +```python |
| 157 | +@dataclass(frozen=True) |
| 158 | +class SubAgentConfig: |
| 159 | + model: LLMModel | None = None |
| 160 | + """Model for the sub-agent. None inherits the parent's model.""" |
| 161 | + |
| 162 | + generate_content_config: GenerateContentConfig | None = None |
| 163 | + """Generation config (temperature, top_p, etc.). None inherits from parent.""" |
| 164 | + |
| 165 | + parallel_tool_calls: bool | None = None |
| 166 | + """Whether the sub-agent may issue parallel tool calls. None inherits from parent.""" |
| 167 | + |
| 168 | + include_parent_history: bool = False |
| 169 | + """Whether to inject parent conversation history into the sub-agent's session.""" |
| 170 | + |
| 171 | + max_parent_history_turns: int | None = None |
| 172 | + """Max parent turns to inject. None = unlimited. Only used when include_parent_history=True.""" |
| 173 | + |
| 174 | + max_turns: int | None = None |
| 175 | + """Max LLM calls the sub-agent may make. None = unlimited.""" |
| 176 | +``` |
| 177 | + |
| 178 | +## Usage |
| 179 | + |
| 180 | +### SpawnSubAgentTool |
| 181 | + |
| 182 | +**Zero config** — only the built-in `default` archetype: |
| 183 | + |
| 184 | +```python |
| 185 | +from trpc_agent_sdk.agents import LlmAgent |
| 186 | +from trpc_agent_sdk.tools import SpawnSubAgentTool |
| 187 | + |
| 188 | +orchestrator = LlmAgent( |
| 189 | + name="main", |
| 190 | + model=opus_model, |
| 191 | + instruction="When a task benefits from isolated context, spawn a sub-agent via spawn_subagent.", |
| 192 | + tools=[SpawnSubAgentTool()], |
| 193 | +) |
| 194 | +``` |
| 195 | + |
| 196 | +**Code-defined archetypes**: |
| 197 | + |
| 198 | +```python |
| 199 | +from trpc_agent_sdk.agents.sub_agent import SubAgentArchetype |
| 200 | +from trpc_agent_sdk.tools import SpawnSubAgentTool |
| 201 | + |
| 202 | +security_auditor = SubAgentArchetype( |
| 203 | + name="security-auditor", |
| 204 | + description="Use for security code audit. **IMPORTANT:** This agent is read-only.", |
| 205 | + instruction="You are a security auditor...", |
| 206 | + tools=(ReadTool, GrepTool, GlobTool), |
| 207 | +) |
| 208 | + |
| 209 | +orchestrator = LlmAgent( |
| 210 | + tools=[SpawnSubAgentTool(agents=[security_auditor])], |
| 211 | +) |
| 212 | +``` |
| 213 | + |
| 214 | +**Loading archetypes from Markdown files**: |
| 215 | + |
| 216 | +Place `.md` files in a directory with YAML frontmatter: |
| 217 | + |
| 218 | +```markdown |
| 219 | +--- |
| 220 | +name: security-auditor |
| 221 | +description: Use for security code audit. |
| 222 | +tools: |
| 223 | + - Read |
| 224 | + - Glob |
| 225 | + - Grep |
| 226 | +--- |
| 227 | + |
| 228 | +You are a security auditor... |
| 229 | +``` |
| 230 | + |
| 231 | +```python |
| 232 | +tools=[SpawnSubAgentTool(agent_paths=[".trpc_agents/"])] |
| 233 | +``` |
| 234 | + |
| 235 | +### DynamicSubAgentTool |
| 236 | + |
| 237 | +**Unbounded (default)** — the sub-agent inherits all parent tools. The LLM narrows the tool set per call via `tools`: |
| 238 | + |
| 239 | +```python |
| 240 | +from trpc_agent_sdk.agents import LlmAgent |
| 241 | +from trpc_agent_sdk.tools import DynamicSubAgentTool |
| 242 | + |
| 243 | +orchestrator = LlmAgent( |
| 244 | + name="main", |
| 245 | + model=opus_model, |
| 246 | + instruction="When you need a specialist, create one via dynamic_subagent. Narrow tools as needed.", |
| 247 | + tools=[DynamicSubAgentTool()], |
| 248 | +) |
| 249 | +``` |
| 250 | + |
| 251 | +**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: |
| 252 | + |
| 253 | +```python |
| 254 | +orchestrator = LlmAgent( |
| 255 | + name="main", |
| 256 | + model=opus_model, |
| 257 | + instruction="You can only use tools by delegating via dynamic_subagent. Do not attempt direct calls.", |
| 258 | + tools=[ |
| 259 | + DynamicSubAgentTool( |
| 260 | + tools=(calculator, word_count), |
| 261 | + expose_tool_selection=False, |
| 262 | + ), |
| 263 | + ], |
| 264 | +) |
| 265 | +``` |
| 266 | + |
| 267 | +## Additional Notes |
| 268 | + |
| 269 | +- **Tool inheritance**: `DynamicSubAgentTool()` inherits all parent tools by default; pass `tools=(...)` to give the sub-agent a fixed set instead. For `SpawnSubAgentTool`, the archetype's `tools` field decides — `None` means inherit, `(ReadTool, ...)` means that exact set. In all cases, spawn tools are stripped from the sub-agent to prevent recursion. |
| 270 | +- **Session isolation**: sub-agents run in a fresh ephemeral session. Parent history is not shared by default; opt in via `include_parent_history=True`. |
| 271 | +- **Nesting**: 1-level hard cap. Sub-agents cannot spawn further sub-agents. |
| 272 | +- **Result shape**: the sub-agent's final text is returned as the tool result string. |
0 commit comments