# Adding a tool

One file, one `impl`, one registry entry. The architecture handles the rest — cancellation, progress, identity, and workdir all ride inside `ExecContext` rather than being plumbed through parameters.

## 1. Pick a name

The name is what the model sees when it decides to call your tool. Keep it `snake_case`, short, and action-first: `read_file`, `web_search`, `execute_command`. MCP tools get the `mcp__<server>__<name>` prefix automatically; don't replicate that here.

## 2. Implement `ToolExecutor`

```rust
// src/providers/tool/my_tool.rs
use async_trait::async_trait;

use crate::domain::{ToolMetadata, ToolOutcome, ToolRunMetadata};
use crate::providers::ctx::ExecContext;
use crate::providers::tool::ToolExecutor;

pub struct MyTool;

#[async_trait]
impl ToolExecutor for MyTool {
    fn name(&self) -> &'static str {
        "my_tool"
    }

    async fn execute(&self, args: serde_json::Value, ctx: ExecContext) -> ToolOutcome {
        let Some(input) = args.get("input").and_then(|v| v.as_str()) else {
            return ToolOutcome::error("my_tool requires 'input' (string)", 0.0);
        };

        let start = std::time::Instant::now();

        // Race any meaningful await against ctx.token. This is
        // the whole contract for cancellation — if your code blocks
        // for more than a few ms without select!-ing the token,
        // Ctrl+C won't abort cleanly.
        tokio::select! {
            biased;
            _ = ctx.token.cancelled() => ToolOutcome::cancelled(),
            result = do_the_work(input, &ctx.workdir) => match result {
                Ok(output) => ToolOutcome::success(
                    output,
                    "my_tool completed",
                    start.elapsed().as_secs_f64(),
                )
                .with_metadata(ToolRunMetadata {
                    detail: ToolMetadata::Custom {
                        name: "my_tool".to_string(),
                        data: serde_json::json!({ "input": input }),
                    },
                    ..ToolRunMetadata::default()
                }),
                Err(e) => ToolOutcome::error(
                    format!("my_tool: {}", e),
                    start.elapsed().as_secs_f64(),
                ),
            },
        }
    }
}

async fn do_the_work(input: &str, workdir: &std::path::Path) -> anyhow::Result<String> {
    // Your actual tool body.
    Ok(format!("processed {} in {}", input, workdir.display()))
}
```

## 3. Register it

Either in the default registry or a custom one the effect runner is wired with.

```rust
// src/providers/tool/mod.rs (abridged)
impl Default for ToolRegistry {
    fn default() -> Self {
        let mut r = Self::new();
        r.register(Arc::new(filesystem::ReadFileTool));
        r.register(Arc::new(filesystem::WriteFileTool));
        r.register(Arc::new(my_tool::MyTool));  // ← new line
        r
    }
}
```

## 4. Describe it to the model

The registry routes calls; the model needs to know the name + schema. Add a `ToolDefinition` alongside the existing ones (same file, adjacent spot) so the outgoing `ChatRequest::tools` includes your tool.

## Dos and don'ts

**Do** emit progress events through `ctx.progress` for multi-step work. The reducer doesn't act on them today, but the renderer will surface them in a follow-up commit and recorded sessions include them.

**Don't** call `tokio::time::sleep` or any other long await without racing it against `ctx.token.cancelled()`. Users expect Ctrl+C to abort; forgetting the select race is the structural bug the old architecture let slip repeatedly.

**Do** return `ToolOutcome::error(...)` for expected failures (bad input, missing file, permission denied). The error message lands in the tool-result message the model sees next turn, so make it useful.

**Do** attach typed metadata with `with_metadata(...)` when the UI or recorder should know facts like line counts, result counts, process ids, or generated artifacts. The model still receives `model_content`; Mermaid renders and records from the structured fields.

**Don't** hand back paths outside `ctx.workdir` unless the tool explicitly accepts absolute paths. Relative args should resolve against the working directory so the behavior matches user expectations.

**Do** write a unit test. `crate::providers::ctx::test_exec_context(turn, call_id, workdir)` gives you a pre-built `ExecContext` + receiver you can assert against without needing a runtime.

## Testing checklist

- Happy path: valid args, success outcome.
- Missing args: returns `ToolStatus::Error`.
- Cancellation: pre-cancel the token, call execute, assert `outcome.was_cancelled()`.
- Workdir handling: relative paths resolve against `ctx.workdir`.
