PicoBot/src/tools/wait_tool.rs
oudecheng 1019dbe8cc refactor(code-quality): 清理 clippy 存量告警(unwrap/clone/redundant 等)
- 移除无用克隆与冗余引用,减少不必要内存分配
- 规范 unwrap/expect 使用,修复可提前失败路径
- 修复 anthropic provider llm_timeout_secs 死代码并补全超时日志
- cargo fmt 统一格式
2026-08-16 23:22:22 +08:00

249 lines
9.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use std::time::Duration;
use async_trait::async_trait;
use serde_json::json;
use crate::tools::{Tool, ToolContext, ToolResult, WaitEvent};
/// wait_for_subagents 工具 — 等待异步子代理完成或用户消息到达。
///
/// 调用后释放 serial_lock进入 select! 等待:
/// - 子代理完成 → 返回结果 + 未完成列表
/// - 用户消息到达 → 返回 "有新用户消息"(消息已注入 history
/// - 超时 → 返回超时 + 未完成列表
///
/// 等待结束后重新获取 serial_lock保证后续工具调用串行。
pub struct WaitForSubagentsTool {
/// 默认超时LLM 未指定时使用
default_timeout_secs: u64,
}
impl WaitForSubagentsTool {
pub const TOOL_NAME: &'static str = "wait_for_subagents";
pub fn new(default_timeout_secs: u64) -> Self {
Self {
default_timeout_secs,
}
}
}
#[async_trait]
impl Tool for WaitForSubagentsTool {
fn name(&self) -> &str {
Self::TOOL_NAME
}
fn description(&self) -> &str {
"Wait for asynchronous subagents to complete, or for new user messages to arrive. \
Use this after launching subagents via the task tool to receive their results. \
Returns the first completed subagent's result and a list of still-pending task IDs. \
If pending_task_ids is non-empty, call this tool again to wait for the next one."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"timeout_secs": {
"type": "integer",
"description": "Maximum seconds to wait. Default 60. The tool returns early if a subagent completes or a user message arrives.",
"default": 60
}
},
"required": []
})
}
fn read_only(&self) -> bool {
false
}
fn exclusive(&self) -> bool {
// wait 工具释放/重获取 serial_lock不应与其他工具并发
true
}
async fn execute(&self, _args: serde_json::Value) -> anyhow::Result<ToolResult> {
Ok(ToolResult {
success: false,
output: String::new(),
error: Some(
"wait_for_subagents requires tool context with wait_coordinator".to_string(),
),
})
}
async fn execute_with_context(
&self,
context: &ToolContext,
args: serde_json::Value,
) -> anyhow::Result<ToolResult> {
// 1. 获取 wait_coordinator仅主 agent 有值)
let coordinator = match &context.wait_coordinator {
Some(c) => c.clone(),
None => {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(
"wait_for_subagents is not available in this context (no wait_coordinator)"
.to_string(),
),
});
}
};
// 2. 解析超时参数
let timeout_secs = args
.get("timeout_secs")
.and_then(|v| v.as_u64())
.unwrap_or(self.default_timeout_secs);
let timeout = Duration::from_secs(timeout_secs);
// 3. 先尝试排空队列中已缓冲的结果
// 场景:子代理已完成 → 结果 send 到队列 + DB 更新为 completed
// 但 LLM 上轮未调用 wait或 wait 超时未消费)→ 结果缓冲在队列中。
// 若不排空query_pending_task_ids 返回空DB 已非 running
// 返回 "No pending" → 缓冲结果永远丢失。
let drained = coordinator.try_drain_queued_results().await;
if !drained.is_empty() {
let pending_after = coordinator.query_pending_task_ids();
let pending_str = if pending_after.is_empty() {
"none".to_string()
} else {
pending_after.join(", ")
};
let formatted: Vec<String> = drained
.iter()
.map(|r| {
format!(
"Subagent {} completed (status: {:?}). Output: {}",
r.task_id, r.status, r.output
)
})
.collect();
return Ok(ToolResult {
success: true,
output: format!(
"Retrieved {} buffered subagent result(s):\n{}\nStill pending: [{}]",
drained.len(),
formatted.join("\n"),
pending_str
),
error: None,
});
}
// 4. 查询 pending 子代理
let pending = coordinator.query_pending_task_ids();
if pending.is_empty() {
return Ok(ToolResult {
success: true,
output: "No pending subagents to wait for.".to_string(),
error: None,
});
}
tracing::info!(
topic_id = ?context.topic_id,
pending_count = pending.len(),
timeout_secs,
"wait_for_subagents: entering wait"
);
// 4. 进入等待coordinator 内部:释放锁 → select! → 重获取锁)
// 传入 cancel_rx 使 /stop 命令能立即中断等待。
// coordinator 在 select! 中以 biased 优先级处理:
// 子代理结果 > 用户消息 > 取消信号 > 超时
let event = coordinator.wait(timeout, context.cancel_rx.clone()).await;
// 5. 格式化返回结果
let output = match event {
WaitEvent::SubagentResult(result) => {
let pending_str = if result.pending_task_ids.is_empty() {
"none".to_string()
} else {
result.pending_task_ids.join(", ")
};
format!(
"Subagent {} completed (status: {:?}). Output: {}\nStill pending: [{}]",
result.task_id, result.status, result.output, pending_str
)
}
WaitEvent::UserMessage(messages) => {
let pending = coordinator.query_pending_task_ids();
let pending_str = if pending.is_empty() {
"none".to_string()
} else {
pending.join(", ")
};
if messages.is_empty() {
format!(
"A new user message arrived while waiting (content could not be retrieved). \
Still pending subagents: [{}]",
pending_str
)
} else {
let formatted_msgs: Vec<String> = messages
.iter()
.enumerate()
.map(|(i, msg)| format!(" [{}] {}", i + 1, msg))
.collect();
format!(
"New user message(s) arrived while waiting:\n{}\n\
These messages have been added to the conversation history. \
Still pending subagents: [{}]",
formatted_msgs.join("\n"),
pending_str
)
}
}
WaitEvent::Timeout => {
let pending = coordinator.query_pending_task_ids();
format!(
"Wait timed out after {}s. Still pending subagents: [{}]",
timeout_secs,
if pending.is_empty() {
"none".to_string()
} else {
pending.join(", ")
}
)
}
WaitEvent::Cancelled => {
// /stop 命令中断了等待。coordinator 已完成全部状态清理
//(重获取 serial_lock、回填 guard、清除 is_waiting、归还 receiver
// 返回提示性输出Agent 下一轮迭代会检测到 cancel 并退出。
let pending = coordinator.query_pending_task_ids();
tracing::info!(
topic_id = ?context.topic_id,
pending_count = pending.len(),
"wait_for_subagents: cancelled by /stop"
);
format!(
"Wait was cancelled by /stop command. \
Pending subagents (if any) have been cancelled separately. \
Still pending in DB: [{}]",
if pending.is_empty() {
"none".to_string()
} else {
pending.join(", ")
}
)
}
};
tracing::info!(
topic_id = ?context.topic_id,
"wait_for_subagents: returning result"
);
Ok(ToolResult {
success: true,
output,
error: None,
})
}
}