Skip to Content
Rius

How do I group the traces of one conversation into a session with LangChain?

Rius groups the traces of one LangChain conversation into a session when you wrap each turn in a session scope that carries your conversation id. In Python you use rius.session(conversation_id), and in TypeScript you use withSession(conversationId, fn). Every span the turn creates, including the chain, tool, retriever and model spans from LangChain, carries the same session.id. In the console, each turn stays its own trace and the session ties them together.

How it works

A trace is one turn of your chain or graph, and a session is the id you assign to correlate several turns. You mint the id, and the SDK stamps it as the session.id attribute on every span started inside the scope. The LangChain integration turns each chain, tool, retriever and model call into a span, so all of them pick up the id from the scope around your call.

A few properties of the scope matter for conversations:

  • Scopes ride OpenTelemetry context, not a global. They nest, and the innermost one wins.
  • A scope unwinds with the block, even when the turn raises an error.
  • A scope follows async work the same way the active span does.
  • The session depends on the id, not the process. A conversation that outlives a deploy or moves between replicas stays one session.

The tracing guide covers sessions in full, and Concepts defines them.

Set it up

  1. Install the LangChain instrumentation and call init() once at startup. The extra installs the instrumentation, not langchain itself. The LangChain page has the TypeScript install and the full setup.
import rius rius.init(api_key="ri_...", service_name="my-agent") # build and run your chains exactly as before
  1. In Python, wrap each turn in rius.session() and pass the conversation id you already have. Here chain, conversation_id and message are your own names.
import rius with rius.session(conversation_id): chain.invoke(message) # every span of the turn carries session.id
  1. In TypeScript, use withSession and put the call to your chain inside the callback. The scope follows await, so async chains and graphs are covered.
import { withSession } from '@glassflow-ai/rius' await withSession(conversationId, async () => { await chain.invoke(message) // every span of the turn carries session.id })
  1. Reuse the same id on every turn of the conversation. Each turn becomes its own trace, and all of them land in one session.

  2. If you have no id of your own, call rius.session() with no argument in Python, or withSession with only a callback in TypeScript. Rius mints a fresh UUID for the scope and hands it to you, so you can log it or pass it to the next turn.

with rius.session() as session_id: chain.invoke(message)
  1. For a process that handles exactly one session, such as a CLI run or a batch job, skip the scope. Set the id once with init(session_id=...) in Python, init({ sessionId }) in TypeScript, or the RIUS_SESSION_ID environment variable. An active session scope overrides that default.

  2. Run two turns with the same id, then open the traces in the console and check that they share a session.

What you get

  • Each turn of the conversation stays a short trace of its own, with the nested chain, tool, retriever and model spans that mirror your graph. The session groups those traces across turns.
  • The grouping is independent of the process serving it, so a conversation that moves between replicas or survives a deploy reads as one session.
  • Your own conversation id doubles as the session id, so you can match a session in the console to a record you already have.
  • A trace with no session id is grouped as its own single-trace session. If an agent’s session count equals its trace count, nothing is setting session.id, and those sessions are not meaningful groupings. Check the scope around your calls.
  • You can nest a user scope with rius.user(...) in Python or withUser(...) in TypeScript. It stamps user.id next to session.id, so the same conversation also ties to the person it served.

For how sessions fit into watching an agent over time, see Monitor AI agents in production.

When something else fits

  • If one process serves exactly one session, the process-wide default through RIUS_SESSION_ID or init is simpler than scoping every call.
  • Never set a process-wide default in a server that handles many users, because every user’s traces would merge into one session. Scope each request instead.
  • If your runs are one-off and nothing spans several turns, leave the session unset. Each trace then reads as its own session, which is the right result.

Still stuck? Get help

Last updated on