feat(llm): LlmCycle 切换 IR 消息类型,移除 Phase 0 桥接层
核心改动: - LlmCycle.messages 从 Vec<OpenaiChatMessage> 切换为 Vec<Message>, 移除 Phase 0 引入的 chat_message_to_message / message_to_chat_message 转换函数 - 移除 LlmCycle.system_prompt 字段(FIX-D),调用方通过 Message::system_text() + with_messages() 管理系统提示; with_system_prompt() 标记 #[deprecated] 保留为过渡期 shim - submit_stream 简化(FIX-E):Item = StreamEvent 不再带 Result 包装, 错误用 StreamEvent::Error 传出;self.messages 不再自动 push_message, 调用方在收到 MessageComplete 后手动调用 cycle.push_message() - compact.rs 适配 Message 类型 + ContentBlock 估算;microcompact 跳过 is_error:true 的 ToolResult(FIX-F),保留 LLM 错误诊断 - ConversationMemory 持久化从 OpenaiChatMessage 切换为 Message - ToolInvocation 增加 tool_call_id 字段(FIX-A), invoke/invoke_all 签名增加 tool_call_id 参数 调用方迁移: - AgentSession::submit_turn 用 Message::system_text() + with_messages() - simple_visit example 同步迁移 测试 177 passed / 0 failed(compact 新增 5 个单元测试覆盖 FIX-F + 估计全变体)。
This commit is contained in:
+55
-16
@@ -15,6 +15,12 @@ use crate::tools::permission::PermissionChecker;
|
||||
/// 工具调用记录 —— 用于追踪和调试。
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToolInvocation {
|
||||
/// LLM 返回的 tool_call_id —— 用于回传 `Message::ToolResult` 时关联原始调用。
|
||||
///
|
||||
/// ponytail: Phase 2 引入。老的循环用 `tool_name` 冒充 tool_call_id,
|
||||
/// 对 OpenAI 碰巧可用,对 Anthropic 必然失败。Anthropic 协议要求
|
||||
/// `tool_result.tool_use_id` 与上一轮 `tool_use.id` 严格一致。
|
||||
pub tool_call_id: String,
|
||||
/// 被调用的工具名。
|
||||
pub tool_name: String,
|
||||
/// 工具的入参。
|
||||
@@ -25,8 +31,14 @@ pub struct ToolInvocation {
|
||||
|
||||
impl ToolInvocation {
|
||||
/// 创建一个新的工具调用记录。
|
||||
pub fn new(tool_name: String, input: Value, output: Result<Value, ToolError>) -> Self {
|
||||
pub fn new(
|
||||
tool_call_id: String,
|
||||
tool_name: String,
|
||||
input: Value,
|
||||
output: Result<Value, ToolError>,
|
||||
) -> Self {
|
||||
Self {
|
||||
tool_call_id,
|
||||
tool_name,
|
||||
input,
|
||||
output,
|
||||
@@ -128,7 +140,15 @@ impl ToolRegistry {
|
||||
}
|
||||
|
||||
/// 调用单个工具(含权限检查)。
|
||||
pub async fn invoke(&self, name: &str, args: Value) -> Result<ToolInvocation, ToolError> {
|
||||
///
|
||||
/// `tool_call_id` 来源于 LLM 流式响应中的 `tool_calls[i].id`,用于回传
|
||||
/// 工具结果时与原始 `tool_use` block 关联。
|
||||
pub async fn invoke(
|
||||
&self,
|
||||
tool_call_id: &str,
|
||||
name: &str,
|
||||
args: Value,
|
||||
) -> Result<ToolInvocation, ToolError> {
|
||||
let tool = self
|
||||
.get(name)
|
||||
.ok_or_else(|| ToolError::NotFound(name.to_string()))?;
|
||||
@@ -139,35 +159,48 @@ impl ToolRegistry {
|
||||
|
||||
let ctx = ToolContext::new(name, "");
|
||||
let output = tool.execute(args.clone(), &ctx).await;
|
||||
Ok(ToolInvocation::new(name.to_string(), args, output))
|
||||
Ok(ToolInvocation::new(
|
||||
tool_call_id.to_string(),
|
||||
name.to_string(),
|
||||
args,
|
||||
output,
|
||||
))
|
||||
}
|
||||
|
||||
/// 并行执行多个工具调用(互不依赖的工具)。
|
||||
///
|
||||
/// 每个工具独立超时(`timeout_per_call_secs`,0 表示不超时)。
|
||||
/// 单个工具超时不会影响其他工具的返回。
|
||||
///
|
||||
/// 入参元组为 `(tool_call_id, tool_name, args)` —— `tool_call_id` 来自 LLM 响应。
|
||||
pub async fn invoke_all(
|
||||
&self,
|
||||
calls: Vec<(String, Value)>,
|
||||
calls: Vec<(String, String, Value)>,
|
||||
timeout_per_call_secs: u64,
|
||||
) -> Vec<ToolInvocation> {
|
||||
let this = self.clone();
|
||||
let futures = calls.into_iter().map(|(name, args)| {
|
||||
let futures = calls.into_iter().map(|(tool_call_id, name, args)| {
|
||||
let this = this.clone();
|
||||
async move {
|
||||
match if timeout_per_call_secs == 0 {
|
||||
Ok(this.invoke(&name, args.clone()).await)
|
||||
Ok(this.invoke(&tool_call_id, &name, args.clone()).await)
|
||||
} else {
|
||||
tokio::time::timeout(
|
||||
Duration::from_secs(timeout_per_call_secs),
|
||||
this.invoke(&name, args.clone()),
|
||||
this.invoke(&tool_call_id, &name, args.clone()),
|
||||
)
|
||||
.await
|
||||
} {
|
||||
Ok(result) => result.unwrap_or_else(|e| {
|
||||
ToolInvocation::new(name.clone(), args.clone(), Err(e))
|
||||
ToolInvocation::new(
|
||||
tool_call_id.clone(),
|
||||
name.clone(),
|
||||
args.clone(),
|
||||
Err(e),
|
||||
)
|
||||
}),
|
||||
Err(_) => ToolInvocation::new(
|
||||
tool_call_id,
|
||||
name,
|
||||
args,
|
||||
Err(ToolError::McpTimeout("timeout".into())),
|
||||
@@ -313,15 +346,17 @@ mod tests {
|
||||
async fn test_invoke_success() {
|
||||
let mut reg = ToolRegistry::new();
|
||||
reg.register(Arc::new(AddTool { base: 100 })).unwrap();
|
||||
let result = reg.invoke("add", json!({ "n": 5 })).await.unwrap();
|
||||
let result = reg.invoke("call_1", "add", json!({ "n": 5 })).await.unwrap();
|
||||
let value = result.output.unwrap();
|
||||
assert_eq!(value["result"], 105);
|
||||
assert_eq!(result.tool_call_id, "call_1");
|
||||
assert_eq!(result.tool_name, "add");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_invoke_not_found() {
|
||||
let reg = ToolRegistry::new();
|
||||
let result = reg.invoke("nope", json!({})).await;
|
||||
let result = reg.invoke("call_x", "nope", json!({})).await;
|
||||
assert!(matches!(result, Err(ToolError::NotFound(_))));
|
||||
}
|
||||
|
||||
@@ -329,7 +364,7 @@ mod tests {
|
||||
async fn test_invoke_execution_error() {
|
||||
let mut reg = ToolRegistry::new();
|
||||
reg.register(Arc::new(FailTool)).unwrap();
|
||||
let result = reg.invoke("fail", json!({})).await.unwrap();
|
||||
let result = reg.invoke("call_y", "fail", json!({})).await.unwrap();
|
||||
assert!(result.output.is_err());
|
||||
}
|
||||
|
||||
@@ -338,7 +373,7 @@ mod tests {
|
||||
let mut reg = ToolRegistry::new()
|
||||
.with_permission_checker(PermissionChecker::new(Default::default()));
|
||||
reg.register(Arc::new(ShellTool)).unwrap();
|
||||
let result = reg.invoke("shell", json!({})).await;
|
||||
let result = reg.invoke("call_z", "shell", json!({})).await;
|
||||
assert!(matches!(result, Err(ToolError::PermissionDenied(_, _))));
|
||||
}
|
||||
|
||||
@@ -348,12 +383,15 @@ mod tests {
|
||||
reg.register(Arc::new(AddTool { base: 1 })).unwrap();
|
||||
reg.register(Arc::new(FailTool)).unwrap();
|
||||
let calls = vec![
|
||||
("add".into(), json!({ "n": 1 })),
|
||||
("add".into(), json!({ "n": 2 })),
|
||||
("fail".into(), json!({})),
|
||||
("c1".into(), "add".into(), json!({ "n": 1 })),
|
||||
("c2".into(), "add".into(), json!({ "n": 2 })),
|
||||
("c3".into(), "fail".into(), json!({})),
|
||||
];
|
||||
let results = reg.invoke_all(calls, 0).await;
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(results[0].tool_call_id, "c1");
|
||||
assert_eq!(results[1].tool_call_id, "c2");
|
||||
assert_eq!(results[2].tool_call_id, "c3");
|
||||
assert!(results[0].output.is_ok());
|
||||
assert!(results[1].output.is_ok());
|
||||
assert!(results[2].output.is_err());
|
||||
@@ -363,9 +401,10 @@ mod tests {
|
||||
async fn test_invoke_all_with_timeout() {
|
||||
let mut reg = ToolRegistry::new();
|
||||
reg.register(Arc::new(AddTool { base: 0 })).unwrap();
|
||||
let calls = vec![("add".into(), json!({ "n": 1 }))];
|
||||
let calls = vec![("c1".into(), "add".into(), json!({ "n": 1 }))];
|
||||
let results = reg.invoke_all(calls, 5).await;
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0].tool_call_id, "c1");
|
||||
assert!(results[0].output.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user