Traces and spans: the anatomy of one agent run

3 min read

Short answer

A span is one operation: it has a name, a start and end time, its own span ID and, unless it's the root, its parent's span ID. A trace is every span that shares one trace ID, such as all the work in one agent run. The parent IDs turn those spans into a tree with a single root.

On this page

My demo agent got one instruction: pay the vendor 0.25 ETH, then withdraw 1 ETH from the vault. Jaeger shows that run as 13 spans. Every one of them carries the trace ID ad2bb276e4162eb8240ae6da1418c7a8.

An AI agent run as an OpenTelemetry span tree: 13 spans share one trace ID, each child points at its parent's span ID, and hashspan's send and confirm spans sit under each tool call.

A span is one operation, a trace is all of them

Span Trace
What it is One unit of work: an LLM call, a tool call, a send The path of a request, here one agent run
ID Span ID, 8 bytes (16 hex characters) Trace ID, 16 bytes (32 hex characters)
Holds Name, times, attributes, events, links, status, parent ID Nothing of its own
In this run send 31337 is 7d4ae38f72dcc62f 13 spans with the ID above

The last column is the part people miss. A trace has no record of its own. Your backend collects the spans with the same trace ID and rebuilds the tree from their parent IDs (OpenTelemetry traces).

Parent IDs make the tree

Every span names its parent’s span ID, except the root, which has none. Here is the real run, with each span’s ID:

invoke_agent scripted-model 8334389a156b7405 (root, no parent)
├─ step 1 85060f6a3968c350
│ ├─ chat scripted-model 663c7fb4bbfbe7c9
│ └─ execute_tool pay_vendor a2b40d3b18065b98
│ ├─ send 31337 7d4ae38f72dcc62f
│ └─ confirm 31337 c16b039d3ac7c712 success
├─ step 2 5659d93628e2ca4a
│ ├─ chat scripted-model 88775ded822b3845
│ └─ execute_tool withdraw_from_vault c35dd3745ec1bb8a
│ ├─ send 31337 316f3f59999a43ec
│ └─ confirm 31337 0014335169b58fe0 reverted
└─ step 3 d8954f629a11cc56
└─ chat scripted-model 01f25c86b2bab452

The AI SDK emitted the agent, step, chat and tool spans. The invoke_agent, chat and execute_tool names come from the GenAI semantic conventions, which are still in Development status. hashspan added send and confirm. Because they share the trace ID, the reverted withdrawal sits right under the tool call that caused it. This post shows the same run without them.

How does a span get its parent?

From the active span. Start a span while another one is active, and the SDK gives it the same trace ID and records the active span as its parent.

import { trace } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
const exporter = new InMemorySpanExporter();
new NodeTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] }).register();
const tracer = trace.getTracer('learn-demo');
await tracer.startActiveSpan('invoke_agent treasury-agent', async (agent) => {
await tracer.startActiveSpan('execute_tool pay_vendor', async (tool) => {
tracer.startSpan('send 31337').end(); // a child of the active span: execute_tool
tool.end();
});
agent.end();
});
for (const s of exporter.getFinishedSpans()) {
const { traceId, spanId } = s.spanContext();
console.log(s.name.padEnd(28), traceId, spanId, s.parentSpanContext?.spanId ?? '(root)');
}
send 31337 e9de5ec038380ef8d93baa1114cacdeb 54cd9c6148d52556 d35e923db7496ee1
execute_tool pay_vendor e9de5ec038380ef8d93baa1114cacdeb d35e923db7496ee1 242f4a55addcdbba
invoke_agent treasury-agent e9de5ec038380ef8d93baa1114cacdeb 242f4a55addcdbba (root)

One trace ID, three span IDs, and each child points at its parent. Spans finish child first, so the root prints last. register() also installs a context manager, which keeps the active span across await.

FAQ

What is the difference between a trace ID and a span ID?

A trace ID is 16 bytes, written as 32 hex characters, and is the same on every span in the trace. A span ID is 8 bytes, written as 16 hex characters, and belongs to one span. A child records its parent by the parent's span ID.

What is a span in OpenTelemetry?

A unit of work, such as one LLM call, one tool call or sending one transaction. It has a name, start and end times, a span context with its IDs, and optional attributes, events, links and a status.

How do I find the root span of a trace?

It's the span without a parent span ID. It covers the whole operation, for an agent the whole run.

Go further

Type to search the docs, Learn topics and the blog.