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:
tracer.trace_memory() /
tracer.record_memory() - a span_kind="memory" child span with the read/write operation in
its metadata, deliberately distinct from retrieval (see Span Kinds):
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 - the root span itself renders as a row on the Execution Timeline, with the wrapped call’s input/output, even when 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 context-local (aContextVar): asyncio tasks inherit a copy of their creator’s span stack, but a bare thread starts with an empty one. If you fan work out to a ThreadPoolExecutor (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
Every parameter except input works in both decorator and context-manager mode - the decorator
forwards sync, monitor, pattern_ids, agent_id, and span_kind to each call’s span the
same way the context manager does. input is the exception: in decorator mode the captured
function arguments always overwrite it.
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 judge scorers and scorer groups 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
Ingest-time checks run automatically on every root trace you send - the built-in checks plus every active detection pattern, with no flag and no dashboard setup - and matches become triage-ready signals. The opt-outs aretracer.trace(..., monitor=False), which skips every ingest-time check for that trace, and disabling the agent’s monitoring profile. Passing monitor=True with pattern_ids=[...] restricts detection for that trace to exactly those patterns instead of the full sweep. See Monitor for the full picture.

