Tracing and Observing What Your Agent Did
Why Observability Matters More for Agents Than a Single Call
A single client.responses.create() call is easy to inspect after the fact — Unit 2, Lesson 3 covered reading response.output directly, and every unit since has built on that same habit of checking exactly what a response contains. A multi-step agent run involving several internal model calls, a handoff (Lesson 4) to a different agent, and one or more tool calls (Lesson 3) is considerably harder to reconstruct after the fact from a single final answer alone — which specific agent handled which part of the request, which tools were called with what arguments, and where a run spent most of its time or cost all become questions a single result.final_output string can't answer. Tracing is the Agents SDK's built-in mechanism for recording exactly this kind of detail automatically, without requiring you to build your own logging for every run the way earlier units did by hand.
Tracing Happens Automatically
Every run through Runner.run() or Runner.run_sync() is traced by default, recording each step of the run — which agent handled it, which tools were called and with what arguments, and any handoffs that occurred.
from agents import Agent, Runner
support_agent = Agent(
name="Support Agent",
instructions="You help customers with account questions.",
model="gpt-5.6-terra",
)
result = Runner.run_sync(support_agent, "What's the status of my last order?")
Note: Whether tracing is on by default, where trace data is sent or stored, and how to view it can vary by SDK version and by your platform account configuration. Confirm the current tracing behavior and dashboard location against the current official documentation.
This is a meaningful contrast with everything built directly against client.responses.create() in Units 1 through 10: getting equivalent visibility there required writing your own logging, exactly as Unit 8, Lesson 6's log_tool_selection_for_test_questions() and Unit 9, Lesson 5's summarize_tool_usage() did by hand, function by function, project by project. Tracing gives you this same kind of visibility automatically, for every agent run, without custom code.
Naming Traces for a Multi-Step Workflow
A group of related agent runs — several steps in a single larger workflow — can be tagged with a shared trace name, making it possible to view them together rather than as disconnected individual runs.
from agents import trace
with trace("Customer Support Session"):
triage_result = Runner.run_sync(triage_agent, "I was charged twice for my order.")
followup_result = Runner.run_sync(triage_result.last_agent, "Can you also check my shipping address?")
Note: The exact API for grouping related runs under a shared trace (the
trace()context manager shown here, or an equivalent) can vary by SDK version. Confirm the current interface against the current official documentation.
Grouping runs this way matters specifically for a multi-turn interaction spanning more than one Runner.run_sync() call — without it, each call would appear as an isolated, disconnected trace, making it harder to reconstruct the full arc of a single customer's session from triage through to a specialist's follow-up response.
What Tracing Is Useful For
Tracing addresses several practical needs that this course previously required custom code to satisfy at all: debugging a specific run that produced an unexpected result (was the wrong specialist agent selected? did a tool receive the arguments you expected?), understanding routing behavior in aggregate (following Unit 8, Lesson 6's guidance to check whether test questions consistently trigger the intended tool or, here, the intended handoff), and monitoring cost and latency across a system where several different agents and tool calls contribute to a single interaction's overall cost, extending the per-tool cost-awareness Unit 9, Lesson 5 introduced to a full multi-agent system.
Tracing and Privacy
Because a trace can capture the full content of inputs, tool arguments, and outputs across a run, the same data-handling care this course has applied throughout — not logging sensitive information insecurely, curating what a tool's error messages reveal (Unit 8, Lesson 5) — extends directly to whatever a trace records.
from agents import function_tool
@function_tool
def look_up_account(account_id: str, ssn_last_four: str) -> str:
"""Look up account details for verification purposes.
Args:
account_id: The account identifier.
ssn_last_four: Last four digits of SSN for identity verification.
"""
return "Account verified."
Note: Whether sensitive tool arguments (such as
ssn_last_fourabove) are captured in trace data by default, and how to exclude or redact specific fields from tracing, can vary by SDK version and platform configuration. Confirm the current data-handling behavior against the current official documentation and your organization's data handling requirements before passing genuinely sensitive values through a traced tool call.
This is worth checking deliberately before relying on tracing in a system that handles sensitive customer data: automatic observability is valuable, but it shouldn't come at the cost of accidentally retaining sensitive information somewhere it wasn't intended to persist.
Tracing as a Complement to, Not a Replacement for, Your Own Testing
Tracing shows you what actually happened during real runs; it does not replace the dependency-injection testing pattern this course has used throughout (fake clients, fake response objects, assert-based tests with no real API calls) for verifying that your own logic — a tool's business rules, a guardrail's decision logic, a triage agent's routing — behaves correctly before it's ever exercised by a real run at all.
def test_triage_routes_billing_question_to_billing_agent():
# A fake, deterministic stand-in for what a real Runner.run_sync() call
# would report, used to verify routing-adjacent application logic without
# depending on a real (and non-deterministic) agent run.
fake_result = {"last_agent_name": "Billing Agent", "final_output": "Refund processed."}
assert fake_result["last_agent_name"] == "Billing Agent"
print("PASS: routing logic under test correctly identifies the billing agent as having handled the request")
test_triage_routes_billing_question_to_billing_agent()
Tracing and this kind of test serve different purposes and are both worth having: tests catch a logic error before it ever reaches a real run, while tracing reveals how the system actually behaved once it's genuinely running against real inputs — neither one substitutes for the other.
Common Mistakes
Building custom logging for agent behavior that tracing already provides automatically, duplicating effort this course previously required (Unit 8, Lesson 6; Unit 9, Lesson 5) but which the Agents SDK now handles by default.
Passing genuinely sensitive values through a traced tool call without checking what tracing actually captures and retains, risking sensitive data persisting somewhere it wasn't intended to.
Treating tracing as a substitute for your own tests, when tracing shows what happened during real runs and tests verify logic correctness independent of any real run — both are needed, for different reasons.
Running related multi-step interactions without grouping them under a shared trace, making it harder to reconstruct a full session's arc from a set of disconnected individual traces.
Best Practices
Rely on tracing for debugging and monitoring a live agent system, rather than rebuilding the kind of custom logging earlier units required by hand.
Group related runs under a shared trace name for any multi-step workflow spanning more than one Runner.run() call, so the full interaction can be reviewed together.
Check what data tracing actually captures before relying on it in a system handling sensitive information, applying the same data-handling care established throughout this course to whatever a trace persists.
Keep testing your own agent-adjacent logic with fakes and dependency injection, using tracing to observe real runs and tests to verify correctness before those runs ever happen.