# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Python SDK for the Staso platform. Provides decorator-based tracing and automatic data ingestion for AI agent observability. Users instrument their AI agents with `@st.agent`, `@st.tool`, `@st.trace` decorators and auto-instrumentation for Anthropic/OpenAI SDKs.

## Commands

```bash
# Install for development
pip install -e ".[dev]"

# Run all tests
uv run pytest tests/ -v

# Run a single test file
uv run pytest tests/test_decorators.py -v

# Run a single test by name
uv run pytest tests/test_decorators.py -k "test_name" -v

# Lint
ruff check src/ tests/
ruff format --check src/ tests/

# Type check
mypy src/staso/
```

## Architecture

### Initialization flow
`st.init()` -> creates `Config` -> creates `Client` (stored as module-level global) -> creates `BatchTransport` (background daemon thread) + `TracerProvider`

### Span lifecycle
Decorators/integrations call `get_tracer()` -> `Tracer.start_as_current_span()` -> creates `Span` with parent-child linking via `contextvars` -> on exit, `Span.end()` serializes to dict and enqueues in `BatchTransport` -> background thread batches and POSTs to `/v1/traces/ingest`

### Context propagation
Parent-child span linking uses `contextvars` (`context.py`). The current span, session_id, and user_id are all propagated via `ContextVar`. `st.conversation()` context manager sets session_id (and optionally user_id) for all spans within its scope.

### Integration monkey-patching
Anthropic/OpenAI integrations work by patching `Messages.create` / `ChatCompletions.create` (both sync and async). Streaming responses use proxy objects (`_streaming.py`, `_openai_streaming.py`) that accumulate data and close the span when the stream ends.

### Annotations
`st.annotate()` and `span.annotate()` send evaluations directly (not batched) to `/v1/traces/{trace_id}/annotations`. Uses a separate lazy-initialized `httpx.Client`.

## Project structure

```
src/staso/                 # SDK package
  __init__.py               # Public API (init, shutdown, conversation, decorators)
  client.py                 # Core client + global state
  config.py                 # Config dataclass
  context.py                # contextvars-based span context (session_id, user_id)
  decorators.py             # @agent, @tool, @trace decorators
  annotation.py             # st.annotate() — attach evaluations to traces
  provider.py               # Span, Tracer, TracerProvider
  transport.py              # Background batch HTTP transport
  types.py                  # SpanKind, SpanStatus, metadata keys
  integrations/
    anthropic.py            # Auto-instrumentation for Anthropic SDK
    openai.py               # Auto-instrumentation for OpenAI SDK
    _streaming.py           # Anthropic streaming proxies
    _openai_streaming.py    # OpenAI streaming proxies
  claude_code/              # Claude Code CLI tracing integration
    hook.py                 # Hook dispatcher + all event handlers
    state.py                # Per-session state persistence (~/.staso/claude-code/)
    transcript.py           # JSONL transcript parser (extracts LLM spans + tokens)
    sender.py               # Synchronous HTTP span sender
    __main__.py             # Entry point: python3 -m staso.claude_code
cli.py                     # CLI entry point: staso setup-claude-code
test_codebase/              # Standalone integration test agent
  agent.py                  # Multi-provider support agent (Anthropic / OpenAI)
```

## Key conventions

- Python >= 3.11 required (uses `StrEnum`, `X | Y` union syntax)
- `httpx` + `pydantic-settings` are the only runtime dependencies
- Integrations are optional extras (`pip install staso[anthropic]`, `staso[openai]`)
- The SDK must never crash the host application -- all emit paths catch exceptions and log warnings
- Background transport thread is daemon + atexit-registered for clean shutdown
- Span fields map directly to ClickHouse columns; `metadata` dict serializes to `attributes` JSON column
- Tests use a `capturing` fixture (see `conftest.py`) that mocks transport and collects enqueued span dicts
- `ruff` for linting/formatting (line-length 100, target py311); `mypy --strict` for type checking
- Claude Code hooks run as short-lived processes — use synchronous HTTP send, not background transport

## Running the integration test agent

```bash
cd test_codebase
pip install -e "..[anthropic,openai]"
pip install -r requirements.txt
# Create .env with PROVIDER, API keys, STASO_API_KEY, STASO_BASE_URL, etc.
python agent.py
```

Switch providers by changing `PROVIDER=openai` in `.env`. Default models: `claude-haiku-4-5` (Anthropic), `gpt-4o-mini` (OpenAI).

## Claude Code integration

Traces Claude Code CLI sessions — tool calls, LLM token usage, turn timing — into Staso.

**Setup (one-time):**
```bash
uv run staso setup-claude-code \
  --api-key ak_... \
  --workspace my-workspace \
  --environment production \
  --agent-id my-agent \
  --scope global          # writes to ~/.claude/settings.json
                          # use --scope project for .claude/settings.local.json
```

This writes Claude Code hook config pointing at `python3 -m staso.claude_code`.

**How it works:**
- Hooks fire on `SessionStart`, `UserPromptSubmit`, `PostToolUse`, `Stop`, `SubagentStop`, `SessionEnd`
- Each conversation turn becomes one trace (shared `session_id` groups turns together)
- Tool spans are sent immediately on `PostToolUse`; LLM spans (with token counts) are extracted from the transcript on `Stop`
- State persisted at `~/.staso/claude-code/<session_id>.json` across invocations
- Requires `STASO_API_KEY` env var (set automatically by `setup-claude-code`)

**Manual hook invocation (debugging):**
```bash
echo '{"hook_event_name":"SessionStart","session_id":"test-1","cwd":"/tmp","transcript_path":""}' \
  | STASO_DEBUG=1 python3 -m staso.claude_code
```

## Backend compatibility

Targets `/v1/traces/ingest` and `/v1/traces/{trace_id}/annotations` endpoints.
Auth via `X-API-Key` header. Spans batched (max 500 per request).
