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
Transcript economics. The llm spans' input tokens grow 407095 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
trajectory search_policy process_refund finish is an llm decision, not a tool
llm calls 3
tokens in=205 out=47
cost $0.00252
latency 690ms (sum of spans)
cost per success · you pay for failures too
tasksuccesscost
refund ORD-3300yes$0.00252
warranty lookupyes$0.00180
find a nonexistent discountno$0.00320
3 runs, 2 successes$0.00752
$0.00376 cost 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