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 WithSubAgents option, serving as the foundation for building complex collaboration patterns
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_agent tool
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 (team package)
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 WithSubAgents mechanism
import("trpc.group/trpc-go/trpc-agent-go/agent""trpc.group/trpc-go/trpc-agent-go/agent/llmagent")// Create SubAgent.mathAgent:=llmagent.New("math-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Handles mathematical calculations and numerical problems"),llmagent.WithInstruction("You are a mathematics expert, focusing on mathematical operations and numerical reasoning..."),)weatherAgent:=llmagent.New("weather-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Provides weather information and suggestions"),llmagent.WithInstruction("You are a weather expert, providing weather analysis and activity suggestions..."),)// Use WithSubAgents option to configure SubAgent.mainAgent:=llmagent.New("coordinator-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Coordinator Agent responsible for task delegation"),llmagent.WithInstruction("You are a coordinator, analyzing user requests and delegating to appropriate experts..."),llmagent.WithSubAgents([]agent.Agent{mathAgent,weatherAgent}),)
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.
r:=runner.NewRunnerWithAgentFactory("support-app","support-coordinator",func(ctxcontext.Context,roagent.RunOptions)(agent.Agent,error){tenantID,_:=agent.GetRuntimeStateValue[string](&ro,"tenant_id")billingAgent:=llmagent.New("billing-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Handles billing and invoice questions"),llmagent.WithInstruction("Answer billing questions for tenant "+tenantID+".",),)coordinator:=llmagent.New("support-coordinator",llmagent.WithModel(modelInstance),llmagent.WithDescription("Routes support requests to specialists"),llmagent.WithInstruction("Decide whether to answer directly or transfer to a specialist.",),llmagent.WithSubAgents([]agent.Agent{billingAgent}),)returncoordinator,nil},)events,err:=r.Run(ctx,userID,sessionID,model.NewUserMessage("Why was my invoice higher this month?"),agent.MergeRuntimeState(map[string]any{"tenant_id":"tenant-001",}),)_=events_=err
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.
lazyBillingAgent:=agent.NewLazyAgent(agent.Info{Name:"billing-agent",Description:"Handles billing and invoice questions",},func(ctxcontext.Context,roagent.RunOptions)(agent.Agent,error){tenantID,_:=agent.GetRuntimeStateValue[string](&ro,"tenant_id")returnllmagent.New("billing-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Handles billing and invoice questions"),llmagent.WithInstruction("Answer billing questions for tenant "+tenantID+".",),),nil},)coordinator:=llmagent.New("support-coordinator",llmagent.WithModel(modelInstance),llmagent.WithDescription("Routes support requests to specialists"),llmagent.WithInstruction("Use transfer_to_agent when a specialist should handle the request.",),llmagent.WithSubAgents([]agent.Agent{lazyBillingAgent}),)
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.Name is 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.
NewLazyAgent is 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.Agent values
or agent.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
🔗 Multi-Agent Chain Demo
Chain Flow: Planning → Research → Writing
==================================================
👤 User: Explain the benefits of renewable energy
📋 Planning Agent: I will create a structured analysis plan...
🔍 Research Agent:
🔧 Using tools:
• web_search (ID: call_123)
🔄 Executing...
✅ Tool result: Latest renewable energy data...
✍️ Writing Agent: Based on planning and research:
[Structured comprehensive response]
Parallel Agent (ParallelAgent)
Parallel Agent uses SubAgents to process different aspects of the same input simultaneously, providing multi-perspective analysis.
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:
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):
🔄 Multi-Agent Cycle Demo
Max iterations: 3
Cycle: Generate → Evaluate → Improve → Repeat
==================================================
👤 User: Write a short joke
🤖 Cycle Response:
🤖 Generate Agent: Why don't skeletons fight each other?
Because they don't have the guts!
👀 Evaluate Agent:
🔧 Using tools:
• record_score (ID: call_123)
🔄 Executing...
✅ Quality score: 75/100
⚠️ Needs improvement - continue iteration
🔄 **2nd Iteration**
🤖 Generate Agent: This is an improved version with a new twist:
**Why do skeletons never win arguments?**
Because they always lose their backbone halfway through!
👀 Evaluate Agent:
🔧 Using tools:
• record_score (ID: call_456)
🔄 Executing...
✅ Quality score: 85/100
🎉 Quality threshold reached - cycle completed
🏁 Cycle completed after 2 iterations
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
import("trpc.group/trpc-go/trpc-agent-go/agent/llmagent""trpc.group/trpc-go/trpc-agent-go/tool"agenttool"trpc.group/trpc-go/trpc-agent-go/tool/agent""trpc.group/trpc-go/trpc-agent-go/tool/function")// Create specialized Agent.mathAgent:=llmagent.New("math-specialist",llmagent.WithModel(modelInstance),llmagent.WithDescription("Agent specialized in mathematical operations"),llmagent.WithInstruction("You are a mathematics expert, focusing on mathematical operations, calculations and numerical reasoning..."),llmagent.WithTools([]tool.Tool{calculatorTool}),)// Wrap Agent as tool.agentTool:=agenttool.NewTool(mathAgent,// The default value for skip summarization=false. // When set to true, the current round will end immediately after tool.response.agenttool.WithSkipSummarization(false),// Enable inner forwarding: stream child Agent events inline to the parentagenttool.WithStreamInner(true),// Return only the child Agent's final assistant message as the tool result.agenttool.WithResponseMode(agenttool.ResponseModeFinalOnly),)// Use Agent tool in main Agent.mainAgent:=llmagent.New("chat-assistant",llmagent.WithTools([]tool.Tool{timeTool,agentTool}),)
🚀 Agent Tool Example
Model: deepseek-v4-flash
Available tools: current_time, math-specialist
==================================================
👤 User: Calculate 923476 * 273472354
🤖 Assistant: I will use the math specialist Agent to calculate this result.
🔧 Tool call initiated:
• math-specialist (ID: call_0_e53a77e9-c994-4421-bfc3-f63fe85678a1)
Parameters: {"request":"Calculate 923476 multiplied by 273472354"}
🔄 Executing tool...
✅ Tool response (ID: call_0_e53a77e9-c994-4421-bfc3-f63fe85678a1):
"The result of calculating 923,476 multiplied by 273,472,354 is:
\[
923,\!476 \times 273,\!472,\!354 = 252,\!545,\!155,\!582,\!504
\]"
✅ Tool execution completed.
Streaming Inner Forwarding (StreamInner)
When WithStreamInner(true) is enabled for the Agent tool:
Child Agent events are forwarded as streaming event.Event items; you can directly display choice.Delta.Content
To avoid duplicates, the child Agent’s final full text is not forwarded again; it is aggregated into the final tool.response that 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
// Child Agent forwarded delta (author != parent)ifev.Author!=parentName&&ev.Response!=nil&&len(ev.Response.Choices)>0{ifdelta:=ev.Response.Choices[0].Delta.Content;delta!=""{fmt.Print(delta)}return}// Tool response (aggregated content), skip by default to avoid duplicatesifev.Response!=nil&&ev.Object==model.ObjectTypeToolResponse{// ...show on demand or skipreturn}
Option Matrix
WithSkipSummarization(false): (default) Allow one more summarization LLM call after the tool
WithSkipSummarization(true): Skip the outer summarization so the tool output is surfaced directly
WithStreamInner(true): Forward child Agent events (use Stream: true on both parent and child Agents)
WithStreamInner(false): Treat as a callable-only tool, without inner forwarding
WithInnerTextMode(agenttool.InnerTextModeInclude): show child
assistant text when inner streaming is enabled
WithInnerTextMode(agenttool.InnerTextModeExclude): keep inner
progress events, but suppress forwarded child assistant text
WithResponseMode(agenttool.ResponseModeDefault): default compatibility mode;
concatenate child assistant messages into the tool result
WithResponseMode(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 Pinning
By default, a sub-agent inherits the caller's RunOptions.ModelName,
RunOptions.Model and RunOptions.ModelSelector. This only takes effect
when the caller passes agent.WithModelName(...), agent.WithModel(...)
or agent.WithModelSelector(...) at runner.Run time (for example, an
AGUI server forwarding the end-user's model choice). If no runtime model
is set in RunOptions, the sub-agent naturally uses its own model
configured via llmagent.WithModel.
When the sub-agent should always use its own model regardless of the
runtime model selection propagated through RunOptions:
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
import("trpc.group/trpc-go/trpc-agent-go/agent""trpc.group/trpc-go/trpc-agent-go/agent/llmagent""trpc.group/trpc-go/trpc-agent-go/tool""trpc.group/trpc-go/trpc-agent-go/tool/function")// Create SubAgent.mathAgent:=llmagent.New("math-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Handles mathematical calculations and numerical problems"),llmagent.WithInstruction("You are a mathematics expert, focusing on mathematical operations and numerical reasoning..."),llmagent.WithTools([]tool.Tool{calculatorTool}),)weatherAgent:=llmagent.New("weather-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Provides weather information and suggestions"),llmagent.WithInstruction("You are a weather expert, providing weather analysis and activity suggestions..."),llmagent.WithTools([]tool.Tool{weatherTool}),)// Create coordinator Agent, use WithSubAgents to configure SubAgent.coordinatorAgent:=llmagent.New("coordinator-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Coordinator Agent responsible for task delegation"),llmagent.WithInstruction("You are a coordinator, analyzing user requests and delegating to appropriate experts..."),llmagent.WithSubAgents([]agent.Agent{mathAgent,weatherAgent}),)
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:
import("fmt""context""trpc.group/trpc-go/trpc-agent-go/agent""trpc.group/trpc-go/trpc-agent-go/agent/a2aagent")funcrefreshSubAgents(ctxcontext.Context,agagent.Agent)error{cfg,ok:=ag.(agent.SubAgentSetter)if!ok{returnfmt.Errorf("agent does not support dynamic SubAgents")}// 1. Discover remote Agents from your registry or config source.urls:=[]string{"http://localhost:8087/","http://localhost:8088/",}// 2. Build A2AAgent proxies for each remote Agent.subAgents:=make([]agent.Agent,0,len(urls))for_,url:=rangeurls{a2,err:=a2aagent.New(a2aagent.WithAgentCardURL(url))iferr!=nil{// In production you may want to log and skip failures.continue}subAgents=append(subAgents,a2)}// 3. Atomically replace SubAgents on the coordinator.cfg.SetSubAgents(subAgents)returnnil}
This pattern lets you:
Integrate with any registry (service discovery, database, config file)
Dynamically add or remove remote SubAgents
Keep Runner and session logic unchanged, since the coordinator
remains the same Agent instance
🔄 Agent Transfer Demo
Available SubAgents: math-agent, weather-agent, research-agent
==================================================
👤 User: Calculate compound interest, principal $5000, annual rate 6%, term 8 years
🎯 Coordinator: I will delegate this task to our mathematics expert for accurate calculation.
🔄 Initiating delegation...
🔄 Transfer event: Transferring control to Agent: math-agent
🧮 Math Expert: I will help you calculate compound interest step by step.
🔧 🧮 Executing tool:
• calculate ({"operation":"power","a":1.06,"b":8})
✅ Tool completed
🔧 🧮 Executing tool:
• calculate ({"operation":"multiply","a":5000,"b":1.593})
✅ Tool completed
Compound Interest Calculation Result:
- Principal: $5,000
- Annual Rate: 6%
- Term: 8 years
- Result: $7,969.24 (interest approximately $2,969.24)
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, Runner starts 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_reply
immediately before it asks the user for missing information.
import("context""trpc.group/trpc-go/trpc-agent-go/agent""trpc.group/trpc-go/trpc-agent-go/agent/llmagent""trpc.group/trpc-go/trpc-agent-go/model""trpc.group/trpc-go/trpc-agent-go/runner""trpc.group/trpc-go/trpc-agent-go/tool")profileAgent:=llmagent.New("profile-agent",llmagent.WithModel(modelInstance),llmagent.WithDescription("Collects and updates customer profile information.",),llmagent.WithInstruction(`You update customer profiles.If any required field is missing:1. call await_user_reply2. ask one clear question to the user3. stop and wait for the next user messageWhen all required fields are present, call update_profile.`),llmagent.WithAwaitUserReplyTool(true),llmagent.WithTools([]tool.Tool{updateProfileTool}),)coordinatorAgent:=llmagent.New("coordinator-agent",llmagent.WithModel(modelInstance),llmagent.WithInstruction(`Delegate profile-update requests to profile-agent.Do not collect profile fields yourself.`),llmagent.WithSubAgents([]agent.Agent{profileAgent}),)r:=runner.NewRunner("crm-app",coordinatorAgent,runner.WithAwaitUserReplyRouting(true),)// Turn 1:// user: "Update my profile."// coordinator-agent -> transfer_to_agent(profile-agent)// profile-agent -> await_user_reply + "What phone number should I save?"// Turn 2 (same session):// user: "+1-555-0101"// Runner resumes directly at profile-agent for this turn.events,err:=r.Run(context.Background(),"user-1","session-1",model.NewUserMessage("+1-555-0101"),)iferr!=nil{// handle error}forevt:=rangeevents{ifevt.IsRunnerCompletion(){break}}
Important behavior:
The route is consumed once. After the next user turn starts, it is cleared.
Explicit agent.WithAgent(...) or agent.WithAgentByName(...) still wins.
If the target Agent path is no longer valid, Runner falls back to its
default entry Agent and clears the stale route.
Runner restores 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.
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:
import("trpc.group/trpc-go/trpc-agent-go/agent""trpc.group/trpc-go/trpc-agent-go/agent/llmagent""trpc.group/trpc-go/trpc-agent-go/agent/llmagent/builtin"agenttool"trpc.group/trpc-go/trpc-agent-go/tool/agent")// Option 1: as a SubAgent, the model hands control over via transfer_to_agent.root:=llmagent.New("assistant",llmagent.WithModel(modelInstance),llmagent.WithTools(tools),llmagent.WithSubAgents([]agent.Agent{builtin.NewExplorer()}),)// Option 2: wrapped by AgentTool, the model calls explorer synchronously and// continues after collecting the result.root:=llmagent.New("assistant",llmagent.WithModel(modelInstance),llmagent.WithTools(append(tools,agenttool.NewTool(builtin.NewExplorer()))),)
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.
builtin.NewExplorer(builtin.WithName("explorer"),builtin.WithDescription("Reads and investigates available context without modifying anything."),builtin.WithInstruction(customReadOnlyPrompt),builtin.WithSkills(readOnlySkillsRepo),// explicit replacementbuiltin.WithModel(modelInstance),// explicit; otherwise inherits parent model)
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. With
WithTools: 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, Run returns 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: