Repository navigation
LOU-V1: cancel a run with an AbortSignal - #42
Merged
Merged
Conversation
What: `signal?: AbortSignal` on AgentExecutor.execute(), createAgent().send(input, { signal })
and resumeAfterApproval() (via its executeOptions). The loop checks the signal before every
model call and every tool call. An aborted run resolves (does not reject) with
finishReason 'aborted' and the transcript/steps so far, emits a typed `abort` event (with
`abortReason`) followed by `finish`, and is checkpointed when sessionId + checkpointStore are
set, so a later execute() with the same sessionId resumes it. Tool calls the run never reached
get a cancelled {error} result so the transcript stays valid for the provider. An
already-aborted signal returns without calling the provider.
The signal is forwarded to the provider (new GenerateOptions.signal, passed to the ai SDK as
`abortSignal`; the mock provider rejects promptly, including mid-delay), to tools as
`execute(args, { abortSignal })` (and as a third `{ abortSignal }` argument to
sandboxExecute), to fetch in the built-in http tool, and to delegated child agents.
A rejection caused by the abort is the abort, not a failure: it is not compacted or surfaced
to the model for another attempt, and retry() never retries an AbortError (new
RetryOptions.signal also stops retries).
Adds an exported ExecutionFinishReason type (known reasons incl. 'awaiting-approval' and
'aborted', still open to any string) used by ExecutionResult and ExecutionEvent.
Why: runs could not be cancelled, which blocks responsive UIs and timeouts.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts: # src/execution/AgentExecutor.ts
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
What
Runs can now be cancelled with an
AbortSignal:ExecuteOptions.signal,SendOptions.signal(new exported type forcreateAgent().send), andresumeAfterApproval'sexecuteOptions.signal.finishReason: 'aborted'and the transcript/steps so far. It emits a typedabortevent (abortReason=signal.reason) and thenfinish. WithsessionId+checkpointStoreit is checkpointed, and a laterexecute()with the samesessionIdresumes it. Tool calls the run never reached get a cancelled{error}tool result, so the transcript stays valid for the provider on resume. An already-aborted signal returns without calling the provider.GenerateOptions.signal, sent asabortSignalto theaiSDK in the singlebuildCallSettingsplace. The mock provider rejects promptly, including mid-delay and instream().execute(args, { abortSignal }), plus an optional third{ abortSignal }argument tosandboxExecute.httptool: passes the signal tofetchand rejects with anAbortError.createDelegateToolchildren: inherit the parent's signal.error.retry()never retries anAbortError, and the newRetryOptions.signalstops retries.ExecutionFinishReasontype ('stop' | 'length' | 'tool_calls' | 'content_filter' | 'error' | 'awaiting-approval' | 'aborted' | (string & {})) is used forExecutionResult.finishReasonandExecutionEvent.finishReason. It stays open to any string, so existing code compiles.docs/api-overview.md(anAbortControllerexample and anAbortSignal.timeoutexample) and a README feature bullet.Why
The audit found that a run could not be cancelled. Without that, a responsive UI (a Stop button) or a hard time limit is impossible.
How verified
npx tsc --noEmit: exit 0npm run lint:✖ 436 problems (0 errors, 436 warnings). No warnings in the new code; one{} as anywas removed fromsandboxGuard.ts.npm run build && npm run build --workspace=packages/create-loushy-agent: both exit 0npm run test:coverage:Test Files 89 passed (89),All files | 83.83 | 87.17 | 86.2 | 83.83, thresholds metnpm run fallow: exit 0.✓ No issues found,✓ No code duplication found,✓ 0 above threshold · maintainability 92.4 (good)npm run docs:verify-snippets:all 5 snippet(s) type-check and run cleanly. The new Cancellation snippets are not in the script's default doc set, so I ran them separately withnpx tsx scripts/verify-docs-snippets.ts <extracted-section>.md:all 2 snippet(s) type-check and run cleanly.npm run typecheckandnpm run typecheck:server --workspace apps/agent-forge: both clean. No agent-forge files were changed. It has no exhaustive switch over finish reasons or event types: its log-builder map is partial, and thefinishevent already logsRun finished (aborted).The new tests are in
src/execution/cancellation.test.ts(mock/scripted providers, no network): abort before start; during a model call (scripted provider plus mock provider mid-delay); during a tool call (the tool observesabortSignal.aborted); between steps; between two tool calls in one turn; checkpoint written and resumed with the samesessionId; no compaction/surfacing/error event on an abort-caused rejection; a genuine failure still rejects; delegated child aborted with its parent;retry()withAbortErrorand withsignal;createAgent().send(..., { signal }); mock provider;resumeAfterApprovalwithsignal. There is also anabortSignalpass-through test inaiSdkProvider.test.tsand an http-tool abort test inhttp.test.ts.Notes for reviewer
aiSdkProvider.tsare one line inbuildCallSettings. IntoolCallExecution.tsthey are a context field plus passingsignalthroughdoExecuteToolCall; the thrown-tool-error catch block is untouched.abortSignalruns to completion; its result is recorded, and the run then ends asabortedbefore the next call.stop). The signal is only checked before model and tool calls, as specified.sandboxExecutegets the third{ abortSignal }argument only when a signal is set, so existingtoHaveBeenCalledWith(args, sandbox)expectations still hold. The sandboxed HTTP transport (sandboxHttpFetch) does not take a signal yet; the http tool's directexecute()path passes it tofetch.'aborted'to the provider-levelGenerateResult['finishReason']union, because providers reject on abort rather than returning a result. It is in the run-levelExecutionFinishReasoninstead.🤖 Generated with Claude Code