duplicate tool call bugfix
This commit is contained in:
@@ -244,13 +244,19 @@ impl StreamingToolParser {
|
|||||||
self.text_buffer.push_str(&chunk.content);
|
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 {
|
if let Some(ref tool_calls) = chunk.tool_calls {
|
||||||
debug!("Received native tool calls: {:?}", 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 {
|
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");
|
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
|
// Fallback: Try to parse JSON tool calls from text if no native tool calls
|
||||||
if completed_tools.is_empty() && !chunk.content.is_empty() {
|
if completed_tools.is_empty() && !chunk.content.is_empty() {
|
||||||
if let Some(json_tool) = self.try_parse_json_tool_call(&chunk.content) {
|
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;
|
let mut iteration_count = 0;
|
||||||
const MAX_ITERATIONS: usize = 400; // Prevent infinite loops
|
const MAX_ITERATIONS: usize = 400; // Prevent infinite loops
|
||||||
let mut response_started = false;
|
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
|
// Check if we need to summarize before starting
|
||||||
if self.context_window.should_summarize() {
|
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)
|
// 2. At the end when no tools were executed (handled in the "no tool executed" branch)
|
||||||
|
|
||||||
tool_executed = true;
|
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
|
// 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
|
// 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.print_agent_response(&filtered_content);
|
||||||
self.ui_writer.flush();
|
self.ui_writer.flush();
|
||||||
current_response.push_str(&filtered_content);
|
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();
|
let has_response = !current_response.is_empty() || !full_response.is_empty();
|
||||||
|
|
||||||
if !has_response {
|
if !has_response {
|
||||||
|
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!(
|
warn!(
|
||||||
"Loop exited without any response after {} iterations",
|
"Loop exited without any response after {} iterations",
|
||||||
iteration_count
|
iteration_count
|
||||||
);
|
);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Only set full_response if it's empty (first iteration without tools)
|
// Only set full_response if it's empty (first iteration without tools)
|
||||||
// This prevents duplication when the agent responds without calling final_output
|
// This prevents duplication when the agent responds without calling final_output
|
||||||
|
|||||||
@@ -607,6 +607,8 @@ impl AnthropicProvider {
|
|||||||
debug!("Receiver dropped, stopping stream");
|
debug!("Receiver dropped, stopping stream");
|
||||||
return accumulated_usage;
|
return accumulated_usage;
|
||||||
}
|
}
|
||||||
|
// Clear tool calls after sending to prevent duplicates at message_stop
|
||||||
|
current_tool_calls.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"message_stop" => {
|
"message_stop" => {
|
||||||
|
|||||||
Reference in New Issue
Block a user