AI agents

A Trace for a Multi-Agent Run: What to Record Across Hops

Antonio Evans9 min read

A clean left-to-right ladder diagram of a multi-agent run: a parent task box at the top, three subagent call boxes below it connected by downward arrows labeled 'delegate', and a merged result box at the bottom.
Key points

Key points

  1. 1Record three things at every hop: identity (trace_id, span_id, parent_span_id), payload (prompt hash, model ID, parameters) and outcome (tokens, latency, error code, stop reason).
  2. 2Propagate one trace_id from the parent task into every subagent call using W3C Trace Context headers, so a failed run can be rebuilt as one connected tree instead of orphaned spans.
  3. 3Store the full prompt text by hash reference and keep only the hash plus model parameters inline, which keeps logs small and makes replay exact.
  4. 4Log the merged result separately from the subagent outputs, including conflicts and the stop reason, because synthesis is where silent wrong answers appear.

Why a multi-agent trace breaks without a schema

A multi-agent system splits one user request across an orchestrator and several specialist agents, each with its own context window and tools. The orchestrator turns an input into a set of subtasks, routes each to a worker, and combines the local results into a final output Arize's orchestrator-worker deep dive. That is exactly why a failure is hard to place: the wrong answer may come from a bad subtask split, a hallucinated worker result, a dropped tool response, or a synthesis step that merged the wrong pieces.

Anthropic's engineering writeup on its research system makes the cost and behavior concrete: the multi-agent configuration beat a single-agent baseline by 90.2 percent on an internal research eval, but used about 15 times the tokens of a chat interaction, and token usage alone explained 80 percent of performance variance on the BrowseComp benchmark Anthropic's multi-agent research system. When a run burns that much budget and still fails, "look at the logs" is not enough. You need a per-hop record.

The failure signature in a tracing backend is easy to recognize. Look for root spans with no parent that appear mid-sequence, trace ID discontinuities between the gateway and a downstream agent, tool call spans that are roots instead of children of an LLM span, and timing gaps with no span explaining the gap TianPan on tracing across agent boundaries. Each of those symptoms means one delegation boundary lost its context.

Identity: one trace, many spans

The rule is simple. Generate one trace_id when the parent task starts. Every step in that run, including the orchestrator's own reasoning, each subagent call, each tool call fetched by a subagent, and the final merge, gets a fresh span_id and a parent_span_id pointing to the span that caused it.

The W3C Trace Context specification standardizes the headers and value format for this, and the traceparent field has four parts: version, trace-id, parent-id and trace-flags the W3C Trace Context recommendation. OpenTelemetry's default propagator uses those W3C headers, and its context propagation model is what makes a trace causal across process and network boundaries OpenTelemetry context propagation.

For agents this matters because the W3C model assumes a synchronous request that carries a header. Agents break that assumption in three ways: they hand off work asynchronously through queues, they call external tool servers that do not automatically receive headers, and framework internals run tool loops that expose no hooks TianPan on tracing across agent boundaries. The fix is explicit injection and extraction: capture the active span context before crossing any boundary, serialize it into the message payload or metadata, and reattach it on the receiving side before creating new spans.

The A2A project has an open proposal for exactly this, carrying traceparent and tracestate in message metadata and reusing existing OTel GenAI attribute names such as gen_ai.agent.name and gen_ai.operation.name, plus three A2A-specific attributes (a2a.task.id, a2a.task.state, a2a.method) the A2A tracing proposal. The proposal also notes that everyone currently invents their own attribute names, which is why dashboards break when you switch tools. Adopt the OTel GenAI names where they exist and add only what your protocol needs.

A minimal header set to propagate on every hop:

traceparent: 00-<32 hex trace_id>-<16 hex span_id>-01
tracestate: tenant=<tenant_id>,workflow=<workflow_version>
x-quanta-task-id: <parent task id>
x-quanta-agent-name: <target agent name>

Keep baggage out of the default path unless you need it. The A2A proposal specifically excludes baggage from its first version because baggage propagates in plaintext to every downstream service, which is a data leakage vector in multi-tenant deployments the A2A tracing proposal.

What to record at the parent task

The parent task span is the root of the tree and sets the run's identity. Record it once, before any delegation, and write it even if the run crashes early.

FieldExampleWhy it matters for replay
trace_id4bf92f3577b34da6a3ce929d0e0e4736Joins every hop in the run
task_idtask_01HZX...Your own ID for the run, stable across retries
goal"Find all board members of IT companies in the S&P 500"Human-readable intent; the input hash cannot tell you what was asked
input_hashsha256:9f86d081...Detects a changed input between a failed run and a replay
prompt_id and prompt_versionresearch_lead / 3Tells you which orchestration prompt drove the split
model_id and model_paramsmodel identifier, temperature, max tokensTemperature and token limits change output
started_at, ended_atISO 8601 with timezoneCorrelate with queue latency and outages
tokens_in, tokens_out, cost_usdnumbersThe 15x token multiplier makes cost a debugging signal
budgettoken or dollar ceilingExplains why a run was cut short

Log the goal text and not just the hash, because an input hash proves identity but tells a reviewer nothing. Log the budget separately from the spent amount so you can tell a graceful stop from a crash.

What to record at each subagent call

Each delegation creates a child span under the orchestrator span that made the decision. This is the hop where most detail belongs, because a worker is the unit you will replay.

FieldWhat to capture
span_id, parent_span_idLinks the worker to the delegation that caused it
agent_name, agent_roleWhich specialist was chosen
delegation_reasonThe orchestrator's stated reason for this subtask, stored as text
task_description_hashHash of the exact prompt or message handed to the worker
prompt_id, prompt_version, model_id, model_paramsThe worker's own prompt and model configuration
tool_callsTool name, sanitized input hash, output hash, status
attempt and retry_reason1-based attempt number and why a retry fired
error_codeFramework, provider, or tool error string on failure
latency_msWall clock for this hop, measured from send to result
tokens_in, tokens_outPer-hop token counts
stop_reasonWhy the worker stopped: normal end, max tokens, tool error, timeout
output_hash and output_refHash of the returned text plus a pointer to the stored full text

Two fields earn their place more than the rest. delegation_reason is what lets a reviewer ask why the orchestrator sent a task to the billing agent at all, which is the first question in a misrouting failure. The A2A proposal points out that orchestrators can misclassify a task and that context overflow is common once four or more workers accumulate output, so a written reason per delegation is the cheapest guard against both the A2A tracing proposal. stop_reason separated from error_code lets you distinguish a clean but truncated result from a hard failure, and truncated results are a frequent cause of confidently wrong merged answers.

Tool inputs and outputs are the riskiest fields to log because they carry user data, credentials, and the contents of retrieved documents. Log a hash and a reference, and store the full payload in a separate store with its own retention and access rules. A hash gives you equality checks, and a reference gives you a lookup when someone with permission needs to see the text.

What to record at the merged result

The merge span is a child of the parent task and a sibling of the subagent spans. It records what the orchestrator chose, not what the workers produced.

  • selected_output_refs: the list of span IDs whose outputs went into the final answer
  • excluded_output_refs and exclusion_reason: which worker results were dropped and why
  • conflicts: pairs of worker results that disagreed, with a short description of the disagreement
  • output_hash and output_ref: the exact final text that was returned to the user
  • merge_prompt_id, merge_model_id, merge_model_params: the synthesis step is its own model call and needs its own configuration
  • stop_reason: why the run ended, for example orchestration complete, token budget exhausted, human override, or error escalation
  • human_review: whether a person approved, edited, or rejected the result
  • tokens_in, tokens_out: merge tokens, which grow with the number of workers

Conflicts deserve a structured field, not a sentence inside a free-text summary. The Beam AI analysis of orchestration patterns notes that when parallel agents disagree the system needs an explicit conflict resolution strategy rather than a generic summarization step, and that LLM-based synthesis can invent a consensus that does not exist in the underlying data Beam AI on orchestration patterns. Recording the conflicting pair and the resolution rule turns a silent wrong answer into a queryable event.

Propagating context through the run

Context propagation has two halves: context is the object holding the trace and span identifiers, and propagation is the mechanism that serializes it and moves it across a boundary OpenTelemetry context propagation. For agent systems, the practical rule is to serialize the context yourself at every point that is not a plain HTTP request.

from opentelemetry import trace
from opentelemetry.propagate import inject, extract

tracer = trace.get_tracer("multi_agent")

def delegate_to_subagent(task, payload, queue):
    with tracer.start_as_current_span("invoke_agent " + task["agent_name"]) as span:
        span.set_attribute("gen_ai.agent.name", task["agent_name"])
        span.set_attribute("a2a.task.id", task["task_id"])
        span.set_attribute("delegation.reason", task["reason"])
        carrier = {}
        inject(carrier)              # writes traceparent from the active span
        message = {"task": task, "payload": payload, "trace": carrier}
        queue.put(message)

def worker_loop(queue):
    while True:
        message = queue.get()
        ctx = extract(message["trace"])   # reattach before creating spans
        with tracer.start_as_current_span("process_message", context=ctx) as span:
            result = run_task(message["task"], message["payload"])
            span.set_attribute("stop.reason", result["stop_reason"])
            span.set_attribute("output.hash", result["output_hash"])

The inject call writes the current traceparent into the carrier dictionary, and extract rebuilds the parent context on the receiving side so the new span attaches under it instead of starting a fresh trace. Because a carrier is just a dictionary, the same pattern works for message queues, database rows, or Redis keys TianPan on tracing across agent boundaries.

For agent-to-agent traffic, the W3C format itself is short enough to carry in metadata: version, trace-id, parent-id, trace-flags, as in the example 00-a0892f3577b34da6a3ce929d0e0e4736-f03067aa0ba902b7-01 OpenTelemetry context propagation. Record the full traceparent string on the receiving span as an attribute as well as in headers, so you can still join traces when a message is replayed from a dead-letter queue with headers stripped.

Redaction and retention

A trace that contains every prompt is a privacy liability. Three rules cover most cases.

First, never log raw credentials, API keys, or full request headers. Redact at the logging call, not at the storage layer, so a mistake in one sink cannot leak. Second, treat full prompt and tool output text as a separate store keyed by hash, with its own retention window and access control, because multi-tenant systems are where cross-tenant leakage actually happens. Third, keep the trace record itself long enough to replay a regression and short enough to satisfy your data policy: identity, hashes, timings and error codes age well, while raw text does not.

A workable split is to keep the index (trace_id, span_id, parent_span_id, hashes, model IDs, timings, tokens, error codes, stop reasons) for the full period you support incident review, and keep payload text for a much shorter window. When you redact a payload, record that the redaction happened, so a later reader knows the missing text is policy and not a logging bug.

Replaying a failed run

Replay is the payoff of the schema, and it works because a hash is a stable pointer. To reconstruct and re-run a failed hop:

  1. Start from the trace_id and pull all spans ordered by parent_span_id.
  2. Find the first span where stop_reason or error_code departs from a normal completion.
  3. Fetch the stored text behind that span's task_description_hash and its prompt_id and model_id.
  4. Check the model_params before re-running, because a changed temperature produces a different answer and invalidates the comparison.
  5. Re-run only that span with the original context, then compare output_hash against the recorded value and diff the merged result.

Replay against a fixed model configuration is a fair test of a prompt change. Replay against a different model configuration is an experiment, not a regression check, so label it that way in the run record. Keep both hashes in the new span so the comparison is auditable.

This is also where the architecture choice matters. Multi-agent systems earn their cost on tasks that decompose into independent parallel threads, and are a poor fit for tightly interdependent work where workers need to see each other's decisions Anthropic's multi-agent research system. If your orchestrator decomposes badly, no trace will fix the design. The trace will, however, show you the bad split quickly, because delegation_reason and excluded_output_refs sit next to each other in the record.

When runs fan out across many models and providers, the per-hop model ID becomes a routing signal as well as a debugging field. Teams that run several families of models through one endpoint can capture a single model_id string per hop and keep the rest of the schema unchanged; Quanta Inference (https://quantaintelligence.ai/inference) is one way to put text, image, video, audio and embedding models behind a single OpenAI-compatible API key, which keeps the identifier recorded at each hop consistent.

FAQ

FAQ

What is the minimum set of fields for a usable multi-agent trace?

Identity, payload and outcome, at every hop. Identity is a trace_id generated once with a new span_id and a parent_span_id per step. Payload is the hash of the exact prompt or message plus the model identifier and parameters, and outcome is tokens, latency, error code and stop reason.

Should I log full prompts inside the trace record?

No. Log a hash of the exact prompt and a pointer to the full text in a separate store with its own retention and access rules. Inline prompts make the trace record large, expensive to search and a privacy risk, while a hash still proves two runs received identical input and lets you fetch the text when someone with permission needs it.

Why does my trace show separate trees for one user request?

The most common cause is a delegation boundary that did not carry context, so the downstream agent started a new root span instead of a child span. This happens with message queues, tool servers and framework internals that do not propagate W3C headers automatically TianPan on tracing across agent boundaries. Inject the traceparent value into the message payload on the sender side and extract it before creating any span on the receiving side.

How do I debug a merged answer that is wrong even though every worker succeeded?

Open the merge span and read selected_output_refs, excluded_output_refs, conflicts and stop_reason. A wrong merged answer usually means the synthesis step dropped or misweighted a correct worker result, or invented agreement where the workers actually disagreed Beam AI on orchestration patterns. Then replay the merge span alone with the recorded worker outputs; if the answer is still wrong, the merge prompt is the defect, not the workers.

How long should I keep a trace?

Keep the identity and outcome fields, such as trace IDs, hashes, timings, tokens, error codes and stop reasons, for the full period you support incident review. Keep the raw prompt and tool output text for a much shorter window. Redaction should happen at the logging call, and the record should note when a payload was redacted so a later reader knows the missing text followed policy rather than a broken logger.

Sources

Sources

  1. Orchestrator-Worker Agents: Comparing Agent Frameworks
  2. How we built our multi-agent research system - Anthropic
  3. Distributed Tracing Across Agent Service Boundaries - TianPan.co
  4. Trace Context - W3C
  5. Context propagation - OpenTelemetry
  6. Proposal: Distributed Tracing Semantic Conventions for A2A #2103
  7. 6 Multi-Agent Orchestration Patterns for Production (2026) - Beam AI
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles