One agent execution = one trace. Child LLM calls, tool calls, retrieval, and app logic belong inside that root — not as separate top-level traces.
The ideal trace
trace rootsupport-agent2.41s
input: “Where is my order #1843?“
output: “It ships tomorrow and arrives Friday.”
spanretrieve-context412ms
input:
output:
toolsearch_docs183ms
args:
result:
toollookup_order96ms
args:
result:
generationanswer1.87s
model: gpt-4o
messages: [system, prior turns, user, tool results]
output: “It ships tomorrow and arrives Friday.”
- Root — current user input, final answer (or error), measured duration
- Spans / tools — real input and output, nested under the step that caused them, measured start → end
- Generations — model name plus the full message list sent to that call (system, prior turns, tool results, current user), not just the latest user string
Quality checklist
| Rule | Why it matters |
|---|---|
| One root per agent execution | Otherwise one run scatters across unrelated traces |
| Root has input and final output (or error) | You can read what the user asked and whether the run succeeded |
| Correct typed children | Generations, tools, and generic spans render and filter correctly |
| Every span has a real start and end | Duration, latency, and timelines stay trustworthy |
| Every successful span has input and output | Opaque spans hide what happened; failures keep input plus error |
| Every failure is recorded on the span where it occurred | You can see which operation failed and what input caused it |
| Nest under the work that caused the child | Flat trees hide retrieval, retries, and sub-steps |
| Generations record the full message list for that call | Prior turns and tool results are part of what the model saw |
| Tools record arguments and result (or error) | Tool behavior becomes inspectable |
Structure and nesting
Bad — each call is its own top-level trace:lemma.trace() and record children from the trace or span handle that owns them. See Traces and Spans.
Input / output
Bad — named span with nothing useful:Timing
Bad — no duration at all:startSpan / start_span, and the matching tool and generation helpers) so .end(...) establishes duration after the work completes. Pass durationMs / duration_ms only when you already measured the operation externally. Lemma can infer missing child durations from a parent, but measured bounds are better.
Errors
Record a failure on the exact span, generation, or tool where it happened. Keep the input, pass the error instead of an output, and end the span so timing remains accurate. Bad — the exception is swallowed and the failed operation is missing:tool.end({ error }) / tool.end(error=error) and rethrow when the whole execution should fail. Callback traces automatically record uncaught exceptions on the root. If you catch an error at the root boundary, call trace.fail(error). If the agent recovers, keep the child marked as failed and let the root complete successfully.
See Errors in the trace contract for the exact recording rules and SDK examples.
Generations
For each LLM call, record the full ordered message list sent to that call — system prompt, prior conversation turns, tool results, and the current user message — not only the latest user string. That list is what the model saw; the root trace input is only the current user turn. Bad — little more than a name:input and llmInputMessages / llm_input_messages. See Generations and the trace contract.
Tool calls
Bad — tools ran but were never recorded:trace.recordTool({ name, input, output }) / trace.record_tool(name=..., input=..., output=...), or use live tool handles when you want measured duration. See Tool calls.
Happy-path example
Use live handles so start → end establishes timing. Put the current user message on the root, and the full model message list on the generation.- TypeScript
- Python
Next steps
Trace contract
The exact shape and fields Lemma reads.
Instrument an agent
Build a complete trace one piece at a time.
Generations
Capture model, full messages, and completion.
Common issues
Diagnose missing spans, flat trees, and blank I/O.