@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest';
33import type { ExecutableToolResult } from '../../../src/loop/types' ;
44import { ToolCallDeduplicator , __testing } from '../../../src/agent/turn/tool-dedup' ;
55
6- const { REMINDER_TEXT , CROSS_STEP_DEDUP_TRIGGER_COUNT } = __testing ;
6+ const { REMINDER_TEXT_1 , makeReminderText2 } = __testing ;
77
88function okResult ( text : string ) : ExecutableToolResult {
99 return { output : text } ;
@@ -73,10 +73,10 @@ describe('ToolCallDeduplicator', () => {
7373 } ) ;
7474
7575 describe ( 'cross-step streak' , ( ) => {
76- it ( ` does not inject reminder below ${ String ( CROSS_STEP_DEDUP_TRIGGER_COUNT ) } consecutive` , async ( ) => {
76+ it ( ' does not inject reminder below 3 consecutive' , async ( ) => {
7777 const dedup = new ToolCallDeduplicator ( ) ;
7878 let last : ExecutableToolResult | undefined ;
79- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT - 1 ; i += 1 ) {
79+ for ( let i = 0 ; i < 2 ; i += 1 ) {
8080 dedup . beginStep ( ) ;
8181 last = await runOriginal ( dedup , `c${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
8282 dedup . endStep ( ) ;
@@ -85,22 +85,72 @@ describe('ToolCallDeduplicator', () => {
8585 expect ( last ! . output as string ) . not . toContain ( '<system-reminder>' ) ;
8686 } ) ;
8787
88- it ( ` injects reminder at exactly ${ String ( CROSS_STEP_DEDUP_TRIGGER_COUNT ) } consecutive` , async ( ) => {
88+ it ( ' injects reminder1 at exactly 3 consecutive' , async ( ) => {
8989 const dedup = new ToolCallDeduplicator ( ) ;
9090 let last : ExecutableToolResult | undefined ;
91- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT ; i += 1 ) {
91+ for ( let i = 0 ; i < 3 ; i += 1 ) {
9292 dedup . beginStep ( ) ;
9393 last = await runOriginal ( dedup , `c${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
9494 dedup . endStep ( ) ;
9595 }
9696 expect ( last ! . output as string ) . toContain ( '<system-reminder>' ) ;
9797 expect ( last ! . output as string ) . toContain ( 'repeating the exact same tool call' ) ;
98+ expect ( last ! . output as string ) . not . toContain ( 'repeated_times' ) ;
99+ } ) ;
100+
101+ it ( 'does not inject reminder at 4 consecutive' , async ( ) => {
102+ const dedup = new ToolCallDeduplicator ( ) ;
103+ let last : ExecutableToolResult | undefined ;
104+ for ( let i = 0 ; i < 4 ; i += 1 ) {
105+ dedup . beginStep ( ) ;
106+ last = await runOriginal ( dedup , `c${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
107+ dedup . endStep ( ) ;
108+ }
109+ expect ( last ! . output as string ) . not . toContain ( '<system-reminder>' ) ;
110+ } ) ;
111+
112+ it ( 'injects reminder2 at exactly 5 consecutive' , async ( ) => {
113+ const dedup = new ToolCallDeduplicator ( ) ;
114+ let last : ExecutableToolResult | undefined ;
115+ for ( let i = 0 ; i < 5 ; i += 1 ) {
116+ dedup . beginStep ( ) ;
117+ last = await runOriginal ( dedup , `c${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
118+ dedup . endStep ( ) ;
119+ }
120+ expect ( last ! . output as string ) . toContain ( '<system-reminder>' ) ;
121+ expect ( last ! . output as string ) . toContain ( 'repeated_times: 5' ) ;
122+ expect ( last ! . output as string ) . toContain ( 'tool: Read' ) ;
123+ expect ( last ! . output as string ) . toContain ( 'arguments:' ) ;
124+ } ) ;
125+
126+ it ( 'does not inject reminder at 6 or 7 consecutive' , async ( ) => {
127+ const dedup = new ToolCallDeduplicator ( ) ;
128+ let last : ExecutableToolResult | undefined ;
129+ for ( let i = 0 ; i < 7 ; i += 1 ) {
130+ dedup . beginStep ( ) ;
131+ last = await runOriginal ( dedup , `c${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
132+ dedup . endStep ( ) ;
133+ }
134+ expect ( last ! . output as string ) . not . toContain ( '<system-reminder>' ) ;
135+ } ) ;
136+
137+ it ( 'injects reminder2 at exactly 8 consecutive' , async ( ) => {
138+ const dedup = new ToolCallDeduplicator ( ) ;
139+ let last : ExecutableToolResult | undefined ;
140+ for ( let i = 0 ; i < 8 ; i += 1 ) {
141+ dedup . beginStep ( ) ;
142+ last = await runOriginal ( dedup , `c${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
143+ dedup . endStep ( ) ;
144+ }
145+ expect ( last ! . output as string ) . toContain ( '<system-reminder>' ) ;
146+ expect ( last ! . output as string ) . toContain ( 'repeated_times: 8' ) ;
147+ expect ( last ! . output as string ) . toContain ( 'tool: Read' ) ;
98148 } ) ;
99149
100150 it ( 'resets streak when a different call is interleaved' , async ( ) => {
101151 const dedup = new ToolCallDeduplicator ( ) ;
102- // 6 × Read({p:1}) — should NOT trigger yet
103- for ( let i = 0 ; i < 6 ; i += 1 ) {
152+ // 2 × Read({p:1}) — should NOT trigger yet
153+ for ( let i = 0 ; i < 2 ; i += 1 ) {
104154 dedup . beginStep ( ) ;
105155 await runOriginal ( dedup , `a${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
106156 dedup . endStep ( ) ;
@@ -116,15 +166,15 @@ describe('ToolCallDeduplicator', () => {
116166 expect ( last . output as string ) . not . toContain ( '<system-reminder>' ) ;
117167 } ) ;
118168
119- it ( 'same-step dups inherit the reminder when streak triggers on original' , async ( ) => {
169+ it ( 'same-step dups inherit reminder1 when streak triggers on original' , async ( ) => {
120170 const dedup = new ToolCallDeduplicator ( ) ;
121- // Build streak up to N-1 across previous steps.
122- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT - 1 ; i += 1 ) {
171+ // Build streak up to 2 across previous steps.
172+ for ( let i = 0 ; i < 2 ; i += 1 ) {
123173 dedup . beginStep ( ) ;
124174 await runOriginal ( dedup , `p${ String ( i ) } ` , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
125175 dedup . endStep ( ) ;
126176 }
127- // Next step: same call appears twice. First is the original (triggers reminder ),
177+ // Next step: same call appears twice. First is the original (triggers reminder1 at streak=3 ),
128178 // second is a same-step dup that should inherit it.
129179 dedup . beginStep ( ) ;
130180 const original = await runOriginal (
@@ -140,18 +190,20 @@ describe('ToolCallDeduplicator', () => {
140190 dedup . endStep ( ) ;
141191
142192 expect ( original . output as string ) . toContain ( '<system-reminder>' ) ;
193+ expect ( original . output as string ) . toContain ( 'repeating the exact same tool call' ) ;
143194 expect ( finalDup . output as string ) . toContain ( '<system-reminder>' ) ;
195+ expect ( finalDup . output as string ) . toContain ( 'repeating the exact same tool call' ) ;
144196 } ) ;
145197
146198 it ( 'same-step spam alone does not trigger reminder' , async ( ) => {
147199 const dedup = new ToolCallDeduplicator ( ) ;
148- // 7 occurrences of the same call within a single step, but no prior
200+ // 8 occurrences of the same call within a single step, but no prior
149201 // streak — the trigger is about sustained behaviour across steps, not
150202 // intra-step spam. Same-step dedup already short-circuits execution.
151203 dedup . beginStep ( ) ;
152204 const cached = dedup . checkSameStep ( 'orig' , 'Read' , { p : 1 } ) ;
153205 expect ( cached ) . toBeNull ( ) ;
154- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT - 1 ; i += 1 ) {
206+ for ( let i = 0 ; i < 7 ; i += 1 ) {
155207 dedup . checkSameStep ( `dup${ String ( i ) } ` , 'Read' , { p : 1 } ) ;
156208 }
157209 const final = await dedup . finalizeResult ( 'orig' , 'Read' , { p : 1 } , okResult ( 'R' ) ) ;
@@ -160,13 +212,13 @@ describe('ToolCallDeduplicator', () => {
160212 } ) ;
161213
162214 describe ( 'reminder injection into ContentPart[] outputs' , ( ) => {
163- it ( 'appends to a trailing text part' , async ( ) => {
215+ it ( 'appends reminder1 to a trailing text part at streak 3 ' , async ( ) => {
164216 const dedup = new ToolCallDeduplicator ( ) ;
165217 const arrayResult : ExecutableToolResult = {
166218 output : [ { type : 'text' , text : 'hello' } ] ,
167219 } ;
168- // Force streak = trigger by running N-1 prior steps then this one.
169- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT - 1 ; i += 1 ) {
220+ // Build streak up to 2 prior steps then this one (streak=3) .
221+ for ( let i = 0 ; i < 2 ; i += 1 ) {
170222 dedup . beginStep ( ) ;
171223 await runOriginal ( dedup , `p${ String ( i ) } ` , 'X' , { } , okResult ( 'R' ) ) ;
172224 dedup . endStep ( ) ;
@@ -177,15 +229,36 @@ describe('ToolCallDeduplicator', () => {
177229 const arr = final . output as Array < { type : string ; text : string } > ;
178230 expect ( arr ) . toHaveLength ( 1 ) ;
179231 expect ( arr [ 0 ] ! . type ) . toBe ( 'text' ) ;
180- expect ( arr [ 0 ] ! . text ) . toBe ( 'hello' + REMINDER_TEXT ) ;
232+ expect ( arr [ 0 ] ! . text ) . toBe ( 'hello' + REMINDER_TEXT_1 ) ;
233+ } ) ;
234+
235+ it ( 'appends reminder2 to a trailing text part at streak 5' , async ( ) => {
236+ const dedup = new ToolCallDeduplicator ( ) ;
237+ const arrayResult : ExecutableToolResult = {
238+ output : [ { type : 'text' , text : 'hello' } ] ,
239+ } ;
240+ // Build streak up to 4 prior steps then this one (streak=5).
241+ for ( let i = 0 ; i < 4 ; i += 1 ) {
242+ dedup . beginStep ( ) ;
243+ await runOriginal ( dedup , `p${ String ( i ) } ` , 'X' , { a : 1 } , okResult ( 'R' ) ) ;
244+ dedup . endStep ( ) ;
245+ }
246+ dedup . beginStep ( ) ;
247+ const final = await runOriginal ( dedup , 'final' , 'X' , { a : 1 } , arrayResult ) ;
248+ dedup . endStep ( ) ;
249+ const arr = final . output as Array < { type : string ; text : string } > ;
250+ expect ( arr ) . toHaveLength ( 1 ) ;
251+ expect ( arr [ 0 ] ! . type ) . toBe ( 'text' ) ;
252+ expect ( arr [ 0 ] ! . text ) . toBe ( 'hello' + makeReminderText2 ( 'X' , 5 , { a : 1 } ) ) ;
181253 } ) ;
182254
183255 it ( 'pushes a new text part when trailing part is non-text' , async ( ) => {
184256 const dedup = new ToolCallDeduplicator ( ) ;
185257 const arrayResult : ExecutableToolResult = {
186258 output : [ { type : 'image_url' , imageUrl : { url : 'data:foo' } } ] ,
187259 } ;
188- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT - 1 ; i += 1 ) {
260+ // Build streak to 3.
261+ for ( let i = 0 ; i < 2 ; i += 1 ) {
189262 dedup . beginStep ( ) ;
190263 await runOriginal ( dedup , `p${ String ( i ) } ` , 'X' , { } , okResult ( 'R' ) ) ;
191264 dedup . endStep ( ) ;
@@ -197,12 +270,13 @@ describe('ToolCallDeduplicator', () => {
197270 expect ( arr ) . toHaveLength ( 2 ) ;
198271 expect ( arr [ 0 ] ! . type ) . toBe ( 'image_url' ) ;
199272 expect ( arr [ 1 ] ! . type ) . toBe ( 'text' ) ;
200- expect ( arr [ 1 ] ! . text ) . toBe ( REMINDER_TEXT ) ;
273+ expect ( arr [ 1 ] ! . text ) . toBe ( REMINDER_TEXT_1 ) ;
201274 } ) ;
202275
203276 it ( 'preserves isError flag when injecting reminder' , async ( ) => {
204277 const dedup = new ToolCallDeduplicator ( ) ;
205- for ( let i = 0 ; i < CROSS_STEP_DEDUP_TRIGGER_COUNT - 1 ; i += 1 ) {
278+ // Build streak to 3.
279+ for ( let i = 0 ; i < 2 ; i += 1 ) {
206280 dedup . beginStep ( ) ;
207281 await runOriginal ( dedup , `p${ String ( i ) } ` , 'X' , { } , errResult ( 'boom' ) ) ;
208282 dedup . endStep ( ) ;
0 commit comments