@@ -84,6 +84,53 @@ export class ToolExecutor {
8484 } ;
8585 }
8686
87+ /**
88+ * Generate a stable signature for a tool call based on its name and parameters
89+ * Used for semantic duplicate detection
90+ */
91+ private generateToolCallSignature ( toolName : string , input : any ) : string {
92+ try {
93+ // Create a stable string representation by sorting keys
94+ const sortedInput = JSON . stringify ( input , Object . keys ( input || { } ) . sort ( ) ) ;
95+ return `${ toolName } ::${ sortedInput } ` ;
96+ } catch ( error ) {
97+ this . logger . error ( 'Failed to generate tool call signature' , {
98+ error : error instanceof Error ? error . message : String ( error ) ,
99+ toolName,
100+ } ) ;
101+ // Fallback to basic signature
102+ return `${ toolName } ::${ JSON . stringify ( input ) } ` ;
103+ }
104+ }
105+
106+ /**
107+ * Check if a tool call is a duplicate of recent calls
108+ * Returns the count of consecutive duplicates (0 if not a duplicate)
109+ */
110+ private checkForDuplicate (
111+ toolCall : any ,
112+ recentSignatures : string [ ]
113+ ) : { isDuplicate : boolean ; consecutiveCount : number } {
114+ const newSignature = this . generateToolCallSignature ( toolCall . toolName , toolCall . input ) ;
115+
116+ // Check if the most recent signature matches (consecutive duplicate)
117+ if ( recentSignatures . length > 0 && recentSignatures [ 0 ] === newSignature ) {
118+ // Count how many times this same signature appears consecutively
119+ let consecutiveCount = 0 ;
120+ for ( const sig of recentSignatures ) {
121+ if ( sig === newSignature ) {
122+ consecutiveCount ++ ;
123+ } else {
124+ break ; // Stop at first non-matching signature
125+ }
126+ }
127+
128+ return { isDuplicate : true , consecutiveCount } ;
129+ }
130+
131+ return { isDuplicate : false , consecutiveCount : 0 } ;
132+ }
133+
87134 /**
88135 * Parse tool calls from XML format (fallback for when Bedrock doesn't recognize tools)
89136 */
@@ -153,21 +200,120 @@ export class ToolExecutor {
153200 context ?: any [ ] ;
154201 threadId ?: string ;
155202 runId ?: string ;
156- }
203+ } ,
204+ recentToolSignatures ?: string [ ]
157205 ) : Promise < {
158206 toolResults : Record < string , any > ;
159207 toolResultMessage ?: any ;
160208 shouldContinue : boolean ;
161209 isClientTools : boolean ;
210+ updatedSignatures ?: string [ ] ;
211+ duplicateDetected ?: boolean ;
162212 } > {
163213 const toolResults : Record < string , any > = { } ;
214+ const signatures = recentToolSignatures || [ ] ;
164215
165216 this . logger . info ( 'Executing tools' , {
166217 toolCallsCount : toolCalls . length ,
167218 toolNames : toolCalls . map ( ( tc ) => tc . toolName ) ,
168219 toolIds : toolCalls . map ( ( tc ) => tc . toolUseId ) ,
220+ recentSignaturesCount : signatures . length ,
169221 } ) ;
170222
223+ // Check for duplicates in the first tool call
224+ // Only check the first one since that's what matters for the loop detection
225+ if ( toolCalls . length > 0 && signatures . length > 0 ) {
226+ const firstToolCall = toolCalls [ 0 ] ;
227+ const duplicateCheck = this . checkForDuplicate ( firstToolCall , signatures ) ;
228+
229+ if ( duplicateCheck . isDuplicate ) {
230+ const duplicateCount = duplicateCheck . consecutiveCount + 1 ; // +1 for this call
231+ this . logger . warn ( 'Duplicate tool call detected' , {
232+ toolName : firstToolCall . toolName ,
233+ consecutiveCount : duplicateCount ,
234+ toolUseId : firstToolCall . toolUseId ,
235+ } ) ;
236+
237+ // If we've hit the threshold (3 consecutive duplicates), warn the LLM
238+ if ( duplicateCount >= 3 ) {
239+ this . logger . warn ( 'Duplicate threshold reached - injecting warning to LLM' , {
240+ duplicateCount,
241+ toolName : firstToolCall . toolName ,
242+ } ) ;
243+
244+ // Emit Prometheus metric
245+ const metricsEmitter = getPrometheusMetricsEmitter ( ) ;
246+ metricsEmitter . emitCounter ( 'react_agent_duplicate_tool_calls_total' , 1 , {
247+ agent_type : 'react' ,
248+ tool_name : firstToolCall . toolName ,
249+ } ) ;
250+
251+ // Create a warning message for the LLM
252+ const warningResult = {
253+ duplicate_detected : true ,
254+ consecutive_count : duplicateCount ,
255+ message : `⚠️ You have called the tool "${ firstToolCall . toolName } " with identical parameters ${ duplicateCount } times in a row. The results are not changing.` ,
256+ suggestion :
257+ 'Please synthesize the information from your previous tool calls to provide a final answer, or try using different parameters if you need additional information.' ,
258+ note :
259+ 'Repeatedly calling the same tool with the same parameters will not yield different results.' ,
260+ } ;
261+
262+ toolResults [ firstToolCall . toolUseId ] = warningResult ;
263+
264+ // Still create the tool result message for proper conversation flow
265+ const toolResultMessage = {
266+ role : 'user' as const ,
267+ content : [
268+ {
269+ toolResult : {
270+ toolUseId : firstToolCall . toolUseId ,
271+ content : [ { text : JSON . stringify ( warningResult , null , 2 ) } ] ,
272+ } ,
273+ } ,
274+ ] ,
275+ } ;
276+
277+ // Update signatures with this duplicate
278+ const newSignature = this . generateToolCallSignature (
279+ firstToolCall . toolName ,
280+ firstToolCall . input
281+ ) ;
282+ const updatedSignatures = [ newSignature , ...signatures ] . slice ( 0 , 10 ) ; // Keep last 10
283+
284+ // Emergency stop if we've hit 10 consecutive duplicates
285+ if ( duplicateCount >= 10 ) {
286+ this . logger . error ( 'Emergency stop: 10+ consecutive duplicate tool calls' , {
287+ toolName : firstToolCall . toolName ,
288+ } ) ;
289+
290+ streamingCallbacks ?. onError ?.(
291+ 'The agent appears stuck in a loop. Ending conversation for safety.'
292+ ) ;
293+
294+ return {
295+ toolResults,
296+ toolResultMessage,
297+ shouldContinue : false , // Force stop
298+ isClientTools : false ,
299+ updatedSignatures,
300+ duplicateDetected : true ,
301+ } ;
302+ }
303+
304+ // Return with warning but keep conversation alive
305+ return {
306+ toolResults,
307+ toolResultMessage,
308+ shouldContinue : true , // Let LLM handle it
309+ isClientTools : false ,
310+ updatedSignatures,
311+ duplicateDetected : true ,
312+ } ;
313+ }
314+ }
315+ }
316+
171317 // Check if we've already executed these exact tool calls to prevent duplicates
172318 const toolCallSignatures = toolCalls . map ( ( tc ) => tc . toolUseId ) ;
173319
@@ -198,6 +344,8 @@ export class ToolExecutor {
198344 toolResults : { } ,
199345 shouldContinue : false ,
200346 isClientTools : false ,
347+ updatedSignatures : signatures ,
348+ duplicateDetected : false ,
201349 } ;
202350 }
203351
@@ -223,10 +371,19 @@ export class ToolExecutor {
223371
224372 // For client tools, we need to return an empty tool result message
225373 // This signals the completion of this request, client will send results in next request
374+
375+ // Update signatures even for client tools
376+ const newSignatures = clientToolCalls . map ( ( tc ) =>
377+ this . generateToolCallSignature ( tc . toolName , tc . input )
378+ ) ;
379+ const updatedSignatures = [ ...newSignatures , ...signatures ] . slice ( 0 , 10 ) ;
380+
226381 return {
227382 toolResults : { } ,
228383 shouldContinue : false , // Stop here for client execution
229384 isClientTools : true ,
385+ updatedSignatures,
386+ duplicateDetected : false ,
230387 } ;
231388 }
232389
@@ -324,6 +481,8 @@ export class ToolExecutor {
324481 toolResults : { } ,
325482 shouldContinue : false ,
326483 isClientTools : false ,
484+ updatedSignatures : signatures ,
485+ duplicateDetected : false ,
327486 } ;
328487 }
329488
@@ -332,11 +491,19 @@ export class ToolExecutor {
332491 content : toolResultContent ,
333492 } ;
334493
494+ // Update signatures with executed tool calls
495+ const newSignatures = mcpToolCalls . map ( ( tc ) =>
496+ this . generateToolCallSignature ( tc . toolName , tc . input )
497+ ) ;
498+ const updatedSignatures = [ ...newSignatures , ...signatures ] . slice ( 0 , 10 ) ; // Keep last 10
499+
335500 return {
336501 toolResults,
337502 toolResultMessage,
338503 shouldContinue : true ,
339504 isClientTools : false ,
505+ updatedSignatures,
506+ duplicateDetected : false ,
340507 } ;
341508 }
342509
0 commit comments