Skip to main content
Conversational agents and workflow agents make many LLM calls per task — a planning call, tool calls, follow-ups. By default the gateway logs each as an independent request. Agent run tracking lets you stamp those calls with a shared id so the dashboard shows them as one run: a rolled-up total cost, plus a nested waterfall of which tool/agent step drove which calls and what each cost. Because the gateway is a proxy, it sees your LLM calls but never the tool/agent code that runs between them. So the tree is reconstructed from headers your client sends — no SDK or in-app tracer required. Any client that can set request headers works.

The headers

Graceful degradation. A trace id alone gives you a flat run rollup — every call in the run plus its total cost. Adding a span path gives you the waterfall — the calls grouped under named tool/agent steps, with cost rolled up each step. Start with just a trace id; add paths when you want the tree.
/ is the reserved separator between step names. If a step name itself contains a /, percent-encode it so it doesn’t split into extra nodes.

Modeling the tree

Send, on each LLM call, the path of the steps above it. Interior tool/agent nodes don’t make their own request — they’re inferred from the paths of the calls beneath them. For an agent that plans, then calls a search_db tool that makes two calls:
The tool:search_db node’s cost ($0.048) is the sum of the calls beneath it; the planner node totals the whole run.

Sending the headers

Frameworks that hide per-request headers

Pydantic-AI, Mastra, and similar frameworks don’t expose a per-request header hook on agent.run() — they own the model-call loop internally. Two options:
  • A client per conversation — build the model/client with the trace id in its default headers and use it for that conversation’s calls.
  • Context-scoped injection — keep one client and inject the current run’s headers from a context store, so every model request the framework makes picks them up automatically. This is usually the cleaner option, shown below.
The idea is the same in both languages: hold the current run/step headers in a context store (contextvars in Python, AsyncLocalStorage in Node), and give the model client a custom HTTP layer that reads that store on every request. You then set the store once around each agent.run() — every internal model call in that run (including tool-loop calls) inherits the headers. The natural granularity is per agent/tool step, which is exactly the span_path you want: set the store before each agent or tool boundary.
AsyncLocalStorage isolates concurrent runs automatically; in Python, set the contextvars value inside each task so parallel runs don’t share one store.

Viewing runs

In the dashboard, open Observe → Runs:
  • The runs list shows one row per run with its rolled-up cost, token totals, call count, and duration.
  • Opening a run shows the waterfall: the nested steps and LLM calls with per-node cost. Click any LLM node to open its full request detail.
Runs and Logs are two views of the same requests. Every request still appears in Logs; a request that belongs to a run gets a Run link there, and a quick Exclude runs toggle hides run requests when you want only standalone traffic.

Querying runs directly

The headers are stored as first-class columns on llm_requests, so a run’s cost is a plain aggregate (see the data model):

Runs vs. metadata

Use agent run tracking to group the calls of one execution into a tree. Use metadata headers (feature, team, environment, user) to attribute cost across dimensions. They compose: stamp a run with a trace id and a X-Majordomo-Feature so you can both drill into a single run and roll runs up by feature.