Skip to main content

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 one Laminar.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

To get the project API key, go to the Laminar dashboard, click the project settings, and generate a project API key. This is available both in the cloud and in the self-hosted version of Laminar.Specify the key at 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 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.
This run produces one trace: a 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.
pi-durable is ESM-only, so Laminar can’t patch it when it’s imported: pass the module to 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/lmnr that isn’t Laminar’s, Laminar logs a warning and leaves it in place.
  • With Next.js, add @earendil-works/pi-durable to serverExternalPackages. When it’s bundled, the copy your app runs isn’t the one Laminar patched.
If a conversation selects its extensions explicitly (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.telemetry goes to Harness.open({ telemetry }). It gives every run its trace and carries that trace across restarts.
  • lmnr.extension goes into the registry. Its hooks record the LLM and TOOL spans.
Call 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 are pi.run, pi.chat, and the tool’s name.
  • systemPrompt: report this system prompt on LLM spans instead of the one Pi sent.
  • projectApiKey or tracerProvider: send Pi Durable traces to a different project than the rest of your app. Laminar.initialize() isn’t needed in that case; call await 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 single pi.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

  • Confirm LMNR_PROJECT_API_KEY is set in the process that opens the Harness.
  • With instrumentModules, call Laminar.initialize() before the first Harness.open(), and check that your bundler doesn’t bundle @earendil-works/pi-durable.
  • With laminarPiDurable(), pass telemetry: lmnr.telemetry to Harness.open(). Without it, Laminar’s hooks log a warning once and record nothing.
  • Call await Laminar.shutdown() (or await lmnr.shutdown() with projectApiKey) before the process exits so buffered spans are sent.
Laminar’s telemetry is on the Harness but its extension doesn’t run.
  • If the conversation selects its extensions explicitly (settings.extensions or configure({ extensions: [...] })), switch to laminarPiDurable() and include lmnr.extension in that list.
  • With laminarPiDurable(), check that lmnr.extension is installed in the registry.
Laminar’s extension runs before that extension. With instrumentModules, install your extensions before Harness.open(). With laminarPiDurable(), install lmnr.extension last.
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.

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.