feat(nodejs): plumb AbortSignal through ToolInvocation (#1433)#1701
feat(nodejs): plumb AbortSignal through ToolInvocation (#1433)#1701gimenete wants to merge 2 commits into
Conversation
Add a cooperative cancellation signal to tool handlers so session.abort() (and a new session.cancelToolCall(toolCallId)) can cancel in-flight tool handlers. Handlers opt in via the AbortSignal on their ToolInvocation; handlers that ignore it run to completion, preserving existing behavior. Closes github#1433 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Add cooperative cancellation for Node.js tool handlers by wiring an AbortSignal into tool invocations, aborting in-flight handlers on session abort/disconnect, and documenting/testing the behavior.
Changes:
- Pass an
AbortSignalto tool handlers and abort it onsession.abort()/disconnect(). - Track in-flight tool calls and add
cancelToolCall(toolCallId)to abort a single handler. - Extend docs and add an e2e assertion that tool-handler signals are aborted.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| nodejs/test/e2e/abort.e2e.test.ts | Adds an e2e check that tool handler AbortSignal is triggered by session.abort() |
| nodejs/src/types.ts | Extends ToolInvocation with a signal: AbortSignal and documentation |
| nodejs/src/session.ts | Tracks in-flight tool calls via AbortController, aborts them on abort()/disconnect(), adds cancelToolCall() |
| nodejs/README.md | Documents cooperative tool cancellation and the new cancelToolCall API |
| let signalAbortedResolve!: (value: void) => void; | ||
| const signalAborted = new Promise<void>((resolve) => { | ||
| signalAbortedResolve = resolve; | ||
| }); |
| handler: async ({ value }) => { | ||
| handler: async ({ value }, { signal }) => { | ||
| toolStartedResolve(value); | ||
| signal.addEventListener("abort", () => signalAbortedResolve()); |
| handler: async ({ value }) => { | ||
| handler: async ({ value }, { signal }) => { | ||
| toolStartedResolve(value); | ||
| signal.addEventListener("abort", () => signalAbortedResolve()); |
| toolCallId: string; | ||
| toolName: string; | ||
| arguments: unknown; | ||
| /** | ||
| * An `AbortSignal` that aborts when `session.abort()` or | ||
| * `session.cancelToolCall(toolCallId)` is invoked while this handler is | ||
| * in flight. Handlers may opt in to cooperative cancellation by forwarding | ||
| * it to abortable APIs (`fetch(url, { signal })`, `child_process.spawn`, | ||
| * etc.) or by checking `signal.aborted`. Handlers that ignore it continue | ||
| * to run to completion, preserving existing behavior. | ||
| */ | ||
| signal: AbortSignal; |
There was a problem hiding this comment.
Keeping signal required on purpose for cross-SDK consistency — the same field is non-optional in the Python, Go, .NET, Rust, and Java equivalents, and the SDK always injects a live signal at runtime, so handlers never have to null-check it. The only friction is for consumers who hand-construct/mock ToolInvocation in tests, which is a one-line addition (signal: new AbortController().signal). Given it's a new field on a handler-input type rather than a return type, the runtime guarantee felt worth the minor upgrade cost.
| cancelToolCall(toolCallId: string): boolean { | ||
| const controller = this.inFlightToolCalls.get(toolCallId); | ||
| if (!controller) { | ||
| return false; | ||
| } | ||
| controller.abort(); | ||
| return true; | ||
| } |
- Fix signalAbortedResolve type to () => void in abort e2e test
- Handle already-aborted signal and use { once: true } listener
- cancelToolCall now removes the in-flight entry so repeat/completed
ids return false (consistent with other SDKs)
- Add e2e test covering cancelToolCall (true for in-flight, false for
unknown and already-cancelled ids) with recorded snapshot
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
Implements the feature requested in #1433 for the Node.js SDK: plumb an
AbortSignalthroughToolInvocationso thatsession.abort()(and a newsession.cancelToolCall(toolCallId)) can cooperatively cancel in-flight tool handlers.Previously
session.abort()only cancelled the agentic loop — a currently-executing tool handler ran to completion regardless, forcing consumers to implement OS-level process-tree kills.Changes
ToolInvocationnow carries a requiredsignal: AbortSignalthat aborts whensession.abort()orsession.cancelToolCall(toolCallId)is invoked while the handler is in flight.session.abort()aborts the signals of all in-flight tool handlers before sending the abort RPC.session.cancelToolCall(toolCallId)(new) cancels a single in-flight handler without aborting the broader agentic loop; returnstrueif a matching call was found.session.disconnect()aborts in-flight handlers so they can release resources.Map<toolCallId, AbortController>.Backwards compatibility
Handlers that ignore the signal continue to run to completion — existing handlers work unchanged.
Tests
Extended the
aborte2e test to assert the handler'sAbortSignalfires whensession.abort()is called. Typecheck, lint, format check, and unit tests all pass.Closes #1433
Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com