Skip to content

Latest commit

 

History

History
297 lines (225 loc) · 14.1 KB

File metadata and controls

297 lines (225 loc) · 14.1 KB

Spawned Sub-Agents

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_type and writes a task-specific prompt, 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).

How this differs from other multi-agent mechanisms

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.

Quick Start

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
)

Two 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

SpawnSubAgentTool

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/"])

SubAgentArchetype

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 model
  • description — 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 and InstructionProvider callables.

Built-in Archetypes

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.

DynamicSubAgentTool

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.

Shared Configuration

SubAgentConfig

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).

Usage

SpawnSubAgentTool

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/"])]

DynamicSubAgentTool

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,
        ),
    ],
)

Additional Notes

  • 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.
  • 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): set SubAgentConfig(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 via tool_progress=True on event.custom_metadata and read payload. See examples/dynamic_subagent and examples/spawn_subagent for a working consumer.