chore: 添加 IntelliJ IDEA 项目配置文件
添加 .idea 目录下的 VCS、检查配置、模块设置等 IntelliJ IDEA 项目配置文件,以便在 IDE 中正确管理项目。
This commit is contained in:
parent
f9a3993335
commit
3810cbb5e4
|
|
@ -18,7 +18,7 @@
|
||||||
2. 使用deepseek的apiKey, 模型 qwen-plus
|
2. 使用deepseek的apiKey, 模型 qwen-plus
|
||||||
```
|
```
|
||||||
export DEEPSEEK_API_KEY=sk-your-key
|
export DEEPSEEK_API_KEY=sk-your-key
|
||||||
export DEEPSEEK_MODEL="qwen-plus"
|
export DEEPSEEK_MODEL="deepseek-v4-flash"
|
||||||
|
|
||||||
./claw
|
./claw
|
||||||
```
|
```
|
||||||
|
|
@ -389,9 +389,14 @@ struct StreamState {
|
||||||
message_started: bool,
|
message_started: bool,
|
||||||
text_started: bool,
|
text_started: bool,
|
||||||
text_finished: bool,
|
text_finished: bool,
|
||||||
|
reasoning_started: bool,
|
||||||
|
reasoning_finished: bool,
|
||||||
finished: bool,
|
finished: bool,
|
||||||
stop_reason: Option<String>,
|
stop_reason: Option<String>,
|
||||||
usage: Option<Usage>,
|
usage: Option<Usage>,
|
||||||
|
next_block_index: u32,
|
||||||
|
reasoning_block_index: Option<u32>,
|
||||||
|
text_block_index: Option<u32>,
|
||||||
tool_calls: BTreeMap<u32, ToolCallState>,
|
tool_calls: BTreeMap<u32, ToolCallState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -402,9 +407,14 @@ impl StreamState {
|
||||||
message_started: false,
|
message_started: false,
|
||||||
text_started: false,
|
text_started: false,
|
||||||
text_finished: false,
|
text_finished: false,
|
||||||
|
reasoning_started: false,
|
||||||
|
reasoning_finished: false,
|
||||||
finished: false,
|
finished: false,
|
||||||
stop_reason: None,
|
stop_reason: None,
|
||||||
usage: None,
|
usage: None,
|
||||||
|
next_block_index: 0,
|
||||||
|
reasoning_block_index: None,
|
||||||
|
text_block_index: None,
|
||||||
tool_calls: BTreeMap::new(),
|
tool_calls: BTreeMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -443,20 +453,58 @@ impl StreamState {
|
||||||
}
|
}
|
||||||
|
|
||||||
for choice in chunk.choices {
|
for choice in chunk.choices {
|
||||||
|
if let Some(reasoning) = choice
|
||||||
|
.delta
|
||||||
|
.reasoning_content
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
if !self.reasoning_started {
|
||||||
|
self.reasoning_started = true;
|
||||||
|
self.reasoning_block_index = Some(self.next_block_index);
|
||||||
|
events.push(StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
||||||
|
index: self.next_block_index,
|
||||||
|
content_block: OutputContentBlock::Thinking {
|
||||||
|
thinking: String::new(),
|
||||||
|
signature: None,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
self.next_block_index += 1;
|
||||||
|
}
|
||||||
|
if let Some(idx) = self.reasoning_block_index {
|
||||||
|
events.push(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
||||||
|
index: idx,
|
||||||
|
delta: ContentBlockDelta::ThinkingDelta {
|
||||||
|
thinking: reasoning,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(content) = choice.delta.content.filter(|value| !value.is_empty()) {
|
if let Some(content) = choice.delta.content.filter(|value| !value.is_empty()) {
|
||||||
|
if self.reasoning_started && !self.reasoning_finished {
|
||||||
|
self.reasoning_finished = true;
|
||||||
|
if let Some(idx) = self.reasoning_block_index {
|
||||||
|
events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent {
|
||||||
|
index: idx,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
if !self.text_started {
|
if !self.text_started {
|
||||||
self.text_started = true;
|
self.text_started = true;
|
||||||
|
self.text_block_index = Some(self.next_block_index);
|
||||||
events.push(StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
events.push(StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
||||||
index: 0,
|
index: self.next_block_index,
|
||||||
content_block: OutputContentBlock::Text {
|
content_block: OutputContentBlock::Text {
|
||||||
text: String::new(),
|
text: String::new(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
self.next_block_index += 1;
|
||||||
|
}
|
||||||
|
if let Some(idx) = self.text_block_index {
|
||||||
|
events.push(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
||||||
|
index: idx,
|
||||||
|
delta: ContentBlockDelta::TextDelta { text: content },
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
events.push(StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
|
||||||
index: 0,
|
|
||||||
delta: ContentBlockDelta::TextDelta { text: content },
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for tool_call in choice.delta.tool_calls {
|
for tool_call in choice.delta.tool_calls {
|
||||||
|
|
@ -507,11 +555,22 @@ impl StreamState {
|
||||||
self.finished = true;
|
self.finished = true;
|
||||||
|
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
if self.reasoning_started && !self.reasoning_finished {
|
||||||
|
self.reasoning_finished = true;
|
||||||
|
if let Some(idx) = self.reasoning_block_index {
|
||||||
|
events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent {
|
||||||
|
index: idx,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if self.text_started && !self.text_finished {
|
if self.text_started && !self.text_finished {
|
||||||
self.text_finished = true;
|
self.text_finished = true;
|
||||||
events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent {
|
if let Some(idx) = self.text_block_index {
|
||||||
index: 0,
|
events.push(StreamEvent::ContentBlockStop(ContentBlockStopEvent {
|
||||||
}));
|
index: idx,
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for state in self.tool_calls.values_mut() {
|
for state in self.tool_calls.values_mut() {
|
||||||
|
|
@ -581,7 +640,8 @@ impl ToolCallState {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fn block_index(&self) -> u32 {
|
const fn block_index(&self) -> u32 {
|
||||||
self.openai_index + 1
|
// We use a large offset for tool calls to avoid colliding with text/reasoning blocks
|
||||||
|
self.openai_index + 100
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
|
|
@ -641,6 +701,8 @@ struct ChatMessage {
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
tool_calls: Vec<ResponseToolCall>,
|
tool_calls: Vec<ResponseToolCall>,
|
||||||
|
#[serde(default)]
|
||||||
|
reasoning_content: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -685,6 +747,8 @@ struct ChunkChoice {
|
||||||
struct ChunkDelta {
|
struct ChunkDelta {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
content: Option<String>,
|
content: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
reasoning_content: Option<String>,
|
||||||
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
|
#[serde(default, deserialize_with = "deserialize_null_as_empty_vec")]
|
||||||
tool_calls: Vec<DeltaToolCall>,
|
tool_calls: Vec<DeltaToolCall>,
|
||||||
}
|
}
|
||||||
|
|
@ -901,10 +965,12 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec<Value> {
|
||||||
match message.role.as_str() {
|
match message.role.as_str() {
|
||||||
"assistant" => {
|
"assistant" => {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
|
let mut reasoning = String::new();
|
||||||
let mut tool_calls = Vec::new();
|
let mut tool_calls = Vec::new();
|
||||||
for block in &message.content {
|
for block in &message.content {
|
||||||
match block {
|
match block {
|
||||||
InputContentBlock::Text { text: value } => text.push_str(value),
|
InputContentBlock::Text { text: value } => text.push_str(value),
|
||||||
|
InputContentBlock::Thinking { thinking, .. } => reasoning.push_str(thinking),
|
||||||
InputContentBlock::ToolUse { id, name, input } => tool_calls.push(json!({
|
InputContentBlock::ToolUse { id, name, input } => tool_calls.push(json!({
|
||||||
"id": id,
|
"id": id,
|
||||||
"type": "function",
|
"type": "function",
|
||||||
|
|
@ -916,13 +982,16 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec<Value> {
|
||||||
InputContentBlock::ToolResult { .. } => {}
|
InputContentBlock::ToolResult { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if text.is_empty() && tool_calls.is_empty() {
|
if text.is_empty() && tool_calls.is_empty() && reasoning.is_empty() {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
let mut msg = serde_json::json!({
|
let mut msg = serde_json::json!({
|
||||||
"role": "assistant",
|
"role": "assistant",
|
||||||
"content": (!text.is_empty()).then_some(text),
|
"content": (!text.is_empty()).then_some(text),
|
||||||
});
|
});
|
||||||
|
if !reasoning.is_empty() {
|
||||||
|
msg["reasoning_content"] = json!(reasoning);
|
||||||
|
}
|
||||||
// Only include tool_calls when non-empty: some providers reject
|
// Only include tool_calls when non-empty: some providers reject
|
||||||
// assistant messages with an explicit empty tool_calls array.
|
// assistant messages with an explicit empty tool_calls array.
|
||||||
if !tool_calls.is_empty() {
|
if !tool_calls.is_empty() {
|
||||||
|
|
@ -957,6 +1026,7 @@ pub fn translate_message(message: &InputMessage, model: &str) -> Vec<Value> {
|
||||||
Some(msg)
|
Some(msg)
|
||||||
}
|
}
|
||||||
InputContentBlock::ToolUse { .. } => None,
|
InputContentBlock::ToolUse { .. } => None,
|
||||||
|
InputContentBlock::Thinking { .. } => None,
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
}
|
}
|
||||||
|
|
@ -1135,6 +1205,16 @@ fn normalize_response(
|
||||||
"chat completion response missing choices",
|
"chat completion response missing choices",
|
||||||
))?;
|
))?;
|
||||||
let mut content = Vec::new();
|
let mut content = Vec::new();
|
||||||
|
if let Some(thinking) = choice
|
||||||
|
.message
|
||||||
|
.reasoning_content
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
{
|
||||||
|
content.push(OutputContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
if let Some(text) = choice.message.content.filter(|value| !value.is_empty()) {
|
if let Some(text) = choice.message.content.filter(|value| !value.is_empty()) {
|
||||||
content.push(OutputContentBlock::Text { text });
|
content.push(OutputContentBlock::Text { text });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,11 @@ pub enum InputContentBlock {
|
||||||
Text {
|
Text {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
|
Thinking {
|
||||||
|
thinking: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
signature: Option<String>,
|
||||||
|
},
|
||||||
ToolUse {
|
ToolUse {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
|
||||||
|
|
@ -187,38 +187,38 @@ async fn stream_message_normalizes_text_and_multiple_tool_calls() {
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[3],
|
events[3],
|
||||||
StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
||||||
index: 1,
|
index: 100,
|
||||||
content_block: OutputContentBlock::ToolUse { .. },
|
content_block: OutputContentBlock::ToolUse { .. },
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[4],
|
events[4],
|
||||||
StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
||||||
index: 1,
|
index: 100,
|
||||||
delta: ContentBlockDelta::InputJsonDelta { .. },
|
delta: ContentBlockDelta::InputJsonDelta { .. },
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[5],
|
events[5],
|
||||||
StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
StreamEvent::ContentBlockStart(ContentBlockStartEvent {
|
||||||
index: 2,
|
index: 101,
|
||||||
content_block: OutputContentBlock::ToolUse { .. },
|
content_block: OutputContentBlock::ToolUse { .. },
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[6],
|
events[6],
|
||||||
StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
StreamEvent::ContentBlockDelta(ContentBlockDeltaEvent {
|
||||||
index: 2,
|
index: 101,
|
||||||
delta: ContentBlockDelta::InputJsonDelta { .. },
|
delta: ContentBlockDelta::InputJsonDelta { .. },
|
||||||
})
|
})
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[7],
|
events[7],
|
||||||
StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 1 })
|
StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 100 })
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[8],
|
events[8],
|
||||||
StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 2 })
|
StreamEvent::ContentBlockStop(ContentBlockStopEvent { index: 101 })
|
||||||
));
|
));
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
events[9],
|
events[9],
|
||||||
|
|
|
||||||
|
|
@ -3158,8 +3158,12 @@ impl ApiClient for DefaultRuntimeClient {
|
||||||
input.push_str(&partial_json);
|
input.push_str(&partial_json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlockDelta::ThinkingDelta { .. }
|
ContentBlockDelta::ThinkingDelta { thinking } => {
|
||||||
| ContentBlockDelta::SignatureDelta { .. } => {}
|
events.push(AssistantEvent::ThinkingDelta(thinking));
|
||||||
|
}
|
||||||
|
ContentBlockDelta::SignatureDelta { signature } => {
|
||||||
|
events.push(AssistantEvent::SignatureDelta(signature));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
ApiStreamEvent::ContentBlockStop(_) => {
|
ApiStreamEvent::ContentBlockStop(_) => {
|
||||||
if let Some(rendered) = markdown_stream.flush(&renderer) {
|
if let Some(rendered) = markdown_stream.flush(&renderer) {
|
||||||
|
|
@ -3819,7 +3823,15 @@ fn push_output_block(
|
||||||
};
|
};
|
||||||
*pending_tool = Some((id, name, initial_input));
|
*pending_tool = Some((id, name, initial_input));
|
||||||
}
|
}
|
||||||
OutputContentBlock::Thinking { .. } | OutputContentBlock::RedactedThinking { .. } => {}
|
OutputContentBlock::Thinking { thinking, signature } => {
|
||||||
|
if !thinking.is_empty() {
|
||||||
|
events.push(AssistantEvent::ThinkingDelta(thinking));
|
||||||
|
}
|
||||||
|
if let Some(sig) = signature {
|
||||||
|
events.push(AssistantEvent::SignatureDelta(sig));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutputContentBlock::RedactedThinking { .. } => {}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -5050,7 +5062,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn response_to_events_ignores_thinking_blocks() {
|
fn response_to_events_preserves_thinking_blocks() {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let events = response_to_events(
|
let events = response_to_events(
|
||||||
MessageResponse {
|
MessageResponse {
|
||||||
|
|
@ -5083,8 +5095,15 @@ mod tests {
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
&events[0],
|
&events[0],
|
||||||
|
AssistantEvent::ThinkingDelta(text) if text == "step 1"
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&events[1],
|
||||||
|
AssistantEvent::SignatureDelta(sig) if sig == "sig_123"
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
&events[2],
|
||||||
AssistantEvent::TextDelta(text) if text == "Final answer"
|
AssistantEvent::TextDelta(text) if text == "Final answer"
|
||||||
));
|
));
|
||||||
assert!(!String::from_utf8(out).expect("utf8").contains("step 1"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -212,7 +212,7 @@ fn summarize_messages(messages: &[ConversationMessage]) -> String {
|
||||||
.filter_map(|block| match block {
|
.filter_map(|block| match block {
|
||||||
ContentBlock::ToolUse { name, .. } => Some(name.as_str()),
|
ContentBlock::ToolUse { name, .. } => Some(name.as_str()),
|
||||||
ContentBlock::ToolResult { tool_name, .. } => Some(tool_name.as_str()),
|
ContentBlock::ToolResult { tool_name, .. } => Some(tool_name.as_str()),
|
||||||
ContentBlock::Text { .. } => None,
|
ContentBlock::Text { .. } | ContentBlock::Thinking { .. } => None,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
tool_names.sort_unstable();
|
tool_names.sort_unstable();
|
||||||
|
|
@ -327,6 +327,7 @@ fn summarize_block(block: &ContentBlock) -> String {
|
||||||
"tool_result {tool_name}: {}{output}",
|
"tool_result {tool_name}: {}{output}",
|
||||||
if *is_error { "error " } else { "" }
|
if *is_error { "error " } else { "" }
|
||||||
),
|
),
|
||||||
|
ContentBlock::Thinking { thinking, .. } => format!("thinking {thinking}"),
|
||||||
};
|
};
|
||||||
truncate_summary(&raw, 160)
|
truncate_summary(&raw, 160)
|
||||||
}
|
}
|
||||||
|
|
@ -378,6 +379,7 @@ fn collect_key_files(messages: &[ConversationMessage]) -> Vec<String> {
|
||||||
ContentBlock::Text { text } => text.as_str(),
|
ContentBlock::Text { text } => text.as_str(),
|
||||||
ContentBlock::ToolUse { input, .. } => input.as_str(),
|
ContentBlock::ToolUse { input, .. } => input.as_str(),
|
||||||
ContentBlock::ToolResult { output, .. } => output.as_str(),
|
ContentBlock::ToolResult { output, .. } => output.as_str(),
|
||||||
|
ContentBlock::Thinking { thinking, .. } => thinking.as_str(),
|
||||||
})
|
})
|
||||||
.flat_map(extract_file_candidates)
|
.flat_map(extract_file_candidates)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
@ -400,6 +402,7 @@ fn first_text_block(message: &ConversationMessage) -> Option<&str> {
|
||||||
ContentBlock::Text { text } if !text.trim().is_empty() => Some(text.as_str()),
|
ContentBlock::Text { text } if !text.trim().is_empty() => Some(text.as_str()),
|
||||||
ContentBlock::ToolUse { .. }
|
ContentBlock::ToolUse { .. }
|
||||||
| ContentBlock::ToolResult { .. }
|
| ContentBlock::ToolResult { .. }
|
||||||
|
| ContentBlock::Thinking { .. }
|
||||||
| ContentBlock::Text { .. } => None,
|
| ContentBlock::Text { .. } => None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -450,6 +453,7 @@ fn estimate_message_tokens(message: &ConversationMessage) -> usize {
|
||||||
ContentBlock::ToolResult {
|
ContentBlock::ToolResult {
|
||||||
tool_name, output, ..
|
tool_name, output, ..
|
||||||
} => (tool_name.len() + output.len()) / 4 + 1,
|
} => (tool_name.len() + output.len()) / 4 + 1,
|
||||||
|
ContentBlock::Thinking { thinking, .. } => thinking.len() / 4 + 1,
|
||||||
})
|
})
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ pub struct ApiRequest {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum AssistantEvent {
|
pub enum AssistantEvent {
|
||||||
TextDelta(String),
|
TextDelta(String),
|
||||||
|
ThinkingDelta(String),
|
||||||
|
SignatureDelta(String),
|
||||||
ToolUse {
|
ToolUse {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -760,15 +762,33 @@ fn build_assistant_message(
|
||||||
RuntimeError,
|
RuntimeError,
|
||||||
> {
|
> {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
|
let mut thinking = String::new();
|
||||||
|
let mut signature = None;
|
||||||
let mut blocks = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
let mut prompt_cache_events = Vec::new();
|
let mut prompt_cache_events = Vec::new();
|
||||||
let mut finished = false;
|
let mut finished = false;
|
||||||
let mut usage = None;
|
let mut usage = None;
|
||||||
|
|
||||||
|
let flush_thinking_block =
|
||||||
|
|thinking: &mut String, signature: &mut Option<String>, blocks: &mut Vec<ContentBlock>| {
|
||||||
|
if !thinking.is_empty() {
|
||||||
|
blocks.push(ContentBlock::Thinking {
|
||||||
|
thinking: std::mem::take(thinking),
|
||||||
|
signature: signature.take(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for event in events {
|
for event in events {
|
||||||
match event {
|
match event {
|
||||||
AssistantEvent::TextDelta(delta) => text.push_str(&delta),
|
AssistantEvent::TextDelta(delta) => {
|
||||||
|
flush_thinking_block(&mut thinking, &mut signature, &mut blocks);
|
||||||
|
text.push_str(&delta);
|
||||||
|
}
|
||||||
|
AssistantEvent::ThinkingDelta(delta) => thinking.push_str(&delta),
|
||||||
|
AssistantEvent::SignatureDelta(delta) => signature = Some(delta),
|
||||||
AssistantEvent::ToolUse { id, name, input } => {
|
AssistantEvent::ToolUse { id, name, input } => {
|
||||||
|
flush_thinking_block(&mut thinking, &mut signature, &mut blocks);
|
||||||
flush_text_block(&mut text, &mut blocks);
|
flush_text_block(&mut text, &mut blocks);
|
||||||
blocks.push(ContentBlock::ToolUse { id, name, input });
|
blocks.push(ContentBlock::ToolUse { id, name, input });
|
||||||
}
|
}
|
||||||
|
|
@ -780,6 +800,7 @@ fn build_assistant_message(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flush_thinking_block(&mut thinking, &mut signature, &mut blocks);
|
||||||
flush_text_block(&mut text, &mut blocks);
|
flush_text_block(&mut text, &mut blocks);
|
||||||
|
|
||||||
if !finished {
|
if !finished {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ pub enum ContentBlock {
|
||||||
Text {
|
Text {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
|
Thinking {
|
||||||
|
thinking: String,
|
||||||
|
signature: Option<String>,
|
||||||
|
},
|
||||||
ToolUse {
|
ToolUse {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
@ -737,6 +741,19 @@ impl ContentBlock {
|
||||||
object.insert("type".to_string(), JsonValue::String("text".to_string()));
|
object.insert("type".to_string(), JsonValue::String("text".to_string()));
|
||||||
object.insert("text".to_string(), JsonValue::String(text.clone()));
|
object.insert("text".to_string(), JsonValue::String(text.clone()));
|
||||||
}
|
}
|
||||||
|
Self::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} => {
|
||||||
|
object.insert(
|
||||||
|
"type".to_string(),
|
||||||
|
JsonValue::String("thinking".to_string()),
|
||||||
|
);
|
||||||
|
object.insert("thinking".to_string(), JsonValue::String(thinking.clone()));
|
||||||
|
if let Some(sig) = signature {
|
||||||
|
object.insert("signature".to_string(), JsonValue::String(sig.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
Self::ToolUse { id, name, input } => {
|
Self::ToolUse { id, name, input } => {
|
||||||
object.insert(
|
object.insert(
|
||||||
"type".to_string(),
|
"type".to_string(),
|
||||||
|
|
@ -783,6 +800,13 @@ impl ContentBlock {
|
||||||
"text" => Ok(Self::Text {
|
"text" => Ok(Self::Text {
|
||||||
text: required_string(object, "text")?,
|
text: required_string(object, "text")?,
|
||||||
}),
|
}),
|
||||||
|
"thinking" => Ok(Self::Thinking {
|
||||||
|
thinking: required_string(object, "thinking")?,
|
||||||
|
signature: object
|
||||||
|
.get("signature")
|
||||||
|
.and_then(JsonValue::as_str)
|
||||||
|
.map(ToOwned::to_owned),
|
||||||
|
}),
|
||||||
"tool_use" => Ok(Self::ToolUse {
|
"tool_use" => Ok(Self::ToolUse {
|
||||||
id: required_string(object, "id")?,
|
id: required_string(object, "id")?,
|
||||||
name: required_string(object, "name")?,
|
name: required_string(object, "name")?,
|
||||||
|
|
|
||||||
|
|
@ -6645,6 +6645,12 @@ fn render_export_text(session: &Session) -> String {
|
||||||
"[tool_result id={tool_use_id} name={tool_name} error={is_error}] {output}"
|
"[tool_result id={tool_use_id} name={tool_name} error={is_error}] {output}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
ContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} => {
|
||||||
|
lines.push(format!("[thinking signature={signature:?}] {thinking}"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push(String::new());
|
lines.push(String::new());
|
||||||
|
|
@ -6846,6 +6852,17 @@ fn render_session_markdown(session: &Session, session_id: &str, session_path: &P
|
||||||
}
|
}
|
||||||
lines.push(String::new());
|
lines.push(String::new());
|
||||||
}
|
}
|
||||||
|
ContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} => {
|
||||||
|
lines.push(format!("**Thinking** _{signature:?}_"));
|
||||||
|
let summary = summarize_tool_payload_for_markdown(thinking);
|
||||||
|
if !summary.is_empty() {
|
||||||
|
lines.push(format!("> {summary}"));
|
||||||
|
}
|
||||||
|
lines.push(String::new());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(usage) = message.usage {
|
if let Some(usage) = message.usage {
|
||||||
|
|
@ -7691,13 +7708,16 @@ impl AnthropicRuntimeClient {
|
||||||
input.push_str(&partial_json);
|
input.push_str(&partial_json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ContentBlockDelta::ThinkingDelta { .. } => {
|
ContentBlockDelta::ThinkingDelta { thinking } => {
|
||||||
if !block_has_thinking_summary {
|
if !block_has_thinking_summary {
|
||||||
render_thinking_block_summary(out, None, false)?;
|
render_thinking_block_summary(out, None, false)?;
|
||||||
block_has_thinking_summary = true;
|
block_has_thinking_summary = true;
|
||||||
}
|
}
|
||||||
|
events.push(AssistantEvent::ThinkingDelta(thinking));
|
||||||
|
}
|
||||||
|
ContentBlockDelta::SignatureDelta { signature } => {
|
||||||
|
events.push(AssistantEvent::SignatureDelta(signature));
|
||||||
}
|
}
|
||||||
ContentBlockDelta::SignatureDelta { .. } => {}
|
|
||||||
},
|
},
|
||||||
ApiStreamEvent::ContentBlockStop(_) => {
|
ApiStreamEvent::ContentBlockStop(_) => {
|
||||||
block_has_thinking_summary = false;
|
block_has_thinking_summary = false;
|
||||||
|
|
@ -8840,6 +8860,13 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|block| match block {
|
.map(|block| match block {
|
||||||
ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() },
|
ContentBlock::Text { text } => InputContentBlock::Text { text: text.clone() },
|
||||||
|
ContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} => InputContentBlock::Thinking {
|
||||||
|
thinking: thinking.clone(),
|
||||||
|
signature: signature.clone(),
|
||||||
|
},
|
||||||
ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
|
ContentBlock::ToolUse { id, name, input } => InputContentBlock::ToolUse {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
name: name.clone(),
|
name: name.clone(),
|
||||||
|
|
|
||||||
|
|
@ -4777,6 +4777,13 @@ fn convert_messages(messages: &[ConversationMessage]) -> Vec<InputMessage> {
|
||||||
}],
|
}],
|
||||||
is_error: *is_error,
|
is_error: *is_error,
|
||||||
},
|
},
|
||||||
|
ContentBlock::Thinking {
|
||||||
|
thinking,
|
||||||
|
signature,
|
||||||
|
} => InputContentBlock::Thinking {
|
||||||
|
thinking: thinking.clone(),
|
||||||
|
signature: signature.clone(),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
(!content.is_empty()).then(|| InputMessage {
|
(!content.is_empty()).then(|| InputMessage {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue