Tracer captures your agent’s inputs, outputs, latency, and tool calls, with a single decorator or context manager. Traces appear in the Live Traces tab and can be evaluated against your test datasets.
Running self-hosted? If your app is already instrumented with OpenTelemetry, you don’t need this SDK to get traces in at all - point your OTel exporter straight at the engine. See Connect via OpenTelemetry.
Decorator
The simplest usage. AgentX captures function arguments asinput, the return value as output, and wall-clock time as latencyMs.
Context manager
Use when you need to set input/output manually or record tool calls mid-span.tracer.trace_tool_call() instead of reporting it after the fact:
success=False plus the error text - what Monitor’s built-in “Tool failure” check reads) and then propagates unchanged. To set the outcome yourself, use tracer.record_tool_call(name, input=..., output=..., success=False, error="...").
_TraceSpan attributes and methods
On a root span,
sync=True covers the whole tree: child spans recorded inside the block
(tool calls, LLM calls) are drained before the root is sent, so a read immediately after the
block sees every span. Child-only spans keep their async fire-and-forget behavior;
client.tracer.flush() remains available for manual control.Decorator vs. context manager
Both forms capture a usable trace, including at least one Execution Timeline step synthesized from the wrapped call’s input/output if nothing more granular was recorded. They differ in what you get back:
If you don’t need the id back, the decorator is simplest. If you do, use the context manager with
sync=True. It adds one blocking network round-trip (typically well under a second) in exchange for the id being ready the moment the with block exits.
Parallel work across threads
The active span is tracked per-thread. If you fan work out to aThreadPoolExecutor (or any other thread) from inside a tracer.trace(...) block, worker threads don’t automatically see the span opened on the calling thread, so each LLM or tool call inside them would start its own independent trace instead of landing as a step on the parent span.
Wrap each worker’s body in tracer.use_span(span) to attach it to the parent span for the duration of that block:
use_span is safe to call concurrently from multiple threads for the same span. Each thread pushes and pops on its own stack, so parallel workers don’t interfere with each other.
tracer.trace() parameters
Framework examples
The decorator shown here records one trace per call - simple and dependable. For the full
execution tree (graph nodes, every LLM call, every tool call as its own timed span), use the
per-framework integrations instead: a LangChain/LangGraph
callback handler, an OpenAI Agents trace processor, a CrewAI/AutoGen/ADK hook, or
MLflow OTLP export. Both approaches share the same tracer and
the same dashboard views.
Session grouping
Usesession_id to link traces from the same user conversation:
session_id each get their own auto-generated session, so passing it is only about grouping - never required. On self-host, sessions are a first-class surface: a Sessions view lists each conversation with turn counts and a conversation-level coherence score, and online evaluators can judge whole sessions (scope="session") once they go idle.
Error handling
Exceptions inside a traced function are captured as theerror field and re-raised. The trace is still submitted:
Flushing
Traces are sent in a background thread. Callflush() before process exit in scripts:
Evaluating a trace
Score a previously recorded trace against a dataset without re-running the agent:Linking a trace to an evaluation result
client.evaluations.run(...).execute(your_fn) calls your_fn once per test case and scores whatever it returns. Wrap the call inside your_fn with tracer.trace(..., sync=True) and return span.trace_id alongside your output. The eval result gets linked to the real trace, so its row in the dashboard shows a “View trace” action opening the full Execution Timeline, not just the score:
trace_id is a plain top-level key in the dict your function returns, in the same place as output and metadata. It works the same way if your function returns an EvaluationResult directly (set trace_id=span.trace_id on it) instead of a dict.
sync=True blocks until the trace is ingested (typically well under a second) before your function returns. For a high test-case count where that latency adds up, keep tracing async (the default) and skip the timeline for that run. You’ll still get the score.Monitor
Every trace you send can also be checked against detection patterns and turned into a triage-ready signal, either immediately viatracer.trace(..., monitor=True, pattern_ids=[...]) with no dashboard setup, or automatically for every trace once a monitoring profile is enabled for the agent in Governance > Observe > Agents. See Monitor for the full picture.

