Merge sessions/fowler/9b17499a

This commit is contained in:
Dhanji R. Prasanna
2026-01-12 10:15:19 +05:30
2 changed files with 71 additions and 55 deletions

View File

@@ -48,8 +48,6 @@ use anyhow::Result;
use g3_config::Config; use g3_config::Config;
use g3_providers::{CacheControl, CompletionRequest, Message, MessageRole, ProviderRegistry}; use g3_providers::{CacheControl, CompletionRequest, Message, MessageRole, ProviderRegistry};
use prompts::{get_system_prompt_for_native, SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE}; use prompts::{get_system_prompt_for_native, SYSTEM_PROMPT_FOR_NON_NATIVE_TOOL_USE};
#[allow(unused_imports)]
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -1034,11 +1032,6 @@ impl<W: UiWriter> Agent<W> {
/// Check if a tool call is a duplicate of the last tool call in the previous assistant message. /// Check if a tool call is a duplicate of the last tool call in the previous assistant message.
/// Returns Some("DUP IN MSG") if it's a duplicate, None otherwise. /// Returns Some("DUP IN MSG") if it's a duplicate, None otherwise.
fn check_duplicate_in_previous_message(&self, tool_call: &ToolCall) -> Option<String> { fn check_duplicate_in_previous_message(&self, tool_call: &ToolCall) -> Option<String> {
// Helper to check if two tool calls are duplicates
let are_duplicates = |tc1: &ToolCall, tc2: &ToolCall| -> bool {
tc1.tool == tc2.tool && tc1.args == tc2.args
};
// Find the most recent assistant message // Find the most recent assistant message
for msg in self.context_window.conversation_history.iter().rev() { for msg in self.context_window.conversation_history.iter().rev() {
if !matches!(msg.role, MessageRole::Assistant) { if !matches!(msg.role, MessageRole::Assistant) {
@@ -1065,7 +1058,7 @@ impl<W: UiWriter> Agent<W> {
// Parse and compare the tool call // Parse and compare the tool call
if let Ok(prev_tool) = serde_json::from_str::<ToolCall>(tool_json) { if let Ok(prev_tool) = serde_json::from_str::<ToolCall>(tool_json) {
if are_duplicates(&prev_tool, tool_call) { if streaming::are_tool_calls_duplicate(&prev_tool, tool_call) {
return Some("DUP IN MSG".to_string()); return Some("DUP IN MSG".to_string());
} }
} }
@@ -2016,11 +2009,6 @@ impl<W: UiWriter> Agent<W> {
// Always process all tool calls - they will be executed after stream ends // Always process all tool calls - they will be executed after stream ends
let tools_to_process: Vec<ToolCall> = completed_tools; let tools_to_process: Vec<ToolCall> = completed_tools;
// Helper function to check if two tool calls are duplicates
let are_duplicates = |tc1: &ToolCall, tc2: &ToolCall| -> bool {
tc1.tool == tc2.tool && tc1.args == tc2.args
};
// De-duplicate tool calls and track duplicates // De-duplicate tool calls and track duplicates
let mut last_tool_in_chunk: Option<ToolCall> = None; let mut last_tool_in_chunk: Option<ToolCall> = None;
let mut deduplicated_tools: Vec<(ToolCall, Option<String>)> = Vec::new(); let mut deduplicated_tools: Vec<(ToolCall, Option<String>)> = Vec::new();
@@ -2031,7 +2019,7 @@ impl<W: UiWriter> Agent<W> {
// Check for IMMEDIATELY SEQUENTIAL duplicate in current chunk // Check for IMMEDIATELY SEQUENTIAL duplicate in current chunk
// Only the immediately previous tool call counts as a duplicate // Only the immediately previous tool call counts as a duplicate
if let Some(ref last_tool) = last_tool_in_chunk { if let Some(ref last_tool) = last_tool_in_chunk {
if are_duplicates(last_tool, &tool_call) { if streaming::are_tool_calls_duplicate(last_tool, &tool_call) {
duplicate_type = Some("DUP IN CHUNK".to_string()); duplicate_type = Some("DUP IN CHUNK".to_string());
} }
} else { } else {
@@ -2285,7 +2273,7 @@ impl<W: UiWriter> Agent<W> {
// Closure marker with timing // Closure marker with timing
let tokens_delta = self.context_window.used_tokens.saturating_sub(tokens_before); let tokens_delta = self.context_window.used_tokens.saturating_sub(tokens_before);
self.ui_writer self.ui_writer
.print_tool_timing(&Self::format_duration(exec_duration), .print_tool_timing(&streaming::format_duration(exec_duration),
tokens_delta, tokens_delta,
self.context_window.percentage_used()); self.context_window.percentage_used());
self.ui_writer.print_agent_prompt(); self.ui_writer.print_agent_prompt();
@@ -2480,7 +2468,7 @@ impl<W: UiWriter> Agent<W> {
// Add timing if needed // Add timing if needed
let final_response = if show_timing { let final_response = if show_timing {
let turn_tokens = turn_accumulated_usage.as_ref().map(|u| u.total_tokens); let turn_tokens = turn_accumulated_usage.as_ref().map(|u| u.total_tokens);
let timing_footer = Self::format_timing_footer( let timing_footer = streaming::format_timing_footer(
stream_start.elapsed(), stream_start.elapsed(),
_ttft, _ttft,
turn_tokens, turn_tokens,
@@ -2748,7 +2736,7 @@ impl<W: UiWriter> Agent<W> {
// Add timing if needed // Add timing if needed
let final_response = if show_timing { let final_response = if show_timing {
let turn_tokens = turn_accumulated_usage.as_ref().map(|u| u.total_tokens); let turn_tokens = turn_accumulated_usage.as_ref().map(|u| u.total_tokens);
let timing_footer = Self::format_timing_footer( let timing_footer = streaming::format_timing_footer(
stream_start.elapsed(), stream_start.elapsed(),
_ttft, _ttft,
turn_tokens, turn_tokens,
@@ -2778,7 +2766,7 @@ impl<W: UiWriter> Agent<W> {
// Add timing if needed // Add timing if needed
let final_response = if show_timing { let final_response = if show_timing {
let turn_tokens = turn_accumulated_usage.as_ref().map(|u| u.total_tokens); let turn_tokens = turn_accumulated_usage.as_ref().map(|u| u.total_tokens);
let timing_footer = Self::format_timing_footer( let timing_footer = streaming::format_timing_footer(
stream_start.elapsed(), stream_start.elapsed(),
_ttft, _ttft,
turn_tokens, turn_tokens,
@@ -2868,48 +2856,12 @@ impl<W: UiWriter> Agent<W> {
} }
fn format_duration(duration: Duration) -> String {
streaming::format_duration(duration)
}
fn format_timing_footer(
elapsed: Duration,
ttft: Duration,
turn_tokens: Option<u32>,
context_percentage: f32,
) -> String {
streaming::format_timing_footer(elapsed, ttft, turn_tokens, context_percentage)
}
} }
// Re-export utility functions // Re-export utility functions
pub use utils::apply_unified_diff_to_string; pub use utils::apply_unified_diff_to_string;
use utils::truncate_to_word_boundary;
/// Truncate a string to approximately max_len characters, ending at a word boundary
fn truncate_to_word_boundary(s: &str, max_len: usize) -> String {
let char_count = s.chars().count();
if char_count <= max_len {
return s.to_string();
}
// Get the byte index of the max_len-th character
let byte_index: usize = s.char_indices()
.nth(max_len)
.map(|(i, _)| i)
.unwrap_or(s.len());
// Find the last space before the character limit
let truncated = &s[..byte_index];
if let Some(last_space_byte) = truncated.rfind(' ') {
if truncated[..last_space_byte].chars().count() > max_len / 2 {
// Only use word boundary if it's not too short (in characters)
return format!("{}...", &s[..last_space_byte]);
}
}
// Fall back to truncation at character boundary
format!("{}...", truncated)
}
// Implement Drop to clean up safaridriver process // Implement Drop to clean up safaridriver process
impl<W: UiWriter> Drop for Agent<W> { impl<W: UiWriter> Drop for Agent<W> {

View File

@@ -1,6 +1,7 @@
//! Utility functions for diff parsing, shell escaping, and JSON fixing. //! Utility functions for diff parsing, shell escaping, and JSON fixing.
//! //!
//! This module contains helper functions used by the agent for: //! This module contains helper functions used by the agent for:
//! - String truncation utilities
//! - Applying unified diffs to strings //! - Applying unified diffs to strings
//! - Shell command escaping //! - Shell command escaping
//! - JSON quote fixing //! - JSON quote fixing
@@ -8,6 +9,42 @@
use anyhow::Result; use anyhow::Result;
use tracing::debug; use tracing::debug;
/// Truncate a string to approximately max_len characters, ending at a word boundary.
///
/// This function attempts to break at a space character for cleaner display.
/// If no suitable word boundary is found (or it would result in too short a string),
/// it falls back to character-based truncation.
///
/// # Arguments
/// * `s` - The string to truncate
/// * `max_len` - Maximum number of characters (approximate)
///
/// # Returns
/// The truncated string with "..." appended if truncation occurred
pub fn truncate_to_word_boundary(s: &str, max_len: usize) -> String {
let char_count = s.chars().count();
if char_count <= max_len {
return s.to_string();
}
// Get the byte index of the max_len-th character
let byte_index: usize = s.char_indices()
.nth(max_len)
.map(|(i, _)| i)
.unwrap_or(s.len());
// Find the last space before the character limit
let truncated = &s[..byte_index];
if let Some(last_space_byte) = truncated.rfind(' ') {
if truncated[..last_space_byte].chars().count() > max_len / 2 {
// Only use word boundary if it's not too short (in characters)
return format!("{}...", &s[..last_space_byte]);
}
}
// Fall back to truncation at character boundary
format!("{}...", truncated)
}
/// Normalize Unicode space characters in a file path to regular ASCII spaces. /// Normalize Unicode space characters in a file path to regular ASCII spaces.
/// ///
/// macOS uses special Unicode space characters in certain filenames: /// macOS uses special Unicode space characters in certain filenames:
@@ -604,4 +641,31 @@ mod tests {
let cmd = "cat \"/etc/hosts\""; let cmd = "cat \"/etc/hosts\"";
assert_eq!(resolve_paths_in_shell_command(cmd), cmd); assert_eq!(resolve_paths_in_shell_command(cmd), cmd);
} }
#[test]
fn truncate_to_word_boundary_short_string_unchanged() {
assert_eq!(truncate_to_word_boundary("hello", 10), "hello");
assert_eq!(truncate_to_word_boundary("hello world", 20), "hello world");
}
#[test]
fn truncate_to_word_boundary_breaks_at_space() {
// Should break at word boundary
let result = truncate_to_word_boundary("hello world this is a long string", 15);
assert_eq!(result, "hello world...");
}
#[test]
fn truncate_to_word_boundary_falls_back_to_char_limit() {
// When word boundary would be too short, fall back to char limit
let result = truncate_to_word_boundary("a verylongwordwithoutspaces", 10);
assert_eq!(result, "a verylong...");
}
#[test]
fn truncate_to_word_boundary_handles_unicode() {
// Should handle unicode characters correctly
let result = truncate_to_word_boundary("héllo wörld this is long", 12);
assert!(result.ends_with("..."));
}
} }