Telemetry
Trace agent runs, model calls, and tool executions.
The SDK records what happens during a run — every model call, tool execution, and agent loop turn — as a tree of spans. You decide where the spans go: export them to OpenTelemetry, send them to an observability vendor, or print them to the terminal.
Telemetry lives in the ai.experimental_telemetry module. It is experimental
and new in 0.4.0: it is not part of the stable API and may change or be
removed.
Turn on tracing
The SDK instruments itself. Every agent run, loop turn, model call, tool execution, and hook suspension produces a span, with no changes to your code. Nothing is reported until you register an adapter.
Each span is a plain Pydantic record: a name, timestamps, a parent id, typed data
describing the work, events, and an error field when the work failed.
There is a built-in OpenTelemetry adapter:
uv add "ai[otel]"import ai
from ai.experimental_telemetry import otel
ai.experimental_telemetry.register(otel.OtelAdapter())
agent = ai.Agent(tools=[get_weather])
async with agent.run(model, messages) as run:
async for event in run:
...register(otel.OtelAdapter()) registers the adapter that turns framework's
spans into OpenTelemetry spans. OtelAdapter uses the global tracer provider
unless you pass one.
Note
See OpenTelemetry ceremony documentation page to configure the endpoint for your traces to be submitted to.
Open your own spans
You can create your own spans that will automatically nest with SDK's spans and interact with adapters.
import ai
async with ai.experimental_telemetry.span("retrieval") as sp:
sp.set_attrs(query=query)
docs = await search(query)
sp.set_attrs(count=len(docs))Attach attributes with sp.set_attrs(...). Attribute names that are not
valid Python keywords, such as the dotted names many trace viewers use, go
in a positional mapping:
sp.set_attrs({"output.value": title}, model="claude-haiku-4.5")Record a milestone inside a span with sp.add_event(name) — it becomes a
timestamped event on the span, like the first_token event the SDK records on
model calls.
If the block raises, the span records the error and re-raises. You keep your
try/except logic; the trace shows where the failure happened.
Write an adapter
You can connect a vendor SDK directly by writing an adapter. The @adapter
decorator builds one from a single function using a generator trick similar
to pytest fixtures or FastAPI lifecycles.
import ai
@ai.experimental_telemetry.adapter
async def vendor(span):
with sdk.start_span(span.name) as v: # create a span before the loop
while (ev := (yield)) is not None: # process events coming from span.add_event()
v.log_event(ev.name, timestamp=ev.time_ns)
if span.error is not None: # finish the span after the loop
v.set_error(span.error.message)
v.update(output=span.data.model_dump(mode="json"))
ai.experimental_telemetry.register(vendor) # register custom adapterThe function runs once per span:
- Code before the loop runs when the span starts.
- Each span event submitted via
.add_event()resumes theyieldwith aSpanEvent, as it happens. - Span end resumes the loop with
None; the code after the loop runs withspan.datafully populated and the end timestamp set.
Return before the first yield to skip a span.
@adapter can also decorate a class. For a class, the async generator trick should
be put in __call__.
Telemetry never interferes with the run: an adapter that raises is logged and skipped. You can register several adapters at once, and they operate independently.
Next steps
The module also has a low-level API, that can be useful for instrumenting complicated
cases such as durable execution. It provides more control over adapter functionality and
spans' lifecycles. See the
ai.experimental_telemetry reference for the
full API, the span data types, and the durable execution patterns.