feat: TaskOutput non-blocking + persist turn.ended + interruption reminder (upstream ports) - #10
Conversation
* fix: remove the blocking wait from the TaskOutput tool The block/timeout parameters let a model stall the whole turn waiting for a background task (up to 3600s), even though completion already arrives via automatic notification. Remove both parameters from the v1 and v2 engines (kept in model-facing parity), simplify retrieval_status to success/not_ready, and update the tool, Bash, and Agent prompt wording plus user docs accordingly. Stale callers passing block are silently treated as a non-blocking snapshot. * fix: align background-task prompts with the non-blocking TaskOutput The compaction reminder promised TaskOutput could fetch a task's result for tasks that are still running, where it now returns not_ready — reword it to snapshot semantics and point at the completion notification. Also list AskUserQuestion(background=true) as a task source in the TaskOutput description. * test: exercise stale TaskOutput args through the runtime validator A stale block/timeout argument never reaches the tool: the executor's preflight validates args against the closed tool schema and rejects them immediately, so the old test documented silent-tolerance semantics the runtime never exhibits. Assert the real behavior through compileToolArgsValidator/validateToolArgs instead, and drop statement-adjacent comments to match the package's header-only comment convention.
…nshotAI#2457) * feat(agent-core-v2): persist the terminal turn.ended wire record - add a persisted turn.ended op (turnId, reason, error, durationMs) dispatched from the loop's runTurn finally block, alongside the event - fold the record back in the transcript cold rebuild: terminal state (blocked folded into failed, mirroring the live wire edge), durationMs, error message and endedAt; journals without the record keep the grouping default - restrict the test harness's snapshot waiters to emit entries so the same-named wire record no longer shadows the turn.ended event * chore(agent-core-v2): stabilize unique-symbol keys in the state manifest The checker names a unique symbol key __@name@NNNN, where NNNN is a compilation-global counter that shifts with unrelated type additions and churns the generated manifest. Render the stable __@name form instead. * fix(transcript): map turn.ended around hidden turns in the cold fold RetryStepRequest opens a real engine turn with origin 'retry' but contributes no context messages, and a queued-then-cancelled reservation consumes an engine id without starting. Both make engine turn ids drift from the grouping ordinals, so matching turn.ended by ordinal could stamp a later visible turn with the wrong terminal state. Replay the loop's turn-clock records (turn.prompt / turn.cancel) and map engine ids to ordinals past the hidden ids; the hidden turns' own end records map nowhere and are dropped.
…oonshotAI#2400) * feat(agent-core-v2): interruption reminder for user-cancelled turns 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. * feat(transcript): mark user-cancelled turns with an interruption marker 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. * fix(agent-core-v2): make user-turn cancellation idempotent and reconcile interruption reminders on restore * fix(transcript): dedupe user-cancelled interruption markers by turn in the cold fold * chore(agent-core-v2): regenerate state manifest after merging main * refactor(agent-core-v2): split interruptionReminder out of the loop domain 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. --------- Signed-off-by: Haozhe <yanghaozhe@moonshot.ai> Co-authored-by: Haozhe <yanghaozhe@moonshot.ai>
📝 WalkthroughWalkthroughThis change adds interruption-aware turn persistence, partial-output recovery, interruption reminders, and transcript folding. It also changes TaskOutput to always return immediate snapshots and removes blocking parameters, waiting behavior, and related guidance. ChangesInterruption lifecycle
Non-blocking TaskOutput
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant AgentLoop
participant WirePersistence
participant Transcript
User->>AgentLoop: cancel active turn with Esc
AgentLoop->>WirePersistence: persist turn.cancel and turn.ended
AgentLoop->>WirePersistence: persist preserved partial assistant output
WirePersistence->>Transcript: fold terminal turn facts
Transcript-->>User: expose interruption marker and completed turn state
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.changeset/task-output-nonblocking.md:
- Around line 1-5: Change the changeset bump for "`@moonshot-ai/kimi-code`" from
patch to minor in task-output-nonblocking.md, reflecting the removed public
TaskOutput inputs while leaving the release note unchanged.
In `@packages/agent-core-v2/src/agent/loop/loopService.ts`:
- Around line 1127-1129: Remove the statement-level comment inside the
openai_compaction case of createStreamPartHandler, leaving the existing return
behavior unchanged; do not add a replacement inline comment.
In `@packages/agent-core-v2/src/agent/loop/turnOps.ts`:
- Around line 81-89: Update the error field in endTurn’s turn.ended schema to
perform runtime validation instead of relying solely on
z.custom<KimiErrorPayload>(). Add or reuse an ErrorPayload Zod predicate that
validates the serialized KimiErrorPayload shape, while keeping the field
optional and preserving the existing turn.ended schema behavior.
In `@packages/agent-core-v2/test/snapshot/events.ts`:
- Around line 58-64: Update the matching logic for onceWaiters and
onceAnyWaiters to require entry.type === '[rpc]' before comparing event names,
consistent with until and take. Preserve existing event-name matching and
resolution behavior for valid [rpc] entries.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: c392554f-05b5-4c67-abc9-1c6eb409a567
📒 Files selected for processing (51)
.changeset/interrupt-reminder.md.changeset/task-output-nonblocking.mddocs/en/reference/tools.mddocs/zh/reference/tools.mdpackages/agent-core-v2/docs/state-manifest.d.tspackages/agent-core-v2/docs/wire-manifest.d.tspackages/agent-core-v2/scripts/gen-state-manifest.mtspackages/agent-core-v2/src/agent/goal/goalService.tspackages/agent-core-v2/src/agent/interruptionReminder/interruptionReminder.tspackages/agent-core-v2/src/agent/interruptionReminder/interruptionReminderOps.tspackages/agent-core-v2/src/agent/interruptionReminder/interruptionReminderService.tspackages/agent-core-v2/src/agent/loop/loopService.tspackages/agent-core-v2/src/agent/loop/turnEvents.tspackages/agent-core-v2/src/agent/loop/turnOps.tspackages/agent-core-v2/src/agent/task/taskService.tspackages/agent-core-v2/src/agent/tools/agent/agent-background-enabled.mdpackages/agent-core-v2/src/agent/tools/os/bash/bash.mdpackages/agent-core-v2/src/agent/tools/os/bash/bashTool.tspackages/agent-core-v2/src/agent/tools/task/task-output/task-output.mdpackages/agent-core-v2/src/agent/tools/task/task-output/task-output.tspackages/agent-core-v2/src/agent/tools/task/task-output/taskOutputTool.tspackages/agent-core-v2/src/index.tspackages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.tspackages/agent-core-v2/test/agent/goal/goal.test.tspackages/agent-core-v2/test/agent/loop/loop.test.tspackages/agent-core-v2/test/agent/plan/plan.test.tspackages/agent-core-v2/test/agent/task/taskService.test.tspackages/agent-core-v2/test/agent/task/tools/task-tools.test.tspackages/agent-core-v2/test/app/config/config.test.tspackages/agent-core-v2/test/harness/agent.tspackages/agent-core-v2/test/index.test.tspackages/agent-core-v2/test/os/backends/node-local/tools/bash.test.tspackages/agent-core-v2/test/snapshot/events.tspackages/agent-core-v2/test/tool/tool.test.tspackages/agent-core-v2/test/wire/resume.test.tspackages/agent-core/src/agent/injection/manager.tspackages/agent-core/src/tools/background/task-output.mdpackages/agent-core/src/tools/background/task-output.tspackages/agent-core/src/tools/builtin/collaboration/agent-background-enabled.mdpackages/agent-core/src/tools/builtin/shell/bash.mdpackages/agent-core/src/tools/builtin/shell/bash.tspackages/agent-core/test/harness/coder-subagent-tools.test.tspackages/agent-core/test/tools/background/task-tools.test.tspackages/kap-server/src/protocol/events-zod.tspackages/kap-server/src/services/transcript/coreEventMap.tspackages/kap-server/test/services/transcript.test.tspackages/klient/src/contract/agent/events.tspackages/klient/test/e2e/invalid-input-matrix.test.tspackages/transcript/src/history/foldFacts.tspackages/transcript/src/history/groupTurns.tspackages/transcript/test/layers.test.ts
💤 Files with no reviewable changes (1)
- packages/agent-core-v2/src/agent/tools/task/task-output/task-output.ts
| --- | ||
| "@moonshot-ai/kimi-code": patch | ||
| --- | ||
|
|
||
| Remove the blocking `block`/`timeout` wait from the TaskOutput tool so checking a background task can no longer stall the conversation; it now always returns an immediate snapshot, and completion still arrives via automatic notification. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Use a minor changeset for the removed TaskOutput inputs.
Removing block and timeout changes the public tool input contract. A patch release can break callers that still send either field. Change the bump to minor.
As per coding guidelines, .changeset/*.{md,yml,yaml} requires minor when major is not explicitly confirmed and minor is clear.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.changeset/task-output-nonblocking.md around lines 1 - 5, Change the
changeset bump for "`@moonshot-ai/kimi-code`" from patch to minor in
task-output-nonblocking.md, reflecting the removed public TaskOutput inputs
while leaving the release note unchanged.
Source: Coding guidelines
| case 'openai_compaction': { | ||
| // Opaque compaction replay items are not streamed to the UI. | ||
| return; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the statement-level comment.
Line 1128 adds a comment inside createStreamPartHandler. Keep this explanation out of the implementation body.
As per coding guidelines, packages/agent-core-v2/src/**/*.ts must use only a top-of-file /** */ comment block and must not place comments beside functions, methods, or statements.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/agent-core-v2/src/agent/loop/loopService.ts` around lines 1127 -
1129, Remove the statement-level comment inside the openai_compaction case of
createStreamPartHandler, leaving the existing return behavior unchanged; do not
add a replacement inline comment.
Source: Coding guidelines
| export const endTurn = TurnModel.defineOp('turn.ended', { | ||
| schema: z.object({ | ||
| turnId: z.number(), | ||
| reason: z.enum(['completed', 'cancelled', 'failed', 'blocked']), | ||
| error: z.custom<KimiErrorPayload>().optional(), | ||
| durationMs: z.number().optional(), | ||
| }), | ||
| apply: (s) => s, | ||
| }); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 'interface ErrorPayload|type ErrorPayload|KimiErrorPayload|z\.custom' packages/agent-core-v2/srcRepository: YaseenHQ/kimi
Length of output: 26321
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== turnOps outline =="
ast-grep outline packages/agent-core-v2/src/agent/loop/turnOps.ts --match endTurn --view expanded || true
echo
echo "== turnOps relevant lines =="
sed -n '1,130p' packages/agent-core-v2/src/agent/loop/turnOps.ts | cat -n
echo
echo "== wire model schema definitions =="
rg -n -C 3 'defineOp|toEvent|fromEvent|payload|schema' packages/agent-core-v2/src/wire packages/agent-core-v2/src/agent/loop/turnOps.ts packages/agent-core-v2/src/agent/loop/loopService.ts | sed -n '1,220p'
echo
echo "== ErrorPayload implementation =="
sed -n '1,120p' packages/agent-core-v2/src/_base/errors/serialize.ts | cat -n
echo
echo "== Zod custom predicate usages =="
rg -n -C 2 'z\.custom\([^)]*(\{\s*[^}]+\}\s*|=>|predicate|check)' packages/agent-core-v2/src/_base/types packages/agent-core-v2/src || trueRepository: YaseenHQ/kimi
Length of output: 25734
🏁 Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
try:
from pathlib import Path
import tomllib
data = tomllib.loads(Path('package.json').read_text())
print("root zod:", data.get('dependencies', {}).get('zod') or data.get('devDependencies', {}).get('zod'))
except Exception as e:
print("root package.json zod not found:", type(e).__name__, str(e))
for path in ['packages/agent-core-v2/package.json']:
try:
data = tomllib.loads(Path(path).read_text())
except Exception as e:
print(f"{path}: {type(e).__name__} {str(e)}")
continue
print(f"{path} zod:", data.get('dependencies', {}).get('zod') or data.get('devDependencies', {}).get('zod'))
try:
import zod, inspect
print("installed zod:", getattr(zod, '__version__', 'unknown'))
print("z.custom source signature:")
print(inspect.getsource(zod.ZodType.custom))
except Exception as e:
print("zod runtime probe unavailable:", type(e).__name__, str(e))
PY
echo
sed -n '1,180p' packages/agent-core-v2/src/wire/wireService.ts | cat -nRepository: YaseenHQ/kimi
Length of output: 7737
Add a runtime predicate to the error serializer.
z.custom<KimiErrorPayload>() only narrows TypeScript type inference. The turn.ended wire replay path accepts this schema at the record boundary, so a malformed persisted error payload can bypass validation and flow into transcript restoration. Add an ErrorPayload Zod validation predicate, or otherwise check the serialized payload shape.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/agent-core-v2/src/agent/loop/turnOps.ts` around lines 81 - 89,
Update the error field in endTurn’s turn.ended schema to perform runtime
validation instead of relying solely on z.custom<KimiErrorPayload>(). Add or
reuse an ErrorPayload Zod predicate that validates the serialized
KimiErrorPayload shape, while keeping the field optional and preserving the
existing turn.ended schema behavior.
| // Snapshot-returning waiters match EMIT entries only: a persisted wire | ||
| // record can share its type with an event (e.g. `turn.ended`), and the | ||
| // waiter's intent is the event — resolving on the wire entry would also | ||
| // truncate the matching emit entry out of the returned snapshot. | ||
| for (let index = eventWaiters.length - 1; index >= 0; index -= 1) { | ||
| const waiter = eventWaiters[index]!; | ||
| if (waiter.event !== entry.event) continue; | ||
| if (entry.type !== '[rpc]' || waiter.event !== entry.event) continue; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 3 '\.(once|onceAny)\s*\(' packages/agent-core-v2/testRepository: YaseenHQ/kimi
Length of output: 39723
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== snapshots.ts outline =="
ast-grep outline packages/agent-core-v2/test/snapshot/events.ts --view expanded || true
echo "== relevant snapshot code =="
cat -n packages/agent-core-v2/test/snapshot/events.ts | sed -n '1,120p'
echo "== harness relevant lines =="
cat -n packages/agent-core-v2/test/harness/agent.ts | sed -n '1560,1595p'
echo "== tests using onceAny with compaction =="
cat -n packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts | sed -n '520,568p'
cat -n packages/agent-core-v2/test/agent/fullCompaction/fullCompaction.test.ts | sed -n '680,702p'Repository: YaseenHQ/kimi
Length of output: 9819
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== snapshot files =="
fd -a 'snapshots\.(ts|tsx)$|event\.test\.ts$' packages/agent-core-v2/test | sed 's#^\./##'
echo "== event emitter implementation references =="
rg -n "class Event|interface Event|type Event|createEventSnapshotter|export function createEventSnapshotter|createSnapshot|recordedEntries|Emit|records|WireRecord|type:" packages/agent-core-v2/test packages/agent-core-v2/src -g '*.{ts,tsx}' | head -200
echo "== snapshot implementation around createEventSnapshotter =="
fd -a 'snapshots\.(ts|tsx)$' packages/agent-core-v2/test | xargs -r -I{} sh -c 'echo "--- {}"; cat -n "{}" | sed -n "1,240p"'Repository: YaseenHQ/kimi
Length of output: 32435
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== snapshot events remainder =="
cat -n packages/agent-core-v2/test/snapshot/events.ts | sed -n '120,220p'
echo "== inspect existing one-time tests for same-name wire entries =="
rg -n -C 8 "once\\(|onceAny\\(" packages/agent-core-v2/test/agent packages/agent-core-v2/test/snapshot | head -220
echo "== deterministic semantics probe for current dispatch/order =="
python3 - <<'PY'
entries = []
once_waiters = []
once_any_waiters = []
while True:
pass
PY
echo "== git diff stat/name =="
git diff --stat
git diff -- packages/agent-core-v2/test/snapshot/events.ts | sed -n '1,220p'Repository: YaseenHQ/kimi
Length of output: 143
Align once/onceAny with the [rpc] event filter.
until and take only resolve on [rpc] entries. once and onceAny still match any persisted wire entry with the same event name. If these helpers wait for emitted notifications, filter onceWaiters/onceAnyWaiters for [rpc] entries too.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/agent-core-v2/test/snapshot/events.ts` around lines 58 - 64, Update
the matching logic for onceWaiters and onceAnyWaiters to require entry.type ===
'[rpc]' before comparing event names, consistent with until and take. Preserve
existing event-name matching and resolution behavior for valid [rpc] entries.
Related Issue
Upstream port batch 3 of 6. Three interlocked loop/turn-surface improvements from
MoonshotAI/kimi-code, applied in strict dependency order.What changed
691ec4679, fix: remove the blocking wait from the TaskOutput tool MoonshotAI/kimi-code#2379): the TaskOutput tool could block a whole turn for up to 3600s waiting on a background task. Removes theblock/timeoutparams (kept in model-facing parity), simplifiesretrieval_statusto success/not_ready. Both engines + docs.4c4df1bb0, feat(agent-core-v2): persist the terminal turn.ended wire record MoonshotAI/kimi-code#2457): ensures the finalturn.endedfact (reason/error/durationMs) is always persisted, even on abort/error, so downstream history rebuilds recover how a turn ended.1f3f5dada, feat(agent-core-v2): interruption reminder for user-cancelled turns MoonshotAI/kimi-code#2400): on Esc-interrupt, appends a durable<system-reminder>so the model does not replay a deliberately-cancelled action. Adds a newinterruptionReminderdomain.Conflicts resolved
loopService.ts: took upstream side (adds partial-token accumulation for interruption persistence), re-added the fork'sopenai_compactioncase that upstream's switch didn't include.turnOps.ts(x2): pure header-comment merges.state-manifest.d.ts: regenerated viagen:state-manifest.loop.test.ts+tool.test.ts: updated snapshots (turn.ended now carries reason/durationMs; tool-description hashes are fork-specific).Verification
Checklist
Summary by CodeRabbit
New Features
Bug Fixes
TaskOutputnow always returns an immediate task snapshot and no longer waits or times out.Documentation