Skip to main content
The 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 as input, the return value as output, and wall-clock time as latencyMs.
Works with async functions too:

Context manager

Use when you need to set input/output manually or record tool calls mid-span.
To time a tool call and capture its failures automatically, wrap the execution itself with tracer.trace_tool_call() instead of reporting it after the fact:
An exception escaping the block records the call as failed (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 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

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

Use session_id to link traces from the same user conversation:
Traces sent without a 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 the error field and re-raised. The trace is still submitted:
To set an error manually in context manager mode:

Flushing

Traces are sent in a background thread. Call flush() before process exit in scripts:

Evaluating a trace

Score a previously recorded trace against a dataset without re-running the agent:
This is for scoring a trace you already recorded, standalone. To capture a trace while an evaluation run is scoring your agent, so the run’s own results show a full Execution Timeline and not just a rating, see the next section instead.

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 via tracer.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.

Limits