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:
@@ -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 }))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user