mirror of
https://github.com/lWolvesl/claw-code.git
synced 2026-04-02 21:31:52 +08:00
Expose structured thinking without polluting normal assistant output
Extended thinking needed to travel end-to-end through the API, runtime, and CLI so the client can request a thinking budget, preserve streamed reasoning blocks, and present them in a collapsed text-first form. The implementation keeps thinking strictly opt-in, adds a session-local toggle, and reuses the existing flag/slash-command/reporting surfaces instead of introducing a new UI layer. Constraint: Existing non-thinking text/tool flows had to remain backward compatible by default Constraint: Terminal UX needed a lightweight collapsed representation rather than an interactive TUI widget Rejected: Heuristic CLI-only parsing of reasoning text | brittle against structured stream payloads Rejected: Expanded raw thinking output by default | too noisy for normal assistant responses Confidence: medium Scope-risk: moderate Reversibility: clean Directive: Keep thinking blocks structurally separate from answer text unless the upstream API contract changes Tested: cargo fmt --all; cargo clippy --workspace --all-targets -- -D warnings; cargo test -q Not-tested: Live upstream thinking payloads against the production API contract
This commit is contained in:
@@ -17,6 +17,8 @@ pub struct ApiRequest {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum AssistantEvent {
|
||||
TextDelta(String),
|
||||
ThinkingDelta(String),
|
||||
ThinkingSignature(String),
|
||||
ToolUse {
|
||||
id: String,
|
||||
name: String,
|
||||
@@ -247,15 +249,26 @@ fn build_assistant_message(
|
||||
events: Vec<AssistantEvent>,
|
||||
) -> Result<(ConversationMessage, Option<TokenUsage>), RuntimeError> {
|
||||
let mut text = String::new();
|
||||
let mut thinking = String::new();
|
||||
let mut thinking_signature: Option<String> = None;
|
||||
let mut blocks = Vec::new();
|
||||
let mut finished = false;
|
||||
let mut usage = None;
|
||||
|
||||
for event in events {
|
||||
match event {
|
||||
AssistantEvent::TextDelta(delta) => text.push_str(&delta),
|
||||
AssistantEvent::TextDelta(delta) => {
|
||||
flush_thinking_block(&mut thinking, &mut thinking_signature, &mut blocks);
|
||||
text.push_str(&delta);
|
||||
}
|
||||
AssistantEvent::ThinkingDelta(delta) => {
|
||||
flush_text_block(&mut text, &mut blocks);
|
||||
thinking.push_str(&delta);
|
||||
}
|
||||
AssistantEvent::ThinkingSignature(signature) => thinking_signature = Some(signature),
|
||||
AssistantEvent::ToolUse { id, name, input } => {
|
||||
flush_text_block(&mut text, &mut blocks);
|
||||
flush_thinking_block(&mut thinking, &mut thinking_signature, &mut blocks);
|
||||
blocks.push(ContentBlock::ToolUse { id, name, input });
|
||||
}
|
||||
AssistantEvent::Usage(value) => usage = Some(value),
|
||||
@@ -266,6 +279,7 @@ fn build_assistant_message(
|
||||
}
|
||||
|
||||
flush_text_block(&mut text, &mut blocks);
|
||||
flush_thinking_block(&mut thinking, &mut thinking_signature, &mut blocks);
|
||||
|
||||
if !finished {
|
||||
return Err(RuntimeError::new(
|
||||
@@ -290,6 +304,19 @@ fn flush_text_block(text: &mut String, blocks: &mut Vec<ContentBlock>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_thinking_block(
|
||||
thinking: &mut String,
|
||||
signature: &mut Option<String>,
|
||||
blocks: &mut Vec<ContentBlock>,
|
||||
) {
|
||||
if !thinking.is_empty() || signature.is_some() {
|
||||
blocks.push(ContentBlock::Thinking {
|
||||
text: std::mem::take(thinking),
|
||||
signature: signature.take(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type ToolHandler = Box<dyn FnMut(&str) -> Result<String, ToolError>>;
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -325,8 +352,8 @@ impl ToolExecutor for StaticToolExecutor {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
ApiClient, ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError,
|
||||
StaticToolExecutor,
|
||||
build_assistant_message, ApiClient, ApiRequest, AssistantEvent, ConversationRuntime,
|
||||
RuntimeError, StaticToolExecutor,
|
||||
};
|
||||
use crate::compact::CompactionConfig;
|
||||
use crate::permissions::{
|
||||
@@ -408,7 +435,7 @@ mod tests {
|
||||
.sum::<i32>();
|
||||
Ok(total.to_string())
|
||||
});
|
||||
let permission_policy = PermissionPolicy::new(PermissionMode::Prompt);
|
||||
let permission_policy = PermissionPolicy::new(PermissionMode::WorkspaceWrite);
|
||||
let system_prompt = SystemPromptBuilder::new()
|
||||
.with_project_context(ProjectContext {
|
||||
cwd: PathBuf::from("/tmp/project"),
|
||||
@@ -487,7 +514,7 @@ mod tests {
|
||||
Session::new(),
|
||||
SingleCallApiClient,
|
||||
StaticToolExecutor::new(),
|
||||
PermissionPolicy::new(PermissionMode::Prompt),
|
||||
PermissionPolicy::new(PermissionMode::WorkspaceWrite),
|
||||
vec!["system".to_string()],
|
||||
);
|
||||
|
||||
@@ -502,6 +529,29 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thinking_blocks_are_preserved_separately_from_text() {
|
||||
let (message, usage) = build_assistant_message(vec![
|
||||
AssistantEvent::ThinkingDelta("first ".to_string()),
|
||||
AssistantEvent::ThinkingDelta("second".to_string()),
|
||||
AssistantEvent::ThinkingSignature("sig-1".to_string()),
|
||||
AssistantEvent::TextDelta("final".to_string()),
|
||||
AssistantEvent::MessageStop,
|
||||
])
|
||||
.expect("assistant message should build");
|
||||
|
||||
assert_eq!(usage, None);
|
||||
assert!(matches!(
|
||||
&message.blocks[0],
|
||||
ContentBlock::Thinking { text, signature }
|
||||
if text == "first second" && signature.as_deref() == Some("sig-1")
|
||||
));
|
||||
assert!(matches!(
|
||||
&message.blocks[1],
|
||||
ContentBlock::Text { text } if text == "final"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconstructs_usage_tracker_from_restored_session() {
|
||||
struct SimpleApi;
|
||||
@@ -536,7 +586,7 @@ mod tests {
|
||||
session,
|
||||
SimpleApi,
|
||||
StaticToolExecutor::new(),
|
||||
PermissionPolicy::new(PermissionMode::Allow),
|
||||
PermissionPolicy::new(PermissionMode::DangerFullAccess),
|
||||
vec!["system".to_string()],
|
||||
);
|
||||
|
||||
@@ -563,7 +613,7 @@ mod tests {
|
||||
Session::new(),
|
||||
SimpleApi,
|
||||
StaticToolExecutor::new(),
|
||||
PermissionPolicy::new(PermissionMode::Allow),
|
||||
PermissionPolicy::new(PermissionMode::DangerFullAccess),
|
||||
vec!["system".to_string()],
|
||||
);
|
||||
runtime.run_turn("a", None).expect("turn a");
|
||||
|
||||
Reference in New Issue
Block a user