@@ -4,123 +4,96 @@ export interface ToolTracker {
44}
55
66export function createToolTracker ( ) : ToolTracker {
7- return {
8- seenToolResultIds : new Set ( ) ,
9- toolResultCount : 0
10- }
7+ return { seenToolResultIds : new Set ( ) , toolResultCount : 0 }
118}
129
13- // ============================================================================
14- // OpenAI Chat / Anthropic Format
15- // ============================================================================
16-
17- function countToolResults ( messages : any [ ] , tracker : ToolTracker ) : number {
18- let newCount = 0
19-
20- for ( const m of messages ) {
21- if ( m . role === 'tool' && m . tool_call_id ) {
22- const id = String ( m . tool_call_id ) . toLowerCase ( )
23- if ( ! tracker . seenToolResultIds . has ( id ) ) {
24- tracker . seenToolResultIds . add ( id )
25- newCount ++
26- }
27- } else if ( m . role === 'user' && Array . isArray ( m . content ) ) {
28- for ( const part of m . content ) {
29- if ( part . type === 'tool_result' && part . tool_use_id ) {
30- const id = String ( part . tool_use_id ) . toLowerCase ( )
31- if ( ! tracker . seenToolResultIds . has ( id ) ) {
32- tracker . seenToolResultIds . add ( id )
33- newCount ++
34- }
35- }
36- }
37- }
38- }
39-
40- tracker . toolResultCount += newCount
41- return newCount
10+ /** Adapter interface for format-specific message operations */
11+ interface MessageFormatAdapter {
12+ countToolResults ( messages : any [ ] , tracker : ToolTracker ) : number
13+ appendNudge ( messages : any [ ] , nudgeText : string ) : void
4214}
4315
44- /**
45- * Counts new tool results and injects nudge instruction every N tool results.
46- * Returns true if injection happened.
47- */
48- export function injectNudge (
16+ /** Generic nudge injection - counts tool results and injects nudge every N results */
17+ function injectNudgeCore (
4918 messages : any [ ] ,
5019 tracker : ToolTracker ,
5120 nudgeText : string ,
52- freq : number
21+ freq : number ,
22+ adapter : MessageFormatAdapter
5323) : boolean {
5424 const prevCount = tracker . toolResultCount
55- const newCount = countToolResults ( messages , tracker )
56-
25+ const newCount = adapter . countToolResults ( messages , tracker )
5726 if ( newCount > 0 ) {
58- // Check if we crossed a multiple of freq
5927 const prevBucket = Math . floor ( prevCount / freq )
6028 const newBucket = Math . floor ( tracker . toolResultCount / freq )
6129 if ( newBucket > prevBucket ) {
62- // Inject at the END of messages so it's in immediate context
63- return appendNudge ( messages , nudgeText )
30+ adapter . appendNudge ( messages , nudgeText )
31+ return true
6432 }
6533 }
6634 return false
6735}
6836
69- export function isIgnoredUserMessage ( msg : any ) : boolean {
70- if ( ! msg || msg . role !== 'user' ) {
71- return false
72- }
37+ // ============================================================================
38+ // OpenAI Chat / Anthropic Format
39+ // ============================================================================
7340
74- // Skip ignored or synthetic messages
75- if ( msg . ignored || msg . info ?. ignored || msg . synthetic ) {
76- return true
41+ const openaiAdapter : MessageFormatAdapter = {
42+ countToolResults ( messages , tracker ) {
43+ let newCount = 0
44+ for ( const m of messages ) {
45+ if ( m . role === 'tool' && m . tool_call_id ) {
46+ const id = String ( m . tool_call_id ) . toLowerCase ( )
47+ if ( ! tracker . seenToolResultIds . has ( id ) ) {
48+ tracker . seenToolResultIds . add ( id )
49+ newCount ++
50+ }
51+ } else if ( m . role === 'user' && Array . isArray ( m . content ) ) {
52+ for ( const part of m . content ) {
53+ if ( part . type === 'tool_result' && part . tool_use_id ) {
54+ const id = String ( part . tool_use_id ) . toLowerCase ( )
55+ if ( ! tracker . seenToolResultIds . has ( id ) ) {
56+ tracker . seenToolResultIds . add ( id )
57+ newCount ++
58+ }
59+ }
60+ }
61+ }
62+ }
63+ tracker . toolResultCount += newCount
64+ return newCount
65+ } ,
66+ appendNudge ( messages , nudgeText ) {
67+ messages . push ( { role : 'user' , content : nudgeText , synthetic : true } )
7768 }
69+ }
7870
71+ export function isIgnoredUserMessage ( msg : any ) : boolean {
72+ if ( ! msg || msg . role !== 'user' ) return false
73+ if ( msg . ignored || msg . info ?. ignored || msg . synthetic ) return true
7974 if ( Array . isArray ( msg . content ) && msg . content . length > 0 ) {
80- const allPartsIgnored = msg . content . every ( ( part : any ) => part ?. ignored )
81- if ( allPartsIgnored ) {
82- return true
83- }
75+ if ( msg . content . every ( ( part : any ) => part ?. ignored ) ) return true
8476 }
85-
8677 return false
8778}
8879
89- /**
90- * Appends a nudge message at the END of the messages array as a new user message.
91- * This ensures it's in the model's immediate context, not buried in old messages.
92- */
93- function appendNudge ( messages : any [ ] , nudgeText : string ) : boolean {
94- messages . push ( {
95- role : 'user' ,
96- content : nudgeText ,
97- synthetic : true
98- } )
99- return true
80+ export function injectNudge ( messages : any [ ] , tracker : ToolTracker , nudgeText : string , freq : number ) : boolean {
81+ return injectNudgeCore ( messages , tracker , nudgeText , freq , openaiAdapter )
10082}
10183
10284export function injectSynth ( messages : any [ ] , instruction : string ) : boolean {
103- // Find the last user message that is not ignored
10485 for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
10586 const msg = messages [ i ]
10687 if ( msg . role === 'user' && ! isIgnoredUserMessage ( msg ) ) {
107- // Avoid double-injecting the same instruction
10888 if ( typeof msg . content === 'string' ) {
109- if ( msg . content . includes ( instruction ) ) {
110- return false
111- }
89+ if ( msg . content . includes ( instruction ) ) return false
11290 msg . content = msg . content + '\n\n' + instruction
11391 } else if ( Array . isArray ( msg . content ) ) {
11492 const alreadyInjected = msg . content . some (
11593 ( part : any ) => part ?. type === 'text' && typeof part . text === 'string' && part . text . includes ( instruction )
11694 )
117- if ( alreadyInjected ) {
118- return false
119- }
120- msg . content . push ( {
121- type : 'text' ,
122- text : instruction
123- } )
95+ if ( alreadyInjected ) return false
96+ msg . content . push ( { type : 'text' , text : instruction } )
12497 }
12598 return true
12699 }
@@ -132,72 +105,42 @@ export function injectSynth(messages: any[], instruction: string): boolean {
132105// Google/Gemini Format (body.contents with parts)
133106// ============================================================================
134107
135- function countToolResultsGemini ( contents : any [ ] , tracker : ToolTracker ) : number {
136- let newCount = 0
137-
138- for ( const content of contents ) {
139- if ( ! Array . isArray ( content . parts ) ) continue
140-
141- for ( const part of content . parts ) {
142- if ( part . functionResponse ) {
143- // Use function name + index as a pseudo-ID since Gemini doesn't have tool call IDs
144- const funcName = part . functionResponse . name ?. toLowerCase ( ) || 'unknown'
145- const pseudoId = `gemini:${ funcName } :${ tracker . seenToolResultIds . size } `
146- if ( ! tracker . seenToolResultIds . has ( pseudoId ) ) {
147- tracker . seenToolResultIds . add ( pseudoId )
148- newCount ++
108+ const geminiAdapter : MessageFormatAdapter = {
109+ countToolResults ( contents , tracker ) {
110+ let newCount = 0
111+ for ( const content of contents ) {
112+ if ( ! Array . isArray ( content . parts ) ) continue
113+ for ( const part of content . parts ) {
114+ if ( part . functionResponse ) {
115+ const funcName = part . functionResponse . name ?. toLowerCase ( ) || 'unknown'
116+ const pseudoId = `gemini:${ funcName } :${ tracker . seenToolResultIds . size } `
117+ if ( ! tracker . seenToolResultIds . has ( pseudoId ) ) {
118+ tracker . seenToolResultIds . add ( pseudoId )
119+ newCount ++
120+ }
149121 }
150122 }
151123 }
124+ tracker . toolResultCount += newCount
125+ return newCount
126+ } ,
127+ appendNudge ( contents , nudgeText ) {
128+ contents . push ( { role : 'user' , parts : [ { text : nudgeText } ] } )
152129 }
153-
154- tracker . toolResultCount += newCount
155- return newCount
156130}
157131
158- /**
159- * Counts new tool results and injects nudge instruction every N tool results (Gemini format).
160- * Returns true if injection happened.
161- */
162- export function injectNudgeGemini (
163- contents : any [ ] ,
164- tracker : ToolTracker ,
165- nudgeText : string ,
166- freq : number
167- ) : boolean {
168- const prevCount = tracker . toolResultCount
169- const newCount = countToolResultsGemini ( contents , tracker )
170-
171- if ( newCount > 0 ) {
172- const prevBucket = Math . floor ( prevCount / freq )
173- const newBucket = Math . floor ( tracker . toolResultCount / freq )
174- if ( newBucket > prevBucket ) {
175- return appendNudgeGemini ( contents , nudgeText )
176- }
177- }
178- return false
179- }
180-
181- function appendNudgeGemini ( contents : any [ ] , nudgeText : string ) : boolean {
182- contents . push ( {
183- role : 'user' ,
184- parts : [ { text : nudgeText } ]
185- } )
186- return true
132+ export function injectNudgeGemini ( contents : any [ ] , tracker : ToolTracker , nudgeText : string , freq : number ) : boolean {
133+ return injectNudgeCore ( contents , tracker , nudgeText , freq , geminiAdapter )
187134}
188135
189136export function injectSynthGemini ( contents : any [ ] , instruction : string ) : boolean {
190- // Find the last user content that is not ignored
191137 for ( let i = contents . length - 1 ; i >= 0 ; i -- ) {
192138 const content = contents [ i ]
193139 if ( content . role === 'user' && Array . isArray ( content . parts ) ) {
194- // Check if already injected
195140 const alreadyInjected = content . parts . some (
196141 ( part : any ) => part ?. text && typeof part . text === 'string' && part . text . includes ( instruction )
197142 )
198- if ( alreadyInjected ) {
199- return false
200- }
143+ if ( alreadyInjected ) return false
201144 content . parts . push ( { text : instruction } )
202145 return true
203146 }
@@ -209,77 +152,43 @@ export function injectSynthGemini(contents: any[], instruction: string): boolean
209152// OpenAI Responses API Format (body.input with type-based items)
210153// ============================================================================
211154
212- function countToolResultsResponses ( input : any [ ] , tracker : ToolTracker ) : number {
213- let newCount = 0
214-
215- for ( const item of input ) {
216- if ( item . type === 'function_call_output' && item . call_id ) {
217- const id = String ( item . call_id ) . toLowerCase ( )
218- if ( ! tracker . seenToolResultIds . has ( id ) ) {
219- tracker . seenToolResultIds . add ( id )
220- newCount ++
155+ const responsesAdapter : MessageFormatAdapter = {
156+ countToolResults ( input , tracker ) {
157+ let newCount = 0
158+ for ( const item of input ) {
159+ if ( item . type === 'function_call_output' && item . call_id ) {
160+ const id = String ( item . call_id ) . toLowerCase ( )
161+ if ( ! tracker . seenToolResultIds . has ( id ) ) {
162+ tracker . seenToolResultIds . add ( id )
163+ newCount ++
164+ }
221165 }
222166 }
167+ tracker . toolResultCount += newCount
168+ return newCount
169+ } ,
170+ appendNudge ( input , nudgeText ) {
171+ input . push ( { type : 'message' , role : 'user' , content : nudgeText } )
223172 }
224-
225- tracker . toolResultCount += newCount
226- return newCount
227- }
228-
229- /**
230- * Counts new tool results and injects nudge instruction every N tool results (Responses API format).
231- * Returns true if injection happened.
232- */
233- export function injectNudgeResponses (
234- input : any [ ] ,
235- tracker : ToolTracker ,
236- nudgeText : string ,
237- freq : number
238- ) : boolean {
239- const prevCount = tracker . toolResultCount
240- const newCount = countToolResultsResponses ( input , tracker )
241-
242- if ( newCount > 0 ) {
243- const prevBucket = Math . floor ( prevCount / freq )
244- const newBucket = Math . floor ( tracker . toolResultCount / freq )
245- if ( newBucket > prevBucket ) {
246- return appendNudgeResponses ( input , nudgeText )
247- }
248- }
249- return false
250173}
251174
252- function appendNudgeResponses ( input : any [ ] , nudgeText : string ) : boolean {
253- input . push ( {
254- type : 'message' ,
255- role : 'user' ,
256- content : nudgeText
257- } )
258- return true
175+ export function injectNudgeResponses ( input : any [ ] , tracker : ToolTracker , nudgeText : string , freq : number ) : boolean {
176+ return injectNudgeCore ( input , tracker , nudgeText , freq , responsesAdapter )
259177}
260178
261179export function injectSynthResponses ( input : any [ ] , instruction : string ) : boolean {
262- // Find the last user message in the input array
263180 for ( let i = input . length - 1 ; i >= 0 ; i -- ) {
264181 const item = input [ i ]
265182 if ( item . type === 'message' && item . role === 'user' ) {
266- // Check if already injected
267183 if ( typeof item . content === 'string' ) {
268- if ( item . content . includes ( instruction ) ) {
269- return false
270- }
184+ if ( item . content . includes ( instruction ) ) return false
271185 item . content = item . content + '\n\n' + instruction
272186 } else if ( Array . isArray ( item . content ) ) {
273187 const alreadyInjected = item . content . some (
274188 ( part : any ) => part ?. type === 'input_text' && typeof part . text === 'string' && part . text . includes ( instruction )
275189 )
276- if ( alreadyInjected ) {
277- return false
278- }
279- item . content . push ( {
280- type : 'input_text' ,
281- text : instruction
282- } )
190+ if ( alreadyInjected ) return false
191+ item . content . push ( { type : 'input_text' , text : instruction } )
283192 }
284193 return true
285194 }
0 commit comments