feat: Agent 全栈升级——模块化重构、Hooks/Skills/Memory/SubAgent/Team 子系统、审计与任务持久化

架构重构:
  - Agent Runtime 由单文件拆为 runtime/ 目录 12 模块(熔断/流式执行/Token预算/文件缓存/权限等)
  - Agent Tools 由单文件拆为 tools/ 目录 20+ 模块(filesystem/astro/memory/skill/subagent/team 等)
  - 解析器体系重构(common.rs 836行变更),各解析器同步升级
  - Download 服务重构(562行),反爬策略强化
  - LLM 客户端重构(266行),流式调用优化

  新子系统:
  - Hooks 生命周期系统(9种事件类型,PreToolUse/PostToolUse 支持输入输出拦截)
  - Skills 双层加载系统(system-reminder 轻量注入 + LoadSkillTool 按需加载,notify 文件监听热更新)
  - Memory 项目记忆管理(类型/提取/去重/衰减/保活/选择策略/护栏 7 模块)
  - SubAgent 上下文隔离子代理运行器(独立 ReAct 循环 + Hook 管道)
  - Team 多智能体团队协作(文件 inbox 通信、lead/teammate 协调)
  - TaskBoard DAG 任务依赖管理
  - Trajectory 会话轨迹、Terminal 终止信号、Autonomous 自主模式、Background 异步通知

  数据库:
  - agent_tasks 表(DAG 依赖模式,blocked_by JSON 数组)
  - agent_audit_log 表(工具调用审计:名称/状态/耗时/输出预览)
  - agent_identity 迁移(消息/审计/任务的 agent_name 归属,agent_team_members 团队注册表)

  API:
  - GET /chat/metrics 聚合指标端点
  - GET /chat/sessions/:id/audit 会话审计查询
  - GET /chat/questions + POST /chat/answer 人机交互问答

  工程:
  - 新增依赖:serde_yaml、notify、glob、walkdir、lru
  - Skills 目录含 methodology/plotting/presentation 三个初始 SKILL.md
  - CLAUDE.md 完整项目架构文档
This commit is contained in:
fmq
2026-06-17 00:14:02 +08:00
parent b1fb884f21
commit 49784739fa
113 changed files with 20253 additions and 2869 deletions
+446
View File
@@ -0,0 +1,446 @@
// src/agent/subagent.rs
//
// 子代理运行器 — 上下文隔离子代理(参考 Claude Code s04 Subagents)。
//
// 父代理通过 delegate_research 工具将子任务委托给子代理执行。
// 子代理拥有:
// - 全新的 messages 上下文(不包含父代理的中间工具调用)
// - 完整的工具访问权限(与父代理共享 ToolRegistry
// - 独立的 ReAct 循环
// - 完整的 Hook 管道(PreToolUse/PostToolUse)和权限检查
//
// 子代理只返回最终文本摘要给父代理,中间工具调用不污染父上下文。
use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender;
use tracing::{info, warn};
use super::compact;
use super::hooks::{
HookRegistry, PostToolUseContext, PreToolUseContext, SubagentStartContext,
SubagentStopContext,
};
use super::runtime::permission::PermissionChecker;
use super::runtime::{AgentConfig, AgentStreamEvent};
use super::tools::{ToolContext, ToolOutput, ToolRegistry};
use crate::api::AppState;
use crate::clients::llm::{ChatMessage, LlmClient, StreamEvent, ToolDefinition};
/// 子代理运行器
pub struct SubAgentRunner {
app_state: Arc<AppState>,
config: AgentConfig,
tool_registry: ToolRegistry,
/// 可选的 Hook 注册表(用于 PreToolUse/PostToolUse 生命周期事件)
hook_registry: Option<Arc<HookRegistry>>,
/// 权限检查器
permission_checker: Arc<PermissionChecker>,
/// 可选的进度发送器(用于向父代理报告中间步骤)
progress_tx: Option<UnboundedSender<AgentStreamEvent>>,
}
impl SubAgentRunner {
/// 创建新的子代理运行器(无 hook/permission/progress)。
pub fn new(app_state: Arc<AppState>) -> Self {
let skill_registry = app_state.skill_registry.clone();
SubAgentRunner {
app_state,
config: AgentConfig::default(),
tool_registry: ToolRegistry::new(skill_registry),
hook_registry: None,
permission_checker: Arc::new(PermissionChecker::new()),
progress_tx: None,
}
}
/// 创建带完整 hooks/permissions/progress 的子代理运行器。
pub fn new_with_hooks(
app_state: Arc<AppState>,
hook_registry: Option<Arc<HookRegistry>>,
permission_checker: Arc<PermissionChecker>,
progress_tx: Option<UnboundedSender<AgentStreamEvent>>,
) -> Self {
let skill_registry = app_state.skill_registry.clone();
SubAgentRunner {
app_state,
config: AgentConfig::default(),
tool_registry: ToolRegistry::new(skill_registry),
hook_registry,
permission_checker,
progress_tx,
}
}
/// 使用自定义 ToolRegistry 创建子代理运行器。
/// 用于受限场景(如记忆提取子代理仅需只读 + save_memory)。
pub fn new_with_registry(app_state: Arc<AppState>, tool_registry: ToolRegistry) -> Self {
SubAgentRunner {
app_state,
config: AgentConfig::default(),
tool_registry,
hook_registry: None,
permission_checker: Arc::new(PermissionChecker::new()),
progress_tx: None,
}
}
/// 运行子代理的 ReAct 循环,返回最终文本摘要。
///
/// # Arguments
/// * `system_prompt` - 子代理的系统提示词
/// * `research_prompt` - 要执行的研究任务描述
/// * `max_steps` - 子代理最大推理步数(默认 5)
/// * `hook_registry` - 可选的 HookRegistry(用于触发子代理生命周期事件)
pub async fn run(
&self,
system_prompt: &str,
research_prompt: &str,
max_steps: usize,
) -> ToolOutput {
let subagent_name = "delegate_research";
// OnSubagentStart hook
if let Some(ref registry) = self.hook_registry {
registry
.run_on_subagent_start(&SubagentStartContext {
parent_session_id: String::new(),
subagent_name: subagent_name.to_string(),
prompt: research_prompt.to_string(),
})
.await;
}
// 执行实际工作并捕获结果,以便触发 OnSubagentStop hook
let result = self
.run_inner(system_prompt, research_prompt, max_steps)
.await;
let (is_error, result_summary) = if result.is_error {
(true, result.content.clone())
} else {
(false, result.content.chars().take(200).collect())
};
if let Some(ref registry) = self.hook_registry {
registry
.run_on_subagent_stop(&SubagentStopContext {
parent_session_id: String::new(),
subagent_name: subagent_name.to_string(),
result_summary,
steps: max_steps,
is_error,
})
.await;
}
result
}
/// 实际执行逻辑(提取为内部方法以便 hook 包装)
async fn run_inner(
&self,
system_prompt: &str,
research_prompt: &str,
max_steps: usize,
) -> ToolOutput {
let llm = &self.app_state.llm;
let tool_defs = self.tool_registry.definitions();
// 全新上下文
let mut messages = vec![
ChatMessage::system(system_prompt),
ChatMessage::user(research_prompt),
];
// 跟踪工具调用防止死循环
let mut last_call: Option<(String, String)> = None;
let mut consecutive_count: usize = 0;
let duplicate_threshold: usize = 3;
for step in 1..=max_steps {
// 上下文压缩检查
let est_tokens: usize = messages
.iter()
.map(|m| m.content.as_ref().map_or(0, |c| c.len()) + 4)
.sum();
if est_tokens > self.config.context_char_limit * 3 / 2 {
info!(
"[SubAgent] 上下文超限 (est. {} tokens),触发压缩",
est_tokens
);
compact::compress_context(
&mut messages,
llm,
self.config.context_char_limit,
"subagent",
)
.await;
}
// LLM 流式调用
let mut stream_rx = match llm.chat_stream(&messages, &tool_defs).await {
Ok(rx) => rx,
Err(e) => {
warn!("[SubAgent] LLM stream 失败: {}", e);
return ToolOutput::error(format!("子代理 LLM 调用失败: {}", e));
}
};
let mut accumulated_content = String::new();
let mut accumulated_tool_calls: Option<Vec<crate::clients::llm::ToolCall>> = None;
while let Some(event) = stream_rx.recv().await {
match event {
StreamEvent::TextDelta(delta) => {
accumulated_content.push_str(&delta);
}
StreamEvent::ToolCallsComplete(tool_calls) => {
accumulated_tool_calls = Some(tool_calls);
}
StreamEvent::Done => break,
StreamEvent::Error(e) => {
warn!("[SubAgent] 流式错误: {}", e);
return ToolOutput::error(format!("子代理流式错误: {}", e));
}
_ => {}
}
}
// 无工具调用 = 最终回答
let tool_calls = match accumulated_tool_calls {
Some(ref tc) if !tc.is_empty() => tc.clone(),
_ => {
// 转发最终文本到父代理
if let Some(ref tx) = self.progress_tx {
let _ = tx.send(AgentStreamEvent::TextDelta {
content: format!(
"[子代理] {}",
accumulated_content.chars().take(200).collect::<String>()
),
});
}
let content_len = accumulated_content.len();
info!("[SubAgent] 子代理完成,返回 {} 字符摘要", content_len);
return ToolOutput::success(
accumulated_content,
serde_json::json!({
"steps": step,
"content_length": content_len
}),
);
}
};
// 构建 assistant 消息
let assistant_msg = ChatMessage::assistant_with_reasoning(
if accumulated_content.is_empty() {
None
} else {
Some(accumulated_content.clone())
},
None,
Some(tool_calls.clone()),
);
messages.push(assistant_msg);
// 执行工具调用
for tool_call in &tool_calls {
let tool_name = &tool_call.function.name;
let tool_args_str = &tool_call.function.arguments;
// 死循环检测
let call_key = (tool_name.clone(), tool_args_str.clone());
if last_call.as_ref() == Some(&call_key) {
consecutive_count += 1;
if consecutive_count >= duplicate_threshold {
warn!("[SubAgent] 检测到死循环:{}", tool_name);
let error_msg = ChatMessage::tool_result(
&tool_call.id,
format!(
"工具 {} 被连续重复调用。请停止并给出当前收集到的答案。",
tool_name
),
);
messages.push(error_msg);
continue;
}
} else {
last_call = Some(call_key);
consecutive_count = 1;
}
// 解析参数
let args: serde_json::Value = match serde_json::from_str(tool_args_str) {
Ok(v) => v,
Err(e) => {
let error_msg = ChatMessage::tool_result(
&tool_call.id,
format!("参数解析失败: {}", e),
);
messages.push(error_msg);
continue;
}
};
// ── 向父代理发送进度事件 ──
if let Some(ref tx) = self.progress_tx {
let _ = tx.send(AgentStreamEvent::ToolCall {
name: format!("[sub] {}", tool_name),
arguments: args.clone(),
step,
});
}
// ── PreToolUse hooks + Permission check ──
let tool_ctx = ToolContext::silent(self.app_state.clone());
let final_args = if let Some(ref registry) = self.hook_registry {
let pre_ctx = PreToolUseContext {
session_id: "subagent".to_string(),
tool_name: tool_name.clone(),
tool_args: args.clone(),
step,
};
let pre_result = registry.run_pre_tool_use(&pre_ctx).await;
// Block check
if pre_result.action.is_blocked() {
let reason = pre_result
.action
.block_reason()
.unwrap_or("tool blocked by hook");
warn!(
"[SubAgent] PreToolUse hook 阻止了工具: {} ({})",
tool_name, reason
);
let tool_msg = ChatMessage::tool_result(
&tool_call.id,
format!("工具 {} 被阻止: {}", tool_name, reason),
);
messages.push(tool_msg);
continue;
}
pre_result.final_args
} else {
args.clone()
};
// Permission check
if self.permission_checker.is_denied(tool_name) {
warn!("[SubAgent] 权限检查拒绝工具: {}", tool_name);
let tool_msg = ChatMessage::tool_result(
&tool_call.id,
format!("工具 {} 在子代理上下文中不可用(权限不足)", tool_name),
);
messages.push(tool_msg);
continue;
}
// 执行工具
let output = match self.tool_registry.get(tool_name) {
Some(tool) => {
match tokio::time::timeout(
std::time::Duration::from_secs(self.config.tool_timeout_secs),
tool.execute(final_args.clone(), &tool_ctx),
)
.await
{
Ok(output) => output,
Err(_) => ToolOutput::error(format!("工具 {} 执行超时", tool_name)),
}
}
None => ToolOutput::error(format!("未知工具: {}", tool_name)),
};
// ── PostToolUse hooks ──
let final_output_content = if let Some(ref registry) = self.hook_registry {
let post_ctx = PostToolUseContext {
session_id: "subagent".to_string(),
agent_name: "subagent".to_string(),
tool_name: tool_name.clone(),
tool_args: final_args,
output_content: output.content.clone(),
is_error: output.is_error,
step,
elapsed_ms: 0,
};
let post_result = registry.run_post_tool_use(&post_ctx).await;
post_result.final_content
} else {
output.content.clone()
};
// 向父代理发送工具结果进度
if let Some(ref tx) = self.progress_tx {
let preview: String = final_output_content.chars().take(200).collect();
let _ = tx.send(AgentStreamEvent::ToolResult {
name: format!("[sub] {}", tool_name),
output: preview,
is_error: output.is_error,
metadata: serde_json::json!({}),
step,
});
}
// 截断输出(使用 post-hook 处理后的内容)
let truncated = if final_output_content.len() > self.config.max_tool_output_chars {
let t: String = final_output_content
.chars()
.take(self.config.max_tool_output_chars)
.collect();
format!(
"{}...\n[已截断,原始 {} 字符]",
t,
final_output_content.len()
)
} else {
final_output_content.clone()
};
let tool_msg = ChatMessage::tool_result(&tool_call.id, &truncated);
messages.push(tool_msg);
}
}
// 达到最大步数,强制生成最终答案
info!("[SubAgent] 达到最大步数 ({}), 生成最终答案", max_steps);
self.force_final_answer(llm, &messages).await
}
/// 强制 LLM 生成最终答案(不带工具调用)
async fn force_final_answer(&self, llm: &LlmClient, messages: &[ChatMessage]) -> ToolOutput {
let mut final_messages = messages.to_vec();
final_messages.push(ChatMessage::user(
"请根据已收集的信息直接给出最终答案,不要再调用工具。",
));
let empty_tools: Vec<ToolDefinition> = Vec::new();
let mut stream_rx = match llm.chat_stream(&final_messages, &empty_tools).await {
Ok(rx) => rx,
Err(e) => {
return ToolOutput::error(format!("子代理最终答案生成失败: {}", e));
}
};
let mut accumulated = String::new();
while let Some(event) = stream_rx.recv().await {
match event {
StreamEvent::TextDelta(delta) => {
accumulated.push_str(&delta);
}
StreamEvent::Done => break,
StreamEvent::Error(e) => {
warn!("[SubAgent] 最终答案流式错误: {}", e);
break;
}
_ => {}
}
}
if accumulated.is_empty() {
ToolOutput::error("子代理无法生成最终答案")
} else {
ToolOutput::success(accumulated, serde_json::json!({ "forced": true }))
}
}
}