PicoBot/src/tools/traits.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

162 lines
7.2 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::sync::Arc;
use std::time::Duration;
use async_trait::async_trait;
use tokio::sync::{mpsc, watch};
use crate::domain::CapabilityPolicy;
use crate::tools::task::SubagentResult;
#[derive(Debug, Clone)]
pub struct ToolResult {
pub success: bool,
pub output: String,
pub error: Option<String>,
}
/// wait_for_subagents 工具等待期间的事件。
#[derive(Debug, Clone)]
pub enum WaitEvent {
/// 一个子代理完成,携带其结果
SubagentResult(SubagentResult),
/// wait 期间有新用户消息到达(已注入 history携带新用户消息的内容列表
UserMessage(Vec<String>),
/// 等待超时
Timeout,
/// 收到取消信号(/stop等待已优雅终止状态已清理完毕
Cancelled,
}
/// wait_for_subagents 工具的协调器接口。
///
/// 封装了释放/重获取 serial_lock + select! 等待 + 管理等待状态的逻辑,
/// 使 wait 工具不直接依赖 gateway 模块(避免循环依赖)。
///
/// 具体实现 `SessionWaitCoordinator` 在 gateway 模块中,持有 Session 引用。
#[async_trait]
pub trait WaitCoordinator: Send + Sync + 'static {
/// 查询当前 topic 下仍处于 running 状态的子代理 task_id 列表。
fn query_pending_task_ids(&self) -> Vec<String>;
/// 尝试排空 sub_done_q 中已缓冲的子代理结果(非阻塞 drain但方法本身是 async
/// 因为需要获取 Session 锁)。
///
/// 用于两种场景:
/// 1. wait_for_subagents 入口处:即使 DB 中无 running 子代理,
/// 队列可能仍缓冲了已完成子代理的结果(子代理完成 → send 到队列 →
/// DB 更新为 completed但 LLM 上轮未消费队列)。
/// 2. wait() 进入 select! 前:多个子代理同时完成时,批量消费避免
/// 每个结果各触发一次 LLM 调用。
async fn try_drain_queued_results(&self) -> Vec<SubagentResult>;
/// 进入等待状态:释放 serial_lock → select! → 重获取 serial_lock。
///
/// 调用前提serial_lock 已被执行路径获取guard 存于 coordinator 内部。
/// 返回后serial_lock 已被重新获取waiting 标志已清除。
///
/// `cancel_rx`:可选的取消信号接收端。当收到信号时(/stop 命令),
/// select! 立即返回 `WaitEvent::Cancelled`,并完成完整的状态清理
///(重获取锁、回填 guard、清除 is_waiting、归还 receiver
/// 为 None 时退化为不检查取消(向后兼容,子代理场景)。
async fn wait(&self, timeout: Duration, cancel_rx: Option<watch::Receiver<()>>) -> WaitEvent;
}
#[derive(Clone, Default)]
pub struct ToolContext {
pub channel_name: Option<String>,
pub sender_id: Option<String>,
pub chat_id: Option<String>,
pub session_id: Option<String>,
pub topic_id: Option<String>,
pub message_id: Option<String>,
pub message_seq: Option<i64>,
/// 子代理标识,用于标注消息来源
pub subagent_description: Option<String>,
/// 当前嵌套深度0 = 主 agent1 = 子 agent2 = 孙 agent...
pub nesting_depth: u32,
/// 当前智能体自身的任务 ID主 agent 为 None子/孙 agent 为 Some(uuid)
pub task_id: Option<String>,
/// 父任务 ID仅子/孙智能体有值,用于构建任务层级)
pub parent_task_id: Option<String>,
/// 当前工具调用的 ID由 agent_loop 在执行前注入,用于精确关联 TaskStarted 事件)
pub tool_call_id: Option<String>,
/// 父智能体(主 agent 所选专家或上级子代理)的 capability 策略快照。
/// TaskTool 据此强制校验子代理加载(白/黑名单),与 spawn/resume 安全范式一致。
/// 以数据形式传递,避免 task 模块反向依赖 experts 模块。
pub parent_capability: Option<CapabilityPolicy>,
/// 端到端追踪 ID从 InboundMessage 继承,用于 tool 执行路径的日志关联)。
/// None 表示无追踪上下文(如子代理独立执行或测试环境)。
pub trace_id: Option<String>,
/// 异步子代理完成队列的 sender按 topic 隔离)。
/// 仅主 agentnesting_depth=0有值agent_factory 构建时从 SessionHistory 注入。
/// TaskTool spawn 异步子代理后,子代理完成时通过此 sender 发送 SubagentResult
/// 由 wait_for_subagents 工具的 receiver 端消费。
/// 子代理自身nesting_depth>0为 None嵌套层不支持异步走同步路径。
pub sub_done_sender: Option<mpsc::Sender<SubagentResult>>,
/// wait_for_subagents 工具的协调器(仅主 agent 有值)。
/// 封装了释放/重获取 serial_lock + select! 等待逻辑。
/// wait 工具通过此接口实现真等待(释放锁让 process_one 注入用户消息)。
pub wait_coordinator: Option<Arc<dyn WaitCoordinator>>,
/// 取消信号接收端(仅主 agent 有值,由 agent_factory 从 cancel_token clone 注入)。
/// wait_for_subagents 工具将其传给 coordinator.wait() 的 select!
/// 使 /stop 命令能立即中断等待并完成状态清理。
/// watch::Receiver 可安全 clone多个 receiver 共享同一 sender
/// 互不影响各自的 has_changed() / changed() 状态。
pub cancel_rx: Option<watch::Receiver<()>>,
}
impl std::fmt::Debug for ToolContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ToolContext")
.field("channel_name", &self.channel_name)
.field("sender_id", &self.sender_id)
.field("chat_id", &self.chat_id)
.field("session_id", &self.session_id)
.field("topic_id", &self.topic_id)
.field("message_id", &self.message_id)
.field("message_seq", &self.message_seq)
.field("subagent_description", &self.subagent_description)
.field("nesting_depth", &self.nesting_depth)
.field("task_id", &self.task_id)
.field("parent_task_id", &self.parent_task_id)
.field("tool_call_id", &self.tool_call_id)
.field("parent_capability", &self.parent_capability)
.field("trace_id", &self.trace_id)
.field("sub_done_sender", &self.sub_done_sender)
.field("wait_coordinator", &self.wait_coordinator.is_some())
.field("cancel_rx", &self.cancel_rx.is_some())
.finish()
}
}
#[async_trait]
pub trait Tool: Send + Sync + 'static {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<ToolResult>;
async fn execute_with_context(
&self,
_context: &ToolContext,
args: serde_json::Value,
) -> anyhow::Result<ToolResult> {
self.execute(args).await
}
/// Whether this tool is side-effect free and safe to parallelize.
fn read_only(&self) -> bool {
false
}
/// Whether this tool can run alongside other concurrency-safe tools.
fn concurrency_safe(&self) -> bool {
self.read_only() && !self.exclusive()
}
/// Whether this tool should run alone even if concurrency is enabled.
fn exclusive(&self) -> bool {
false
}
}