This commit is contained in:
runstone 2026-06-12 10:31:15 +08:00
parent ef1697d8a8
commit 9939e3191f
2 changed files with 82 additions and 2 deletions

View File

@ -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<serde_json::Value> = 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<serde_json::Value> = 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<serde_json::Value> = 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!({

View File

@ -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<String>,
/// 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<ToolCallRef>,
}
/// 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<String, String> {
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),
};