AstroResearch/src/agent/hooks/dispatch.rs
Asfmq 698d007f39 feat: Agent 安全纵深防御、Checkpoint 快照、会话 Rewind/Branch、自进化
Skill、流式执行优化与系统架构全面升级

  本次提交对标 Claude Code 与 Hermes-Agent 的工程细节,在安全、可靠性、
  会话管理、自我进化四个维度进行了系统性加固,变更总量 48 文件 / +12680 -2292 行。

  ═══════ 安全纵深防御 ═══════

  1. Hardline 硬阻止层 (src/agent/runtime/hardline.rs, +534 行)
     - 不可绕过的危险命令拦截(关重启、磁盘擦除、Fork 炸弹、rm -rf /、kill -1)
     - 反规避标准化管线: ANSI 序列剥离 → Unicode NFKC → shell 反斜杠还原 → 空字面量清理
     - 在 PermissionChecker 之前执行,YOLO/Bypass 模式下同样生效
     - 集成到 executor Phase 2,被拒绝工具直接注入错误结果

  2. Permission 优先级裁决器 (src/agent/runtime/permission.rs, +200 行)
     - 7 层正式优先级规则 (P0 Deny → P7 Allow),带冲突日志
     - explain() 方法支持审计追溯
     - Hook PermissionRequired 与 Checker 结果的正确叠加逻辑

  ═══════ Checkpoint 文件快照系统 ═══════

  3. git2 原生快照 (src/agent/runtime/checkpoint.rs, +920 行)
     - 基于 git2 bare repo,内容寻址自动去重
     - 文件变更操作前自动触发 (file_write/file_edit/run_bash)
     - 每目录每 turn 最多一次快照,防止同一轮重复
     - 支持 list/diff/restore API + pre-rollback 安全快照
     - 旧快照自动 prune(保留最近 N 个)+ 按目录隔离 ref
     - 排除规则自动过滤 node_modules/target/.git/*.pdf 等
     - 集成到 executor: 文件操作前 ckpt.ensure_checkpoint()

  ═══════ 错误恢复系统大升级 ═══════

  4. 21 种 FailoverReason 分类 (src/agent/runtime/error_recovery.rs, +1200 行)
     - 参考 Hermes-Agent error_classifier.py
     - 8 步分类管线: provider-specific → HTTP status → text pattern → error body → fallback
     - is_retryable / should_compress / should_failover / is_permanent 方法
     - Context Overflow 自动修复: 从错误消息提取 token 限制,自动下调预算
     - RecoveryStep::AdjustMaxTokens 实现 (参考 Claude Code 自动修复)
     - 向后兼容 ErrorKind 别名

  ═══════ 会话 Rewind / Branch / Retry 体系 ═══════

  5. 完整 undo 栈 (src/agent/runtime/session.rs, +800 行 + 2 迁移脚本)
     - Rewind (软删除): active=0 标记,审计 trail 保留,LLM 不可见
     - Restore (撤销回退): 冲突检测——回退后有新消息则拒绝,引导使用 Branch
     - Branch: 分叉会话,复制所有 active=1 消息到新会话
     - Retry: 硬删除最后一轮对话,返回原消息文本供前端重提交
     - 数据库: agent_messages.active 列 + agent_sessions.rewind_count + parent_session_id
     - API: 4 个新端点 (/branch, /retry, /rewind, /rewind/restore)
     - load_history_for_agent 全面使用 active=1 过滤

  ═══════ Hooks 系统模块化重构 ═══════

  6. 单文件 → 7 模块体系 (src/agent/hooks/)
     hooks.rs (994 行) 拆分为:
     - mod.rs    — 入口 + HookRegistry + SessionHookManager
     - types.rs  — 类型定义 (Context, TaggedContext, PermissionRequestAction 等)
     - traits.rs — AgentHook + AsyncAgentHook + 15 种生命周期事件
     - matcher.rs — 工具名/参数匹配 + session 作用域过滤
     - dispatch.rs — 并行调度引擎 (run_pre/post_tool_use 等)
     - registry.rs — 注册/注销/查询
     - builtins.rs — CancellationHook + MetricsHook + AuditLogHook + ContextDeduplicator

     关键改进:
     - run_pre_tool_use 并行执行所有匹配 hooks,聚合 Block/MutateInput/Continue
     - TaggedContext 带完整来源标记的上下文注入 (hook_name + event)
     - ContextDeduplicator 单 dispatch cycle 内内容哈希去重
     - AsyncAgentHook 支持 fire-and-forget 异步 hooks

  ═══════ Executor 并发执行升级 ═══════

  7. 三阶段管道重写 (src/agent/runtime/executor.rs, +600 行)
     - Phase 1: 死循环检测 + 参数解析 (不变)
     - Phase 2: Hardline 预检查 (新增) → PermissionChecker (改进)
     - Phase 3: ToolPartitioner 分区 → 逐批次执行 (重写)
       - 并行批次内 FuturesUnordered 并发
       - 串行批次确保非并发安全工具独占执行
       - Checkpoint 预触发集成
     - Hook 上下文注入: system-reminder 格式 + ContextDeduplicator 去重
     - Hook 阻塞错误详细记录

  ═══════ 流式执行真正的流式调度 ═══════

  8. StreamingExecutor 重写 (src/agent/runtime/streaming_executor.rs, ~400 行变更)
     - on_tool_use 中对并发安全工具立即 tokio::spawn,不等待 flush
     - executing_non_concurrent 标志阻塞后继工具直到独占工具完成
     - JoinHandle 管理替代自定义 cancel channel
     - completed_queue 按流顺序 yield
     - Sibling Abort 通过 broadcast channel + tokio::select! 竞速
     - ToolContext 实现 Clone (支持 per-task 上下文复制)

  ═══════ 自改进 Skill 系统 ═══════

  9. PatternDetector + SkillCreator + Curator (src/agent/skills/, +1500 行)
     - PatternDetector: 扫描 agent_messages 表,检测跨 session 重复工具调用模式
     - SkillCreator: 将高置信度模式自动生成 SKILL.md (YAML frontmatter + 工作流步骤)
     - SelfImprovePipeline: 一站式 模式检测 → 创建 → 质量审查
     - Curator: 分析 skill 使用统计,标记 stale/deprecated,建议清理
     - Skill frontmatter 新增 pinned 字段 (禁止 Curator 自动清理)

  ═══════ 基础设施优化 ═══════

  10. 系统提示词缓存 (src/agent/runtime/system_prompt.rs + mod.rs)
      - SystemPromptCache: 首次计算后永久复用,/clear 时失效
      - 新增 SAFETY / SYSTEM_CONTEXT / TOOL_USAGE 静态 section
      - 环境/tools/skills/memory 动态 section 通过 get_or_compute 缓存

  11. ToolRegistry schema 缓存 (src/agent/tools/mod.rs)
      - schema_cache + schema_generation 版本号
      - 工具变更/过滤器变更时自动失效
      - precompute_definitions() 预计算 (AgentRuntime 初始化时调用)

  12. 迭代摘要融合 (src/agent/compact.rs, +100 行)
      - 参考 Hermes context_compressor.py
      - CollapseLog 追踪压缩历史,支持溢出合并
      - extract_prior_summary: 提取已有摘要融入新压缩

  13. SubAgent 系统提示词模块化 (src/agent/tools/subagent.rs)
      - 复用 5 个标准 section + 子代理专有上下文 section
      - 独立 ToolRegistry 构建工具列表

  ═══════ 前端 — CSS 变量主题系统 ═══════

  14. 全新主题变量体系 (dashboard/src/index.css + App.tsx + 各面板)
      - CSS 自定义属性: --bg-card, --text-main, --text-muted, --border-precision
      - 语义化颜色: --accent-blueprint, --accent-star
      - 全面替换硬编码 Tailwind 颜色 (slate-xxx → var(--xxx))
      - 文献入库提示优化 ("核心知识节点" 替代 "向量块")
      - ReaderPanel 样式变量化
2026-06-22 20:29:37 +08:00

791 lines
30 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/hooks/dispatch.rs
//!
//! HookRegistry 调度方法——所有 run_* 生命周期事件分发。
use std::time::Duration;
use futures_util::future::join_all;
use tokio::time::timeout as tokio_timeout;
use tracing::{info, warn};
use super::{
AgentHook, AsyncAgentHook, BlockingError, HookEvent, PermissionDeniedContext,
PermissionRequestAction, PermissionRequestContext, PostCompactContext, PostToolUseAction,
PostToolUseContext, PostToolUseFailureContext, PostToolUseResult, PreCompactContext,
PreToolUseAction, PreToolUseContext, PreToolUseResult, SessionStartContext, SessionStopContext,
StepCompleteContext, SubagentStartContext, SubagentStopContext, TaggedContext,
DEFAULT_HOOK_TIMEOUT,
};
use serde_json;
impl super::HookRegistry {
// ── 便捷调用方法 ──
/// 调度匹配指定事件的异步 hooksfire-and-forget5s dispatch 超时)
async fn dispatch_async_hooks_for(
&self,
event: HookEvent,
_session_id: &str,
_tool_name: &str,
_tool_args: &serde_json::Value,
post_ctx: Option<&PostToolUseContext>,
failure_ctx: Option<&PostToolUseFailureContext>,
) {
let async_hooks: Vec<&dyn AsyncAgentHook> = self
.async_event_index
.get(&event)
.map(|indices| {
indices
.iter()
.filter_map(|&idx| self.async_hooks.get(idx).map(|b| b.as_ref()))
.filter(|_hook| {
// Async hooks can declare filtering too via name convention;
// for now, all matching the event fire.
true
})
.collect()
})
.unwrap_or_default();
if async_hooks.is_empty() {
return;
}
let timeout_dur = Duration::from_secs(5); // async dispatch timeout
// Build futures — clone contexts so futures own their data and satisfy 'static.
match event {
HookEvent::PostToolUse if post_ctx.is_some() => {
let ctx = post_ctx.unwrap().clone();
for hook in async_hooks {
let ctx = ctx.clone();
let _ = tokio_timeout(timeout_dur, hook.on_post_tool_use_async(ctx)).await;
}
}
HookEvent::PostToolUseFailure if failure_ctx.is_some() => {
let ctx = failure_ctx.unwrap().clone();
for hook in async_hooks {
let ctx = ctx.clone();
let _ =
tokio_timeout(timeout_dur, hook.on_post_tool_use_failure_async(ctx)).await;
}
}
_ => {}
}
}
/// 调用所有 on_session_start hooks并行执行含 session hooks
pub async fn run_on_session_start(&self, ctx: &SessionStartContext) {
let hooks = self.collect_hooks_for(HookEvent::OnSessionStart, &ctx.session_id);
if hooks.is_empty() {
return;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
async move {
let _ = tokio_timeout(timeout_dur, hook.on_session_start(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
/// 调用所有 pre_tool_use hooks并行执行各自有独立超时
///
/// 所有订阅了 PreToolUse 的 hooks含 session hooks并行运行每个 hook 包装在
/// `tokio::time::timeout` 中。完成后聚合所有结果:
/// - Block → 收集到 blocking_errors
/// - MutateInput → 累积 additional_contexts 并更新 final_args
/// - PermissionRequired → 记录
/// - 超时 → 记录 warning视为 non-blocking error
pub async fn run_pre_tool_use(&self, ctx: &PreToolUseContext) -> PreToolUseResult {
let hooks = self.collect_tool_hooks_for(
HookEvent::PreToolUse,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
);
// Fast path: 单 hook 或空 → 顺序执行
if hooks.len() <= 1 {
return self.run_pre_tool_use_sequential(ctx).await;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
let hook_name = hook.name().to_string();
async move {
(
hook_name,
tokio_timeout(timeout_dur, hook.pre_tool_use(&ctx)).await,
)
}
})
.collect();
// 并行执行所有 hooks
let results = join_all(futures).await;
// 聚合结果
let mut additional_contexts: Vec<String> = Vec::new();
let mut tagged_contexts: Vec<TaggedContext> = Vec::new();
let mut final_args = ctx.tool_args.clone();
let mut final_action = PreToolUseAction::Continue;
let mut blocking_errors: Vec<BlockingError> = Vec::new();
for (hook_name, result) in results {
match result {
Ok(action) => {
match &action {
PreToolUseAction::Block { reason } => {
warn!(
"[Hooks] {hook_name} 阻止了工具 {} 的执行: {reason}",
ctx.tool_name
);
blocking_errors.push(BlockingError::new(hook_name, reason.clone()));
}
PreToolUseAction::MutateInput {
updated_args,
additional_context,
} => {
info!(
"[Hooks] {hook_name} 修改了工具 {} 的输入参数",
ctx.tool_name
);
final_args = updated_args.clone();
if let Some(ctx_str) = additional_context {
additional_contexts.push(ctx_str.clone());
tagged_contexts.push(TaggedContext::new(
&hook_name,
HookEvent::PreToolUse,
ctx_str,
));
}
}
PreToolUseAction::PermissionRequired { .. } => {
info!(
"[Hooks] {hook_name} 请求了工具 {} 的权限检查",
ctx.tool_name
);
}
PreToolUseAction::Continue => {}
}
if !matches!(action, PreToolUseAction::Continue) {
final_action = action;
}
}
Err(_elapsed) => {
warn!("[Hooks] {hook_name} 超时PreToolUse跳过其反馈");
}
}
}
let additional_context = if additional_contexts.is_empty() {
None
} else {
Some(additional_contexts.join("\n"))
};
PreToolUseResult {
action: final_action,
additional_contexts,
additional_context,
blocking_errors,
final_args,
tagged_contexts,
}
}
/// 顺序执行 pre_tool_use hooks含 session hooks
async fn run_pre_tool_use_sequential(&self, ctx: &PreToolUseContext) -> PreToolUseResult {
let mut additional_contexts: Vec<String> = Vec::new();
let mut tagged_contexts: Vec<TaggedContext> = Vec::new();
let mut final_args = ctx.tool_args.clone();
let mut final_action = PreToolUseAction::Continue;
let mut blocking_errors: Vec<BlockingError> = Vec::new();
// 全局 hooks通过 collect_tool_hooks_for 应用 match_filter 过滤)
let hooks = self.collect_tool_hooks_for(
HookEvent::PreToolUse,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
);
for hook in &hooks {
let action = hook.pre_tool_use(ctx).await;
Self::process_pre_tool_action(
&action,
hook.name(),
&ctx.tool_name,
&mut blocking_errors,
&mut final_args,
&mut additional_contexts,
&mut tagged_contexts,
&mut final_action,
);
}
let additional_context = if additional_contexts.is_empty() {
None
} else {
Some(additional_contexts.join("\n"))
};
PreToolUseResult {
action: final_action,
additional_contexts,
additional_context,
blocking_errors,
final_args,
tagged_contexts,
}
}
/// 调用所有 post_tool_use hooks并行执行含 session hooks各自有独立超时
pub async fn run_post_tool_use(&self, ctx: &PostToolUseContext) -> PostToolUseResult {
// 同时调度异步 hooksfire-and-forget
self.dispatch_async_hooks_for(
HookEvent::PostToolUse,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
Some(ctx),
None,
)
.await;
let hooks = self.collect_tool_hooks_for(
HookEvent::PostToolUse,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
);
// Fast path: 单 hook 或空 → 顺序执行
if hooks.len() <= 1 {
return self.run_post_tool_use_sequential(ctx).await;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
let hook_name = hook.name().to_string();
async move {
(
hook_name,
tokio_timeout(timeout_dur, hook.post_tool_use(&ctx)).await,
)
}
})
.collect();
let results = join_all(futures).await;
let mut final_content = ctx.output_content.clone();
let mut additional_contexts: Vec<String> = Vec::new();
let mut tagged_contexts: Vec<TaggedContext> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
let mut post_permission_requests: Vec<(String, String)> = Vec::new();
let mut metadata: std::collections::HashMap<String, serde_json::Value> =
std::collections::HashMap::new();
for (hook_name, result) in results {
match result {
Ok(action) => match &action {
PostToolUseAction::MutateOutput {
updated_content: ref uc,
additional_context: ref ac,
} => {
info!("[Hooks] {hook_name} 修改了工具 {} 的输出", ctx.tool_name);
final_content = uc.clone();
if let Some(ctx_str) = ac {
additional_contexts.push(ctx_str.clone());
tagged_contexts.push(TaggedContext::new(
&hook_name,
HookEvent::PostToolUse,
ctx_str,
));
}
}
PostToolUseAction::Warning {
message,
truncate_output,
} => {
warn!(
"[Hooks] {hook_name} 发出了对 {} 的警告: {message}",
ctx.tool_name
);
warnings.push(message.clone());
if *truncate_output {
final_content = final_content.chars().take(1000).collect::<String>()
+ "\n\n[输出已由 Hook 截断]";
}
}
PostToolUseAction::PermissionRequired {
permission,
tool_name,
} => {
info!("[Hooks] {hook_name} 事后请求工具 {tool_name} 的权限: {permission}");
post_permission_requests.push((tool_name.clone(), permission.clone()));
}
PostToolUseAction::Metadata { key, value } => {
metadata.insert(key.clone(), value.clone());
}
PostToolUseAction::Continue => {}
},
Err(_elapsed) => {
warn!("[Hooks] {hook_name} 超时PostToolUse跳过其反馈");
}
}
}
PostToolUseResult {
final_content,
additional_contexts,
tagged_contexts,
warnings,
post_permission_requests,
metadata,
}
}
/// 顺序执行 post_tool_use hooks含 session hooks
async fn run_post_tool_use_sequential(&self, ctx: &PostToolUseContext) -> PostToolUseResult {
let mut final_content = ctx.output_content.clone();
let mut additional_contexts: Vec<String> = Vec::new();
let mut tagged_contexts: Vec<TaggedContext> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
let mut post_permission_requests: Vec<(String, String)> = Vec::new();
let mut metadata: std::collections::HashMap<String, serde_json::Value> =
std::collections::HashMap::new();
let hooks = self.collect_tool_hooks_for(
HookEvent::PostToolUse,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
);
for hook in &hooks {
let action = hook.post_tool_use(ctx).await;
match action {
PostToolUseAction::MutateOutput {
updated_content,
additional_context,
} => {
final_content = updated_content;
if let Some(ctx_str) = additional_context {
additional_contexts.push(ctx_str.clone());
tagged_contexts.push(TaggedContext::new(
hook.name(),
HookEvent::PostToolUse,
ctx_str,
));
}
}
PostToolUseAction::Warning {
message,
truncate_output,
} => {
warnings.push(message);
if truncate_output {
final_content = final_content.chars().take(1000).collect::<String>()
+ "\n\n[输出已由 Hook 截断]";
}
}
PostToolUseAction::PermissionRequired {
permission,
tool_name,
} => {
post_permission_requests.push((tool_name, permission));
}
PostToolUseAction::Metadata { key, value } => {
metadata.insert(key, value.clone());
}
PostToolUseAction::Continue => {}
}
}
PostToolUseResult {
final_content,
additional_contexts,
tagged_contexts,
warnings,
post_permission_requests,
metadata,
}
}
/// 调用所有 post_tool_use_failure hooks并行执行含 session hooks
pub async fn run_on_post_tool_use_failure(
&self,
ctx: &PostToolUseFailureContext,
) -> PostToolUseResult {
// 同时调度异步 hooksfire-and-forget
self.dispatch_async_hooks_for(
HookEvent::PostToolUseFailure,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
None,
Some(ctx),
)
.await;
let hooks = self.collect_tool_hooks_for(
HookEvent::PostToolUseFailure,
&ctx.session_id,
&ctx.tool_name,
&ctx.tool_args,
);
// Fast path: 单 hook 或空 → 顺序执行
if hooks.len() <= 1 {
let mut final_content = String::new();
let mut additional_contexts: Vec<String> = Vec::new();
let mut tagged_contexts: Vec<TaggedContext> = Vec::new();
for hook in &hooks {
let action = hook.on_post_tool_use_failure(ctx).await;
match action {
PostToolUseAction::MutateOutput {
updated_content,
additional_context,
} => {
final_content = updated_content;
if let Some(ctx_str) = additional_context {
additional_contexts.push(ctx_str.clone());
tagged_contexts.push(TaggedContext::new(
hook.name(),
HookEvent::PostToolUseFailure,
ctx_str,
));
}
}
PostToolUseAction::Warning {
message,
truncate_output,
} => {
if truncate_output {
final_content = final_content.chars().take(1000).collect();
}
tagged_contexts.push(TaggedContext::new(
hook.name(),
HookEvent::PostToolUseFailure,
format!("Warning: {message}"),
));
}
PostToolUseAction::PermissionRequired { .. }
| PostToolUseAction::Metadata { .. }
| PostToolUseAction::Continue => {}
}
}
return PostToolUseResult {
final_content,
additional_contexts,
tagged_contexts,
warnings: Vec::new(),
post_permission_requests: Vec::new(),
metadata: std::collections::HashMap::new(),
};
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
let hook_name = hook.name().to_string();
async move {
(
hook_name,
tokio_timeout(timeout_dur, hook.on_post_tool_use_failure(&ctx)).await,
)
}
})
.collect();
let results = join_all(futures).await;
let mut final_content = String::new();
let mut additional_contexts: Vec<String> = Vec::new();
let mut tagged_contexts: Vec<TaggedContext> = Vec::new();
for (hook_name, result) in results {
match result {
Ok(action) => match action {
PostToolUseAction::MutateOutput {
updated_content,
additional_context,
} => {
final_content = updated_content;
if let Some(ctx_str) = additional_context {
additional_contexts.push(ctx_str.clone());
tagged_contexts.push(TaggedContext::new(
&hook_name,
HookEvent::PostToolUseFailure,
ctx_str,
));
}
}
PostToolUseAction::Warning {
message,
truncate_output,
} => {
if truncate_output {
final_content = final_content.chars().take(1000).collect();
}
tagged_contexts.push(TaggedContext::new(
&hook_name,
HookEvent::PostToolUseFailure,
format!("Warning: {message}"),
));
}
PostToolUseAction::PermissionRequired { .. }
| PostToolUseAction::Metadata { .. }
| PostToolUseAction::Continue => {}
},
Err(_elapsed) => {
// 超时已在警告中体现,跳过该 hook 的反馈
}
}
}
PostToolUseResult {
final_content,
additional_contexts,
tagged_contexts,
warnings: Vec::new(),
post_permission_requests: Vec::new(),
metadata: std::collections::HashMap::new(),
}
}
/// 调用所有 on_step_complete hooks并行执行含 session hooks
pub async fn run_on_step_complete(&self, ctx: &StepCompleteContext) {
let hooks = self.collect_hooks_for(HookEvent::OnStepComplete, &ctx.session_id);
if hooks.is_empty() {
return;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
async move {
let _ = tokio_timeout(timeout_dur, hook.on_step_complete(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
/// 调用所有 on_session_stop hooks顺序执行 + 自动清理 session hooks
pub async fn run_on_session_stop(&self, ctx: &SessionStopContext<'_>) {
// 全局 hooks
for &idx in self.indices_for(HookEvent::OnSessionStop) {
if let Some(hook) = self.hooks.get(idx) {
let _ = tokio_timeout(
hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT),
hook.on_session_stop(ctx),
)
.await;
}
}
// Session hooksStop 事件也通知它们)
for hook in self.get_session_hooks(&ctx.session_id) {
let _ = tokio_timeout(
hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT),
hook.on_session_stop(ctx),
)
.await;
}
// 注意clear_session_hooks 需要 &mut self但此方法是 &self。
// 调用方应在 run_on_session_stop 返回后主动调用 clear_session_hooks。
}
/// 调用所有 on_subagent_start hooks并行执行含 session hooks
pub async fn run_on_subagent_start(&self, ctx: &SubagentStartContext) {
let hooks = self.collect_hooks_for(HookEvent::OnSubagentStart, &ctx.parent_session_id);
if hooks.is_empty() {
return;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
async move {
let _ = tokio_timeout(timeout_dur, hook.on_subagent_start(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
/// 调用所有 on_subagent_stop hooks并行执行含 session hooks
pub async fn run_on_subagent_stop(&self, ctx: &SubagentStopContext) {
let hooks = self.collect_hooks_for(HookEvent::OnSubagentStop, &ctx.parent_session_id);
if hooks.is_empty() {
return;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
async move {
let _ = tokio_timeout(timeout_dur, hook.on_subagent_stop(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
/// 调用所有 on_pre_compact hooks并行执行含 session hooks
pub async fn run_on_pre_compact(&self, ctx: &PreCompactContext) {
let hooks = self.collect_hooks_for(HookEvent::OnPreCompact, &ctx.session_id);
if hooks.is_empty() {
return;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
async move {
let _ = tokio_timeout(timeout_dur, hook.on_pre_compact(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
/// 调用所有 on_post_compact hooks并行执行含 session hooks
pub async fn run_on_post_compact(&self, ctx: &PostCompactContext) {
let hooks = self.collect_hooks_for(HookEvent::OnPostCompact, &ctx.session_id);
if hooks.is_empty() {
return;
}
let ctx_clone = ctx.clone();
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let ctx = ctx_clone.clone();
let timeout_dur = hook.timeout().unwrap_or(DEFAULT_HOOK_TIMEOUT);
async move {
let _ = tokio_timeout(timeout_dur, hook.on_post_compact(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
/// 分发 PermissionRequest 事件。
///
/// 并行调用所有匹配的 hook。每个 hook 返回 `PermissionRequestAction`
/// - `Continue` → 保持当前决策
/// - `Override` → 覆盖决策(第一个 Override 生效,后续不再检查)
/// - `InjectContext` → 注入上下文但不改变决策
///
/// 返回 (final_action, additional_contexts)。
pub async fn run_on_permission_request(
&self,
ctx: &PermissionRequestContext,
) -> (PermissionRequestAction, Vec<String>) {
let hooks = self.collect_hooks_for(HookEvent::PermissionRequest, &ctx.session_id);
if hooks.is_empty() {
return (PermissionRequestAction::Continue, Vec::new());
}
let mut final_action = PermissionRequestAction::Continue;
let mut injected_contexts: Vec<String> = Vec::new();
let mut overridden = false;
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let hook: &dyn AgentHook = *hook;
let ctx = ctx.clone();
let timeout_dur = DEFAULT_HOOK_TIMEOUT;
async move {
let result = tokio_timeout(timeout_dur, hook.on_permission_request(&ctx)).await;
match result {
Ok(action) => Some((hook.name().to_string(), action)),
Err(_) => {
warn!("[HookDispatch] PermissionRequest hook {} 超时", hook.name());
None
}
}
}
})
.collect();
let results: Vec<Option<(String, PermissionRequestAction)>> = join_all(futures).await;
for result in results.into_iter().flatten() {
let (hook_name, action) = result;
match action {
PermissionRequestAction::Continue => {
// 默认,不改变
}
PermissionRequestAction::Override(decision) if !overridden => {
overridden = true;
final_action = PermissionRequestAction::Override(decision);
info!(
"[HookDispatch] PermissionRequest hook {} 覆盖决策",
hook_name
);
}
PermissionRequestAction::Override(_) => {
warn!(
"[HookDispatch] PermissionRequest hook {}: 覆盖被忽略(已有其他 hook 先覆盖)",
hook_name
);
}
PermissionRequestAction::InjectContext { ref content } => {
injected_contexts.push(content.clone());
}
}
}
(final_action, injected_contexts)
}
/// 分发 PermissionDenied 事件fire-and-forget仅用于审计日志
///
/// 并行调用所有匹配的 hook。返回值不影响执行流程。
pub async fn run_on_permission_denied(&self, ctx: &PermissionDeniedContext) {
let hooks = self.collect_hooks_for(HookEvent::PermissionDenied, &ctx.session_id);
if hooks.is_empty() {
return;
}
let futures: Vec<_> = hooks
.iter()
.map(|hook| {
let hook: &dyn AgentHook = *hook;
let ctx = ctx.clone();
let timeout_dur = DEFAULT_HOOK_TIMEOUT;
async move {
let _ = tokio_timeout(timeout_dur, hook.on_permission_denied(&ctx)).await;
}
})
.collect();
join_all(futures).await;
}
}