duplicate tool call bugfix

This commit is contained in:
Dhanji R. Prasanna
2025-12-19 15:24:03 +11:00
parent faa6512b1f
commit b4f6da6bf2
2 changed files with 62 additions and 26 deletions

View File

@@ -244,13 +244,19 @@ impl StreamingToolParser {
self.text_buffer.push_str(&chunk.content);
}
// Handle native tool calls
// Handle native tool calls - return them immediately when received
// This allows tools to be executed as soon as they're fully parsed,
// preventing duplicate tool calls from being accumulated
if let Some(ref tool_calls) = chunk.tool_calls {
debug!("Received native tool calls: {:?}", tool_calls);
// Accumulate native tool calls
// Convert and return tool calls immediately
for tool_call in tool_calls {
self.native_tool_calls.push(tool_call.clone());
let converted_tool = ToolCall {
tool: tool_call.tool.clone(),
args: tool_call.args.clone(),
};
completed_tools.push(converted_tool);
}
}
@@ -260,25 +266,6 @@ impl StreamingToolParser {
debug!("Message finished, processing accumulated tool calls");
}
// If we have native tool calls and the message is stopped, return them
if self.message_stopped && !self.native_tool_calls.is_empty() {
debug!(
"Converting {} native tool calls",
self.native_tool_calls.len()
);
for native_tool in &self.native_tool_calls {
let converted_tool = ToolCall {
tool: native_tool.tool.clone(),
args: native_tool.args.clone(),
};
completed_tools.push(converted_tool);
}
// Clear native tool calls after processing
self.native_tool_calls.clear();
}
// Fallback: Try to parse JSON tool calls from text if no native tool calls
if completed_tools.is_empty() && !chunk.content.is_empty() {
if let Some(json_tool) = self.try_parse_json_tool_call(&chunk.content) {
@@ -3638,6 +3625,11 @@ impl<W: UiWriter> Agent<W> {
let mut iteration_count = 0;
const MAX_ITERATIONS: usize = 400; // Prevent infinite loops
let mut response_started = false;
let mut any_tool_executed = false; // Track if ANY tool was executed across all iterations
let mut auto_summary_attempts = 0; // Track auto-summary prompt attempts
const MAX_AUTO_SUMMARY_ATTEMPTS: usize = 2; // Limit auto-summary retries
let mut last_action_was_tool = false; // Track if the last action was a tool call (vs text response)
let mut any_text_response = false; // Track if LLM ever provided a text response
// Check if we need to summarize before starting
if self.context_window.should_summarize() {
@@ -4369,6 +4361,8 @@ impl<W: UiWriter> Agent<W> {
// 2. At the end when no tools were executed (handled in the "no tool executed" branch)
tool_executed = true;
any_tool_executed = true; // Track across all iterations
last_action_was_tool = true; // Last action was a tool call
// Reset the JSON tool call filter state after each tool execution
// This ensures the filter doesn't stay in suppression mode for subsequent streaming content
@@ -4416,6 +4410,8 @@ impl<W: UiWriter> Agent<W> {
self.ui_writer.print_agent_response(&filtered_content);
self.ui_writer.flush();
current_response.push_str(&filtered_content);
last_action_was_tool = false; // Text response received
any_text_response = true;
}
}
}
@@ -4672,10 +4668,48 @@ impl<W: UiWriter> Agent<W> {
let has_response = !current_response.is_empty() || !full_response.is_empty();
if !has_response {
warn!(
"Loop exited without any response after {} iterations",
iteration_count
);
if any_tool_executed && last_action_was_tool && !any_text_response {
// Only auto-prompt for summary if:
// 1. Tools were executed in previous iterations
// 2. The last action was a tool call (not a text response)
// 3. No text response was ever provided by the LLM
if auto_summary_attempts < MAX_AUTO_SUMMARY_ATTEMPTS {
// Auto-prompt for a summary by adding a follow-up message
auto_summary_attempts += 1;
warn!(
"LLM stopped without final response after executing tools ({} iterations, auto-summary attempt {})",
iteration_count, auto_summary_attempts
);
self.ui_writer.print_context_status(
"\n🔄 Model stopped without response. Auto-prompting for summary...\n"
);
// Add a follow-up message asking for summary
let summary_prompt = Message::new(
MessageRole::User,
"Please provide a brief summary of what was accomplished and any next steps.".to_string(),
);
self.context_window.add_message(summary_prompt);
request.messages = self.context_window.conversation_history.clone();
// Continue the loop to get the summary
continue;
} else {
// Max auto-summary attempts reached, give up gracefully
warn!(
"Max auto-summary attempts ({}) reached, returning without summary",
MAX_AUTO_SUMMARY_ATTEMPTS
);
self.ui_writer.print_agent_response(
"\n⚠️ The model stopped without providing a final response after multiple attempts.\n"
);
}
} else {
warn!(
"Loop exited without any response after {} iterations",
iteration_count
);
}
} else {
// Only set full_response if it's empty (first iteration without tools)
// This prevents duplication when the agent responds without calling final_output

View File

@@ -607,6 +607,8 @@ impl AnthropicProvider {
debug!("Receiver dropped, stopping stream");
return accumulated_usage;
}
// Clear tool calls after sending to prevent duplicates at message_stop
current_tool_calls.clear();
}
}
"message_stop" => {