Skip to content

java: plumb AbortSignal through ToolInvocation for cooperative cancellation#1707

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

java: plumb AbortSignal through ToolInvocation for cooperative cancellation#1707
gimenete wants to merge 3 commits into
github:mainfrom
gimenete:gimenete/java-abort-signal-tool-invocation

Conversation

@gimenete

@gimenete gimenete commented Jun 17, 2026

Copy link
Copy Markdown

Summary

Fixes #1433 for the Java SDK.

Adds AbortSignal to ToolInvocation so tool handlers can observe when session.abort() is called and stop in-flight work cooperatively — without needing OS-level process kills. Also adds cancelToolCall(toolCallId) for single-handler cancellation, mirroring the Node.js reference (PR #1701).

Changes

New: AbortSignal class (com.github.copilot.rpc)

public final class AbortSignal {
    public boolean isAborted()               // poll the cancelled state
    public void onAborted(Runnable listener) // register a cancel callback
    public void abort()                      // SDK-internal: fires the signal
}

abort() is idempotent — the signal fires exactly once. Callbacks registered after the signal has already fired are invoked immediately.

Updated: ToolInvocation.getAbortSignal()

ToolHandler handler = invocation -> {
    AbortSignal signal = invocation.getAbortSignal();
    return CompletableFuture.supplyAsync(() -> {
        while (!signal.isAborted()) {
            // do incremental work here
        }
        throw new CancellationException("Tool aborted");
    });
};

Or register a callback:

signal.onAborted(() -> myHttpClient.cancel());

Updated: CopilotSession.abort()

When session.abort() is called, the SDK now:

  1. Fires all AbortSignal instances for currently in-flight tool invocations
  2. Then sends the existing session.abort RPC as before

New: CopilotSession.cancelToolCall(String toolCallId)

Fires the AbortSignal for only the named in-flight handler — without aborting the agentic loop or other running handlers.

boolean wasCancelled = session.cancelToolCall(toolCallId);
// true  → handler found and signal fired
// false → no in-flight handler with that ID (already completed or unknown)

The activeToolSignals tracking map is keyed by toolCallId (falls back to requestId when toolCallId is null) so cancelToolCall can do a direct map lookup.

New: Java README "Tool Handler Cancellation" section

Documents both cancellation paths with handler examples.

Tests

  • ToolInvocationTest: 6 new unit tests for AbortSignal behaviours (idempotent abort, immediate callback when already aborted, null listener rejection, etc.)
  • CancelToolCallTest: 3 new unit tests — targeted cancel fires only signal A while signal B stays unaffected; returns false for unknown id; cancelled signal is removed from the map

Confirmed not in PR

  • java/mvnw — only modified locally (chmod), not committed

…lation

Add AbortSignal to ToolInvocation so tool handlers can observe when
session.abort() is called and stop in-flight work cooperatively.

- New AbortSignal class in com.github.copilot.rpc with isAborted(),
  onAborted(Runnable), and abort() (SDK-internal) methods
- ToolInvocation.getAbortSignal() returns the signal; initialized to a
  fresh (non-aborted) instance by default
- CopilotSession tracks active signals per requestId; injects the signal
  into each ToolInvocation before calling the handler
- CopilotSession.abort() now fires all tracked active tool signals before
  sending the RPC abort request, enabling cooperative cancellation
- Signals are removed from the tracking map when a tool invocation
  completes (success or error) to avoid leaks
- Unit tests for all AbortSignal behaviours added to ToolInvocationTest

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:52
@gimenete gimenete requested a review from a team as a code owner June 17, 2026 12:52

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.

This PR introduces cooperative cancellation for tool executions by adding an AbortSignal that propagates from CopilotSession.abort() into each ToolInvocation.

Changes:

  • Added AbortSignal type with isAborted(), listener registration, and idempotent abort().
  • Wired per-tool AbortSignal creation/management into CopilotSession and exposed it via ToolInvocation#getAbortSignal().
  • Added unit tests and package documentation for the new cancellation signal.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
java/src/main/java/com/github/copilot/rpc/AbortSignal.java New cancellation signal implementation with listener support.
java/src/main/java/com/github/copilot/rpc/ToolInvocation.java Adds AbortSignal to tool invocation API + docs.
java/src/main/java/com/github/copilot/CopilotSession.java Tracks active tool signals and aborts them on session abort.
java/src/main/java/com/github/copilot/rpc/package-info.java Documents AbortSignal in RPC package overview.
java/src/test/java/com/github/copilot/ToolInvocationTest.java Adds tests covering AbortSignal and default signal behavior.

Comment thread java/src/main/java/com/github/copilot/rpc/AbortSignal.java
Comment thread java/src/main/java/com/github/copilot/rpc/AbortSignal.java
Comment thread java/src/main/java/com/github/copilot/rpc/ToolInvocation.java
Comment thread java/src/main/java/com/github/copilot/rpc/ToolInvocation.java
Comment thread java/src/main/java/com/github/copilot/rpc/ToolInvocation.java
Comment thread java/src/main/java/com/github/copilot/rpc/ToolInvocation.java
Comment thread java/src/main/java/com/github/copilot/rpc/AbortSignal.java Outdated
Comment thread java/src/main/java/com/github/copilot/rpc/AbortSignal.java Outdated
gimenete and others added 2 commits June 17, 2026 15:08
- CopilotSession.cancelToolCall(toolCallId): fires the AbortSignal for
  only the named in-flight handler without aborting the agentic loop or
  other handlers; returns true if found and cancelled, false otherwise
- activeToolSignals map is now keyed by toolCallId (falls back to
  requestId when toolCallId is null) so cancelToolCall can look up
  directly by the ID exposed on ToolInvocation
- CancelToolCallTest: 3 unit tests covering targeted cancel, unknown id
  returns false, and signal removal from the tracking map
- java/README.md: new 'Tool Handler Cancellation' section documenting
  both abort() (all handlers) and cancelToolCall() (single handler)
  with isAborted()/onAborted() handler examples

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix at-most-once callback delivery: wrap each onAborted() listener in
  an AtomicBoolean-guarded Runnable so it cannot fire twice even when
  abort() races with onAborted() registration
- Catch Throwable (not just Exception) in listener invocations to align
  with the 'silently ignored' Javadoc contract
- Add @JsonIgnore to setAbortSignal() so it is excluded from Jackson
  serialization/deserialization, matching the getter annotation
- setAbortSignal(null) now silently preserves the existing signal for
  backwards compatibility, rather than throwing NullPointerException
- Add tests: at-most-once callback guarantee; null setAbortSignal is
  ignored and leaves the existing signal in place

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.

Feature request: Plumb AbortSignal through ToolInvocation so session.abort() can cancel in-flight tool handlers

2 participants