# CLAUDE.md

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

## Project Overview

Tsunagi is an SDK-first orchestration framework for AI pipelines and agents. The core philosophy is to keep SDK calls (OpenAI, Anthropic, Qdrant, etc.) as plain user code while adding composition, retry, timeout, and tracing.

**Critical Constraints:**
- Zero runtime dependencies — core relies ONLY on Python standard library (see line 8 in pyproject.toml)
- Small pieces — keep modules under 400 lines and readable
- SDK-first — NEVER wrap or abstract SDK calls; users call SDKs directly

## Development Commands

```bash
# Install for local development (includes dev dependencies)
pip install -e .[dev]
# Or using uv:
uv pip install -e .[dev]

# Linting (Ruff)
ruff check .

# Type checking (Mypy with strict mode)
mypy src tests

# Run all tests
pytest

# Run tests with coverage
pytest --cov=src/tsunagi --cov-report=term-missing

# Run a specific test file
pytest tests/test_pipeline.py

# Run a specific test function
pytest tests/test_pipeline.py::test_pipeline_basic_sequence
```

**Python Version:** Requires Python 3.11+

## Architecture & Design Decisions

### Core Components

1. **`@step` decorator** (`src/tsunagi/step.py`)
   - Wraps async functions to make them composable in pipelines
   - **Key behavior**: Direct calls (`await my_step(input)`) behave exactly like the raw function
   - Retry, timeout, and tracing ONLY activate when run inside a Pipeline via `pipeline.run()`
   - Steps compose via `>>` operator: `step_a >> step_b >> step_c`

2. **`Pipeline` class** (`src/tsunagi/pipeline.py`)
   - Orchestrates step sequences, manages Context and Tracing
   - SDK exceptions are NEVER caught-and-rewrapped silently — they propagate as-is wrapped in `PipelineError` with `.original` attribute
   - The pipeline is SDK-agnostic; it knows nothing about OpenAI, Anthropic, etc.
   - Supports parallel execution via `run_parallel()`

3. **`Agent` class** (`src/tsunagi/agent.py`)
   - LLM tool-use loop: sends messages → detects tool_use → executes tools → loops until final response
   - Does NOT wrap any LLM SDK; uses `ChatAdapter` protocol
   - Built-in adapters: `AnthropicAdapter`, `OpenAIAdapter` (thin wrappers ~30 lines each)
   - Users can implement ChatAdapter for any LLM provider
   - `max_turns` prevents infinite loops (default: 10)

4. **`@tool` decorator** (`src/tsunagi/tool.py`)
   - Registers async functions as agent-callable tools
   - Auto-generates tool schema from type hints and docstrings
   - Schema is SDK-agnostic; adapters convert to SDK-specific format

5. **`Tracer` protocol** (`src/tsunagi/tracer.py`)
   - Opt-in observability: pipelines with no tracer have zero tracing overhead
   - Uses Protocol (not ABC) — any object with matching methods works
   - Built-in: `NullTracer` (default, zero overhead), `StdoutTracer` (dev logging)
   - Custom tracers: implement any subset of protocol methods

6. **`RetryConfig`** (`src/tsunagi/retry.py`)
   - Per-step retry configuration with exponential backoff and jitter
   - Predefined configs: `NO_RETRY`, `RETRY_3X`, `RETRY_5X`
   - Optional `retry_on` callable for conditional retry based on exception type

7. **`Context`** (`src/tsunagi/context.py`)
   - Execution context shared across steps in a pipeline run
   - Tracks timing, metadata, and errors
   - Steps can read/write metadata freely for cross-step communication

### Step Composition Pattern

Steps compose via the `>>` operator, creating a `StepSequence`:

```python
result = await pipeline.run(embed >> search >> generate, input="query")
```

This is syntactic sugar for sequential execution. For parallel execution, use:

```python
results = await pipeline.run_parallel([step_a, step_b], input="shared_input")
```

### Direct vs Pipeline Execution

- **Direct call**: `await step(input)` → behaves exactly like the raw function, no framework overhead
- **Pipeline call**: `await pipeline.run(step, input=input)` → activates retry, timeout, tracing

This dual behavior ensures steps are testable in isolation without framework complexity.

### Agent Tool-Use Loop

The `Agent.chat()` method implements a standard tool-use loop:
1. Send messages to LLM
2. If response contains tool calls, execute each tool
3. Append tool results to messages
4. Loop until LLM produces final text response or `max_turns` is reached

The agent is SDK-agnostic thanks to the `ChatAdapter` protocol. Built-in adapters handle Anthropic and OpenAI formats.

## Code Organization

```
src/tsunagi/
  __init__.py         # Public API exports
  step.py             # @step decorator, Step, StepSequence, BoundStep
  pipeline.py         # Pipeline orchestration
  agent.py            # Agent, ChatAdapter, AnthropicAdapter, OpenAIAdapter
  tool.py             # @tool decorator and Tool class
  tracer.py           # Tracer protocol, NullTracer, StdoutTracer
  retry.py            # RetryConfig and predefined retry strategies
  context.py          # Context and StepTiming for execution tracking
  errors.py           # TsunagiError, StepError, PipelineError, TimeoutError
  _types.py           # Internal type definitions

tests/                # Test suite (pytest with asyncio mode)
examples/             # Usage examples (some require API keys)
```

## Testing Notes

- Tests use `pytest` with `pytest-asyncio` (asyncio_mode = "auto" in pyproject.toml)
- mypy runs in strict mode with `warn_return_any` and `warn_unused_configs`
- Tests are excluded from certain mypy checks (see `[[tool.mypy.overrides]]`)
- Do NOT test files inside the `test/` directory when using glob patterns for mypy (see recent commits)

## Optional Dependencies

The project has ZERO runtime dependencies. Optional extras are:
- `[otel]`: OpenTelemetry integration for distributed tracing
- `[dev]`: Development tools (pytest, mypy, ruff, coverage)

When adding features, ensure they do NOT introduce new runtime dependencies unless absolutely necessary and approved.

## Important Conventions

1. **All core functions are async** — use `async def` and `await`
2. **Type hints are mandatory** — strict mypy mode is enforced
3. **Docstrings follow Google style** — especially for public APIs
4. **Error handling** — preserve original exceptions; wrap in TsunagiError subclasses with `.original` attribute
5. **Protocols over inheritance** — use `Protocol` (typing) instead of abstract base classes when possible
6. **Small modules** — keep each module under 400 lines for readability

## Examples

See `examples/` directory for usage patterns:
- `basic_pipeline.py` — Simple step composition
- `agent_with_tools.py` — Agent with custom tools
- `custom_tracer.py` — Implementing a custom tracer
- `rag_pipeline.py` — RAG pipeline example (requires API keys)
- `japanese_rag.py` — Japanese language RAG example

**Note**: Examples with API key requirements are annotated in their docstrings.
