Skip to content

Cancel completed host tool invocations across SDKs - #2509

Merged
stephentoub merged 12 commits into
mainfrom
stephentoub-external-tool-cancellation
Sep 4, 2026
Merged

Cancel completed host tool invocations across SDKs#2509
stephentoub merged 12 commits into
mainfrom
stephentoub-external-tool-cancellation

Conversation

@stephentoub

Copy link
Copy Markdown
Collaborator

Host-owned external tool callbacks could continue running after the runtime had already completed or drained their request, wasting resources and risking stale responses. This adds request-scoped cancellation across every SDK language while preserving existing SendAndWait wait-only cancellation semantics.

Summary

  • Track each external tool invocation by requestId and pass the language-native cancellation primitive to the host callback.
  • Cancel pending callbacks when external_tool.completed arrives, when sessions or clients terminate, and when the transport closes unexpectedly.
  • Atomically claim requests before responding so completion, teardown, duplicate clients, and late callback results cannot produce stale responses or leak cancellation handles.
  • Preserve normal completion, timeout, and failed-disconnect retry behavior with existing runtimes; abort and suspend cancellation lights up when the runtime emits terminal completion events.
  • Add focused lifecycle, race, retry, force-stop, connection-loss, and late-response coverage across .NET, Node.js, Python, Go, Java, and Rust.

Testing

  • .NET: SDK build for netstandard2.0, net8.0, and net10.0; 57 focused lifetime tests.
  • Node.js: 194 relevant tests, typecheck, and lint.
  • Python: focused cancellation and shutdown tests plus Ruff.
  • Rust: 130 session tests plus format, check, and Clippy.
  • Go and Java changes were statically reviewed because those toolchains were unavailable locally.
  • Repeated Claude Opus 5 full-diff reviews concluded CLEAN with high confidence.

Generated by Copilot

Track request-scoped cancellation across each SDK and terminate pending host callbacks when the runtime completes a request or the session transport shuts down.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 3, 2026 20:05
@stephentoub
stephentoub requested a review from a team as a code owner September 3, 2026 20:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Six moderate cancellation and cleanup defects remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 6 Medium severity

New issues introduced by this change (6)
Severity Finding
Medium severity dotnet/​src/​Client.csJsonRpc.Completion can be faulted by an abnormal transport/protocol failure. In that case this…
Medium severity dotnet/​src/​Session.csCancellationTokenSource.Cancel() runs registered callbacks synchronously. Because…
Medium severity java/​sdk/​src/​main/​java/​com/​github/​copilot/​CopilotSession.java — This drains only entries currently visible in the map and does not prevent a later insertion.…
Medium severity nodejs/​src/​session.ts — After a normal handler result or error, _claimExternalTool has already removed this controller,…
Medium severity rust/​src/​session.rs — This create path can return from the later register_mcp_auth_interest(...).await? before a…
Medium severity rust/​src/​session.rs — The event-loop shutdown path does not cancel this token. In particular,…
What changed in this PR

Adds request-scoped cancellation for host-owned external tool invocations across all six SDKs.

Changes:

  • Cancels pending callbacks on completion, teardown, and connection loss.
  • Atomically claims requests to prevent stale or duplicate responses.
  • Adds lifecycle, race, retry, and cancellation coverage.

Required fixes:

  • dotnet/src/Client.cs (moderate, 1 vote): Observe JsonRpc.Completion failures and continue cancellation cleanup.
  • dotnet/src/Session.cs (moderate, 1 vote): Move synchronous token cancellation off the JSON-RPC read loop.
  • java/.../CopilotSession.java (moderate, 1 vote): Prevent tool registration after force-stop draining begins.
  • nodejs/src/session.ts (moderate, 1 vote): Always abort invocation controllers in both affected finally blocks.
  • rust/src/session.rs:1619 (moderate, 1 vote): Cancel external-tool tasks during event-loop shutdown.
  • rust/src/session.rs:1115 (moderate, 1 vote): Add cleanup protection for create-session registration failures.
File Description
rust/​tests/​session_test.rs Tests cancellation and connection-loss behavior.
rust/​src/​session.rs Tracks and cancels tool futures; has two unresolved cleanup issues.
rust/​src/​jsonrpc.rs Exposes connection-closure cancellation.
python/​test_session.py Tests task cancellation and disconnect behavior.
python/​test_client.py Tests client shutdown cleanup.
python/​copilot/​session.py Tracks and claims external-tool tasks.
python/​copilot/​client.py Cancels session work during teardown.
nodejs/​test/​external-tool-cancellation.test.ts Covers cancellation, retries, duplicates, and late results.
nodejs/​test/​client.test.ts Tests transport-close cancellation.
nodejs/​src/​types.ts Exposes invocation abort signals.
nodejs/​src/​session.ts Manages invocation controllers; final abort remains incomplete.
nodejs/​src/​client.ts Handles connection-close cleanup.
java/​sdk/​src/​test/​java/​com/​github/​copilot/​SessionEventHandlingTest.java Tests Java tool cancellation.
java/​sdk/​src/​test/​java/​com/​github/​copilot/​JsonRpcClientTest.java Tests close-handler delivery.
java/​sdk/​src/​test/​java/​com/​github/​copilot/​CopilotClientTest.java Tests force-stop cancellation.
java/​sdk/​src/​main/​java/​com/​github/​copilot/​JsonRpcClient.java Adds connection-close callbacks.
java/​sdk/​src/​main/​java/​com/​github/​copilot/​CopilotSession.java Tracks tool futures; force-stop has a registration race.
java/​sdk/​src/​main/​java/​com/​github/​copilot/​CopilotClient.java Propagates teardown to sessions.
go/​types.go Documents invocation-context cancellation.
go/​session.go Implements request-scoped contexts and claiming.
go/​session_test.go Tests completion-driven cancellation.
go/​client.go Cancels tools during shutdown and connection loss.
go/​client_test.go Tests client lifecycle cancellation.
dotnet/​test/​Unit/​ClientSessionLifetimeTests.cs Covers cancellation and teardown scenarios.
dotnet/​src/​Session.cs Adds cancellable tool calls; cancellation can block dispatch.
dotnet/​src/​Client.cs Propagates teardown; faulted completion can skip cleanup.
CHANGELOG.md Documents cross-SDK cancellation behavior.
Suppressed comments (1)

nodejs/src/session.ts:2068

  • A concurrent caller returns immediately while the first destroy RPC is still pending. If that RPC later fails, the second caller has already observed a successful disconnect() even though the session remains connected and retryable. Store the in-flight disconnect promise and have concurrent callers await the same operation, clearing it on failure.
        if (this.disconnected || this.disconnecting) {
            return;
        }
        this.disconnecting = true;

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dotnet/src/Client.cs Outdated
Comment thread dotnet/src/Session.cs Outdated
Comment thread java/sdk/src/main/java/com/github/copilot/CopilotSession.java Outdated
Comment thread nodejs/src/session.ts
Comment thread rust/src/session.rs
Comment thread rust/src/session.rs
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread python/test_session.py Fixed
stephentoub and others added 2 commits September 3, 2026 17:22
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread dotnet/src/Session.cs Fixed
Comment thread dotnet/src/Client.cs Fixed
Comment thread dotnet/src/Session.cs Fixed
Use lexical ownership for request cancellation sources and observe JSON-RPC completion faults without a generic catch.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Add new standalone E2E coverage for long-running external tools being cancelled when the session is terminated/disposed, without modifying existing E2E tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread nodejs/test/e2e/external-tool-cancellation.e2e.test.ts Fixed
@github-actions

This comment has been minimized.

Apply language formatters and remove an unused Vitest import from the cross-SDK E2E coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@SteveSandersonMS SteveSandersonMS left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the solid cross-SDK fix and for keeping behavior aligned across runtimes.

I added follow-up standalone E2E tests in commit b6ac7f4 to cover the concrete cancellation scenario (long-running external tool + session termination) across all six SDKs:

  • .NET: dispose path
  • Node/Python/Go/Rust/Java: disconnect/close path

Locally, the new targeted tests pass in Node, Python, Go, Rust, and .NET. Java test execution is blocked in this environment by Maven TLS trust-chain resolution of org.sonatype.central:central-publishing-maven-plugin, so we should rely on CI for the Java lane signal.

Given this, I recommend we proceed to merge once CI is green.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Comment thread dotnet/src/Client.cs Fixed
Avoid swallowing fatal runtime exceptions while retaining best-effort cleanup after connection startup failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

@stephentoub
stephentoub added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 4, 2026
Preserve request-scoped external tool cancellation alongside PreparedSession registration ownership and the latest Python client tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review — PR #2509

Reviewed the authoritative diff (get_files + get_diff) across all six SDKs for the "cancellation for host-owned external tools" feature.

Result: Consistent. This PR implements the feature in parallel across all six SDKs with idiomatic-per-language cancellation primitives, matching the CHANGELOG description precisely:

SDK Cancellation primitive Notes
.NET CancellationToken threaded through AIFunction.InvokeAsync / HandlePendingToolCallAsync Cancelled on ForceStopAsync, connection close, and session DisposeAsync
Node.js ToolInvocation.signal (AbortSignal), new optional field in types.ts Cancelled on session._markDisconnected() and connection close
Go ToolInvocation.TraceContext cancellation Doc comment updated to warn that background work must derive its own lifetime — good catch, avoids a footgun
Java Cancels the CompletableFuture returned by the tool handler New PendingExternalTool state machine handles the start/cancel race cleanly
Python Cancels the asyncio.Task running the handler Handles cross-thread cancellation via call_soon_threadsafe from the connection-close path
Rust Races the handler future against a CancellationToken via tokio::select! (drops the future on cancel) PendingExternalToolGuard cleanly claims/removes pending entries

All six implementations:

  • Cancel pending external tool handlers on ExternalToolCompletedEvent/broadcast equivalent (server said "done, no need to keep working")
  • Cancel all pending tools on session disconnect/dispose and on client force-stop/connection-close
  • Include unit tests and matching E2E snapshot-based tests (test/snapshots/external_tool_cancellation/*.yaml used by dotnet, with each SDK's own E2E test hitting the same scenarios)
  • Guard against double-completion races (claim/remove-once patterns) consistently

No cross-SDK gaps or naming inconsistencies found. This is a good example of maintaining feature parity across the six language implementations.

Generated by SDK Consistency Review Agent for #2509 · copilot · sonnet50 · 38.1 AIC · ⌖ 12 AIC · ⊞ 9.7K ·

@stephentoub
stephentoub added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@stephentoub
stephentoub added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@stephentoub
stephentoub added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@stephentoub
stephentoub added this pull request to the merge queue Sep 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 4, 2026
@stephentoub
stephentoub added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 53e74d8 Sep 4, 2026
163 of 165 checks passed
@stephentoub
stephentoub deleted the stephentoub-external-tool-cancellation branch September 4, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants