diff --git a/crates/llm/AGENTS.md b/crates/llm/AGENTS.md index 6cdea4e..bf9479d 100644 --- a/crates/llm/AGENTS.md +++ b/crates/llm/AGENTS.md @@ -59,15 +59,16 @@ Trait-based LLM client implementations for multiple providers. - when converting assistant tool calls into provider requests, the function `arguments` include the original tool-call `id` under the `_id` field - chat messages are an enum of `UserMessage`, `AssistantMessage`, `SystemMessage`, and `ToolMessage`, each with only relevant fields - `AssistantMessage` holds a `Vec` for text, tool calls, and thinking segments + - each assistant part carries optional `encrypted_content`; GeminiRust forwards it to and restores it from the Gemini `thought_signature` - tool calls include an `id` string, assigned locally when missing - tool messages carry the same `id` and store results via `JsonResult` (`content` or `error`) - Chat message, request, and response types serialize to and from JSON - skips serializing fields that are `None`, empty strings, or empty arrays - Responses - - `ResponseChunk` is an enum of `Thinking`, `ToolCall`, `Content`, `Usage`, or `Done` + - `ResponseChunk` emits `Part(AssistantPart)` items alongside `Usage` and `Done` - usage chunks carry `input_tokens` and `output_tokens` - - tool call chunks hold a single `ToolCall` and repeat as needed - - thinking, tool calls, and content stream first, followed by optional usage then `Done` + - streaming text and thinking segments arrive as assistant parts; consecutive parts without `encrypted_content` are merged while segments with encrypted data remain isolated + - tool call parts stream as single `AssistantPart::ToolCall` values - OpenAiChat client converts assistant history messages with tool calls into request `tool_calls` and stitches streaming tool call deltas into complete tool calls - OpenAiChat client parses `reasoning_content` from streamed responses into thinking text - Tool orchestration diff --git a/crates/llm/src/gemini_rust.rs b/crates/llm/src/gemini_rust.rs index f07b1cc..d1ca9a2 100644 --- a/crates/llm/src/gemini_rust.rs +++ b/crates/llm/src/gemini_rust.rs @@ -66,21 +66,27 @@ impl LlmClient for GeminiRustClient { let mut parts_vec: Vec = Vec::new(); for part in a.content { match part { - AssistantPart::Text { text } => { + AssistantPart::Text { + text, + encrypted_content, + } => { parts_vec.push(Part::Text { text, thought: None, - thought_signature: None, + thought_signature: encrypted_content, }); } - AssistantPart::ToolCall(tc) => { - let args = match tc.arguments { - JsonResult::Content { .. } => tc.arguments_content_with_id(), + AssistantPart::ToolCall { + call, + encrypted_content, + } => { + let args = match call.arguments { + JsonResult::Content { .. } => call.arguments_content_with_id(), JsonResult::Error { .. } => Value::Null, }; parts_vec.push(Part::FunctionCall { - function_call: gemini_rust::FunctionCall::new(tc.name, args), - thought_signature: None, + function_call: gemini_rust::FunctionCall::new(call.name, args), + thought_signature: encrypted_content, }); } AssistantPart::Thinking { .. } => {} @@ -170,24 +176,36 @@ impl LlmClient for GeminiRustClient { Part::Text { text, thought, - thought_signature: _, + thought_signature, } => { + let encrypted_content = thought_signature.clone(); if thought.unwrap_or(false) { - out.push(Ok(ResponseChunk::Thinking(text.clone()))); + out.push(Ok(ResponseChunk::Part( + AssistantPart::Thinking { + text: text.clone(), + encrypted_content, + }, + ))); } else if !text.is_empty() { - out.push(Ok(ResponseChunk::Content(text.clone()))); + out.push(Ok(ResponseChunk::Part(AssistantPart::Text { + text: text.clone(), + encrypted_content, + }))); } } Part::FunctionCall { function_call, - thought_signature: _, + thought_signature, } => { - out.push(Ok(ResponseChunk::ToolCall(ToolCall { - id: Uuid::new_v4().to_string(), - name: function_call.name.clone(), - arguments: JsonResult::Content { - content: function_call.args.clone(), + out.push(Ok(ResponseChunk::Part(AssistantPart::ToolCall { + call: ToolCall { + id: Uuid::new_v4().to_string(), + name: function_call.name.clone(), + arguments: JsonResult::Content { + content: function_call.args.clone(), + }, }, + encrypted_content: thought_signature.clone(), }))); } _ => {} diff --git a/crates/llm/src/harmony.rs b/crates/llm/src/harmony.rs index 33c310e..54be65a 100644 --- a/crates/llm/src/harmony.rs +++ b/crates/llm/src/harmony.rs @@ -120,29 +120,29 @@ fn build_prompt( ChatMessage::Assistant(a) => { for part in &a.content { match part { - AssistantPart::Thinking { text } => { + AssistantPart::Thinking { text, .. } => { convo_msgs.push( Message::from_role_and_content(Role::Assistant, text.clone()) .with_channel("analysis"), ); } - AssistantPart::Text { text } => { + AssistantPart::Text { text, .. } => { convo_msgs.push( Message::from_role_and_content(Role::Assistant, text.clone()) .with_channel("final"), ); } - AssistantPart::ToolCall(tc) => { - let args = match &tc.arguments { + AssistantPart::ToolCall { call, .. } => { + let args = match &call.arguments { JsonResult::Content { .. } => { - tc.arguments_content_with_id().to_string() + call.arguments_content_with_id().to_string() } JsonResult::Error { error } => error.clone(), }; convo_msgs.push( Message::from_role_and_content(Role::Assistant, args) .with_channel("commentary") - .with_recipient(format!("functions.{}", tc.name)) + .with_recipient(format!("functions.{}", call.name)) .with_content_type("<|constrain|>json"), ); } @@ -303,9 +303,17 @@ impl LlmClient for HarmonyClient { if !delta.is_empty() && parser.current_recipient().is_none() { match parser.current_channel().as_deref() { Some("analysis") => { - out.push(Ok(ResponseChunk::Thinking(delta))) + out.push(Ok(ResponseChunk::Part(AssistantPart::Thinking { + text: delta, + encrypted_content: None, + }))) + } + Some("final") => { + out.push(Ok(ResponseChunk::Part(AssistantPart::Text { + text: delta, + encrypted_content: None, + }))) } - Some("final") => out.push(Ok(ResponseChunk::Content(delta))), _ => {} } } @@ -328,10 +336,13 @@ impl LlmClient for HarmonyClient { error: text.clone(), }, }; - out.push(Ok(ResponseChunk::ToolCall(ToolCall { - id: Uuid::new_v4().to_string(), - name: name.to_string(), - arguments, + out.push(Ok(ResponseChunk::Part(AssistantPart::ToolCall { + call: ToolCall { + id: Uuid::new_v4().to_string(), + name: name.to_string(), + arguments, + }, + encrypted_content: None, }))); } } @@ -393,6 +404,7 @@ mod tests { ChatMessage::Assistant(AssistantMessage { content: vec![AssistantPart::Thinking { text: "ponder".into(), + encrypted_content: None, }], }), ]); @@ -424,9 +436,11 @@ mod tests { content: vec![ AssistantPart::Thinking { text: "ponder".into(), + encrypted_content: None, }, AssistantPart::Text { text: "Hello".into(), + encrypted_content: None, }, ], }), @@ -435,9 +449,11 @@ mod tests { content: vec![ AssistantPart::Thinking { text: "think".into(), + encrypted_content: None, }, AssistantPart::Text { text: "I'm good".into(), + encrypted_content: None, }, ], }), @@ -459,13 +475,16 @@ mod tests { let (_, prompt, prefill_tokens, _) = setup(vec![ ChatMessage::user("2+2?".into()), ChatMessage::Assistant(AssistantMessage { - content: vec![AssistantPart::ToolCall(ToolCall { - id: "1".into(), - name: "add".into(), - arguments: JsonResult::Content { - content: json!({"a": 2, "b": 2}), + content: vec![AssistantPart::ToolCall { + call: ToolCall { + id: "1".into(), + name: "add".into(), + arguments: JsonResult::Content { + content: json!({"a": 2, "b": 2}), + }, }, - })], + encrypted_content: None, + }], }), ChatMessage::tool( "1".into(), diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index 379ab50..b28a357 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -25,7 +25,10 @@ impl ChatMessage { pub fn assistant(content: String) -> Self { Self::Assistant(AssistantMessage { - content: vec![AssistantPart::Text { text: content }], + content: vec![AssistantPart::Text { + text: content, + encrypted_content: None, + }], }) } @@ -106,9 +109,22 @@ impl ToolCall { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AssistantPart { - Text { text: String }, - ToolCall(ToolCall), - Thinking { text: String }, + Text { + text: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + encrypted_content: Option, + }, + ToolCall { + #[serde(flatten)] + call: ToolCall, + #[serde(default, skip_serializing_if = "Option::is_none")] + encrypted_content: Option, + }, + Thinking { + text: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + encrypted_content: Option, + }, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -225,9 +241,7 @@ pub fn client_from( #[derive(Debug, Clone)] pub enum ResponseChunk { - Thinking(String), - ToolCall(ToolCall), - Content(String), + Part(AssistantPart), Usage { input_tokens: u32, output_tokens: u32, diff --git a/crates/llm/src/ollama.rs b/crates/llm/src/ollama.rs index aed7006..6373460 100644 --- a/crates/llm/src/ollama.rs +++ b/crates/llm/src/ollama.rs @@ -57,24 +57,24 @@ impl LlmClient for OllamaClient { OllamaChatMessage::new(OllamaMessageRole::Assistant, String::new()); for part in a.content { match part { - AssistantPart::Text { text } => { + AssistantPart::Text { text, .. } => { msg.content.push_str(&text); } - AssistantPart::ToolCall(tc) => { - let args = match tc.arguments { + AssistantPart::ToolCall { call, .. } => { + let args = match call.arguments { JsonResult::Content { .. } => { - tc.arguments_content_with_id() + call.arguments_content_with_id() } JsonResult::Error { .. } => Value::Null, }; msg.tool_calls.push(OllamaToolCall { function: OllamaToolCallFunction { - name: tc.name, + name: call.name, arguments: args, }, }); } - AssistantPart::Thinking { text } => { + AssistantPart::Thinking { text, .. } => { let thinking = msg.thinking.get_or_insert_with(String::new); thinking.push_str(&text); } @@ -126,7 +126,10 @@ impl LlmClient for OllamaClient { let mut out: Vec>> = Vec::new(); if !r.message.thinking.clone().unwrap_or_default().is_empty() { if let Some(thinking) = r.message.thinking.clone() { - out.push(Ok(ResponseChunk::Thinking(thinking))); + out.push(Ok(ResponseChunk::Part(AssistantPart::Thinking { + text: thinking, + encrypted_content: None, + }))); } } let tool_calls: Vec = r @@ -142,10 +145,16 @@ impl LlmClient for OllamaClient { }) .collect(); for tc in tool_calls { - out.push(Ok(ResponseChunk::ToolCall(tc))); + out.push(Ok(ResponseChunk::Part(AssistantPart::ToolCall { + call: tc, + encrypted_content: None, + }))); } if !r.message.content.is_empty() { - out.push(Ok(ResponseChunk::Content(r.message.content))); + out.push(Ok(ResponseChunk::Part(AssistantPart::Text { + text: r.message.content, + encrypted_content: None, + }))); } if r.done { if let Some(f) = r.final_data.as_ref() { diff --git a/crates/llm/src/openai_chat.rs b/crates/llm/src/openai_chat.rs index cfb8494..c584c35 100644 --- a/crates/llm/src/openai_chat.rs +++ b/crates/llm/src/openai_chat.rs @@ -77,9 +77,9 @@ impl LlmClient for OpenAiChatClient { let mut tool_calls_acc: Vec = Vec::new(); for part in a.content { match part { - AssistantPart::Text { text } => content_acc.push_str(&text), - AssistantPart::Thinking { text } => thinking_acc.push_str(&text), - AssistantPart::ToolCall(tc) => tool_calls_acc.push(tc), + AssistantPart::Text { text, .. } => content_acc.push_str(&text), + AssistantPart::Thinking { text, .. } => thinking_acc.push_str(&text), + AssistantPart::ToolCall { call, .. } => tool_calls_acc.push(call), } } if !content_acc.is_empty() { @@ -255,13 +255,22 @@ impl LlmClient for OpenAiChatClient { None }; if !thinking_acc.is_empty() { - out.push(Ok(ResponseChunk::Thinking(thinking_acc))); + out.push(Ok(ResponseChunk::Part(AssistantPart::Thinking { + text: thinking_acc, + encrypted_content: None, + }))); } for tc in tool_calls { - out.push(Ok(ResponseChunk::ToolCall(tc))); + out.push(Ok(ResponseChunk::Part(AssistantPart::ToolCall { + call: tc, + encrypted_content: None, + }))); } if !content_acc.is_empty() { - out.push(Ok(ResponseChunk::Content(content_acc))); + out.push(Ok(ResponseChunk::Part(AssistantPart::Text { + text: content_acc, + encrypted_content: None, + }))); } if let Some((input_tokens, output_tokens)) = usage { out.push(Ok(ResponseChunk::Usage { diff --git a/crates/llm/src/test_provider.rs b/crates/llm/src/test_provider.rs index 0e8b898..ab5bd41 100644 --- a/crates/llm/src/test_provider.rs +++ b/crates/llm/src/test_provider.rs @@ -52,7 +52,7 @@ impl LlmClient for TestProvider { mod tests { use super::*; use crate::tools::{ToolExecutor, run_tool_loop}; - use crate::{ChatMessage, JsonResult, ToolCall}; + use crate::{AssistantPart, ChatMessage, JsonResult, ToolCall}; use serde_json::Value; use std::sync::{Arc, Mutex}; @@ -73,17 +73,23 @@ mod tests { async fn captures_requests_and_iterates() { let client = Arc::new(TestProvider::new()); client.enqueue(vec![ - ResponseChunk::ToolCall(ToolCall { - id: "call-1".into(), - name: "test".into(), - arguments: JsonResult::Content { - content: Value::Null, + ResponseChunk::Part(AssistantPart::ToolCall { + call: ToolCall { + id: "call-1".into(), + name: "test".into(), + arguments: JsonResult::Content { + content: Value::Null, + }, }, + encrypted_content: None, }), ResponseChunk::Done, ]); client.enqueue(vec![ - ResponseChunk::Content("final".into()), + ResponseChunk::Part(AssistantPart::Text { + text: "final".into(), + encrypted_content: None, + }), ResponseChunk::Done, ]); let exec = Arc::new(DummyExec); @@ -103,7 +109,7 @@ mod tests { if let ChatMessage::Assistant(a) = final_msg { assert_eq!(a.content.len(), 1); match &a.content[0] { - crate::AssistantPart::Text { text } => assert_eq!(text, "final"), + crate::AssistantPart::Text { text, .. } => assert_eq!(text, "final"), _ => panic!("expected text part"), } } else { diff --git a/crates/llm/src/tools.rs b/crates/llm/src/tools.rs index 9caf951..21dfa55 100644 --- a/crates/llm/src/tools.rs +++ b/crates/llm/src/tools.rs @@ -74,71 +74,104 @@ pub async fn run_tool_loop( while let Some(chunk) = stream.next().await { let chunk = chunk?; let mut done = false; - match &chunk { - ResponseChunk::Content(content) => { - if let Some(AssistantPart::Text { text }) = current_part.as_mut() { - text.push_str(content); - } else { - if let Some(part) = current_part.take() { - parts.push(part); + match chunk.clone() { + ResponseChunk::Part(part) => match part { + AssistantPart::Text { + text, + encrypted_content, + } => { + // Merge unless there's encrypted content. + if let ( + Some(AssistantPart::Text { + encrypted_content: None, + text: current_text, + }), + None, + ) = (current_part.as_mut(), encrypted_content.as_ref()) + { + current_text.push_str(&text); + } else { + if let Some(existing) = current_part.take() { + parts.push(existing); + } + current_part = Some(AssistantPart::Text { + text, + encrypted_content, + }); } - current_part = Some(AssistantPart::Text { - text: content.into(), - }); } - } - ResponseChunk::Thinking(thinking) => { - if let Some(AssistantPart::Thinking { text }) = current_part.as_mut() { - text.push_str(thinking); - } else { - if let Some(part) = current_part.take() { - parts.push(part); + AssistantPart::Thinking { + text, + encrypted_content, + } => { + // Merge unless there's encrypted content. + if let ( + Some(AssistantPart::Thinking { + encrypted_content: None, + text: current_text, + }), + None, + ) = (current_part.as_mut(), encrypted_content.as_ref()) + { + current_text.push_str(&text); + } else { + if let Some(existing) = current_part.take() { + parts.push(existing); + } + current_part = Some(AssistantPart::Thinking { + text, + encrypted_content, + }); } - current_part = Some(AssistantPart::Thinking { - text: thinking.into(), - }); - } - } - ResponseChunk::ToolCall(tc) => { - if let Some(part) = current_part.take() { - parts.push(part); } - current_part = Some(AssistantPart::ToolCall(tc.clone())); - tx.send(ToolEvent::ToolStarted { - call_id: tc.id.clone(), - name: tc.name.clone(), - args: tc.arguments.clone(), - }) - .ok(); - let executor = tool_executor.clone(); - let name = tc.name.clone(); - let args = tc.arguments.clone(); - let call_id = tc.id.clone(); - handles.spawn(async move { - match args { - JsonResult::Content { content } => { - let res = executor.call(&name, content).await; - (call_id, name, res) - } - JsonResult::Error { .. } => ( - call_id, - name, - Err::>(Box::new( - std::io::Error::new( - std::io::ErrorKind::Other, - "Could not parse arguments as JSON", - ), - )), - ), + AssistantPart::ToolCall { + call, + encrypted_content, + } => { + if let Some(existing) = current_part.take() { + parts.push(existing); } - }); - } + let part = AssistantPart::ToolCall { + call: call.clone(), + encrypted_content, + }; + tx.send(ToolEvent::ToolStarted { + call_id: call.id.clone(), + name: call.name.clone(), + args: call.arguments.clone(), + }) + .ok(); + let executor = tool_executor.clone(); + let name = call.name.clone(); + let args = call.arguments.clone(); + let call_id = call.id.clone(); + current_part = Some(part); + handles.spawn(async move { + match args { + JsonResult::Content { content } => { + let res = executor.call(&name, content).await; + (call_id, name, res) + } + JsonResult::Error { .. } => ( + call_id, + name, + Err::>(Box::new( + std::io::Error::new( + std::io::ErrorKind::Other, + "Could not parse arguments as JSON", + ), + )), + ), + } + }); + } + }, ResponseChunk::Usage { .. } => {} ResponseChunk::Done => { done = true; } } - tx.send(ToolEvent::Chunk(chunk)).ok(); + tx.send(ToolEvent::Chunk(chunk.clone())).ok(); if done { break; } @@ -194,7 +227,7 @@ pub async fn run_tool_loop( #[cfg(test)] mod tests { use super::*; - use crate::JsonResult; + use crate::{AssistantPart, JsonResult}; use serde_json::Value; use std::sync::{Arc, Mutex}; use tokio_stream::{self}; @@ -213,18 +246,27 @@ mod tests { *calls += 1; let stream: Vec>> = match *calls { 1 => vec![ - Ok(ResponseChunk::Content("first".into())), - Ok(ResponseChunk::ToolCall(crate::ToolCall { - id: "call-1".into(), - name: "test".into(), - arguments: JsonResult::Content { - content: Value::Null, + Ok(ResponseChunk::Part(AssistantPart::Text { + text: "first".into(), + encrypted_content: None, + })), + Ok(ResponseChunk::Part(AssistantPart::ToolCall { + call: crate::ToolCall { + id: "call-1".into(), + name: "test".into(), + arguments: JsonResult::Content { + content: Value::Null, + }, }, + encrypted_content: None, })), Ok(ResponseChunk::Done), ], 2 => vec![ - Ok(ResponseChunk::Content("final".into())), + Ok(ResponseChunk::Part(AssistantPart::Text { + text: "final".into(), + encrypted_content: None, + })), Ok(ResponseChunk::Done), ], _ => vec![], @@ -272,11 +314,11 @@ mod tests { if let ChatMessage::Assistant(a) = first_assistant { assert_eq!(a.content.len(), 2); match &a.content[0] { - AssistantPart::Text { text } => assert_eq!(text, "first"), + AssistantPart::Text { text, .. } => assert_eq!(text, "first"), _ => panic!("expected first part to be text"), } match &a.content[1] { - AssistantPart::ToolCall(tc) => assert_eq!(tc.name, "test"), + AssistantPart::ToolCall { call, .. } => assert_eq!(call.name, "test"), _ => panic!("expected second part to be tool call"), } } else { @@ -287,7 +329,7 @@ mod tests { if let ChatMessage::Assistant(a) = final_msg { assert_eq!(a.content.len(), 1); match &a.content[0] { - AssistantPart::Text { text } => assert_eq!(text, "final"), + AssistantPart::Text { text, .. } => assert_eq!(text, "final"), _ => panic!("expected text part"), } } else { @@ -300,7 +342,9 @@ mod tests { while let Ok(ev) = rx.try_recv() { match ev { ToolEvent::ToolResult { .. } => saw_tool = true, - ToolEvent::Chunk(ResponseChunk::Content(content)) if content == "final" => { + ToolEvent::Chunk(ResponseChunk::Part(AssistantPart::Text { text, .. })) + if text == "final" => + { saw_final = true } ToolEvent::RequestStarted => requests += 1, @@ -326,17 +370,23 @@ mod tests { *calls += 1; let stream: Vec>> = match *calls { 1 => vec![ - Ok(ResponseChunk::ToolCall(crate::ToolCall { - id: "call-1".into(), - name: "test".into(), - arguments: JsonResult::Error { - error: "nope".into(), + Ok(ResponseChunk::Part(AssistantPart::ToolCall { + call: crate::ToolCall { + id: "call-1".into(), + name: "test".into(), + arguments: JsonResult::Error { + error: "nope".into(), + }, }, + encrypted_content: None, })), Ok(ResponseChunk::Done), ], 2 => vec![ - Ok(ResponseChunk::Content("final".into())), + Ok(ResponseChunk::Part(AssistantPart::Text { + text: "final".into(), + encrypted_content: None, + })), Ok(ResponseChunk::Done), ], _ => vec![], diff --git a/crates/llment/src/app.rs b/crates/llment/src/app.rs index a99a614..96e836b 100644 --- a/crates/llment/src/app.rs +++ b/crates/llment/src/app.rs @@ -19,7 +19,7 @@ use crate::{ }; use crossterm::event::Event; use llm::{ - ChatMessage, ChatMessageRequest, JsonResult, Provider, ResponseChunk, + AssistantPart, ChatMessage, ChatMessageRequest, JsonResult, Provider, ResponseChunk, mcp::{McpContext, McpService}, tools::{ToolEvent, ToolExecutor, tool_event_stream}, }; @@ -214,19 +214,19 @@ impl App { let _ = self.model.needs_redraw.send(true); } ToolEvent::Chunk(chunk) => match chunk { - ResponseChunk::Thinking(thinking) => { + ResponseChunk::Part(AssistantPart::Thinking { text, .. }) => { self.state = ConversationState::Thinking; let _ = self.model.needs_redraw.send(true); - self.conversation.append_thinking(&thinking); + self.conversation.append_thinking(&text); } - ResponseChunk::Content(content) => { - if !content.is_empty() { + ResponseChunk::Part(AssistantPart::Text { text, .. }) => { + if !text.is_empty() { self.state = ConversationState::Responding; let _ = self.model.needs_redraw.send(true); - self.conversation.append_response(&content); + self.conversation.append_response(&text); } } - ResponseChunk::ToolCall(_) => {} + ResponseChunk::Part(AssistantPart::ToolCall { .. }) => {} ResponseChunk::Usage { input_tokens, output_tokens, diff --git a/crates/llment/src/conversation/conversation.rs b/crates/llment/src/conversation/conversation.rs index 203126d..b66a895 100644 --- a/crates/llment/src/conversation/conversation.rs +++ b/crates/llment/src/conversation/conversation.rs @@ -290,17 +290,17 @@ impl Conversation { ChatMessage::Assistant(a) => { for part in &a.content { match part { - AssistantPart::Thinking { text } => { + AssistantPart::Thinking { text, .. } => { if !text.is_empty() { self.append_thinking(text); } } - AssistantPart::Text { text } => { + AssistantPart::Text { text, .. } => { if !text.is_empty() { self.append_response(text); } } - AssistantPart::ToolCall(call) => { + AssistantPart::ToolCall { call, .. } => { let args = match &call.arguments { JsonResult::Content { content } => { to_string(content).unwrap_or_default() diff --git a/crates/llment/src/history_edits.rs b/crates/llment/src/history_edits.rs index 27dab55..a8cb247 100644 --- a/crates/llment/src/history_edits.rs +++ b/crates/llment/src/history_edits.rs @@ -18,7 +18,10 @@ pub type HistoryEdit = pub fn append_thought(text: String) -> HistoryEdit { Box::new(move |history: &mut Vec| { history.push(ChatMessage::Assistant(llm::AssistantMessage { - content: vec![AssistantPart::Thinking { text: text.clone() }], + content: vec![AssistantPart::Thinking { + text: text.clone(), + encrypted_content: None, + }], })); Ok(HistoryEditResult::default()) }) @@ -26,13 +29,20 @@ pub fn append_thought(text: String) -> HistoryEdit { pub fn append_response(text: String) -> HistoryEdit { Box::new(move |history: &mut Vec| { - let append = matches!(history.last(), Some(ChatMessage::Assistant(a)) if !a - .content - .iter() - .any(|p| matches!(p, AssistantPart::Text { .. } | AssistantPart::ToolCall(_)))); + let append = matches!( + history.last(), + Some(ChatMessage::Assistant(a)) + if !a + .content + .iter() + .any(|p| matches!(p, AssistantPart::Text { .. } | AssistantPart::ToolCall { .. })) + ); if append { if let Some(ChatMessage::Assistant(a)) = history.last_mut() { - a.content.push(AssistantPart::Text { text: text.clone() }); + a.content.push(AssistantPart::Text { + text: text.clone(), + encrypted_content: None, + }); } } else { history.push(ChatMessage::assistant(text.clone()));