From 89600e2b5c157187b6a426fd916e4e4b43160067 Mon Sep 17 00:00:00 2001 From: Zaki Manian Date: Wed, 4 Mar 2026 10:16:26 -0800 Subject: [PATCH] fix(agent): strip leaked [Called tool ...] text from responses (#497) * fix(agent): strip leaked [Called tool ...] text from agent responses When the NEAR AI provider flattens tool_call messages to plain text, markers like [Called tool ...] and [Tool ... returned: ...] can leak into the user-visible response if the LLM echoes them back. This adds a sanitization step in the agentic loop's text response path that strips these internal markers before returning. If stripping leaves the response empty, a generic fallback message is returned instead. Closes #487 Co-Authored-By: Claude Opus 4.6 * refactor: use fold instead of collect+join to avoid heap allocation Address review feedback: replace Vec collect + join with fold to build the filtered string directly, avoiding an intermediate heap allocation. Co-Authored-By: Claude Opus 4.6 * style: cargo fmt Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 Co-authored-by: Pierre LE GUEN <26087574+PierreLeGuen@users.noreply.github.com> --- src/agent/dispatcher.rs | 66 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/agent/dispatcher.rs b/src/agent/dispatcher.rs index bdade0e0..79ac4821 100644 --- a/src/agent/dispatcher.rs +++ b/src/agent/dispatcher.rs @@ -291,7 +291,11 @@ impl Agent { match output.result { RespondResult::Text(text) => { - return Ok(AgenticLoopResult::Response(text)); + // Strip internal "[Called tool ...]" text that can leak when + // provider flattening (e.g. NEAR AI) converts tool_calls to + // plain text and the LLM echoes it back. + let sanitized = strip_internal_tool_call_text(&text); + return Ok(AgenticLoopResult::Response(sanitized)); } RespondResult::ToolCalls { tool_calls, @@ -900,6 +904,38 @@ fn compact_messages_for_retry(messages: &[ChatMessage]) -> Vec { compacted } +/// Strip internal `[Called tool ...]` and `[Tool ... returned: ...]` markers +/// from a response string. These markers are inserted by provider-level message +/// flattening (e.g. NEAR AI) and can leak into the user-visible response when +/// the LLM echoes them back. +fn strip_internal_tool_call_text(text: &str) -> String { + // Remove lines that are purely internal tool-call markers. + // Pattern: lines matching `[Called tool (...)]` or `[Tool returned: ...]` + let result = text + .lines() + .filter(|line| { + let trimmed = line.trim(); + !((trimmed.starts_with("[Called tool ") && trimmed.ends_with(']')) + || (trimmed.starts_with("[Tool ") + && trimmed.contains(" returned:") + && trimmed.ends_with(']'))) + }) + .fold(String::new(), |mut acc, s| { + if !acc.is_empty() { + acc.push('\n'); + } + acc.push_str(s); + acc + }); + + let result = result.trim(); + if result.is_empty() { + "I wasn't able to complete that request. Could you try rephrasing or providing more details?".to_string() + } else { + result.to_string() + } +} + #[cfg(test)] mod tests { use std::sync::Arc; @@ -1902,4 +1938,32 @@ mod tests { } } } + + #[test] + fn test_strip_internal_tool_call_text_removes_markers() { + let input = "[Called tool search({\"query\": \"test\"})]\nHere is the answer."; + let result = super::strip_internal_tool_call_text(input); + assert_eq!(result, "Here is the answer."); + } + + #[test] + fn test_strip_internal_tool_call_text_removes_returned_markers() { + let input = "[Tool search returned: some result]\nSummary of findings."; + let result = super::strip_internal_tool_call_text(input); + assert_eq!(result, "Summary of findings."); + } + + #[test] + fn test_strip_internal_tool_call_text_all_markers_yields_fallback() { + let input = "[Called tool search({\"query\": \"test\"})]\n[Tool search returned: error]"; + let result = super::strip_internal_tool_call_text(input); + assert!(result.contains("wasn't able to complete")); + } + + #[test] + fn test_strip_internal_tool_call_text_preserves_normal_text() { + let input = "This is a normal response with [brackets] inside."; + let result = super::strip_internal_tool_call_text(input); + assert_eq!(result, input); + } }