# Tool Call Router

The **ToolCallRouter** translates between LLM provider-specific tool call formats and the internal `ToolRunner`. It supports OpenAI, Anthropic, and LangChain formats, producing normalized `NormalizedToolCallResult` objects regardless of the input format.

## Provider Format Mapping

| Provider   | Tool Call Shape | Arguments Key | Arguments Type |
|------------|-----------------|---------------|----------------|
| **OpenAI** | `{"name": "...", "arguments": "..."}` | `arguments` | JSON string |
| **Anthropic** | `{"name": "...", "input": {...}}` | `input` | dict |
| **LangChain** | `{"name": "...", "type": "tool_call", "args": {...}}` | `args` | dict |

## Architecture

```
Provider (OpenAI/Anthropic/LangChain)
    |
    v
ToolCallRouter.route(payload)
    |-- detect_provider_format(payload)
    |-- normalize_tool_call(payload) -> ToolCallRequest
    |-- generate_tool_call_id(plan_id, sequence)
    |-- ToolRunner.execute(name, args)
    |-- classify_tool_error(error) [if failed]
    |-- _check_is_validation(spec) [validation surfacing]
    v
NormalizedToolCallResult
```

## Usage

### Basic Routing

```python
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runner import ToolRunner
from cleveragents.tool.router import ToolCallRouter

registry = ToolRegistry()
# ... register tools ...
runner = ToolRunner(registry)
router = ToolCallRouter(registry=registry, runner=runner, plan_id="plan-001")

# Route an OpenAI-format tool call
result = router.route({
    "name": "my-ns/my-tool",
    "arguments": '{"key": "value"}'
})
print(result.result.success)
print(result.tool_call_id)
print(result.provider_format)  # "openai"
```

### Batch Routing

```python
results = router.route_batch([
    {"name": "ns/tool-a", "arguments": '{"x": 1}'},
    {"name": "ns/tool-b", "input": {"y": 2}},
])
for r in results:
    print(f"{r.tool_name}: success={r.result.success}")
```

### Streaming Execution

```python
for update in router.route_streaming(payload):
    if isinstance(update, StreamingToolUpdate):
        print(f"Status: {update.status}")
    elif isinstance(update, NormalizedToolCallResult):
        print(f"Final: success={update.result.success}")
```

### Schema Export

```python
from cleveragents.tool.router import ProviderFormat

schemas = router.export_schemas(ProviderFormat.OPENAI)
# Returns provider-normalized schemas for all registered tools
```

## Stable ID Generation

Tool call IDs are generated deterministically from `plan_id + sequence`:

```python
from cleveragents.tool.router import generate_tool_call_id

call_id = generate_tool_call_id("plan-001", 0)
# Returns: "tc_<24-char-hex>"
```

The same inputs always produce the same ID, enabling reproducible tracking.

## Error Classification

Errors are classified into structured categories:

| Category | Matches |
|----------|---------|
| `timeout` | "timeout", "timed out" |
| `permission` | "permission", "access denied", "forbidden" |
| `not_found` | "not found" |
| `resource` | "resource", "memory", "disk" |
| `schema` | "schema", "validation", "json" |
| `parse` | "parse", "decode", "deserializ" |
| `execution` | Everything else |
| `unknown` | Empty error messages |

## Validation Surfacing

When a tool is detected as a validation (read-only tool with "valid" in its name), the router:

1. Sets `is_validation=True` on the result
2. Extracts `passed` from the tool output
3. Includes the `validation_mode` if available in the output schema

## Provider Metadata

The router captures provider-level metadata passed during routing:

```python
result = router.route(payload, provider_metadata={
    "model": "gpt-4",
    "provider_id": "openai",
    "latency_ms": 150,
})
print(result.provider_metadata["model"])  # "gpt-4"
```

## Schema Normalization

Schemas are normalized per provider with field pruning and description truncation:

| Provider | Schema Key | Default Max Description |
|----------|-----------|------------------------|
| OpenAI | `parameters` | 1024 chars |
| Anthropic | `input_schema` | 1024 chars |
| LangChain | `args_schema` | 1024 chars |
