StepStatus::Completed 字段类型从废弃的 ChatResponse 切换为 IR 层 MessageResponse,同时清理 task_agent_demo.rs 的 3 处废弃类型引用: - ChatResponse → MessageResponse (字段映射见 §3.1.2) - OpenaiChatMessage::assistant_text(t) → Message::assistant(t) - FinishReason::Stop → StopReason::Stop (Option 包裹同步移除) 移除 src/agent/task.rs 的两处 #[allow(deprecated)] 与 examples/task_agent_demo.rs 顶部 #![allow(deprecated)],零 deprecated warning。 验收: cargo build + clippy -D warnings + test --all-targets 全绿; cargo run --example task_agent_demo exit 0。
240 lines
7.9 KiB
Rust
240 lines
7.9 KiB
Rust
//! 任务规划数据结构 + 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<Step>,
|
||
}
|
||
|
||
/// 任务步骤。
|
||
#[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<String>) -> 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<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::*;
|
||
|
||
#[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(_)));
|
||
}
|
||
}
|