Files
AstroResearch/src/agent/runtime/executor/mod.rs
T
fmq d6b064a490 feat: 科研分析层全栈落地——光谱/时域/运动学分析工具链 + JWST/X 射线数据源 + 定时文献同步
数据分析层(新增 services/{spectrum,timeseries,analysis}):
- 光谱参数提取 parameters.rs:LAMOST/SDSS/APOGEE/DESI FITS header 跨源归一化读取
  Teff/logg/[Fe/H]/RV 及 ASPCAP 20+ 元素丰度,rayon 并发批量提取
- 谱线测量 lines.rs:内置真空/空气波长谱线表,窗口内极值搜索 + 梯形法积分 EW + FWHM,支持自定义谱线
- 交叉相关测速 cross_correlate.rs:对数波长重采样对齐,内置 Pickles 模板按光谱型插值,
  CCF 峰值位置提取 RV 及不确定度
- 周期搜索 periodicity.rs:Lomb-Scargle 周期图(含 FAP 误报概率)+ BLS 凌星检测 + 相位折叠
- 变星分类 classification.rs:振幅/偏度/峰度/过零率/eta 等统计特征 + 规则分类(RR Lyrae/Cepheid/食双星/AGN 等)
- SED 拟合 sed.rs:多波段测光黑体模型拟合,输出 T_eff/半径/消光 A_V/光度及不确定度
- 运动学 kinematics.rs:视差+自行+RV → 银河系 UVW 空间速度,含移动星群成员概率(Banyan Σ 简化版)
- 化学丰度 chemistry.rs:[α/Fe] vs [Fe/H] 计算,厚盘/薄盘/晕星族判别
- 观测规划 observability.rs:目标升落时间/airmass/月相影响/曝光时间估算
- 赫罗图 hr_diagram.rs:Gaia TAP CMD 查询,新增 GET /api/analysis/hr-diagram 端点

数据获取层:
- JWST:clients/mast/jwst.rs 封装 MAST Portal 锥形检索 + JwstSpectrumFetcher(NIRSpec/MIRI 光谱)
- X 射线:clients/heasarc 封装 HEASARC TAP(ADQL)+ XMM-Newton/Chandra 光谱 fetcher
- 图像 cutout:SDSS SkyServer/STScI DSS/Pan-STARRS 三源 cutout + 发现图(Finding Chart)生成
- Source 枚举新增 Jwst/Xmm/Chandra 并注册 ObservationRegistry,前端 SOURCE_THEME 与筛选器同步三源

Agent 工具集(24→35):
- 新增 9 个分析工具:get_spectrum_parameters / measure_spectral_lines / measure_radial_velocity /
  find_period / classify_variable_star / fit_sed / analyze_kinematics / analyze_abundance_pattern / plan_observation
- batch_process:批量样本"查询→下载→分析→报告"流水线,并发控制防数据源速率限制
- literature_monitor:按 ADS 查询式/时间窗/最低引用数检查最新文献

定时文献同步:
- sync_queries 表新增 is_scheduled 列(migration 20260713)
- 新增 POST /sync/queries/:id/schedule 端点
- 服务启动时拉起每小时调度器,对 is_scheduled=1 的检索配置静默执行 ADS(entdate 增量)/arXiv 增量同步
- search_history 工具收敛至 services/search::search_agent_history,消除 FTS 查询逻辑重复

其他:
- plotting skill 由占位填充为完整科研绘图规范:光谱/光变/折叠曲线/CMD/SED/[α/Fe]/周期图/Mollweide/发现图 9 类 matplotlib 模板
- 删除死代码 streaming_executor.rs(929 行,仅剩 mod 声明引用,无调用方)
- 新增 docs/roadmap-research-features.md 科研功能路线图及实现状态
2026-09-07 21:50:32 +08:00

776 lines
31 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.
// 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<ChatMessage>,
tool_registry: &ToolRegistry,
tap: &EventTap,
step: usize,
) -> (Vec<PreparedCall>, bool) {
let mut prepared_calls: Vec<PreparedCall> = 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<AppState>,
/// 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<DenialTracker>>,
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<std::sync::Mutex<FileStateCache>>,
pub enable_thinking: bool,
pub additional_allowed_dirs: Vec<String>,
/// AskUser 权限请求处理策略
pub ask_policy: AskPolicy,
/// 取消观察:会话取消表(交互式)或外部原子标志
pub cancel: Arc<AtomicBool>,
pub interactive_cancel: bool,
pub cancel_session_id: Option<String>,
pub tap: EventTap,
pub sink: Option<Arc<dyn MessageSink>>,
}
/// 并行执行所有准备好的工具调用。
///
/// 流程:
/// 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<Arc<dyn MessageSink>>,
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<ToolResultMessage>,
) {
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<serde_json::Value> = Vec::new();
let mut additional_contexts: Vec<String> = Vec::new();
let mut hook_permission_info: Vec<Option<(String, String)>> = Vec::new();
// ^^^ (permission_desc, hook_tool_name)
let mut hook_blocking_errors: Vec<String> = 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<ToolResultMessage> = Vec::new();
let mut denied_indices: std::collections::HashSet<usize> = 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<PreparedCall> =
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<String, usize> = 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,
}
}