Metadata-Version: 2.4
Name: claudekit
Version: 0.1.0
Summary: Production-grade Python developer toolkit wrapping the entire Anthropic ecosystem
Project-URL: Homepage, https://github.com/claudekit/claudekit
Project-URL: Documentation, https://github.com/claudekit/claudekit#readme
Author: claudekit contributors
License-Expression: MIT
Keywords: ai,anthropic,claude,sdk,toolkit
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: anthropic>=0.86.0
Requires-Dist: anyio>=4.0
Requires-Dist: claude-agent-sdk>=0.1.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: mcp>=1.0.0
Requires-Dist: opentelemetry-api>=1.0.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pytest>=7.0
Provides-Extra: dev
Requires-Dist: coverage>=7.0; extra == 'dev'
Requires-Dist: mypy>=1.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
Requires-Dist: rich>=13.0; extra == 'dev'
Requires-Dist: ruff>=0.1; extra == 'dev'
Description-Content-Type: text/markdown

# claudekit

**Production-grade Python framework for the entire Anthropic ecosystem.**

claudekit wraps the Anthropic Client SDK, Agent SDK, MCP, and all deployment
platforms (Bedrock, Vertex, Foundry) into one coherent production framework. It
never replaces the underlying SDKs — it orchestrates them with usage tracking,
security policies, memory, multi-session management, and zero-API-call testing.
Every component is opt-in and works independently — use what you need.

---

## Table of Contents

- [Installation](#installation)
- [Quick Start](#quick-start)
- [Architecture](#architecture)
- [Module Reference](#module-reference)
  - [Client & Cost Tracking](#client--cost-tracking)
  - [Agents & Orchestration](#agents--orchestration)
  - [Sessions](#sessions)
  - [Batch Processing](#batch-processing)
  - [Memory](#memory)
  - [Tools](#tools)
  - [Skills](#skills)
  - [Plugins](#plugins)
  - [Security](#security)
  - [Testing](#testing)
  - [Prompts](#prompts)
  - [Thinking](#thinking)
  - [Pre-flight & Token Counting](#pre-flight--token-counting)
  - [Models](#models)
  - [Errors](#errors)
- [Extending claudekit](#extending-claudekit)
- [Complete Export Index](#complete-export-index)
- [License](#license)

---

## Installation

```bash
# Core
pip install claudekit

# With optional extras
pip install claudekit[agent]     # Agent SDK support
pip install claudekit[mcp]       # MCP server builder
pip install claudekit[web]       # Prebuilt web tools (httpx)
pip install claudekit[otel]      # OpenTelemetry tracing
pip install claudekit[all]       # Everything
```

---

## Quick Start

```python
from claudekit import TrackedClient, tool
from claudekit.security import SecurityLayer, Policy
from claudekit.testing import create_mock_anthropic, assert_response, expect

# 1. Create a tracked client
client = TrackedClient(api_key="sk-...")

# 2. Define a tool
@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"Sunny in {city}"

# 3. Add security
security = SecurityLayer([
    Policy.no_prompt_injection(),
    Policy.no_pii_in_output(action="redact"),
])

# 4. Make a call
response = client.messages.create(
    model="claude-haiku-4-5",
    max_tokens=1024,
    tools=[get_weather.to_dict()],
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)

# 5. Check usage
print(client.usage.summary())

# 6. Test with zero API calls — full SDK stack runs
mock, handler = create_mock_anthropic(patterns={"weather": "It's sunny!"})
test_response = mock.messages.create(
    model="claude-haiku-4-5",
    max_tokens=100,
    messages=[{"role": "user", "content": "weather in Paris"}],
)
assert_response(test_response, expect.contains("sunny"), expect.has_text())
```

---

## Architecture

```
claudekit/
├── client/          TrackedClient, AsyncTrackedClient, Bedrock, Vertex, Foundry
├── agents/          Agent, AgentRunner, AgentInspector, BudgetGuard, HookBuilder
├── orchestration/   Orchestrator, LLMRouter, RuleRouter, ManualRouter
├── sessions/        Session, SessionConfig, SessionManager, MultiSessionUsage
├── batches/         BatchManager, BatchBuilder, BatchResult, BatchStats
├── memory/          MemoryStore, MemoryEntry, JSONFileBackend, SQLiteBackend
├── tools/           @tool, ToolRegistry, MCPServer, ToolInputValidator
│   └── prebuilt/    web_search, web_fetch, read_file, write_file, run_python, ...
├── skills/          Skill, SkillRegistry
│   └── prebuilt/    SummarizerSkill, ClassifierSkill, DataExtractorSkill, ...
├── plugins/         Plugin, PluginLoader, PluginRegistry
├── security/        SecurityLayer, Policy, SecurityContext
│   ├── policies/    PromptInjection, PII, Budget, ToolGuard, OutputSchema, ...
│   └── presets/     CustomerFacing, InternalTools, Financial, Healthcare, Developer
├── testing/         MockClient, MockTransport, expect.*, assert_response, ...
├── prompts/         PromptManager, PromptVersion, ComparisonResult
├── thinking/        thinking_enabled, thinking_adaptive, extract_thinking
├── precheck/        TokenCounter, TokenCountResult
├── models/          Model, MODELS, select_model, ModelTask
└── errors/          ClaudekitError, 30+ typed errors, enable_rich_errors
```

---

## Module Reference

### Client & Cost Tracking

**Module:** `claudekit.client`

Every client is a transparent wrapper around the real Anthropic SDK client. All API calls are automatically recorded with model, token counts, cache savings, and estimated cost. Streaming is fully supported — cost is extracted from `get_final_message()` at stream close.

| Class / Function | Import | Description |
|------------------|--------|-------------|
| `TrackedClient` | `claudekit` | Drop-in `anthropic.Anthropic` wrapper with automatic usage tracking |
| `AsyncTrackedClient` | `claudekit` | Async variant with identical tracking interface |
| `TrackedBedrockClient` | `claudekit` | AWS Bedrock platform support |
| `TrackedVertexClient` | `claudekit` | Google Vertex AI platform support |
| `TrackedFoundryClient` | `claudekit` | Azure Foundry platform support |
| `create_client()` | `claudekit` | Auto-detect platform from `ANTHROPIC_API_KEY`, `AWS_*`, `GOOGLE_*` env vars |
| `SessionUsage` | `claudekit` | Per-model cost breakdown, CSV export, calls list, cache savings calculation |
| `CallRecord` | `claudekit` | Individual API call metadata: model, tokens, cost, latency, cache metrics, `idempotency_key` |

#### SessionUsage API

| Method | Description |
|--------|-------------|
| `.record(call_record)` | Record a new API call |
| `.estimated_cost` | Total estimated cost in USD |
| `.total_cost` | Alias for `estimated_cost` |
| `.summary()` | Human-readable multi-line summary |
| `.breakdown()` | Per-model cost/token breakdown dict |
| `.export_csv(path)` | Export all calls to CSV |
| `.calls` | List of all `CallRecord` objects (copy) |
| `.reset()` | Clear all recorded calls |

```python
from claudekit import TrackedClient, create_client

# Direct API
client = TrackedClient(api_key="sk-...")

# Auto-detect platform from env vars
client = create_client()

# Usage tracking
print(client.usage.summary())
print(f"Total cost: ${client.usage.estimated_cost:.4f}")
client.usage.export_csv("usage.csv")

# Per-model breakdown
for model, stats in client.usage.breakdown().items():
    print(f"  {model}: {stats['calls']} calls, ${stats['cost']:.4f}")
```

---

### Agents & Orchestration

**Module:** `claudekit.agents` + `claudekit.orchestration`

#### Agent Module

| Class | Description |
|-------|-------------|
| `Agent` | Named agent definition — model, system prompt, tools, skills, security, memory config |
| `AgentRunner` | Wraps Agent SDK `query()` with cost tracking, budget guards, and hooks |
| `AgentResult` | Structured result from `AgentRunner.run()` — output, usage, turns, duration |
| `AgentInspector` | Turn-by-turn debugging visualizer for agent runs |
| `BudgetGuard` | Cost and turn limits with warning callbacks and hard stops |
| `HookBuilder` | Composable Agent SDK hook helpers for `before_model_call`, `after_model_call`, etc. |

#### BudgetGuard Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `max_cost_usd` | `float` | Hard stop when cost exceeds this |
| `warn_cost_usd` | `float` | Fire warning callback at this threshold |
| `max_turns` | `int` | Maximum number of agent turns |
| `on_warning` | `Callable` | Callback when warn threshold is hit |

#### Orchestration Module

| Class | Description |
|-------|-------------|
| `Orchestrator` | Multi-agent routing and delegation engine |
| `OrchestrationResult` | Result from `Orchestrator.run()` — agent responses, routing decisions, total usage |
| `LLMRouter` | Route tasks to agents using a classifier model (Claude) |
| `RuleRouter` | Route tasks via regex pattern matching against message content |
| `ManualRouter` | Explicit agent selection — developer picks the agent |

```python
from claudekit.agents import Agent, AgentRunner, AgentResult, BudgetGuard
from claudekit.orchestration import Orchestrator, RuleRouter, OrchestrationResult

agent = Agent(name="helper", model="claude-sonnet-4-6", system="You help users.")
runner = AgentRunner(agent, budget=BudgetGuard(max_cost_usd=0.10))
result: AgentResult = runner.run("What is 2 + 2?")
print(result.output, result.usage)

# Multi-agent orchestration
orch = Orchestrator(router=RuleRouter({"billing": [r"invoice", r"charge"]}))
orch.register(agent)
result: OrchestrationResult = await orch.run("Why was I charged?", entry_agent="helper")
```

---

### Sessions

**Module:** `claudekit.sessions`

Sessions provide managed lifecycle, per-session budgets, usage isolation, pause/resume, cost warning callbacks, and event broadcasting.

| Class | Description |
|-------|-------------|
| `Session` | Managed session wrapping a tracked client — lifecycle states: `active`, `paused`, `finished`, `error` |
| `SessionConfig` | Declarative config: name, model, tags, max_cost_usd, on_cost_warning, on_error callbacks |
| `SessionManager` | Named session lifecycle management — create, get, terminate, broadcast events |
| `MultiSessionUsage` | Aggregated usage view across all sessions |

#### Session Lifecycle States

```
active → paused → active → finished
                       ↘ error
```

#### SessionConfig Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | `str` | Unique session identifier |
| `model` | `str` | Claude model ID |
| `tags` | `list[str]` | Tags for filtering with `manager.by_tag()` |
| `max_cost_usd` | `float` | Per-session budget limit |
| `on_cost_warning` | `Callable` | Callback when cost approaches limit |
| `on_error` | `Callable` | Callback when session enters error state |

#### SessionManager API

| Method | Description |
|--------|-------------|
| `.create(config)` | Create and register a new session |
| `.get(name)` | Look up session by name |
| `.all()` | All sessions in creation order |
| `.by_tag(tag)` | Filter sessions by tag |
| `.pause(name)` | Pause a session |
| `.resume(name)` | Resume a paused session |
| `.terminate(name)` | Terminate a session |
| `.terminate_all()` | Terminate every session |
| `.status()` | Dict mapping session names to states |
| `.usage` | Aggregated `MultiSessionUsage` |
| `.broadcast_event(event, data)` | Send event to all active sessions |

Three modes of operation:
- **Mode 1** — Implicit single session (zero config)
- **Mode 2** — Inline `create_session()` mid-flow
- **Mode 3** — Upfront `SessionManager` with named sessions

```python
from claudekit.sessions import SessionManager, SessionConfig

manager = SessionManager(client)
support = manager.create(SessionConfig(
    name="support",
    model="claude-haiku-4-5",
    max_cost_usd=0.50,
    tags=["support", "tier1"],
))
billing = manager.create(SessionConfig(name="billing", model="claude-sonnet-4-6"))

response = support.run("How do I reset my password?")
print(manager.status())  # {"support": "active", "billing": "active"}
manager.terminate_all()
```

---

### Batch Processing

**Module:** `claudekit.batches`

Batch processing uses the Anthropic Message Batches API at 50% cost. Includes polling with exponential backoff, sidecar JSON persistence for batch IDs, cancellation, and per-entry cost tracking.

| Class | Description |
|-------|-------------|
| `BatchManager` | Submit, poll, collect results, cancel, persist batch IDs to sidecar JSON |
| `BatchBuilder` | Fluent API for constructing batch requests — `.add(custom_id, messages, ...)` |
| `BatchResult` | Container for completed batch entries with stats |
| `BatchStats` | Aggregate: succeeded, failed, expired, cancelled, total_cost, `summary()` |

#### BatchManager API

| Method | Description |
|--------|-------------|
| `.submit(requests)` | Submit raw batch request dicts |
| `.submit_builder(builder)` | Submit from a `BatchBuilder` |
| `await .wait(batch_id)` | Poll until complete with exponential backoff |
| `.cancel(batch_id)` | Request batch cancellation |
| `.usage` | `SessionUsage` tracker for batch costs |

```python
from claudekit.batches import BatchBuilder, BatchManager

builder = BatchBuilder()
builder.add("q1", model="claude-haiku-4-5", messages=[...])
builder.add("q2", model="claude-haiku-4-5", messages=[...])

manager = BatchManager(client, sidecar_path="./batches.json")
batch_id = manager.submit_builder(builder)
result = await manager.wait(batch_id)
print(result.stats.summary())  # "2 succeeded, 0 failed, $0.0012"
```

---

### Memory

**Module:** `claudekit.memory`

Cross-session memory with pluggable backends, TTL expiration, scope-based isolation, LRU eviction, and context injection into messages.

| Class / Function | Description |
|------------------|-------------|
| `MemoryStore` | Main store — get, save, delete, list, search, TTL, LRU eviction |
| `MemoryEntry` | Single entry: key, value, scope, metadata, created_at, expires_at, access_count |
| `AbstractBackend` | Base class for custom backends — implement `get`, `save`, `delete`, `list_keys` |
| `JSONFileBackend` | File-based backend with atomic writes and file locking (dev/test) |
| `SQLiteBackend` | Database backend with full-text search (production) |
| `context_with_memory()` | Inject relevant memory entries into a message list as context |

#### MemoryStore API

| Method | Description |
|--------|-------------|
| `.save(key, value, scope, ttl, metadata)` | Store an entry |
| `.get(key, scope)` | Retrieve an entry (or `None`) |
| `.delete(key, scope)` | Delete an entry |
| `.list_keys(scope)` | List all keys in a scope |
| `.search(query, scope, limit)` | Search entries by content |
| `.clear(scope)` | Clear all entries in a scope |

```python
from claudekit.memory import MemoryStore, JSONFileBackend, SQLiteBackend, context_with_memory

# Development
store = MemoryStore(backend=JSONFileBackend("./memory.json"))

# Production
store = MemoryStore(backend=SQLiteBackend("./memory.db"))

store.save("user-lang", "Python", scope="user:123", ttl=3600)
entry = store.get("user-lang", scope="user:123")
print(entry.value)  # "Python"
print(entry.access_count)  # 1

# Inject memory into messages
messages = context_with_memory(store, messages, scope="user:123")
```

---

### Tools

**Module:** `claudekit.tools`

Schema inference from type hints and docstrings, tool registries for sharing across agents, MCP server builder, and input validation.

| Class / Decorator | Description |
|-------------------|-------------|
| `@tool` | Decorator — infers JSON Schema from type hints + Google-style docstrings |
| `ToolWrapper` | Object returned by `@tool` — `.to_dict()`, `.name`, `.description`, `.schema` |
| `ToolRegistry` | Named tool collections — `.register()`, `.to_anthropic_format()`, `.get()` |
| `MCPServer` | stdio MCP server builder from `@tool` functions |
| `ToolInputValidator` | JSON Schema validation with type coercion |
| `ToolError` | Re-export from anthropic SDK (fallback provided) |
| `ToolInputValidationError` | Raised when tool input fails validation |

#### @tool Decorator

The `@tool` decorator extracts:
- **Name** from the function name
- **Description** from the first line of the docstring
- **Parameter descriptions** from the `Args:` section (Google-style)
- **Parameter types** from Python type annotations → JSON Schema types
- **Required parameters** from those without defaults

#### Prebuilt Tools

| Tool | Module | Description |
|------|--------|-------------|
| `web_search(query, max_results)` | `tools.prebuilt` | Search the web using httpx |
| `web_fetch(url)` | `tools.prebuilt` | Fetch and extract text from a URL |
| `read_file(path)` | `tools.prebuilt` | Read a local file's contents |
| `write_file(path, content)` | `tools.prebuilt` | Write content to a local file |
| `read_directory(path)` | `tools.prebuilt` | List directory contents |
| `run_python(code)` | `tools.prebuilt` | Execute Python code in a subprocess |
| `run_bash(command)` | `tools.prebuilt` | Execute a bash command |
| `parse_json(text)` | `tools.prebuilt` | Parse and format JSON |
| `parse_csv(text)` | `tools.prebuilt` | Parse CSV into structured data |
| `format_table(data)` | `tools.prebuilt` | Format data as an ASCII table |

```python
from claudekit.tools import tool, ToolRegistry, MCPServer, ToolInputValidator

@tool
def add(a: int, b: int) -> str:
    """Add two numbers.

    Args:
        a: First number.
        b: Second number.
    """
    return str(a + b)

# Use in API call
tools = [add.to_dict()]

# Registry for sharing across agents
registry = ToolRegistry()
registry.register(add)
tools = registry.to_anthropic_format()

# MCP Server
server = MCPServer("math_server")
server.add(add)
server.run()  # blocking stdio server

# Input validation
validator = ToolInputValidator(add)
validator.validate({"a": 1, "b": 2})  # OK
validator.validate({"a": "bad"})       # raises ToolInputValidationError
```

---

### Skills

**Module:** `claudekit.skills`

A skill packages a system prompt, model choice, tools, output validation, memory, and security into a single portable, composable unit.

| Class | Description |
|-------|-------------|
| `Skill` | Core abstraction — name, system prompt, model, tools, security, memory config |
| `SkillRegistry` | Named skill lookup and composition |

#### Prebuilt Skills

| Skill | Description | Key Parameters |
|-------|-------------|----------------|
| `SummarizerSkill` | Summarize text | `style` (`"bullet"`, `"narrative"`, `"tldr"`), `max_length` |
| `ClassifierSkill` | Classify text into categories | `categories`, `multi_label`, `confidence_threshold` |
| `DataExtractorSkill` | Extract structured data | `schema` (Pydantic model or dict), `examples` |
| `CodeReviewerSkill` | Review code for issues | `languages`, `severity_threshold` |
| `ResearcherSkill` | Multi-source research with citations | `max_sources`, `depth` |

#### Prebuilt Skill Data Classes

| Class | Description |
|-------|-------------|
| `CodeReview` | Result from CodeReviewerSkill — issues list + suggestions |
| `CodeReviewIssue` | Individual code issue — file, line, severity, message |
| `CodeReviewSuggestion` | Improvement suggestion — description, example |
| `Research` | Result from ResearcherSkill — summary + findings |
| `ResearchFinding` | Individual finding — source, content, relevance |

```python
from claudekit.skills import Skill, SkillRegistry, SummarizerSkill, ClassifierSkill

# Custom skill
skill = Skill(name="qa", system="Answer concisely.", model="claude-haiku-4-5")
result = await skill.run(input="What is Python?", client=client)

# Prebuilt
summarizer = SummarizerSkill(style="bullet", max_length=200)
classifier = ClassifierSkill(categories=["billing", "support", "sales"])

# Registry
registry = SkillRegistry()
registry.register(summarizer)
skill = registry.get("summarizer")
```

---

### Plugins

**Module:** `claudekit.plugins`

Lifecycle-hook plugin framework. Plugins receive callbacks at every stage of the API lifecycle without modifying source code.

| Class | Description |
|-------|-------------|
| `Plugin` | Base class — override `on_request`, `on_response`, `on_tool_call`, `on_session_start`, etc. |
| `PluginLoader` | Dynamic plugin discovery, loading, and safe dispatch |
| `PluginRegistry` | Plugin management — register, unregister, list |

#### Plugin Lifecycle Hooks

| Hook | When Fired |
|------|------------|
| `on_request(messages, model, context)` | Before every API call |
| `on_response(response, context)` | After every API response |
| `on_tool_call(tool_name, tool_input, context)` | When a tool is called |
| `on_session_start(session_name, config)` | When a session starts |
| `on_session_end(session_name, usage)` | When a session ends |
| `on_error(error, context)` | When an error occurs |

#### Prebuilt Plugins

| Plugin | Description |
|--------|-------------|
| `LoggingPlugin` | Structured logging for all lifecycle events |
| `CostAlertPlugin` | Fires a callback when cost exceeds a threshold (per-session or global) |
| `OpenTelemetryPlugin` | Creates OpenTelemetry tracing spans — no-op if `opentelemetry` not installed |

```python
from claudekit.plugins import Plugin, PluginLoader, PluginRegistry
from claudekit.plugins import LoggingPlugin, CostAlertPlugin, OpenTelemetryPlugin

# Custom plugin
class AuditPlugin(Plugin):
    name = "audit"
    version = "1.0.0"

    def on_response(self, response, context):
        log_to_audit_trail(response)
        return response

# Cost alerts
cost_plugin = CostAlertPlugin(
    threshold=1.00,
    callback=lambda cost, session: print(f"Cost alert: ${cost:.2f}"),
    per_session=True,
)

# Load plugins
loader = PluginLoader()
loader.load(AuditPlugin())
loader.load(cost_plugin)
loader.dispatch_on_request(messages, model)
```

---

### Security

**Module:** `claudekit.security`

Composable security policy pipeline. Policies are evaluated in order; each can inspect, modify, or reject requests and responses.

| Class | Description |
|-------|-------------|
| `SecurityLayer` | Composable policy pipeline — `.check_request()`, `.check_response()` |
| `Policy` | Base class for custom policies with factory methods |
| `SecurityContext` | Per-request metadata: user_id, session_id, request_id, custom fields |

#### 8 Built-in Policies

| Policy Class | Factory Method | Description |
|-------------|----------------|-------------|
| `PromptInjectionPolicy` | `Policy.no_prompt_injection()` | Detect prompt injection attacks via regex patterns, base64 decoding, suspicious keywords |
| `PIIPolicy` | `Policy.no_pii_in_output(action=)` | Detect/redact PII: emails, SSNs, phone numbers, credit cards. Actions: `"block"`, `"redact"`, `"warn"` |
| `BudgetPolicy` | `Policy.max_cost_per_user(limit_usd=, window=)` | Cost and token limits per user within a sliding window |
| `ToolGuardPolicy` | `Policy.tool_guard(blocked=, allowed=)` | Block/allow specific tool call patterns |
| `OutputSchemaPolicy` | `Policy.output_schema(schema=)` | Validate outputs against Pydantic models or JSON Schema |
| `JailbreakPolicy` | `Policy.no_jailbreak()` | Detect jailbreak attempts using keyword and pattern analysis |
| `RateLimitPolicy` | `Policy.rate_limit(requests_per_minute=)` | Request rate limiting with sliding window |
| `InputSanitizerPolicy` | `Policy.sanitize_inputs()` | Sanitize tool results before re-injection into the conversation |

#### 5 Security Presets

| Preset | Use Case | Included Policies |
|--------|----------|-------------------|
| `CustomerFacingPreset` | Support bots, public interfaces | Injection, PII redaction, jailbreak, rate limit |
| `InternalToolsPreset` | Developer tools, back-office | Tool guard, budget, rate limit |
| `FinancialServicesPreset` | Banking, payments | All policies with strict limits |
| `HealthcarePreset` | Patient data, HIPAA compliance | PII block, injection, jailbreak, strict output schema |
| `DeveloperToolsPreset` | Code assistants, code review | Tool guard (code-related), budget, sanitizer |

```python
from claudekit.security import SecurityLayer, Policy, SecurityContext
from claudekit.security.presets import CustomerFacingPreset, FinancialServicesPreset

# Custom policies
layer = SecurityLayer([
    Policy.no_prompt_injection(),
    Policy.no_pii_in_output(action="redact"),
    Policy.max_cost_per_user(limit_usd=1.00, window="24h"),
    Policy.rate_limit(requests_per_minute=30),
    Policy.tool_guard(blocked=["run_bash", "write_file"]),
])

# Per-request context
ctx = SecurityContext(user_id="user:123", session_id="sess:456")
layer.check_request(messages, model, context=ctx)
layer.check_response(response, context=ctx)

# Or use a preset
layer = CustomerFacingPreset()
layer = FinancialServicesPreset()
```

---

### Testing

**Module:** `claudekit.testing`

Zero-API-call testing utilities. Two mocking approaches, plus assertions, agent/session mocks, pytest fixtures, and response recording.

#### Approach 1: MockTransport (Realistic)

Injects `httpx.MockTransport` directly into a **real `anthropic.Anthropic`** client.
The full SDK stack runs — auth headers, retries, Pydantic validation, error mapping —
with zero network calls. **This is the recommended approach for production tests.**

| Export | Description |
|--------|-------------|
| `create_mock_anthropic(patterns, error_patterns)` | Returns `(anthropic.Anthropic, MockTransportHandler)` |
| `MockTransportHandler` | Handler object with `.on()`, `.on_tool()`, `.on_error()`, `.on_stream()` |

```python
from claudekit.testing import create_mock_anthropic

client, handler = create_mock_anthropic(
    patterns={"weather": "It is sunny."},
    error_patterns={"bad": (401, "authentication_error", "Invalid key")},
)

# Full SDK validation runs
response = client.messages.create(
    model="claude-haiku-4-5", max_tokens=100,
    messages=[{"role": "user", "content": "What is the weather?"}],
)
assert isinstance(response, anthropic.types.Message)

# Register more patterns after creation
handler.on("greeting", "Hello!")
handler.on_tool("search", "web_search", {"query": "test"})
handler.on_error("fail", 429, "rate_limit_error", "Too many requests")
handler.on_stream("story", ["Once upon", " a time", "..."])
```

#### Approach 2: MockClient (High-Level)

Pattern-matching with `construct()`-based responses. Quick to set up, no SDK validation.

| Export | Description |
|--------|-------------|
| `MockClient` | Drop-in TrackedClient replacement with pattern matching |
| `MockClientUnexpectedCallError` | Raised in strict mode for unmatched calls |
| `MockStreamContext` | Context manager for streamed mock responses |

```python
from claudekit.testing import MockClient

mock = MockClient(strict=True)
mock.on("greeting", "Hello!")
mock.on_tool("search", "web_search", {"query": "test"})
mock.on_stream("story", ["Once", " upon", " a time"])

response = mock.messages.create(
    model="claude-haiku-4-5", max_tokens=100,
    messages=[{"role": "user", "content": "greeting"}],
)
```

#### Assertions

All assertions run without short-circuiting — every assertion is evaluated, and all failures are reported at once.

| Function | Description |
|----------|-------------|
| `assert_response(response, *assertions)` | Evaluate all assertions against a `Message` |
| `assert_agent_result(result, *assertions)` | Evaluate all assertions against an `AgentResult` |

##### 17 Assertion Builders (`expect.*`)

| Builder | Description |
|---------|-------------|
| `expect.contains(text)` | Response text includes substring |
| `expect.not_contains(text)` | Response text excludes substring |
| `expect.equals(text)` | Exact text match |
| `expect.matches(pattern)` | Regex match |
| `expect.max_tokens(n)` | Output tokens ≤ n |
| `expect.min_tokens(n)` | Output tokens ≥ n |
| `expect.stop_reason(reason)` | Stop reason match |
| `expect.tool_called(name)` | Tool use block present |
| `expect.tool_called_with(name, **kw)` | Tool called with specific input |
| `expect.no_tool_call()` | No tool use blocks |
| `expect.tool_count(n)` | Exactly n tool use blocks |
| `expect.json_valid()` | Response text is valid JSON |
| `expect.json_contains(key, value)` | JSON has key=value |
| `expect.model_used(model)` | Model matches |
| `expect.has_text()` | Non-empty text block |
| `expect.has_thinking()` | Response contains thinking block |
| `expect.custom(fn, name)` | Custom assertion function |

```python
from claudekit.testing import assert_response, expect

assert_response(response,
    expect.contains("sunny"),
    expect.not_contains("rainy"),
    expect.model_used("claude-haiku-4-5"),
    expect.stop_reason("end_turn"),
    expect.has_text(),
    expect.no_tool_call(),
    expect.max_tokens(100),
)
```

#### Agent & Session Mocks

| Export | Description |
|--------|-------------|
| `MockAgentRunner` | Mock for `AgentRunner` — pattern matching, error simulation, strict mode, call tracking |
| `MockAgentResult` | Structured mock result from `MockAgentRunner` |
| `MockSession` | Mock session with lifecycle (active/paused/terminated) and pattern matching |
| `MockSessionManager` | Mock manager — create/get/terminate/status for multi-session tests |

#### Response Recorder

| Export | Description |
|--------|-------------|
| `ResponseRecorder` | Record real API responses to JSON, replay them in CI with zero API calls |

```python
from claudekit.testing import ResponseRecorder

# Record mode (run locally, hits real API)
recorder = ResponseRecorder("./recordings/")
with recorder.record_mode():
    response = client.messages.create(...)

# Replay mode (run in CI, zero API calls)
with recorder.replay_mode():
    response = client.messages.create(...)  # reads from JSON
```

#### pytest Fixtures

```python
# In conftest.py — import the fixtures you need:
from claudekit.testing._fixtures import (
    mock_client,          # Yields a MockClient instance
    mock_anthropic,       # Yields (anthropic.Anthropic, MockTransportHandler) tuple
    tracked_client,       # Yields a TrackedClient backed by MockTransport
    mock_agent_runner,    # Yields a MockAgentRunner instance
    mock_session_manager, # Yields a MockSessionManager instance
)
```

---

### Prompts

**Module:** `claudekit.prompts`

Versioned prompt storage, diff, and A/B comparison.

| Class | Description |
|-------|-------------|
| `PromptManager` | Save, load, diff, and compare prompt versions |
| `PromptVersion` | A specific version of a prompt — system text, version string, metadata |
| `ComparisonResult` | A/B comparison results between two prompt versions |
| `JSONPromptStorage` | File-based prompt persistence backend |

#### PromptManager API

| Method | Description |
|--------|-------------|
| `.save(name, system, version, metadata)` | Save a prompt version |
| `.load(name, version)` | Load a specific version |
| `.latest(name)` | Load the latest version |
| `.list_versions(name)` | List all version strings |
| `.diff(name, v1, v2)` | Diff two versions (unified diff format) |
| `.compare(name, v1, v2, messages, client)` | A/B compare two versions against the same input |

```python
from claudekit.prompts import PromptManager, PromptVersion

pm = PromptManager()
pm.save("support", system="Be concise.", version="1.0")
pm.save("support", system="Be concise and empathetic.", version="2.0")

# Diff
print(pm.diff("support", "1.0", "2.0"))

# List versions
versions = pm.list_versions("support")  # ["1.0", "2.0"]

# A/B comparison
result = await pm.compare("support", "1.0", "2.0",
    messages=[{"role": "user", "content": "I need help"}],
    client=client,
)
```

---

### Thinking

**Module:** `claudekit.thinking`

Configuration helpers for Claude's extended thinking capability.

| Function | Description |
|----------|-------------|
| `thinking_enabled(budget_tokens)` | Enable extended thinking with a fixed token budget |
| `thinking_adaptive(min_tokens, max_tokens)` | Adaptive thinking — model chooses depth within bounds |
| `thinking_disabled()` | Explicitly disable thinking (default) |
| `extract_thinking(response)` | Extract `(thinking_text, answer_text)` tuple from a response |

```python
from claudekit.thinking import thinking_enabled, thinking_adaptive, extract_thinking

# Fixed budget
response = client.messages.create(
    model="claude-opus-4-6",
    max_tokens=16000,
    thinking=thinking_enabled(budget_tokens=10000),
    messages=[{"role": "user", "content": "Prove that √2 is irrational."}],
)

# Adaptive
response = client.messages.create(
    model="claude-sonnet-4-6",
    thinking=thinking_adaptive(min_tokens=1000, max_tokens=8000),
    ...
)

# Extract
thoughts, answer = extract_thinking(response)
print(f"Thinking: {thoughts[:200]}...")
print(f"Answer: {answer}")
```

---

### Pre-flight & Token Counting

**Module:** `claudekit.precheck`

Count tokens before sending to avoid context window overflows and surprise costs.

| Class | Description |
|-------|-------------|
| `TokenCounter` | Pre-flight token counter using the Anthropic token counting API |
| `TokenCountResult` | Result: `input_tokens`, model, fits_context_window, headroom |

```python
from claudekit.precheck import TokenCounter, TokenCountResult

counter = TokenCounter(client)
result: TokenCountResult = counter.count(
    messages=messages,
    model="claude-haiku-4-5",
    system="You are a helpful assistant.",
)
print(f"Input tokens: {result.input_tokens}")
print(f"Fits in context window: {result.fits_context_window}")
print(f"Headroom: {result.headroom} tokens")
```

---

### Models

**Module:** `claudekit.models`

Model registry with pricing data, context window sizes, and constraint-based selection.

| Class / Function | Description |
|------------------|-------------|
| `Model` | Dataclass: api_id, name, input_price, output_price, context_window, max_output, capabilities |
| `MODELS` | List of all registered `Model` instances |
| `MODELS_BY_ID` | Dict mapping api_id → `Model` |
| `get_model(api_id)` | Look up a model by its API ID string |
| `select_model(task, platform, ...)` | Constraint-based model selection |
| `ModelTask` | Enum: `SIMPLE`, `BALANCED`, `COMPLEX` |

#### Model Dataclass Fields

| Field | Type | Description |
|-------|------|-------------|
| `api_id` | `str` | Exact API model string, e.g. `"claude-haiku-4-5-20251001"` |
| `aliases` | `tuple[str, ...]` | Short names that resolve to this model, e.g. `("claude-haiku-4-5",)` |
| `name` | `str` | Human-readable display name |
| `input_per_mtok` | `float` | Cost per million input tokens (USD) |
| `output_per_mtok` | `float` | Cost per million output tokens (USD) |
| `cache_read_per_mtok` | `float` | Cost per million cache-read tokens (0.1x input) |
| `cache_write_per_mtok` | `float` | Cost per million cache-write tokens (1.25x input) |
| `context_window` | `int` | Maximum context window size in tokens |
| `max_output_tokens` | `int` | Maximum output tokens |
| `bedrock_id` | `str \| None` | AWS Bedrock model identifier |
| `vertex_id` | `str \| None` | Google Vertex AI model identifier |
| `supports_thinking` | `bool` | Whether extended thinking is supported |
| `supports_vision` | `bool` | Whether image input is supported |
| `supports_streaming` | `bool` | Whether streaming is supported |
| `is_deprecated` | `bool` | Whether the model is deprecated |
| `eol_date` | `str \| None` | End-of-life date (ISO-8601) |
| `recommended_replacement` | `str \| None` | Suggested replacement model `api_id` |

#### Default Model Constants

All defaults are centralised in `claudekit._defaults` — no hardcoded model strings anywhere:

| Constant | Value | Used For |
|----------|-------|----------|
| `DEFAULT_MODEL` | `"claude-sonnet-4-6"` | General-purpose default (Agent, Skill fallback) |
| `DEFAULT_FAST_MODEL` | `"claude-haiku-4-5-20251001"` | Routing, classification, batch, testing mocks |
| `DEFAULT_POWERFUL_MODEL` | `"claude-opus-4-6"` | Research, deep analysis, code review |

#### Registered Models (12 total)

| Model | `api_id` | Alias | Context | Max Output |
|-------|----------|-------|---------|------------|
| Claude Opus 4.6 | `claude-opus-4-6` | — | 1M | 128K |
| Claude Sonnet 4.6 | `claude-sonnet-4-6` | — | 1M | 64K |
| Claude Haiku 4.5 | `claude-haiku-4-5-20251001` | `claude-haiku-4-5` | 200K | 64K |
| Claude Opus 4.5 | `claude-opus-4-5-20251101` | `claude-opus-4-5` | 200K | 64K |
| Claude Sonnet 4.5 | `claude-sonnet-4-5-20250929` | `claude-sonnet-4-5` | 200K | 64K |
| Claude Opus 4.1 | `claude-opus-4-1-20250805` | `claude-opus-4-1` | 200K | 32K |
| Claude Sonnet 4 | `claude-sonnet-4-20250514` | `claude-sonnet-4-0` | 200K | 64K |
| Claude Opus 4 | `claude-opus-4-20250514` | `claude-opus-4-0` | 200K | 32K |
| Claude Haiku 3.5 | `claude-3-5-haiku-20241022` | `claude-haiku-3-5` | 200K | 8K |
| Claude Sonnet 3.7 | `claude-3-7-sonnet-20250219` | — | 200K | 64K |
| Claude Haiku 3 | `claude-3-haiku-20240307` | — | 200K | 4K |
| Claude Opus 3 | `claude-3-opus-20240229` | — | 200K | 4K |

```python
from claudekit.models import get_model, select_model, ModelTask, MODELS

# Look up by full ID or alias — both work
haiku = get_model("claude-haiku-4-5")
haiku = get_model("claude-haiku-4-5-20251001")  # same result
print(haiku.estimate_cost(input_tokens=10_000, output_tokens=2_000))

# Constraint-based selection
model_id = select_model(task=ModelTask.BALANCED, platform="bedrock")
model_id = select_model(require_thinking=True, prefer_speed=True)
model_id = select_model(max_cost_usd=0.01, input_tokens=10_000, output_tokens=2_000)

# List all models
for model in MODELS:
    print(f"{model.api_id}: ${model.input_per_mtok}/Mtok in, ${model.output_per_mtok}/Mtok out")
```

---

### Errors

**Module:** `claudekit.errors`

Rich error hierarchy with **30+ typed exceptions**. Every exception carries:
- `message` — human-readable description
- `code` — machine-readable constant (e.g. `"PROMPT_INJECTION_DETECTED"`)
- `context` — relevant metadata dict (user_id, model, request_id, etc.)
- `recovery_hint` — what the developer should do
- `original` — the wrapped original exception (if any)

#### Exception Hierarchy

```
ClaudekitError
├── SecurityError
│   ├── PromptInjectionError
│   ├── PIIDetectedError
│   └── JailbreakDetectedError
├── BudgetError
│   ├── BudgetExceededError
│   ├── SessionBudgetExceededError
│   ├── TokenLimitError
│   └── RateLimitError
├── AgentError
│   ├── AgentTimeoutError
│   └── AgentMaxTurnsError
├── OrchestratorError
│   └── DelegationLoopError
├── ClaudekitMemoryError
│   ├── MemoryBackendError
│   ├── MemoryKeyNotFoundError
│   └── MemoryValueTooLargeError
├── BatchError
│   ├── BatchNotReadyError
│   ├── BatchCancelledError
│   └── BatchPartialFailureError
├── SessionError
│   ├── SessionPausedError
│   ├── SessionTerminatedError
│   └── SessionNameConflictError
├── PrecheckError
│   └── ContextWindowExceededError
├── ConfigurationError
│   ├── MissingAPIKeyError
│   ├── DeprecatedModelError
│   └── PlatformNotAvailableError
├── OutputValidationError
├── OverloadedError
├── ToolBlockedError
├── ToolInputValidationError
├── ToolJSONError
└── ToolResultTooLargeWarning (Warning)
```

#### Error Code Constants

All error codes are importable string constants:

```python
from claudekit.errors import (
    PROMPT_INJECTION_DETECTED,  # "PROMPT_INJECTION_DETECTED"
    PII_DETECTED,               # "PII_DETECTED"
    BUDGET_EXCEEDED,            # "BUDGET_EXCEEDED"
    AGENT_TIMEOUT,              # "AGENT_TIMEOUT"
    BATCH_CANCELLED,            # "BATCH_CANCELLED"
    # ... 30+ more
)
```

#### Rich Error Wrapping

```python
from claudekit.errors import enable_rich_errors, wrap_sdk_error, DeprecatedModelWarning

# Wrap all anthropic SDK exceptions with claudekit rich errors
enable_rich_errors()

# Manual wrapping
try:
    response = client.messages.create(...)
except anthropic.RateLimitError as e:
    raise wrap_sdk_error(e)  # → claudekit.errors.RateLimitError with context
```

---

## Extending claudekit

### Custom Policy

```python
from claudekit.security import Policy, SecurityContext

class ProfanityFilter(Policy):
    name = "profanity_filter"

    def check_response(self, response, context: SecurityContext):
        text = response.content[0].text
        if has_profanity(text):
            raise SecurityError("Profanity detected in response")
        return response
```

### Custom Skill

```python
from claudekit.skills import Skill

skill = Skill(
    name="my_skill",
    system="You are an expert at...",
    model="claude-haiku-4-5",
    tools=[my_tool],
)
result = await skill.run(input="...", client=client)
```

### Custom Plugin

```python
from claudekit.plugins import Plugin

class MetricsPlugin(Plugin):
    name = "metrics"
    version = "1.0.0"

    def on_request(self, messages, model, context):
        self.request_count += 1

    def on_response(self, response, context):
        self.response_count += 1
        return response
```

### Custom Memory Backend

```python
from claudekit.memory import AbstractBackend, MemoryEntry

class RedisBackend(AbstractBackend):
    def __init__(self, redis_url: str):
        self.client = redis.from_url(redis_url)

    def get(self, key: str, scope: str | None = None) -> MemoryEntry | None: ...
    def save(self, entry: MemoryEntry) -> None: ...
    def delete(self, key: str, scope: str | None = None) -> None: ...
    def list_keys(self, scope: str | None = None) -> list[str]: ...
```

### Custom Router

```python
from claudekit.orchestration import Orchestrator

class PriorityRouter:
    """Route to agents based on message priority."""

    def route(self, message: str, agent_names: list[str]) -> str:
        if "urgent" in message.lower():
            return "priority_agent"
        return agent_names[0]

orch = Orchestrator(router=PriorityRouter())
```

---

## Every Component is Opt-In

claudekit is a framework, but not an opinionated one. Every component works
independently with lazy imports — use what you need, ignore the rest.
A developer using only `TrackedClient` imports nothing from security or memory.

```python
# Just tracking — no other imports loaded
from claudekit import TrackedClient

# Just tools
from claudekit.tools import tool

# Just security
from claudekit.security import SecurityLayer, Policy

# Just testing
from claudekit.testing import MockClient, expect
```

---

## Complete Export Index

All public names organized by module:

<details>
<summary><strong>claudekit (root)</strong> — 12 exports</summary>

`__version__`, `TrackedClient`, `AsyncTrackedClient`, `TrackedBedrockClient`, `TrackedVertexClient`, `TrackedFoundryClient`, `create_client`, `CallRecord`, `SessionUsage`, `tool`, `SecurityLayer`, `MemoryStore`, `SessionConfig`, `SessionManager`, `Orchestrator`, `MockClient`, `ClaudekitError`, `enable_rich_errors`

</details>

<details>
<summary><strong>claudekit.agents</strong> — 6 exports</summary>

`Agent`, `AgentResult`, `AgentRunner`, `AgentInspector`, `BudgetGuard`, `HookBuilder`

</details>

<details>
<summary><strong>claudekit.orchestration</strong> — 5 exports</summary>

`Orchestrator`, `OrchestrationResult`, `LLMRouter`, `RuleRouter`, `ManualRouter`

</details>

<details>
<summary><strong>claudekit.sessions</strong> — 4 exports</summary>

`Session`, `SessionConfig`, `SessionManager`, `MultiSessionUsage`

</details>

<details>
<summary><strong>claudekit.batches</strong> — 4 exports</summary>

`BatchManager`, `BatchBuilder`, `BatchResult`, `BatchStats`

</details>

<details>
<summary><strong>claudekit.memory</strong> — 6 exports</summary>

`MemoryStore`, `MemoryEntry`, `AbstractBackend`, `JSONFileBackend`, `SQLiteBackend`, `context_with_memory`

</details>

<details>
<summary><strong>claudekit.tools</strong> — 7 exports</summary>

`tool`, `ToolWrapper`, `ToolRegistry`, `MCPServer`, `ToolInputValidator`, `ToolError`, `ToolInputValidationError`

</details>

<details>
<summary><strong>claudekit.skills</strong> — 11 exports</summary>

`Skill`, `SkillRegistry`, `SummarizerSkill`, `ClassifierSkill`, `DataExtractorSkill`, `CodeReviewerSkill`, `ResearcherSkill`, `CodeReview`, `CodeReviewIssue`, `CodeReviewSuggestion`, `Research`, `ResearchFinding`

</details>

<details>
<summary><strong>claudekit.plugins</strong> — 6 exports</summary>

`Plugin`, `PluginLoader`, `PluginRegistry`, `LoggingPlugin`, `CostAlertPlugin`, `OpenTelemetryPlugin`

</details>

<details>
<summary><strong>claudekit.security</strong> — 3 exports</summary>

`SecurityLayer`, `Policy`, `SecurityContext`

</details>

<details>
<summary><strong>claudekit.testing</strong> — 12 exports</summary>

`MockClient`, `MockClientUnexpectedCallError`, `MockStreamContext`, `MockTransportHandler`, `create_mock_anthropic`, `MockAgentRunner`, `MockAgentResult`, `MockSession`, `MockSessionManager`, `assert_response`, `assert_agent_result`, `expect`, `ResponseRecorder`

</details>

<details>
<summary><strong>claudekit.prompts</strong> — 4 exports</summary>

`PromptManager`, `PromptVersion`, `ComparisonResult`, `JSONPromptStorage`

</details>

<details>
<summary><strong>claudekit.thinking</strong> — 4 exports</summary>

`thinking_enabled`, `thinking_adaptive`, `thinking_disabled`, `extract_thinking`

</details>

<details>
<summary><strong>claudekit.precheck</strong> — 2 exports</summary>

`TokenCounter`, `TokenCountResult`

</details>

<details>
<summary><strong>claudekit.models</strong> — 7 exports</summary>

`Model`, `MODELS`, `MODELS_BY_ID`, `MODELS_BY_NAME`, `ModelTask`, `get_model`, `select_model`

</details>

<details>
<summary><strong>claudekit.errors</strong> — 64 exports</summary>

**30 error code constants:** `PROMPT_INJECTION_DETECTED`, `PII_DETECTED`, `JAILBREAK_DETECTED`, `BUDGET_EXCEEDED`, `RATE_LIMIT_EXCEEDED`, `SESSION_BUDGET_EXCEEDED`, `TOKEN_LIMIT_EXCEEDED`, `AGENT_TIMEOUT`, `AGENT_MAX_TURNS`, `DELEGATION_LOOP`, `MEMORY_BACKEND_ERROR`, `MEMORY_KEY_NOT_FOUND`, `MEMORY_VALUE_TOO_LARGE`, `BATCH_CANCELLED`, `BATCH_NOT_READY`, `BATCH_PARTIAL_FAILURE`, `SESSION_PAUSED`, `SESSION_TERMINATED`, `SESSION_NAME_CONFLICT`, `CONTEXT_WINDOW_EXCEEDED`, `CONFIGURATION_ERROR`, `MISSING_API_KEY`, `DEPRECATED_MODEL`, `PLATFORM_NOT_AVAILABLE`, `OUTPUT_VALIDATION_FAILED`, `OVERLOADED`, `TOOL_BLOCKED`, `TOOL_INPUT_VALIDATION_FAILED`, `TOOL_JSON_ERROR`, `TOOL_RESULT_TOO_LARGE`, `API_CONNECTION_ERROR`, `API_TIMEOUT`, `PRICE_DISCLOSURE_DETECTED`

**31 exception classes:** `ClaudekitError`, `SecurityError`, `PromptInjectionError`, `PIIDetectedError`, `JailbreakDetectedError`, `BudgetError`, `BudgetExceededError`, `SessionBudgetExceededError`, `TokenLimitError`, `RateLimitError`, `AgentError`, `AgentTimeoutError`, `AgentMaxTurnsError`, `OrchestratorError`, `DelegationLoopError`, `ClaudekitMemoryError`, `MemoryBackendError`, `MemoryKeyNotFoundError`, `MemoryValueTooLargeError`, `BatchError`, `BatchNotReadyError`, `BatchCancelledError`, `BatchPartialFailureError`, `SessionError`, `SessionPausedError`, `SessionTerminatedError`, `SessionNameConflictError`, `PrecheckError`, `ContextWindowExceededError`, `ConfigurationError`, `MissingAPIKeyError`, `DeprecatedModelError`, `PlatformNotAvailableError`, `OutputValidationError`, `OverloadedError`, `ToolBlockedError`, `ToolInputValidationError`, `ToolJSONError`, `ToolResultTooLargeWarning`

**3 utilities:** `enable_rich_errors`, `wrap_sdk_error`, `DeprecatedModelWarning`

</details>

---

## License

MIT
