Skip to main content

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.

Overview

Laminar is an open-source, OpenTelemetry-native observability platform for AI agents. Trace, debug, and monitor every Pydantic AI agent run, model turn, and tool call with a single Laminar.initialize() call. Self-host via Helm or use managed cloud. Pydantic AI is a Python agent framework built by the Pydantic team. Agent drives the loop, typed tools are added with @agent.tool, and the framework emits its own OpenTelemetry GenAI semconv spans for agent runs, model calls, and tool executions. Laminar wires those spans to its tracer provider so the full conversation lands in your project with no manual exporter setup. What Laminar captures:
  • The root agent run span with the prompt you sent and the final output.
  • Every chat <model> turn with prompts, responses, token counts, latency, and cost.
  • Every execute_tool <name> call with arguments and return value.
  • Multi-agent handoffs and sub-agent runs nested under the call that spawned them.

Getting started

1

Install

Ensure you have lmnr version 0.7.49 or higher and pydantic-ai-slim >= 1.0 (or the full pydantic-ai distribution):
pip install -U lmnr "pydantic-ai-slim>=1.0"
2

Set environment variables

export LMNR_PROJECT_API_KEY=your-laminar-project-api-key
export OPENAI_API_KEY=your-openai-api-key
3

Initialize Laminar

Laminar.initialize() auto-instruments Pydantic AI when pydantic-ai-slim or pydantic-ai is importable. No Agent.instrument_all() call is needed: Laminar installs itself as the default instrumentation for every Agent you construct afterwards.
import asyncio

from pydantic_ai import Agent
from lmnr import Laminar, observe

Laminar.initialize()

agent = Agent(
    "openai:gpt-5-mini",
    system_prompt="You are a concise assistant. Answer in one sentence.",
)

@observe(name="capital-lookup")
async def main():
    result = await agent.run("What is the capital of France?")
    print(result.output)

if __name__ == "__main__":
    asyncio.run(main())
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.
When Laminar auto-enables the Pydantic AI instrument, it auto-removes the overlapping raw-provider instrumentors (OpenAI, Anthropic, Google GenAI, Groq, Mistral, Cohere, Bedrock) from the default set. Pydantic AI already emits GenAI-semconv spans at the model abstraction layer, so running the provider instrumentors alongside would double-count every model call. If you call the provider SDK directly elsewhere in the same process and want both traced, pass an explicit instruments set to Laminar.initialize(instruments={Instruments.PYDANTIC_AI, Instruments.OPENAI, ...}).

See what happened in a trace

Open a multi-agent trace and switch to tree view to see how Pydantic AI nests sub-agents and tool calls. The example below is a concierge agent that delegates flight and hotel booking to two sub-agents, each running its own model turns and tool calls, so the full hierarchy is visible at a glance.
Pydantic AI multi-agent trace in Laminar, tree view showing nested sub-agents
More on the trace UX: Viewing traces.

Multi-agent with tool delegation

Register tools with @agent.tool_plain (no dependency state) or @agent.tool (with injected RunContext). A coordinator agent can delegate to specialist sub-agents by calling Agent.run() inside a tool. Pydantic AI emits an execute_tool <name> span for each invocation, and Laminar nests the sub-agent’s run underneath it so the hierarchy mirrors the conversation.
import asyncio

from pydantic_ai import Agent, RunContext
from lmnr import Laminar, observe

Laminar.initialize()

flight_agent = Agent(
    "openai:gpt-5-mini",
    system_prompt="You are a flight search specialist. Return one recommended flight.",
)

@flight_agent.tool_plain
def search_flights(origin: str, destination: str, date: str) -> str:
    return f"Options {origin}->{destination} on {date}: DL204 Delta $412, UA880 United $438."

hotel_agent = Agent(
    "openai:gpt-5-mini",
    system_prompt="You are a hotel booking specialist. Return one recommended hotel.",
)

@hotel_agent.tool_plain
def search_hotels(city: str, check_in: str, check_out: str) -> str:
    return f"Hotels in {city}: Kimpton Gray $285/nt, Hyatt Place $199/nt."

concierge = Agent(
    "openai:gpt-5-mini",
    system_prompt="You are a travel concierge. Use the tools to book a flight and a hotel.",
)

@concierge.tool
async def book_flight(ctx: RunContext, origin: str, destination: str, date: str) -> str:
    result = await flight_agent.run(f"Find a flight from {origin} to {destination} on {date}.")
    return result.output

@concierge.tool
async def book_hotel(ctx: RunContext, city: str, check_in: str, check_out: str) -> str:
    result = await hotel_agent.run(f"Find a hotel in {city} from {check_in} to {check_out}.")
    return result.output

@observe(name="plan-trip")
async def main():
    result = await concierge.run(
        "Plan a trip from JFK to Chicago on 2026-05-12, returning 2026-05-15."
    )
    print(result.output)

if __name__ == "__main__":
    asyncio.run(main())
The transcript view reads as a conversation: the input prompt, each book_flight / book_hotel tool call with its sub-agent’s input and output, and the final concierge summary.
Pydantic AI multi-agent trace in Laminar, transcript view

Track outcomes with Signals

Traces answer what happened on this run. Signals answer the cross-trace question: how often does the agent call a tool with missing arguments, when does the model hallucinate a booking code, how many runs exceed three turns without a final answer. 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, 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 Pydantic AI 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 and piping JSON into shell tools or coding agents.
  • MCP server to query Laminar directly from Claude Code, Cursor, or Codex.

Troubleshooting

  • Confirm LMNR_PROJECT_API_KEY is set in the same process that runs the agent.
  • pydantic-ai-slim (or pydantic-ai) must be importable when Laminar.initialize() runs. Install with pip install "pydantic-ai-slim>=1.0".
  • The integration requires pydantic-ai-slim >= 1.0.0 and lmnr >= 0.7.49.
Laminar auto-removes the raw provider instrumentors (OpenAI, Anthropic, Google GenAI, Groq, Mistral, Cohere, Bedrock) when Pydantic AI is auto-enabled, so this should not happen with a default setup. If you passed an explicit instruments set that includes both Instruments.PYDANTIC_AI and one of the overlapping providers, drop the provider entry or use disabled_instruments={Instruments.PYDANTIC_AI} to keep only the raw spans.
Pass disabled_instruments={Instruments.PYDANTIC_AI} to Laminar.initialize(). The raw provider instrumentors will be auto-enabled again.
from lmnr import Laminar, Instruments

Laminar.initialize(disabled_instruments={Instruments.PYDANTIC_AI})
Set base_url and the ports of your instance when initializing. For a local OSS deployment:
Laminar.initialize(
    base_url="http://localhost",
    http_port=8000,
    grpc_port=8001,
)

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, metadata, and tags for deeper control.

OpenAI SDK

Using the OpenAI SDK directly without Pydantic AI? Trace it here.