From 1a62febe67cbf0fafffa3f6ee35fe751d39a5a4d Mon Sep 17 00:00:00 2001 From: Nige Date: Sun, 22 Mar 2026 07:04:02 +0000 Subject: [PATCH] perf(agent): avoid preview allocations for non-truncated strings (fix #894) (#924) * perf(agent): avoid preview allocation on non-truncated strings * Update src/worker/container.rs Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * chore(ci): annotate test assertions for no-panics gate * fix: remove unnecessary allocation and consolidate tests - Remove redundant `.to_string()` on `&String` in container.rs error arm - Bind `format!()` result to a let in job.rs to avoid Cow borrowing from temporary - Merge borrowed/owned Cow assertions into existing tests, drop misleading comments Co-Authored-By: Claude Opus 4.6 (1M context) * fix: restore separate test functions for CI regression check Keep dedicated `test_truncate_short_string_borrows` and `test_truncate_long_string_owns` tests so the PR diff contains new `#[test]` functions, satisfying the regression test enforcement check. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: ilblackdragon@gmail.com Co-authored-by: Claude Opus 4.6 (1M context) --- src/agent/agentic_loop.rs | 19 ++++++++++++++++--- src/worker/container.rs | 2 +- src/worker/job.rs | 6 +++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/agent/agentic_loop.rs b/src/agent/agentic_loop.rs index 6cefdb42..cc6fd486 100644 --- a/src/agent/agentic_loop.rs +++ b/src/agent/agentic_loop.rs @@ -6,6 +6,7 @@ //! via the `LoopDelegate` trait. use async_trait::async_trait; +use std::borrow::Cow; use crate::agent::session::PendingApproval; use crate::error::Error; @@ -235,12 +236,12 @@ pub async fn run_agentic_loop( /// /// `max` is a byte budget. The result is truncated at the last valid char /// boundary at or before `max` bytes, so it is always valid UTF-8. -pub fn truncate_for_preview(s: &str, max: usize) -> String { +pub fn truncate_for_preview(s: &str, max: usize) -> Cow<'_, str> { if s.len() <= max { - s.to_string() + Cow::Borrowed(s) } else { let end = crate::util::floor_char_boundary(s, max); - format!("{}...", &s[..end]) + Cow::Owned(format!("{}...", &s[..end])) } } @@ -597,12 +598,24 @@ mod tests { assert_eq!(truncate_for_preview("hello", 10), "hello"); } + #[test] + fn test_truncate_short_string_borrows() { + let result = truncate_for_preview("hello", 10); + assert!(matches!(result, Cow::Borrowed("hello"))); + } + #[test] fn test_truncate_long_string_adds_ellipsis() { let result = truncate_for_preview("hello world", 5); assert_eq!(result, "hello..."); } + #[test] + fn test_truncate_long_string_owns() { + let result = truncate_for_preview("hello world", 5); + assert!(matches!(result, Cow::Owned(_))); + } + #[test] fn test_truncate_multibyte_safe() { let result = truncate_for_preview("café", 4); diff --git a/src/worker/container.rs b/src/worker/container.rs index 0b7f41d0..920cc2ce 100644 --- a/src/worker/container.rs +++ b/src/worker/container.rs @@ -472,7 +472,7 @@ impl LoopDelegate for ContainerDelegate { "tool_name": tc.name, "output": match &result { Ok(output) => truncate_for_preview(output, 2000), - Err(e) => format!("Error: {}", truncate_for_preview(e, 500)), + Err(e) => format!("Error: {}", truncate_for_preview(e, 500)).into(), }, "success": result.is_ok(), }), diff --git a/src/worker/job.rs b/src/worker/job.rs index 1b2be6f3..436a23ce 100644 --- a/src/worker/job.rs +++ b/src/worker/job.rs @@ -800,12 +800,16 @@ Report when the job is complete or if you encounter issues you cannot resolve."# }); } + let error_preview = { + let msg = format!("Error: {}", e); + truncate_for_preview(&msg, 500).into_owned() + }; self.log_event( "tool_result", serde_json::json!({ "tool_name": selection.tool_name, "success": false, - "output": truncate_for_preview(&format!("Error: {}", e), 500), + "output": error_preview, }), );