55 errors to zero. Nearly all of them traced back to two causes rather than 55. THE DECORATOR. @logged wraps ~24 functions across this package and was declared `def decorator(func: Callable):` with no ParamSpec and no return annotation, so it erased the signature of everything it touched. ToolResult.execute() is annotated `-> ToolResult`; through the decorator it came back Any, and mypy reported 33 no-any-return errors spread across the tools and agents. Each looked like a local annotation slip. All of them were one decorator. Typed with ParamSpec/TypeVar; the async branch casts at the await rather than loosening R, because loosening R would put the Any straight back into every caller. THE MISSING TYPE PARAMETER. BaseAgent was not generic, so _create_agent returned a bare Agent — Agent[Any, Any] — and pydantic_ai then typed every run() result as Any. BaseAgent is now Generic[CtxT] bound to AgentContext, _agent is declared on the base instead of reached through hasattr, and the three tool-registration functions take their agent's real context type. tools_streaming.py already did this; the other three had not been updated. Eight `execute` overrides carry a targeted ignore rather than a package-wide disable_error_code. Every tool narrows the base's **kwargs to its own named parameters, which is a real LSP violation — but nothing anywhere is typed as BaseTool, and every call site constructs the concrete tool. The abstract method earns its place by making a tool without execute impossible to instantiate. The reasoning lives in BaseTool.execute's docstring; the per-site suppressions mean an override that IS unsound still gets caught. BaseAgent.run_stream widened to AsyncIterator[str | StreamEvent], which is what callers already receive: task streams structured events, explore and plan stream strings, and the router branches on isinstance with a comment calling the string path legacy. The annotation now says what the code does. AND THE PART THAT MATTERS MORE THAN THE TYPES. Chasing the last error found that the Ollama sanitiser has been broken. It fetched the parent's chat getter with `AsyncOpenAI.chat.fget`, and openai made `chat` a functools.cached_property, whose getter is `.func`. Touching `.chat` raised AttributeError — meaning the content: null workaround that CLAUDE.md documents as live would have failed on the first completion any agent attempted. Confirmed in the running container (openai 2.46.0) as well as locally (2.15.0). Two things hid it. The line carried a bare `# type: ignore`, which suppressed precisely the complaint that would have caught it. And /agents/run and /agents/stream have served zero requests in 30 days, so nothing exercised the path. A mitigation can rot completely while every check stays green, if no check actually runs it. The lookup now reads whichever getter the descriptor exposes and raises a legible TypeError if openai adopts a third shape. tests/test_ollama_provider.py walks the chain an agent request walks, short of the network call — mutation-checked: all four fail against the old lookup. 215 passed, 23 skipped, plus the four new. mypy clean over 90 files. Co-Authored-By: Claude <noreply@anthropic.com>
Tools Domain
This domain contains tool implementations for agent use.
Tool Categories
File Tools (file/)
Tools for reading, writing, and finding files.
| Tool | Purpose | Key Parameters |
|---|---|---|
| Read | Read file contents | file_path, offset, limit |
| Write | Create/overwrite files | file_path, content |
| Edit | Exact string replacement | file_path, old_string, new_string, replace_all |
| Glob | Find files by pattern | pattern, path |
Read Tool:
- Returns line-numbered content (
cat -nformat) - Supports offset/limit for large files
- Can read images, PDFs, Jupyter notebooks
- Default: 2000 lines, 2000 chars per line
Edit Tool:
- Performs exact string replacements
- Fails if
old_stringis not unique (usereplace_allor provide more context) - Preserves indentation from Read output
Glob Tool:
- Supports patterns like
**/*.py,src/**/*.ts - Returns files sorted by modification time
- Use for finding files by name patterns
Shell Tools (shell/)
Tools for executing system commands.
| Tool | Purpose | Key Parameters |
|---|---|---|
| Bash | Execute shell commands | command, timeout, description |
Bash Tool:
- Persistent shell session
- 2-minute default timeout (max 10 minutes)
- Supports background execution (
run_in_background) - Quote paths with spaces:
cd "/path with spaces"
Git Operations:
- Never commit to main/master directly
- Use conventional commits format
- Never use
-iflag (interactive) - Never skip hooks unless explicitly requested
- Never force push to main/master
Security:
- Sandboxed execution when
SANDBOX_ENABLED=true - Validates against
ALLOWED_PATHS - Timeout enforcement
Search Tools (search/)
Tools for searching content and the web.
| Tool | Purpose | Key Parameters |
|---|---|---|
| Grep | Search file contents | pattern, path, glob, output_mode |
| WebSearch | Search the web | query, allowed_domains, blocked_domains |
| WebFetch | Fetch and analyze URLs | url, prompt |
Grep Tool:
- Built on ripgrep (NOT grep/rg bash commands)
- Supports regex patterns
- Output modes:
files_with_matches(default),content,count - Context lines:
-A,-B,-C
WebSearch Tool:
- Returns search results with URLs
- Always include sources in responses
- Domain filtering supported
WebFetch Tool:
- Fetches URL, converts HTML to markdown
- Processes content with AI for extraction
- 15-minute cache for repeated URLs
- Handles redirects (returns redirect URL)
Structure
tools/
├── router.py # Tool routes (list, execute)
├── controller.py # Tool orchestration
├── schemas.py # Tool request/response models
│
├── file/ # File operation tools
│ ├── __init__.py
│ ├── read.py # Read file contents
│ ├── write.py # Write file contents
│ ├── edit.py # Edit file contents
│ ├── glob.py # Find files by pattern
│ └── example-prompt.md
│
├── shell/ # Shell execution tools
│ ├── __init__.py
│ ├── bash.py # Execute bash commands
│ └── example-prompt.md
│
└── search/ # Search tools
├── __init__.py
├── grep.py # Search file contents
├── web.py # Web search and fetch
└── example-prompt.md
Tool Pattern
Tools are registered with PydanticAI agents via the @agent.tool decorator.
Each tool should:
- Have clear input/output types
- Include a docstring (used by LLM)
- Handle errors gracefully
- Respect sandbox settings
from src.shared.config import get_settings
from src.shared.logging import logged
settings = get_settings()
@logged()
async def read_file(file_path: str, limit: int = 2000) -> str:
"""
Read contents of a file.
Args:
file_path: Absolute path to the file
limit: Maximum lines to read
Returns:
File contents as string with line numbers
"""
# Check path is allowed
if settings.sandbox_enabled:
_validate_path(file_path, settings.allowed_paths)
# Read and return with line numbers
pass
Adding a New Tool
- Create a new file in appropriate category (file/, shell/, search/)
- Implement the tool function with proper types and docstring
- Add
@logged()decorator for timing - Handle sandbox restrictions
- Register with agents that need it
- Add tests
Reference Prompts
Each tool category directory contains an example-prompt.md file with reference
prompts from the claude-code-system-prompts repository. These document the expected
behavior and usage patterns for each tool.