From 97b2c26fb736d10d1c607d5b7281a5ed5d842f13 Mon Sep 17 00:00:00 2001 From: "toyamagu2021@gmail.com" Date: Sun, 6 Jul 2025 19:35:20 +0900 Subject: [PATCH 1/4] fix: use safe_truncate for truncate charactor Signed-off-by: toyamagu2021@gmail.com --- crates/goose-cli/src/commands/project.rs | 2 +- crates/goose-cli/src/commands/session.rs | 2 +- crates/goose-cli/src/lib.rs | 1 - crates/goose-cli/src/session/export.rs | 3 ++- crates/goose-cli/src/session/mod.rs | 3 ++- crates/goose/src/context_mgmt/truncate.rs | 7 ++++--- crates/goose/src/lib.rs | 1 + crates/goose/src/session/storage.rs | 19 ++++++++++--------- crates/{goose-cli => goose}/src/utils.rs | 1 - 9 files changed, 21 insertions(+), 18 deletions(-) rename crates/{goose-cli => goose}/src/utils.rs (95%) diff --git a/crates/goose-cli/src/commands/project.rs b/crates/goose-cli/src/commands/project.rs index 17e63e412baf..e049e66e323e 100644 --- a/crates/goose-cli/src/commands/project.rs +++ b/crates/goose-cli/src/commands/project.rs @@ -4,7 +4,7 @@ use cliclack::{self, intro, outro}; use std::path::Path; use crate::project_tracker::ProjectTracker; -use crate::utils::safe_truncate; +use goose::utils::safe_truncate; /// Format a DateTime for display fn format_date(date: DateTime) -> String { diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index fbb862482ad0..ff8f37e47ca6 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -1,9 +1,9 @@ use crate::session::message_to_markdown; -use crate::utils::safe_truncate; use anyhow::{Context, Result}; use cliclack::{confirm, multiselect, select}; use goose::session::info::{get_valid_sorted_sessions, SessionInfo, SortOrder}; use goose::session::{self, Identifier}; +use goose::utils::safe_truncate; use regex::Regex; use std::fs; use std::path::{Path, PathBuf}; diff --git a/crates/goose-cli/src/lib.rs b/crates/goose-cli/src/lib.rs index 055f38b9033a..68f2357f5ee0 100644 --- a/crates/goose-cli/src/lib.rs +++ b/crates/goose-cli/src/lib.rs @@ -7,7 +7,6 @@ pub mod project_tracker; pub mod recipes; pub mod session; pub mod signal; -pub mod utils; // Re-export commonly used types pub use session::Session; diff --git a/crates/goose-cli/src/session/export.rs b/crates/goose-cli/src/session/export.rs index 57b83b1efa22..674430608e37 100644 --- a/crates/goose-cli/src/session/export.rs +++ b/crates/goose-cli/src/session/export.rs @@ -1,4 +1,5 @@ use goose::message::{Message, MessageContent, ToolRequest, ToolResponse}; +use goose::utils::safe_truncate; use mcp_core::content::Content as McpContent; use mcp_core::resource::ResourceContents; use mcp_core::role::Role; @@ -11,7 +12,7 @@ fn value_to_simple_markdown_string(value: &Value, export_full_strings: bool) -> match value { Value::String(s) => { if !export_full_strings && s.len() > MAX_STRING_LENGTH_MD_EXPORT { - let prefix = &s[..REDACTED_PREFIX_LENGTH.min(s.len())]; + let prefix = safe_truncate(s, REDACTED_PREFIX_LENGTH); let trimmed_chars = s.len() - prefix.len(); format!("`{}[ ... trimmed : {} chars ... ]`", prefix, trimmed_chars) } else { diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index e978fbe12beb..6c551fbc35d7 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -15,6 +15,7 @@ use goose::permission::Permission; use goose::permission::PermissionConfirmation; use goose::providers::base::Provider; pub use goose::session::Identifier; +use goose::utils::safe_truncate; use anyhow::{Context, Result}; use completion::GooseCompleter; @@ -993,7 +994,7 @@ impl Session { // High/Medium verbosity: show truncated response if let Some(response_content) = msg.strip_prefix("Responded: ") { if response_content.len() > 100 { - format!("🤖 Responded: {}...", &response_content[..100]) + format!("🤖 Responded: {}", safe_truncate(response_content, 100)) } else { format!("🤖 {}", msg) } diff --git a/crates/goose/src/context_mgmt/truncate.rs b/crates/goose/src/context_mgmt/truncate.rs index ba2f6490e0bb..3c77540905c6 100644 --- a/crates/goose/src/context_mgmt/truncate.rs +++ b/crates/goose/src/context_mgmt/truncate.rs @@ -1,4 +1,5 @@ use crate::message::{Message, MessageContent}; +use crate::utils::safe_truncate; use anyhow::{anyhow, Result}; use mcp_core::{Content, ResourceContents, Role}; use std::collections::HashSet; @@ -78,7 +79,7 @@ fn truncate_message_content(message: &Message, max_content_size: usize) -> Resul if text_content.text.len() > max_content_size { let truncated = format!( "{}\n\n[... content truncated from {} to {} characters ...]", - &text_content.text[..max_content_size.min(text_content.text.len())], + safe_truncate(&text_content.text, max_content_size), text_content.text.len(), max_content_size ); @@ -92,7 +93,7 @@ fn truncate_message_content(message: &Message, max_content_size: usize) -> Resul if text_content.text.len() > max_content_size { let truncated = format!( "{}\n\n[... tool response truncated from {} to {} characters ...]", - &text_content.text[..max_content_size.min(text_content.text.len())], + safe_truncate(&text_content.text, max_content_size), text_content.text.len(), max_content_size ); @@ -107,7 +108,7 @@ fn truncate_message_content(message: &Message, max_content_size: usize) -> Resul if text.len() > max_content_size { let truncated = format!( "{}\n\n[... resource content truncated from {} to {} characters ...]", - &text[..max_content_size.min(text.len())], + safe_truncate(text, max_content_size), text.len(), max_content_size ); diff --git a/crates/goose/src/lib.rs b/crates/goose/src/lib.rs index 83c4934d76fa..80e8e1abef32 100644 --- a/crates/goose/src/lib.rs +++ b/crates/goose/src/lib.rs @@ -15,6 +15,7 @@ pub mod temporal_scheduler; pub mod token_counter; pub mod tool_monitor; pub mod tracing; +pub mod utils; #[cfg(test)] mod cron_test; diff --git a/crates/goose/src/session/storage.rs b/crates/goose/src/session/storage.rs index b176511f300b..3b9cbdbcef2f 100644 --- a/crates/goose/src/session/storage.rs +++ b/crates/goose/src/session/storage.rs @@ -7,6 +7,7 @@ use crate::message::Message; use crate::providers::base::Provider; +use crate::utils::safe_truncate; use anyhow::Result; use chrono::Local; use etcetera::{choose_app_strategy, AppStrategy, AppStrategyArgs}; @@ -605,7 +606,7 @@ pub fn read_messages_with_truncation( // Log details about corrupted lines (with limited detail for security) for (num, line) in &corrupted_lines { let preview = if line.len() > 50 { - format!("{}... (truncated)", &line[..50]) + format!("{}... (truncated)", safe_truncate(line, 50)) } else { line.clone() }; @@ -681,7 +682,7 @@ fn truncate_message_content_in_place(message: &mut Message, max_content_size: us if text_content.text.len() > max_content_size { let truncated = format!( "{}\n\n[... content truncated during session loading from {} to {} characters ...]", - &text_content.text[..max_content_size.min(text_content.text.len())], + safe_truncate(&text_content.text, max_content_size), text_content.text.len(), max_content_size ); @@ -696,7 +697,7 @@ fn truncate_message_content_in_place(message: &mut Message, max_content_size: us if text_content.text.len() > max_content_size { let truncated = format!( "{}\n\n[... tool response truncated during session loading from {} to {} characters ...]", - &text_content.text[..max_content_size.min(text_content.text.len())], + safe_truncate(&text_content.text, max_content_size), text_content.text.len(), max_content_size ); @@ -710,7 +711,7 @@ fn truncate_message_content_in_place(message: &mut Message, max_content_size: us if text.len() > max_content_size { let truncated = format!( "{}\n\n[... resource content truncated during session loading from {} to {} characters ...]", - &text[..max_content_size.min(text.len())], + safe_truncate(text, max_content_size), text.len(), max_content_size ); @@ -751,7 +752,7 @@ fn attempt_corruption_recovery(json_str: &str, max_content_size: Option) // Strategy 4: Create a placeholder message with the raw content println!("[SESSION] All recovery strategies failed, creating placeholder message"); let preview = if json_str.len() > 200 { - format!("{}...", &json_str[..200]) + format!("{}...", safe_truncate(json_str, 200)) } else { json_str.to_string() }; @@ -968,7 +969,7 @@ fn truncate_json_string(json_str: &str, max_content_size: usize) -> String { if text_content.len() > max_content_size { let truncated_text = format!( "{}\n\n[... content truncated during JSON parsing from {} to {} characters ...]", - &text_content[..max_content_size.min(text_content.len())], + safe_truncate(text_content, max_content_size), text_content.len(), max_content_size ); @@ -1270,7 +1271,7 @@ pub async fn generate_description_with_schedule_id( .map(|m| { let text = m.as_concat_text(); if text.len() > 300 { - format!("{}...", &text[..300]) + format!("{}...", safe_truncate(&text, 300)) } else { text } @@ -1304,7 +1305,7 @@ pub async fn generate_description_with_schedule_id( // Validate description length for security let sanitized_description = if description.len() > 100 { tracing::warn!("Generated description too long, truncating"); - format!("{}...", &description[..97]) + format!("{}...", safe_truncate(&description, 97)) } else { description }; @@ -1379,7 +1380,7 @@ mod tests { println!( "[TEST] Input: {}", if corrupt_json.len() > 100 { - &corrupt_json[..100] + &safe_truncate(corrupt_json, 100) } else { corrupt_json } diff --git a/crates/goose-cli/src/utils.rs b/crates/goose/src/utils.rs similarity index 95% rename from crates/goose-cli/src/utils.rs rename to crates/goose/src/utils.rs index 69daddf1d2a1..60121f1bfe4d 100644 --- a/crates/goose-cli/src/utils.rs +++ b/crates/goose/src/utils.rs @@ -1,4 +1,3 @@ -/// Utility functions for safe string handling and other common operations /// Safely truncate a string at character boundaries, not byte boundaries /// /// This function ensures that multi-byte UTF-8 characters (like Japanese, emoji, etc.) From 684d92c37c463ea615bbf5aa63083c247e02a76b Mon Sep 17 00:00:00 2001 From: "toyamagu2021@gmail.com" Date: Tue, 8 Jul 2025 21:10:39 +0900 Subject: [PATCH 2/4] fix: test Signed-off-by: toyamagu2021@gmail.com --- crates/goose/src/session/storage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/goose/src/session/storage.rs b/crates/goose/src/session/storage.rs index 3b9cbdbcef2f..25f2b3148cd3 100644 --- a/crates/goose/src/session/storage.rs +++ b/crates/goose/src/session/storage.rs @@ -1380,9 +1380,9 @@ mod tests { println!( "[TEST] Input: {}", if corrupt_json.len() > 100 { - &safe_truncate(corrupt_json, 100) + safe_truncate(corrupt_json, 100) } else { - corrupt_json + corrupt_json.to_string() } ); From 0b17f5e0398353082a838cf5e915e400e24ddc8b Mon Sep 17 00:00:00 2001 From: "toyamagu2021@gmail.com" Date: Wed, 9 Jul 2025 01:40:32 +0900 Subject: [PATCH 3/4] fix: text.len() Signed-off-by: toyamagu2021@gmail.com --- Cargo.lock | 1 + crates/goose-cli/src/session/export.rs | 8 +++---- crates/goose-cli/src/session/mod.rs | 6 +---- crates/goose-llm/Cargo.toml | 1 + .../goose-llm/src/extractors/session_name.rs | 7 ++---- .../src/agents/large_response_handler.rs | 4 ++-- crates/goose/src/context_mgmt/truncate.rs | 12 +++++----- crates/goose/src/session/storage.rs | 22 ++++++++----------- 8 files changed, 26 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13c6faba4138..ef146e89654e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3597,6 +3597,7 @@ dependencies = [ "criterion", "ctor", "dotenv", + "goose", "include_dir", "indoc 1.0.9", "lazy_static", diff --git a/crates/goose-cli/src/session/export.rs b/crates/goose-cli/src/session/export.rs index 674430608e37..ea5fbe193850 100644 --- a/crates/goose-cli/src/session/export.rs +++ b/crates/goose-cli/src/session/export.rs @@ -11,9 +11,9 @@ const REDACTED_PREFIX_LENGTH: usize = 100; // Show first 100 chars before trimmi fn value_to_simple_markdown_string(value: &Value, export_full_strings: bool) -> String { match value { Value::String(s) => { - if !export_full_strings && s.len() > MAX_STRING_LENGTH_MD_EXPORT { + if !export_full_strings && s.chars().count() > MAX_STRING_LENGTH_MD_EXPORT { let prefix = safe_truncate(s, REDACTED_PREFIX_LENGTH); - let trimmed_chars = s.len() - prefix.len(); + let trimmed_chars = s.chars().count() - prefix.chars().count(); format!("`{}[ ... trimmed : {} chars ... ]`", prefix, trimmed_chars) } else { // Escape backticks and newlines for inline code. @@ -41,7 +41,7 @@ fn value_to_markdown(value: &Value, depth: usize, export_full_strings: bool) -> md_string.push_str(&format!("{}* **{}**: ", base_indent_str, key)); match val { Value::String(s) => { - if s.contains('\n') || s.len() > 80 { + if s.contains('\n') || s.chars().count() > 80 { // Heuristic for block md_string.push_str(&format!( "\n{} ```\n{}{}\n{} ```\n", @@ -75,7 +75,7 @@ fn value_to_markdown(value: &Value, depth: usize, export_full_strings: bool) -> md_string.push_str(&format!("{}* - ", base_indent_str)); match item { Value::String(s) => { - if s.contains('\n') || s.len() > 80 { + if s.contains('\n') || s.chars().count() > 80 { // Heuristic for block md_string.push_str(&format!( "\n{} ```\n{}{}\n{} ```\n", diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 6c551fbc35d7..c7d9b14a423f 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -993,11 +993,7 @@ impl Session { if min_priority > 0.1 && !self.debug { // High/Medium verbosity: show truncated response if let Some(response_content) = msg.strip_prefix("Responded: ") { - if response_content.len() > 100 { - format!("🤖 Responded: {}", safe_truncate(response_content, 100)) - } else { - format!("🤖 {}", msg) - } + format!("🤖 Responded: {}", safe_truncate(response_content, 100)) } else { format!("🤖 {}", msg) } diff --git a/crates/goose-llm/Cargo.toml b/crates/goose-llm/Cargo.toml index 17723e31aac4..ef073a37f90b 100644 --- a/crates/goose-llm/Cargo.toml +++ b/crates/goose-llm/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["lib", "cdylib"] name = "goose_llm" [dependencies] +goose = { path = "../goose" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" anyhow = "1.0" diff --git a/crates/goose-llm/src/extractors/session_name.rs b/crates/goose-llm/src/extractors/session_name.rs index a01b18ab2eeb..ab0cf97df8a1 100644 --- a/crates/goose-llm/src/extractors/session_name.rs +++ b/crates/goose-llm/src/extractors/session_name.rs @@ -3,6 +3,7 @@ use crate::providers::errors::ProviderError; use crate::types::core::Role; use crate::{message::Message, types::json_value_ffi::JsonValueFfi}; use anyhow::Result; +use goose::utils::safe_truncate; use indoc::indoc; use serde_json::{json, Value}; @@ -60,11 +61,7 @@ pub async fn generate_session_name( .take(3) .map(|m| { let text = m.content.concat_text_str(); - if text.len() > 300 { - text.chars().take(300).collect() - } else { - text - } + safe_truncate(&text, 300) }) .collect(); diff --git a/crates/goose/src/agents/large_response_handler.rs b/crates/goose/src/agents/large_response_handler.rs index e4c0ab105544..0369bfa5cbb5 100644 --- a/crates/goose/src/agents/large_response_handler.rs +++ b/crates/goose/src/agents/large_response_handler.rs @@ -17,14 +17,14 @@ pub fn process_tool_response( match content { Content::Text(text_content) => { // Check if text exceeds threshold - if text_content.text.len() > LARGE_TEXT_THRESHOLD { + if text_content.text.chars().count() > LARGE_TEXT_THRESHOLD { // Write to temp file match write_large_text_to_file(&text_content.text) { Ok(file_path) => { // Create a new text content with reference to the file let message = format!( "The response returned from the tool call was larger ({} characters) and is stored in the file which you can use other tools to examine or search in: {}", - text_content.text.len(), + text_content.text.chars().count(), file_path ); processed_contents.push(Content::text(message)); diff --git a/crates/goose/src/context_mgmt/truncate.rs b/crates/goose/src/context_mgmt/truncate.rs index 3c77540905c6..2bc49f924386 100644 --- a/crates/goose/src/context_mgmt/truncate.rs +++ b/crates/goose/src/context_mgmt/truncate.rs @@ -76,11 +76,11 @@ fn truncate_message_content(message: &Message, max_content_size: usize) -> Resul for content in &mut new_message.content { match content { MessageContent::Text(text_content) => { - if text_content.text.len() > max_content_size { + if text_content.text.chars().count() > max_content_size { let truncated = format!( "{}\n\n[... content truncated from {} to {} characters ...]", safe_truncate(&text_content.text, max_content_size), - text_content.text.len(), + text_content.text.chars().count(), max_content_size ); text_content.text = truncated; @@ -90,11 +90,11 @@ fn truncate_message_content(message: &Message, max_content_size: usize) -> Resul if let Ok(ref mut result) = tool_response.tool_result { for content_item in result { if let Content::Text(ref mut text_content) = content_item { - if text_content.text.len() > max_content_size { + if text_content.text.chars().count() > max_content_size { let truncated = format!( "{}\n\n[... tool response truncated from {} to {} characters ...]", safe_truncate(&text_content.text, max_content_size), - text_content.text.len(), + text_content.text.chars().count(), max_content_size ); text_content.text = truncated; @@ -105,11 +105,11 @@ fn truncate_message_content(message: &Message, max_content_size: usize) -> Resul if let ResourceContents::TextResourceContents { text, .. } = &mut resource_content.resource { - if text.len() > max_content_size { + if text.chars().count() > max_content_size { let truncated = format!( "{}\n\n[... resource content truncated from {} to {} characters ...]", safe_truncate(text, max_content_size), - text.len(), + text.chars().count(), max_content_size ); *text = truncated; diff --git a/crates/goose/src/session/storage.rs b/crates/goose/src/session/storage.rs index 25f2b3148cd3..794b6748eac6 100644 --- a/crates/goose/src/session/storage.rs +++ b/crates/goose/src/session/storage.rs @@ -679,11 +679,11 @@ fn truncate_message_content_in_place(message: &mut Message, max_content_size: us for content in &mut message.content { match content { MessageContent::Text(text_content) => { - if text_content.text.len() > max_content_size { + if text_content.text.chars().count() > max_content_size { let truncated = format!( "{}\n\n[... content truncated during session loading from {} to {} characters ...]", safe_truncate(&text_content.text, max_content_size), - text_content.text.len(), + text_content.text.chars().count(), max_content_size ); text_content.text = truncated; @@ -694,11 +694,11 @@ fn truncate_message_content_in_place(message: &mut Message, max_content_size: us for content_item in result { match content_item { Content::Text(ref mut text_content) => { - if text_content.text.len() > max_content_size { + if text_content.text.chars().count() > max_content_size { let truncated = format!( "{}\n\n[... tool response truncated during session loading from {} to {} characters ...]", safe_truncate(&text_content.text, max_content_size), - text_content.text.len(), + text_content.text.chars().count(), max_content_size ); text_content.text = truncated; @@ -708,11 +708,11 @@ fn truncate_message_content_in_place(message: &mut Message, max_content_size: us if let ResourceContents::TextResourceContents { text, .. } = &mut resource_content.resource { - if text.len() > max_content_size { + if text.chars().count() > max_content_size { let truncated = format!( "{}\n\n[... resource content truncated during session loading from {} to {} characters ...]", safe_truncate(text, max_content_size), - text.len(), + text.chars().count(), max_content_size ); *text = truncated; @@ -1270,11 +1270,7 @@ pub async fn generate_description_with_schedule_id( .take(3) // Use up to first 3 user messages for context .map(|m| { let text = m.as_concat_text(); - if text.len() > 300 { - format!("{}...", safe_truncate(&text, 300)) - } else { - text - } + safe_truncate(&text, 300) }) .collect(); @@ -1303,9 +1299,9 @@ pub async fn generate_description_with_schedule_id( let description = result.0.as_concat_text(); // Validate description length for security - let sanitized_description = if description.len() > 100 { + let sanitized_description = if description.chars().count() > 100 { tracing::warn!("Generated description too long, truncating"); - format!("{}...", safe_truncate(&description, 97)) + safe_truncate(&description, 100) } else { description }; From eb356d777cbe5353175a5c321cd1591975c6e6af Mon Sep 17 00:00:00 2001 From: "toyamagu2021@gmail.com" Date: Mon, 14 Jul 2025 09:42:34 +0900 Subject: [PATCH 4/4] fix: test Signed-off-by: toyamagu2021@gmail.com --- crates/goose-cli/src/session/export.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/goose-cli/src/session/export.rs b/crates/goose-cli/src/session/export.rs index ea5fbe193850..90d1c9a76054 100644 --- a/crates/goose-cli/src/session/export.rs +++ b/crates/goose-cli/src/session/export.rs @@ -398,7 +398,7 @@ mod tests { assert!(result.starts_with("`")); assert!(result.contains("[ ... trimmed : ")); assert!(result.contains("4900 chars ... ]`")); - assert!(result.contains(&"a".repeat(100))); // Should contain the prefix + assert!(result.contains(&"a".repeat(97))); // Should contain the prefix (100 - 3 for "...") } #[test]