Describes the current tool layer: the common tool contract, how tools are registered and dispatched, and what each built-in tool does at a high level.
The tool layer gives the runtime a typed interface for project-local actions.
Today that built-in tool set is intentionally small:
read_filelist_dirsearch_codegit_statusgit_diffgit_logedit_filewrite_file
The layer is built around explicit types rather than text parsing. Raw assistant text is parsed in src/runtime/protocol/tool_codec.rs before any tool is called, and the runtime may expose only a subset of registered tools on a given turn. Current tool-surface policy applies only to the read-only retrieval/Git families; edit_file and write_file are gated separately by mutation intent and approval.
Every tool implements the Tool trait in src/tools/mod.rs.
It exposes:
spec()for static tool metadatarun()for phase-one executionexecute_approved()for phase-two execution when approval is required
Read-only tools only use run(). Mutating tools use both phases.
The runtime never passes raw strings into tools. It passes one typed ToolInput variant:
ReadFileListDirSearchCodeGitStatusGitDiffGitLogEditFileWriteFile
Completed tools return typed outputs:
- file contents
- directory listings
- search results
- git status / diff / log results
- edit confirmations
- write confirmations
The runtime later decides how those outputs are rendered for the model and for the TUI.
run() returns one of two outcomes:
Immediate(ToolOutput)for read-only workApproval(PendingAction)for proposed mutations
PendingAction is the handoff object between the tool layer and the runtime approval flow.
It contains:
tool_namesummaryriskpayload
The runtime owns the approval lifecycle, but payload is opaque tool-owned data that is passed back into execute_approved().
ToolRegistry owns tool registration and lookup.
It is responsible for:
- registering tools by their
spec().name - dispatching typed
ToolInputto the right tool - delegating approved mutations back to the correct tool
- exposing sorted tool specs for the system prompt
The default registry is built in src/tools/mod.rs and initially registers only read_file and list_dir.
The remaining root-aware tools are added by ToolRegistry::with_project_root(...):
search_codegit_statusgit_diffgit_logedit_filewrite_file
ToolContext carries the discovered project root into each tool.
Relative paths:
- are resolved against the project root, not the process working directory
Absolute paths:
- are canonicalized for read-only tools and must still resolve within the project root
- are allowed for mutating tools only if they normalize within the project root
Mutating tools also reject .. path traversal.
Reads a file and returns its contents immediately.
Current behavior:
- resolves paths against the project root
- reads raw bytes, then converts to UTF-8 lossily
- truncates to the first
200lines - reports line count and truncation status
Runtime behavior adds one guardrail around failed reads: if read_file cannot read the requested file, the runtime injects the tool error and emits a runtime-owned terminal answer instead of asking the model to retry repeatedly.
Lists the immediate contents of one directory.
Current behavior:
- does not recurse
- returns entry name, kind, and file size when available
- skips directories in
DEFAULT_SKIP_DIRS - sorts directories before files
- sorts alphabetically within each group
- caps the returned listing at
200entries and reports truncation metadata when the directory is larger
Runtime investigation behavior can block list_dir before search_code on code-location questions. Directory listings are still useful as a read-only tool, but they are not accepted as the first evidence step for investigation-required prompts.
Searches recursively for lines containing a literal substring.
Current behavior:
- rejects empty queries
- walks the project tree recursively
- skips hidden directories and common build/output directories such as
target,node_modules,.git,dist, andbuild - searches only a fixed set of text-like extensions
- returns matching file path, line number, and line text
- collects up to
50matches internally - orders collected matches by file class before truncation: source files, then config/data files, then docs/text/unknown files
- preserves deterministic ordering within each file class
- shows up to
15matches in the conversation output - does not interpret the query as regex or semantic search
Rendered search output is grouped by file for model readability:
- each file group shows the file path and match count
- each file group shows up to
MAX_LINES_PER_FILE = 3representative matching lines - files with more shown matches include a per-file "showing" count
When exactly one source-tier file has a definition-like matching line, tool_codec prepends:
[definition found in <file> — read this file first]
Definition detection is based on match content, not the search query. If zero or multiple source files contain definition-like lines, no hint is rendered.
This grouping and hinting are presentation-only in tool_codec. The underlying typed data remains
the same: SearchMatch and SearchResultsOutput are unchanged, and runtime behavior does not
depend on parsing the grouped text. This is not a broad ranking system or query-intent classifier.
The typed input supports an optional scoped path, but the current model-facing wire format does not expose that scoped form yet.
Runtime behavior adds guardrails around the tool because prompt-only guidance was not enough for live local-model behavior:
- model-facing instructions ask for one plain literal keyword or identifier
- runtime simplifies phrase-like or method-like model queries to one literal token before dispatch
- each user turn gets one search, plus one retry only if the first search returned no matches
- after search is closed, additional
search_codeattempts are blocked by runtime correction - investigation-required turns must read a file from the current search candidates before synthesis
- usage lookups reject definition-only reads as sufficient evidence when usage candidates exist
- after a definition-only read on a usage lookup, runtime can inject a bounded correction naming a concrete matched usage file
- definition lookups still accept a definition-file read as sufficient evidence
Runs a fixed read-only status command and returns structured repository status information immediately.
Current behavior:
- runs a bounded
git status --short --branch - captures stdout/stderr with deterministic size limits
- truncates long status entry paths in rendered output
- returns a deterministic tool error when Git is unavailable or the project root is not a Git repository
This is not an arbitrary Git command runner. The tool takes no free-form arguments.
Runs a fixed read-only diff command and returns the current working-tree diff immediately.
Current behavior:
- runs a bounded
git diff --no-ext-diff --no-textconv --no-color -- - captures stdout/stderr with deterministic size limits
- returns structured diff text plus truncation metadata
- returns a deterministic tool error when Git is unavailable or the project root is not a Git repository
Like the other Git tools, this is intentionally narrow and argument-free.
Runs a fixed read-only recent-history command and returns recent commits immediately.
Current behavior:
- runs a bounded recent-history log with
--max-count=20 - disables pager/signature noise and uses short dates
- returns parsed commit entries with truncation metadata
- treats empty history and non-repo failures deterministically
Proposes an exact text replacement in an existing file.
Current behavior:
- requires a non-empty path and non-empty search text
- requires the search text to exist before approval is requested
- returns
Approval(PendingAction)instead of mutating immediately - uses
RiskLevel::Medium - re-checks that the search text still exists when approved
- replaces only the first occurrence
This tool is intentionally exact. It does not do fuzzy patching or diff application.
If the model tries to repair a malformed edit_file call after an edit tool error but still omits the required structure, the runtime injects an edit-specific correction and asks for a valid block instead of silently treating the malformed retry as a final answer.
The model-facing form remains the canonical ---search--- / ---replace--- block. For observed local-model drift, tool_codec also accepts narrow compatibility forms such as old content: / new content: labels and generic triple-dash delimiter pairs inside [edit_file]...[/edit_file]. Those still become exact EditFile inputs and go through normal validation and approval.
Proposes creating or overwriting a file with full content.
Current behavior:
- requires a non-empty path
- returns
Approval(PendingAction)instead of writing immediately - uses
RiskLevel::Mediumfor creates andRiskLevel::Highfor overwrites - checks actual file existence again at execution time
- does not create missing parent directories
This is a full-file write tool, not an append or patch tool.
Tools must not:
- parse raw assistant output
- decide which tools are available for the current turn
- manage approval state after returning
PendingAction - write UI messages
- persist sessions
- depend on terminal state
Those responsibilities belong to the runtime, app/storage, and TUI layers.
- There are only eight built-in tools.
- There is no shell, web, or external integration tool.
search_codeuses a simple literal line substring search, not regex or semantic search.- The current model-facing
search_codewire format exposes only the plain query form, even though the typed input supports an optional scoped path. edit_fileonly replaces the first exact match.write_filedoes not create parent directories.- Tool result rendering is optimized for the runtime and TUI, not for rich previews or diffs.