Tools
kaid ships with six built-in tools the LLM can call during a review.
This page documents each tool's purpose, input schema, and example
input/output. The full machine-readable definitions live in
internal/config/toolsconfig/tools.json.
Tool availability per phase¶
Each tool declares whether it's exposed during the plan phase, the main task, or both:
| Tool | Plan | Main | Purpose |
|---|---|---|---|
task_done |
✗ | ✓ | Signal "I'm finished" — terminates the loop. |
code_comment |
✗ | ✓ | Emit a review comment with line range + suggestion. |
file_read |
✗ | ✓ | Read a slice of a file from the post-change snapshot. |
file_read_diff |
✓ | ✓ | Read another file's diff to confirm a cross-file concern. |
file_find |
✓ | ✓ | Locate files by filename keyword. |
code_search |
✓ | ✓ | Grep across the repo (literal or regex). |
task_done and code_comment are intentionally not available
during the plan phase: planning is read-only.
Context tools are read-only context, not comment targets. The
main_taskprompt explicitly forbids commenting on findings in other files.file_read,file_read_diff,file_find, andcode_searchexist so the model can understand the current file's diff better — any issue spotted while gathering that context is ignored by design. Cross-file concerns surface as comments only when they're observable from the current file's diff.
To override the tool registry, pass --tools <path> to a JSON file with
the same shape as the embedded one. This lets you disable a tool, edit
a description, or add a new tool backed by an existing provider.
task_done¶
Terminate the main loop.
| Field | Required | Meaning |
|---|---|---|
state |
yes | DONE (default) or FAILED. FAILED is for "I literally cannot use the available tools to do this" — almost never the right choice. |
When the agent sees task_done, it stops calling the LLM and starts
processing accumulated code_comment calls. task_done returns
immediately (before the result is recorded in the session log), so the
state value is accepted but not persisted — it doesn't affect exit
codes either.
code_comment¶
Emit one or more review comments. Each comment is anchored to a code
snippet (existing_code) so kaid can compute line numbers automatically.
Schema¶
{
"name": "code_comment",
"input": {
"path": "string — optional, override the file path for this comment",
"comments": [
{
"content": "string — the comment in the configured language",
"existing_code": "string — snippet from the diff to anchor on",
"suggestion_code": "string — optional fix snippet",
"thinking": "string — optional, the model's reasoning for this comment"
}
]
}
}
comments is an array, so the model can emit several comments in one
tool call. content and existing_code are required; suggestion_code
is optional but encouraged. path is a top-level optional override —
if omitted, the agent injects the file currently under review. The
agent also injects path automatically when the model leaves it out, so
the model rarely needs to set it explicitly. thinking (per-comment)
captures the model's reasoning and is preserved on the comment; kaid
backfills it from the reasoning content the model emits on the current
turn (it stays empty when the model emits none), and includes it in
the JSON output (the terminal output does not
render it).
thinkingis a runtime-only field. kaid parses and stores it, but it is deliberately not listed in thecode_commentschema advertised to the model intools.json(onlycontent,existing_code, andsuggestion_codeare). Stronger models that emit athinkingblock anyway will have it persisted; most won't send it, which is fine.
Anchoring algorithm¶
kaid walks the diff looking for the text in existing_code using a
dynamic sliding window. The match tries, in order:
- Hunk new-side — a run of consecutive context + added lines (not deleted-only, not unchanged-only), yielding new-file line numbers. If that fails, kaid retries the hunk old-side — context + deleted lines — yielding old-file line numbers.
- Full new-file scan — if no hunk matched, kaid scans the entire
post-change file content line-by-line for consecutive matches
(
resolveFromFileContent). - Re-location task — if text matching still fails on a non-trivial
diff, kaid runs the
RE_LOCATION_TASKprompt asking the model to re-anchor the snippet.
Matching is whitespace-insensitive: lines are trimmed and diff
+/- markers stripped before comparison, so indentation need not
match exactly. As a last resort the comment is delivered with
start_line=0, telling the user "the issue is real but you'll need to
find the spot yourself".
Example¶
{
"comments": [
{
"content": "`tx.Rollback()` is never deferred — early returns leak the transaction.",
"existing_code": "tx, err := db.Begin()\nif err != nil {\n return err\n}",
"suggestion_code": "tx, err := db.Begin()\nif err != nil {\n return err\n}\ndefer tx.Rollback()"
}
]
}
file_read¶
Read a range of lines from a file in its post-change form.
Schema¶
| Field | Required | Default | Notes |
|---|---|---|---|
file_path |
yes | — | Path relative to the repo root. |
start_line |
no | 1 |
1-indexed. |
end_line |
no | end of file | Inclusive. |
Output¶
File: src/foo.go (Total lines: 220)
IS_TRUNCATED: false
LINE_RANGE: 10-80
10|package foo
11|
12|import (
13| "fmt"
…
Each content line is prefixed with its 1-indexed line number and a |
separator so the model can quote line numbers precisely in subsequent
code_comment calls.
Limits¶
- 500 lines max per call. Larger ranges are truncated,
IS_TRUNCATED: trueis set, and a trailingNote: Results truncated to 500 lines. Please narrow your line range.is appended. - Reads only the modified version of the file. To see the old
version, use
file_read_diff.
When the model needs surrounding context (for a comment about a
function it can only see in the diff), it should compute the range
from the diff hunk header @@ -x,y +m,n @@ — typically m-50 to
m+n+50.
file_read_diff¶
Read the diff for one or more other files in the same change set — useful when a comment hinges on whether a related file was updated.
Schema¶
{
"name": "file_read_diff",
"input": {
"path_array": ["src/api/handler.go", "src/db/queries.go"]
}
}
Output¶
==== FILE: src/api/handler.go ====
--- a/src/api/handler.go
+++ b/src/api/handler.go
@@ -10,1 +10,2 @@
- old line
+ new line 1
+ new line 2
==== FILE: src/db/queries.go ====
@@ -5,1 +5,1 @@
- query := "SELECT *"
+ query := "SELECT id"
A path that isn't in the change set is not silently dropped: it is
named in Error: diff not found for: <path>, and diffs for the paths
that were found are still returned ahead of it. When the run has
diffs for other files the message lists them (Diffs exist for: ...)
so a mis-spelled path can be corrected; when the run has no diffs at
all it points to file_read instead. An empty path_array returns
Error: no files found.
file_find¶
Find files in the repo by repository-relative path or filename keyword (substring match).
Schema¶
| Field | Required | Default | Notes |
|---|---|---|---|
query_name |
yes | — | Substring matched against each file's repository-relative path (accepting both / and \ path separators). |
case_sensitive |
no | false |
Set to true for exact-case matching. |
The candidate set comes from git ls-files --cached --others
--exclude-standard in workspace mode, or git ls-tree -r --name-only
<ref> in range / commit mode. Extensionless files are skipped except
for Makefile, Dockerfile, LICENSE, Vagrantfile, Containerfile.
Output¶
A newline-separated list of paths:
src/main/java/com/example/UserService.java
src/test/java/com/example/UserServiceTest.java
src/main/java/com/example/internal/UserServiceImpl.java
When no file matches (or query_name is blank), the tool returns the
literal string // The file was not found.
Limits¶
Returns up to 100 matches; excess is silently truncated. If the
model needs broader search, it should fall through to code_search.
code_search¶
Full-text search across the repo. Backed by git grep, so it
understands pathspec syntax and respects .gitignore.
Schema¶
{
"name": "code_search",
"input": {
"search_text": "TODO|FIXME",
"file_patterns": ["*.go", ":(exclude)vendor/"],
"case_sensitive": false,
"use_perl_regexp": true
}
}
| Field | Required | Default | Notes |
|---|---|---|---|
search_text |
yes | — | Literal string or PCRE pattern (see use_perl_regexp). |
file_patterns |
no | whole repo | Array of pathspec entries. Use :(exclude)pat to subtract. |
case_sensitive |
no | false |
— |
use_perl_regexp |
no | false |
When true, search_text is treated as a regex. |
Output¶
Results are grouped by file. Each group starts with File: <path> and
Match lines: <n>, followed by one line|content line per hit:
File: path/to/example.java
Match lines: 2
433| String name = toolRequest.get().getName();
438| logToolRequest(newPath, tool, toolRequest.get());
File: path/to/other.java
Match lines: 1
22| var req = new ToolRequest();
When there are no matches, the tool returns the literal string
No matches found.
Pathspec cookbook¶
| Goal | file_patterns |
|---|---|
| Single file | ["src/main.go"] |
| All Go files | ["*.go"] |
| All Go except tests | ["*.go", ":(exclude)*_test.go"] |
| Only one directory | ["src/api/"] |
| Multiple types, no vendor | ["*.go", "*.ts", ":(exclude)vendor/", ":(exclude)node_modules/"] |
Limits¶
- Caps matches at 100 per file via
git grep --max-count 100, so total output across many files can exceed 100. When the per-file cap is hit the output is prefixed withNote: The results have been truncated. Only showing first 100 results.. - Empty / whitespace-only
search_textreturnsError: search_text is blankinstead of expanding to every line. - Searches the current working tree in workspace mode, or the
resolved target ref in range / commit mode (the
FileReader.Refis passed as a positional argument togit grep).
Tool execution & errors¶
Tools execute synchronously inside the agent loop, with two exceptions:
code_commentis dispatched to the CommentWorkerPool so the loop doesn't block on line-resolution + reflection.task_doneshort-circuits — it returns immediately without invoking any provider.
When a tool errors (network failure, malformed args, file not found),
the result is delivered to the model as a regular tool result with text
like "Error: file not found: src/missing.go". The model then decides
whether to retry, ask for a different file, or call task_done.
If a tool name isn't in the registry, kaid returns the constant
tool.NotAvailableMsg rather than crashing. This makes runtime tool
disabling (via --tools) safe.
Customizing tools¶
Two paths to extend:
1. Disable a tool¶
Copy tools.json, drop the entry you don't want, then run:
For example, if you want a "comment-only" reviewer that never reads
extra context, keep only code_comment and task_done.
2. Re-describe a tool¶
Keep the name (the providers are looked up by name internally) but
change the description to nudge the model. This is the easiest way to
inject project-specific guidance — e.g., "When using file_read,
always read at least 30 lines around the change."
Adding new tool names requires Go-side wiring; see
internal/tool/definitions.goand the providers underinternal/tool/. The JSON file alone can't add new behaviour.
See Also¶
- Architecture — how the agent loop drives tools.
- Review Rules — what the LLM is told to focus on.
- Session Viewer — see exactly which tools fired in past reviews.