Multi-Agent System
The Multi-Agent System is one of the core features of the trpc-agent-go framework, allowing you to create complex systems composed of multiple specialized Agents. These Agents can collaborate in different ways to implement various application scenarios from simple to complex.
Overview
The Multi-Agent System is built on the SubAgent concept, implementing various collaboration patterns through the WithSubAgents option:
Basic Concepts
- SubAgent - Specialized Agents configured through the
WithSubAgentsoption, serving as the foundation for building complex collaboration patterns
Core Collaboration Patterns
- Chain Agent (ChainAgent) - Uses SubAgents to execute sequentially, forming processing pipelines
- Parallel Agent (ParallelAgent) - Uses SubAgents to process different aspects of the same input simultaneously
- Cycle Agent (CycleAgent) - Uses SubAgents to iterate in loops until specific conditions are met
Auxiliary Functions
- Agent Tool (AgentTool) - Wraps Agents as tools for other Agents to call
- Agent Transfer - Implements task delegation between Agents through the
transfer_to_agenttool - Await User Reply Routing - Lets an Agent explicitly claim the next user turn when it needs follow-up information
- Team - A high-level wrapper for coordinator teams and swarm-style handoffs (
teampackage)
SubAgent Basics
SubAgent is the core concept of the Multi-Agent System, implemented through the WithSubAgents option. It allows you to combine multiple specialized Agents to build complex collaboration patterns.
Role of SubAgent
- Specialized Division of Labor: Each SubAgent focuses on specific domains or task types
- Modular Design: Decomposes complex systems into manageable components
- Flexible Combination: Can combine different SubAgents as needed
- Unified Interface: All collaboration patterns are based on the same
WithSubAgentsmechanism
Basic Usage
Dynamic SubAgents and Agent Factories
In trpc-agent-go, a SubAgent is not a different runtime type. It is still an
ordinary agent.Agent. The word "SubAgent" only describes where the Agent is
visible: it is listed under a parent Agent through WithSubAgents, so that the
parent can delegate work to it.
This distinction is important when you need dynamic Agent construction.
Root Agent vs. SubAgent
Use the smallest API that matches the problem:
| Need | Recommended API | Why |
|---|---|---|
| The whole request should start from a different Agent by name | runner.WithAgentFactory + agent.WithAgentByName |
Runner chooses the root Agent before the run starts. |
| A parent Agent should delegate to already-built specialists | llmagent.WithSubAgents |
The parent can immediately describe all specialists to the model. |
| A parent Agent should delegate to a specialist, but building that specialist is expensive or request-specific | agent.NewLazyAgent inside WithSubAgents |
The parent can advertise the specialist from agent.Info, while the concrete Agent is built only if it is invoked. |
| A tenant, project, or database decides which specialists exist | Build the slice in your application code, then pass it to WithSubAgents |
Discovery, authorization, and configuration ownership usually belong to the application. |
runner.WithAgentFactory is for root Agent lookup. It answers: "I already
know the root Agent name for this run; please create it."
WithSubAgents is for parent-scoped visibility. It answers: "While this
parent Agent is running, which specialists may it delegate to?"
Registering an Agent factory on the Runner does not automatically make that Agent visible to every parent Agent. This is intentional: different parent Agents often need different delegation boundaries.
Request-Scoped Root Agent with Request-Scoped SubAgents
If the full Agent tree depends on the request, build the parent Agent in a
Runner factory and pass the request-specific SubAgents into WithSubAgents.
This pattern keeps ownership clear:
- Your application decides which tenant or project configuration to load.
- The Runner creates the request-specific root Agent.
- The parent Agent explicitly receives the SubAgents it is allowed to use.
Lazy SubAgent Construction
Sometimes the parent only needs a specialist in rare cases. Creating that specialist up front may be wasteful if it opens network clients, builds a large tool set, or prepares a sandbox.
agent.NewLazyAgent solves this small framework boundary. It gives the parent
an agent.Info immediately, so the transfer_to_agent tool can show the
specialist name and description. The concrete Agent is created only when the
lazy Agent's Run method is called.
The lazy Agent is still an ordinary agent.Agent. The parent sees it through
the normal WithSubAgents mechanism, and transfer_to_agent finds it through
the normal FindSubAgent path.
Keep these rules in mind:
agent.Info.Nameis the stable transfer target name. The factory should return a concrete Agent with the same name so events and session branches are easy to follow.NewLazyAgentis best for leaf specialists. If the specialist itself needs nested SubAgents, build and configure them inside the factory.- The framework does not own resources created by the factory. If the factory opens per-request resources, clean them up in your application or callbacks.
Application-Owned Discovery
The framework deliberately does not prescribe where dynamic SubAgent definitions come from. They may come from code, a database, a tenant configuration service, a plugin, or files in your own application.
The recommended boundary is:
- Your application discovers and validates the configuration.
- Your application converts that configuration into
agent.Agentvalues oragent.NewLazyAgent(...)descriptors. - The framework runs the resulting Agents through
WithSubAgents,transfer_to_agent, AgentTool, ChainAgent, ParallelAgent, CycleAgent, or GraphAgent.
This keeps configuration, authorization, and rollout policy in the application
while preserving the framework's single runtime abstraction: agent.Agent.
Core Collaboration Patterns
All collaboration patterns are based on the SubAgent concept, implemented through different execution strategies:
Chain Agent (ChainAgent)
Chain Agent uses SubAgents connected sequentially to form processing pipelines. Each SubAgent focuses on specific tasks and passes results to the next SubAgent.
Use Cases
- Content Creation Workflow: Planning → Research → Writing
- Problem Solving Workflow: Analysis → Design → Implementation
- Data Processing Workflow: Collection → Cleaning → Analysis
Basic Usage
Example Session
Parallel Agent (ParallelAgent)
Parallel Agent uses SubAgents to process different aspects of the same input simultaneously, providing multi-perspective analysis.
Use Cases
- Business Decision Analysis: Market analysis, technical assessment, risk evaluation, opportunity analysis
- Multi-dimensional Evaluation: Different experts simultaneously evaluating the same problem
- Fast Parallel Processing: Scenarios requiring multiple perspectives simultaneously
Basic Usage
Example Session
Cycle Agent (CycleAgent)
Cycle Agent uses SubAgents to run in iterative loops until specific conditions are met (such as quality thresholds or maximum iterations).
Use Cases
- Content Optimization: Generate → Evaluate → Improve → Repeat
- Problem Solving: Propose → Evaluate → Enhance → Repeat
- Quality Assurance: Draft → Review → Revise → Repeat
Basic Usage
Escalation Function (WithEscalationFunc)
In a CycleAgent, escalation simply means: stop the loop now.
A CycleAgent runs its SubAgents in order, then repeats the whole sequence.
It stops when one of these happens:
- Your
EscalationFuncreturnstruefor an event WithMaxIterations(n)is reached- The
context.Contextis cancelled
What does EscalationFunc receive?
The callback signature is:
The function is evaluated on events forwarded from sub-agents. To avoid
stopping on half-finished streaming chunks, CycleAgent only checks
escalation on "meaningful" events such as:
- error events (
evt.Error != nil) - tool response events (
evt.Object == model.ObjectTypeToolResponse) - final completion events (
evt.Done == true, non-streaming)
Default behavior
If you do not set WithEscalationFunc, CycleAgent stops only on errors.
Example: quality-based stopping
A common pattern is: Generate → Critic → stop when "good enough".
Have your critic Agent emit a machine-readable signal (for example, a
record_score tool that returns JSON with needs_improvement). Then stop
the cycle as soon as needs_improvement becomes false (requires
encoding/json):
Keep the function fast and defensive (check nil, ignore parse errors),
because it runs inside the event loop.
Example Session
Auxiliary Functions
Agent Tool (AgentTool)
Agent Tool is an important foundational function for building complex multi-agent systems. It allows you to wrap any Agent as a callable tool for use by other Agents or applications.
Use Cases
- Specialized Delegation: Different Agents handle specific types of tasks
- Tool Integration: Agents can be integrated as tools into larger systems
- Modular Design: Reusable Agent components can be combined together
- Complex Workflows: Complex workflows involving multiple specialized Agents
Basic Usage
Agent Tool Architecture
Example Session
Streaming Inner Forwarding (StreamInner)
When WithStreamInner(true) is enabled for the Agent tool:
- Child Agent events are forwarded as streaming
event.Eventitems; you can directly displaychoice.Delta.Content - To avoid duplicates, the child Agent’s final full text is not forwarded again; it is aggregated into the final
tool.responsethat follows tool_calls (satisfying provider requirements) - To keep inner progress but hide child assistant prose, add
WithInnerTextMode(agenttool.InnerTextModeExclude) - UI recommendations:
- Show forwarded child deltas as they stream
- By default, don’t reprint the final aggregated tool response text unless debugging
Example: Distinguish outer assistant, child Agent (forwarded), and tool responses in your event loop
Option Matrix
WithSkipSummarization(false): (default) Allow one more summarization LLM call after the toolWithSkipSummarization(true): Skip the outer summarization so the tool output is surfaced directlyWithStreamInner(true): Forward child Agent events (useStream: trueon both parent and child Agents)WithStreamInner(false): Treat as a callable-only tool, without inner forwardingWithInnerTextMode(agenttool.InnerTextModeInclude): show child assistant text when inner streaming is enabledWithInnerTextMode(agenttool.InnerTextModeExclude): keep inner progress events, but suppress forwarded child assistant textWithResponseMode(agenttool.ResponseModeDefault): default compatibility mode; concatenate child assistant messages into the tool resultWithResponseMode(agenttool.ResponseModeFinalOnly): return only the last complete child assistant message as the tool result
Use ResponseModeFinalOnly when the child Agent is a context-isolated worker
and the parent Agent should only consume its final answer. Use
InnerTextModeExclude separately when you also want to hide child assistant
text from the streamed UI.
Model and Structured Output Pinning
By default, a sub-agent inherits the caller's run-scoped overrides from
RunOptions. This only matters when the caller passes options at
runner.Run time (for example, an AGUI server forwarding the end-user's model
or output-format choice). If no runtime override is set in RunOptions, the
sub-agent naturally uses its own static configuration.
For fixed AgentTool sub-agents, you can pin selected child-side
configuration so the inherited runtime overrides are cleared at the AgentTool
boundary:
The options map to different inherited fields:
WithPinModel(true): clearsRunOptions.ModelName,RunOptions.ModelandRunOptions.ModelSelector, so the sub-agent's ownllmagent.WithModel(...)or model selector takes effect.WithPinStructuredOutput(true): clearsRunOptions.StructuredOutputandRunOptions.StructuredOutputType, so the sub-agent's ownllmagent.WithStructuredOutputJSON(...)orllmagent.WithStructuredOutputJSONSchema(...)takes effect.
Correlating Sub-Agent Events to the Parent Tool Call
Events emitted by a sub-agent invoked through AgentTool carry a
ParentMetadata field whose TriggerID is the parent's toolCallId. AG-UI
consumers can use this as the join key to attach sub-agent events to the
specific TOOL_CALL_START that spawned them, which is essential when a model
issues parallel AgentTool calls to the same sub-agent in one turn (in that
case all child invocations share the same ParentInvocationID, so
ParentMetadata.TriggerID is the only disambiguator). See the
Event Source Metadata section in the
AG-UI chat doc and the
ParentMetadata field
in the Event doc for the wire format and field semantics.
Agent Transfer
Agent Transfer implements task delegation between Agents through the transfer_to_agent tool, allowing the main Agent to automatically select appropriate SubAgents based on task type.
Use Cases
- Task Classification: Automatically select appropriate SubAgents based on user requests
- Intelligent Routing: Route complex tasks to the most suitable handlers
- Specialized Processing: Each SubAgent focuses on specific domains
- Seamless Switching: Seamlessly switch between SubAgents while maintaining conversation continuity
Basic Usage
Dynamic SubAgent Discovery (with A2A)
In real systems, SubAgents are often remote Agents exposed through the A2A protocol. Their list may change over time (for example when new services are registered in a central registry).
To support this, LLMAgent implements the agent.SubAgentSetter
interface. You can refresh its SubAgents at runtime without recreating
the coordinator:
This pattern lets you:
- Integrate with any registry (service discovery, database, config file)
- Dynamically add or remove remote SubAgents
- Keep
Runnerand session logic unchanged, since the coordinator remains the same Agent instance
Agent Transfer Architecture
Example Session
Follow-Up Questions Across Turns
transfer_to_agent solves who handles the current run. It does not, by
itself, tell Runner who should handle the next user message.
That becomes visible when a SubAgent asks a clarifying question:
- The coordinator transfers to a SubAgent.
- The SubAgent asks the user for missing information.
- The user replies in a later request.
- By default,
Runnerstarts from its normal entry Agent again.
The clean solution is to make this routing explicit and one-shot:
- Enable
runner.WithAwaitUserReplyRouting(true)on the Runner. - Enable
llmagent.WithAwaitUserReplyTool(true)on any Agent that may ask the user for follow-up data. - In that Agent's instruction, tell the model to call
await_user_replyimmediately before it asks the user for missing information.
Important behavior:
- The route is consumed once. After the next user turn starts, it is cleared.
- Explicit
agent.WithAgent(...)oragent.WithAgentByName(...)still wins. - If the target Agent path is no longer valid,
Runnerfalls back to its default entry Agent and clears the stale route. Runnerrestores nested SubAgents by their full agent-chain path, so you do not need to register every SubAgent separately in the common coordinator-plus-SubAgents setup.
For custom Agents that do not use LLMAgent, see the runner guide for the
low-level agent.MarkAwaitingUserReply(...) API.
Correlating Transfer Target Events to the Triggering Tool Call
When transfer_to_agent hands off control to a target Agent, events emitted
by the target invocation carry a ParentMetadata field with
TriggerType=transfer, TriggerID=<the parent's toolCallId>, and
TriggerName=transfer_to_agent. AG-UI consumers can use TriggerID to attach
the target's events to the specific TOOL_CALL_START of the
transfer_to_agent call. See the
Event Source Metadata section in the
AG-UI chat doc for the wire format.
Built-in Explorer (Read-only Exploration Agent)
agent/llmagent/builtin provides a ready-to-use, read-only "explore / search /
analyze / inspect" agent preset, so you do not have to hand-write the name,
description, read-only prompt, and parent-capability inheritance every time.
builtin.NewExplorer() returns a plain agent.Agent, so both mounting styles
work:
Capability inheritance is identical for both: transfer and AgentTool each run
the sub-agent on a clone of the parent invocation, so the explorer derives its
default surface from the direct parent invocation at Run time.
Default inheritance
With no options, the explorer inherits from the direct parent invocation:
- User tools: tools the parent registered via
WithTools/WithToolSets. Framework-injected tools (transfer_to_agent,await_user_reply, ...) are not inherited. - Knowledge: the parent's retrieval capability (
knowledge_search, ...) is regenerated from the parent's knowledge configuration. - Skills / code executor: regenerated from the parent's configuration so they bind to the child invocation instead of carrying the parent's runtime state.
- Model: inherits the parent invocation's currently resolved model.
Read-only is an advisory constraint
The explorer's read-only behavior is an advisory system prompt, not a permission boundary. It is intended as a convenient built-in read-only role: the model is instructed to inspect, search, and summarize, but the framework does not automatically classify tools as read-only or mutating.
Customizing the surface
Available options: WithName, WithDescription, WithInstruction,
WithTools, WithSkills, WithModel, WithCodeExecutor, plus the advanced
escape hatch WithLLMAgentOptions (forwards raw llmagent.Option values to the
inner agent; use sparingly).
Behavior notes:
- Without
WithTools: inherits the parent's user tools at run time. WithWithTools: uses the explicit set and inherits neither parent user tools nor knowledge. - Without
WithSkills/WithCodeExecutor: regenerated from the parent's capabilities at run time. - Without
WithModel: inherits the parent invocation's model; if the parent has no model either,Runreturns a clear error. - No parent invocation (for example when run as a root): nothing is inherited; only explicit configuration is used.
Environment Variable Configuration
All multi-agent examples require the following environment variables:
| Variable Name | Required | Default Value | Description |
|---|---|---|---|
OPENAI_API_KEY |
Yes | - | OpenAI API key |
OPENAI_BASE_URL |
No | https://api.openai.com/v1 |
OpenAI API base URL |
Running Examples
All example code is located at examples