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
This commit is contained in:
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user