Skip to content

Commit f0bbcde

Browse files
committed
fix: prevent duplicate tool calls with full conversation history dedup (#154)
Three changes to eliminate wasted AI credits from duplicate tool call loops: 1. validateToolCall() now scans ALL previous tool calls in conversation history, not just the most recent one. Catches non-consecutive duplicates (e.g. upsert_event → other tool → upsert_event again). 2. Break out of tool call foreach when conversation is complete. Multi-handler safe: only breaks when ALL configured handlers fired. 3. Stronger duplicate rejection message that explicitly tells the AI not to call skip_item as a follow-up to a rejected duplicate. Impact: ~2,554 jobs (30% of agent_skipped) followed a pattern where the AI called upsert_event, got blocked on retry, then called skip_item with a verbose duplicate explanation. Each burned ~36k tokens for zero value (~93M tokens total waste).
1 parent d8bc06c commit f0bbcde

2 files changed

Lines changed: 25 additions & 26 deletions

File tree

inc/Engine/AI/AIConversationLoop.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,15 @@ public function execute(
330330
$turn_count
331331
);
332332
$messages[] = $tool_result_message;
333+
334+
// Break out of the foreach when conversation is complete.
335+
// This is multi-handler safe: $conversation_complete only becomes true
336+
// when ALL configured handlers have fired (or legacy single-handler mode).
337+
// Without this break, remaining tool calls in the same AI response batch
338+
// would still execute, wasting credits on duplicate/unnecessary calls.
339+
if ( $conversation_complete ) {
340+
break;
341+
}
333342
}
334343
} else {
335344
// No tool calls = conversation complete

inc/Engine/AI/ConversationManager.php

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,13 @@ public static function generateFailureMessage( string $tool_name, string $error_
282282
}
283283

284284
/**
285-
* Validate if a tool call is a duplicate of the previous tool call.
285+
* Validate if a tool call is a duplicate of any previous tool call in the conversation.
286+
*
287+
* Scans the ENTIRE conversation history (not just the most recent tool call)
288+
* to catch non-consecutive duplicates. For example: AI calls upsert_event(A),
289+
* then some other tool, then upsert_event(A) again — the old logic only checked
290+
* the immediately previous call and would miss this. This broader check prevents
291+
* wasted AI credits on duplicate tool executions.
286292
*
287293
* @param string $tool_name Tool name to validate
288294
* @param array $tool_parameters Tool parameters to validate
@@ -297,7 +303,7 @@ public static function validateToolCall( string $tool_name, array $tool_paramete
297303
);
298304
}
299305

300-
$previous_tool_call = null;
306+
// Scan ALL previous tool_call messages, not just the most recent one.
301307
for ( $i = count( $conversation_messages ) - 1; $i >= 0; $i-- ) {
302308
$message = $conversation_messages[ $i ];
303309

@@ -316,29 +322,13 @@ public static function validateToolCall( string $tool_name, array $tool_paramete
316322
continue;
317323
}
318324

319-
$previous_tool_call = array(
320-
'tool_name' => $prev_tool_name,
321-
'parameters' => $prev_parameters,
322-
);
323-
break;
324-
}
325-
326-
if ( ! $previous_tool_call ) {
327-
return array(
328-
'is_duplicate' => false,
329-
'message' => '',
330-
);
331-
}
332-
333-
$is_duplicate = ( $previous_tool_call['tool_name'] === $tool_name ) &&
334-
( $previous_tool_call['parameters'] === $tool_parameters );
335-
336-
if ( $is_duplicate ) {
337-
$correction_message = "You just called the {$tool_name} tool with the exact same parameters as your previous action. Please try a different approach or use different parameters instead.";
338-
return array(
339-
'is_duplicate' => true,
340-
'message' => $correction_message,
341-
);
325+
if ( $prev_tool_name === $tool_name && $prev_parameters === $tool_parameters ) {
326+
$correction_message = "You already called the {$tool_name} tool with these exact parameters earlier in this conversation. That call already executed successfully. Do not retry — move on to the next step or end the conversation.";
327+
return array(
328+
'is_duplicate' => true,
329+
'message' => $correction_message,
330+
);
331+
}
342332
}
343333

344334
return array(
@@ -418,7 +408,7 @@ public static function extractToolCallFromMessage( array $message ): ?array {
418408
public static function generateDuplicateToolCallMessage( string $tool_name, int $turn_count = 0 ): array {
419409
$tool_result = array(
420410
'success' => false,
421-
'error' => 'Duplicate tool call - same parameters as previous action. Try a different approach.',
411+
'error' => "DUPLICATE REJECTED: You already called {$tool_name} with these exact parameters earlier in this conversation and it succeeded. Do NOT call it again. Do NOT call skip_item about this. The task is done — end the conversation.",
422412
);
423413

424414
return self::formatToolResultMessage( $tool_name, $tool_result, array(), false, $turn_count );

0 commit comments

Comments
 (0)