Merge sessions/fowler/9b17499a
This commit is contained in:
@@ -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> {
|
||||||
|
|||||||
@@ -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("..."));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user