The abort story currently stops at the tool boundary. Ctrl+C aborts the LLM stream and the loop, but a running bash command (npm install, a test suite), a sub-agent, or a large directory walk ignores it — the agent waits for the tool to finish, so ^C appears broken exactly when users need it most. This PR makes cancellation flow from the agent, through every tool, down to the child process.
Changes
Tool.execute(args, signal?) — signal plumbing (src/tools/base.ts, src/agent.ts)
The Tool interface now accepts an optional AbortSignal; execTool passes the agent's signal into every tool.
execTool rethrows AbortError instead of stringifying it into a tool result. Previously a tool-level abort would have been fed back to the model as Error executing bash: AbortError and the turn would limp on.
After tool results are pushed, an abort that fired during execution ends the turn immediately (no wasted LLM round); pending replies are backfilled with [interrupted] as before.
bash — actually kill the command (src/tools/bash.ts)
Replaced exec with spawn so we own the process. shell: true makes the real command a grandchild of the shell; a plain child.kill() kills only the shell, and the command keeps running — and keeps holding the stdout pipe, so even exec's callback waits for it.
POSIX: detached: true makes the shell a process-group leader; kill(-pid) kills the whole group.
Windows: taskkill /pid <pid> /T /F walks the process tree (verified: a 10s node -e command now aborts in ~450ms instead of 10s).
Kept the 16MB output cap (now explicit, with a [output truncated] marker), and the timeout now kills the tree too.
agent (sub-agent tool) — propagate the signal (src/tools/agent.ts)
The parent's signal is passed into sub.chat(); an abort during the sub-agent's run propagates as AbortError (re-thrown, not stringified), so ^C cancels a runaway sub-agent too.
glob / grep — prompt walk cancellation
Both walks (up to 20k/5k entries) check the signal per iteration and on entry, so large-tree searches abort immediately instead of running to completion.
Tests (tests/abort.test.ts, 4 new)
bash kills a running child on abort (<5s bound; measured ~450ms)
agent aborts a running tool and backfills [interrupted]
abort propagates through the sub-agent tool into the sub-agent's LLM call
pre-aborted signals make glob and grep reject immediately
All 28 tests pass (npm test); demo still runs end-to-end.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this is core
The abort story currently stops at the tool boundary. Ctrl+C aborts the LLM stream and the loop, but a running bash command (npm install, a test suite), a sub-agent, or a large directory walk ignores it — the agent waits for the tool to finish, so ^C appears broken exactly when users need it most. This PR makes cancellation flow from the agent, through every tool, down to the child process.
Changes
Tool.execute(args, signal?) — signal plumbing (src/tools/base.ts, src/agent.ts)
bash — actually kill the command (src/tools/bash.ts)
agent (sub-agent tool) — propagate the signal (src/tools/agent.ts)
glob / grep — prompt walk cancellation
Tests (tests/abort.test.ts, 4 new)
All 28 tests pass (npm test); demo still runs end-to-end.