---
title: "Python SDK"
description: "Add 2-line agent runtime reliability to any Python agent"
---

<Note>
  Using LangChain, Langflow, LangGraph, or LangServe? See the dedicated [LangChain integration](/sdk/integrations/langchain) — `LangSightLangChainCallback` covers all four without wrapping individual MCP clients.
</Note>

## Installation

LangSight is already installed — the SDK is part of the main package:

```bash
pip install langsight
# or
uv add langsight
```

## Zero-code auto_patch()

The fastest path — patches OpenAI, Anthropic, and Gemini SDK classes globally. No `wrap_llm()` needed:

```python
import langsight

langsight.auto_patch()   # reads LANGSIGHT_URL, LANGSIGHT_API_KEY, LANGSIGHT_PROJECT_ID from env

from google import genai

async def run(question: str) -> str:
    async with langsight.session(agent_name="orchestrator") as session_id:
        client = genai.Client()   # automatically traced
        response = await client.aio.models.generate_content(
            model="gemini-2.5-flash",
            contents=[question],
        )
        return response.text
```

`langsight.session()` is an async context manager that:
- Generates a `session_id` (or use your own: `session(session_id="my-id")`)
- Tags all LLM calls inside the block with that session
- Calls `flush()` when the block exits

## Quick start with `init()` + explicit wrapping

```python
import langsight

ls = langsight.init()  # reads LANGSIGHT_URL, LANGSIGHT_API_KEY, LANGSIGHT_PROJECT_ID from env

# Wrap MCP sessions
traced = ls.wrap(mcp_session, server_name="my-server", session_id="sess-001")

# Wrap LLM clients (OpenAI, Anthropic, Gemini — auto-detected)
client = ls.wrap_llm(genai.Client(), agent_name="analyst", session_id="sess-001")
```

Set the environment variables:

```bash
export LANGSIGHT_URL=http://localhost:8000
export LANGSIGHT_API_KEY=lsk_your_key
export LANGSIGHT_PROJECT_ID=my-project
```

`init()` returns `None` if `LANGSIGHT_URL` is not set — safe to call unconditionally. All extra kwargs are forwarded to `LangSightClient` (e.g. `loop_detection=True`, `max_steps=25`).

## 2-line integration

```python
from langsight.sdk import LangSightClient

client = LangSightClient(
    url="http://localhost:8000",
    project_id="my-project",   # required for multi-project deployments
)
traced_mcp = client.wrap(mcp_session, server_name="my-server") # line 2

# All call_tool() calls are now traced automatically
result = await traced_mcp.call_tool("query", {"sql": "SELECT 1"})
```

## Full example

```python
import asyncio
from mcp import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
from langsight.sdk import LangSightClient

async def main():
    # Start LangSight client
    langsight = LangSightClient(
        url="http://localhost:8000",
        api_key="your-api-key",      # required in production (X-API-Key header)
        project_id="my-project",     # required for multi-project deployments
        redact_payloads=False,       # set True to omit input_args/output_result from spans
    )

    # Connect to your MCP server
    params = StdioServerParameters(command="python", args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()

            # Wrap the session — one line
            traced = langsight.wrap(
                session,
                server_name="postgres-mcp",
                agent_name="support-agent",    # optional metadata
                session_id="sess-abc123",       # optional metadata
            )

            # Use exactly like the original session
            result = await traced.call_tool("query", {"sql": "SELECT COUNT(*) FROM orders"})
            print(result)

asyncio.run(main())
```

## How it works

The `wrap()` method returns `MCPClientProxy` — a transparent proxy that intercepts two methods:

**`call_tool()`**:
1. Calls the original `call_tool()` method
2. Records start time, end time, and latency
3. Sends a `ToolCallSpan` to `POST /api/traces/spans` **asynchronously** (fire-and-forget)
4. Returns the original result unchanged

**`list_tools()`**:
1. Calls the original `list_tools()` method
2. Posts the returned tool names, descriptions, and input schemas to `PUT /api/servers/{server_name}/tools` **asynchronously** (fire-and-forget)
3. Returns the original result unchanged

This means the **Tools tab** in the MCP Servers catalog populates automatically the first time an instrumented agent runs — no health checker or manual registration is needed. Tools declared in the server schema but never called appear with `0 calls` and their description.

**Fail-open**: if LangSight is unreachable, both `call_tool()` and `list_tools()` still succeed and the error is logged. Your agents are never blocked by monitoring.

## Span metadata

```python
traced = langsight.wrap(
    session,
    server_name="my-mcp",      # required — matches MCPServer.name in config
    agent_name="my-agent",     # shown in investigate output
    session_id="sess-123",     # groups spans into a conversation
    trace_id="trace-abc",      # links to a parent trace
)
```

## PII redaction

Set `redact_payloads=True` on the client to omit `input_args` and `output_result` from every span. Use this when tool arguments or results may contain PII:

```python
langsight = LangSightClient(
    url="http://localhost:8000",
    project_id="my-project",
    redact_payloads=True,
)
```

You can also set this globally in `.langsight.yaml`:

```yaml
redact_payloads: true
```

<Warning>
  `redact_payloads: true` disables session replay payload inspection and Playground Replay. Span timing, status, and counts are still captured.
</Warning>

## LLM token and cost fields

When wrapping a session that calls an LLM, attach token counts and the model ID to spans for cost tracking (Phase 7):

```python
from langsight.sdk.models import ToolCallSpan, ToolCallStatus

await langsight.send_span(ToolCallSpan.record(
    server_name="openai-mcp",
    tool_name="chat_completion",
    started_at=started,
    status=ToolCallStatus.SUCCESS,
    input_tokens=512,
    output_tokens=128,
    model_id="gpt-4o",
))
```

These map directly to OTLP semantic conventions: `gen_ai.usage.input_tokens`, `gen_ai.usage.output_tokens`, `gen_ai.request.model`. If you are sending OTLP traces, LangSight extracts these attributes automatically — no SDK changes needed.

## `latency_ms` auto-computation

`latency_ms` on `ToolCallSpan` is now **optional**. If you omit it, LangSight auto-computes it from `ended_at - started_at` via a Pydantic `model_validator`. You can still pass an explicit value if you prefer — the validator only runs when `latency_ms` is `None`.

This means manual spans and OTLP ingestion no longer need to calculate latency separately.

## Manual spans

Record spans without wrapping a client:

```python
from datetime import UTC, datetime
from langsight.sdk.models import ToolCallSpan, ToolCallStatus

started = datetime.now(UTC)
# ... your tool call ...

await langsight.send_span(ToolCallSpan.record(
    server_name="my-server",
    tool_name="custom_tool",
    started_at=started,
    status=ToolCallStatus.SUCCESS,
))
```

## Multi-agent tracing

When Agent A delegates to Agent B, pass the `trace_id` and the parent agent's span ID so LangSight can reconstruct the full call tree.

### Recommended pattern

```python
from datetime import UTC, datetime
from langsight.sdk import LangSightClient
from langsight.sdk.models import ToolCallSpan

client = LangSightClient(url="http://localhost:8000")

async def orchestrator_agent():
    session_id = "sess-f2a9b1"
    trace_id = "trace-abc123"

    traced_jira = client.wrap(
        jira_session,
        server_name="jira-mcp",
        agent_name="orchestrator",
        session_id=session_id,
        trace_id=trace_id,
    )
    await traced_jira.call_tool("get_issue", {"id": "TASK-42"})

    handoff = ToolCallSpan.handoff_span(
        from_agent="orchestrator",
        to_agent="research-agent",
        started_at=datetime.now(UTC),
        session_id=session_id,
        trace_id=trace_id,
    )
    await client.send_span(handoff)

    await research_agent(
        session_id=session_id,
        trace_id=trace_id,
        parent_span_id=handoff.span_id,
    )

async def research_agent(session_id: str, trace_id: str, parent_span_id: str):
    traced_confluence = client.wrap(
        confluence_session,
        server_name="confluence-mcp",
        agent_name="research-agent",
        session_id=session_id,
        trace_id=trace_id,
        parent_span_id=parent_span_id,
    )
    await traced_confluence.call_tool("search", {"query": "returns policy"})
```

### How parent_span_id works

`parent_span_id` uses the same model as OpenTelemetry distributed tracing. Each span has a unique `span_id`. When a child agent sets `parent_span_id`, LangSight can reconstruct the full call tree by following parent-child relationships from the flat span storage. No separate tree structure is required — tree reconstruction is a recursive query at read time.

### Auto parent linking (LangChain/LangGraph)

When using the [LangChain callback](/sdk/integrations/langchain) in auto-detect mode, parent linking happens automatically. The callback maintains a thread-local tool stack so that when a supervisor tool calls `ainvoke()` on a sub-agent, the sub-agent's spans are linked to the parent tool call without any manual `parent_span_id` wiring. See the [LangChain integration](/sdk/integrations/langchain) for details.

## Silent MCP error detection

The MCP SDK returns errors as JSON-RPC responses (`isError=True` on the result object) instead of raising Python exceptions. Without LangSight, these errors pass silently through your agent code — the agent sees a "successful" call that actually returned an error.

LangSight's `wrap()` proxy detects `result.isError` and marks the span as `status=error`:

```python
traced = langsight.wrap(session, server_name="postgres-mcp")

# MCP server returns an error response (e.g., invalid SQL)
result = await traced.call_tool("query", {"sql": "SELEC * FROM"})
# result.isError == True
# LangSight records: status=error, error="MCP error response"
```

This means:
- Silent MCP errors show up as red in the dashboard trace view
- Session health tags correctly reflect failures (e.g., `tool_failure`)
- Alert rules fire on error rate thresholds
- Circuit breakers count silent errors toward their failure threshold

No configuration needed — silent error detection is always on.

## Direct LLM SDK tracing (wrap_llm)

For agents that call OpenAI, Anthropic, or Gemini SDKs directly (not through LangChain), use `wrap_llm()` to auto-trace LLM generation calls and tool use blocks from responses. See the dedicated [Direct SDK integration](/sdk/integrations/direct-sdk) page.

```python
from openai import OpenAI

client = langsight.wrap_llm(OpenAI(), agent_name="my-agent", session_id="sess-001")
response = client.chat.completions.create(model="gpt-4o", tools=[...], messages=[...])
# LLM generation span + tool_use spans auto-traced
```

## Prevention Guardrails

LangSight v0.3 adds a prevention layer that stops runaway agents before they waste tokens or cascade failures.

### Loop Detection

Detects when an agent calls the same tool with the same arguments repeatedly.

```python
client = LangSightClient(
    url="http://localhost:8000",
    loop_detection=True,     # enable loop detection
    loop_threshold=3,        # same tool+args 3 times = loop (default: 3)
    loop_action="terminate", # "terminate" (default) or "warn"
)
```

Raises `LoopDetectedError` when a loop is detected (with `action="terminate"`).

Detection patterns: **repetition** (same call N times), **ping-pong** (A→B→A→B→A), **retry-without-progress** (same error repeated).

### Budget Guardrails

Prevents sessions from exceeding cost, step count, or time budgets.

```python
client = LangSightClient(
    url="http://localhost:8000",
    max_steps=25,            # hard stop at 25 tool calls
    max_cost_usd=1.00,       # hard stop at $1 (requires pricing_table)
    max_wall_time_s=120,     # hard stop at 2 minutes
    budget_soft_alert=0.80,  # log warning at 80% of each limit
    pricing_table={          # optional: model_id → (input_per_1m, output_per_1m)
        "claude-sonnet-4-6": (3.00, 15.00),
        "gpt-4o": (2.50, 10.00),
    },
)
```

Raises `BudgetExceededError` when a limit is hit.

### Circuit Breaker

Automatically disables a failing MCP server after N consecutive failures.

```python
client = LangSightClient(
    url="http://localhost:8000",
    circuit_breaker=True,            # enable circuit breaker
    circuit_breaker_threshold=5,     # open after 5 consecutive failures (default)
    circuit_breaker_cooldown=60.0,   # disable for 60 seconds (default)
    circuit_breaker_half_open_max=2, # test with 2 calls before closing (default)
)
```

Raises `CircuitBreakerOpenError` when a tool call is blocked.

Per-server configuration in `.langsight.yaml` overrides SDK defaults:

```yaml
servers:
  - name: postgres-mcp
    transport: stdio
    command: python server.py
    circuit_breaker:
      failure_threshold: 3
      cooldown_seconds: 30
```

### Handling Prevention Errors

```python
from langsight.exceptions import LoopDetectedError, BudgetExceededError, CircuitBreakerOpenError

try:
    result = await traced.call_tool("query", {"sql": "SELECT 1"})
except LoopDetectedError as e:
    print(f"Loop stopped: {e.tool_name} repeated {e.loop_count} times ({e.pattern})")
except BudgetExceededError as e:
    print(f"Budget exceeded: {e.limit_type} = {e.actual_value} (limit: {e.limit_value})")
except CircuitBreakerOpenError as e:
    print(f"Server disabled: {e.server_name}, cooldown: {e.cooldown_remaining_s:.0f}s")
```

Prevented calls are still recorded as spans with `status="prevented"` — visible in the dashboard trace view.

### Server-Managed Configuration

Constructor params are offline fallbacks. When a LangSight backend is configured, thresholds are managed from the dashboard and applied automatically on each `wrap()` call.

```python
# Constructor params = offline defaults (used when API unreachable)
client = LangSightClient(
    url="http://localhost:8000",
    loop_detection=True,
    loop_threshold=3,      # overridden by dashboard config if set
    max_steps=25,          # overridden by dashboard config if set
    circuit_breaker=True,
)

# On client.wrap(), thresholds are fetched from the dashboard and applied
proxy = client.wrap(mcp, agent_name="orchestrator")
# orchestrator's dashboard config (if set) overrides constructor params
```

`wrap()` returns immediately — the remote config fetch runs as a background task and takes effect before the first tool call. If the API is unreachable, constructor params remain active.

Configure thresholds at **Settings → Prevention** in the dashboard, or via the API:

```bash
curl -X PUT http://localhost:8000/api/agents/orchestrator/prevention-config \
  -H "X-API-Key: lsk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"loop_threshold": 5, "max_steps": 30, "max_cost_usd": 1.00}'
```

Set `agent_name` to `"*"` for a project-level default that applies to all agents without a specific config entry.
