fix(acp): return stopReason: cancelled (not end_turn) on user cancel#25977
Open
truenorth-lj wants to merge 1 commit into
Open
fix(acp): return stopReason: cancelled (not end_turn) on user cancel#25977truenorth-lj wants to merge 1 commit into
truenorth-lj wants to merge 1 commit into
Conversation
Closes anomalyco#25899 `Agent.prompt()` resolves the ACP RPC with `stopReason: "end_turn"` whether the turn finished naturally or was interrupted by `session/cancel`. ACP defines a dedicated `stopReason: "cancelled"` for exactly that case (`zStopReason` in the SDK schema), so reporting `end_turn` makes a user cancel look like a clean completion to clients. The same paths also returned `usage: { totalTokens: 0, inputTokens: 0, outputTokens: 0 }` on cancel. The LLM stream is interrupted before the AI SDK emits its `finish-step` event, so `assistantMessage.tokens` stays at the zero initialiser from `session/prompt.ts`. Reporting `0` is wrong (Anthropic / Bedrock have already consumed the prompt tokens — we just don't know the count); omit `usage` so clients show "unknown" instead of "zero work". Detection signal: when the runner is interrupted, the `Effect.onInterrupt` in `session/processor.ts` runs `halt(new DOMException("Aborted", "AbortError"))`, which `MessageV2.fromError` maps to a `MessageAbortedError` written to `assistantMessage.error`. The runner's onInterrupt callback then returns the in-flight assistant message, so `sdk.session.prompt()` resolves with `info.error.name === "MessageAbortedError"`. That's the wire-stable tag we check. Applied to both prompt-RPC return sites in `Agent.prompt(...)` (the default path and the slash-command path); the third path that handles top-level commands like `/compact` is left untouched because it synthesises its own response without a message. Test plan: - `bun test test/acp/event-subscription.test.ts` — 12 pass / 0 fail (was 10) - new: prompt() returns stopReason: cancelled with no usage when message has MessageAbortedError - new: prompt() returns stopReason: end_turn with usage on normal completion - `bun run typecheck` — clean - staging E2E (real sandbox + cancel mid-bash): pre-fix observed `stopReason: "end_turn", usage.totalTokens: 0`; post-fix expected to observe `stopReason: "cancelled"` with no `usage` field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
@rekram1-node — would you have a moment when you're next in the ACP queue? Same area as the This one returns Happy to add a regression test or reshape if anything's off. |
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.
Issue for this PR
Closes #25899
Type of change
What does this PR do?
Returns
stopReason: "cancelled"instead of"end_turn"on the ACPsession/promptRPC when the turn was interrupted bysession/cancel, and omits the misleading all-zerousagefield on cancel.Agent.prompt()currently resolves the ACP RPC withstopReason: "end_turn"whether the turn finished naturally or was interrupted. ACP defines a dedicatedstopReason: "cancelled"for the cancel case, so reportingend_turnmakes a user cancel indistinguishable from a clean completion.Cancelled turns can also return
usage: { totalTokens: 0, inputTokens: 0, outputTokens: 0 }. The LLM stream is killed before the AI SDK emits itsfinish-stepusage event, so the assistant message tokens are still at their zero initializer. Reporting zero is misleading; omittingusagelets clients treat it as unknown.The fix detects
msg?.error?.name === "MessageAbortedError"and uses that at bothAgent.prompt()return sites that return an assistant message:The command-only return site is unchanged because it has no assistant message to inspect.
A larger follow-up could recover provider usage during interrupt cleanup, but this PR intentionally keeps the fix scoped to reporting the correct stop reason and avoiding known-bad zero usage.
How did you verify your code works?
bun test test/acp/event-subscription.test.ts- 12 pass / 0 failprompt() returns stopReason: cancelled with no usage when message has MessageAbortedErrorprompt() returns stopReason: end_turn with usage on normal completionbun run typecheck- cleanScreenshots / recordings
N/A - backend protocol fix.
Checklist