Agent tracing

An agent is a workflow where a language model decides what to do next. Production agents combine LLM calls, tool calls, sub-agent invocations, and retries, often in parallel and on non-deterministic branches. Tracing captures this entire flow as a hierarchical span tree so you can debug, cost-attribute, and optimize at every step. The marketing-side hub lives at /agent-tracing.

The four-layer span tree

Spanlens renders every trace as four nested layers. Each layer maps to a concrete entity in the data model.

LayerSpan kindEntityWhat it captures
1. Trace roottracetraces tableEnd-to-end latency, total cost, trace ID, user/session tags
2. Agent stepspan(kind="agent_step")spans tableStep name, input state, output state, latency
3. LLM callspan(kind="llm")spans + requestsModel, tokens, cost, full request/response body
4. Tool callspan(kind="tool")spans tableTool name, arguments from the LLM, return value, latency

Parent/child links

Every span has a parent_span_id field that points to the immediate parent. Spanlens does not enforce a foreign-key constraint on this column, by design — parallel agent fan-out and out-of-order ingestion mean child spans sometimes arrive before their parent. The trace builder reconstructs the tree at query time by joining on (trace_id, parent_span_id). Orphaned spans (parent never arrives) attach to the trace root with a visible "orphan" marker rather than disappearing.

Critical path

Critical path is the longest dependency chain through the trace — the path that determines total wall-clock time. For an agent that runs four steps in parallel and one sequentially after, the critical path is max(parallel_4) + sequential_1. Optimizing a non-critical-path span has zero effect on total latency. Spanlens computes critical path automatically and colors those spans differently in the trace view.

The algorithm walks the span tree from the trace root, follows the slowest child at each branch (when children run in parallel), and adds sequential children directly. Span overlap is detected via (start_ms, end_ms) ranges.

Emitting spans manually

For frameworks Spanlens does not ship a native integration for, emit spans with the SDK.

import { SpanlensClient } from '@spanlens/sdk'

const client = new SpanlensClient()
const trace = await client.startTrace({ name: 'support_ticket_flow' })

const classifyStep = await trace.startSpan({ name: 'classify', kind: 'agent_step' })
const classifyLlm = await classifyStep.startSpan({ name: 'classify.llm', kind: 'llm' })
// ... call your LLM ...
await classifyLlm.end({ tokens: { in: 1240, out: 12 }, cost_usd: 0.0023 })
await classifyStep.end()

await trace.end()
ts

OpenTelemetry mapping

Spanlens accepts OTLP/HTTP at /v1/traces. OTel span attributes map to Spanlens fields by convention:

OTel attributeSpanlens field
gen_ai.systemprovider
gen_ai.request.modelmodel
gen_ai.usage.input_tokenstokens.in
gen_ai.usage.output_tokenstokens.out
gen_ai.response.finish_reasonsfinish_reason

Where to go next