Skip to content

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_task prompt explicitly forbids commenting on findings in other files. file_read, file_read_diff, file_find, and code_search exist 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.

{
  "name": "task_done",
  "input": { "state": "DONE" }
}
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).

thinking is a runtime-only field. kaid parses and stores it, but it is deliberately not listed in the code_comment schema advertised to the model in tools.json (only content, existing_code, and suggestion_code are). Stronger models that emit a thinking block 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:

  1. 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.
  2. Full new-file scan — if no hunk matched, kaid scans the entire post-change file content line-by-line for consecutive matches (resolveFromFileContent).
  3. Re-location task — if text matching still fails on a non-trivial diff, kaid runs the RE_LOCATION_TASK prompt 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

{
  "name": "file_read",
  "input": {
    "file_path": "src/foo.go",
    "start_line": 10,
    "end_line": 80
  }
}
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: true is set, and a trailing Note: 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

{
  "name": "file_find",
  "input": {
    "query_name": "UserService",
    "case_sensitive": false
  }
}
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.

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 with Note: The results have been truncated. Only showing first 100 results..
  • Empty / whitespace-only search_text returns Error: search_text is blank instead of expanding to every line.
  • Searches the current working tree in workspace mode, or the resolved target ref in range / commit mode (the FileReader.Ref is passed as a positional argument to git grep).

Tool execution & errors

Tools execute synchronously inside the agent loop, with two exceptions:

  • code_comment is dispatched to the CommentWorkerPool so the loop doesn't block on line-resolution + reflection.
  • task_done short-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:

kaid review --tools ./my-tools.json

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.go and the providers under internal/tool/. The JSON file alone can't add new behaviour.

See Also