Skip to content

Commit fe9479d

Browse files
authored
fix: rewrite repeated tool call reminders to redirect instead of prohibit (#1518)
The r1/r2/r3 reminders injected into repeated tool results led with prohibition verdicts and, in r2, echoed the repeated tool name and full arguments back into the context, reinforcing the very pattern they were meant to break. Rewrite them to state the situation factually and hand the model a concrete next action: an expectation-setting sentence for the next call (r1), a forced decision menu of falsify / ask-user / conclude (r2), and a final hand-off summary without further tool calls (r3). Detection, thresholds (3/5/8/12), force-stop, and telemetry are unchanged.
1 parent 173bdfd commit fe9479d

2 files changed

Lines changed: 32 additions & 36 deletions

File tree

packages/agent-core/src/agent/turn/tool-dedup.ts

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,28 @@ import { canonicalTelemetryArgs } from './canonical-args';
77

88
const REMINDER_TEXT_1 =
99
'\n\n<system-reminder>\n' +
10-
'You are repeating the exact same tool call with identical parameters.' +
11-
' Please carefully analyze the previous result. If the task is not yet complete,' +
12-
' try a different method or parameters instead of repeating the same call.' +
10+
'The same tool call has been repeated several times in a row. ' +
11+
'Before making your next call, write one sentence stating what new information you expect it to produce. ' +
12+
'Then act on that sentence: if it names something this result does not already give you, choose the action that best provides it; otherwise, continue with the evidence you already have.' +
1313
'\n</system-reminder>';
1414

15-
function makeReminderText2(toolName: string, repeatCount: number, args: unknown): string {
16-
const argsStr = canonicalTelemetryArgs(args);
15+
function makeReminderText2(repeatCount: number): string {
1716
return (
1817
'\n\n<system-reminder>\n' +
19-
'You have repeatedly called the same tool with identical parameters many times.\n' +
20-
'Repeated tool call detected:\n' +
21-
`- tool: ${toolName}\n` +
22-
`- repeated_times: ${String(repeatCount)}\n` +
23-
`- arguments: ${argsStr}\n` +
24-
'The previous repeated calls did not make progress. Do not call this exact same tool with the exact same arguments again.\n' +
25-
'Carefully inspect the latest tool result and choose a different next action, different parameters, or finish the task if enough evidence has been gathered.' +
18+
`The same tool call has now been issued ${String(repeatCount)} times in a row. ` +
19+
'Choose exactly one of the following and state your choice before acting:\n' +
20+
'(1) Falsification check: run the cheapest test that could conclusively disprove your current approach, if such a test exists.\n' +
21+
'(2) Missing input: tell the user precisely what information or decision you need to proceed, and ask for it.\n' +
22+
'(3) Conclude: deliver your best result based on the evidence already gathered, listing anything that remains uncertain.' +
2623
'\n</system-reminder>'
2724
);
2825
}
2926

3027
const REMINDER_TEXT_3 =
3128
'\n\n<system-reminder>\n' +
32-
'You are stuck in a dead end and have repeatedly made the same function call without progress.\n' +
33-
'Stop all function calls immediately. Do not call any tool in your next response.\n' +
34-
'In analysis, review the current execution state and identify why progress is blocked.\n' +
35-
'Then return a text-only summary to the user that reports the current problem, what has already been tried, and what information or decision is needed next.' +
29+
'Write your final response now, without any further tool calls. ' +
30+
'Cover: the current blocker, each approach you have tried and what it established, and the specific information or decision you need from the user to unblock progress. ' +
31+
'Text only.' +
3632
'\n</system-reminder>';
3733

3834
const REPEAT_REMINDER_1_START = 3;
@@ -105,8 +101,8 @@ const DEDUP_PLACEHOLDER_RESULT: ExecutableToolResult = { output: '' };
105101
* - Cross-step dedup: when the exact same call is repeated consecutively
106102
* across steps, the result returned to the model is suffixed with a system
107103
* reminder once the streak hits 3. The reminder escalates as the streak
108-
* grows: r1 (gentle nudge) from streak 3, r2 (concrete repeat report) from
109-
* streak 5, r3 (dead-end stop instruction) from streak 8. From streak 12
104+
* grows: r1 (expectation-setting nudge) from streak 3, r2 (forced decision
105+
* menu) from streak 5, r3 (final hand-off instruction) from streak 8. From streak 12
110106
* onward the turn is force-stopped via `{ stopTurn: true }` so the loop
111107
* cannot keep spinning on the same call. Force-stop does not flip a
112108
* successful tool result into an error — the underlying tool's `isError`
@@ -239,7 +235,7 @@ export class ToolCallDeduplicator {
239235
finalResult = appendReminder(result, REMINDER_TEXT_3);
240236
action = 'r3';
241237
} else if (streak >= REPEAT_REMINDER_2_START) {
242-
finalResult = appendReminder(result, makeReminderText2(toolName, streak, args));
238+
finalResult = appendReminder(result, makeReminderText2(streak));
243239
action = 'r2';
244240
} else if (streak >= REPEAT_REMINDER_1_START) {
245241
finalResult = appendReminder(result, REMINDER_TEXT_1);

packages/agent-core/test/agent/turn/tool-dedup.test.ts

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ describe('ToolCallDeduplicator', () => {
116116
dedup.endStep();
117117
}
118118
expect(last!.output as string).toContain('<system-reminder>');
119-
expect(last!.output as string).toContain('repeating the exact same tool call');
120-
expect(last!.output as string).not.toContain('repeated_times');
119+
expect(last!.output as string).toContain('what new information you expect');
120+
expect(last!.output as string).not.toContain('Choose exactly one');
121121
});
122122

123123
it('keeps injecting reminder1 at 4 consecutive', async () => {
@@ -129,7 +129,7 @@ describe('ToolCallDeduplicator', () => {
129129
dedup.endStep();
130130
}
131131
expect(last!.output as string).toContain('<system-reminder>');
132-
expect(last!.output as string).toContain('repeating the exact same tool call');
132+
expect(last!.output as string).toContain('what new information you expect');
133133
});
134134

135135
it('injects reminder2 at exactly 5 consecutive', async () => {
@@ -141,9 +141,9 @@ describe('ToolCallDeduplicator', () => {
141141
dedup.endStep();
142142
}
143143
expect(last!.output as string).toContain('<system-reminder>');
144-
expect(last!.output as string).toContain('repeated_times: 5');
145-
expect(last!.output as string).toContain('tool: Read');
146-
expect(last!.output as string).toContain('arguments:');
144+
expect(last!.output as string).toContain('issued 5 times in a row');
145+
expect(last!.output as string).toContain('Choose exactly one of the following');
146+
expect(last!.output as string).toContain('Falsification check');
147147
});
148148

149149
it.each([6, 7])('keeps injecting reminder2 at %i consecutive', async (streak) => {
@@ -155,8 +155,8 @@ describe('ToolCallDeduplicator', () => {
155155
dedup.endStep();
156156
}
157157
expect(last!.output as string).toContain('<system-reminder>');
158-
expect(last!.output as string).toContain(`repeated_times: ${String(streak)}`);
159-
expect(last!.output as string).toContain('tool: Read');
158+
expect(last!.output as string).toContain(`issued ${String(streak)} times in a row`);
159+
expect(last!.output as string).toContain('Choose exactly one of the following');
160160
});
161161

162162
it('injects the dead-end reminder at exactly 8 consecutive', async () => {
@@ -168,7 +168,7 @@ describe('ToolCallDeduplicator', () => {
168168
dedup.endStep();
169169
}
170170
expect(last!.output as string).toContain('<system-reminder>');
171-
expect(last!.output as string).toContain('stuck in a dead end');
171+
expect(last!.output as string).toContain('without any further tool calls');
172172
});
173173

174174
it('resets streak when a different call is interleaved', async () => {
@@ -214,9 +214,9 @@ describe('ToolCallDeduplicator', () => {
214214
dedup.endStep();
215215

216216
expect(original.output as string).toContain('<system-reminder>');
217-
expect(original.output as string).toContain('repeating the exact same tool call');
217+
expect(original.output as string).toContain('what new information you expect');
218218
expect(finalDup.output as string).toContain('<system-reminder>');
219-
expect(finalDup.output as string).toContain('repeating the exact same tool call');
219+
expect(finalDup.output as string).toContain('what new information you expect');
220220
});
221221

222222
it('same-step spam alone does not trigger reminder', async () => {
@@ -273,7 +273,7 @@ describe('ToolCallDeduplicator', () => {
273273
const arr = final.output as Array<{ type: string; text: string }>;
274274
expect(arr).toHaveLength(1);
275275
expect(arr[0]!.type).toBe('text');
276-
expect(arr[0]!.text).toBe('hello' + makeReminderText2('X', 5, { a: 1 }));
276+
expect(arr[0]!.text).toBe('hello' + makeReminderText2(5));
277277
});
278278

279279
it('pushes a new text part when trailing part is non-text', async () => {
@@ -408,8 +408,8 @@ describe('ToolCallDeduplicator', () => {
408408
const dedup = new ToolCallDeduplicator();
409409
const last = await runStreak(dedup, 8);
410410
expect(last.output as string).toContain('<system-reminder>');
411-
expect(last.output as string).toContain('stuck in a dead end');
412-
expect(last.output as string).toContain('Stop all function calls immediately');
411+
expect(last.output as string).toContain('Write your final response now');
412+
expect(last.output as string).toContain('without any further tool calls');
413413
// 8 is the reminder threshold, not yet force-stop.
414414
expect(last.isError).toBeUndefined();
415415
expect(stopTurnOf(last)).toBeUndefined();
@@ -420,7 +420,7 @@ describe('ToolCallDeduplicator', () => {
420420
async (streak) => {
421421
const dedup = new ToolCallDeduplicator();
422422
const last = await runStreak(dedup, streak);
423-
expect(last.output as string).toContain('stuck in a dead end');
423+
expect(last.output as string).toContain('Write your final response now');
424424
expect(last.isError).toBeUndefined();
425425
expect(stopTurnOf(last)).toBeUndefined();
426426
},
@@ -429,7 +429,7 @@ describe('ToolCallDeduplicator', () => {
429429
it('force-stops the turn at exactly 12 consecutive without marking the tool failed', async () => {
430430
const dedup = new ToolCallDeduplicator();
431431
const last = await runStreak(dedup, 12);
432-
expect(last.output as string).toContain('stuck in a dead end');
432+
expect(last.output as string).toContain('Write your final response now');
433433
// The underlying tool succeeded — force-stop must not flip it to error.
434434
expect(last.isError).toBeUndefined();
435435
expect(stopTurnOf(last)).toBe(true);
@@ -459,7 +459,7 @@ describe('ToolCallDeduplicator', () => {
459459
// The underlying tool was an error — that must survive force-stop.
460460
expect(last!.isError).toBe(true);
461461
expect(stopTurnOf(last!)).toBe(true);
462-
expect(last!.output as string).toContain('stuck in a dead end');
462+
expect(last!.output as string).toContain('Write your final response now');
463463
});
464464
});
465465

0 commit comments

Comments
 (0)