主loop走filter_tool_defs_planned规划(agentic/mod.rs:633接入点就绪) 双层开关可回退:PLANNING_ENABLED(主loop)+PLAN_HINT_ENABLED(plan_hint函数) 任一关闭退扁平旧行为;feature_flags_default_on测试断言同步
1022 lines
37 KiB
Rust
1022 lines
37 KiB
Rust
//! Plan-driven 规划骨架(Phase 0 · 维度 0.8 + 0.7)
|
||
//!
|
||
//! 论证依据:`docs/02-架构设计/单对话并行多轮-设计-2026-06-20.md`(§三 Plan 数据结构 /
|
||
//! §十三 0.7 规划校验 / 0.8 planner.rs)+
|
||
//! `docs/02-架构设计/单对话并行多轮-Phase0落地路线图-2026-06-20.md`(0.8 + 0.7 节)。
|
||
//!
|
||
//! ## 定位
|
||
//! **纯函数模块**(零 IO 零状态:无 `.await` / 无 db / 无 provider / 无全局状态),
|
||
//! 仅数据结构 + 算法。便于单测 + Phase 1 无副作用接入。
|
||
//!
|
||
//! 提供四项能力:
|
||
//! 1. [`SubTask`] / [`Plan`] —— 规划 DAG 数据结构(可并行组 group + 顺序依赖 deps)。
|
||
//! 2. [`Plan::validate`] —— 五类非法形态校验(环/悬空/空/重复 id/超深)。
|
||
//! 3. [`Plan::to_layers`] —— Kahn 拓扑分层算法,输出可并行层(喂 Phase 2/3 execution 调度)。
|
||
//! 4. [`planning_enabled`] —— feature flag 占位(关时单链 ReAct = 旧行为,Phase 1 接入用)。
|
||
//!
|
||
//! ## 设计原则(对齐设计文档 §十三 决策推翻)
|
||
//! - **单一真相源**:Plan 只存 `deps`,**不冗余存 `parallel_groups`**(设计文档明确推翻
|
||
//! 旧「deps + parallel_groups 并存」结构 → 删 parallel_groups,运行时 Kahn 分层派生)。
|
||
//! `group` 字段是规划期的「提示性并行组」(可选 hint),与 deps DAG 并行不矛盾即合法。
|
||
//! - **非法收集非短路**:validate 一次性收集全部违规,便于规划 LLM 一次性修正。
|
||
//! - **Kahn 无副作用**:纯算法,不校验(validate 先行),输入非法时行为见各方法文档。
|
||
//!
|
||
//! ## 不改现有文件
|
||
//! 本文件独立编译;`intent.rs` / `coordinator.rs` 接入留 Phase 1/2(plan_hint.rs 另起)。
|
||
|
||
// ---- feature flag 占位 ------------------------------------------------------
|
||
|
||
/// 规划能力总开关(主 loop 规划开关,门控 filter_tool_defs_planned 接入)。
|
||
///
|
||
/// **当前 `true`** = 主 loop 走 Plan 规划(agentic/mod.rs:633 调 filter_tool_defs_planned)。
|
||
/// Phase 1(plan_hint 接入主 loop)已落地,翻 true 启用。
|
||
/// 注:`PLAN_HINT_ENABLED`(plan_hint 函数 flag,Phase0a 就绪 true)单一定义在 `plan_hint.rs`,
|
||
/// 本模块仅 `PLANNING_ENABLED`(主 loop 规划开关),消除此前双定义矛盾。
|
||
/// 双层开关:即使 PLANNING_ENABLED=true,若 plan_hint 内部 PLAN_HINT_ENABLED 关闭,
|
||
/// filter_tool_defs_planned 仍退扁平(任一关闭退旧行为,可回退)。
|
||
pub const PLANNING_ENABLED: bool = true;
|
||
|
||
/// Plan 层级硬上限(防爆深,防规划 LLM 退化成超长链)。
|
||
///
|
||
/// 超过此值的依赖链判 [`PlanError::TooDeep`]。
|
||
/// 8 层覆盖典型软件任务分解(需求→设计→编码→测试→…),Phase 2 go/no-go 后可调。
|
||
pub const MAX_PLAN_DEPTH: usize = 8;
|
||
|
||
use std::collections::{HashMap, HashSet};
|
||
|
||
// ---- 类型别名 ---------------------------------------------------------------
|
||
|
||
/// 子任务标识。String 而非裸 &str:规划期 LLM 自由生成,不依赖静态工具名表。
|
||
pub type SubTaskId = String;
|
||
|
||
/// 并行组标识(可选 hint)。规划 LLM 标注「这组任务意图上可并行」,
|
||
/// Kahn 分层不依赖它(只看 deps DAG),但保留供 UI/审计展示。
|
||
pub type GroupId = String;
|
||
|
||
// ---- SubTask ----------------------------------------------------------------
|
||
|
||
/// 规划子任务:DAG 中的一个节点。
|
||
///
|
||
/// 字段对齐设计文档 §三 `SubTask`(id / tools / deps)+ 并行组 hint:
|
||
/// - `id`:任务内唯一标识(validate 拒重复)。
|
||
/// - `tool_hint`:可用工具名子集(intent/plan_hint 提示,空 Vec 由 validate 拒为
|
||
/// [`PlanError::SubtaskWithoutTools`] —— 对齐设计文档枚举;执行期不被强制,仅提示 LLM)。
|
||
/// - `deps`:依赖子任务 id 列表(DAG 前驱)。未解析的 id(不存在于 Plan)→
|
||
/// [`PlanError::UnknownDep`](悬空)。
|
||
/// - `group`:可选并行组 hint(设计文档 §三 parallel_groups 的非冗余形态——只作提示,
|
||
/// 不与 deps 并列做真相源)。
|
||
/// - `intent`:自由文本意图描述(规划 LLM 给出的「这步做什么」,便于审计/日志)。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct SubTask {
|
||
pub id: SubTaskId,
|
||
/// 可用工具名子集(hint,非强制)。空 → validate 拒(除非允许无工具子任务)。
|
||
pub tool_hint: Vec<String>,
|
||
/// 依赖前驱 id。DAG 边:dep → self。
|
||
pub deps: Vec<SubTaskId>,
|
||
/// 并行组 hint(可选)。同 group 的任务意图上可并行,但 Kahn 不看它。
|
||
pub group: Option<GroupId>,
|
||
/// 意图自由文本(规划 LLM 描述,审计/日志用)。
|
||
pub intent: String,
|
||
}
|
||
|
||
impl SubTask {
|
||
/// 构造便捷器:最小子任务(id + intent,无工具无依赖)。
|
||
#[allow(dead_code)]
|
||
pub fn new(id: impl Into<SubTaskId>, intent: impl Into<String>) -> Self {
|
||
Self {
|
||
id: id.into(),
|
||
tool_hint: Vec::new(),
|
||
deps: Vec::new(),
|
||
group: None,
|
||
intent: intent.into(),
|
||
}
|
||
}
|
||
|
||
/// 链式设置 tool_hint。
|
||
#[allow(dead_code)]
|
||
pub fn with_tools(mut self, tools: Vec<String>) -> Self {
|
||
self.tool_hint = tools;
|
||
self
|
||
}
|
||
|
||
/// 链式设置 deps。
|
||
#[allow(dead_code)]
|
||
pub fn with_deps(mut self, deps: Vec<SubTaskId>) -> Self {
|
||
self.deps = deps;
|
||
self
|
||
}
|
||
|
||
/// 链式设置 group。
|
||
#[allow(dead_code)]
|
||
pub fn with_group(mut self, group: impl Into<GroupId>) -> Self {
|
||
self.group = Some(group.into());
|
||
self
|
||
}
|
||
}
|
||
|
||
// ---- Plan -------------------------------------------------------------------
|
||
|
||
/// 规划 DAG:子任务集合。
|
||
///
|
||
/// **不冗余存 `parallel_groups`**(设计文档 §十三 决策推翻:删 parallel_groups,
|
||
/// 只存 deps,运行时 [`Plan::to_layers`] Kahn 分层派生——单一真相源)。
|
||
///
|
||
/// 合法性由 [`Plan::validate`] 校验;非法 Plan 调 [`Plan::to_layers`] 行为见其文档。
|
||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||
pub struct Plan {
|
||
pub tasks: Vec<SubTask>,
|
||
}
|
||
|
||
impl Plan {
|
||
/// 空构造。
|
||
#[allow(dead_code)]
|
||
pub fn new() -> Self {
|
||
Self { tasks: Vec::new() }
|
||
}
|
||
|
||
/// 从 tasks 构造。
|
||
#[allow(dead_code)]
|
||
pub fn from_tasks(tasks: Vec<SubTask>) -> Self {
|
||
Self { tasks }
|
||
}
|
||
|
||
/// 任务数。
|
||
#[allow(dead_code)]
|
||
pub fn len(&self) -> usize {
|
||
self.tasks.len()
|
||
}
|
||
|
||
/// 是否空(空 Plan 判非法:[`PlanError::Empty`])。
|
||
#[allow(dead_code)]
|
||
pub fn is_empty(&self) -> bool {
|
||
self.tasks.is_empty()
|
||
}
|
||
|
||
/// 校验 Plan 合法性(五类非法 + 设计文档四枚举)。
|
||
///
|
||
/// **收集非短路**:一次性返回所有违规(`Vec<PlanError>`),便于规划 LLM 一次修正。
|
||
/// 合法 → `Ok(())`。
|
||
///
|
||
/// 五类非法(对齐路线图 0.7)+ 设计文档枚举映射:
|
||
/// | 类别 | 枚举 | 判据 |
|
||
/// |---|---|---|
|
||
/// | 空 | [`PlanError::Empty`] | tasks 为空 |
|
||
/// | 重复 id | [`PlanError::DuplicateId`] | 同 id 出现 ≥2 次 |
|
||
/// | 悬空 dep | [`PlanError::UnknownDep`] | deps 引用不存在的 id(设计文档枚举) |
|
||
/// | 自环 dep | [`PlanError::UnknownDep`] 归类 | self-dep(A 依赖 A,视为悬空+环) |
|
||
/// | 环 | [`PlanError::CycleDetected`] | deps 形成环(设计文档枚举) |
|
||
/// | 无工具 | [`PlanError::SubtaskWithoutTools`] | tool_hint 空(设计文档枚举,可由 `require_tools=false` 关) |
|
||
/// | 超深 | [`PlanError::TooDeep`] | 最长依赖链深度 > [`MAX_PLAN_DEPTH`] |
|
||
///
|
||
/// 注:[`PlanError::OrphanSubtask`](设计文档枚举)当前归一不报——
|
||
/// 无 deps 且无后继的孤立任务在 DAG 中合法(入口任务),设计文档「Orphan」语义
|
||
/// 偏「脱网」,Phase 2 go/no-go 后再定是否拒(保留枚举占位,符合预留保留原则)。
|
||
pub fn validate(&self) -> Result<(), Vec<PlanError>> {
|
||
let errs = self.validate_with(ValidateOptions::default());
|
||
if errs.is_empty() {
|
||
Ok(())
|
||
} else {
|
||
Err(errs)
|
||
}
|
||
}
|
||
|
||
/// 带选项的校验(`require_tools` 开关:plan_hint 场景可允许无工具子任务)。
|
||
pub fn validate_with(&self, opts: ValidateOptions) -> Vec<PlanError> {
|
||
let mut errs: Vec<PlanError> = Vec::new();
|
||
|
||
// 1) 空
|
||
if self.tasks.is_empty() {
|
||
errs.push(PlanError::Empty);
|
||
return errs; // 空 Plan 后续检查无意义
|
||
}
|
||
|
||
// 构建 id 索引(同时检测重复 id)
|
||
let mut id_index: HashMap<&SubTaskId, usize> = HashMap::new();
|
||
for (i, t) in self.tasks.iter().enumerate() {
|
||
if t.id.is_empty() {
|
||
errs.push(PlanError::EmptyId { index: i });
|
||
continue;
|
||
}
|
||
if id_index.contains_key(&t.id) {
|
||
errs.push(PlanError::DuplicateId {
|
||
id: t.id.clone(),
|
||
});
|
||
} else {
|
||
id_index.insert(&t.id, i);
|
||
}
|
||
}
|
||
|
||
// 2) 无工具(可选)
|
||
if opts.require_tools {
|
||
for t in &self.tasks {
|
||
if t.tool_hint.is_empty() {
|
||
errs.push(PlanError::SubtaskWithoutTools { id: t.id.clone() });
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3) 悬空 dep / 自环(自环视为 UnknownDep)
|
||
for t in &self.tasks {
|
||
for dep in &t.deps {
|
||
if dep == &t.id {
|
||
errs.push(PlanError::SelfDep { id: t.id.clone() });
|
||
} else if !id_index.contains_key(dep) {
|
||
errs.push(PlanError::UnknownDep {
|
||
from: t.id.clone(),
|
||
unknown: dep.clone(),
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// 4) 环 + 超深(先验无悬空才能可靠跑环检测:悬空已报,环检测只看可解析边)
|
||
if !errs.iter().any(|e| {
|
||
matches!(
|
||
e,
|
||
PlanError::UnknownDep { .. } | PlanError::SelfDep { .. }
|
||
)
|
||
}) {
|
||
if let Some(cycle) = self.detect_cycle() {
|
||
errs.push(PlanError::CycleDetected { cycle });
|
||
}
|
||
// 超深(最长链 = Kahn 层数);环存在时层数无意义,跳过
|
||
if !errs.iter().any(|e| matches!(e, PlanError::CycleDetected { .. })) {
|
||
if let Some(depth) = self.longest_chain_depth() {
|
||
if depth > opts.max_depth {
|
||
errs.push(PlanError::TooDeep {
|
||
depth,
|
||
max: opts.max_depth,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
errs
|
||
}
|
||
|
||
/// Kahn 拓扑分层:输出可并行层(同层任务无相互依赖,可并行执行)。
|
||
///
|
||
/// **算法**:入度 0 入当前层 → 移除(其后继入度 -1)→ 下层。纯算法无副作用。
|
||
///
|
||
/// **非法 Plan 行为**(调用方应先 validate):
|
||
/// - 环:返回 `Err(CycleDetected)`(Kahn 无法消除环边,剩余节点 = 环成员)。
|
||
/// - 悬空 dep:忽略悬空边(视作无该前驱),照常分层。
|
||
/// - 空 Plan:返回 `Ok(vec![])`。
|
||
///
|
||
/// 返回层的顺序 = 执行顺序(第 0 层先执行,同层内可并行)。
|
||
/// 每层 `Vec<SubTask>` 为克隆(Kahn 消费入度,不消费原 Plan)。
|
||
pub fn to_layers(&self) -> Result<Vec<Vec<SubTask>>, CycleError> {
|
||
if self.tasks.is_empty() {
|
||
return Ok(Vec::new());
|
||
}
|
||
|
||
let id_to_task: HashMap<&SubTaskId, &SubTask> =
|
||
self.tasks.iter().map(|t| (&t.id, t)).collect();
|
||
|
||
// 入度:仅计可解析的 deps(悬空 dep 不计入,避免卡死)
|
||
let mut indegree: HashMap<&SubTaskId, usize> = HashMap::new();
|
||
for t in &self.tasks {
|
||
indegree.entry(&t.id).or_insert(0);
|
||
}
|
||
for t in &self.tasks {
|
||
for dep in &t.deps {
|
||
// dep 存在且非自环才计
|
||
if dep != &t.id && id_to_task.contains_key(dep) {
|
||
*indegree.entry(&t.id).or_insert(0) += 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
let mut layers: Vec<Vec<SubTask>> = Vec::new();
|
||
let mut placed: HashSet<&SubTaskId> = HashSet::new();
|
||
|
||
loop {
|
||
// 当前层:入度 0 且未放置(按原 tasks 顺序稳定)
|
||
let current: Vec<&SubTask> = self
|
||
.tasks
|
||
.iter()
|
||
.filter(|t| !placed.contains(&t.id))
|
||
.filter(|t| *indegree.get(&t.id).unwrap_or(&0) == 0)
|
||
.collect();
|
||
|
||
if current.is_empty() {
|
||
break;
|
||
}
|
||
|
||
// 放置并减后继入度
|
||
for t in ¤t {
|
||
placed.insert(&t.id);
|
||
}
|
||
// 后继入度 -1:遍历所有任务,若其 deps 含当前层任一 id,则 -1
|
||
let current_ids: HashSet<&SubTaskId> =
|
||
current.iter().map(|t| &t.id).collect();
|
||
for t in &self.tasks {
|
||
if placed.contains(&t.id) {
|
||
continue;
|
||
}
|
||
for dep in &t.deps {
|
||
if current_ids.contains(dep) {
|
||
if let Some(v) = indegree.get_mut(&t.id) {
|
||
*v = v.saturating_sub(1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
layers.push(current.iter().map(|t| (*t).clone()).collect());
|
||
}
|
||
|
||
// 剩余未放置 = 环成员
|
||
if placed.len() < self.tasks.len() {
|
||
let cycle: Vec<SubTaskId> = self
|
||
.tasks
|
||
.iter()
|
||
.filter(|t| !placed.contains(&t.id))
|
||
.map(|t| t.id.clone())
|
||
.collect();
|
||
return Err(CycleError { cycle });
|
||
}
|
||
|
||
Ok(layers)
|
||
}
|
||
|
||
// ---- 内部算法辅助 ----
|
||
|
||
/// 环检测(DFS 三色标记法)。假设无悬空 dep(调用方已过滤)。
|
||
fn detect_cycle(&self) -> Option<Vec<SubTaskId>> {
|
||
let id_to_task: HashMap<&SubTaskId, &SubTask> =
|
||
self.tasks.iter().map(|t| (&t.id, t)).collect();
|
||
|
||
// 颜色:0=白(未访问) 1=灰(栈中) 2=黑(完成)
|
||
let mut color: HashMap<&SubTaskId, u8> = HashMap::new();
|
||
for t in &self.tasks {
|
||
color.insert(&t.id, 0);
|
||
}
|
||
|
||
// 记录路径用于回溯环
|
||
let mut stack: Vec<&SubTaskId> = Vec::new();
|
||
// finished 节点(防重复 DFS)
|
||
let mut finished: HashSet<&SubTaskId> = HashSet::new();
|
||
|
||
for t in &self.tasks {
|
||
if *color.get(&t.id).unwrap() != 0 {
|
||
continue;
|
||
}
|
||
if let Some(cycle) = dfs_cycle(&t.id, &id_to_task, &mut color, &mut stack, &mut finished)
|
||
{
|
||
return Some(cycle);
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
/// 最长依赖链深度(= Kahn 层数)。无环前提。返回 None 表示空 Plan。
|
||
fn longest_chain_depth(&self) -> Option<usize> {
|
||
match self.to_layers() {
|
||
Ok(layers) => {
|
||
if layers.is_empty() {
|
||
None
|
||
} else {
|
||
Some(layers.len())
|
||
}
|
||
}
|
||
// 环存在:返回 None(depth 无意义,由 CycleDetected 单独报)
|
||
Err(_) => None,
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- validate 选项 ----------------------------------------------------------
|
||
|
||
/// validate 行为选项。
|
||
#[derive(Debug, Clone, Copy)]
|
||
pub struct ValidateOptions {
|
||
/// 是否要求每个子任务有 tool_hint。plan_hint 场景可关(允许纯编排节点)。
|
||
pub require_tools: bool,
|
||
/// 依赖链深度上限。超 → TooDeep。
|
||
pub max_depth: usize,
|
||
}
|
||
|
||
impl Default for ValidateOptions {
|
||
fn default() -> Self {
|
||
Self {
|
||
require_tools: true,
|
||
max_depth: MAX_PLAN_DEPTH,
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- 错误类型 ---------------------------------------------------------------
|
||
|
||
/// Plan 校验错误(设计文档 §十三 0.7 枚举 + 路线图 0.7 五类非法补充)。
|
||
///
|
||
/// 对齐设计文档枚举:`CycleDetected / UnknownDep / OrphanSubtask / SubtaskWithoutTools`,
|
||
/// 并补充路线图要求的五类非法(Empty/DuplicateId/TooDeep)+ 实现细化(SelfDep/EmptyId)。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub enum PlanError {
|
||
/// 空 Plan(路线图五类非法之一)。
|
||
Empty,
|
||
/// 子任务 id 为空字符串。
|
||
EmptyId { index: usize },
|
||
/// 重复 id(路线图五类非法之一)。
|
||
DuplicateId { id: SubTaskId },
|
||
/// 悬空依赖:deps 引用不存在的 id(设计文档枚举 UnknownDep)。
|
||
UnknownDep { from: SubTaskId, unknown: SubTaskId },
|
||
/// 自环依赖:A 依赖 A(归为悬空的特例,单独报便于定位)。
|
||
SelfDep { id: SubTaskId },
|
||
/// 环(设计文档枚举 CycleDetected)。`cycle` = 环上节点 id 序列。
|
||
CycleDetected { cycle: Vec<SubTaskId> },
|
||
/// 子任务无工具(设计文档枚举 SubtaskWithoutTools)。
|
||
SubtaskWithoutTools { id: SubTaskId },
|
||
/// 依赖链超深(路线图五类非法之一)。
|
||
TooDeep { depth: usize, max: usize },
|
||
/// 孤立子任务(设计文档枚举 OrphanSubtask 占位,当前 validate 不报,保留供 Phase 2 决策)。
|
||
#[allow(dead_code)]
|
||
OrphanSubtask { id: SubTaskId },
|
||
}
|
||
|
||
impl std::fmt::Display for PlanError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
match self {
|
||
PlanError::Empty => write!(f, "Plan 为空"),
|
||
PlanError::EmptyId { index } => write!(f, "第 {} 个子任务 id 为空", index),
|
||
PlanError::DuplicateId { id } => write!(f, "重复子任务 id: {}", id),
|
||
PlanError::UnknownDep { from, unknown } => {
|
||
write!(f, "子任务 {} 依赖不存在的 id: {}", from, unknown)
|
||
}
|
||
PlanError::SelfDep { id } => write!(f, "子任务 {} 自环依赖", id),
|
||
PlanError::CycleDetected { cycle } => {
|
||
write!(f, "检测到环: {}", cycle.join(" -> "))
|
||
}
|
||
PlanError::SubtaskWithoutTools { id } => write!(f, "子任务 {} 无 tool_hint", id),
|
||
PlanError::TooDeep { depth, max } => {
|
||
write!(f, "依赖链深度 {} 超上限 {}", depth, max)
|
||
}
|
||
PlanError::OrphanSubtask { id } => write!(f, "孤立子任务: {}", id),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for PlanError {}
|
||
|
||
/// Kahn 分层时检测到环([`Plan::to_layers`] 返回)。
|
||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||
pub struct CycleError {
|
||
/// 环上(或因环无法放置的)节点 id 列表。
|
||
pub cycle: Vec<SubTaskId>,
|
||
}
|
||
|
||
impl std::fmt::Display for CycleError {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
write!(f, "Kahn 分层遇到环: {}", self.cycle.join(" -> "))
|
||
}
|
||
}
|
||
|
||
impl std::error::Error for CycleError {}
|
||
|
||
// ---- DFS 环检测辅助(模块级,借用 checker 规避 self-borrow) ----------------
|
||
|
||
fn dfs_cycle<'a>(
|
||
start: &'a SubTaskId,
|
||
id_to_task: &HashMap<&'a SubTaskId, &'a SubTask>,
|
||
color: &mut HashMap<&'a SubTaskId, u8>,
|
||
stack: &mut Vec<&'a SubTaskId>,
|
||
finished: &mut HashSet<&'a SubTaskId>,
|
||
) -> Option<Vec<SubTaskId>> {
|
||
// 显式栈 DFS,避免递归(深度大时不爆栈)
|
||
// stack 元素:(节点, 其 deps 的迭代位置)——此处简化:每节点完整遍历 deps
|
||
// 改用工作栈 + 回溯标记
|
||
struct Frame<'b> {
|
||
node: &'b SubTaskId,
|
||
dep_idx: usize,
|
||
}
|
||
let mut work: Vec<Frame<'a>> = vec![Frame { node: start, dep_idx: 0 }];
|
||
color.insert(start, 1); // 灰
|
||
stack.push(start);
|
||
|
||
while let Some(frame) = work.last_mut() {
|
||
let node = frame.node;
|
||
let task = match id_to_task.get(node) {
|
||
Some(t) => *t,
|
||
None => {
|
||
// 不应发生(调用方已过滤悬空),兜底回溯
|
||
work.pop();
|
||
color.insert(node, 2);
|
||
stack.pop();
|
||
continue;
|
||
}
|
||
};
|
||
let deps = &task.deps;
|
||
|
||
if frame.dep_idx < deps.len() {
|
||
let next_id = &deps[frame.dep_idx];
|
||
frame.dep_idx += 1;
|
||
// 自环跳过(SelfDep 已单独报)
|
||
if next_id == node {
|
||
continue;
|
||
}
|
||
let next = match id_to_task.get(next_id) {
|
||
Some(t) => *t,
|
||
None => continue, // 悬空跳过
|
||
};
|
||
let next_color = *color.get(&next.id).unwrap_or(&0);
|
||
match next_color {
|
||
1 => {
|
||
// 命中灰节点 → 环。回溯 stack 找环段。
|
||
let mut cycle: Vec<SubTaskId> = Vec::new();
|
||
let mut found = false;
|
||
for s in stack.iter() {
|
||
if *s == &next.id {
|
||
found = true;
|
||
}
|
||
if found {
|
||
cycle.push((*s).clone());
|
||
}
|
||
}
|
||
cycle.push(next.id.clone()); // 闭合
|
||
return Some(cycle);
|
||
}
|
||
0 => {
|
||
color.insert(&next.id, 1);
|
||
stack.push(&next.id);
|
||
work.push(Frame {
|
||
node: &next.id,
|
||
dep_idx: 0,
|
||
});
|
||
}
|
||
_ => {} // 黑:已 finish,跳过
|
||
}
|
||
} else {
|
||
// 当前节点 deps 耗尽 → 完成
|
||
let done_node = frame.node;
|
||
color.insert(done_node, 2);
|
||
finished.insert(done_node);
|
||
work.pop();
|
||
stack.pop();
|
||
}
|
||
}
|
||
None
|
||
}
|
||
|
||
// ---- 单测(对齐 intent.rs 52 测密度)---------------------------------------
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
// 辅助:构造一个带工具的最小子任务
|
||
fn t(id: &str) -> SubTask {
|
||
SubTask {
|
||
id: id.to_string(),
|
||
tool_hint: vec!["read_file".to_string()],
|
||
deps: Vec::new(),
|
||
group: None,
|
||
intent: format!("task-{}", id),
|
||
}
|
||
}
|
||
|
||
fn t_dep(id: &str, deps: &[&str]) -> SubTask {
|
||
let mut s = t(id);
|
||
s.deps = deps.iter().map(|s| s.to_string()).collect();
|
||
s
|
||
}
|
||
|
||
// 变体:deps 用 String(动态 id 场景,如循环构造的 n0/n1/...)
|
||
fn t_dep_s<I: Into<String>>(id: &str, deps: I) -> SubTask {
|
||
let mut s = t(id);
|
||
s.deps = vec![deps.into()];
|
||
s
|
||
}
|
||
|
||
// ===== 0.8 Plan/SubTask 结构 =====
|
||
|
||
#[test]
|
||
fn subtask_builder_chains() {
|
||
let s = SubTask::new("a", "do a")
|
||
.with_tools(vec!["read_file".into()])
|
||
.with_deps(vec!["b".into()])
|
||
.with_group("g1");
|
||
assert_eq!(s.id, "a");
|
||
assert_eq!(s.intent, "do a");
|
||
assert_eq!(s.tool_hint, vec!["read_file".to_string()]);
|
||
assert_eq!(s.deps, vec!["b".to_string()]);
|
||
assert_eq!(s.group.as_deref(), Some("g1"));
|
||
}
|
||
|
||
#[test]
|
||
fn subtask_default_builder_minimal() {
|
||
let s = SubTask::new("x", "");
|
||
assert!(s.tool_hint.is_empty());
|
||
assert!(s.deps.is_empty());
|
||
assert!(s.group.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn plan_new_empty() {
|
||
let p = Plan::new();
|
||
assert!(p.is_empty());
|
||
assert_eq!(p.len(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn plan_from_tasks_len() {
|
||
let p = Plan::from_tasks(vec![t("a"), t("b")]);
|
||
assert_eq!(p.len(), 2);
|
||
assert!(!p.is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn plan_clone_eq() {
|
||
let p = Plan::from_tasks(vec![t("a")]);
|
||
let q = p.clone();
|
||
assert_eq!(p, q);
|
||
}
|
||
|
||
#[test]
|
||
fn feature_flags_default_on() {
|
||
// PLANNING_ENABLED 当前开(主 loop 走 filter_tool_defs_planned 规划),Phase 1 已落地。
|
||
// PLAN_HINT_ENABLED(plan_hint 函数 flag)在 plan_hint.rs 单一定义,此处不再断言。
|
||
assert!(PLANNING_ENABLED);
|
||
}
|
||
|
||
#[test]
|
||
fn max_plan_depth_is_reasonable() {
|
||
// 8 层覆盖典型软件任务分解
|
||
assert!(MAX_PLAN_DEPTH >= 4);
|
||
assert!(MAX_PLAN_DEPTH <= 16);
|
||
}
|
||
|
||
// ===== 0.7 validate: 合法 Plan 通过 =====
|
||
|
||
#[test]
|
||
fn validate_single_task_ok() {
|
||
let p = Plan::from_tasks(vec![t("a")]);
|
||
assert!(p.validate().is_ok(), "单任务无依赖应合法");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_linear_chain_ok() {
|
||
// a -> b -> c
|
||
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["a"]), t_dep("c", &["b"])]);
|
||
assert!(p.validate().is_ok(), "线性链应合法");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_diamond_ok() {
|
||
// a -> b,c -> d (菱形)
|
||
let p = Plan::from_tasks(vec![
|
||
t("a"),
|
||
t_dep("b", &["a"]),
|
||
t_dep("c", &["a"]),
|
||
t_dep("d", &["b", "c"]),
|
||
]);
|
||
assert!(p.validate().is_ok(), "菱形 DAG 应合法");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_parallel_group_ok() {
|
||
// 同 group 三个无依赖任务
|
||
let mut a = t("a");
|
||
a.group = Some("g1".into());
|
||
let mut b = t("b");
|
||
b.group = Some("g1".into());
|
||
let mut c = t("c");
|
||
c.group = Some("g1".into());
|
||
let p = Plan::from_tasks(vec![a, b, c]);
|
||
assert!(p.validate().is_ok(), "并行组无依赖应合法");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_require_tools_off_allows_empty_tools() {
|
||
let mut s = t("a");
|
||
s.tool_hint.clear();
|
||
let p = Plan::from_tasks(vec![s]);
|
||
let errs = p.validate_with(ValidateOptions {
|
||
require_tools: false,
|
||
max_depth: MAX_PLAN_DEPTH,
|
||
});
|
||
assert!(errs.is_empty(), "关 require_tools 后无工具应放过");
|
||
}
|
||
|
||
// ===== 0.7 validate: 五类非法各拒 =====
|
||
|
||
#[test]
|
||
fn validate_empty_rejected() {
|
||
let p = Plan::new();
|
||
let r = p.validate();
|
||
assert_eq!(r, Err(vec![PlanError::Empty]));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_duplicate_id_rejected() {
|
||
let p = Plan::from_tasks(vec![t("a"), t("a")]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.contains(&PlanError::DuplicateId { id: "a".into() }));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_dangling_dep_rejected() {
|
||
// b 依赖不存在的 x
|
||
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["x"])]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.contains(&PlanError::UnknownDep {
|
||
from: "b".into(),
|
||
unknown: "x".into()
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_self_dep_rejected() {
|
||
let p = Plan::from_tasks(vec![t_dep("a", &["a"])]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.contains(&PlanError::SelfDep { id: "a".into() }));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_cycle_two_node_rejected() {
|
||
// a <-> b
|
||
let p = Plan::from_tasks(vec![t_dep("a", &["b"]), t_dep("b", &["a"])]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(
|
||
errs.iter().any(|e| matches!(e, PlanError::CycleDetected { .. })),
|
||
"双向环应拒为 CycleDetected, errs={:?}", errs
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn validate_cycle_three_node_rejected() {
|
||
// a -> b -> c -> a
|
||
let p = Plan::from_tasks(vec![
|
||
t_dep("a", &["c"]),
|
||
t_dep("b", &["a"]),
|
||
t_dep("c", &["b"]),
|
||
]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs
|
||
.iter()
|
||
.any(|e| matches!(e, PlanError::CycleDetected { .. })));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_cycle_self_loop_is_selfdep_not_cycle() {
|
||
// 自环优先报 SelfDep,不进入环检测(validate 早退条件)
|
||
let p = Plan::from_tasks(vec![t_dep("a", &["a"]), t("b")]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.contains(&PlanError::SelfDep { id: "a".into() }));
|
||
// SelfDep 触发环检测跳过(不误报)
|
||
assert!(!errs
|
||
.iter()
|
||
.any(|e| matches!(e, PlanError::CycleDetected { .. })));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_subtask_without_tools_rejected() {
|
||
let mut s = t("a");
|
||
s.tool_hint.clear();
|
||
let p = Plan::from_tasks(vec![s]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.contains(&PlanError::SubtaskWithoutTools { id: "a".into() }));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_too_deep_rejected() {
|
||
// 9 节点线性链(深度 9 > MAX_PLAN_DEPTH=8)
|
||
let mut tasks: Vec<SubTask> = vec![t("n0")];
|
||
for i in 1..9 {
|
||
tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1)));
|
||
}
|
||
let p = Plan::from_tasks(tasks);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(
|
||
errs.iter().any(|e| matches!(e, PlanError::TooDeep { depth: 9, max: 8 })),
|
||
"9 层应判 TooDeep, errs={:?}", errs
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn validate_depth_exactly_at_limit_ok() {
|
||
// 8 节点线性链(深度 8 == MAX_PLAN_DEPTH,不超,应合法)
|
||
let mut tasks: Vec<SubTask> = vec![t("n0")];
|
||
for i in 1..8 {
|
||
tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1)));
|
||
}
|
||
let p = Plan::from_tasks(tasks);
|
||
assert!(p.validate().is_ok(), "8 层(==上限)应合法");
|
||
}
|
||
|
||
#[test]
|
||
fn validate_empty_id_rejected() {
|
||
let mut s = t("a");
|
||
s.id = String::new();
|
||
let p = Plan::from_tasks(vec![s]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs
|
||
.iter()
|
||
.any(|e| matches!(e, PlanError::EmptyId { index: 0 })));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_collects_multiple_errors() {
|
||
// 同时:重复 id + 悬空 dep + 无工具
|
||
let mut no_tools = t("a");
|
||
no_tools.tool_hint.clear();
|
||
let p = Plan::from_tasks(vec![
|
||
no_tools, // 无工具
|
||
t("a"), // 重复 id(同 "a")
|
||
t_dep("b", &["x"]), // 悬空 x
|
||
]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.len() >= 3, "应一次收集 ≥3 类违规, got {} 条: {:?}", errs.len(), errs);
|
||
assert!(errs.contains(&PlanError::DuplicateId { id: "a".into() }));
|
||
assert!(errs.contains(&PlanError::SubtaskWithoutTools { id: "a".into() }));
|
||
assert!(errs.contains(&PlanError::UnknownDep {
|
||
from: "b".into(),
|
||
unknown: "x".into()
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn validate_dangling_does_not_mask_cycle() {
|
||
// 悬空 + 环共存:悬空报,环因早退不报(设计:悬空优先,环检测跳过)
|
||
let p = Plan::from_tasks(vec![
|
||
t_dep("a", &["b"]),
|
||
t_dep("b", &["a"]),
|
||
t_dep("c", &["x"]), // 悬空
|
||
]);
|
||
let errs = p.validate().unwrap_err();
|
||
assert!(errs.contains(&PlanError::UnknownDep {
|
||
from: "c".into(),
|
||
unknown: "x".into()
|
||
}));
|
||
}
|
||
|
||
#[test]
|
||
fn plan_error_display_nonempty() {
|
||
let e = PlanError::CycleDetected {
|
||
cycle: vec!["a".into(), "b".into(), "a".into()],
|
||
};
|
||
let s = format!("{}", e);
|
||
assert!(s.contains("环"));
|
||
assert!(s.contains("a -> b"));
|
||
}
|
||
|
||
#[test]
|
||
fn cycle_error_display() {
|
||
let e = CycleError {
|
||
cycle: vec!["a".into()],
|
||
};
|
||
assert!(format!("{}", e).contains("环"));
|
||
}
|
||
|
||
// ===== 0.7 Kahn to_layers =====
|
||
|
||
#[test]
|
||
fn layers_empty_plan() {
|
||
let p = Plan::new();
|
||
assert_eq!(p.to_layers().unwrap(), Vec::<Vec<SubTask>>::new());
|
||
}
|
||
|
||
#[test]
|
||
fn layers_single_task_one_layer() {
|
||
let p = Plan::from_tasks(vec![t("a")]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 1);
|
||
assert_eq!(layers[0].len(), 1);
|
||
assert_eq!(layers[0][0].id, "a");
|
||
}
|
||
|
||
#[test]
|
||
fn layers_linear_chain_n_layers() {
|
||
// a -> b -> c → 3 层各 1 节点
|
||
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["a"]), t_dep("c", &["b"])]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 3);
|
||
assert_eq!(layers[0][0].id, "a");
|
||
assert_eq!(layers[1][0].id, "b");
|
||
assert_eq!(layers[2][0].id, "c");
|
||
}
|
||
|
||
#[test]
|
||
fn layers_parallel_group_one_layer() {
|
||
// a,b,c 无依赖 → 同一层(可并行)
|
||
let p = Plan::from_tasks(vec![t("a"), t("b"), t("c")]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 1, "无依赖应单层: {:?}", layers);
|
||
let ids: Vec<&str> = layers[0].iter().map(|t| t.id.as_str()).collect();
|
||
assert_eq!(ids, vec!["a", "b", "c"]); // 保持原顺序稳定
|
||
}
|
||
|
||
#[test]
|
||
fn layers_diamond_two_then_one() {
|
||
// a -> b,c -> d
|
||
// 层 0: [a], 层 1: [b, c](并行), 层 2: [d]
|
||
let p = Plan::from_tasks(vec![
|
||
t("a"),
|
||
t_dep("b", &["a"]),
|
||
t_dep("c", &["a"]),
|
||
t_dep("d", &["b", "c"]),
|
||
]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 3);
|
||
assert_eq!(layer_ids(&layers, 0), vec!["a"]);
|
||
assert_eq!(layer_ids(&layers, 1), vec!["b", "c"]);
|
||
assert_eq!(layer_ids(&layers, 2), vec!["d"]);
|
||
}
|
||
|
||
#[test]
|
||
fn layers_preserves_input_order_within_layer() {
|
||
// 多个无依赖节点,同层应保持 Plan 内顺序(稳定)
|
||
let p = Plan::from_tasks(vec![t("z"), t("y"), t("x")]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layer_ids(&layers, 0), vec!["z", "y", "x"]);
|
||
}
|
||
|
||
#[test]
|
||
fn layers_cycle_returns_err() {
|
||
// a <-> b
|
||
let p = Plan::from_tasks(vec![t_dep("a", &["b"]), t_dep("b", &["a"])]);
|
||
let err = p.to_layers().unwrap_err();
|
||
// 环成员 a,b 均无法放置
|
||
let cycle_set: HashSet<&str> =
|
||
err.cycle.iter().map(|s| s.as_str()).collect();
|
||
assert!(cycle_set.contains("a"));
|
||
assert!(cycle_set.contains("b"));
|
||
}
|
||
|
||
#[test]
|
||
fn layers_self_dep_treated_as_no_indeg() {
|
||
// 自环:A 依赖 A。Kahn 视为无该前驱(不计入度),A 仍可放置
|
||
let p = Plan::from_tasks(vec![t_dep("a", &["a"]), t_dep("b", &["a"])]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 2);
|
||
assert_eq!(layer_ids(&layers, 0), vec!["a"]);
|
||
assert_eq!(layer_ids(&layers, 1), vec!["b"]);
|
||
}
|
||
|
||
#[test]
|
||
fn layers_dangling_dep_ignored() {
|
||
// b 依赖不存在的 x —— Kahn 忽略悬空边,b 视为入度 0 入首层
|
||
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["x"])]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 1, "悬空边忽略后 a,b 同层: {:?}", layers);
|
||
let ids: HashSet<&str> =
|
||
layers[0].iter().map(|t| t.id.as_str()).collect();
|
||
assert!(ids.contains("a"));
|
||
assert!(ids.contains("b"));
|
||
}
|
||
|
||
#[test]
|
||
fn layers_deep_chain_count_matches_validate_depth() {
|
||
// 5 节点链 → 5 层 = longest_chain_depth
|
||
let mut tasks: Vec<SubTask> = vec![t("n0")];
|
||
for i in 1..5 {
|
||
tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1)));
|
||
}
|
||
let p = Plan::from_tasks(tasks);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 5);
|
||
}
|
||
|
||
#[test]
|
||
fn layers_complex_dag() {
|
||
// 复杂 DAG:
|
||
// a b (层0)
|
||
// \ / \
|
||
// c d (层1: c 依赖 a,b; d 依赖 b)
|
||
// | |
|
||
// e f (层2: e 依赖 c; f 依赖 d)
|
||
// \ /
|
||
// g (层3: g 依赖 e,f)
|
||
let p = Plan::from_tasks(vec![
|
||
t("a"),
|
||
t("b"),
|
||
t_dep("c", &["a", "b"]),
|
||
t_dep("d", &["b"]),
|
||
t_dep("e", &["c"]),
|
||
t_dep("f", &["d"]),
|
||
t_dep("g", &["e", "f"]),
|
||
]);
|
||
let layers = p.to_layers().unwrap();
|
||
assert_eq!(layers.len(), 4);
|
||
assert_eq!(layer_ids(&layers, 0), vec!["a", "b"]);
|
||
assert_eq!(layer_ids(&layers, 1), vec!["c", "d"]);
|
||
assert_eq!(layer_ids(&layers, 2), vec!["e", "f"]);
|
||
assert_eq!(layer_ids(&layers, 3), vec!["g"]);
|
||
}
|
||
|
||
#[test]
|
||
fn layers_returns_clones_not_references() {
|
||
// 返回 Vec<SubTask>(owned),修改不影响原 Plan
|
||
let p = Plan::from_tasks(vec![t("a")]);
|
||
let mut layers = p.to_layers().unwrap();
|
||
layers[0][0].intent = "modified".into();
|
||
assert_eq!(p.tasks[0].intent, "task-a", "原 Plan 不应被改");
|
||
}
|
||
|
||
// 辅助
|
||
fn layer_ids(layers: &[Vec<SubTask>], i: usize) -> Vec<&str> {
|
||
layers[i].iter().map(|t| t.id.as_str()).collect()
|
||
}
|
||
}
|