# Put identity on the wire

Set the trace, step and agent headers so a run collapses into one costed tree. By hand, from a framework, or by wrapping an agent.

Source: https://tokentriage.com/docs/build/observability-headers

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

By the end of this page an agent run shows up as one tree with a dollar on every step, instead
of a dozen unrelated rows.

Nothing needs enabling first. Identity headers are read whenever a client sends them, in every
mode, with no configuration. The work is entirely on your side of the connection.

## The rule, in four lines

> `x-tt-trace-id` once per run · `x-tt-agent` per role · `x-tt-step-id` per call ·
> `x-tt-parent-id` when one step spawns another.

Everything below is that rule expressed in a different client.

## By hand

One request, so you can see the shape before you wire it into anything:

```console
$ curl -s http://127.0.0.1:8787/v1/chat/completions \
    -H 'content-type: application/json' \
    -H 'x-tt-trace-id: run-7f3a91c2' \
    -H 'x-tt-step-id: step-01' \
    -H 'x-tt-agent: planner' \
    -H 'x-tt-step-class: user_turn' \
    -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Plan the summary."}]}'
```

A delegated call adds one header naming the step that spawned it:

```console
$ curl -s http://127.0.0.1:8787/v1/chat/completions \
    -H 'content-type: application/json' \
    -H 'x-tt-trace-id: run-7f3a91c2' \
    -H 'x-tt-step-id: step-02' \
    -H 'x-tt-parent-id: step-01' \
    -H 'x-tt-agent: researcher' \
    -H 'x-tt-step-class: tool_loop' \
    -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"Fetch the revenue table."}]}'
```

Same trace id, new step id, and a parent edge. That is the whole protocol.

## From an application

Any client where you control outbound headers works: point `base_url` at the proxy and attach
the headers per call.

The place to attach them differs by framework, and the pattern is the same in each. Mint the
trace id where a run begins and carry it in whatever context object the framework already
threads through. Mint a step id per model call. Set the agent name from the node, role or task
you are executing.

| Framework | Where the headers go |
|---|---|
| LangChain / LangGraph | `default_headers` on the chat model client, or `extra_headers` per call; a `RunnableConfig` callback stamps a fresh step id per node |
| CrewAI | the LLM client's default headers; agent name from the crew member's role, parent id when a task delegates |
| OpenAI and Anthropic SDKs | the per-request `extra_headers` argument, or client-level defaults for the values that do not change |
| a hand-written loop | wherever you build the request; a small helper that takes agent and parent and returns a header map is usually enough |

[Attribute cost per agent step](/docs/recipes/attribute-cost-per-agent-step) is the full worked
version of a hand-written loop, with the Python and the read-back in one place. The
[Python](/docs/sdks/python) and [TypeScript](/docs/sdks/typescript) SDKs mint and propagate the
same headers from ambient context so you do not thread them by hand.

<Callout type="tip">
Set the trace id in one place and let it flow. A helper on your side that stamps trace, agent
and step for every outbound call gives you a complete tree; per-call-site headers give you a
tree with holes in it, and the holes only show up when you are trying to explain a bill.
</Callout>

## By wrapping a CLI agent

For a command-line agent you do not control the HTTP client of, `tokentriage wrap` sets the
environment for you and executes it:

```console
$ tokentriage wrap --agent researcher -- your-agent
tokentriage wrap: base=http://127.0.0.1:8787 trace=tt_72z5fkfij3tumtec4xsjthlvcu agent="researcher"
```

The child process inherits four things:

```text
ANTHROPIC_BASE_URL=http://127.0.0.1:8787
OPENAI_BASE_URL=http://127.0.0.1:8787/v1
ANTHROPIC_CUSTOM_HEADERS=x-tt-trace-id: tt_72z5fkfij3tumtec4xsjthlvcu
x-tt-agent: researcher
```

`ANTHROPIC_CUSTOM_HEADERS` is newline-separated, which is why the agent header appears on its own
line. The trace id is minted fresh per invocation, so one `wrap` is one trace.

Two limits are worth knowing before you rely on it. The custom-header environment variable is
read by Anthropic-dialect SDKs only, so a child speaking the OpenAI dialect is routed and costed
but not grouped by trace. And because the headers are fixed for the whole session, `wrap` gives
you per-session identity rather than per-step: there is no `x-tt-step-id` to vary. For Claude
Code specifically that means per-session, per-model and per-tool attribution, and not per-subagent
attribution, because Claude Code exposes no sub-agent identity on the wire and TokenTriage does
not guess at one.

## Check it worked

Send one request, then read the row back. The `trace` block is the proof:

```console
$ tokentriage top --trace run-7f3a91c2
trace run-7f3a91c2  (5 steps)
STEP       CLASS      AGENT       MODEL        COST       IN    OUT   CACHE-R
step-01    user_turn  planner     gpt-4o-mini  $0.000442  768   512   256
  step-02  tool_loop  researcher  gpt-4o-mini  $0.000442  768   512   256
  step-03  tool_loop  writer      gpt-4o-mini  $0.000442  768   512   256
  step-05  tool_loop  reviewer    gpt-4o-mini  $0.000442  768   512   256
step-04    user_turn  planner     gpt-4o-mini  $0.000442  768   512   256
TOTAL                                          $0.002208  3840  2560  1280
```

That capture is from a local run against a mock upstream whose token counts are fixed, so the
dollars are arithmetic over those counts rather than a bill. If the command prints nothing, the
trace id in your header and the one you passed differ; if it prints a flat list with no
indentation, no request carried `x-tt-parent-id`.

## What can go wrong

**The headers never arrive.** A service mesh or API gateway between your application and
TokenTriage is dropping unknown request headers. Add the `x-tt-*` prefix to its allowlist. You
can tell this apart from a client bug in one request: send it with `curl` directly at the proxy
and see whether `trace.source` changes.

**Every call has the same step id.** A step id captured once at client construction is a
constant. Mint it per call, next to where you build the request.

**The tree is flat.** Parent ids are missing. A step with no `x-tt-parent-id` renders at the root
whether or not it logically had a parent, so a run where only the root is unparented is correct
and a run where everything is unparented is a bug in your instrumentation.

**Turning it on did not fix last week.** Identity is recorded as rows are written. Rows logged
before you deployed the headers stay standalone, and nothing rebuilds them, because the
information was never on the wire.

<Details summary="Naming ids so a collision cannot merge two runs">

Ids are opaque and are compared for equality, so two runs that happen to choose the same string
become one trace. Anything with real entropy avoids that: a UUID, a request id from your own
system, or a short random suffix on a readable prefix (`run-7f3a91c2`).

Values are capped at 128 bytes and truncated at the boundary, so put the varying part near the
front. A 200-byte id whose entropy is at the end truncates to a constant.

Agent names are the opposite case: keep them stable and few. They are how a rollup groups, so
`planner` and `Planner` split one agent into two rows.

</Details>

<NextSteps items={[
  {title: "How steps become a tree", href: "/docs/build/observability-trace-trees", description: "What the parent edge builds, what a rollup by agent shows instead, and the four ways a tree comes out wrong."},
  {title: "Step classes", href: "/docs/build/observability-step-classes", description: "The three-value vocabulary, when to set it explicitly, and what routing does with it."},
  {title: "Attribute cost per agent step", href: "/docs/recipes/attribute-cost-per-agent-step", description: "The full worked task for a hand-written agent loop, from the Python to the costed tree."},
  {title: "Turn on business dimensions", href: "/docs/build/cost-dimensions", description: "The other header family: tenant, feature, user and your own tags, for grouping spend by budget rather than by run."},
]} />
