> ## 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 Claude Agent SDK

## Overview

Laminar is an open-source, OpenTelemetry-native observability platform for the Claude Agent SDK. Trace, debug, and monitor every agent turn, tool call, and subagent spawned by `ClaudeSDKClient.query` and the module-level `query` function. Self-host via Helm or use managed cloud.

[Claude Agent SDK](https://docs.claude.com/en/api/agent-sdk/overview) (formerly Claude Code SDK) lets you call the same agent that powers Claude Code from your own code. It handles process management, tool execution, and model calls, and exposes a `query` function (TypeScript) and `ClaudeSDKClient` (Python) for driving a session.

Laminar captures the full structure of each run: the root call, every model turn, each tool call, and every subagent spawned via the `Agent` tool. Nested subagents show up as child spans under the invocation that spawned them, so you can read the whole agent tree in one trace.

What Laminar captures:

* The root call (`ClaudeSDKClient.query` / module-level `query`) with the prompt you sent.
* Every model turn with prompts, responses, token counts, latency, and cost.
* Tool calls (`Read`, `Write`, `Edit`, `Bash`, `Glob`, `Grep`, `WebSearch`, custom MCP tools) with arguments and results.
* Subagents spawned via the `Agent` tool as nested spans under the parent turn.

## Getting started

<Tabs items={['TypeScript', 'Python']}>
  <Tab title="TypeScript">
    <Steps>
      <Step title="Install">
        Ensure you are using `@lmnr-ai/lmnr` version `0.7.10` or higher.

        ```bash theme={null}
        npm install @lmnr-ai/lmnr@latest @anthropic-ai/claude-agent-sdk@latest
        # or
        pnpm add @lmnr-ai/lmnr@latest @anthropic-ai/claude-agent-sdk@latest
        ```
      </Step>

      <Step title="Set environment variables">
        ```bash theme={null}
        export LMNR_PROJECT_API_KEY=your-laminar-project-api-key
        export ANTHROPIC_API_KEY=your-anthropic-api-key
        ```
      </Step>

      <Step title="Wrap the query function">
        ```typescript {2,5,8} theme={null}
        import { query as origQuery } from '@anthropic-ai/claude-agent-sdk';
        import { Laminar } from '@lmnr-ai/lmnr';

        Laminar.initialize();

        // Wrap the original query function to capture every turn, tool call, and subagent.
        const query = Laminar.wrapClaudeAgentQuery(origQuery);

        async function run() {
          for await (const message of query({
            prompt: "Scan the current directory for TODOs and create a summary markdown file.",
          })) {
            // Messages stream in; Laminar records the full span tree.
          }
        }
        run();
        ```

        <Note>
          `query` is an async generator. You must iterate it (or collect it) for the agent to run to completion. If you need the return value before initialization, the module-level `instrumentClaudeAgentQuery(originalQuery)` export is equivalent to `Laminar.wrapClaudeAgentQuery`.
        </Note>
      </Step>
    </Steps>
  </Tab>

  <Tab title="Python">
    <Steps>
      <Step title="Install">
        ```bash theme={null}
        pip install -U lmnr claude-agent-sdk
        ```
      </Step>

      <Step title="Set environment variables">
        ```bash theme={null}
        export LMNR_PROJECT_API_KEY=your-laminar-project-api-key
        export ANTHROPIC_API_KEY=your-anthropic-api-key
        ```
      </Step>

      <Step title="Initialize Laminar">
        `Laminar.initialize()` auto-instruments `claude-agent-sdk` when the package is importable. No wrapping call is needed.

        ```python {4-6} theme={null}
        import asyncio

        from claude_agent_sdk import ClaudeSDKClient
        from lmnr import Laminar, observe

        Laminar.initialize()

        @observe()
        async def main():
            async with ClaudeSDKClient() as client:
                await client.query(
                    "Explain with examples how memoization speeds up recursive calls, "
                    "using Fibonacci as the example."
                )
                async for msg in client.receive_response():
                    print(msg)

        if __name__ == "__main__":
            asyncio.run(main())
        ```

        <Note>
          Wrapping your entry point in `@observe()` is optional but recommended: it creates a root span that captures inputs and outputs and makes the trace easy to find in the UI.
        </Note>
      </Step>
    </Steps>
  </Tab>
</Tabs>

## See what happened in a trace

Open the trace in Laminar and you land on the transcript view: each turn reads as a conversation, with the prompt, the model's response, tool calls inline with their inputs and outputs, and any subagents collapsed to their final output. A tree of span names tells you the shape of the run; the transcript tells you what actually happened.

<Frame caption="Transcript view: the user prompt, tool calls, and model responses read as a dialogue. Expand any span to see its full input and output.">
  <img src="https://mintcdn.com/laminarai/JVNqSDGCVymWBwzK/images/traces/claude-agent-sdk.png?fit=max&auto=format&n=JVNqSDGCVymWBwzK&q=85&s=a94c2dd89f055226730256b497f157e2" alt="Claude Agent SDK trace in Laminar, transcript view" width="1280" height="720" data-path="images/traces/claude-agent-sdk.png" />
</Frame>

More on the trace UX: [Viewing traces](/platform/viewing-traces).

## Multi-agent runs

The Claude Agent SDK lets you define subagents programmatically with the `agents` option. Each subagent gets its own system prompt and tool allowlist, and the orchestrator invokes them via the `Agent` tool (formerly `Task`). In Laminar, every subagent invocation becomes a child span of the turn that spawned it, with its own LLM calls and tool calls nested underneath.

<Tabs items={['TypeScript', 'Python']}>
  <Tab title="TypeScript">
    ```typescript theme={null}
    import { query as origQuery } from '@anthropic-ai/claude-agent-sdk';
    import { Laminar } from '@lmnr-ai/lmnr';

    Laminar.initialize();
    const query = Laminar.wrapClaudeAgentQuery(origQuery);

    for await (const _ of query({
      prompt: `Review fizzbuzz.py. Kick off three subagents in parallel:
    1. researcher: summarize what the code does.
    2. reviewer: flag bugs, naming, and missing edge cases.
    3. test-runner: propose 3-5 unit tests.
    Combine their findings into a short report.`,
      options: {
        allowedTools: ['Read', 'Glob', 'Grep', 'Agent'],
        agents: {
          researcher: {
            description: 'Summarizes what code does. Use for investigation and overview.',
            prompt: 'Read the code and describe in 3-5 bullets what it does, its inputs, and outputs.',
            tools: ['Read', 'Glob', 'Grep'],
          },
          reviewer: {
            description: 'Reviews code for quality, bugs, and maintainability.',
            prompt: 'Identify bugs, unclear naming, missing edge cases, and style issues. Respond with a numbered list.',
            tools: ['Read', 'Glob', 'Grep'],
          },
          'test-runner': {
            description: 'Designs unit tests. Use for test planning.',
            prompt: 'Propose 3-5 unit tests covering happy path and edge cases.',
            tools: ['Read', 'Glob', 'Grep'],
          },
        },
      },
    })) {
      // stream through messages
    }
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    import asyncio
    from claude_agent_sdk import query, ClaudeAgentOptions, AgentDefinition
    from lmnr import Laminar, observe

    Laminar.initialize()

    @observe(name="multi-agent-code-review")
    async def main():
        options = ClaudeAgentOptions(
            allowed_tools=["Read", "Glob", "Grep", "Agent"],
            agents={
                "researcher": AgentDefinition(
                    description="Summarizes what code does. Use for investigation and overview.",
                    prompt="Read the code and describe in 3-5 bullets what it does, its inputs, and outputs.",
                    tools=["Read", "Glob", "Grep"],
                ),
                "reviewer": AgentDefinition(
                    description="Reviews code for quality, bugs, and maintainability.",
                    prompt="Identify bugs, unclear naming, missing edge cases, and style issues. Respond with a numbered list.",
                    tools=["Read", "Glob", "Grep"],
                ),
                "test-runner": AgentDefinition(
                    description="Designs unit tests. Use for test planning.",
                    prompt="Propose 3-5 unit tests covering happy path and edge cases.",
                    tools=["Read", "Glob", "Grep"],
                ),
            },
        )

        prompt = (
            "Review fizzbuzz.py. Kick off three subagents in parallel (researcher, reviewer, "
            "test-runner) and combine their findings into a short report."
        )
        async for _ in query(prompt=prompt, options=options):
            pass

    asyncio.run(main())
    ```
  </Tab>
</Tabs>

<Note>
  Subagents are only invoked when `Agent` is in `allowedTools` / `allowed_tools`. Without it, the orchestrator cannot delegate and you'll see a single-threaded trace.
</Note>

The resulting trace renders each subagent as a branch under the turn that spawned it. Switch to tree view when you want to see the hierarchy at a glance:

<Frame caption="Tree view of a multi-agent run: the ClaudeSDKClient.query root contains the orchestrator turn and three Agent-tool subagents, each with its own LLM and tool calls nested underneath.">
  <img src="https://mintcdn.com/laminarai/JVNqSDGCVymWBwzK/images/traces/claude-agent-sdk-multi-agent.png?fit=max&auto=format&n=JVNqSDGCVymWBwzK&q=85&s=1fd2701e8d6e436c768d5342d8c53859" alt="Claude Agent SDK multi-agent trace in Laminar" width="1280" height="720" data-path="images/traces/claude-agent-sdk-multi-agent.png" />
</Frame>

## Track outcomes with Signals

Traces answer *what happened on this run*. **[Signals](/signals/introduction) answer the cross-trace question**: *how often does the agent edit a file it wasn't asked to, when does a subagent exceed five tool calls, which reviewers miss null-check bugs*. 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 Claude Agent SDK 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 Claude Code, Cursor, Codex, or any MCP-aware client.

## Troubleshooting

<AccordionGroup>
  <Accordion title="I don't see any traces in Laminar">
    * Confirm `LMNR_PROJECT_API_KEY` is set in the same process that runs the SDK.
    * In TypeScript, make sure you're iterating the async generator returned by `query`. The agent only runs while you consume messages.
    * In Python, `claude-agent-sdk` must be importable when `Laminar.initialize()` runs. Install it with `pip install claude-agent-sdk`.
  </Accordion>

  <Accordion title="Subagents aren't showing up as child spans">
    * Add `Agent` to `allowedTools` / `allowed_tools`. Without it the orchestrator falls back to doing everything in-line.
    * Define subagents on the `agents` option; otherwise the `Agent` tool has nothing to invoke.
    * The `Agent` tool was called `Task` in earlier Claude Agent SDK versions (\< 2.1.63). Upgrade to the latest SDK to match the examples above.
  </Accordion>

  <Accordion title="Self-hosting Laminar">
    Set `LMNR_BASE_URL` and the ports of your instance before initializing. For a local OSS deployment:

    ```bash theme={null}
    export LMNR_BASE_URL=http://localhost
    # Python
    # Laminar.initialize(base_url="http://localhost", http_port=8000, grpc_port=8001)
    ```
  </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 run, 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="OpenCode" href="/tracing/integrations/opencode">
    Running Claude Agent SDK alongside OpenCode? Trace both here.
  </Card>
</CardGroup>
