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#"