> ## 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.

# Observability for OpenCode coding agents

## Overview

[OpenCode](https://opencode.ai/) is an open-source AI coding agent. Its [`@opencode-ai/sdk`](https://opencode.ai/docs/sdk/) package lets you call OpenCode programmatically — spin up a server, drive a session, and build products on top of the agent instead of using the TUI. Laminar makes those runs observable.

What Laminar captures:

* Every conversation turn, one trace per turn.
* LLM prompts, responses, token counts, latency, and cost.
* Tool calls (`bash`, `read`, `write`, `edit`, custom tools) with arguments and results.
* Sub-agents (the Title Generator, task runners) as nested spans.
* When you wrap `client.session.prompt` in `observe`, the server-side turn nests under your caller span, so the entire call stack lands in a single trace.

Each trace carries the OpenCode session ID as its Laminar session ID, so every turn in the same OpenCode conversation shows up under one session in Laminar.

## Calling OpenCode from TypeScript

Laminar ships two pieces that cooperate:

* **`@lmnr-ai/lmnr`** patches `@opencode-ai/sdk` on the caller side. When you wrap a `client.session.prompt` call in `observe`, the instrumentation injects the current span context into the request.
* **`@lmnr-ai/opencode-plugin`** runs inside the OpenCode server, picks up that context, and emits the turn, LLM, and tool-call spans as children of your caller span.

The result: one trace per turn that spans from your `observe` block into the server's `turn` span and all the way down to each LLM call.

<Steps>
  <Step title="Install">
    ```bash theme={null}
    npm install @lmnr-ai/lmnr@latest @opencode-ai/sdk@latest
    ```
  </Step>

  <Step title="Add the Laminar plugin to opencode.json">
    The caller-side instrumentation only injects context; the server-side plugin is what actually emits the turn and tool spans. Create (or edit) `opencode.json` next to your code:

    ```json opencode.json theme={null}
    {
      "$schema": "https://opencode.ai/config.json",
      "plugin": ["@lmnr-ai/opencode-plugin"]
    }
    ```

    The JSON key is `plugin`, not `plugins`. OpenCode will reject the file otherwise.

    <Note>
      OpenCode reads config from [multiple locations](https://opencode.ai/docs/config/#locations). The closest one wins: a file in the current project overrides `~/.config/opencode/opencode.json`. If you want tracing on by default everywhere, put the plugin block in the global file. See [OpenCode's plugin docs](https://opencode.ai/docs/config/#plugins) for the full layering rules.
    </Note>
  </Step>

  <Step title="Set LMNR_PROJECT_API_KEY">
    The plugin reads `LMNR_PROJECT_API_KEY` from the environment of whatever process starts the OpenCode server. When you use `opencode.createOpencode()`, that's the same process that runs your TypeScript code, so exporting it in your shell is enough:

    ```bash theme={null}
    export LMNR_PROJECT_API_KEY=your-laminar-project-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.

    <Tip>
      Self-hosting Laminar? Also set `LMNR_BASE_URL` and `LMNR_GRPC_PORT` to point at your instance. For the OSS default, that's `http://localhost` and `8001`.
    </Tip>
  </Step>

  <Step title="Initialize Laminar with the opencode module">
    ```typescript {1,2,5-9,16} theme={null}
    import * as opencode from "@opencode-ai/sdk";
    import { Laminar, observe } from "@lmnr-ai/lmnr";

    // Initialize Laminar once at the entry point.
    Laminar.initialize({
      instrumentModules: {
        opencode,
      },
    });

    const { client, server } = await opencode.createOpencode();

    try {
      const sessionRes = await client.session.create({ body: { title: "agent run" } });

      await observe({ name: "my-agent-step" }, async () => {
        await client.session.prompt({
          path: { id: sessionRes.data.id },
          body: {
            model: { providerID: "anthropic", modelID: "claude-haiku-4-5" },
            parts: [{ type: "text", text: "Create a Python factorial function and test it." }],
          },
        });
      });
    } finally {
      server.close();
      await Laminar.shutdown();
    }
    ```

    <Note>
      If you import `@opencode-ai/sdk` before `Laminar.initialize()` (common when the project has `"type": "module"` in `package.json`), your local reference won't be auto-wrapped. Pass the module via `instrumentModules` as shown above; the instrumentation attaches to the SDK's `Session` class and every new `OpencodeClient` picks it up.
    </Note>
  </Step>
</Steps>

## Trace structure

Because the caller-side instrumentation hands the span context to the server-side plugin, the trace comes back as a single tree: your `observe` span is the root, the server's `turn` span is its child, and every `session.llm`, model call, and tool call hangs off `turn`. There is no separate trace for the server — everything the OpenCode process does on this turn shows up nested under the TypeScript code that kicked it off.

The tree view makes that nesting obvious:

<Frame caption="Tree view: the outer my-agent-step span from the TypeScript caller is the parent of the server's turn, which wraps every session.llm, model call, and tool call (write, bash) the agent made on this turn.">
  <img src="https://mintcdn.com/laminarai/P-3ZqFud3nr77I2g/images/traces/opencode-sdk-tree.png?fit=max&auto=format&n=P-3ZqFud3nr77I2g&q=85&s=976e2c7ec71661558a6b7e894d616a51" alt="OpenCode SDK trace in Laminar, tree view" width="1512" height="982" data-path="images/traces/opencode-sdk-tree.png" />
</Frame>

Switch to transcript view on the same trace to read the conversation — user prompt, model output, tool calls, and tool results read as a dialogue instead of a span tree. More on the trace UX: [Viewing traces](/platform/viewing-traces).

<Frame caption="Same trace in transcript view: the turn reads as a dialogue with the model responses and tool invocations inline.">
  <img src="https://mintcdn.com/laminarai/P-3ZqFud3nr77I2g/images/traces/opencode-sdk.png?fit=max&auto=format&n=P-3ZqFud3nr77I2g&q=85&s=e14059176cddefb927220cbb7a4496be" alt="OpenCode SDK trace in Laminar, transcript view" width="1512" height="982" data-path="images/traces/opencode-sdk.png" />
</Frame>

## Using the OpenCode CLI

The plugin works the same way on a standalone `opencode` CLI run: add it to `opencode.json`, export `LMNR_PROJECT_API_KEY`, and every conversation turn gets traced. There's no TypeScript caller to be the parent span, so each turn becomes its own top-level trace.

<Steps>
  <Step title="Install the OpenCode CLI">
    Follow [OpenCode's install guide](https://opencode.ai/docs/#install). You do not need to install the plugin package yourself; OpenCode resolves it from npm on first run.
  </Step>

  <Step title="Configure the plugin and key">
    Use the same `opencode.json` plugin entry as the SDK section above, and export `LMNR_PROJECT_API_KEY` in the shell you launch `opencode` from. You can also put the key in a `.env` file in the project directory; OpenCode loads it automatically.
  </Step>

  <Step title="Run opencode">
    ```bash theme={null}
    opencode
    ```

    The first run downloads the plugin. On startup you'll see a log line like `Laminar tracing initialized → https://api.lmnr.ai`. The plugin also flips OpenCode's experimental OpenTelemetry flag on for you, so you don't need to touch the OTel config.
  </Step>
</Steps>

<Frame caption="CLI run traced on its own: the turn span is the root, wrapping the model response, the write tool call, and the bash tool call for a Python factorial task.">
  <img src="https://mintcdn.com/laminarai/e-0mEj5oSezdTWn-/images/traces/opencode.png?fit=max&auto=format&n=e-0mEj5oSezdTWn-&q=85&s=1547dbb9d0d3ccd39805a6ec29df6ba2" alt="OpenCode CLI trace in Laminar" width="1512" height="982" data-path="images/traces/opencode.png" />
</Frame>

## Track outcomes with Signals

Traces answer *what happened on this turn*. **[Signals](/signals/introduction) answer the cross-trace question**: *how often does the agent edit a file it wasn't asked to, when does the bash tool run a destructive command, which turns burn tokens without touching a file*. A Signal pairs a plain-language prompt 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](/platform/sql-editor), [cluster](/signals/clusters), and [alert](/signals/alerts) 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 OpenCode traces arrive.
</Note>

## Query across traces

* **[SQL editor](/platform/sql-editor)** for ad-hoc queries across traces, spans, signals, and evals.
* **SQL API** for programmatic access from scripts and pipelines.
* **[CLI](/platform/cli)** (`lmnr-cli sql query`) for terminal-driven queries and piping JSON into shell tools or coding agents.
* **[MCP server](/platform/mcp)** to query Laminar directly from OpenCode, Claude Code, Cursor, or Codex.

## Troubleshooting

<AccordionGroup>
  <Accordion title="I don't see any traces in Laminar">
    * `LMNR_PROJECT_API_KEY` must be set in the environment of the process that launches the OpenCode server, not just in the process that creates the OpenCode client. The plugin runs on the server side and reads the key there.
    * Check the OpenCode startup log for `Laminar tracing initialized → ...`. If you see `LMNR_PROJECT_API_KEY not set, skipping plugin initialization`, the plugin loaded but had no key.
    * Confirm `opencode.json` uses the key `"plugin"` (singular). `"plugins"` silently does nothing.
  </Accordion>

  <Accordion title="Traces from my TypeScript caller aren't linked to the server turn">
    * Make sure both sides are on a current `@lmnr-ai/lmnr` (install with `@latest`). The caller-side instrumentation and the plugin's context extraction need to match up.
    * The server process also needs the plugin in `opencode.json`. Without it, the caller-side span lands in Laminar but the server spans land in their own trace.
    * Wrap the `client.session.prompt` call in `observe` (or another active span). The SDK instrumentation only injects the context if there is one.
  </Accordion>

  <Accordion title="Self-hosting Laminar">
    Export `LMNR_BASE_URL` and `LMNR_GRPC_PORT` in the same environment as `LMNR_PROJECT_API_KEY`. For a local OSS instance, that's `LMNR_BASE_URL=http://localhost` and `LMNR_GRPC_PORT=8001`. The plugin passes these through when it initializes Laminar.
  </Accordion>
</AccordionGroup>

## What's next

<CardGroup cols={2}>
  <Card title="Viewing traces" href="/platform/viewing-traces">
    Read the transcript view, filter, and search across traces.
  </Card>

  <Card title="Signals" href="/signals/introduction">
    Detect behaviors and failures across every OpenCode turn, then query, cluster, and alert on them.
  </Card>

  <Card title="SQL editor and MCP server" href="/platform/sql-editor">
    Query traces programmatically from the UI, API, or your IDE.
  </Card>

  <Card title="Tracing structure" href="/tracing/structure/overview">
    Sessions, metadata, and tags for deeper control.
  </Card>

  <Card title="Claude Agent SDK" href="/tracing/integrations/claude-agent-sdk">
    Running OpenCode alongside Claude Agent SDK? Trace both here.
  </Card>
</CardGroup>
