Skip to content

Commit e3bf50c

Browse files
authored
fix(agent-core-v2): remove interruption reminder on undo of its turn (#4076)
1 parent a940f2f commit e3bf50c

3 files changed

Lines changed: 54 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@moonshot-ai/kimi-code": patch
3+
---
4+
5+
Remove interruption reminders from the conversation when their interrupted turns are undone.

‎packages/agent-core-v2/src/agent/interruptionReminder/interruptionReminderService.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Disposable } from '#/_base/di/lifecycle';
22
import { LifecycleScope } from '#/app/scopes';
33
import { ScopeActivation, registerScopedService } from '#/_base/di/scope';
44
import { IAgentContextMemoryService } from '#/agent/contextMemory/contextMemory';
5+
import { isUndoAnchor } from '#/agent/contextMemory/conversationTime';
56
import type { ContextMessage } from '#/agent/contextMemory/types';
67
import { isVacuousContentPart } from '#/agent/contextMemory/vacuousContent';
78
import { TurnEnded } from '#/agent/loop/turnOps';
@@ -35,10 +36,12 @@ export class AgentInterruptionReminderService
3536
this._register(
3637
eventBus.subscribe(TurnEnded, (event) => {
3738
if (event.reason !== 'cancelled' || event.interruptReason !== 'user_cancelled') return;
38-
const origin = lastComparableMessage(this.context.get())?.origin;
39+
const history = this.context.get();
40+
const origin = lastComparableMessage(history)?.origin;
3941
if (origin?.kind === 'injection' && origin.variant === INTERRUPTION_REMINDER_VARIANT) return;
4042
this.reminder.notify(INTERRUPTION_REMINDER, {
4143
variant: INTERRUPTION_REMINDER_VARIANT,
44+
ownerPromptId: history.findLast(isUndoAnchor)?.id,
4245
});
4346
}),
4447
);

‎packages/agent-core-v2/test/agent/loop/loop.test.ts‎

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1981,22 +1981,56 @@ describe('interruption reminder', () => {
19811981

19821982
await ctx.undoHistory(1);
19831983

1984-
expect(
1985-
ctx.contextData().history.map((message) => ({
1986-
role: message.role,
1987-
origin: message.origin,
1988-
})),
1989-
).toEqual([
1990-
{
1991-
role: 'user',
1992-
origin: { kind: 'injection', variant: 'interruption', ownerPromptId: undefined },
1993-
},
1994-
]);
1984+
expect(ctx.contextData().history).toEqual([]);
1985+
await ctx.expectResumeMatches();
19951986

19961987
ctx.mockNextResponse({ type: 'text', text: 'second answer' });
19971988
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Next' }] });
19981989
await ctx.untilTurnEnd();
1990+
expect(interruptionReminders()).toHaveLength(0);
1991+
});
1992+
1993+
it('keeps an interruption reminder when undo only removes a later turn', async () => {
1994+
ctx.mockNextResponse({ type: 'text', text: 'partial answer' });
1995+
const subscription = cancelOnFirstDelta();
1996+
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Hello' }] });
1997+
await ctx.untilTurnEnd();
1998+
subscription.dispose();
1999+
const interruptedHistory = ctx.contextData().history;
2000+
2001+
ctx.mockNextResponse({ type: 'text', text: 'second answer' });
2002+
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Next' }] });
2003+
await ctx.untilTurnEnd();
2004+
2005+
await ctx.undoHistory(1);
2006+
2007+
expect(ctx.contextData().history).toEqual(interruptedHistory);
2008+
expect(interruptionReminders()).toHaveLength(1);
2009+
await ctx.expectResumeMatches();
2010+
2011+
await ctx.undoHistory(1);
2012+
2013+
expect(ctx.contextData().history).toEqual([]);
2014+
});
2015+
2016+
it('undo removes a cancelled retry reminder with its preceding user prompt', async () => {
2017+
ctx.mockNextResponse({ type: 'text', text: 'first answer' });
2018+
await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Hello' }] });
2019+
await ctx.untilTurnEnd();
2020+
2021+
ctx.mockNextResponse({ type: 'text', text: 'partial retry' });
2022+
const subscription = cancelOnFirstDelta();
2023+
const retry = submitPromptTurn(loop, {
2024+
message: { role: 'user', content: [] },
2025+
meta: { origin: { kind: 'retry' } },
2026+
}).turn;
2027+
await expect(retry.result).resolves.toMatchObject({ type: 'cancelled' });
2028+
subscription.dispose();
19992029
expect(interruptionReminders()).toHaveLength(1);
2030+
2031+
await ctx.undoHistory(1);
2032+
2033+
expect(ctx.contextData().history).toEqual([]);
20002034
});
20012035

20022036
it('drops unsigned thinking but keeps signed thinking on user cancel', async () => {

0 commit comments

Comments
 (0)