Skip to main content
A high-quality Lemma trace is not just “data arrived.” It is a complete, nested record of one agent execution: every span has real timing, every successful span has input and output, generations carry the full message list sent to the model, and tools show what they were asked and what they returned. This page is the pedagogical ideal path. For the exact ingest fields, validation rules, and client-vs-normalization mapping, see the trace contract. Use Common issues when something is missing or malformed.
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.”
What “ideal” means on each node:
  • 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

Record complete inputs and outputs only when safe. Redact secrets, credentials, and sensitive user data before tracing them.

Structure and nesting

Bad — each call is its own top-level trace:
Better — one root, but everything is flat under it:
Best — one root, children nested under the step that performed them:
Wrap the run with 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:
Better — partial payload:
Best — every successful span carries both sides:
Failures should still keep the input and record the error instead of an output. Opaque spans hide what happened; input and output make the tree inspectable.

Timing

Bad — no duration at all:
Better — duration inferred from the parent:
Best — measured from real start → end:
Prefer live handles (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:
Better — the root fails, but the source is unclear:
Best — the child records its input, error, and duration; the root also fails when the execution cannot recover:
With a live handle, call 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:
Better — model and latest user string only:
Best — model, complete message history for this call, and the response:
In the SDK, pass that full list as both input and llmInputMessages / llm_input_messages. See Generations and the trace contract.

Tool calls

Bad — tools ran but were never recorded:
Better — tool span exists, but opaque:
Best — typed tool under the correct parent, with args and result:
Record each completed tool with 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.

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.