Add batch reads, @file completion, interruptible commands, tool sets
Tools, six built-in to fourteen: - read_many_files: up to 20 paths read concurrently, each with its own window. An unreadable path is reported in its own block instead of throwing. - multi_edit: several edits to one file, validated in memory first so a late failure cannot leave the file half-written. - list_dir: ignore-aware depth-limited tree. - git_status/diff/log/show/blame: read-only, spawned with a fixed argv rather than a shell string, which is what makes them safe to auto-approve. toolSets gates them. core is always on; edit-plus and git are optional. A disabled set reaches neither the wire nor the system prompt, since a prompt naming an absent tool teaches calls that cannot succeed. Interface: - Reasoning streams to a collapsed panel, ctrl-r expands, dropped when the turn ends: it is progress, not the answer. - The tool in flight is named from tool-input-start, before its arguments finish streaming, and cleared on its result. - Prompts typed mid-turn queue and drain in order. esc clears the queue as well as aborting. - @ opens a path picker fed by the ignore-aware walker. Prefix matches rank above substring matches, so @src/ means "under src/". The walk runs on the first @, not at startup. ctrl-c kills the command in flight and keeps the turn. The call throws rather than returning, so the model cannot read a killed command as one that ran and failed on its own terms. The kill takes the whole process tree: killing cmd /c alone left the real command holding both pipes open, so the read never returned and the interrupt did nothing for 19 seconds. Two pruning fixes: - A tool result whose tool call was pruned is now dropped with it. Pruning counts messages, so the cut landed between an assistant tool-call and the tool message answering it, producing 400 "No tool call found for function call output with call_id ...". The reverse pairing is left alone: a call awaiting its result is what a suspended approval looks like. - ignore.ts called statFs without importing it, so walk() crashed on the first symlink. 482 tests, up from 404. Docs synced across README, ROADMAP, TODO, and all of docs/: tool sets, the new tools, ctrl-c semantics, the tool-start event, and the two hand-maintained tool-name lists recorded as a known weakness.
This commit is contained in:
+112
-6
@@ -4,14 +4,15 @@
|
||||
|
||||
Three categories.
|
||||
|
||||
**Free.** Read-only, no prompt: `read_file`, `glob`, `grep`, `task`.
|
||||
**Free.** Read-only, no prompt: `read_file`, `read_many_files`, `glob`, `grep`, `list_dir`,
|
||||
`task`, and the whole git set.
|
||||
|
||||
**Session tools.** Also free, because they touch the agent's own state rather than your
|
||||
files: `todo_write`, `remember`, `recall`, `forget`, `skill`, `ask`, and anything a plugin
|
||||
marks auto-approved.
|
||||
|
||||
**Gated.** Every call stops for a decision: `write_file`, `edit_file`, `bash`, and every
|
||||
`mcp__*` tool.
|
||||
**Gated.** Every call stops for a decision: `write_file`, `edit_file`, `multi_edit`, `bash`,
|
||||
and every `mcp__*` tool.
|
||||
|
||||
```
|
||||
edit_file wants to run
|
||||
@@ -29,6 +30,28 @@ to ask what to do instead. `--yolo` skips all prompts.
|
||||
**The guard runs before all of this.** It is not an approval — it is a refusal, and `--yolo`
|
||||
does not reach it. See [plugins](plugins.md).
|
||||
|
||||
## Tool sets
|
||||
|
||||
Each tool costs roughly 550 characters of JSON schema on every request, and selection
|
||||
accuracy drops as the list grows. Sets let you switch off what a project does not need:
|
||||
|
||||
| Set | Tools |
|
||||
|---|---|
|
||||
| `core` | `read_file` `write_file` `edit_file` `glob` `grep` `bash` |
|
||||
| `edit-plus` | `multi_edit` `list_dir` `read_many_files` |
|
||||
| `git` | `git_status` `git_diff` `git_log` `git_show` `git_blame` |
|
||||
|
||||
```json
|
||||
{ "toolSets": ["edit-plus"] }
|
||||
```
|
||||
|
||||
Omit `toolSets` for all of them. `core` is always on — without read, edit, and bash the
|
||||
agent is not an agent. A disabled set reaches neither the wire nor the system prompt, since
|
||||
a prompt that names an absent tool teaches the model to attempt calls that cannot succeed.
|
||||
Session, plugin, and MCP tools are not part of this budget and are never gated here.
|
||||
|
||||
`/tools` shows which set each live tool came from.
|
||||
|
||||
## File tools
|
||||
|
||||
### `read_file`
|
||||
@@ -43,6 +66,27 @@ Returns contents with 1-based line numbers. Refuses binaries: a NUL byte in the
|
||||
means the file is not text, and a model that reads a 90 MB executable has burned its whole
|
||||
context on nothing.
|
||||
|
||||
### `read_many_files`
|
||||
|
||||
```
|
||||
files [{ path, offset?, limit? }], at most 20
|
||||
```
|
||||
|
||||
One round trip for several files, each with its own window. Reads run concurrently and the
|
||||
blocks come back in the order given, labelled:
|
||||
|
||||
```
|
||||
===== src/app.ts =====
|
||||
1: export const port = 8080;
|
||||
|
||||
===== src/gone.ts =====
|
||||
[unreadable: No such file: src/gone.ts]
|
||||
```
|
||||
|
||||
A path that cannot be read is reported in its own block rather than throwing, so one wrong
|
||||
guess costs a line instead of the whole call. Numbering and binary refusal are the same code
|
||||
path as `read_file`, so a batch read cannot drift from a single one.
|
||||
|
||||
### `write_file`
|
||||
|
||||
```
|
||||
@@ -65,6 +109,32 @@ replaceAll replace every occurrence instead of requiring exactly one
|
||||
An ambiguous match is an error naming the count, which pushes the model to add surrounding
|
||||
context rather than guessing which occurrence it meant.
|
||||
|
||||
### `multi_edit`
|
||||
|
||||
```
|
||||
path file path
|
||||
edits [{ oldString, newString, replaceAll? }], in the order to apply them
|
||||
```
|
||||
|
||||
Several edits to one file in one call, one approval, one write. Each edit sees the result of
|
||||
the previous one, so edits may build on each other.
|
||||
|
||||
Atomic: every edit is validated and applied in memory first, so a failure on the third edit
|
||||
leaves the file exactly as it was rather than half-changed. The same uniqueness rule as
|
||||
`edit_file` applies per edit, and the error names which edit failed.
|
||||
|
||||
### `list_dir`
|
||||
|
||||
```
|
||||
path directory, relative to the workspace root, default the root
|
||||
depth levels to descend, 1-6, default 2
|
||||
includeIgnored also show files git ignores
|
||||
```
|
||||
|
||||
Tree view honouring `.gitignore`. Directories end with `/`, files show their size. Past the
|
||||
depth limit the containing directory is still listed, so the shape of the tree stays visible
|
||||
without its contents. Capped at 300 entries.
|
||||
|
||||
### `glob`
|
||||
|
||||
```
|
||||
@@ -75,7 +145,9 @@ includeIgnored also return files git ignores
|
||||
|
||||
Walks the tree honouring `.gitignore` and `.shiroignore`, skipping `.git` and
|
||||
`node_modules` unconditionally. Nested ignore files apply only within their own directory,
|
||||
as git does. Returns posix paths relative to the workspace root.
|
||||
as git does. Returns posix paths relative to the workspace root. A symlinked directory is
|
||||
classified as a directory and not descended into, since it can point anywhere including
|
||||
back into the tree.
|
||||
|
||||
### `grep`
|
||||
|
||||
@@ -104,6 +176,39 @@ command that fills one while you block on the other deadlocks.
|
||||
|
||||
Returns exit code, stdout, stderr, and a note if a signal killed it.
|
||||
|
||||
**`ctrl-c` interrupts the command, not the turn.** The shell and everything it started are
|
||||
killed — on Windows through `taskkill /T`, because killing `cmd` alone leaves the real command
|
||||
holding both pipes open and the read never ends. The call then fails rather than returning,
|
||||
so the model cannot mistake a killed command for one that ran and failed on its own:
|
||||
|
||||
```
|
||||
The user interrupted this command. It did not finish, so its effects are unknown.
|
||||
stdout:
|
||||
[whatever it printed first]
|
||||
```
|
||||
|
||||
The turn continues from there. `esc` still aborts everything, and `ctrl-c` with nothing
|
||||
running quits as usual.
|
||||
|
||||
## Git tools
|
||||
|
||||
All five are read-only and therefore approval-free. Each spawns `git` with a fixed argument
|
||||
array rather than a shell string, so an argument like `--author="; rm -rf /"` can only ever
|
||||
be a literal argument — which is what makes auto-approval safe.
|
||||
|
||||
Output is described rather than raw porcelain: `git_status` names the branch and says
|
||||
`staged modified` or `untracked` per file instead of leaving the model to decode two columns
|
||||
of flags. Outside a repository they fail with `<cwd> is not a git repository.` rather than
|
||||
passing git's own error text through.
|
||||
|
||||
```
|
||||
git_status branch, staged, modified, untracked
|
||||
git_diff staged? path? unified diff of uncommitted changes
|
||||
git_log limit? path? hash, date, author, subject; newest first
|
||||
git_show ref path? one commit: message, author, diff
|
||||
git_blame path startLine? endLine? who last changed each line
|
||||
```
|
||||
|
||||
## Agent tools
|
||||
|
||||
### `task`
|
||||
@@ -168,5 +273,6 @@ every call rather than being assumed.
|
||||
## Output caps
|
||||
|
||||
Any single tool result is truncated at 30,000 characters with a note saying how much was
|
||||
cut. `grep` stops at 200 hits, `glob` at 200 paths, `read_file` at 2000 lines by default.
|
||||
Without caps one `grep` for `function` can end a session.
|
||||
cut. `grep` stops at 200 hits, `glob` at 200 paths, `list_dir` at 300 entries,
|
||||
`read_many_files` at 20 files, `read_file` at 2000 lines by default. Without caps one `grep`
|
||||
for `function` can end a session.
|
||||
|
||||
Reference in New Issue
Block a user