# Skill: Add a New Tool

Use when you need to add a new callable capability that agents can use during their ReAct loop.
Tools are the functions an LLM calls when it needs external information or side effects.

## File Role Distinction: `arccrew/tools/` vs `arccrew/utils/helpers.py`

This distinction is strict and non-negotiable:

| File | What goes here | Decorator |
|------|---------------|-----------|
| `arccrew/tools/` (library) or `tools/` (your project) | Functions agents call during ReAct | `@tool` (LangChain) |
| `arccrew/utils/helpers.py` | Pure Python utilities, no LLM interaction | None |

**`@tool`** makes the function visible to the LLM's tool-calling API. The docstring becomes
the description the LLM reads when deciding whether to call it. The parameter names and
type hints become the JSON schema the LLM fills in.

**Do not add `@tool`** to: formatters, text cleaners, date helpers, math utilities. Those
go in `arccrew/utils/helpers.py` (already has `truncate_text`, `extract_json_safe`, `utcnow_iso`,
`slugify`, `chunk_list`, `safe_get`).

## `@tool` Decorator Syntax

LangChain's `@tool` decorator is from `langchain_core.tools`:

```python
from langchain_core.tools import tool

@tool
async def my_tool(param: str) -> str:
    """One-sentence description of what this tool does.

    Use for: specific use cases that tell the LLM when to call this.
    Do NOT use for: cases where the agent should use a different tool.

    Args:
        param: What this parameter is and the format the agent should use.

    Returns:
        Description of the return value format.
    """
    # implementation
    return result
```

The docstring is **critical** — the LLM reads this to decide whether to call the tool and
how to fill its arguments. Write it from the LLM's perspective.

## Reserved Parameter Names to Avoid

LangChain wraps `@tool` functions with Pydantic models. Avoid these parameter names — they
shadow built-in Pydantic `BaseModel` attributes and produce `UserWarning` at startup:

| Avoid | Use instead |
|-------|-------------|
| `copy` | `post_copy`, `text_copy`, `content` |
| `schema` | `output_schema`, `data_schema` |
| `fields` | `field_list`, `field_names` |
| `validate` | `should_validate`, `run_validation` |
| `dict` | `data`, `payload` |
| `json` | `json_str`, `json_data` |

## Async vs Sync

**Prefer async for anything that does I/O** (HTTP requests, file reads, subprocess calls,
database queries). The agent loop runs in an async context; sync tools block the event loop.

```python
# Good — async I/O
@tool
async def fetch_page(url: str) -> str:
    """Fetch the content of a web page."""
    import httpx
    async with httpx.AsyncClient() as client:
        resp = await client.get(url, timeout=10.0)
        return resp.text[:5000]

# OK — sync for pure CPU work with no I/O
@tool
def parse_csv_row(row: str) -> str:
    """Parse a CSV-formatted row into key=value pairs."""
    import csv
    reader = csv.reader([row])
    fields = next(reader)
    return ", ".join(f"col{i}={v}" for i, v in enumerate(fields))
```

## Error Handling: Return Strings, Never Raise

Tools must **return error strings** rather than raising exceptions. When a tool raises,
the ReAct loop crashes. When a tool returns an error string, the LLM reads it and
decides how to proceed (retry with different args, skip, or report failure).

```python
@tool
async def lookup_price(product_id: str) -> str:
    """Look up the current price for a product by its ID."""
    try:
        # Validate input
        if not product_id.strip():
            return "ERROR: product_id cannot be empty."

        result = await _call_pricing_api(product_id)
        return f"Price: ${result['price']:.2f} USD"

    except httpx.TimeoutException:
        return "ERROR: Pricing API timed out. Try again or use a different product ID."
    except KeyError:
        return f"ERROR: Product '{product_id}' not found."
    except Exception as e:
        logger.exception("[lookup_price] unexpected error for %r: %s", product_id, e)
        return f"ERROR: Lookup failed: {e}"
```

Prefix error returns with `"ERROR:"` — agents are trained to recognize this pattern.

## DuckDuckGo Search as Reference

The existing `web_search` tool in `arccrew/tools/agent_tools.py` is the canonical example:

```python
@tool
async def web_search(query: str) -> str:
    """Search the web for information.

    Use for: market research, competitor analysis, best practices,
    current trends, or any external data your agents need.

    Args:
        query: Search query (be specific for better results)

    Returns:
        Formatted search results with titles, URLs, and snippets.
    """
    try:
        from ddgs import DDGS
    except ImportError:
        return "ERROR: ddgs package not installed. Run: pip install ddgs"

    try:
        from concurrent.futures import ThreadPoolExecutor
        def _do_search():
            return list(DDGS().text(query, max_results=5))

        loop = asyncio.get_event_loop()
        with ThreadPoolExecutor() as executor:
            try:
                results = await asyncio.wait_for(
                    loop.run_in_executor(executor, _do_search), timeout=10.0
                )
            except asyncio.TimeoutError:
                return "ERROR: Web search timed out."

        if not results:
            return "No results found."

        lines = []
        for idx, r in enumerate(results, 1):
            lines.append(f"{idx}. **{r.get('title', '')}**")
            lines.append(f"   URL: {r.get('href', r.get('url', ''))}")
            if r.get("body"):
                lines.append(f"   {r['body']}")
            lines.append("")
        return "\n".join(lines)

    except Exception as e:
        return f"ERROR: Web search failed: {e}"
```

Key patterns from this reference:
- Graceful `ImportError` handling for optional dependencies
- `asyncio.wait_for` for timeouts on blocking calls
- `ThreadPoolExecutor` to run sync library in async context
- Empty result case handled explicitly
- Output truncated/formatted for the LLM (not raw API response)

## Expose via a Getter Function

Group related tools behind a getter so agents import cleanly:

```python
# tools/my_tools.py

@tool
async def tool_a(param: str) -> str:
    """Does A."""
    ...

@tool
async def tool_b(param: str) -> str:
    """Does B."""
    ...

def get_my_tools() -> list:
    """Get tools for the my-domain use case."""
    return [tool_a, tool_b]
```

Then in agents:
```python
from tools.my_tools import get_my_tools

result = await self.run_react(
    task="...",
    tools=get_my_tools(),
)
```

You can also compose with existing tool sets:
```python
from arccrew.tools.agent_tools import get_research_tools
from tools.my_tools import get_my_tools

tools = get_research_tools() + get_my_tools()
result = await self.run_react(task="...", tools=tools)
```

## How Agents Receive Tools via `run_react()`

`BaseAgent.run_react()` in `arccrew/agents/base.py` creates a new `create_react_agent` with
the provided tools on each call:

```python
async def run_react(self, task: str, tools: list, ...) -> str:
    agent = self.create_agent(tools, prompt)          # tools bound here
    result = await agent.ainvoke({"messages": [("user", task)]}, ...)
    ...
```

This means:
- Tools are bound per-call, not per-agent-instance
- Different calls to the same agent can use different tools
- The LLM sees the tool names and docstrings in its context window

## Workspace Tools (File + Shell)

`arccrew/tools/workspace.py` provides `create_workspace_tools(path)` which returns:
- `read_file` — read a file from workspace
- `write_file` — write a file to workspace
- `list_directory` — list files in workspace
- `copy_file` — copy a file within workspace
- `run_shell` — run an async shell command scoped to workspace

These are not in `arccrew/tools/agent_tools.py` because they are stateful (scoped to a path):

```python
from arccrew.tools.workspace import create_workspace_tools
from pathlib import Path

workspace = Path(state.get("workspace_dir") or get_settings().workspace_dir).resolve()
tools = create_workspace_tools(workspace, include_shell=True)

result = await self.run_react(task="...", tools=tools)
```

`include_shell=False` disables `run_shell` for agents that should not execute commands.
`readonly=True` removes write/copy tools.

## Using MCP Servers as Tools

Instead of writing a custom `@tool`, you can connect any MCP-compatible server
and get its tools automatically. This is ideal for external services like GitHub,
Notion, Slack, databases, or any service that exposes an MCP endpoint.

```python
import os
from arccrew.tools import get_mcp_tools

# In execute() — async, returns list[BaseTool]
tools = await get_mcp_tools({
    "github": {
        "transport": "sse",
        "url": "https://api.githubcopilot.com/mcp/",
        "headers": {"Authorization": f"Bearer {os.getenv('GITHUB_TOKEN')}"},
    }
})
result = await self.run_react(task=task, tools=tools)
```

Requires: `pip install "arccrew[mcp]"`

**When to use MCP vs custom `@tool`:**

| Use custom `@tool` | Use `get_mcp_tools()` |
|--------------------|-----------------------|
| Domain logic specific to your project | Calling an external service that already has an MCP server |
| Wrapping a Python library | GitHub, Notion, Slack, databases |
| Transforming/formatting data | Any service from the MCP servers directory |
| Logic the LLM needs to control | |

See `skills/configure-mcp/SKILL.md` for full documentation and transport options.

## Adding a Tool to a New File

When creating a brand-new tools file:

1. Create `tools/my_domain.py` in your project directory
2. Import `tool` from `langchain_core.tools`
3. Define `@tool` functions with complete docstrings
4. Export via `get_my_domain_tools()` function
5. In your agent, import and pass to `run_react(tools=get_my_domain_tools())`

Do NOT modify `arccrew/tools/agent_tools.py` or `arccrew/tools/workspace.py` unless you are
extending the framework itself.
