Explorar o código

Ignore reasoning blocks in runtime adapters without affecting tool/text flows

After the parser can accept thinking-style blocks, the CLI and tools adapters must explicitly ignore them so only user-visible text and tool calls drive runtime behavior. This keeps reasoning metadata from surfacing as text or interfering with tool accumulation.

Constraint: Runtime behavior must remain unchanged for normal text/tool streaming
Rejected: Treat thinking blocks as assistant text | would leak hidden reasoning into visible output and session flow
Confidence: high
Scope-risk: narrow
Directive: If future features need persisted reasoning blocks, add a dedicated runtime representation instead of overloading text handling
Tested: cargo test -p claw-cli response_to_events_ignores_thinking_blocks -- --nocapture; cargo test -p tools response_to_events_ignores_thinking_blocks -- --nocapture
Not-tested: End-to-end interactive run against a live thinking-enabled model
Yeachan-Heo hai 2 meses
pai
achega
be2bce7f8e
Modificáronse 2 ficheiros con 83 adicións e 1 borrados
  1. 42 0
      rust/crates/rusty-claude-cli/src/main.rs
  2. 41 1
      rust/crates/tools/src/lib.rs

+ 42 - 0
rust/crates/rusty-claude-cli/src/main.rs

@@ -2557,6 +2557,8 @@ impl ApiClient for AnthropicRuntimeClient {
                                 input.push_str(&partial_json);
                             }
                         }
+                        ContentBlockDelta::ThinkingDelta { .. }
+                        | ContentBlockDelta::SignatureDelta { .. } => {}
                     },
                     ApiStreamEvent::ContentBlockStop(_) => {
                         if let Some(rendered) = markdown_stream.flush(&renderer) {
@@ -3056,6 +3058,7 @@ fn push_output_block(
             };
             *pending_tool = Some((id, name, initial_input));
         }
+        OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {}
     }
     Ok(())
 }
@@ -4007,4 +4010,43 @@ mod tests {
                 if name == "read_file" && input == "{\"path\":\"rust/Cargo.toml\"}"
         ));
     }
+
+    #[test]
+    fn response_to_events_ignores_thinking_blocks() {
+        let mut out = Vec::new();
+        let events = response_to_events(
+            MessageResponse {
+                id: "msg-3".to_string(),
+                kind: "message".to_string(),
+                model: "claude-opus-4-6".to_string(),
+                role: "assistant".to_string(),
+                content: vec![
+                    OutputContentBlock::Thinking {
+                        thinking: "step 1".to_string(),
+                        signature: Some("sig_123".to_string()),
+                    },
+                    OutputContentBlock::Text {
+                        text: "Final answer".to_string(),
+                    },
+                ],
+                stop_reason: Some("end_turn".to_string()),
+                stop_sequence: None,
+                usage: Usage {
+                    input_tokens: 1,
+                    output_tokens: 1,
+                    cache_creation_input_tokens: 0,
+                    cache_read_input_tokens: 0,
+                },
+                request_id: None,
+            },
+            &mut out,
+        )
+        .expect("response conversion should succeed");
+
+        assert!(matches!(
+            &events[0],
+            AssistantEvent::TextDelta(text) if text == "Final answer"
+        ));
+        assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
+    }
 }

+ 41 - 1
rust/crates/tools/src/lib.rs

@@ -1953,6 +1953,8 @@ impl ApiClient for AnthropicRuntimeClient {
                                 input.push_str(&partial_json);
                             }
                         }
+                        ContentBlockDelta::ThinkingDelta { .. }
+                        | ContentBlockDelta::SignatureDelta { .. } => {}
                     },
                     ApiStreamEvent::ContentBlockStop(_) => {
                         if let Some((id, name, input)) = pending_tool.take() {
@@ -2147,6 +2149,7 @@ fn push_output_block(
             };
             *pending_tool = Some((id, name, initial_input));
         }
+        OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {}
     }
 }
 
@@ -3192,8 +3195,9 @@ mod tests {
     use super::{
         agent_permission_policy, allowed_tools_for_subagent, execute_agent_with_spawn,
         execute_tool, final_assistant_text, mvp_tool_specs, persist_agent_terminal_state,
-        AgentInput, AgentJob, GlobalToolRegistry, SubagentToolExecutor,
+        response_to_events, AgentInput, AgentJob, GlobalToolRegistry, SubagentToolExecutor,
     };
+    use api::{MessageResponse, OutputContentBlock, Usage};
     use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
     use runtime::{
         ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError, Session, ToolExecutor,
@@ -4026,6 +4030,42 @@ mod tests {
         let _ = std::fs::remove_file(path);
     }
 
+    #[test]
+    fn response_to_events_ignores_thinking_blocks() {
+        let events = response_to_events(MessageResponse {
+            id: "msg-1".to_string(),
+            kind: "message".to_string(),
+            model: "claude-opus-4-6".to_string(),
+            role: "assistant".to_string(),
+            content: vec![
+                OutputContentBlock::Thinking {
+                    thinking: "step 1".to_string(),
+                    signature: Some("sig_123".to_string()),
+                },
+                OutputContentBlock::Text {
+                    text: "Final answer".to_string(),
+                },
+            ],
+            stop_reason: Some("end_turn".to_string()),
+            stop_sequence: None,
+            usage: Usage {
+                input_tokens: 1,
+                output_tokens: 1,
+                cache_creation_input_tokens: 0,
+                cache_read_input_tokens: 0,
+            },
+            request_id: None,
+        });
+
+        assert!(matches!(
+            &events[0],
+            AssistantEvent::TextDelta(text) if text == "Final answer"
+        ));
+        assert!(!events
+            .iter()
+            .any(|event| matches!(event, AssistantEvent::ToolUse { .. })));
+    }
+
     #[test]
     fn agent_rejects_blank_required_fields() {
         let missing_description = execute_tool(