By Sagar Shankaran, Founder of CallSphere
Master techniques for debugging multi-agent systems including interaction diagrams, distributed message tracing, replay tools, and correlation analysis. Turn opaque agent failures into diagnosable problems.
Key takeaways
Debugging a single agent is straightforward — you inspect its input, trace its reasoning, and check its output. Debugging a multi-agent system is fundamentally different because failures emerge from interactions between agents, not from any single agent in isolation.
Agent A produces a valid but suboptimal intermediate result. Agent B misinterprets it. Agent C compounds the error. The final output is wrong, but examining any individual agent shows no obvious bug. This is the core challenge: multi-agent bugs are systemic, not local.
The foundation of multi-agent debugging is capturing every interaction in a structured, queryable format. Every message, tool call, decision, and handoff needs a trace.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
flowchart TD
INPUT(["Task input"])
SUPER["Supervisor agent<br/>plans plus monitors"]
W1["Worker 1<br/>research"]
W2["Worker 2<br/>code"]
W3["Worker 3<br/>writing"]
CRITIC{"Output meets<br/>rubric?"}
REWORK["Rework or<br/>retry path"]
SHARED[("Shared scratchpad<br/>and memory")]
OUT(["Final result"])
INPUT --> SUPER
SUPER --> W1 --> CRITIC
SUPER --> W2 --> CRITIC
SUPER --> W3 --> CRITIC
W1 --> SHARED
W2 --> SHARED
W3 --> SHARED
SHARED --> SUPER
CRITIC -->|Pass| OUT
CRITIC -->|Fail| REWORK --> SUPER
style SUPER fill:#4f46e5,stroke:#4338ca,color:#fff
style CRITIC fill:#f59e0b,stroke:#d97706,color:#1f2937
style OUT fill:#059669,stroke:#047857,color:#fff
style SHARED fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
from dataclasses import dataclass, field
from datetime import datetime
from typing import Any
import uuid
import json
@dataclass
class TraceEvent:
trace_id: str
span_id: str
parent_span_id: str | None
agent_id: str
event_type: str # "message_sent", "tool_call", "decision", "handoff"
timestamp: str
data: dict[str, Any]
duration_ms: float | None = None
class MultiAgentTracer:
def __init__(self):
self.events: list[TraceEvent] = []
self._active_spans: dict[str, dict] = {}
def start_trace(self) -> str:
return str(uuid.uuid4())
def start_span(
self,
trace_id: str,
agent_id: str,
event_type: str,
parent_span_id: str | None = None,
data: dict | None = None,
) -> str:
span_id = str(uuid.uuid4())
self._active_spans[span_id] = {
"trace_id": trace_id,
"agent_id": agent_id,
"event_type": event_type,
"start_time": datetime.now(),
}
event = TraceEvent(
trace_id=trace_id,
span_id=span_id,
parent_span_id=parent_span_id,
agent_id=agent_id,
event_type=event_type,
timestamp=datetime.now().isoformat(),
data=data or {},
)
self.events.append(event)
return span_id
def end_span(self, span_id: str, result: dict | None = None):
span_info = self._active_spans.pop(span_id, None)
if span_info:
duration = (
datetime.now() - span_info["start_time"]
).total_seconds() * 1000
# Update the event with duration and result
for event in reversed(self.events):
if event.span_id == span_id:
event.duration_ms = duration
if result:
event.data["result"] = result
break
def get_trace(self, trace_id: str) -> list[TraceEvent]:
return [e for e in self.events if e.trace_id == trace_id]
def get_agent_events(self, agent_id: str) -> list[TraceEvent]:
return [e for e in self.events if e.agent_id == agent_id]
Once you have traces, visualize the interaction flow. This function generates a text-based sequence diagram from trace events — invaluable for understanding what happened in what order.
class InteractionDiagramGenerator:
def generate(self, events: list[TraceEvent]) -> str:
events_sorted = sorted(events, key=lambda e: e.timestamp)
agents = list(dict.fromkeys(e.agent_id for e in events_sorted))
lines = []
header = " | ".join(f"{a:^20}" for a in agents)
lines.append(header)
lines.append("-" * len(header))
for event in events_sorted:
agent_idx = agents.index(event.agent_id)
if event.event_type == "message_sent":
target = event.data.get("target_agent", "?")
if target in agents:
target_idx = agents.index(target)
arrow = self._draw_arrow(
agent_idx, target_idx, len(agents),
event.data.get("summary", event.event_type),
)
lines.append(arrow)
elif event.event_type == "decision":
marker = " " * (agent_idx * 23) + f"[{event.data.get('decision', '?')}]"
lines.append(marker)
elif event.event_type == "tool_call":
marker = (
" " * (agent_idx * 23)
+ f">> {event.data.get('tool', '?')}()"
)
lines.append(marker)
return "\n".join(lines)
def _draw_arrow(self, from_idx, to_idx, num_agents, label):
line = [" " * 20] * num_agents
if from_idx < to_idx:
line[from_idx] = f"{'─' * 5}>"
for i in range(from_idx + 1, to_idx):
line[i] = "─" * 20
line[to_idx] = f"> {label[:15]}"
else:
line[to_idx] = f"{label[:15]} <"
for i in range(to_idx + 1, from_idx):
line[i] = "─" * 20
line[from_idx] = f"<{'─' * 5}"
return " | ".join(line)
The most powerful debugging tool for multi-agent systems is the ability to replay an interaction with modifications. Capture the full state at each step, then replay with one agent's behavior changed to isolate the root cause.
@dataclass
class ReplayCheckpoint:
step: int
agent_id: str
input_state: dict
output_state: dict
decision: str
timestamp: str
class MultiAgentReplaySystem:
def __init__(self):
self.checkpoints: dict[str, list[ReplayCheckpoint]] = {}
def capture(
self, trace_id: str, checkpoint: ReplayCheckpoint
):
if trace_id not in self.checkpoints:
self.checkpoints[trace_id] = []
self.checkpoints[trace_id].append(checkpoint)
def replay(
self,
trace_id: str,
agent_overrides: dict[str, callable] | None = None,
) -> list[dict]:
"""
Replay a trace, optionally replacing specific agent
behaviors to test counterfactuals.
"""
checkpoints = self.checkpoints.get(trace_id, [])
if not checkpoints:
raise ValueError(f"No checkpoints for trace {trace_id}")
overrides = agent_overrides or {}
replay_results = []
current_state = checkpoints[0].input_state.copy()
for cp in checkpoints:
if cp.agent_id in overrides:
# Use the override function instead of recorded behavior
override_fn = overrides[cp.agent_id]
new_output = override_fn(current_state)
replay_results.append({
"step": cp.step,
"agent": cp.agent_id,
"original_output": cp.output_state,
"replayed_output": new_output,
"diverged": new_output != cp.output_state,
})
current_state.update(new_output)
else:
replay_results.append({
"step": cp.step,
"agent": cp.agent_id,
"original_output": cp.output_state,
"replayed_output": cp.output_state,
"diverged": False,
})
current_state.update(cp.output_state)
return replay_results
def find_divergence_point(
self, trace_id: str, agent_overrides: dict
) -> dict | None:
results = self.replay(trace_id, agent_overrides)
for r in results:
if r["diverged"]:
return r
return None
When a multi-agent system fails intermittently, you need statistical analysis to find the root cause. Correlation analysis identifies which agents or conditions are most associated with failures.
class FailureCorrelationAnalyzer:
def __init__(self):
self.traces: list[dict] = []
def add_trace_summary(self, summary: dict):
"""
summary includes: trace_id, success (bool),
agents_involved (list), conditions (dict of features)
"""
self.traces.append(summary)
def analyze_agent_correlation(self) -> list[dict]:
agent_stats: dict[str, dict] = {}
for trace in self.traces:
for agent_id in trace["agents_involved"]:
if agent_id not in agent_stats:
agent_stats[agent_id] = {
"total": 0, "failures": 0
}
agent_stats[agent_id]["total"] += 1
if not trace["success"]:
agent_stats[agent_id]["failures"] += 1
results = []
total_traces = len(self.traces)
total_failures = sum(
1 for t in self.traces if not t["success"]
)
base_failure_rate = (
total_failures / total_traces if total_traces else 0
)
for agent_id, stats in agent_stats.items():
agent_failure_rate = (
stats["failures"] / stats["total"]
if stats["total"] else 0
)
lift = (
agent_failure_rate / base_failure_rate
if base_failure_rate else 0
)
results.append({
"agent_id": agent_id,
"failure_rate": round(agent_failure_rate, 3),
"base_rate": round(base_failure_rate, 3),
"lift": round(lift, 2),
"sample_size": stats["total"],
})
results.sort(key=lambda x: x["lift"], reverse=True)
return results
A lift greater than 1.0 means that agent is involved in failures more often than the baseline. A lift of 2.5 means traces involving that agent fail 2.5x more often than average — a strong signal that the agent is a root cause contributor.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
In practice, tracing adds 1-3% overhead when using asynchronous log writes and in-memory buffering. The trace data itself is small — typically under 1KB per event. The cost of not having traces (hours of guessing at root causes) far exceeds the cost of collecting them. For very high-throughput systems, sample traces at 10-20% rather than tracing every interaction.
Capture timestamps with microsecond precision and include queue depths and wait times in your trace data. Replay the trace with artificial delays injected to simulate load conditions. Most timing bugs stem from an agent taking longer than expected, causing a downstream agent to time out or process stale data. The correlation analyzer can reveal which agent latency spikes correlate with failures.
Yes, and you should. Map each agent invocation to a span and use parent-child span relationships to represent the agent hierarchy. OpenTelemetry provides the instrumentation standard. The custom tracer in this article covers the agent-specific semantics (decisions, handoffs, tool calls) that generic tracing tools lack, but the underlying transport and visualization should use established infrastructure.
#Debugging #MultiAgentSystems #Observability #Tracing #Python #AgenticAI #LearnAI #AIEngineering

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
How we built a fault-tolerant HVAC emergency triage and tech-dispatch platform on Kubernetes — three-tier CQRS, 11 micro-agents on the OpenAI Agents SDK + LangGraph, NATS JetStream, DTMF/SMS/WebSocket acceptance, circuit breakers, and an evaluation pipeline that catches regressions before they wake a tech at 3 AM.
Five proven multi-agent architecture patterns built on A2A — orchestrator, peer mesh, hub-and-spoke, marketplace, and tiered specialist.
How to actually observe a WebSocket fleet: ping/pong heartbeats, Prometheus metrics that matter, dead-man switches, and the alerts that fire before customers notice.
Langgraph multi-agent supervisor handoffs docs: the supervisor pattern in LangGraph for coordinating specialist agents, with full code, an eval pipeline that scores routing accuracy, and the failure modes to watch for.
How the modern agent eval stack actually flows: instrument, trace, dataset, evaluator, score, CI gate. The full pipeline that keeps agents from regressing.
Handoffs done right — when one agent should hand control to another, how to preserve context, and how to evaluate the handoff decision itself.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI