docs(roadmap): 更新 Phase 4b 状态为已完成

This commit is contained in:
徐涛
2026-06-11 21:57:10 +08:00
parent 2b189880a9
commit 4de7db0b2c
5 changed files with 162 additions and 11 deletions
+2 -1
View File
@@ -22,4 +22,5 @@ pub use builder::AgentBuilder;
pub use error::AgentError;
pub use runtime::{AgentConfig, RuntimeBundle};
pub use session::AgentSession;
pub use task::{Plan, Step, StepStatus};
pub use task::{Plan, PlanParser, Step, StepStatus, TaskAgent};
pub use task::JsonPlanParser;
+10
View File
@@ -31,6 +31,10 @@ pub enum AgentError {
#[error("记忆错误: {0}")]
Memory(#[from] MemoryError),
/// Plan 解析失败(Phase 4b 新增)。
#[error("Plan 解析错误: {0}")]
PlanParse(String),
/// 钩子阻断操作(Agent 层特有)。
#[error("钩子阻断: {0}")]
HookBlocked(String),
@@ -63,6 +67,7 @@ impl AgentError {
),
Self::Tool(e) => e.is_recoverable(),
Self::Memory(e) => e.is_recoverable(),
Self::PlanParse(_) => false,
Self::HookBlocked(_) | Self::LimitExceeded(_) | Self::Config(_) | Self::Other(_) => {
false
}
@@ -132,6 +137,11 @@ mod tests {
assert!(!AgentError::Other("unknown".into()).is_recoverable());
}
#[test]
fn plan_parse_not_recoverable() {
assert!(!AgentError::PlanParse("bad json".into()).is_recoverable());
}
#[test]
fn from_llm_via_question_mark() {
fn returns_llm() -> Result<(), LlmError> {
+122
View File
@@ -12,6 +12,8 @@
use crate::agent::error::AgentError;
use crate::llm::types::ChatResponse;
use async_trait::async_trait;
/// 任务规划 —— 一组有序的 Step。
#[derive(Debug)]
pub struct Plan {
@@ -78,6 +80,96 @@ impl StepStatus {
}
}
/// Plan 解析接口 —— 将 LLM 原始输出转换为 `Plan` 数据结构。
///
/// **注入式**:上层应用可以注入自定义解析器(如基于 XML / YAML / 自定义 DSL),
/// `JsonPlanParser` 是参考实现而非默认实现。
#[async_trait]
pub trait PlanParser: Send + Sync {
/// 将 LLM 原始输出解析为 `Plan`。
///
/// - `raw`LLM 返回的原始文本
/// - `goal`:规划目标(用于填充 `Plan.goal`
async fn parse(&self, raw: &str, goal: &str) -> Result<Plan, AgentError>;
}
/// JSON 格式的 Plan 解析器(参考实现)。
///
/// 期望 LLM 输出形如:
/// ```json
/// {"steps": [{"description": "..."}, ...]}
/// ```
/// 的 JSON 文本。解析失败返回 `AgentError::PlanParse`。
pub struct JsonPlanParser;
#[async_trait]
impl PlanParser for JsonPlanParser {
async fn parse(&self, raw: &str, goal: &str) -> Result<Plan, AgentError> {
let parsed: serde_json::Value = serde_json::from_str(raw)
.map_err(|e| AgentError::PlanParse(format!("JSON 解析失败: {e}")))?;
let steps_array = parsed
.get("steps")
.and_then(|v| v.as_array())
.ok_or_else(|| AgentError::PlanParse("缺少 'steps' 数组".into()))?;
let steps: Vec<Step> = steps_array
.iter()
.enumerate()
.map(|(i, item)| {
let description = item
.get("description")
.and_then(|v| v.as_str())
.ok_or_else(|| {
AgentError::PlanParse(format!("步骤 {i} 缺少 'description' 字段"))
})?;
Ok(Step::new(i, description))
})
.collect::<Result<Vec<_>, AgentError>>()?;
if steps.is_empty() {
return Err(AgentError::PlanParse(
"Plan 至少需要一个步骤".into(),
));
}
Ok(Plan {
id: uuid(),
goal: goal.to_string(),
steps,
})
}
}
/// 任务型智能体 —— 自主规划与执行。
///
/// 与基础 `Agent` trait 分离:`Agent` 定义"角色"system prompt + 工具集),
/// `TaskAgent` 定义"规划/执行"行为(如何拆 Plan、如何执行 Plan)。
#[async_trait]
pub trait TaskAgent: Send + Sync {
/// 自主式入口:根据目标生成 Plan 并执行。
///
/// 实现内部应调用 `PlanParser::parse` 从 LLM 输出生成 Plan
/// 然后调用 `execute_plan` 执行。
async fn run(&mut self, goal: &str) -> Result<Plan, AgentError>;
/// 外部驱动式入口:执行预定义的 Plan。
///
/// 逐步调用 `AgentSession::submit_turn`,每步完成后触发
/// `OnPlanStepComplete` hook,更新步骤状态。
async fn execute_plan(&mut self, plan: &mut Plan) -> Result<(), AgentError>;
}
/// 生成简易唯一 ID(仅用于 Plan 标识,非加密安全)。
fn uuid() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("{ts:x}")
}
#[cfg(test)]
mod tests {
use super::*;
@@ -118,4 +210,34 @@ mod tests {
assert_eq!(plan.steps[0].index, 0);
assert_eq!(plan.steps[1].index, 1);
}
/// 烟雾测试 1JsonPlanParser 解析合法 JSON。
#[tokio::test]
async fn json_plan_parser_success() {
let parser = JsonPlanParser;
let input = r#"{"steps": [{"description": "step one"}, {"description": "step two"}]}"#;
let plan = parser.parse(input, "my goal").await.unwrap();
assert_eq!(plan.goal, "my goal");
assert_eq!(plan.steps.len(), 2);
assert_eq!(plan.steps[0].description, "step one");
assert_eq!(plan.steps[1].description, "step two");
assert!(plan.steps.iter().all(|s| s.status.is_pending()));
}
/// 烟雾测试 2JsonPlanParser 解析失败返回 AgentError::PlanParse。
#[tokio::test]
async fn json_plan_parser_invalid_json() {
let parser = JsonPlanParser;
let err = parser.parse("not json", "goal").await.unwrap_err();
assert!(matches!(err, AgentError::PlanParse(_)));
}
/// 烟雾测试 3JsonPlanParser 空步骤返回错误。
#[tokio::test]
async fn json_plan_parser_empty_steps() {
let parser = JsonPlanParser;
let input = r#"{"steps": []}"#;
let err = parser.parse(input, "goal").await.unwrap_err();
assert!(matches!(err, AgentError::PlanParse(_)));
}
}
+11
View File
@@ -20,6 +20,8 @@ pub enum HookEvent {
OnTurnStart,
/// Agent 会话完成一轮 turn 之后(Phase 4a 新增)。
OnTurnEnd,
/// TaskAgent 完成一个 Plan 步骤后触发(Phase 4b 新增)。
OnPlanStepComplete,
}
/// 此次钩子调用的上下文。
@@ -35,6 +37,8 @@ pub struct HookContext<'a> {
pub attempt: u32,
/// 当前 turn 序号(0-based,仅 OnTurnStart / OnTurnEnd 可用,Phase 4a 新增)。
pub turn_index: Option<u32>,
/// 当前 plan step 序号(0-based,仅 OnPlanStepComplete 可用,Phase 4b 新增)。
pub plan_step_index: Option<usize>,
}
impl<'a> HookContext<'a> {
@@ -45,6 +49,7 @@ impl<'a> HookContext<'a> {
error: None,
attempt: 0,
turn_index: None,
plan_step_index: None,
}
}
@@ -68,6 +73,12 @@ impl<'a> HookContext<'a> {
self.turn_index = Some(turn_index);
self
}
/// 设置 plan step 序号(仅 OnPlanStepComplete 使用,Phase 4b 新增)。
pub(crate) fn with_plan_step_index(mut self, plan_step_index: usize) -> Self {
self.plan_step_index = Some(plan_step_index);
self
}
}
/// 钩子执行结果。