One run as a span tree, then metrics read back from the spans
The Part 9 event journal already records this refund run. Observability is not a second system; it is a second VIEW of the same log. Here we fold the run into an OTel-GenAI-shaped span tree (by hand, no otel SDK), play it back as a Gantt waterfall, and reconstruct the metrics and cost-per-success directly from those spans.
The world: one refund run run-bb11 for ORD-3300, frozen timings and token counts. The root span invoke_agent runs [0-690ms] over five child spans. Each llm span carries gen_ai.*-STYLE attributes (operation chat, request model, token usage); each execute_tool span carries the tool name and its latency.
Same bytes, two views. The journal is the durability artifact (Part 9). Read as spans, it is the observability artifact. No new storage, no otel SDK: just dicts emitted as JSONL. A real OTLP exporter is a commented one-liner.
Replay the run
span tree · OTel-GenAI-shaped, hand-rolled (no otel SDK)0 / 6 spans
0180240450540690ms
Transcript economics. The llm spans' input tokens grow
40 → 70 → 95
across the run. The agent re-sends a growing transcript every step; that growth, not the tool calls, is the dominant cost lever. Prompt and KV caching are the lever back.
the same spans as JSONL (what a real OTLP exporter would ship)
Metrics reconstructed from the spans, and the number that matters in production
metrics · read from the spans
trajectorysearch_policy→process_refundfinish is an llm decision, not a tool
llm calls3
tokensin=205 out=47
cost$0.00252
latency690ms (sum of spans)
cost per success · you pay for failures too
task
success
cost
refund ORD-3300
yes
$0.00252
warranty lookup
yes
$0.00180
find a nonexistent discount
no
$0.00320
3 runs, 2 successes
∑
$0.00752
$0.00376cost per success = total $0.00752 / 2 successes. Worse than one happy run's $0.00252: the failed discount lookup tripped the Part 8 breaker and still cost money.
Streaming progress (spans revealed in time order)
live feed · stream(spans)0 lines
The controller is prompt plus model (the Part 1 payoff). The llm span's gen_ai.request.model and its token usage are exactly where model choice and thinking budget show up in the bill. Switch the model or the transcript and these spans, and the cost, change with it. Nothing here is a side channel: the metrics are derived from the same spans you see in the waterfall, which are the same bytes as the durability journal.
How we keep it honest: the span tree is a tree of plain dicts with gen_ai.*-STYLE attributes, emitted as JSONL. The names are hedged as -STYLE on purpose: the GenAI semantic conventions are still moving. A real OTLP exporter, like the generate() swap in earlier parts, is one flag away. Storage is roughly 60 lines, a JSONL file or SQLite; for production durability the essay tours Temporal, DBOS, the Vercel Workflow DevKit, and LangGraph, each with honest limits.
invoke_agent / llm
execute_tool
tokens / cost
success
failed run