Overview
Laminar is an open-source, OpenTelemetry-native observability platform for AI agents. Trace, debug, and monitor every Pi Durable run, model request, and tool call with oneLaminar.initialize() call in the Laminar TypeScript SDK. Self-host via Helm or use managed cloud.
Pi Durable (@earendil-works/pi-durable) is a durable agent harness built on @earendil-works/pi-ai. Conversations, model turns, and tool calls are committed to storage before they run, so a process that dies mid-run picks the work up where it stopped when it reopens the storage. Laminar follows the run across that restart: the resumed work lands in the trace the run started in, even when it finishes in a different process.
What Laminar captures:
- One trace per run, with a root span (
pi.run) holding the user’s input and the final answer, or the error when the run ends unanswered. - An LLM span (
pi.chat) per model request: the messages as sent, including the system prompt, the tool definitions, the model, token counts, and cost. - A TOOL span per tool call, named after the tool, with its arguments and result.
- The session ID (Pi’s per-conversation session ID by default), user ID, and metadata you set.
This page covers the Pi Durable SDK you embed in your own app. To trace the Pi coding agent in your terminal, see Pi.
Getting started
1
Install
Requires
@lmnr-ai/lmnr >= 0.8.57 and @earendil-works/pi-durable >= 1.4.0 (1.x).2
Set environment variables
Laminar initialization. If not specified,
Laminar will look for the key in the LMNR_PROJECT_API_KEY environment variable.3
Initialize Laminar with the pi-durable module
Pass the pi-durable module to This run produces one trace: a
Laminar.initialize() before the first Harness.open(). Laminar patches Harness.open, so every Harness you open gets Laminar’s telemetry, and Laminar’s extension is installed after the extensions already in its registry. The rest of your code stays as it is.pi.run root with the question and the answer, a pi.chat span for the request that called get_weather, a get_weather TOOL span, and a second pi.chat span for the final answer.instrumentModules as above. A few cases where the patch steps aside:
- If you open a Harness with your own
telemetry, Laminar leaves that Harness alone. - If the registry already has an extension named
@lmnr-ai/lmnrthat isn’t Laminar’s, Laminar logs a warning and leaves it in place. - With Next.js, add
@earendil-works/pi-durabletoserverExternalPackages. When it’s bundled, the copy your app runs isn’t the one Laminar patched.
settings.extensions or configure({ extensions: [...] })), Laminar’s extension isn’t in that list, so its LLM and TOOL spans are missing. Use laminarPiDurable() below and include lmnr.extension in the list.
Set a user ID, metadata, or span names with laminarPiDurable()
instrumentModules traces every Harness with default settings. To set a user ID or metadata on your traces, rename spans, or control where Laminar’s extension goes, wire Laminar in yourself with laminarPiDurable(). It returns two things, and the Harness needs both:
lmnr.telemetrygoes toHarness.open({ telemetry }). It gives every run its trace and carries that trace across restarts.lmnr.extensiongoes into the registry. Its hooks record the LLM and TOOL spans.
Laminar.initialize() as usual, without instrumentModules. If you keep instrumentModules too, Laminar sees its own telemetry on this Harness and doesn’t patch it again.
Install lmnr.extension after every other extension, or list it last. Pi passes each beforeRequest hook the messages as the hooks before it changed them, so the LLM span shows exactly what was sent to the model only when Laminar’s hook runs last.
The extension is named @lmnr-ai/lmnr. Pi’s registry.install replaces an installed extension with the same name, so don’t give one of your own extensions that name.
Options
sessionId: a string, or a function of the conversation. Defaults to the session ID Pi keeps per conversation, so every run in a conversation lands in the same Laminar session.userId: a string, or a function of the conversation.metadata: key-value pairs added to every trace. See metadata.spanNames:{ run, llm, tool }. Defaults arepi.run,pi.chat, and the tool’s name.systemPrompt: report this system prompt on LLM spans instead of the one Pi sent.projectApiKeyortracerProvider: send Pi Durable traces to a different project than the rest of your app.Laminar.initialize()isn’t needed in that case; callawait lmnr.shutdown()before the process exits.
Runs that resume after a restart
When the process stops mid-run and a new one reopens the storage, Pi resumes the run from its last checkpoint. Laminar’s telemetry stores the run’s trace context with every submission and task Pi saves, so the resumed model requests and tool calls land in the original trace, and the trace has a singlepi.run root whichever process finishes the run. Spans whose work was cut off by the stop are dropped rather than exported half done; a request that Pi sends again appears as a new pi.chat span.
The new process needs Laminar on its Harness too, through instrumentModules or laminarPiDurable().
Pi’s own pi.durable.* telemetry spans are not exported. Model requests run with tracing suppressed, so Laminar’s OpenAI, Anthropic, and Google GenAI instrumentations don’t record a second LLM span for the SDK calls pi-ai makes.
See what happened in a trace
Open the trace in Laminar and the default view is the transcript: the user’s input at the top, each model turn with a preview of its response, and every tool call with its arguments and result, in the order the run made them. Switch to tree view when you want span-by-span structure. More on the trace UX: Viewing traces.Track outcomes with Signals
Traces answer what happened on this run. Signals answer the cross-trace question: how often does the agent answer without calling the tool it needs, which runs end unanswered, which conversations loop on the same tool call. A Signal pairs a plain-language instruction with a JSON output schema. Laminar runs it live on new traces (Triggers) or backfills it across history (Jobs), and records a structured event every time it matches. From there you query, cluster, and alert on events across every trace.Every new project ships with a Failure Detector Signal that categorizes issues on any trace over 1000 tokens. Open it from the Signals sidebar to see events as soon as your Pi Durable traces arrive.
Query across traces
- SQL editor for ad-hoc queries across traces, spans, signals, and evals.
- SQL API for programmatic access from scripts and pipelines.
- CLI (
lmnr-cli sql query) for terminal-driven queries. - MCP server to query Laminar from Claude Code, Cursor, or Codex.
Troubleshooting
I don't see any traces
I don't see any traces
- Confirm
LMNR_PROJECT_API_KEYis set in the process that opens the Harness. - With
instrumentModules, callLaminar.initialize()before the firstHarness.open(), and check that your bundler doesn’t bundle@earendil-works/pi-durable. - With
laminarPiDurable(), passtelemetry: lmnr.telemetrytoHarness.open(). Without it, Laminar’s hooks log a warning once and record nothing. - Call
await Laminar.shutdown()(orawait lmnr.shutdown()withprojectApiKey) before the process exits so buffered spans are sent.
I see a pi.run root but no LLM or TOOL spans
I see a pi.run root but no LLM or TOOL spans
Laminar’s telemetry is on the Harness but its extension doesn’t run.
- If the conversation selects its extensions explicitly (
settings.extensionsorconfigure({ extensions: [...] })), switch tolaminarPiDurable()and includelmnr.extensionin that list. - With
laminarPiDurable(), check thatlmnr.extensionis installed in the registry.
LLM spans don't show a change another extension makes to the request
LLM spans don't show a change another extension makes to the request
Laminar’s extension runs before that extension. With
instrumentModules, install your extensions before Harness.open(). With laminarPiDurable(), install lmnr.extension last.Self-hosting Laminar
Self-hosting Laminar
Set
baseUrl and the ports of your instance when initializing. For a local OSS deployment:What’s next
Viewing traces
Read the transcript view, filter, and search across traces.
Signals
Detect behaviors and failures across every run, then query, cluster, and alert on them.
SQL editor and MCP server
Query traces programmatically from the UI, API, or your IDE.
Tracing structure
Sessions, user IDs, metadata, and tags.
Related integrations
Pi
Trace the Pi coding agent in your terminal with a one-command extension install.
eve
Trace eve durable agents, their turns, and tool calls.
Mastra
Trace Mastra agents and workflows in TypeScript.
Temporal
Trace agents that run as Temporal workflows.
Vercel AI SDK
Trace
generateText and streamText in Next.js and Node.js.All integrations
Browse every provider, framework, coding agent, and browser integration Laminar supports.