Skip to content

dotnet: plumb CancellationToken through ToolInvocation for cooperative cancellation#1704

Open
gimenete wants to merge 3 commits into
github:mainfrom
gimenete:gimenete/gimenete-dotnet-abort-signal-tool-invoca
Open

dotnet: plumb CancellationToken through ToolInvocation for cooperative cancellation#1704
gimenete wants to merge 3 commits into
github:mainfrom
gimenete:gimenete/gimenete-dotnet-abort-signal-tool-invoca

Conversation

@gimenete

Copy link
Copy Markdown

Summary

Implements #1433 for the .NET SDK.

Adds a CancellationToken to ToolInvocation that is cancelled when AbortAsync() or the new CancelToolCall(toolCallId) is called while a tool handler is in flight.

Changes

Types.cs

  • Added CancellationToken CancellationToken { get; set; } to ToolInvocation

Session.cs

  • Added _inFlightToolCalls dictionary (guarded by a lock) to track active CancellationTokenSource instances
  • ExecuteToolAndRespondAsync: creates a CancellationTokenSource per invocation, stores it, passes its token to ToolInvocation.CancellationToken and to tool.InvokeAsync (enabling the direct CancellationToken parameter pattern automatically via AIFunctionFactory)
  • AbortAsync: cancels all in-flight tool handlers before sending the RPC abort
  • Added CancelToolCall(string toolCallId): cancels a single in-flight handler without aborting the agentic loop
  • DisposeAsync: aborts and clears in-flight tool calls before session.destroy

README.md

  • Updated AbortAsync docs to mention cancellation
  • Added new CancelToolCall method entry
  • Added "Cancelling Tool Handlers" section with both patterns (direct parameter and via ToolInvocation)

Tests

  • Unit tests verifying CancellationToken parameter binding and ToolInvocation.CancellationToken exposure

Behavior

Handlers that ignore the token continue to run to completion — fully backwards compatible.

…e cancellation

Add a CancellationToken property to ToolInvocation that is cancelled when
AbortAsync() or CancelToolCall(toolCallId) is called while a tool handler
is in flight.

Key changes:
- ToolInvocation.CancellationToken: populated per-invocation; cancelled on
  abort or targeted cancel
- ExecuteToolAndRespondAsync: creates a CancellationTokenSource per call,
  stores it in _inFlightToolCalls, passes its token to both ToolInvocation
  and tool.InvokeAsync (so direct CancellationToken parameters also work)
- AbortAsync: calls AbortInFlightToolCalls() before the RPC
- CancelToolCall(string): new public method to cancel a single in-flight
  handler without aborting the agentic loop
- DisposeAsync: aborts and clears in-flight tool calls before session.destroy
- README: document both CancellationToken parameter and ToolInvocation patterns,
  with CancelToolCall example
- Unit tests: verify CancellationToken parameter binding and ToolInvocation
  token exposure

Handlers that ignore the token continue to run to completion, preserving
existing behavior.

Fixes github#1433

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 17, 2026 12:30
@gimenete gimenete requested a review from a team as a code owner June 17, 2026 12:30

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.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds cooperative cancellation support for in-flight tool handlers, enabling session-wide and per-tool-call cancellation via CancellationToken.

Changes:

  • Tracks in-flight tool calls with CancellationTokenSource and passes tokens into tool invocations.
  • Adds ToolInvocation.CancellationToken and a new CopilotSession.CancelToolCall(toolCallId) API.
  • Adds unit tests and README documentation describing cancellation usage patterns.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
dotnet/test/Unit/CopilotToolTests.cs Adds unit tests for binding CancellationToken to tool handlers and ToolInvocation.
dotnet/src/Types.cs Extends ToolInvocation with a CancellationToken property and documentation.
dotnet/src/Session.cs Implements in-flight tool call tracking + cooperative cancellation (AbortAsync cancels tools; adds CancelToolCall).
dotnet/README.md Documents new cancellation behavior, including CancelToolCall and handler patterns.
Comments suppressed due to low confidence (1)

dotnet/src/Session.cs:1

  • This introduces new externally observable behavior (AbortAsync now cancels in-flight tool tokens; CancelToolCall returns true/false and signals cancellation). Consider adding unit tests that (1) start a tool handler that waits on the provided token, (2) verify CancelToolCall(toolCallId) returns true and causes the handler to observe cancellation, and (3) verify AbortAsync cancels in-flight tool handlers’ tokens.
/*---------------------------------------------------------------------------------------------

Comment thread dotnet/src/Session.cs
Comment thread dotnet/src/Session.cs
Comment thread dotnet/README.md
gimenete and others added 2 commits June 17, 2026 16:07
- Use requestId (not toolCallId) as the dictionary key in _inFlightToolCalls.
  requestId is unique per RPC request, so toolCallId reuse can never overwrite
  an active entry. CancelToolCall scans by toolCallId (O(n) over the typically
  tiny number of in-flight calls) so the public API is unchanged.

- CancelToolCall: capture the matching CancellationTokenSource under the lock,
  then call Cancel() after releasing it. This avoids CancellationToken callback
  invocations running while the lock is held, preventing potential deadlocks if
  a callback (directly or indirectly) touches session state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ToolInvocation is registered in the source-gen JSON context. The new
CancellationToken property is a runtime-only handle that must never be
serialized over the wire (it exposes a WaitHandle getter that allocates).
Marking it [JsonIgnore] keeps the DTO wire-compatible and keeps the new
field optional/backwards compatible — existing code and serialized payloads
are unaffected.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

2 participants