Files
agcore/src/agent/task.rs
T
徐涛 5b4343a051 refactor(agent): StepStatus::Completed 切换至 MessageResponse
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。
2026-07-05 19:48:59 +08:00

240 lines
7.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 任务规划数据结构 + 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);
}
/// 烟雾测试 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(_)));
}
}