> ## Documentation Index
> Fetch the complete documentation index at: https://laminar.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pi Durable observability

> Trace Pi Durable Harness runs: one trace per run with every model request and tool call, continued in the same trace after a restart.

## Overview

Laminar is an open-source, OpenTelemetry-native observability platform for AI agents. Trace, debug, and monitor every [Pi Durable](https://www.npmjs.com/package/@earendil-works/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.

> [!NOTE]
>
> This page covers the Pi Durable SDK you embed in your own app. To trace the Pi coding agent in your terminal, see [Pi](https://laminar.sh/docs/integrations/pi.md).

## Getting started

**1. Install**

Requires `@lmnr-ai/lmnr >= 0.8.57` and `@earendil-works/pi-durable >= 1.4.0` (1.x).

```bash theme={null}
npm install @lmnr-ai/lmnr @earendil-works/pi-durable @earendil-works/pi-ai @earendil-works/chord
```

**2. Set environment variables**

```bash theme={null}
# .env
LMNR_PROJECT_API_KEY=your-laminar-project-api-key
OPENAI_API_KEY=your-openai-api-key
```

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.

```typescript {5,8,10,60} theme={null}
import 'dotenv/config';
import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';
import { createModels } from '@earendil-works/pi-ai/models';
import { openaiProvider } from '@earendil-works/pi-ai/providers/openai';
import * as piDurable from '@earendil-works/pi-durable';
import { AssistantEntry, createRegistry, Harness } from '@earendil-works/pi-durable';
import { openNodeSqliteStorage } from '@earendil-works/pi-durable/storage/sqlite/node';
import { Laminar } from '@lmnr-ai/lmnr';

Laminar.initialize({ instrumentModules: { piDurable } });

const context = BACKGROUND_CONTEXT;

const weather = {
  name: 'get_weather',
  description: 'Get the current weather in a city',
  parameters: {
    type: 'object',
    properties: { city: { type: 'string' } },
    required: ['city'],
  },
  execute: async ({ city }) => ({
    output: [{ type: 'text', text: `Sunny and 22°C in ${city}` }],
  }),
};

const app = {
  name: 'weather-app',
  tools: [weather],
  sections: [{ key: 'preamble', render: () => 'You are a concise weather assistant.', tag: false }],
};

const registry = createRegistry();
registry.install(app);

const models = createModels();
models.setProvider(openaiProvider());

const harness = await Harness.open(
  await openNodeSqliteStorage('./session.sqlite'),
  { models, registry },
  context,
);

const root = await harness.root(context, {
  agent: { model: { provider: 'openai', modelId: 'gpt-5.6-terra' } },
});

const submission = await root.submit(
  { type: 'input', content: "What's the weather in Paris?" },
  context,
);
const settled = await submission.wait(context);
if (settled.status === 'done' && settled.type === 'input') {
  const answer = await root.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
  console.log(answer?.model?.[0]);
}

await harness.close(context);
await Laminar.shutdown();
```

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.

```typescript {1,3-6,10,14} theme={null}
import { Laminar, laminarPiDurable } from '@lmnr-ai/lmnr';

const lmnr = laminarPiDurable({
  userId: 'user-123',
  metadata: { app: 'weather-bot' },
});

const registry = createRegistry();
registry.install(app);
registry.install(lmnr.extension); // install Laminar's extension last

const harness = await Harness.open(
  await openNodeSqliteStorage('./session.sqlite'),
  { models, registry, telemetry: lmnr.telemetry },
  context,
);
```

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

```typescript theme={null}
const lmnr = laminarPiDurable({
  sessionId: (conversation) => `chat-${conversation.conversationId}`, // default: Pi's session ID
  userId: 'user-123',
  metadata: { app: 'support-bot', env: 'prod' },
  spanNames: { run: 'support.run', llm: 'support.llm' },
  systemPrompt: 'You are a support assistant.',
});
```

* `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](https://laminar.sh/docs/tracing/structure/sessions.md).
* `userId`: a string, or a function of the conversation.
* `metadata`: key-value pairs added to every trace. See [metadata](https://laminar.sh/docs/tracing/structure/metadata.md).
* `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](https://laminar.sh/docs/platform/viewing-traces.md).

## Track outcomes with Signals

Traces answer *what happened on this run*. **[Signals](https://laminar.sh/docs/signals/introduction.md) 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](https://laminar.sh/docs/platform/sql-editor.md), [cluster](https://laminar.sh/docs/signals/clusters.md), and [alert](https://laminar.sh/docs/signals/alerts.md)** on events across every trace.

> [!NOTE]
>
> 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](https://laminar.sh/docs/platform/sql-editor.md)** for ad-hoc queries across traces, spans, signals, and evals.
* **SQL API** for programmatic access from scripts and pipelines.
* **[CLI](https://laminar.sh/docs/platform/cli.md)** (`lmnr-cli sql query`) for terminal-driven queries.
* **[MCP server](https://laminar.sh/docs/platform/mcp.md)** to query Laminar from Claude Code, Cursor, or Codex.

## Troubleshooting

> [accordion] I don't see any traces
>
> * 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.

> [accordion] 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.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.

> [accordion] 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.

> [accordion] Self-hosting Laminar
>
> Set `baseUrl` and the ports of your instance when initializing. For a local OSS deployment:
>
> ```typescript theme={null}
> Laminar.initialize({
>   baseUrl: 'http://localhost',
>   httpPort: 8000,
>   grpcPort: 8001,
> });
> ```

## What's next

> [card] [Viewing traces](https://laminar.sh/docs/platform/viewing-traces.md)
>
> Read the transcript view, filter, and search across traces.

> [card] [Signals](https://laminar.sh/docs/signals/introduction.md)
>
> Detect behaviors and failures across every run, then query, cluster, and alert on them.

> [card] [SQL editor and MCP server](https://laminar.sh/docs/platform/sql-editor.md)
>
> Query traces programmatically from the UI, API, or your IDE.

> [card] [Tracing structure](https://laminar.sh/docs/tracing/structure/overview.md)
>
> Sessions, user IDs, metadata, and tags.

## Related integrations

> [card] [Pi](https://laminar.sh/docs/integrations/pi.md)
>
> Trace the Pi coding agent in your terminal with a one-command extension install.

> [card] [eve](https://laminar.sh/docs/integrations/eve.md)
>
> Trace eve durable agents, their turns, and tool calls.

> [card] [Mastra](https://laminar.sh/docs/integrations/mastra.md)
>
> Trace Mastra agents and workflows in TypeScript.

> [card] [Temporal](https://laminar.sh/docs/integrations/temporal.md)
>
> Trace agents that run as Temporal workflows.

> [card] [Vercel AI SDK](https://laminar.sh/docs/integrations/vercel-ai-sdk.md)
>
> Trace `generateText` and `streamText` in Next.js and Node.js.

> [card] [All integrations](https://laminar.sh/docs/integrations.md)
>
> Browse every provider, framework, coding agent, and browser integration Laminar supports.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.