//! 任务规划数据结构 + Phase 4b 任务执行 trait。 //! //! Phase 4a 范围:仅 `Plan` / `Step` / `StepStatus` 纯数据结构。 //! Phase 4b 在此文件追加 `TaskAgent` trait / `PlanParser` trait / `JsonPlanParser` 参考实现。 //! //! 设计意图(参见 `docs/7-agent-runtime.md` §3.2.4、§3.3.1): //! //! - `StepStatus` 用 enum 而非简单 bool,便于 UI 展示和统计 //! - 状态机单向:`Pending → Running → (Completed | Failed | Skipped)`,不回退 //! - 重试由上层新建 `Plan` 实现,`TaskAgent` 不做自动重试 use crate::agent::error::AgentError; use crate::llm::types::response_v2::MessageResponse; use async_trait::async_trait; /// 任务规划 —— 一组有序的 Step。 #[derive(Debug)] pub struct Plan { /// 规划唯一标识。 pub id: String, /// 规划目标(人类可读)。 pub goal: String, /// 步骤列表。 pub steps: Vec, } /// 任务步骤。 #[derive(Debug)] pub struct Step { /// 步骤在 Plan 中的位置(0-based)。 pub index: usize, /// 步骤描述(注入 LLM 作为 user prompt)。 pub description: String, /// 当前状态。 pub status: StepStatus, } impl Step { /// 创建一个初始为 `Pending` 的步骤。 pub fn new(index: usize, description: impl Into) -> Self { Self { index, description: description.into(), status: StepStatus::Pending, } } } /// 步骤状态机。 /// /// 转换路径:`Pending → Running → (Completed | Failed | Skipped)`,单向不回退。 /// /// **不实现 `Clone`**:`Failed` 变体携带 `AgentError`,下层 `LlmError` / `MemoryError` /// 均未派生 `Clone`(保留原始错误信息,传递所有权而非克隆)。如需复制 `Plan`, /// 只能 clone 处于 `Pending` / `Running` / `Completed` / `Skipped` 状态的步骤。 #[derive(Debug)] #[non_exhaustive] pub enum StepStatus { /// 初始状态 —— 等待执行。 Pending, /// 正在执行(`TaskAgent::execute_plan` 进入)。 Running, /// 已完成(含 LLM 响应)。 Completed(MessageResponse), /// 失败(含错误)。 Failed(AgentError), /// 跳过(上层主动跳过)。 Skipped, } impl StepStatus { /// 状态是否处于"未完成"。 pub fn is_pending(&self) -> bool { matches!(self, Self::Pending) } /// 状态是否处于终态。 pub fn is_terminal(&self) -> bool { matches!(self, Self::Completed(_) | Self::Failed(_) | Self::Skipped) } } /// 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; } /// 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 { 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 = 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::, 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。 /// /// 逐步调用 `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::*; #[test] fn step_initial_state_is_pending() { let s = Step::new(0, "do something"); assert!(s.status.is_pending()); assert!(!s.status.is_terminal()); assert_eq!(s.index, 0); assert_eq!(s.description, "do something"); } #[test] fn terminal_states_classified() { let err = AgentError::Other("x".into()); assert!(StepStatus::Failed(err).is_terminal()); assert!(StepStatus::Skipped.is_terminal()); } #[test] fn running_is_not_terminal() { assert!(!StepStatus::Running.is_terminal()); assert!(!StepStatus::Running.is_pending()); } #[test] fn plan_holds_steps() { let plan = Plan { id: "p1".into(), goal: "test goal".into(), steps: vec![Step::new(0, "first"), Step::new(1, "second")], }; assert_eq!(plan.steps.len(), 2); assert_eq!(plan.steps[0].index, 0); assert_eq!(plan.steps[1].index, 1); } /// 烟雾测试 1:JsonPlanParser 解析合法 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())); } /// 烟雾测试 2:JsonPlanParser 解析失败返回 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(_))); } /// 烟雾测试 3:JsonPlanParser 空步骤返回错误。 #[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(_))); } }