threadId / thread_id.
This page explains how to model that. For the exact fields and normalization, see the trace contract. For the ideal per-trace shape, see Building high-quality traces.
One agent execution = one trace still holds. A conversation turn is one execution — one trace. The
threadId groups the turns; it does not merge them into a single trace.The core rule
When one user has a conversation with your agent, each turn — one user message and the agent’s response — is a complete execution. Wrap each turn in its ownlemma.trace() and give every turn in the same conversation the same thread id. Lemma groups traces that share a threadId / thread_id into one conversation.
What goes where
The split that trips people up is between the root input and the generation’s message history.- Root
inputis the current user turn only — the single message that started this execution. - The generation carries the full ordered message list sent to that model call: system prompt, prior conversation turns, tool results, and the current user message. That list is what the model actually saw.
input and llmInputMessages / llm_input_messages. See Generations and message history and Generations.
Users
SetuserId / user_id on each turn’s trace so Lemma can slice a conversation — and every conversation — by the person on the other side. Set it on every turn in the thread, since each turn is a separate trace.
| Context | SDK option | Client field |
|---|---|---|
| Thread / conversation | threadId / thread_id | trace.thread_id |
| User | userId / user_id | trace.user_id |
Multi-process and async turns
Turns often do not run in the same process: a request handler answers turn 1, a queue worker answers turn 2, or turns are processed asynchronously. The rule is unchanged — start a new top-level trace per turn — but you must carry the thread id (and user id) across the boundary so the new process can reattach the turn to the conversation. Persist the thread id wherever you persist the conversation (session store, database row, queue message), then read it back when the next turn runs.- TypeScript
- Python
Example: two turns of one thread
Both turns sharethreadId / thread_id and userId / user_id. Turn 1 is a plain answer. Turn 2 calls a tool and sends the growing message history — system prompt, the turn 1 exchange, the tool result, and the current user message — to the model.
- TypeScript
- Python
What good looks like
- One trace per turn, each wrapped in its own
lemma.trace(). - The same
threadId/thread_idon every turn of the conversation, so Lemma groups them. userId/user_idset on every turn for per-user slicing.- Root
inputis the current user message only; the final answer (or error) is the root output. - Each turn’s generation carries the full message history for that call, including prior turns and tool results, as
inputandllmInputMessages/llm_input_messages. - The thread id (and user id) travels with the work across queues, workers, and async boundaries.
Pitfalls
Related pages
Threads & context
Group conversations and attach users.
Trace contract
The exact shape and fields Lemma reads.
Building high-quality traces
Bad → better → best examples for the ideal instrumentation path.
Generations
Capture model, full messages, and completion.