重构: Coordinator 接入 agentic loop + audit 拆分(approval/record)

- audit/approval.rs: 审批门控(风险分类+自动执行+挂起审批)

- audit/record.rs: 审计记录写入+历史查询

- coordinator.rs: 移除 deprecated,正式作为 P0 骨架可用

- run_agentic_loop: plan_execution_enabled 时 Coordinator 分解意图→记录 Plan

- audit/mod.rs 从 ~500 行精简至 ~200 行(编排层)
This commit is contained in:
2026-07-01 14:29:34 +08:00
parent a9ee9b1f74
commit fa410e6843
11 changed files with 1234 additions and 523 deletions

View File

@@ -0,0 +1,514 @@
//! 多 Agent 协作调度中心 — Coordinator
//!
//! Phase 1 实现Phase 2 LLM 扩展
//!
//! 职责拆解4 步):
//! 1. **decompose**:接收用户意图 + 消息 → 拆解为 SubTask 列表
//! 2. **分配**:为每个 SubTask 分配人设PersonaRegistry.recommend_for_intent
//! 3. **dispatch**:并行执行 SubTask当前 Phase 1 串行,预留 JoinSet 并行)
//! 4. **merge**:汇总子结果 → 合并产出 → 处理冲突
use crate::persona::PersonaRegistry;
use crate::planner::{Plan, SubTask};
// ---- 枚举 --------------------------------------------------------------------
/// 调度策略枚举
pub enum DispatchStrategy {
/// 规则驱动关键词匹配Phase 1 MVP
RuleBased,
/// LLM 驱动LLM 生成 PlanPhase 2 预留)
LLMDriven,
}
impl DispatchStrategy {
/// 策略名称标签(日志 / 审计用)
pub fn name(&self) -> &'static str {
match self {
Self::RuleBased => "rule_based",
Self::LLMDriven => "llm_driven",
}
}
}
// ---- 数据结构 ----------------------------------------------------------------
/// 拆解结果
pub struct DecompositionResult {
/// 拆解出的子任务列表
pub subtasks: Vec<SubTask>,
/// 对应 Plan 结构(含依赖关系)
pub plan: Plan,
}
/// 执行结果
pub struct ExecutionResult {
/// 对应 SubTask id
pub subtask_id: String,
/// 分配的人设 id
pub persona_id: String,
/// 执行产出(工具结果摘要 / LLM 回复)
pub output: String,
/// 执行是否成功
pub success: bool,
}
/// 合并结果
pub struct MergeResult {
/// 合并后的最终产出
pub merged_output: String,
/// 检测到的冲突列表Phase 2 由 reviewer Agent 填充)
pub conflicts: Vec<ConflictItem>,
}
/// 冲突项
pub struct ConflictItem {
/// 冲突涉及的文件
pub file: String,
/// 冲突描述
pub description: String,
}
// ---- Coordinator 实现 --------------------------------------------------------
/// 多 Agent 协作调度器
///
/// # Phase 1当前
///
/// 规则驱动 decompose + 串行 dispatch + 简单拼接 merge。
/// 适用于「读后写」「搜索后分析」等可预测的意图组合。
///
/// # Phase 2预留
///
/// - LLM 驱动 decomposeLLM 生成 Plan 结构)
/// - JoinSet 并行 dispatch层内并行
/// - Reviewer Agent 仲裁 merge冲突检测 + 自动解决)
pub struct Coordinator {
/// 人设注册表(内置 5 人设 + 自定义)
registry: PersonaRegistry,
}
impl Coordinator {
/// 创建 Coordinator绑定人设注册表
pub fn new(registry: PersonaRegistry) -> Self {
Self { registry }
}
/// 规则拆解intent + text → SubTask 列表 + Plan
///
/// Phase 1 规则(关键词匹配):
/// - 检测 text 中是否含 `read`/`写`/`查看`/`分析`/`search` 等关键词 → 读子任务
/// - 检测 text 中是否含 `write`/`修改`/`生成`/`create`/`fix` 等关键词 → 写子任务
/// - 同时含读+写关键词 → 两个子任务写依赖读read → write
/// - 每 SubTask 分配人设(`PersonaRegistry::recommend_for_intent`
/// - 兜底:无法匹配任何关键词时产单个 default 子任务
///
/// ## Phase 2 扩展点
/// `DispatchStrategy::LLMDriven` 时改为调 LLM 生成 Plan 整体结构,
/// 本方法当前仅走 `RuleBased` 路径。
pub fn decompose(&self, intent: &str, text: &str) -> DecompositionResult {
let lower = text.to_lowercase();
// 规则 1: 读操作关键词
let has_read = lower.contains("read")
|| lower.contains("查看")
|| lower.contains("读取")
|| lower.contains("分析")
|| lower.contains("search")
|| lower.contains("搜索")
|| lower.contains("审查")
|| lower.contains("review");
// 规则 2: 写操作关键词
let has_write = lower.contains("write")
|| lower.contains("")
|| lower.contains("修改")
|| lower.contains("生成")
|| lower.contains("创建")
|| lower.contains("implement")
|| lower.contains("fix")
|| lower.contains("修复")
|| lower.contains("新增");
let mut subtasks = Vec::new();
match (has_read, has_write) {
(true, true) => {
// 读 + 写 → 两个子任务read → write
subtasks.push(
SubTask::new("read", "读取/分析现有代码")
.with_tools(vec![
"read_file".into(),
"search_files".into(),
"list_directory".into(),
])
.with_deps(vec![]),
);
subtasks.push(
SubTask::new("write", "修改/生成代码")
.with_tools(vec![
"write_file".into(),
"patch_file".into(),
"edit_file".into(),
])
.with_deps(vec!["read".into()]),
);
}
(true, false) => {
// 纯读 → 单个读子任务
subtasks.push(
SubTask::new("read", "读取/分析代码")
.with_tools(vec![
"read_file".into(),
"search_files".into(),
"list_directory".into(),
])
.with_deps(vec![]),
);
}
(false, true) => {
// 纯写 → 单个写子任务
subtasks.push(
SubTask::new("write", "修改/生成代码")
.with_tools(vec![
"write_file".into(),
"patch_file".into(),
"edit_file".into(),
])
.with_deps(vec![]),
);
}
(false, false) => {
// 兜底:按 intent 分配单个默认子任务
subtasks.push(
SubTask::new("default", intent.to_string())
.with_tools(vec![])
.with_deps(vec![]),
);
}
}
let plan = Plan::from_tasks(subtasks.clone());
DecompositionResult { subtasks, plan }
}
/// 分发执行:按 Plan 逐层执行 SubTask
///
/// Phase 1串行执行层内层间均串行后续可改为 `JoinSet` 并行。
///
/// ## 参数
/// - `plan`: 待执行的 DAG Plan
/// - `executor`: 子任务执行函数,
/// 签名 `fn(SubTask, AgentPersona) -> Future<Output = ExecutionResult>`
/// 传入 owned SubTask 和 AgentPersona 副本,避免生命周期约束。
///
/// ## 返回值
/// 按 Plan 原始顺序排列的执行结果列表。
pub async fn dispatch<F, Fut>(&self, plan: &Plan, executor: F) -> Vec<ExecutionResult>
where
F: Fn(SubTask, crate::persona::AgentPersona) -> Fut,
Fut: std::future::Future<Output = ExecutionResult>,
{
let mut results = Vec::new();
// Phase 1串行逐任务执行忽略 to_layers 分层,纯顺序执行)
for task in &plan.tasks {
let persona = self.registry.recommend_for_intent(&task.intent);
let result = executor(task.clone(), persona.clone()).await;
results.push(result);
}
results
}
/// 合并:汇总子结果 → 合并产出
///
/// Phase 1简单拼接各 SubTask 产出,每段带标题标识来源。
/// `conflicts` 返回空 vecPhase 2 由 reviewer Agent 仲裁填充)。
pub fn merge(&self, results: &[ExecutionResult]) -> MergeResult {
let parts: Vec<String> = results
.iter()
.map(|r| {
format!(
"## SubTask: {} (Persona: {})\n\n{}",
r.subtask_id, r.persona_id, r.output
)
})
.collect();
MergeResult {
merged_output: parts.join("\n\n---\n\n"),
conflicts: Vec::new(),
}
}
}
// ---- 单元测试 ---------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
use crate::persona::{PersonaRegistry, PERSONA_CODER};
fn make_coord() -> Coordinator {
Coordinator::new(PersonaRegistry::new())
}
// -- decompose 路径覆盖 --
#[test]
fn decompose_read_then_write_chinese() {
let coord = make_coord();
let result = coord.decompose("modify", "请读取当前代码,然后修改它");
assert_eq!(result.subtasks.len(), 2, "读+写应拆为两个子任务");
assert_eq!(result.subtasks[0].id, "read", "第一个子任务应为读");
assert_eq!(result.subtasks[1].id, "write", "第二个子任务应为写");
assert!(
result.subtasks[1].deps.contains(&"read".to_string()),
"写子任务应依赖读子任务"
);
assert_eq!(result.plan.len(), 2);
}
#[test]
fn decompose_read_then_write_english() {
let coord = make_coord();
let result = coord.decompose("modify", "read the file and then write changes");
assert_eq!(result.subtasks.len(), 2);
assert_eq!(result.subtasks[0].id, "read");
assert_eq!(result.subtasks[1].id, "write");
assert!(result.subtasks[1].deps.contains(&"read".to_string()));
}
#[test]
fn decompose_read_only_chinese() {
let coord = make_coord();
let result = coord.decompose("review", "帮我查看这段代码有什么问题");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "read");
}
#[test]
fn decompose_read_only_search() {
let coord = make_coord();
let result = coord.decompose("search", "搜索项目中的 TODO 注释");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "read");
}
#[test]
fn decompose_write_only_generate() {
let coord = make_coord();
let result = coord.decompose("implement", "请生成一个 Rust 模块");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "write");
}
#[test]
fn decompose_write_only_fix() {
let coord = make_coord();
let result = coord.decompose("fix", "修复这个 bug");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "write");
}
#[test]
fn decompose_fallback_default() {
let coord = make_coord();
let result = coord.decompose("chat", "你好,今天天气怎么样");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "default");
}
#[test]
fn decompose_create_keyword() {
let coord = make_coord();
let result = coord.decompose("create", "创建新的 API 端点");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "write");
}
// -- dispatch --
#[test]
fn dispatch_serial_execution() {
let coord = make_coord();
let plan = Plan::from_tasks(vec![
SubTask::new("read", "read code").with_deps(vec![]),
SubTask::new("write", "write code").with_deps(vec!["read".into()]),
]);
let results = futures::executor::block_on(coord.dispatch(&plan, |task, persona| {
async move {
ExecutionResult {
subtask_id: task.id,
persona_id: persona.id,
output: format!("执行 {} 完毕", task.intent),
success: true,
}
}
}));
assert_eq!(results.len(), 2);
assert!(results[0].success);
assert!(results[1].success);
assert_eq!(results[0].subtask_id, "read");
assert_eq!(results[1].subtask_id, "write");
assert_eq!(results[0].persona_id, PERSONA_CODER);
}
#[test]
fn dispatch_empty_plan() {
let coord = make_coord();
let plan = Plan::new();
let results = futures::executor::block_on(coord.dispatch(&plan, |task, persona| {
async move {
ExecutionResult {
subtask_id: task.id,
persona_id: persona.id,
output: String::new(),
success: true,
}
}
}));
assert!(results.is_empty(), "空 Plan 应产生空结果");
}
#[test]
fn dispatch_persona_assigned_by_intent() {
let coord = make_coord();
let plan = Plan::from_tasks(vec![
SubTask::new("code", "implement feature").with_deps(vec![]),
]);
let results = futures::executor::block_on(coord.dispatch(&plan, |task, persona| {
async move {
ExecutionResult {
subtask_id: task.id,
persona_id: persona.id,
output: String::new(),
success: true,
}
}
}));
// "implement" 不触发特殊人设 → 默认 coder
assert_eq!(results[0].persona_id, PERSONA_CODER);
}
// -- merge --
#[test]
fn merge_single_result() {
let coord = make_coord();
let results = vec![ExecutionResult {
subtask_id: "read".into(),
persona_id: "reviewer".into(),
output: "分析完成,发现 3 个问题".into(),
success: true,
}];
let merged = coord.merge(&results);
assert!(merged.merged_output.contains("分析完成"));
assert!(merged.conflicts.is_empty(), "Phase 1 不应有冲突");
}
#[test]
fn merge_multiple_results() {
let coord = make_coord();
let results = vec![
ExecutionResult {
subtask_id: "read".into(),
persona_id: "reviewer".into(),
output: "代码分析结果".into(),
success: true,
},
ExecutionResult {
subtask_id: "write".into(),
persona_id: "coder".into(),
output: "修改后的代码".into(),
success: true,
},
];
let merged = coord.merge(&results);
assert!(merged.merged_output.contains("代码分析结果"));
assert!(merged.merged_output.contains("修改后的代码"));
assert!(merged.merged_output.contains("SubTask: read"));
assert!(merged.merged_output.contains("SubTask: write"));
assert!(merged.conflicts.is_empty(), "Phase 1 不应有冲突");
}
#[test]
fn merge_empty_results() {
let coord = make_coord();
let merged = coord.merge(&[]);
assert!(merged.merged_output.is_empty(), "空输入应产生空输出");
assert!(merged.conflicts.is_empty());
}
// -- DispatchStrategy --
#[test]
fn dispatch_strategy_name() {
assert_eq!(DispatchStrategy::RuleBased.name(), "rule_based");
assert_eq!(DispatchStrategy::LLMDriven.name(), "llm_driven");
}
// -- Edge cases: 关键词混合边界 --
#[test]
fn decompose_review_triggers_read() {
let coord = make_coord();
// "review" 同时触发读关键词
let result = coord.decompose("review", "review this PR for me");
assert_eq!(result.subtasks.len(), 1);
assert_eq!(result.subtasks[0].id, "read");
}
#[test]
fn decompose_create_and_review_triggers_both() {
let coord = make_coord();
let result = coord.decompose("implement", "请先分析代码结构然后创建新模块");
assert_eq!(
result.subtasks.len(),
2,
"含分析和创建关键词应拆为两个子任务"
);
}
#[test]
fn decompose_plan_not_empty() {
let coord = make_coord();
let result = coord.decompose("chat", "随便聊聊");
assert!(!result.plan.is_empty(), "兜底场景也应有 Plan");
}
#[test]
fn decompose_result_holds_plan_consistent() {
let coord = make_coord();
let result = coord.decompose("modify", "读取配置文件然后更新它");
assert_eq!(
result.subtasks.len(),
result.plan.len(),
"DecompositionResult 的 subtasks 应与 plan 一致"
);
for st in &result.subtasks {
assert!(
result.plan.tasks.iter().any(|t| t.id == st.id),
"plan 应包含所有 subtask: {}",
st.id
);
}
}
}

View File

@@ -5,8 +5,8 @@ pub mod anthropic_compat;
pub mod anthropic_helpers;
pub mod context;
pub mod context_helpers;
// coordinator 模块已删除(B 路线空壳占位,零调用)。
// 多 Agent 协作未来从 planner.rs 的 Plan DAG 执行器重新实现。
// 多 Agent 协作调度中心(Phase 1 规则驱动, Phase 2 LLM 扩展)。
pub mod coordinator;
// 会话意图识别层(纯函数,不接入 agentic loop)。依据 docs/02-架构设计/构想审查/
// 意图识别层论证-2026-06-19.md。提供 recognize / tool_subset_for / suggested_model_tier。
// 待 Phase B+C 完成 + 模型模态管理落地后再接入 agentic loop。