diff --git a/crates/dbx-core/src/agent_loop.rs b/crates/dbx-core/src/agent_loop.rs index a9a0e80bb..59ab95141 100644 --- a/crates/dbx-core/src/agent_loop.rs +++ b/crates/dbx-core/src/agent_loop.rs @@ -106,11 +106,15 @@ pub async fn run_agent_loop( let accumulated_text = accumulated_text.lock().await.clone(); - // Add assistant message to conversation + // Add assistant message to conversation (including tool_use blocks) conversation_messages.push(AiMessage { role: "assistant".to_string(), content: accumulated_text.clone(), tool_call_id: None, + tool_calls: collected_tool_calls + .iter() + .map(|tc| ai::ToolCallRef { id: tc.id.clone(), name: tc.name.clone(), arguments: tc.arguments.clone() }) + .collect(), }); if collected_tool_calls.is_empty() { @@ -150,6 +154,7 @@ pub async fn run_agent_loop( role: "tool".to_string(), content: result.content.clone(), tool_call_id: Some(tc.id.clone()), + tool_calls: Vec::new(), }); } @@ -264,6 +269,22 @@ async fn call_openai_with_tools( if let Some(ref tc_id) = m.tool_call_id { msg["tool_call_id"] = json!(tc_id); } + } else if m.role == "assistant" && !m.tool_calls.is_empty() { + let calls: Vec = m + .tool_calls + .iter() + .map(|tc| { + json!({ + "id": tc.id, + "type": "function", + "function": { + "name": tc.name, + "arguments": tc.arguments.to_string() + } + }) + }) + .collect(); + msg["tool_calls"] = json!(calls); } msg })); @@ -336,6 +357,27 @@ async fn call_claude_with_tools( "content": m.content }] })); + } else if m.role == "assistant" && !m.tool_calls.is_empty() { + // Reconstruct assistant message with tool_use content blocks + let mut content_blocks: Vec = Vec::new(); + if !m.content.is_empty() { + content_blocks.push(json!({ + "type": "text", + "text": m.content + })); + } + for tc in &m.tool_calls { + content_blocks.push(json!({ + "type": "tool_use", + "id": tc.id, + "name": tc.name, + "input": tc.arguments + })); + } + messages.push(json!({ + "role": "assistant", + "content": content_blocks + })); } else { messages.push(json!({ "role": m.role, "content": m.content })); } @@ -417,6 +459,23 @@ async fn call_gemini_with_tools( } }] })); + } else if m.role == "assistant" && !m.tool_calls.is_empty() { + let mut parts: Vec = Vec::new(); + if !m.content.is_empty() { + parts.push(json!({ "text": m.content })); + } + for tc in &m.tool_calls { + parts.push(json!({ + "functionCall": { + "name": tc.name, + "args": tc.arguments + } + })); + } + contents.push(json!({ + "role": "model", + "parts": parts + })); } else { let role = if m.role == "assistant" { "model" } else { "user" }; contents.push(json!({ diff --git a/crates/dbx-core/src/ai.rs b/crates/dbx-core/src/ai.rs index 414f17863..ff216a7b3 100644 --- a/crates/dbx-core/src/ai.rs +++ b/crates/dbx-core/src/ai.rs @@ -103,6 +103,22 @@ pub struct AiMessage { /// a tool result with its originating tool call in multi-turn loops. #[serde(default, skip_serializing_if = "Option::is_none")] pub tool_call_id: Option, + /// Tool calls made by the assistant (role="assistant"). Used to + /// reconstruct tool_use content blocks for providers like Anthropic + /// that require them in the conversation history. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub tool_calls: Vec, +} + +/// A lightweight reference to a tool call within an assistant message. +/// Stores the id, name, and arguments needed to reconstruct provider-specific +/// tool_use content blocks (e.g. Anthropic's `{"type":"tool_use", ...}`). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ToolCallRef { + pub id: String, + pub name: String, + pub arguments: serde_json::Value, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -684,7 +700,12 @@ pub async fn test_connection_core(config: &AiConfig) -> Result { let request = AiCompletionRequest { config: config.clone(), system_prompt: String::new(), - messages: vec![AiMessage { role: "user".into(), content: "hi".into(), tool_call_id: None }], + messages: vec![AiMessage { + role: "user".into(), + content: "hi".into(), + tool_call_id: None, + tool_calls: Vec::new(), + }], max_tokens: Some(16), temperature: Some(0.0), };