# Custom Tools

KOAN's `@tool` decorator turns any Python function into an agent-callable tool with automatic JSON schema generation from type hints.

## Basic Usage

```python
from koan.tools import tool

@tool
def get_weather(city: str, units: str = "celsius") -> str:
    """Get the current weather for a city."""
    return f"22C and sunny in {city}"
```

The decorator:
1. Introspects the function signature and type hints
2. Builds a JSON schema via Pydantic
3. Attaches a `ToolDefinition` to the function as `__tool_definition__`

The **docstring** becomes the tool description sent to the LLM -- write clear, actionable docstrings.

## Async Tools

```python
@tool
async def search_database(query: str, limit: int = 10) -> list[dict]:
    """Search the database for matching records."""
    results = await db.search(query, limit=limit)
    return results
```

Both sync and async tools are supported. Async tools are awaited during execution.

## Custom Name and Description

```python
@tool(name="web_search", description="Search the internet for information")
def search(query: str, max_results: int = 5) -> str:
    """Internal implementation of web search."""
    ...
```

## Complex Parameters

Use Pydantic `Field` for parameter descriptions and constraints:

```python
from pydantic import Field

@tool
def create_ticket(
    title: str = Field(description="Short ticket title"),
    priority: int = Field(default=3, ge=1, le=5, description="Priority 1-5"),
    labels: list[str] = Field(default_factory=list, description="Tags for the ticket"),
) -> str:
    """Create a support ticket."""
    return f"Created ticket: {title} (P{priority})"
```

Use Pydantic models for nested/complex input:

```python
from pydantic import BaseModel

class Address(BaseModel):
    street: str
    city: str
    zip_code: str

@tool
def validate_address(address: Address) -> bool:
    """Validate a shipping address."""
    return bool(address.zip_code)
```

## Tool Registry

Group tools into a registry and pass to agents:

```python
from koan.tools import ToolRegistry

registry = ToolRegistry()
registry.add(get_weather)
registry.add(search_database)
registry.add(create_ticket)

# Or use the decorator form
@registry.register
def another_tool(x: int) -> int:
    """Double a number."""
    return x * 2

# List all tools
for defn in registry.definitions():
    print(f"{defn.name}: {defn.description}")

# Use with an agent
agent = ReactAgent("bot", client=client, config=config, tools=registry)
```

## Manual Execution

Execute tools directly from the registry:

```python
result = await registry.execute("get_weather", {"city": "Tokyo"})
print(result.output)   # "22C and sunny in Tokyo"
print(result.is_error)  # False

# With JSON string arguments (as LLMs produce)
result = await registry.execute("get_weather", '{"city": "Tokyo"}')
```

## RunContext Injection

Tools can receive a `RunContext` parameter that is automatically injected (not visible to the LLM):

```python
from koan.agents.types import RunContext

@tool
def user_lookup(user_id: str, context: RunContext) -> str:
    """Look up a user by ID."""
    # context.thread_id, context.run_id, etc. are available
    return f"User {user_id} in thread {context.thread_id}"
```

The `RunContext` parameter is excluded from the JSON schema and injected at execution time.

## ToolProvider Protocol

Both `ToolRegistry` and `MCPToolSource` implement the `ToolProvider` protocol:

```python
from koan.tools import ToolProvider

def create_agent(tools: ToolProvider) -> ReactAgent:
    """Accepts any tool source -- local registry or MCP."""
    return ReactAgent("bot", client=client, config=config, tools=tools)
```

This means you can swap between local tools and MCP-connected tools without changing agent code.

## Error Handling

Tool errors are caught and returned as `ToolResult` with `is_error=True`:

```python
@tool
def risky_operation(x: int) -> int:
    """Divide 100 by x."""
    return 100 // x  # Raises ZeroDivisionError if x=0

result = await registry.execute("risky_operation", {"x": 0})
print(result.is_error)  # True
print(result.error)     # "integer division or modulo by zero"
```

The agent sees the error message and can retry or adjust its approach.
