The view comes second
Designing the trace panel before you know the emission shape produces a component that renders one happy path beautifully and silently drops retries, parallel calls and half-failed runs — the exact cases a reviewer opens the panel to see.
HouseWarm already does the shallow version of this well. A field comes back from extraction, it carries a confidence number, the UI puts that number next to a crop of the source image and the raw OCR text, and nothing downstream fires until a human approves or corrects. It is a real human-in-the-loop gate and it works. It is also, structurally, one value from one model call, and there is no where did this come from beyond the crop, because there is nowhere else it could have come from.
Now change one variable. The run took eleven steps. It called a search tool, then a database read, then two enrichment calls in parallel, then a write. One of those enrichment calls timed out and was retried, and the retry succeeded. The whole thing took nineteen seconds and cost about sixty cents. It cited three documents. It changed two records. A reviewer needs to decide, quickly, whether to accept it.
That is the problem this course is about, and the first thing worth saying about it is that almost nobody builds it in the right order. The near-universal instinct — especially for someone who is good at interface work — is to open a design tool and draw the panel. A left rail of steps. Expandable rows. A little clock icon. A token counter in the corner. It is a genuinely fun thing to draw, and drawing it first is the mistake this entire module exists to prevent.
What a view-first component actually gets wrong
The failure is not aesthetic and it is not vague. A trace panel designed against an imagined payload fails on a short, specific and predictable list of cases, and the reason it fails on exactly those is worth understanding, because it generalizes.
When you draw a trace view before seeing real traces, you draw a finished, successful, strictly sequential run. That is what a mock is: an artifact with no time axis, no failure, and no concurrency. So the component you build from it inherits three assumptions it never stated out loud — that steps arrive in order, that each step happened once, and that the run is over. Every one of those is false in production, and each one produces a different visible bug:
- The retry renders as two steps. The agent called
enrich_property, it timed out, it called it again with identical arguments and got a result. A tree that maps one node per emitted span shows two calls to the same tool, which reads to a reviewer as the agent being confused or duplicating work. The truth — one logical operation that needed two attempts — is present in the data and absent from the view, because nothing in the mock ever suggested a node might need to collapse siblings. - Parallel calls render as a false sequence. Two enrichment calls that ran simultaneously get stacked vertically like everything else. A reviewer reasonably reads vertical order as causal order and concludes the second call used the first call’s output. It didn’t. The overlap is recoverable from the timestamps, but only if the component was built knowing timestamps could overlap.
- A partial failure renders as a success. The run produced an answer, so the collapsed summary says it produced an answer. The step that errored is three clicks away, at an altitude nobody opens unless something already looks wrong. This is the worst of the three, because it is the only one that is invisible rather than merely misleading.
- A still-running run renders as a finished one. A node with no end time is not a node that took zero milliseconds, and a tree that has not received its last span is not a complete tree. A component built from a mock has no state for “this branch is still open,” because a mock is never still open.
Notice what these have in common. Not one of them is a styling problem, and not one of them would be caught by reviewing the design. They are all cases where the data can express something the component has no slot for — and the component, having no slot, does the worst possible thing, which is to render something plausible instead of nothing.
Where people get burned
A trace panel that renders a wrong story confidently is worse than no trace panel. The entire purpose of the surface is to let a human decide whether to trust the run. A view that turns one retried call into two calls, or a partial failure into a success, spends the reviewer’s trust on a claim the system never made. The reviewer’s eventual discovery that the panel lies is not recoverable with a bug fix; they stop opening it.
There is no single thing called “an agent run”
The second reason to start from data is that the data is not settled. This is not a case where a stable schema exists and you merely haven’t read it yet. Several incompatible vocabularies are in production simultaneously, and the one with the best claim to being the standard is explicitly unfinished.
OpenTelemetry’s generative-AI semantic conventions — the vendor-neutral effort, published by the OpenTelemetry project rather than by anyone selling a product — now live in their own repository, open-telemetry/semantic-conventions-genai, and their span conventions document carries a Development status badge as of September 2026. That is the project’s own word for not-yet-stable. The conventions lesson takes this apart properly; what matters here is only that the closest thing to a standard says of itself that it is still moving.
Meanwhile the vendors each ship their own model. LangSmith — LangChain documenting its own commercial product — represents a run as a Run object with exactly seven run_type values (chain, llm, tool, retriever, embedding, prompt, parser) and no agent type at all. OpenInference, published by Arize AI — again, a vendor, backing both the open-source Phoenix and a commercial product — requires an openinference.span.kind on every span and offers a longer list including AGENT, GUARDRAIL, RERANKER and EVALUATOR. Same domain, different carving.
The sharpest illustration is inside a single SDK. Vercel’s AI SDK — vendor documenting its own product — currently ships two parallel telemetry integrations. The recommended one emits OpenTelemetry GenAI attributes: gen_ai.usage.input_tokens, gen_ai.usage.output_tokens. The other, which the docs label in as many words “the legacy format,” emits the SDK’s own ai.* attributes, where the same two numbers are called ai.usage.promptTokens and ai.usage.completionTokens. Different names, different case convention, same integers, both live in the same documented version of the same library.
A component hard-wired to either of those names renders an empty token column against the other. And this is the easy case — the two dialects are documented on one page by one publisher. Across publishers there is no such page, which is why LangSmith has to maintain a published mapping table translating gen_ai.*, OpenInference, TraceLoop, generic llm.* and Logfire attributes into its own fields. That table exists because someone had to pay an engineer to write it.
The order that actually works
Three steps, and the discipline is entirely in not skipping the first one.
Capture, first. Run something real, with tools, that fails at least once, and keep the raw output. Not a documented example payload from a docs page — those are curated, and curated payloads are mocks with better provenance. Your own run, from your own runtime, at the version you actually have installed.
Schema, second. Write the type. The rule that makes this exercise worth doing: every field in the type must trace to a field that was in the captured payload. If you want a field the payload doesn’t have, it does not go in the type as though it were free — it goes on a separate list, because it is now a piece of work (derive it, request it from the runtime, or decide the component renders its absence).
View, third. And by then most of the hard decisions are already made, because the questions that felt like design questions turn out to have been data questions wearing a design costume. Can a step have children? Look at the payload. Can two steps overlap in time? Look at the timestamps. Does a retry come through as one record or two? Look. Every one of those has an answer that is discoverable in ten minutes and unguessable from a mock.
The <AgentTrace /> component this course builds toward is not started in this module at all. Module one produces two artifacts and neither is a component: a captured payload, and a TypeScript type derived from it. That sequencing is the lesson, enforced as an exercise rather than merely asserted in prose.
The second list is the valuable one
When you write down what the payload contained, you will also, involuntarily, notice what it didn’t. That second list is where the real design work is, and it is worth being deliberate about capturing it rather than letting it evaporate into a vague sense that the data is incomplete.
Some of it is genuinely absent and derivable. LangSmith’s Run, for example, documents start_time and end_time but no duration field at all — a duration is a subtraction you perform, not a number you receive. Some of it is absent and expensive: a dollar cost is not emitted by any runtime, because a runtime does not know what you are paying; token counts are emitted, prices live on a vendor page, and multiplying the two is a claim about the world that goes stale silently. And some of it is absent and impossible: the agent’s reason for choosing a tool is not a field anywhere, and any component that shows one is showing something a model generated about itself afterward, which is a different kind of object than a timestamp.
Every item on that second list is a decision the component has to make — derive it, mark it unavailable, or refuse to display it. Making those decisions on purpose, in a schema, is the difference between a trace view and a trace view you can defend in a room.
Retrieval check
A reviewer opens your trace panel and sees two identical calls to enrich_property, back to back. Nothing in the panel is broken. What has gone wrong, and at which layer?
Check your answer
What actually happened is one logical operation that took two attempts — a timeout followed by a successful retry. The runtime emitted both attempts, correctly, because both attempts really happened. The component mapped one emitted record to one visible node, also “correctly,” in the narrow sense that it did what it was written to do.
The failure is at the schema layer, which is why no amount of styling fixes it. A type that models a step as a flat record with a name and a duration has no way to express “attempt two of one operation, the first having failed.” The information exists in the payload — the error status on the first span, the identical arguments, the adjacency — and there is nowhere in the type to put it, so the view cannot show it no matter how it is drawn.
Which is the general shape of every problem in this module: the question that presented itself as “how should retries look?” was really “does my type distinguish an attempt from an operation?” The answer to the second one determines whether the first is answerable at all.
Hands on
Capture one real run and write down both lists
Done when: ARTIFACT.md’s module-one section names the runtime and version, points at a captured payload from a run that failed at least once, and carries two explicit lists — fields the payload actually contained, and fields you wanted that it did not. No TypeScript yet, and no component.
- Pick a runtime you can run today — the AI SDK, an Anthropic tool-use loop written by hand, LangChain, whatever is already installed. It genuinely does not matter which, and picking the “best” one is a way of not starting. Record the exact package version in
ARTIFACT.md, because field names are version-specific in a way this course’s prose cannot be. - Write a task that forces at least three tool calls and make one of them fail. Point a tool at a URL that 404s, or give it an argument you know is invalid. A clean run teaches you nothing here — the failure case is the entire reason for capturing rather than reading a docs example.
- Capture the raw output. Turn on whatever telemetry the runtime has and export to a console or file exporter, or, if that is fiddly, just serialize every message and response object the loop produces. Save it somewhere you can reread; note the location in
ARTIFACT.md. Strip credentials before saving. - Now the actual work. Read the payload and write list one: every field that is really in there, by its literal name as emitted. Not what you think it means — what it is called. This list is the only thing your schema will be allowed to depend on.
- Write list two: everything you assumed would be present and isn’t. Push on this one specifically — is there a duration, or only two timestamps? Is the retry distinguishable from a second call, and by what? Is there any field that says why a tool was chosen? Is there a cost, or only token counts?
- Bring both lists into the chat. I will push back hardest on list two, because a short list two almost always means the payload was skimmed for what was expected rather than read for what was there.
What this does not cover
This lesson argued for an ordering and gave you a payload to stare at. It deliberately did not teach you how to read one. The vocabulary that makes a captured payload legible — what a span actually is, how parent references build a tree, what the status field can and cannot say, why events and links exist and when a timestamp is a lie — is the span-model lesson, which comes next and is the piece of genuine specification reading in this course.
The specific question of how far to trust the OpenTelemetry GenAI attribute names, given that badge, belongs to the conventions lesson after it. And the end-to-end walk of a single tool-calling loop, from the first request to the final answer with every emitted record named, closes the module. Only then does anything get drawn.
Read this next — primary source
Trace with OpenTelemetryLangChain, LangSmith documentation — vendor documenting its own product; free to read, the product it describes is paid
Read past the setup instructions to the mapping tables, which are the real document. LangSmith has to accept telemetry written in at least five different vocabularies — OpenTelemetry’s gen_ai.*, OpenInference, TraceLoop, a generic llm.*, and its own langsmith.* namespace — so it publishes, row by row, what each foreign attribute becomes in its own model. That table is the most concrete available evidence for this lesson’s claim: there is no single shape called “an agent run,” and the industry knows it, because a commercial vendor had to build and maintain a translation layer to stay compatible. Skimming it takes five minutes; reading it properly tells you which fields survive every translation and which ones only exist in one dialect, which is exactly the distinction your schema has to make.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.