feat(agent-core-v2): interruption reminder for user-cancelled turns - #2400
Merged
Conversation
When the user interrupts a turn with Esc, append a durable
<system-reminder> (origin: injection/interruption) to the agent context
via a new loop aspect watching turn.ended, so the model learns the
previous turn was deliberately cut off. The marker persists to the
wire, replays on resume, stays hidden from transcripts, skips non-user
aborts and steer, and does not stack on repeated cancels.
Two supporting fixes:
- An aborted LLM stream now persists its accumulated partial
text/thinking as content.part loop events instead of dropping every
produced token; gated on the turn signal so retried or
step-cancelled attempts keep their partial output out of the record.
- The turn.cancel wire op carries an optional reason
('user_cancelled' | 'aborted') so cold readers can tell deliberate
interrupts from programmatic aborts. Goal-lifecycle cancels now pass
an explicit programmatic reason to keep that field honest.
🦋 Changeset detectedLatest commit: f53b2a7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Project the deliberate user interrupt onto the transcript timeline: the live projector emits an 'interruption' marker when a turn ends with interruptReason 'user_cancelled', and the cold fold consumes the persisted turn.cancel reason into the same marker. Programmatic aborts keep surfacing through their own outlets (errors, goal/task state), and queued cancels that left no visible residue are skipped.
# Conflicts: # packages/agent-core-v2/docs/state-manifest.d.ts
commit: |
…ile interruption reminders on restore
Signed-off-by: Haozhe <yanghaozhe@moonshot.ai>
# Conflicts: # packages/agent-core-v2/docs/state-manifest.d.ts
…omain The loop domain owns turn execution mechanics; whether an interrupted turn should produce a model-visible reminder is a model-context policy. Move it into its own L4 domain with its own wire model that cross-reduces the loop's turn.cancel fact, and rename the op to interruptionReminder.recorded.
# Conflicts: # packages/agent-core-v2/docs/state-manifest.d.ts # packages/agent-core-v2/docs/wire-manifest.d.ts # packages/agent-core-v2/scripts/check-domain-layers.mjs # packages/agent-core-v2/src/agent/loop/loopService.ts # packages/agent-core-v2/src/agent/loop/turnOps.ts # packages/agent-core-v2/test/agent/loop/loop.test.ts # packages/agent-core-v2/test/index.test.ts # packages/transcript/src/history/foldFacts.ts
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.
Related Issue
None — the problem is explained below.
Problem
When the user interrupts a turn with Esc, the engine cancels the turn and the next user message lands as an ordinary message with nothing in between: the model cannot tell its previous turn was deliberately cut off. Two related gaps made this worse — the v2 engine dropped all streaming partial text produced before the interrupt, and the interruption itself was never persisted to the wire log, so after a cold resume neither the model nor the transcript timeline could tell the turn had been interrupted.
What changed
turn.endednow carriesinterruptReason(reusing the telemetry classification). A new Agent-scope loop aspect (AgentLoopInterruptionReminderService) watches it and, only forcancelled + user_cancelled, appends a<system-reminder>(origin: {kind:'injection', variant:'interruption'}) through the existing system-reminder machinery — persisted via the normalcontext.append_messagewire op, replayed on resume, hidden from transcripts, and skipped by undo accounting. A tail check (seeing through a vacuous open assistant) keeps repeated Esc presses from stacking markers; steer never cancels a turn so it never triggers; goal-lifecycle cancels now pass an explicit programmatic reason so they are neither recorded nor reminded as user interrupts.content.partloop events so the interrupted step replays to the partial assistant the user saw. Failed or step-cancelled attempts deliberately keep no partial output, because what follows regenerates it.turn.cancelwire op gained an optionalreason: 'user_cancelled' | 'aborted'field (backward-compatible with older records), mirrored into the kap-server and klient event schemas.interruptionmarker when a turn ends withinterruptReason: 'user_cancelled', and the cold fold rebuilds it from the persistedturn.cancelreason. Programmatic aborts keep surfacing through their own outlets (errors, goal/task state); queued cancels that left no visible residue are skipped.Tests: a new
interruption remindersuite in the loop tests (injection, dedup, queued-cancel, ordering into the next LLM request, undo, thinking/whitespace partials, retry-turn stacking, tool-execution cancel), updated goal-cancellation provenance tests, an updated klient e2e expectation, and marker tests on both transcript paths (live projector and cold fold). The full agent-core-v2 suite (4232 tests) is green, including the harness's live-vs-resumed replay consistency check.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update (no user-visible surface change: Esc behavior is unchanged and the reminder is model-facing only).