Skip to main content
Lemma reads a specific trace contract: one agent execution is one trace with a single root span, and every LLM call, tool call, and unit of work is a child span of that root. When a trace does not match this shape, Lemma can still render what it receives, but input/output, model and token stats, tool visibility, and automated issue detection degrade. This page shows the four most common malformed shapes next to their conformant counterparts. Each tree is drawn the same way as in Traces: the root is the trace, indented lines are child spans.
One agent execution = one trace. If you fix only one thing, make every LLM and tool call a child span of a single root rather than its own top-level trace.

Sibling traces vs one nested trace

The most common problem: each LLM or tool call is exported as its own top-level trace, so a single agent run is scattered across three unrelated traces. Bad - three sibling traces, one per call:
draft-reply                <- trace (generation, no parent)

search_docs                <- trace (tool call, no parent)

final-answer               <- trace (generation, no parent)
Good - one trace, calls nested under the root:
support-agent              <- trace root (input, output)
|- draft-reply             <- generation
|- search_docs             <- tool call
`- final-answer            <- generation
  • Cause: child work ran without an explicit root trace or trace ID, or it crossed a queue, worker, stream, or timer without carrying the trace handle/IDs with it.
  • Fix: wrap the run with lemma.trace() and record child work with trace.recordGeneration() / trace.record_generation(), trace.recordTool() / trace.record_tool(), and trace.recordSpan() / trace.record_span() inside it. See Traces and Every span appears as a separate trace.

Empty root vs root with input and output

A trace whose root carries no input or output shows only timing. You cannot read what the user asked, what the agent answered, or whether the run succeeded. Bad - root with no input/output:
support-agent              <- trace root (no input, no output)
|- draft-reply             <- generation
`- search_docs             <- tool call
Good - root carries the user input and the final answer:
support-agent              <- trace root
                             input:  "Where is my order #1843?"
                             output: "It ships tomorrow and arrives Friday."
|- draft-reply             <- generation
`- search_docs             <- tool call
  • Cause: the root span was never given input/output (or the work was recorded only on children).
  • Fix: pass input to lemma.trace() and return the final answer from the callback. Use trace.output(output) for custom output and trace.fail(error) before re-raising errors. See Traces.

Invisible tool calls vs typed tool spans

Provider instrumentation usually captures model calls but not your application tools, so the model is visible while the tools it called are not. Even when a tool span exists, an untyped one with no args or result is opaque. Bad - provider/LLM spans present, tool calls invisible:
support-agent              <- trace root
|- draft-reply             <- generation
`- final-answer            <- generation
                             (search_docs ran but was never traced)
Good - each tool is a child span typed as a tool, with args and result:
support-agent              <- trace root
|- draft-reply             <- generation
|- search_docs             <- tool call
|                            input:  { "query": "order 1843 status" }
|                            output: { "status": "shipped", "eta": "Fri" }
`- final-answer            <- generation
  • Cause: tools are invoked outside any span, or are wrapped in a generic span that is not typed as a tool.
  • Fix: record each completed tool call with trace.recordTool({ name, input, output }) or trace.record_tool(name=..., input=..., output=...). The SDK records the arguments, result, and tool kind attributes Lemma reads. See Tool calls.

Flat nesting vs a proper parent/child tree

Spans can all exist yet sit as siblings under the root, losing the structure of which step called which. A flat list hides retries, sub-agents, and the retrieval that fed a generation. Bad - flat: every span is a direct child of the root:
support-agent              <- trace root
|- plan                    <- span
|- embed-query             <- generation
|- search_docs             <- tool call
|- rerank                  <- span
`- final-answer            <- generation
Good - work nests under the step that performed it:
support-agent              <- trace root
|- plan                    <- span
|  `- embed-query          <- generation
|- retrieve                <- span
|  |- search_docs          <- tool call
|  `- rerank               <- span
`- final-answer            <- generation
  • Cause: child spans are created without the explicit parent span handle or parent ID, so everything attaches to the root.
  • Fix: record child work from the trace or span handle passed into the helper. Use trace.startSpan() / trace.start_span() when a subtask needs its own measured parent span. See Trace nesting is flat.

Current issue-detection support

Beyond rendering a trace, Lemma runs automated issue detection (silent failures, bad tool calls, loops). Today this runs for traces that arrive in a recognized shape:
  • Lemma SDK traces produced by lemma.trace() with trace.recordGeneration() / trace.record_generation() and trace.recordTool() / trace.record_tool() child records.
A conformant trace (one root, input/output, typed generation and tool children) is the best way to get full value and to be eligible for issue detection.

Trace contract

The exact shape and fields Lemma reads.

Common issues

Symptoms, causes, and fixes for malformed traces.

Traces

Wrap one agent execution in one trace.

Tool calls

Record tool args and results as child spans.