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 科研功能路线图及实现状态
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
// src/agent/runtime/executor/helpers.rs
|
||||
//
|
||||
// 执行器辅助类型与函数:PreparedCall, ToolResultMessage, ToolExecutionResult,
|
||||
// execute_single_tool, process_single_result, save_tool_message_sync。
|
||||
// execute_single_tool, process_single_result。
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::clients::llm::ChatMessage;
|
||||
@@ -15,6 +13,7 @@ use super::AgentStreamEvent;
|
||||
use crate::agent::hooks::{
|
||||
event_label, HookRegistry, PostToolUseContext, PostToolUseFailureContext,
|
||||
};
|
||||
use crate::agent::tools::ToolOutput;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PreparedCall {
|
||||
@@ -42,8 +41,6 @@ pub struct ToolExecutionResult {
|
||||
}
|
||||
|
||||
/// 执行单个工具调用(含超时和取消检测)。
|
||||
///
|
||||
/// 从原 `execute_parallel` 的闭包提取,供分区后的批次执行复用。
|
||||
pub(super) async fn execute_single_tool(
|
||||
tool_opt: Option<&dyn crate::agent::tools::AgentTool>,
|
||||
args: serde_json::Value,
|
||||
@@ -51,10 +48,10 @@ pub(super) async fn execute_single_tool(
|
||||
cancelled: &Arc<AtomicBool>,
|
||||
timeout_dur: std::time::Duration,
|
||||
tool_name: &str,
|
||||
) -> crate::agent::tools::ToolOutput {
|
||||
) -> ToolOutput {
|
||||
let tool = match tool_opt {
|
||||
Some(t) => t,
|
||||
None => return crate::agent::tools::ToolOutput::error(format!("未知工具: {}", tool_name)),
|
||||
None => return ToolOutput::error(format!("未知工具: {}", tool_name)),
|
||||
};
|
||||
|
||||
let interrupt_behavior = tool.interrupt_behavior();
|
||||
@@ -76,7 +73,7 @@ pub(super) async fn execute_single_tool(
|
||||
res = tokio::time::timeout(timeout_dur, tool_fut) => {
|
||||
match res {
|
||||
Ok(output) => output,
|
||||
Err(_) => crate::agent::tools::ToolOutput::error(format!(
|
||||
Err(_) => ToolOutput::error(format!(
|
||||
"工具 {} 执行超时({}秒)",
|
||||
tool_name,
|
||||
timeout_dur.as_secs()
|
||||
@@ -84,50 +81,50 @@ pub(super) async fn execute_single_tool(
|
||||
}
|
||||
}
|
||||
_ = cancel_fut => {
|
||||
crate::agent::tools::ToolOutput::error("执行已被用户取消")
|
||||
ToolOutput::error("执行已被用户取消")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理单个工具执行结果(SSE 事件、PostToolUse hooks、持久化)。
|
||||
///
|
||||
/// 从原 `execute_parallel` 的结果处理循环提取。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(super) async fn process_single_result(
|
||||
tool_call_id: &str,
|
||||
tool_name: &str,
|
||||
tool_args: &serde_json::Value,
|
||||
output: &crate::agent::tools::ToolOutput,
|
||||
output: &ToolOutput,
|
||||
cancelled_flag: bool,
|
||||
exec_start: std::time::Instant,
|
||||
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
|
||||
hook_registry: &HookRegistry,
|
||||
library_dir: &std::path::Path,
|
||||
sid: &str,
|
||||
agent_name: &str,
|
||||
step: usize,
|
||||
max_output_chars: usize,
|
||||
ctx: &super::ExecutorContext<'_>,
|
||||
tool_messages: &mut Vec<ToolResultMessage>,
|
||||
additional_contexts: &mut Vec<String>,
|
||||
db: &SqlitePool,
|
||||
turn_index: i32,
|
||||
tool_registry: &crate::agent::tools::ToolRegistry,
|
||||
) {
|
||||
use crate::agent::tools::persist::maybe_persist_tool_result;
|
||||
use crate::agent::tools::persist::maybe_persist_tool_result_for;
|
||||
|
||||
let elapsed_ms = exec_start.elapsed().as_millis() as u64;
|
||||
let step = ctx.step;
|
||||
let tap = &ctx.tap;
|
||||
|
||||
let (is_internal, display_name) = if let Some(tool) = tool_registry.get(tool_name) {
|
||||
let empty_hooks;
|
||||
let hook_registry: &HookRegistry = match ctx.hook_registry {
|
||||
Some(h) => h,
|
||||
None => {
|
||||
empty_hooks = HookRegistry::new();
|
||||
&empty_hooks
|
||||
}
|
||||
};
|
||||
|
||||
let (is_internal, display_name) = if let Some(tool) = ctx.tool_registry.get(tool_name) {
|
||||
(tool.is_internal(), tool.display_name().to_string())
|
||||
} else {
|
||||
(false, tool_name.to_string())
|
||||
};
|
||||
|
||||
// SSE 事件 — 立即推送到前端
|
||||
let _ = tx.send(AgentStreamEvent::ToolResult {
|
||||
tap.send(AgentStreamEvent::ToolResult {
|
||||
tool_call_id: tool_call_id.to_string(),
|
||||
name: tool_name.to_string(),
|
||||
display_name,
|
||||
name: tap.display(tool_name),
|
||||
display_name: tap.display(&display_name),
|
||||
output: output.content.clone(),
|
||||
is_error: output.is_error,
|
||||
metadata: output.metadata.clone(),
|
||||
@@ -137,23 +134,30 @@ pub(super) async fn process_single_result(
|
||||
|
||||
// 输出处理:小结果直接传递,大结果持久化到磁盘并返回 stub
|
||||
// 但对于已从磁盘读取内容的工具(如 read_file),跳过持久化以防止级联
|
||||
let tool_results_dir = library_dir.join(".agent").join("tool-results");
|
||||
let tool_results_dir = ctx
|
||||
.app_state
|
||||
.config
|
||||
.storage
|
||||
.library_dir
|
||||
.join(".agent")
|
||||
.join("tool-results");
|
||||
let (processed_content, _persisted_path) = if output.skip_persist {
|
||||
(output.content.clone(), None)
|
||||
} else {
|
||||
maybe_persist_tool_result(
|
||||
maybe_persist_tool_result_for(
|
||||
&output.content,
|
||||
tool_call_id,
|
||||
max_output_chars,
|
||||
ctx.max_output_chars,
|
||||
&tool_results_dir,
|
||||
tool_name,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
// PostToolUse hook
|
||||
let post_ctx = PostToolUseContext {
|
||||
session_id: sid.to_string(),
|
||||
agent_name: agent_name.to_string(),
|
||||
session_id: ctx.session_id.to_string(),
|
||||
agent_name: ctx.agent_name.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_args: tool_args.clone(),
|
||||
output_content: processed_content.clone(),
|
||||
@@ -164,9 +168,14 @@ pub(super) async fn process_single_result(
|
||||
let post_result = hook_registry.run_post_tool_use(&post_ctx).await;
|
||||
let final_content = post_result.final_content;
|
||||
|
||||
// 非可信内容包裹(间接 prompt 注入防御)
|
||||
let llm_content =
|
||||
crate::agent::runtime::untrusted::wrap_untrusted_content(tool_name, &final_content);
|
||||
// 非可信内容包裹(间接 prompt 注入防御)。
|
||||
// 是否包裹由工具通过 AgentTool::untrusted_output 声明;
|
||||
// 未知工具回退到名字启发式(mcp__*/web_* 前缀)。
|
||||
let llm_content = if ctx.tool_registry.untrusted_output(tool_name) {
|
||||
crate::agent::runtime::untrusted::wrap_untrusted_content(tool_name, &final_content)
|
||||
} else {
|
||||
final_content
|
||||
};
|
||||
|
||||
// 收集 PostToolUse hook 注入的上下文
|
||||
if !post_result.tagged_contexts.is_empty() {
|
||||
@@ -179,8 +188,8 @@ pub(super) async fn process_single_result(
|
||||
));
|
||||
}
|
||||
} else {
|
||||
for ctx in &post_result.additional_contexts {
|
||||
additional_contexts.push(ctx.clone());
|
||||
for c in &post_result.additional_contexts {
|
||||
additional_contexts.push(c.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,8 +209,8 @@ pub(super) async fn process_single_result(
|
||||
// PostToolUseFailure hook
|
||||
if output.is_error {
|
||||
let failure_ctx = PostToolUseFailureContext {
|
||||
session_id: sid.to_string(),
|
||||
agent_name: agent_name.to_string(),
|
||||
session_id: ctx.session_id.to_string(),
|
||||
agent_name: ctx.agent_name.to_string(),
|
||||
tool_name: tool_name.to_string(),
|
||||
tool_args: tool_args.clone(),
|
||||
error_message: output.content.clone(),
|
||||
@@ -217,51 +226,25 @@ pub(super) async fn process_single_result(
|
||||
// 发送给 LLM 使用包裹后的内容(安全防御)
|
||||
let chat_message = ChatMessage::tool_result(tool_call_id, &llm_content);
|
||||
|
||||
// 持久化到数据库(fire-and-forget)
|
||||
save_tool_message_sync(db, sid, turn_index, step, &chat_message);
|
||||
// 持久化到数据库(fire-and-forget;无 sink 时跳过)。
|
||||
// canonical value 一并写入 metadata(重放/审计消费结构化结果)。
|
||||
if let Some(sink) = &ctx.sink {
|
||||
let sink = sink.clone();
|
||||
let msg = chat_message.clone();
|
||||
let turn_index = ctx.turn_index;
|
||||
let step_i = ctx.step;
|
||||
let extra = output
|
||||
.value
|
||||
.clone()
|
||||
.map(|v| serde_json::json!({ "canonical_value": v }));
|
||||
tokio::spawn(async move {
|
||||
sink.save(turn_index, step_i as i32, &msg, None, extra)
|
||||
.await;
|
||||
});
|
||||
}
|
||||
|
||||
tool_messages.push(ToolResultMessage {
|
||||
chat_message,
|
||||
was_error: output.is_error,
|
||||
});
|
||||
}
|
||||
|
||||
/// 同步保存 tool 角色消息到数据库。
|
||||
pub(super) fn save_tool_message_sync(
|
||||
db: &SqlitePool,
|
||||
session_id: &str,
|
||||
turn_index: i32,
|
||||
step_index: usize,
|
||||
msg: &ChatMessage,
|
||||
) {
|
||||
let db_clone = db.clone();
|
||||
let session_id = session_id.to_string();
|
||||
let content = msg.text().unwrap_or("").to_string();
|
||||
let tool_call_id = msg.tool_call_id.clone();
|
||||
// 提前序列化,避免闭包内的生命周期问题
|
||||
let metadata_str =
|
||||
serde_json::to_string(&serde_json::json!({ "role": "tool" })).unwrap_or_default();
|
||||
let raw_json = serde_json::to_string(&msg).unwrap_or_default();
|
||||
// fire-and-forget: tool 消息保存失败不影响主流程
|
||||
tokio::spawn(async move {
|
||||
let token_count = content.len() as i32 / 4;
|
||||
if let Err(e) = sqlx::query(
|
||||
"INSERT INTO agent_messages (session_id, turn_index, step_index, role, content, tool_call_id, token_count, metadata, raw_json, agent_name) \
|
||||
VALUES (?, ?, ?, 'tool', ?, ?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(&session_id)
|
||||
.bind(turn_index)
|
||||
.bind(step_index as i32)
|
||||
.bind(&content)
|
||||
.bind(&tool_call_id)
|
||||
.bind(token_count)
|
||||
.bind(&metadata_str)
|
||||
.bind(&raw_json)
|
||||
.bind("lead")
|
||||
.execute(&db_clone)
|
||||
.await
|
||||
{
|
||||
warn!("[Executor] 保存 tool 消息失败(非致命): {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+256
-280
@@ -1,18 +1,21 @@
|
||||
// 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 futures_util::stream::FuturesUnordered;
|
||||
use futures_util::StreamExt;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::sync::oneshot;
|
||||
use tracing::{info, warn};
|
||||
|
||||
use crate::api::{AppState, PendingPermission};
|
||||
@@ -26,26 +29,23 @@ 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::{ToolContext, ToolRegistry};
|
||||
use crate::agent::tools::ToolRegistry;
|
||||
|
||||
use helpers::{execute_single_tool, process_single_result, save_tool_message_sync};
|
||||
use helpers::{execute_single_tool, process_single_result};
|
||||
|
||||
/// 验证工具调用:死循环检测 + 参数解析。
|
||||
///
|
||||
/// 返回 (prepared_calls, has_duplicate)。
|
||||
/// 死循环或参数无效时,错误消息直接注入到 messages。
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn validate_and_prepare(
|
||||
tool_calls: &[ToolCall],
|
||||
duplicate_detector: &mut DuplicateDetector,
|
||||
duplicate_threshold: usize,
|
||||
messages: &mut Vec<ChatMessage>,
|
||||
tool_registry: &ToolRegistry,
|
||||
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
|
||||
db: &SqlitePool,
|
||||
session_id: &str,
|
||||
turn_index: i32,
|
||||
tap: &EventTap,
|
||||
step: usize,
|
||||
) -> (Vec<PreparedCall>, bool) {
|
||||
let mut prepared_calls: Vec<PreparedCall> = Vec::new();
|
||||
@@ -68,7 +68,7 @@ pub fn validate_and_prepare(
|
||||
"[Executor] 检测到死循环:{} 连续调用 {} 次",
|
||||
tool_name, duplicate_threshold
|
||||
);
|
||||
let _ = tx.send(AgentStreamEvent::Error {
|
||||
tap.send(AgentStreamEvent::Error {
|
||||
message: format!("检测到工具 {} 的重复调用,已自动终止循环。", tool_name),
|
||||
});
|
||||
let error_msg = ChatMessage::tool_result(
|
||||
@@ -94,10 +94,10 @@ pub fn validate_and_prepare(
|
||||
} else {
|
||||
(false, tool_name.clone())
|
||||
};
|
||||
let _ = tx.send(AgentStreamEvent::ToolResult {
|
||||
tap.send(AgentStreamEvent::ToolResult {
|
||||
tool_call_id: call_id.clone(),
|
||||
name: tool_name.clone(),
|
||||
display_name,
|
||||
display_name: tap.display(&display_name),
|
||||
output: error_output.clone(),
|
||||
is_error: true,
|
||||
metadata: serde_json::json!({}),
|
||||
@@ -105,7 +105,6 @@ pub fn validate_and_prepare(
|
||||
is_internal,
|
||||
});
|
||||
let tool_msg = ChatMessage::tool_result(&call_id, &error_output);
|
||||
save_tool_message_sync(db, session_id, turn_index, step, &tool_msg);
|
||||
messages.push(tool_msg);
|
||||
continue;
|
||||
}
|
||||
@@ -121,36 +120,48 @@ pub fn validate_and_prepare(
|
||||
(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. 权限检查(deny 规则阻止不可执行工具)
|
||||
/// 2. 发送 ToolCall SSE 事件
|
||||
/// 3. 运行 PreToolUse hooks
|
||||
/// 1. Hardline 预检查(不可绕过的参数级拒绝)
|
||||
/// 2. 权限检查(deny 规则拦截;ask 按策略交互等待或 fail-closed 拒绝)
|
||||
/// 3. 发送 ToolCall SSE 事件 + PreToolUse hooks
|
||||
/// 4. 工具分区 + 并行执行(并发安全工具一批并行,不安全工具单独串行)
|
||||
/// 5. 收集结果、发送 ToolResult SSE、运行 PostToolUse hooks
|
||||
/// 6. 返回 ToolResultMessage 列表供调用方推入 messages
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn execute_parallel(
|
||||
prepared_calls: &[PreparedCall],
|
||||
tool_registry: &ToolRegistry,
|
||||
app_state: Arc<AppState>,
|
||||
hook_registry: &HookRegistry,
|
||||
permission_checker: Option<&PermissionChecker>,
|
||||
session_permission_checker: Option<&PermissionChecker>,
|
||||
denial_tracker: Option<&std::sync::Mutex<DenialTracker>>,
|
||||
checkpoint_manager: Option<&std::sync::Arc<CheckpointManager>>,
|
||||
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
|
||||
db: &SqlitePool,
|
||||
session_id: &str,
|
||||
agent_name: &str,
|
||||
turn_index: i32,
|
||||
step: usize,
|
||||
tool_timeout_secs: u64,
|
||||
max_output_chars: usize,
|
||||
read_file_state: Arc<std::sync::Mutex<FileStateCache>>,
|
||||
enable_thinking: bool,
|
||||
additional_allowed_dirs: Vec<String>,
|
||||
ctx: &ExecutorContext<'_>,
|
||||
) -> ToolExecutionResult {
|
||||
if prepared_calls.is_empty() {
|
||||
return ToolExecutionResult {
|
||||
@@ -162,7 +173,72 @@ pub async fn execute_parallel(
|
||||
};
|
||||
}
|
||||
|
||||
let sid = session_id.to_string();
|
||||
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 {
|
||||
@@ -171,10 +247,10 @@ pub async fn execute_parallel(
|
||||
} else {
|
||||
(false, prep.tool_name.clone())
|
||||
};
|
||||
let _ = tx.send(AgentStreamEvent::ToolCall {
|
||||
tap.send(AgentStreamEvent::ToolCall {
|
||||
id: prep.tool_call_id.clone(),
|
||||
name: prep.tool_name.clone(),
|
||||
display_name,
|
||||
name: tap.display(&prep.tool_name),
|
||||
display_name: tap.display(&display_name),
|
||||
arguments: prep.args.clone(),
|
||||
step,
|
||||
is_internal,
|
||||
@@ -203,14 +279,12 @@ pub async fn execute_parallel(
|
||||
prep.tool_name, reason
|
||||
);
|
||||
}
|
||||
// 收集所有阻塞错误详情(含多个 hook 同时 block 的情况)
|
||||
for be in &result.blocking_errors {
|
||||
hook_blocking_errors.push(format!(
|
||||
"[{}] 阻止 {}: {}",
|
||||
be.hook_name, prep.tool_name, be.reason
|
||||
));
|
||||
}
|
||||
// 收集 hook 的权限请求(保留完整信息用于 AskUser prompt)
|
||||
if let Some((permission, tool_name)) = result.permission_info() {
|
||||
info!(
|
||||
"[Executor] Hook 请求了工具 {} 的权限确认: {}",
|
||||
@@ -220,9 +294,7 @@ pub async fn execute_parallel(
|
||||
} else {
|
||||
hook_permission_info.push(None);
|
||||
}
|
||||
// 使用 hook 可能修改后的参数
|
||||
mutated_args.push(result.final_args);
|
||||
// 收集所有 hook 注入的上下文(优先使用带来源标记的 tagged_contexts)
|
||||
if !result.tagged_contexts.is_empty() {
|
||||
for tc in &result.tagged_contexts {
|
||||
additional_contexts.push(format!(
|
||||
@@ -233,39 +305,25 @@ pub async fn execute_parallel(
|
||||
));
|
||||
}
|
||||
} else {
|
||||
for ctx in &result.additional_contexts {
|
||||
additional_contexts.push(ctx.clone());
|
||||
for c in &result.additional_contexts {
|
||||
additional_contexts.push(c.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2.5: 权限检查 — PermissionChecker 规则引擎拦截被拒绝的工具。
|
||||
// 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 预检查(在任何模式下都不可绕过)──
|
||||
// 在 PermissionChecker 之前执行,确保 hardline 规则始终生效。
|
||||
// 检查逻辑由工具自身通过 AgentTool::hardline_check 声明(按参数路由,
|
||||
// 不再按工具名字符串匹配)。
|
||||
for (i, prep) in prepared_calls.iter().enumerate() {
|
||||
let hardline_result = match prep.tool_name.as_str() {
|
||||
"run_bash" => {
|
||||
if let Some(cmd) = prep.args.get("command").and_then(|v| v.as_str()) {
|
||||
hardline::check_command(cmd)
|
||||
} else {
|
||||
hardline::HardlineResult::allowed()
|
||||
}
|
||||
}
|
||||
"file_write" | "file_edit" => {
|
||||
if let Some(path) = prep.args.get("file_path").and_then(|v| v.as_str()) {
|
||||
hardline::check_dangerous_path(path)
|
||||
} else if let Some(path) = prep.args.get("path").and_then(|v| v.as_str()) {
|
||||
hardline::check_dangerous_path(path)
|
||||
} else {
|
||||
hardline::HardlineResult::allowed()
|
||||
}
|
||||
}
|
||||
_ => hardline::HardlineResult::allowed(),
|
||||
};
|
||||
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!(
|
||||
@@ -275,48 +333,30 @@ pub async fn execute_parallel(
|
||||
hardline_result.reason
|
||||
);
|
||||
let err_output = hardline_result.reason.clone();
|
||||
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())
|
||||
};
|
||||
let _ = tx.send(AgentStreamEvent::ToolResult {
|
||||
tool_call_id: prep.tool_call_id.clone(),
|
||||
name: prep.tool_name.clone(),
|
||||
display_name,
|
||||
output: err_output.clone(),
|
||||
is_error: true,
|
||||
metadata: serde_json::json!({
|
||||
record_denial(
|
||||
ctx,
|
||||
tap,
|
||||
&prep.tool_call_id,
|
||||
&prep.tool_name,
|
||||
&err_output,
|
||||
serde_json::json!({
|
||||
"hardline_blocked": true,
|
||||
"hardline_category": hardline_result.category,
|
||||
}),
|
||||
step,
|
||||
is_internal,
|
||||
});
|
||||
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
|
||||
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
|
||||
tool_messages.push(ToolResultMessage {
|
||||
chat_message: err_msg,
|
||||
was_error: true,
|
||||
});
|
||||
// 记录拒绝追踪
|
||||
if let Some(dt) = denial_tracker {
|
||||
if let Ok(mut tracker) = dt.lock() {
|
||||
tracker.record_denial();
|
||||
}
|
||||
}
|
||||
&mut tool_messages,
|
||||
)
|
||||
.await;
|
||||
denied_indices.insert(i);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(checker) = permission_checker {
|
||||
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
|
||||
// 使用 hook 提供的具体权限描述替换泛型消息
|
||||
if let Some(Some((ref perm_desc, _))) = hook_permission_info.get(i) {
|
||||
if perm_result.is_allowed() {
|
||||
perm_result = PermissionResult::AskUser {
|
||||
@@ -330,14 +370,12 @@ pub async fn execute_parallel(
|
||||
}
|
||||
}
|
||||
|
||||
// 工具级 check_permissions() — 在 PermissionChecker 结果基础上叠加
|
||||
// PermissionChecker Deny/Ask 优先,工具级规则在 Allow 时可升级为 Ask
|
||||
// 工具级 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, .. } => {
|
||||
// 工具级 Deny 仅在 PermissionChecker 未 Deny 时生效
|
||||
if !perm_result.is_denied() {
|
||||
perm_result = PermissionResult::Denied {
|
||||
reason: reason.clone(),
|
||||
@@ -345,7 +383,6 @@ pub async fn execute_parallel(
|
||||
}
|
||||
}
|
||||
crate::agent::tools::PermissionRule::Ask { message, .. } => {
|
||||
// 工具级 Ask:若 PermissionChecker 返回 Allowed,升级为 Ask
|
||||
if perm_result.is_allowed() {
|
||||
perm_result = PermissionResult::AskUser {
|
||||
message: message.clone(),
|
||||
@@ -357,67 +394,57 @@ pub async fn execute_parallel(
|
||||
}
|
||||
}
|
||||
|
||||
// 会话级权限检查(API 动态添加的规则,优先级高于环境变量规则)
|
||||
if let Some(session_checker) = session_permission_checker {
|
||||
let session_result = session_checker.check(&prep.tool_name, Some(&prep.args));
|
||||
// 会话规则结果覆盖或升级
|
||||
match session_result {
|
||||
PermissionResult::Denied { reason } => {
|
||||
// 会话 Deny 强制覆盖
|
||||
perm_result = PermissionResult::Denied { reason };
|
||||
}
|
||||
PermissionResult::AskUser { message } => {
|
||||
// 会话 Ask 在 Allow 时升级
|
||||
if perm_result.is_allowed() {
|
||||
perm_result = PermissionResult::AskUser { message };
|
||||
}
|
||||
}
|
||||
PermissionResult::Allowed => {
|
||||
// 会话 Allow 仅覆盖 Allowed,保持 Deny/AskUser 不变
|
||||
// 避免覆盖工具级 check_permissions() 升级的 AskUser
|
||||
}
|
||||
}
|
||||
// 会话级权限检查(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] PermissionChecker 拒绝了工具 {}: {}",
|
||||
"[Executor] 权限检查拒绝了工具 {}: {}",
|
||||
prep.tool_name, reason
|
||||
);
|
||||
let err_output =
|
||||
format!("工具 {} 被权限规则拒绝执行: {}", prep.tool_name, reason);
|
||||
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())
|
||||
};
|
||||
let _ = tx.send(AgentStreamEvent::ToolResult {
|
||||
tool_call_id: prep.tool_call_id.clone(),
|
||||
name: prep.tool_name.clone(),
|
||||
display_name,
|
||||
output: err_output.clone(),
|
||||
is_error: true,
|
||||
metadata: serde_json::json!({}),
|
||||
step,
|
||||
is_internal,
|
||||
});
|
||||
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
|
||||
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
|
||||
tool_messages.push(ToolResultMessage {
|
||||
chat_message: err_msg,
|
||||
was_error: true,
|
||||
});
|
||||
// 记录拒绝追踪
|
||||
if let Some(dt) = denial_tracker {
|
||||
if let Ok(mut tracker) = dt.lock() {
|
||||
tracker.record_denial();
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -427,8 +454,7 @@ pub async fn execute_parallel(
|
||||
let permission_exp = explain_permission(&prep.tool_name, &prep.args);
|
||||
let explanation_json = serde_json::to_value(&permission_exp).ok();
|
||||
|
||||
// 发送权限请求 SSE 事件
|
||||
let _ = tx.send(AgentStreamEvent::PermissionRequest {
|
||||
tap.send(AgentStreamEvent::PermissionRequest {
|
||||
tool_call_id: prep.tool_call_id.clone(),
|
||||
tool_name: prep.tool_name.clone(),
|
||||
message: message.clone(),
|
||||
@@ -439,17 +465,14 @@ pub async fn execute_parallel(
|
||||
// 创建 oneshot 通道等待用户响应
|
||||
let (resp_tx, resp_rx) = oneshot::channel();
|
||||
let perm_id = uuid::Uuid::new_v4().to_string();
|
||||
let tc_id = prep.tool_call_id.clone();
|
||||
let t_name = prep.tool_name.clone();
|
||||
|
||||
// 存储待处理的权限请求
|
||||
{
|
||||
let mut perms = app_state.session.pending_permissions.lock().await;
|
||||
let mut perms = ctx.app_state.session.pending_permissions.lock().await;
|
||||
perms.insert(
|
||||
perm_id.clone(),
|
||||
PendingPermission {
|
||||
tool_call_id: tc_id.clone(),
|
||||
tool_name: t_name.clone(),
|
||||
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,
|
||||
@@ -458,12 +481,11 @@ pub async fn execute_parallel(
|
||||
);
|
||||
}
|
||||
|
||||
// 等待用户响应(120 秒超时)
|
||||
// 等待用户响应(120 秒超时 = fail-closed 拒绝)
|
||||
let timeout_dur = std::time::Duration::from_secs(120);
|
||||
let perm_result = tokio::time::timeout(timeout_dur, resp_rx).await;
|
||||
|
||||
// 清理待处理的权限请求
|
||||
app_state
|
||||
ctx.app_state
|
||||
.session
|
||||
.pending_permissions
|
||||
.lock()
|
||||
@@ -473,99 +495,61 @@ pub async fn execute_parallel(
|
||||
match perm_result {
|
||||
Ok(Ok(response)) if response.allowed => {
|
||||
info!("[Executor] 用户允许了工具 {} 的执行", prep.tool_name);
|
||||
let _ = tx.send(AgentStreamEvent::PermissionResponse {
|
||||
tap.send(AgentStreamEvent::PermissionResponse {
|
||||
tool_call_id: prep.tool_call_id.clone(),
|
||||
allowed: true,
|
||||
});
|
||||
// 用户允许 → 重置连续拒绝计数
|
||||
if let Some(dt) = denial_tracker {
|
||||
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);
|
||||
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())
|
||||
};
|
||||
let _ = tx.send(AgentStreamEvent::ToolResult {
|
||||
tool_call_id: prep.tool_call_id.clone(),
|
||||
name: prep.tool_name.clone(),
|
||||
display_name,
|
||||
output: err_output.clone(),
|
||||
is_error: true,
|
||||
metadata: serde_json::json!({}),
|
||||
step,
|
||||
is_internal,
|
||||
});
|
||||
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
|
||||
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
|
||||
tool_messages.push(ToolResultMessage {
|
||||
chat_message: err_msg,
|
||||
was_error: true,
|
||||
});
|
||||
// 用户拒绝 → 记录拒绝追踪
|
||||
if let Some(dt) = denial_tracker {
|
||||
if let Ok(mut tracker) = dt.lock() {
|
||||
tracker.record_denial();
|
||||
}
|
||||
}
|
||||
denied_indices.insert(i);
|
||||
let _ = tx.send(AgentStreamEvent::PermissionResponse {
|
||||
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);
|
||||
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())
|
||||
};
|
||||
let _ = tx.send(AgentStreamEvent::ToolResult {
|
||||
tool_call_id: prep.tool_call_id.clone(),
|
||||
name: prep.tool_name.clone(),
|
||||
display_name,
|
||||
output: err_output.clone(),
|
||||
is_error: true,
|
||||
metadata: serde_json::json!({}),
|
||||
step,
|
||||
is_internal,
|
||||
});
|
||||
let err_msg = ChatMessage::tool_result(&prep.tool_call_id, &err_output);
|
||||
save_tool_message_sync(db, &sid, turn_index, step, &err_msg);
|
||||
tool_messages.push(ToolResultMessage {
|
||||
chat_message: err_msg,
|
||||
was_error: true,
|
||||
});
|
||||
// 超时 → 记录拒绝追踪
|
||||
if let Some(dt) = denial_tracker {
|
||||
if let Ok(mut tracker) = dt.lock() {
|
||||
tracker.record_denial();
|
||||
}
|
||||
}
|
||||
denied_indices.insert(i);
|
||||
let _ = tx.send(AgentStreamEvent::PermissionResponse {
|
||||
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) = denial_tracker {
|
||||
if let Some(dt) = ctx.denial_tracker {
|
||||
if let Ok(mut tracker) = dt.lock() {
|
||||
tracker.record_success();
|
||||
}
|
||||
@@ -575,38 +559,46 @@ pub async fn execute_parallel(
|
||||
}
|
||||
} // if let Some(checker)
|
||||
|
||||
// Phase 3: 分区并行执行(参考 Claude Code partitionToolCalls + runTools)。
|
||||
//
|
||||
// 改进:原实现将所有非拒绝工具放入单个 FuturesUnordered 无差别并发,
|
||||
// 可能导致非并发安全工具(如 run_bash)错误地并行执行。
|
||||
// 新实现使用 ToolPartitioner 将工具按并发安全性分批:
|
||||
// Phase 3: 分区并行执行。
|
||||
// ToolPartitioner 将工具按并发安全性分批:
|
||||
// - 连续的并发安全工具放入同一个并行批次(FuturesUnordered)
|
||||
// - 非并发安全工具独占一个串行批次(逐次执行)
|
||||
// 批次内工具执行完成后立即推送 SSE 事件,不等待整个批次完成。
|
||||
let cancelled = Arc::new(AtomicBool::new(false));
|
||||
let cancel_flag = cancelled.clone();
|
||||
let app_state_ref = app_state.clone();
|
||||
let sid_ref = sid.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;
|
||||
if app_state_ref.session.cancelled_runs.contains_key(&sid_ref) {
|
||||
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(tool_timeout_secs);
|
||||
let timeout_dur = std::time::Duration::from_secs(ctx.tool_timeout_secs);
|
||||
|
||||
// ── Checkpoint 预触发:对文件变更类工具在执行前创建快照 ──
|
||||
if let Some(ckpt) = checkpoint_manager {
|
||||
// 由工具通过 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| CheckpointManager::should_checkpoint(&p.tool_name))
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -624,7 +616,6 @@ pub async fn execute_parallel(
|
||||
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))
|
||||
@@ -641,7 +632,6 @@ pub async fn execute_parallel(
|
||||
let mut was_cancelled = false;
|
||||
|
||||
// ── Phase 3c: 逐批次执行 ──
|
||||
// 批次之间串行;并行批次内工具并发执行;串行批次内工具逐个执行。
|
||||
for batch in &batches {
|
||||
if was_cancelled {
|
||||
break;
|
||||
@@ -649,7 +639,7 @@ pub async fn execute_parallel(
|
||||
|
||||
if batch.is_parallel {
|
||||
// ── 并行批次:FuturesUnordered 并发执行 ──
|
||||
let mut exec_futs: FuturesUnordered<_> = batch
|
||||
let mut exec_futs: futures_util::stream::FuturesUnordered<_> = batch
|
||||
.calls
|
||||
.iter()
|
||||
.map(|prep| {
|
||||
@@ -662,14 +652,16 @@ pub async fn execute_parallel(
|
||||
.get(orig_idx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| prep.args.clone());
|
||||
let tool_ctx =
|
||||
ToolContext::with_file_cache(app_state.clone(), read_file_state.clone())
|
||||
.with_sse_tx(tx.clone())
|
||||
.with_session_id(session_id.to_string())
|
||||
.with_thinking(enable_thinking)
|
||||
.with_additional_dirs(additional_allowed_dirs.clone())
|
||||
.with_tool_call_id(prep.tool_call_id.clone())
|
||||
.with_max_output_chars(max_output_chars);
|
||||
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);
|
||||
|
||||
@@ -696,6 +688,7 @@ pub async fn execute_parallel(
|
||||
.collect();
|
||||
|
||||
// 渐进式处理:每个工具一完成就处理
|
||||
use futures_util::StreamExt;
|
||||
while let Some((tool_call_id, tool_name, tool_args, output, cancelled_flag)) =
|
||||
exec_futs.next().await
|
||||
{
|
||||
@@ -709,18 +702,9 @@ pub async fn execute_parallel(
|
||||
&output,
|
||||
cancelled_flag,
|
||||
exec_start,
|
||||
tx,
|
||||
hook_registry,
|
||||
&app_state.config.storage.library_dir,
|
||||
&sid,
|
||||
agent_name,
|
||||
step,
|
||||
max_output_chars,
|
||||
ctx,
|
||||
&mut tool_messages,
|
||||
&mut additional_contexts,
|
||||
db,
|
||||
turn_index,
|
||||
tool_registry,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -736,12 +720,13 @@ pub async fn execute_parallel(
|
||||
.get(orig_idx)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| prep.args.clone());
|
||||
let tool_ctx =
|
||||
ToolContext::with_file_cache(app_state.clone(), read_file_state.clone())
|
||||
.with_sse_tx(tx.clone())
|
||||
.with_session_id(session_id.to_string())
|
||||
.with_thinking(enable_thinking)
|
||||
.with_additional_dirs(additional_allowed_dirs.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(
|
||||
@@ -765,18 +750,9 @@ pub async fn execute_parallel(
|
||||
&output,
|
||||
cancelled_flag,
|
||||
exec_start,
|
||||
tx,
|
||||
hook_registry,
|
||||
&app_state.config.storage.library_dir,
|
||||
&sid,
|
||||
agent_name,
|
||||
step,
|
||||
max_output_chars,
|
||||
ctx,
|
||||
&mut tool_messages,
|
||||
&mut additional_contexts,
|
||||
db,
|
||||
turn_index,
|
||||
tool_registry,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user