diff --git a/src/agent/loop_.rs b/src/agent/loop_.rs index 2ee352490..e3851de17 100644 --- a/src/agent/loop_.rs +++ b/src/agent/loop_.rs @@ -131,6 +131,14 @@ const NON_CLI_APPROVAL_POLL_INTERVAL_MS: u64 = 250; const MISSING_TOOL_CALL_RETRY_PROMPT: &str = "Internal correction: your last reply implied a follow-up action or claimed action completion, but no valid tool call was emitted. If a tool is needed, emit it now using the required ... format. If no tool is needed, provide the complete final answer now and do not defer action."; const TOOL_UNAVAILABLE_RETRY_PROMPT_PREFIX: &str = "Internal correction: your prior reply claimed required tools were unavailable. Use only the runtime-allowed tools listed below. If file changes are requested and `file_write`/`file_edit` are listed, call them directly."; +fn display_text_for_tool_execution_turn(response_text: &str, parsed_text: &str) -> String { + if !parsed_text.is_empty() { + return parsed_text.to_string(); + } + + response_text.to_string() +} + /// Detect completion claims that imply state-changing work already happened /// without an accompanying tool call. static ACTION_COMPLETION_CUE_REGEX: LazyLock = LazyLock::new(|| { @@ -1020,18 +1028,17 @@ pub(crate) async fn run_tool_call_loop( }); let response_text = resp.text_or_empty().to_string(); - // First try native structured tool calls (OpenAI-format). - // Fall back to text-based parsing (XML tags, markdown blocks, - // GLM format) only if the provider returned no native calls — - // this ensures we support both native and prompt-guided models. + // Always derive a text-only view of the model response for CLI + // and channel display so raw payloads are hidden + // even when the provider also returned native structured calls. let mut calls = parse_structured_tool_calls(&resp.tool_calls); + let (fallback_text, fallback_calls) = parse_tool_calls(&response_text); let mut parsed_text = String::new(); + if !fallback_text.is_empty() { + parsed_text = fallback_text; + } if calls.is_empty() { - let (fallback_text, fallback_calls) = parse_tool_calls(&response_text); - if !fallback_text.is_empty() { - parsed_text = fallback_text; - } calls = fallback_calls; } @@ -1135,11 +1142,7 @@ pub(crate) async fn run_tool_call_loop( } }; - let display_text = if parsed_text.is_empty() { - response_text.clone() - } else { - parsed_text - }; + let display_text = display_text_for_tool_execution_turn(&response_text, &parsed_text); // ── Progress: LLM responded ───────────────────────────── if let Some(ref tx) = on_delta { @@ -4339,6 +4342,31 @@ mod tests { assert!(calls.is_empty()); } + #[test] + fn display_text_for_tool_execution_turn_hides_raw_tool_call_payloads() { + let response = r#" +{"name": "shell", "arguments": {"command": "pwd"}} +"#; + let (parsed_text, _calls) = parse_tool_calls(response); + + let display = display_text_for_tool_execution_turn(response, &parsed_text); + + assert!(display.is_empty()); + } + + #[test] + fn display_text_for_tool_execution_turn_preserves_preface_while_stripping_payloads() { + let response = r#"Let me check that. + +{"name": "shell", "arguments": {"command": "pwd"}} +"#; + let (parsed_text, _calls) = parse_tool_calls(response); + + let display = display_text_for_tool_execution_turn(response, &parsed_text); + + assert_eq!(display, "Let me check that."); + } + #[test] fn parse_tool_calls_handles_malformed_json() { let response = r#"