Skip to content

feat(agent-core-v2): interruption reminder for user-cancelled turns - #2400

Merged
7Sageer merged 10 commits into
mainfrom
feat-interrupt-reminder
Jul 31, 2026
Merged

feat(agent-core-v2): interruption reminder for user-cancelled turns#2400
7Sageer merged 10 commits into
mainfrom
feat-interrupt-reminder

Conversation

@7Sageer

@7Sageer 7Sageer commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

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

  • Interruption reminder (model-facing): turn.ended now carries interruptReason (reusing the telemetry classification). A new Agent-scope loop aspect (AgentLoopInterruptionReminderService) watches it and, only for cancelled + user_cancelled, appends a <system-reminder> (origin: {kind:'injection', variant:'interruption'}) through the existing system-reminder machinery — persisted via the normal context.append_message wire 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.
  • Partial stream preserved: the loop's stream handler accumulates text/thinking deltas with the same merge semantics as the response assembler; when the turn itself is aborted, they are persisted as content.part loop 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.
  • Interrupt reason persisted: the turn.cancel wire op gained an optional reason: 'user_cancelled' | 'aborted' field (backward-compatible with older records), mirrored into the kap-server and klient event schemas.
  • Interruption marker (user-facing): the transcript projects the same fact onto the timeline — the live projector emits an interruption marker when a turn ends with interruptReason: 'user_cancelled', and the cold fold rebuilds it from the persisted turn.cancel reason. 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 reminder suite 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

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update (no user-visible surface change: Esc behavior is unchanged and the reminder is model-facing only).

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-bot

changeset-bot Bot commented Jul 30, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f53b2a7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Patch

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

7Sageer added 2 commits July 30, 2026 15:42
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
@pkg-pr-new

pkg-pr-new Bot commented Jul 30, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@f53b2a7
npx https://pkg.pr.new/@moonshot-ai/kimi-code@f53b2a7

commit: f53b2a7

7Sageer and others added 7 commits July 30, 2026 17:45
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
@7Sageer
7Sageer merged commit 1f3f5da into main Jul 31, 2026
23 of 24 checks passed
@7Sageer
7Sageer deleted the feat-interrupt-reminder branch July 31, 2026 10:17
@github-actions github-actions Bot mentioned this pull request Jul 31, 2026
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