Multi-Agent Systems
This guide shows how to trace a system where several agents work together: an orchestrator that hands work to sub-agents, sub-agents that run in parallel, and independent agents that call each other. Examples use the Python SDK (ants-platform). For other languages see the JS/TS SDK and Java SDK.
Setup
If this is your first trace, start with Monitor Your First Agent.
How agent names work in a trace
Every trace belongs to exactly one agent:
- The outermost observation of a trace must have an
agent_name. Without it the SDK raisesValueError: agent_name is required. - Every observation opened inside it inherits that agent name. The SDK stores agent names in lowercase.
- A nested observation cannot change the agent name. If you pass a different
agent_nameto a nested observation, the SDK keeps the parent's name and logs a warning:Child span attempted to override parent agent_name (parent: 'orchestrator', child: 'researcher'). Using parent agent_name to maintain consistent attribution.
The AI Agent Center lists agents by these names, so a whole trace, including its cost and latency, counts towards the agent at its root.
That gives you two ways to model a multi-agent system:
| Goal | Pattern |
|---|---|
| See the whole workflow in one trace and treat it as one agent | One trace, nested sub-agents. Sub-agents are told apart by observation name. |
| Track each agent separately, with its own requests, cost and risk | Separate traces per agent, linked by a shared session ID. |
One trace, nested sub-agents
The orchestrator opens the root observation with the agent name. Each sub-agent is a nested agent observation with a descriptive name, and its LLM calls and tools nest inside it.
In Discover AI > Tracing, the trace support-orchestrator shows researcher (with search-docs) and writer (with writer-llm) beneath it. All observations carry the agent name support-orchestrator.
Tips for this pattern:
- Give each sub-agent's LLM calls a name that identifies the sub-agent, such as
writer-llm. Cost is recorded on generations, so the generation name is what lets you split cost by sub-agent. - Use
as_type="tool"for tool calls,"retriever"for searches,"chain"for fixed steps and"generation"for LLM calls you record yourself. See the Python SDK reference. - Do not pass
agent_nameto nested observations. It has no effect and only produces the warning above.
Routing to one sub-agent
A router that picks one sub-agent is the same shape. Record the decision on the root so you can filter for it later:
Parallel sub-agents
Nesting follows the current context. asyncio tasks copy the context when they are created, so sub-agents started with asyncio.gather nest under the orchestrator and inherit its agent name.
The trace shows three analyst observations side by side under research-team. Open the Timeline view of the trace to see that they overlapped.
Threads do not inherit the context. A function submitted to a ThreadPoolExecutor from inside the orchestrator starts a new trace without an agent name, and the SDK raises ValueError: agent_name is required. Submit contextvars.copy_context().run with your function instead, for example executor.submit(contextvars.copy_context().run, analyst_sync, topic), or use asyncio.to_thread, which copies the context for you.
Separate traces per agent
When each agent should appear on its own in the AI Agent Center, with its own requests, cost, latency and risk score, give each agent its own trace. Each agent opens its own root observation with its own agent_name. Link the traces with a shared session_id so the whole conversation is visible together.
support_pipeline is not wrapped in an observation, so research-agent and answer-agent each start their own trace. If you call one agent from inside the other's with block, it joins the outer trace and takes the outer agent name.
When agents run as separate services, each service creates its own client and opens its own root observation in the same way. Pass the session ID between services with your request so the traces line up under Discover AI > Sessions.
Open agent roots with the start_as_current_observation context manager. In the current SDK, an agent observation started with start_observation(...) outside any trace leaves its agent name in the calling context after .end(), so the next root observation opened in that context inherits the old name and logs the override warning.
Compare agents
| Question | Where to look |
|---|---|
| Which agents are running, and what do they cost? | Discover AI > AI Agent Center. One entry per agent name. See AI Agent Center. |
| Which sub-agent in a trace was slow? | The trace Timeline view; Gauge AI > Latency & Analytics for p50 to p99 per span and generation name. See Cost & Performance Metrics. |
| Which sub-agent costs the most? | Gauge AI > Cost Dashboard, widget Top 20 Use Cases (Observation) by Cost (spend per observation name). See Cost & Latency Dashboards. |
| Which agent (trace name) costs the most? | Top 20 Use Cases (Trace) by Cost on the same dashboard. |
| Can an agent move to a cheaper model? | Gauge AI > AI Optimizer, which treats each trace name as an agent. See AI Optimizer. |
| A view of your own | Custom dashboards with widgets over traces or observations, broken down by trace name, observation name, model or tags. |
To score the quality of each agent's output, attach scores in code with score_current_trace(...) or set up LLM-as-a-Judge evaluators filtered by trace name. The managed Goal Accuracy and Topic Adherence templates suit agent outputs.
Framework integrations
- CrewAI.
from ants_platform.crewai import EventListenertraces crews, agents, tasks and tool calls. See CrewAI. - LangChain and LangGraph. Pass
CallbackHandler()inconfig={"callbacks": [handler]}and set the agent name with"metadata": {"ants_platform_agent_name": "..."}. LangGraph traces can also be viewed as a graph. See LangChain and Agent Graphs.
Common issues
| Symptom | Fix |
|---|---|
ValueError: agent_name is required in a worker | The worker runs in a thread without the orchestrator's context. Use contextvars.copy_context().run or asyncio.to_thread, or give the worker its own root observation with an agent_name. |
Warning Child span attempted to override parent agent_name | A nested observation passed a different agent_name. Remove it, or move that agent into its own trace. |
| Sub-agents show up as separate traces | The sub-agent ran outside the orchestrator's with block or in a thread without context. |
| Every sub-agent shows the orchestrator's name in AI Agent Center | Expected for nested sub-agents. Use separate traces if each agent needs its own entry. |
| Trace missing after a script ends | Call ants.flush() before the process exits. |