From 4c1eaa16e0b55c6ed4fd4c7e349d6a8287c3483a Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Wed, 1 Apr 2026 08:06:10 +0000 Subject: [PATCH] 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 rusty-claude-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 --- rust/crates/rusty-claude-cli/src/main.rs | 42 ++++++++++++++++++++++++ rust/crates/tools/src/lib.rs | 42 +++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index b7188c2..acb8aba 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/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")); + } } diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index 72a2fba..38fafe9 100644 --- a/rust/crates/tools/src/lib.rs +++ b/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(