// src/agent/runtime/executor/mod.rs // // 工具调用验证与并行执行器。 // // 执行参数通过 ExecutorContext 组合传递(历史实现有约 20 个位置参数)。 // 行为注入点: // - ask_policy: AskUser 权限请求是交互等待(主代理)还是 fail-closed 自动拒绝(子代理/队友) // - tap: SSE 事件出口(None = 静默;prefix = "[sub]") // - sink: 消息持久化出口(None = 不落库) mod helpers; // Re-export 公共类型 pub use helpers::{PreparedCall, ToolExecutionResult, ToolResultMessage}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::oneshot; use tracing::{info, warn}; use crate::api::{AppState, PendingPermission}; use crate::clients::llm::{ChatMessage, ToolCall}; use super::checkpoint::CheckpointManager; use super::denial_tracker::DenialTracker; use super::file_cache::FileStateCache; use super::hardline; use super::partitioner::ToolPartitioner; use super::permission::{PermissionChecker, PermissionResult}; use super::permission_explainer::explain_permission; use super::{AgentStreamEvent, DuplicateDetector}; use crate::agent::engine::{AskPolicy, EventTap, MessageSink}; use crate::agent::hooks::{event_label, HookRegistry, PreToolUseContext}; use crate::agent::tools::ToolRegistry; use helpers::{execute_single_tool, process_single_result}; /// 验证工具调用:死循环检测 + 参数解析。 /// /// 返回 (prepared_calls, has_duplicate)。 /// 死循环或参数无效时,错误消息直接注入到 messages。 pub fn validate_and_prepare( tool_calls: &[ToolCall], duplicate_detector: &mut DuplicateDetector, duplicate_threshold: usize, messages: &mut Vec, tool_registry: &ToolRegistry, tap: &EventTap, step: usize, ) -> (Vec, bool) { let mut prepared_calls: Vec = Vec::new(); let mut has_duplicate = false; for tool_call in tool_calls { let tool_name = &tool_call.function.name; let tool_args_str = &tool_call.function.arguments; // 确保每个工具调用有唯一 ID(LLM 可能不返回 id) let call_id = if tool_call.id.is_empty() { format!("call_{}", &uuid::Uuid::new_v4().to_string()[..8]) } else { tool_call.id.clone() }; // 死循环检测 if duplicate_detector.record(tool_name, tool_args_str, duplicate_threshold) { warn!( "[Executor] 检测到死循环:{} 连续调用 {} 次", tool_name, duplicate_threshold ); tap.send(AgentStreamEvent::Error { message: format!("检测到工具 {} 的重复调用,已自动终止循环。", tool_name), }); let error_msg = ChatMessage::tool_result( &call_id, format!( "错误:工具 {} 被连续重复调用 {} 次,参数完全相同。\ 请停止重复调用并直接给出目前收集到的答案。", tool_name, duplicate_threshold ), ); messages.push(error_msg); has_duplicate = true; continue; } // 解析参数 let args: serde_json::Value = match serde_json::from_str(tool_args_str) { Ok(v) => v, Err(e) => { let error_output = format!("工具参数 JSON 解析失败: {}", e); let (is_internal, display_name) = if let Some(tool) = tool_registry.get(tool_name) { (tool.is_internal(), tool.display_name().to_string()) } else { (false, tool_name.clone()) }; tap.send(AgentStreamEvent::ToolResult { tool_call_id: call_id.clone(), name: tool_name.clone(), display_name: tap.display(&display_name), output: error_output.clone(), is_error: true, metadata: serde_json::json!({}), step, is_internal, }); let tool_msg = ChatMessage::tool_result(&call_id, &error_output); messages.push(tool_msg); continue; } }; prepared_calls.push(PreparedCall { tool_call_id: call_id.clone(), tool_name: tool_name.clone(), args, }); } (prepared_calls, has_duplicate) } /// 执行器上下文 — 组合全部行为注入点(替代约 20 个位置参数)。 pub struct ExecutorContext<'a> { pub tool_registry: &'a ToolRegistry, pub app_state: Arc, /// hooks 缺省时使用空注册表语义(teammate 等无人值守场景) pub hook_registry: Option<&'a HookRegistry>, pub permission_checker: Option<&'a PermissionChecker>, pub session_checker: Option<&'a PermissionChecker>, pub denial_tracker: Option<&'a std::sync::Mutex>, pub checkpoint_manager: Option<&'a CheckpointManager>, pub db: &'a sqlx::SqlitePool, pub session_id: &'a str, pub agent_name: &'a str, pub turn_index: i32, pub step: usize, pub tool_timeout_secs: u64, pub max_output_chars: usize, pub read_file_state: Arc>, pub enable_thinking: bool, pub additional_allowed_dirs: Vec, /// AskUser 权限请求处理策略 pub ask_policy: AskPolicy, /// 取消观察:会话取消表(交互式)或外部原子标志 pub cancel: Arc, pub interactive_cancel: bool, pub cancel_session_id: Option, pub tap: EventTap, pub sink: Option>, } /// 并行执行所有准备好的工具调用。 /// /// 流程: /// 1. Hardline 预检查(不可绕过的参数级拒绝) /// 2. 权限检查(deny 规则拦截;ask 按策略交互等待或 fail-closed 拒绝) /// 3. 发送 ToolCall SSE 事件 + PreToolUse hooks /// 4. 工具分区 + 并行执行(并发安全工具一批并行,不安全工具单独串行) /// 5. 收集结果、发送 ToolResult SSE、运行 PostToolUse hooks /// 6. 返回 ToolResultMessage 列表供调用方推入 messages pub async fn execute_parallel( prepared_calls: &[PreparedCall], ctx: &ExecutorContext<'_>, ) -> ToolExecutionResult { if prepared_calls.is_empty() { return ToolExecutionResult { tool_messages: Vec::new(), was_cancelled: false, had_duplicate: false, hook_contexts: Vec::new(), blocking_errors: Vec::new(), }; } let tool_registry = ctx.tool_registry; let tap = &ctx.tap; let step = ctx.step; let sid = ctx.session_id.to_string(); let _turn_index = ctx.turn_index; let empty_hooks; let hook_registry: &HookRegistry = match ctx.hook_registry { Some(h) => h, None => { empty_hooks = HookRegistry::new(); &empty_hooks } }; /// 工具消息的统一落库出口(无 sink 时跳过) fn persist_tool_message( sink: &Option>, turn_index: i32, step: usize, msg: &ChatMessage, ) { if let Some(sink) = sink { let sink = sink.clone(); let msg = msg.clone(); tokio::spawn(async move { sink.save(turn_index, step as i32, &msg, None, None).await; }); } } /// 拒绝结果的统一出口:SSE 事件 + 消息落库 + 拒绝追踪 async fn record_denial( ctx: &ExecutorContext<'_>, tap: &EventTap, tool_call_id: &str, tool_name: &str, err_output: &str, metadata: serde_json::Value, tool_messages: &mut Vec, ) { let (is_internal, display_name) = match ctx.tool_registry.get(tool_name) { Some(tool) => (tool.is_internal(), tool.display_name().to_string()), None => (false, tool_name.to_string()), }; tap.send(AgentStreamEvent::ToolResult { tool_call_id: tool_call_id.to_string(), name: tool_name.to_string(), display_name: tap.display(&display_name), output: err_output.to_string(), is_error: true, metadata, step: ctx.step, is_internal, }); let err_msg = ChatMessage::tool_result(tool_call_id, err_output); persist_tool_message(&ctx.sink, ctx.turn_index, ctx.step, &err_msg); tool_messages.push(ToolResultMessage { chat_message: err_msg, was_error: true, }); if let Some(dt) = ctx.denial_tracker { if let Ok(mut tracker) = dt.lock() { tracker.record_denial(); } } } // Phase 1: 发送 ToolCall SSE 事件 for prep in prepared_calls { let (is_internal, display_name) = if let Some(tool) = tool_registry.get(&prep.tool_name) { (tool.is_internal(), tool.display_name().to_string()) } else { (false, prep.tool_name.clone()) }; tap.send(AgentStreamEvent::ToolCall { id: prep.tool_call_id.clone(), name: tap.display(&prep.tool_name), display_name: tap.display(&display_name), arguments: prep.args.clone(), step, is_internal, }); } // Phase 2: PreToolUse hooks — 收集修改后的参数和附加上下文 let exec_start = std::time::Instant::now(); let mut mutated_args: Vec = Vec::new(); let mut additional_contexts: Vec = Vec::new(); let mut hook_permission_info: Vec> = Vec::new(); // ^^^ (permission_desc, hook_tool_name) let mut hook_blocking_errors: Vec = Vec::new(); for prep in prepared_calls { let hook_ctx = PreToolUseContext { session_id: sid.clone(), tool_name: prep.tool_name.clone(), tool_args: prep.args.clone(), step, }; let result = hook_registry.run_pre_tool_use(&hook_ctx).await; if result.action.is_blocked() { let reason = result.action.block_reason().unwrap_or("unknown"); warn!( "[Executor] PreToolUse hook 阻止了 {} 的执行: {}", prep.tool_name, reason ); } for be in &result.blocking_errors { hook_blocking_errors.push(format!( "[{}] 阻止 {}: {}", be.hook_name, prep.tool_name, be.reason )); } if let Some((permission, tool_name)) = result.permission_info() { info!( "[Executor] Hook 请求了工具 {} 的权限确认: {}", prep.tool_name, permission ); hook_permission_info.push(Some((permission.to_string(), tool_name.to_string()))); } else { hook_permission_info.push(None); } mutated_args.push(result.final_args); if !result.tagged_contexts.is_empty() { for tc in &result.tagged_contexts { additional_contexts.push(format!( "[Hook: {} | {}] {}", tc.hook_name, event_label(tc.source_event), tc.content, )); } } else { for c in &result.additional_contexts { additional_contexts.push(c.clone()); } } } // Phase 2.5: Hardline 预检查 + 权限检查。 // 被拒绝的工具直接注入错误 result,不进入执行队列。 let mut tool_messages: Vec = Vec::new(); let mut denied_indices: std::collections::HashSet = std::collections::HashSet::new(); // ── Hardline 预检查(在任何模式下都不可绕过)── // 检查逻辑由工具自身通过 AgentTool::hardline_check 声明(按参数路由, // 不再按工具名字符串匹配)。 for (i, prep) in prepared_calls.iter().enumerate() { let hardline_result = tool_registry .get(&prep.tool_name) .and_then(|tool| tool.hardline_check(&prep.args)) .unwrap_or_else(hardline::HardlineResult::allowed); if hardline_result.blocked { warn!( "[Executor] Hardline 阻止了工具 {} (category={}): {}", prep.tool_name, hardline_result.category.as_deref().unwrap_or("unknown"), hardline_result.reason ); let err_output = hardline_result.reason.clone(); record_denial( ctx, tap, &prep.tool_call_id, &prep.tool_name, &err_output, serde_json::json!({ "hardline_blocked": true, "hardline_category": hardline_result.category, }), &mut tool_messages, ) .await; denied_indices.insert(i); } } if let Some(checker) = ctx.permission_checker { for (i, prep) in prepared_calls.iter().enumerate() { // ── 权限决策合并(单调不变量:Deny 粘滞,只能收紧不能放松)── let mut perm_result = checker.check(&prep.tool_name, Some(&prep.args)); perm_result = checker.apply_mode(perm_result, &prep.tool_name); // Hook PermissionRequired — 若 Checker 返回 Allowed,升级为 Ask if let Some(Some((ref perm_desc, _))) = hook_permission_info.get(i) { if perm_result.is_allowed() { perm_result = PermissionResult::AskUser { message: format!( "[Hook 权限请求] {}\n\n工具: {}\n参数: {}", perm_desc, prep.tool_name, serde_json::to_string_pretty(&prep.args).unwrap_or_default(), ), }; } } // 工具级 check_permissions() — 在已有结果基础上收紧 if let Some(tool) = tool_registry.get(&prep.tool_name) { let tool_rules = tool.check_permissions(&prep.args); for tool_rule in &tool_rules { match tool_rule { crate::agent::tools::PermissionRule::Deny { reason, .. } => { if !perm_result.is_denied() { perm_result = PermissionResult::Denied { reason: reason.clone(), }; } } crate::agent::tools::PermissionRule::Ask { message, .. } => { if perm_result.is_allowed() { perm_result = PermissionResult::AskUser { message: message.clone(), }; } } _ => {} } } } // 会话级权限检查(API 动态添加的规则,只能收紧) if let Some(session_checker) = ctx.session_checker { perm_result = super::permission::tighten( perm_result, session_checker.check(&prep.tool_name, Some(&prep.args)), ); } match perm_result { PermissionResult::Denied { reason } => { warn!( "[Executor] 权限检查拒绝了工具 {}: {}", prep.tool_name, reason ); let err_output = format!("工具 {} 被权限规则拒绝执行: {}", prep.tool_name, reason); record_denial( ctx, tap, &prep.tool_call_id, &prep.tool_name, &err_output, serde_json::json!({}), &mut tool_messages, ) .await; denied_indices.insert(i); } PermissionResult::AskUser { message } => { // 无人值守上下文(子代理/队友)无用户可问:fail-closed 自动拒绝 if ctx.ask_policy == AskPolicy::AutoDeny { let reason = match crate::agent::engine::auto_deny_ask(&prep.tool_name, &message) { PermissionResult::Denied { reason } => reason, _ => "需要用户确认但在无人值守上下文中不可用".to_string(), }; let err_output = format!("工具 {} 被拒绝执行: {}", prep.tool_name, reason); record_denial( ctx, tap, &prep.tool_call_id, &prep.tool_name, &err_output, serde_json::json!({ "auto_denied_ask": true }), &mut tool_messages, ) .await; denied_indices.insert(i); continue; } info!( "[Executor] PermissionChecker 请求用户确认工具 {}: {}", prep.tool_name, message ); // 生成权限风险解释 let permission_exp = explain_permission(&prep.tool_name, &prep.args); let explanation_json = serde_json::to_value(&permission_exp).ok(); tap.send(AgentStreamEvent::PermissionRequest { tool_call_id: prep.tool_call_id.clone(), tool_name: prep.tool_name.clone(), message: message.clone(), arguments: prep.args.clone(), explanation: explanation_json, }); // 创建 oneshot 通道等待用户响应 let (resp_tx, resp_rx) = oneshot::channel(); let perm_id = uuid::Uuid::new_v4().to_string(); { let mut perms = ctx.app_state.session.pending_permissions.lock().await; perms.insert( perm_id.clone(), PendingPermission { tool_call_id: prep.tool_call_id.clone(), tool_name: prep.tool_name.clone(), message: message.clone(), arguments: prep.args.clone(), response_tx: resp_tx, created_at: std::time::Instant::now(), }, ); } // 等待用户响应(120 秒超时 = fail-closed 拒绝) let timeout_dur = std::time::Duration::from_secs(120); let perm_result = tokio::time::timeout(timeout_dur, resp_rx).await; ctx.app_state .session .pending_permissions .lock() .await .remove(&perm_id); match perm_result { Ok(Ok(response)) if response.allowed => { info!("[Executor] 用户允许了工具 {} 的执行", prep.tool_name); tap.send(AgentStreamEvent::PermissionResponse { tool_call_id: prep.tool_call_id.clone(), allowed: true, }); // 用户允许 → 重置连续拒绝计数 if let Some(dt) = ctx.denial_tracker { if let Ok(mut tracker) = dt.lock() { tracker.record_success(); } } } Ok(Ok(_response)) => { info!("[Executor] 用户拒绝了工具 {}", prep.tool_name); let err_output = format!("用户拒绝了工具 {} 的执行", prep.tool_name); tap.send(AgentStreamEvent::PermissionResponse { tool_call_id: prep.tool_call_id.clone(), allowed: false, }); record_denial( ctx, tap, &prep.tool_call_id, &prep.tool_name, &err_output, serde_json::json!({}), &mut tool_messages, ) .await; denied_indices.insert(i); } _ => { // 超时或通道关闭 → fail-closed warn!("[Executor] 权限请求超时或取消: {}", prep.tool_name); let err_output = format!("权限请求超时 (120s): {} 未获得用户确认", prep.tool_name); tap.send(AgentStreamEvent::PermissionResponse { tool_call_id: prep.tool_call_id.clone(), allowed: false, }); record_denial( ctx, tap, &prep.tool_call_id, &prep.tool_name, &err_output, serde_json::json!({ "timeout": true }), &mut tool_messages, ) .await; denied_indices.insert(i); } } } PermissionResult::Allowed => { if let Some(dt) = ctx.denial_tracker { if let Ok(mut tracker) = dt.lock() { tracker.record_success(); } } } } } } // if let Some(checker) // Phase 3: 分区并行执行。 // ToolPartitioner 将工具按并发安全性分批: // - 连续的并发安全工具放入同一个并行批次(FuturesUnordered) // - 非并发安全工具独占一个串行批次(逐次执行) let cancelled = Arc::new(AtomicBool::new(false)); let cancel_flag = cancelled.clone(); let app_state_ref = ctx.app_state.clone(); let _sid_ref = sid.clone(); let interactive = ctx.interactive_cancel; let cancel_sid = ctx.cancel_session_id.clone(); let external_flag = ctx.cancel.clone(); let cancel_handle = tokio::spawn(async move { loop { tokio::time::sleep(std::time::Duration::from_millis(250)).await; let tripped = if interactive { cancel_sid .as_ref() .is_some_and(|s| app_state_ref.session.cancelled_runs.contains_key(s)) } else { external_flag.load(Ordering::SeqCst) }; if tripped { cancel_flag.store(true, Ordering::SeqCst); return; } } }); let timeout_dur = std::time::Duration::from_secs(ctx.tool_timeout_secs); // ── Checkpoint 预触发:对文件变更类工具在执行前创建快照 ── // 由工具通过 AgentTool::causes_file_changes 声明 if let Some(ckpt) = ctx.checkpoint_manager { let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")); for prep in prepared_calls.iter().filter(|p| { tool_registry .get(&p.tool_name) .is_some_and(|t| t.causes_file_changes()) }) { ckpt.ensure_checkpoint(&cwd, &format!("pre-{}", prep.tool_name)); } } // ── Phase 3a: 构建不包含被拒绝工具的 (原索引, PreparedCall) 映射 ── let non_denied: Vec<(usize, &PreparedCall)> = prepared_calls .iter() .enumerate() .filter(|(i, _)| !denied_indices.contains(i)) .collect(); // ── Phase 3b: 分区 ── let non_denied_calls: Vec = non_denied.iter().map(|(_, p)| (*p).clone()).collect(); let partitioner = ToolPartitioner::new(10); let batches = partitioner.partition(&non_denied_calls, tool_registry); let original_index_of: std::collections::HashMap = non_denied .iter() .map(|(orig_idx, prep)| (prep.tool_call_id.clone(), *orig_idx)) .collect(); info!( "[Executor] 工具分区完成: {} 工具 → {} 批次 ({} 串行 + {} 并行)", non_denied.len(), batches.len(), batches.iter().filter(|b| !b.is_parallel).count(), batches.iter().filter(|b| b.is_parallel).count(), ); let mut was_cancelled = false; // ── Phase 3c: 逐批次执行 ── for batch in &batches { if was_cancelled { break; } if batch.is_parallel { // ── 并行批次:FuturesUnordered 并发执行 ── let mut exec_futs: futures_util::stream::FuturesUnordered<_> = batch .calls .iter() .map(|prep| { let orig_idx = original_index_of .get(&prep.tool_call_id) .copied() .unwrap_or(0); let tool_name = prep.tool_name.clone(); let args = mutated_args .get(orig_idx) .cloned() .unwrap_or_else(|| prep.args.clone()); let tool_ctx = crate::agent::tools::ToolContext::with_file_cache( ctx.app_state.clone(), ctx.read_file_state.clone(), ) .with_sse_tx_opt(ctx.tap.sender()) .with_session_id(sid.clone()) .with_thinking(ctx.enable_thinking) .with_additional_dirs(ctx.additional_allowed_dirs.clone()) .with_tool_call_id(prep.tool_call_id.clone()) .with_max_output_chars(ctx.max_output_chars); let cancelled = cancelled.clone(); let tool_opt = tool_registry.get(&tool_name); Box::pin(async move { let output = execute_single_tool( tool_opt, args, &tool_ctx, &cancelled, timeout_dur, &tool_name, ) .await; let was_cancelled = cancelled.load(Ordering::SeqCst); ( prep.tool_call_id.clone(), prep.tool_name.clone(), prep.args.clone(), output, was_cancelled, ) }) }) .collect(); // 渐进式处理:每个工具一完成就处理 use futures_util::StreamExt; while let Some((tool_call_id, tool_name, tool_args, output, cancelled_flag)) = exec_futs.next().await { if cancelled_flag { was_cancelled = true; } process_single_result( &tool_call_id, &tool_name, &tool_args, &output, cancelled_flag, exec_start, ctx, &mut tool_messages, &mut additional_contexts, ) .await; } } else { // ── 串行批次:逐个执行 ── for prep in &batch.calls { let orig_idx = original_index_of .get(&prep.tool_call_id) .copied() .unwrap_or(0); let tool_name = prep.tool_name.clone(); let args = mutated_args .get(orig_idx) .cloned() .unwrap_or_else(|| prep.args.clone()); let tool_ctx = crate::agent::tools::ToolContext::with_file_cache( ctx.app_state.clone(), ctx.read_file_state.clone(), ) .with_session_id(sid.clone()) .with_thinking(ctx.enable_thinking) .with_additional_dirs(ctx.additional_allowed_dirs.clone()); let tool_opt = tool_registry.get(&tool_name); let output = execute_single_tool( tool_opt, args, &tool_ctx, &cancelled, timeout_dur, &tool_name, ) .await; let cancelled_flag = cancelled.load(Ordering::SeqCst); if cancelled_flag { was_cancelled = true; } process_single_result( &prep.tool_call_id, &tool_name, &prep.args, &output, cancelled_flag, exec_start, ctx, &mut tool_messages, &mut additional_contexts, ) .await; if was_cancelled { break; } } } } cancel_handle.abort(); ToolExecutionResult { tool_messages, was_cancelled, had_duplicate: false, hook_contexts: additional_contexts, blocking_errors: hook_blocking_errors, } }