fixed filtering and tool call timeouts

This commit is contained in:
Dhanji Prasanna
2025-10-15 10:18:20 +11:00
parent befc55152d
commit fb64b7fe32
5 changed files with 53 additions and 179 deletions

View File

@@ -14,6 +14,7 @@ thread_local! {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
struct CorrectJsonToolState { struct CorrectJsonToolState {
suppression_mode: bool, suppression_mode: bool,
brace_depth: i32, brace_depth: i32,
@@ -22,7 +23,8 @@ struct CorrectJsonToolState {
} }
impl CorrectJsonToolState { impl CorrectJsonToolState {
fn new() -> Self { #[allow(dead_code)]
fn new() -> Self {
Self { Self {
suppression_mode: false, suppression_mode: false,
brace_depth: 0, brace_depth: 0,
@@ -31,7 +33,8 @@ impl CorrectJsonToolState {
} }
} }
fn reset(&mut self) { #[allow(dead_code)]
fn reset(&mut self) {
self.suppression_mode = false; self.suppression_mode = false;
self.brace_depth = 0; self.brace_depth = 0;
self.buffer.clear(); self.buffer.clear();
@@ -40,6 +43,7 @@ impl CorrectJsonToolState {
} }
// Correct implementation according to specification // Correct implementation according to specification
#[allow(dead_code)]
pub fn correct_filter_json_tool_calls(content: &str) -> String { pub fn correct_filter_json_tool_calls(content: &str) -> String {
CORRECT_JSON_TOOL_STATE.with(|state| { CORRECT_JSON_TOOL_STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
@@ -132,6 +136,7 @@ pub fn correct_filter_json_tool_calls(content: &str) -> String {
// Helper function to extract content with JSON tool call filtered out // Helper function to extract content with JSON tool call filtered out
// Returns everything except the JSON between the first '{' and last '}' (inclusive) // Returns everything except the JSON between the first '{' and last '}' (inclusive)
#[allow(dead_code)]
fn extract_content_without_json(full_content: &str, json_start: usize) -> String { fn extract_content_without_json(full_content: &str, json_start: usize) -> String {
// Find the end of the JSON using proper brace counting with string handling // Find the end of the JSON using proper brace counting with string handling
let mut brace_depth = 0; let mut brace_depth = 0;
@@ -174,6 +179,7 @@ fn extract_content_without_json(full_content: &str, json_start: usize) -> String
} }
// Reset function for testing // Reset function for testing
#[allow(dead_code)]
pub fn reset_correct_json_tool_state() { pub fn reset_correct_json_tool_state() {
CORRECT_JSON_TOOL_STATE.with(|state| { CORRECT_JSON_TOOL_STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();

View File

@@ -181,6 +181,7 @@ fn extract_final_content(full_content: &str, json_start: usize) -> String {
} }
// Reset function for testing // Reset function for testing
#[allow(dead_code)]
pub fn reset_final_json_tool_state() { pub fn reset_final_json_tool_state() {
FINAL_JSON_TOOL_STATE.with(|state| { FINAL_JSON_TOOL_STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();

View File

@@ -14,6 +14,7 @@ thread_local! {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
struct FixedJsonToolState { struct FixedJsonToolState {
suppression_mode: bool, suppression_mode: bool,
brace_depth: i32, brace_depth: i32,
@@ -23,7 +24,8 @@ struct FixedJsonToolState {
} }
impl FixedJsonToolState { impl FixedJsonToolState {
fn new() -> Self { #[allow(dead_code)]
fn new() -> Self {
Self { Self {
suppression_mode: false, suppression_mode: false,
brace_depth: 0, brace_depth: 0,
@@ -33,7 +35,8 @@ impl FixedJsonToolState {
} }
} }
fn reset(&mut self) { #[allow(dead_code)]
fn reset(&mut self) {
self.suppression_mode = false; self.suppression_mode = false;
self.brace_depth = 0; self.brace_depth = 0;
self.buffer.clear(); self.buffer.clear();
@@ -43,6 +46,7 @@ impl FixedJsonToolState {
} }
// FINAL CORRECTED implementation according to specification // FINAL CORRECTED implementation according to specification
#[allow(dead_code)]
pub fn fixed_filter_json_tool_calls(content: &str) -> String { pub fn fixed_filter_json_tool_calls(content: &str) -> String {
if content.is_empty() { if content.is_empty() {
return String::new(); return String::new();
@@ -162,6 +166,7 @@ pub fn fixed_filter_json_tool_calls(content: &str) -> String {
// Helper function to extract content with JSON tool call filtered out // Helper function to extract content with JSON tool call filtered out
// Returns everything except the JSON between the first '{' and last '}' (inclusive) // Returns everything except the JSON between the first '{' and last '}' (inclusive)
#[allow(dead_code)]
fn extract_fixed_content(full_content: &str, json_start: usize) -> String { fn extract_fixed_content(full_content: &str, json_start: usize) -> String {
// Find the end of the JSON using proper brace counting with string handling // Find the end of the JSON using proper brace counting with string handling
let mut brace_depth = 0; let mut brace_depth = 0;
@@ -204,6 +209,7 @@ fn extract_fixed_content(full_content: &str, json_start: usize) -> String {
} }
// Reset function for testing // Reset function for testing
#[allow(dead_code)]
pub fn reset_fixed_json_tool_state() { pub fn reset_fixed_json_tool_state() {
FIXED_JSON_TOOL_STATE.with(|state| { FIXED_JSON_TOOL_STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();

View File

@@ -32,6 +32,7 @@ use anyhow::Result;
use g3_config::Config; use g3_config::Config;
use g3_execution::CodeExecutor; use g3_execution::CodeExecutor;
use g3_providers::{CompletionRequest, Message, MessageRole, ProviderRegistry, Tool}; use g3_providers::{CompletionRequest, Message, MessageRole, ProviderRegistry, Tool};
#[allow(unused_imports)]
use regex::Regex; use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::json; use serde_json::json;
@@ -755,31 +756,31 @@ Do not explain what you're going to do - just do it by calling the tools.
When you need to execute a tool, write ONLY the JSON tool call on a new line: When you need to execute a tool, write ONLY the JSON tool call on a new line:
{\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}} {\"tool\": \"tool_name\", \"args\": {\"param\": \"value\"}
The tool will execute immediately and you'll receive the result (success or error) to continue with. The tool will execute immediately and you'll receive the result (success or error) to continue with.
# Available Tools # Available Tools
- **shell**: Execute shell commands - **shell**: Execute shell commands
- Format: {\"tool\": \"shell\", \"args\": {\"command\": \"your_command_here\"}} - Format: {\"tool\": \"shell\", \"args\": {\"command\": \"your_command_here\"}
- Example: {\"tool\": \"shell\", \"args\": {\"command\": \"ls ~/Downloads\"}} - Example: {\"tool\": \"shell\", \"args\": {\"command\": \"ls ~/Downloads\"}
- **read_file**: Read the contents of a file (supports partial reads via start/end) - **read_file**: Read the contents of a file (supports partial reads via start/end)
- Format: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"path/to/file\", \"start\": 0, \"end\": 100}} - Format: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"path/to/file\", \"start\": 0, \"end\": 100}
- Example: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"src/main.rs\"}} - Example: {\"tool\": \"read_file\", \"args\": {\"file_path\": \"src/main.rs\"}
- Example (partial): {\"tool\": \"read_file\", \"args\": {\"file_path\": \"large.log\", \"start\": 0, \"end\": 1000}} - Example (partial): {\"tool\": \"read_file\", \"args\": {\"file_path\": \"large.log\", \"start\": 0, \"end\": 1000}
- **write_file**: Write content to a file (creates or overwrites) - **write_file**: Write content to a file (creates or overwrites)
- Format: {\"tool\": \"write_file\", \"args\": {\"file_path\": \"path/to/file\", \"content\": \"file content\"}} - Format: {\"tool\": \"write_file\", \"args\": {\"file_path\": \"path/to/file\", \"content\": \"file content\"}
- Example: {\"tool\": \"write_file\", \"args\": {\"file_path\": \"src/lib.rs\", \"content\": \"pub fn hello() {}\"}} - Example: {\"tool\": \"write_file\", \"args\": {\"file_path\": \"src/lib.rs\", \"content\": \"pub fn hello() {}\"}
- **str_replace**: Replace text in a file using a diff - **str_replace**: Replace text in a file using a diff
- Format: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"path/to/file\", \"diff\": \"--- old\\n-old text\\n+++ new\\n+new text\"}} - Format: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"path/to/file\", \"diff\": \"--- old\\n-old text\\n+++ new\\n+new text\"}
- Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-old_code();\\n+++ new\\n+new_code();\"}} - Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-old_code();\\n+++ new\\n+new_code();\"}
- **final_output**: Signal task completion with a detailed summary of work done in markdown format - **final_output**: Signal task completion with a detailed summary of work done in markdown format
- Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}} - Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}
# Instructions # Instructions
@@ -1507,7 +1508,17 @@ The tool will execute immediately and you'll receive the result (success or erro
} }
let exec_start = Instant::now(); let exec_start = Instant::now();
let tool_result = self.execute_tool(&tool_call).await?; // Add 8-minute timeout for tool execution
let tool_result = match tokio::time::timeout(
Duration::from_secs(8 * 60), // 8 minutes
self.execute_tool(&tool_call)
).await {
Ok(result) => result?,
Err(_) => {
warn!("Tool call {} timed out after 8 minutes", tool_call.tool);
format!("❌ Tool execution timed out after 8 minutes")
}
};
let exec_duration = exec_start.elapsed(); let exec_duration = exec_start.elapsed();
total_execution_time += exec_duration; total_execution_time += exec_duration;
@@ -2380,167 +2391,11 @@ The tool will execute immediately and you'll receive the result (success or erro
} }
} }
use std::cell::RefCell; // Helper function to filter JSON tool calls from display content (unused)
#[allow(dead_code)]
// Thread-local state for tracking JSON tool call suppression
thread_local! {
static JSON_TOOL_STATE: RefCell<JsonToolState> = RefCell::new(JsonToolState::new());
}
#[derive(Debug, Clone)]
struct JsonToolState {
suppression_mode: bool,
brace_depth: i32,
buffer: String,
}
impl JsonToolState {
fn new() -> Self {
Self {
suppression_mode: false,
brace_depth: 0,
buffer: String::new(),
}
}
fn reset(&mut self) {
self.suppression_mode = false;
self.brace_depth = 0;
self.buffer.clear();
}
}
// Helper function to filter JSON tool calls from display content
fn filter_json_tool_calls(content: &str) -> String { fn filter_json_tool_calls(content: &str) -> String {
JSON_TOOL_STATE.with(|state| { // This function is no longer used - replaced by final_filter_json::final_filter_json_tool_calls
let mut state = state.borrow_mut(); content.to_string()
// If we're already in suppression mode, continue tracking
if state.suppression_mode {
// Add content to buffer for tracking
state.buffer.push_str(content);
// Count braces to track JSON nesting depth
for ch in content.chars() {
match ch {
'{' => state.brace_depth += 1,
'}' => {
state.brace_depth -= 1;
// Exit suppression mode when we've closed all braces
if state.brace_depth <= 0 {
debug!("Exiting JSON tool suppression mode - completed JSON object");
state.reset();
// Check if there's any content after the JSON
if let Some(close_pos) = content.rfind('}') {
if close_pos + 1 < content.len() {
// Return any content after the JSON
return content[close_pos + 1..].to_string();
}
}
}
}
_ => {}
}
}
// While in suppression mode, return empty string
return String::new();
}
// Check if content contains any JSON tool call patterns
let patterns = [
r#"{"tool":"#,
r#"{"tool"#, // Partial pattern
r#"{"too"#, // Even more partial
r#"{"to"#, // Very partial
r#"{"t"#, // Extremely partial
r#"{ "tool":"#,
r#"{"tool" :"#,
r#"{ "tool" :"#,
r#"{"tool": "#, // Pattern with space after colon
r#"{ "tool": "#, // Pattern with spaces
];
// Check if any pattern is found in the content
for pattern in &patterns {
if let Some(pos) = content.find(pattern) {
debug!("Detected JSON tool call pattern '{}' at position {} - entering suppression mode", pattern, pos);
// Found a tool call pattern - enter suppression mode
state.suppression_mode = true;
state.brace_depth = 0;
state.buffer.clear();
state.buffer.push_str(&content[pos..]);
// Count braces in the remaining content after the pattern
for ch in content[pos..].chars() {
match ch {
'{' => state.brace_depth += 1,
'}' => {
state.brace_depth -= 1;
if state.brace_depth <= 0 {
debug!("JSON tool call completed in same chunk - exiting suppression mode");
state.reset();
break;
}
}
_ => {}
}
}
// Return any content before the JSON tool call
if pos > 0 {
return content[..pos].to_string();
} else {
return String::new();
}
}
}
// Check for partial JSON patterns that might be split across chunks
let trimmed = content.trim();
// Special case: single character chunks that might be part of a JSON tool call
if content.len() <= 3 && state.buffer.len() < 20 {
// Accumulate small chunks to check for patterns
state.buffer.push_str(content);
if state.buffer.contains(r#"{"tool"#) || state.buffer.contains(r#"{ "tool"#) {
state.suppression_mode = true;
state.brace_depth = state.buffer.chars().filter(|&c| c == '{').count() as i32;
return String::new();
}
}
// Check if this looks like the start of a JSON tool call (larger chunks)
let pattern = Regex::new(r#"\s*\{\s*"tool"\s*:"#).unwrap();
if pattern.is_match(trimmed) {
// This might be the start of a JSON tool call
// Enter suppression mode preemptively
debug!("Detected potential JSON tool call start - entering suppression mode");
state.suppression_mode = true;
state.brace_depth = 0;
state.buffer.clear();
state.buffer.push_str(content);
// Count braces
for ch in content.chars() {
match ch {
'{' => state.brace_depth += 1,
'}' => {
state.brace_depth -= 1;
if state.brace_depth <= 0 {
state.reset();
break;
}
}
_ => {}
}
}
return String::new();
}
// No JSON tool call detected, return content as-is
content.to_string()
})
} }
// Apply unified diff to an input string with optional [start, end) bounds // Apply unified diff to an input string with optional [start, end) bounds
@@ -2813,7 +2668,7 @@ fn shell_escape_command(command: &str) -> String {
fn fix_nested_quotes_in_shell_command(json_str: &str) -> String { fn fix_nested_quotes_in_shell_command(json_str: &str) -> String {
let mut _result = String::new(); let mut _result = String::new();
let _chars = json_str.chars().peekable(); let _chars = json_str.chars().peekable();
// Example: {"tool": "shell", "args": {"command": "python -c 'import os; print("hello")'"}} // Example: {"tool": "shell", "args": {"command": "python -c 'import os; print("hello")'"}
// Look for the pattern: "command": " // Look for the pattern: "command": "
if let Some(command_start) = json_str.find(r#""command": ""#) { if let Some(command_start) = json_str.find(r#""command": ""#) {

View File

@@ -11,6 +11,7 @@ thread_local! {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(dead_code)]
struct NewJsonToolState { struct NewJsonToolState {
suppression_mode: bool, suppression_mode: bool,
brace_depth: i32, brace_depth: i32,
@@ -19,7 +20,8 @@ struct NewJsonToolState {
} }
impl NewJsonToolState { impl NewJsonToolState {
fn new() -> Self { #[allow(dead_code)]
fn new() -> Self {
Self { Self {
suppression_mode: false, suppression_mode: false,
brace_depth: 0, brace_depth: 0,
@@ -28,7 +30,8 @@ impl NewJsonToolState {
} }
} }
fn reset(&mut self) { #[allow(dead_code)]
fn reset(&mut self) {
self.suppression_mode = false; self.suppression_mode = false;
self.brace_depth = 0; self.brace_depth = 0;
self.accumulated_content.clear(); self.accumulated_content.clear();
@@ -41,6 +44,7 @@ impl NewJsonToolState {
// 2. Enter suppression mode and use brace counting to find complete JSON // 2. Enter suppression mode and use brace counting to find complete JSON
// 3. Only elide JSON content between first '{' and last '}' (inclusive) // 3. Only elide JSON content between first '{' and last '}' (inclusive)
// 4. Return everything else as the final filtered string // 4. Return everything else as the final filtered string
#[allow(dead_code)]
pub fn new_filter_json_tool_calls(content: &str) -> String { pub fn new_filter_json_tool_calls(content: &str) -> String {
NEW_JSON_TOOL_STATE.with(|state| { NEW_JSON_TOOL_STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();
@@ -136,6 +140,7 @@ pub fn new_filter_json_tool_calls(content: &str) -> String {
// Helper function to extract content with JSON tool call filtered out // Helper function to extract content with JSON tool call filtered out
// Returns everything except the JSON between the first '{' and last '}' (inclusive) // Returns everything except the JSON between the first '{' and last '}' (inclusive)
#[allow(dead_code)]
fn extract_filtered_content(full_content: &str, json_start: usize) -> String { fn extract_filtered_content(full_content: &str, json_start: usize) -> String {
// Find the end of the JSON using proper brace counting // Find the end of the JSON using proper brace counting
let mut brace_depth = 0; let mut brace_depth = 0;
@@ -178,6 +183,7 @@ fn extract_filtered_content(full_content: &str, json_start: usize) -> String {
} }
// Reset function for testing // Reset function for testing
#[allow(dead_code)]
pub fn reset_new_json_tool_state() { pub fn reset_new_json_tool_state() {
NEW_JSON_TOOL_STATE.with(|state| { NEW_JSON_TOOL_STATE.with(|state| {
let mut state = state.borrow_mut(); let mut state = state.borrow_mut();