Files
agcore/docs/7-agent-runtime.md
T

885 lines
46 KiB
Markdown
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.
# Agent Runtime 方案设计
> 设计日期:2026-06-09
> 状态:待实施
> 关联文档:
> - `docs/note-agent-runtime-design.md` — 设计决策记录(接口签名、文件清单、决策依据)
> - `docs/note-agent-harness-references.md` — 参考项目调研(OpenClaw / Hermes / OpenHuman / OpenHarness
> - `docs/6-memory-system.md` — Phase 3 方案
> - `docs/5-tool-system.md` — Phase 2 方案
> - `docs/roadmap.md` — 项目总 Roadmap
---
## 1. 背景与目标
### 1.1 背景
AG Core 已完成 Phase 0LLM 调用周期)、Phase 1(提示词工程)、Phase 2(工具系统)、Phase 3(记忆系统)共 4 个 phase 的交付。`LlmCycle::submit_with_tools()` 已在 Phase 2 末实现"LLM 决策 → 工具执行 → 回传结果"的单次循环;`ConversationMemory` / `KnowledgeStore` / `MemoryRetriever` 在 Phase 3 提供了完整的记忆抽象。
当前缺一个**整合层**:把 Phase 0-3 的能力"装配"起来,对上层应用暴露"智能体"的概念。
### 1.2 目标
Phase 4 整体目标是提供一个**薄胶水层 + 一组 trait 抽象**,让上层应用可以基于 AG Core 构建多轮对话、任务规划等智能体行为。为控制 scope、降低交付风险,拆分为三个子阶段实施:
| 子阶段 | 定位 | 交付物 |
|--------|------|--------|
| **Phase 4a(核心胶水层)** | 最小可用 Agent Runtime | `Agent` + `AgentSession` + `submit_turn` + `RuntimeBundle` / `AgentBuilder` / `AgentError` + `Plan`/`Step` 纯数据 + hooks 扩展 |
| **Phase 4b(任务执行)** | 自主任务规划与执行 | `TaskAgent` + `PlanParser` trait + `JsonPlanParser` + `OnPlanStepComplete` hook |
| **Phase 4c(会话级记忆)** | 跨 context 信息桥接 | `SessionMemory`(基于 `MemoryStore`+ AgentSession 接入 + builder 支持 |
**每个子阶段独立交付**,Phase 4a 完成后上层即可接入;Phase 4b/4c 无相互依赖,可并行或按需延后。
Phase 4a 具体包括:
- **`Agent` trait** — 智能体的"角色"抽象(不绑定 session
- **`AgentSession` struct** — 智能体的"会话"实例(绑定 session_id + 状态)
- **`RuntimeBundle`** — 显式依赖注入容器,集中管理 provider/registry/hook 等依赖
- **`AgentBuilder`** — 链式构造入口
- **`AgentError`** — 统一错误类型,聚合 LlmError / ToolError / MemoryError
- **`Plan` / `Step` / `StepStatus`** — 任务规划纯数据结构(不做解析逻辑)
Phase 4b 追加:
- **`TaskAgent` trait** — 任务型智能体的"规划/执行"抽象
- **`PlanParser` trait + `JsonPlanParser`** — Plan 解析接口与参考实现
Phase 4c 追加:
- **`SessionMemory`** — 会话级记忆,用于 context 间的信息桥接(基于 `MemoryStore` 后端)
### 1.3 设计原则
Phase 4 严格遵循以下原则,所有范围决策都基于这些原则推导:
| 原则 | 含义 | 推导 |
|------|------|------|
| **最小范围** | AG Core 是 lib crate,不是产品;不实现业务循环 | 只暴露 trait + 最小 reference impl |
| **薄胶水层** | 不在 L1 重写已经做好的能力 | 复用 `LlmCycle::submit_with_tools` 等已有 API |
| **依赖注入** | 所有运行时依赖显式打包传递 | 采用 OpenHarness `RuntimeBundle` 模式 |
| **实体/会话分离** | 同一角色可被多 session 复用 | `Agent` + `AgentSession` 两层模型 |
| **记忆弱引用** | 记忆是"被动能力",不内嵌循环 | `memory_store: Option<Arc<dyn MemoryStore>>` 弱引用 |
| **业务可注入** | Plan 拆解是业务能力,不在 core 库实现 | 暴露 `PlanParser` trait,上层注入 |
| **会话级记忆** | session 内共享、context 间桥接,不是持久层也不是对话历史 | `SessionMemory` 基于 `MemoryStore`,按 session_id 命名空间隔离 |
| **借鉴不照搬** | 4 个参考项目均非 Rust 实现 | 只取架构模式,不抄实现细节 |
### 1.4 与已完成的 Phase 关系
```
Phase 0 (L0/L1) ── LlmProvider / LlmCycle / Hook / Stream / Compact
Phase 1 (L2) ── PromptTemplate / PromptComposer
Phase 2 (L1) ── ToolRegistry / BaseTool / PermissionChecker / McpClient
Phase 3 (L2) ── MemoryStore / ConversationMemory / KnowledgeStore / MemoryRetriever
│ 复用
Phase 4a (L1→L2) ── Agent trait + AgentSession + submit_turn + RuntimeBundle + Plan/Step 纯数据(胶水层)
Phase 4b (L2) ── TaskAgent + PlanParser + JsonPlanParser(任务执行)
Phase 4c (L2) ── SessionMemory(会话级记忆)
应用层 (L4) ── 上层 crate / 二进制 / Gateway(不在 Phase 4 范围)
```
详细架构对照见 `docs/note-agent-harness-references.md` §3-5。
## 2. 需求分析
### 2.1 功能需求
| ID | 需求 | 优先级 | 归属 | 说明 |
|----|------|--------|------|------|
| F1 | `Agent` trait 抽象 | P0 | 4a | 角色定义:name / system_prompt / 工具集 |
| F2 | `AgentSession` 会话实例 | P0 | 4a | 绑定 session_id、bundle、turn_index、cost_so_far |
| F3 | `submit_turn()` 最小 reference impl | P0 | 4a | 组装 LlmCycle → submit → 累计 cost~30 行 |
| F6 | `Plan` / `Step` / `StepStatus` 数据结构 | P0 | 4a | 含 Pending / Running / Completed / Failed / Skipped 状态机 |
| F8 | `RuntimeBundle` 依赖注入容器 | P0 | 4a | 聚合 provider/registry/hook/config(不含 session_memory_backend |
| F9 | `AgentBuilder` 链式构造 | P0 | 4a | 构建 `RuntimeBundle`,retriever 存在时自动注册为 tool |
| F10 | `AgentError` 统一错误类型 | P0 | 4a | 聚合 LlmError / ToolError / MemoryError,含 `is_recoverable()` |
| F11a | Hook 事件扩展:OnTurnStart / OnTurnEnd + turn_index 字段 | P0 | 4a | 在 `llm/hooks.rs` 中追加 2 个事件 + 1 个字段 |
| F12a | 烟雾测试 3-4 个(Phase 4a | P0 | 4a | trait 可装配 / RuntimeBundle 可构造 / submit_turn 跑通 mock / Plan 数据结构 |
| F13 | `lib.rs` 导出 `pub mod agent;` | P0 | 4a | 一行 |
| F14 | 方案文档(本文件)+ 决策记录 | P0 | — | ✅ 已完成 |
| F4 | `TaskAgent::run(goal)` 自主式入口 | P0 | 4b | 内部用 LLM 拆 Plan,再调用 `execute_plan` |
| F5 | `TaskAgent::execute_plan(plan)` 外部驱动式入口 | P0 | 4b | 用户预定义 Plan,逐步执行 |
| F7 | `PlanParser` trait + `JsonPlanParser` 参考实现 | P0 | 4b | 注入式,上层可替换 |
| F11b | Hook 事件扩展:OnPlanStepComplete + plan_step_index 字段 | P0 | 4b | 在 `llm/hooks.rs` 中追加 1 个事件 + 1 个字段 |
| F12b | 烟雾测试 2-3 个(Phase 4b | P0 | 4b | TaskAgent + PlanParser 跑通 mock |
| F15a | Roadmap 状态翻转(Phase 4a | P0 | 4a | 实施完成后做 |
| F15b | Roadmap 状态翻转(Phase 4b | P0 | 4b | 实施完成后做 |
| F16 | SessionMemory 会话级记忆 | P0 | 4c | 基于 `MemoryStore`context 间信息桥接 |
| F17 | RuntimeBundle / Builder 扩展 session_memory_backend | P0 | 4c | 追加字段 + setter 方法 |
| F18 | AgentSession 接入 SessionMemory | P0 | 4c | 替换内联 HashMap,接入完整 SessionMemory |
| F12c | 烟雾测试 2-3 个(Phase 4c | P0 | 4c | SessionMemory set/get/snapshot |
| F15c | Roadmap 状态翻转(Phase 4c | P0 | 4c | 实施完成后做 |
### 2.2 非功能需求
| ID | 需求 | 说明 |
|----|------|------|
| NF1 | 不引入新外部依赖 | 仅使用 Phase 0-3 已有的 `async-trait` / `serde` / `thiserror` / `tokio` 等 |
| NF2 | 错误体系完善 | `AgentError` 聚合下层错误,含 `is_recoverable()` 分类 |
| NF3 | 线程安全 | 所有公开类型满足 `Send + Sync` |
| NF4 | 异步优先 | 涉及 IO 的 API 全部 `async` |
| NF5 | 模块化 | 各组件独立可替换,遵循"trait 抽象 + 轻量默认实现"惯例 |
| NF6 | 文档注释 | 所有公开 API 必须有 `///` 文档注释 |
| NF7 | builder 模式 | 复杂配置走 builder 链式构造 |
| NF8 | 显式依赖 | 不引入模块级全局状态,所有依赖通过参数或 bundle 注入 |
| NF9 | 不破坏现有 API | Phase 0-3 的公开 API 一字不改;`hooks.rs` 扩展为"追加变体 + 追加字段"(兼容) |
| NF10 | 最小测试覆盖 | 核心 trait 至少 1 个烟雾测试;`submit_turn` 至少 1 个 mock 测试;不强求集成测试 |
## 3. 方案设计
### 3.1 总体架构
```
┌──────────────────────────────────────────────────────────────────────┐
│ 应用层(不在 Phase 4 范围) │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ CLI Agent │ │ Feishu Bot │ │ Web Service│ │ TUI App │ │
│ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │
└─────────┼────────────────┼────────────────┼────────────────┼───────────┘
│ │ │ │
└────────────────┴────────────────┴────────────────┘
┌──────────────────────────────────────────────────────────────────────┐
│ Agent RuntimePhase 4
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Agent trait │ 1 ──── * │ AgentSession │ │
│ │ (角色) │ │ (会话实例) │ │
│ └────────────────┘ └──────┬───────────┘ │
│ │ Arc<...> │
│ ▼ │
│ ┌──────────────────┐ │
│ │ RuntimeBundle │ │
│ │ - provider │ │
│ │ - tool_registry │ │
│ │ - hook_executor │ │
│ │ - memory_store? │ ◄─ 弱引用 │
│ │ - retriever? │ ◄─ 弱引用 │
│ │ - config │ │
│ └──────┬───────────┘ │
│ │ new() 时若 retriever 存在 │
│ ▼ │
│ ┌──────────────────┐ │
│ │ "retrieve" tool │ ◄─ 自动注册 │
│ └──────────────────┘ │
│ │
│ ┌────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ TaskAgent trait│ │ Plan/Step/Status │ │ PlanParser trait │ │
│ │ run() │ │ 状态机 │ │ JsonPlanParser │ │
│ │ execute_plan()│ │ │ │ (参考实现 ~20行) │ │
│ └────────────────┘ └──────────────────┘ └──────────────────┘ │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ AgentError │ │ AgentBuilder │ │
│ │ (聚合) │ │ (链式构造) │ │
│ └────────────────┘ └──────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
▼ 复用
┌──────────────────────────────────────────────────────────────────────┐
│ LLM / Tool / Prompt / MemoryPhase 0-3
│ LlmCycle / ProviderRegistry / ToolRegistry / PermissionChecker / │
│ HookExecutor / StreamEvents / CompactConfig / │
│ PromptTemplate / PromptComposer / │
│ MemoryStore / ConversationMemory / KnowledgeStore / MemoryRetriever│
└──────────────────────────────────────────────────────────────────────┘
```
### 3.2 接口设计
详细接口签名见 `docs/note-agent-runtime-design.md` §4,本节说明设计意图。
#### 3.2.1 `Agent` trait
```rust
pub trait Agent: Send + Sync {
fn name(&self) -> &str;
fn system_prompt(&self) -> Option<&str>;
/// 列出该 Agent 想要暴露给 LLM 的工具定义。
/// 默认实现:从 RuntimeBundle.tool_registry 取全部(最常用)。
/// 子 trait 可覆盖做白名单/过滤。
fn tool_definitions(&self, bundle: &RuntimeBundle) -> Vec<ToolDefinition>;
}
```
**设计意图**
- `name` / `system_prompt` 是 LLM 调用必需的元数据
- `tool_definitions` 默认从 bundle 全量取,**Agent 可以在不修改 bundle 的情况下做工具白名单**——这与 Hermes 的"Skill 暴露"机制对齐
- 不在 trait 里强制 `submit_turn`——`submit_turn``AgentSession` 的方法,不应绑死在角色定义上
#### 3.2.2 `RuntimeBundle`
```rust
pub struct RuntimeBundle {
pub provider: Arc<dyn LlmProvider>,
pub tool_registry: Arc<ToolRegistry>,
pub hook_executor: Arc<HookExecutor>,
pub memory_store: Option<Arc<dyn MemoryStore>>, // 弱引用
pub retriever: Option<Arc<MemoryRetriever>>, // 弱引用
pub session_memory_backend: Option<Arc<dyn MemoryStore>>, // SessionMemory 后端(选填)
pub config: AgentConfig,
}
```
**设计意图**
- 所有运行时依赖**显式打包**OpenHarness 风格)
- `memory_store` / `retriever` 均为 `Option`——上层应用**不传也能跑**(无记忆模式)
-`retriever` 存在时,`RuntimeBundle::new()` 内部自动注册一个名为 `"retrieve"` 的 tool(具体实现:在 `ToolRegistry` 里加一个 `RetrieveTool` 包装),让 LLM 在对话中**主动**调用检索能力
- `session_memory_backend``SessionMemory` 的持久后端。传入时 `SessionMemory` 使用该后端(支持跨进程共享);不传时 `AgentSession` 内部自动创建 `InMemoryStore` 作为进程级隔离的后端
- `config` 集中管理所有可调参数(max_turns、max_tool_turns、session_ttl、compact_config
#### 3.2.3 `AgentSession` 与最小 reference impl
```rust
pub struct AgentSession {
pub session_id: String,
pub agent: Arc<dyn Agent>,
bundle: Arc<RuntimeBundle>,
turn_index: u32,
cost_so_far: CostTracker,
session_memory: SessionMemory,
}
impl AgentSession {
/// 最小 reference impl(约 30 行):
/// 1. 触发 OnTurnStart hook
/// 2. 组装 LlmCycle(注入 system_prompt + messages 历史 + tool definitions
/// 3. submit_with_tools() 跑单轮对话
/// 4. 累计 cost
/// 5. 触发 OnTurnEnd hook
/// 6. turn_index += 1
/// 7. 返回 ChatResponse
/// 不做 memory 回写(由上层独立 task 处理)
pub async fn submit_turn(
&mut self,
user_input: impl Into<String>,
) -> Result<ChatResponse, AgentError>;
}
```
**设计意图**
- `agent: Arc<dyn Agent>` 而非 `agent_name: String`——`submit_turn` 从 agent 获取 `system_prompt()``tool_definitions()`,同时为 v0.2+ 的"热切换 agent"预留:替换 `self.agent` 即可切换角色
- `session_memory` 是进程内共享的会话级记忆,context 间通过它桥接信息(详见 §3.2.8)
- "最小 reference impl" 只演示**最常见**的对话场景
- 业务循环(多轮策略、错误重试、记忆回写时机)由上层应用或具体的 `TaskAgent` 实现决定
- `submit_turn` 不持有 `ConversationMemory`——上层应用可独立 new 一个 `ConversationMemory`,在合适的时机(如 OnTurnEnd hook)调 `add_message`
#### 3.2.4 `TaskAgent` + `Plan` / `Step`
```rust
pub struct Plan {
pub id: String,
pub goal: String,
pub steps: Vec<Step>,
}
pub struct Step {
pub index: usize,
pub description: String,
pub status: StepStatus,
}
pub enum StepStatus {
Pending,
Running,
Completed(ChatResponse),
Failed(AgentError),
Skipped,
}
```
**设计意图**
- `StepStatus` 用 enum 而非简单 bool,便于上层 UI 展示和统计
- 状态机转换:`Pending → Running → (Completed | Failed | Skipped)`,单向不可回退(重试由上层新建 Plan)
- `Plan` / `Step` 故意保持简单——不引入 `dependencies` / `parallel_group` 等高级字段(v0.3+ 再考虑)
#### 3.2.5 `PlanParser` trait + `JsonPlanParser` 参考实现
```rust
#[async_trait]
pub trait PlanParser: Send + Sync {
async fn parse(&self, raw: &str, goal: &str) -> Result<Plan, AgentError>;
}
pub struct JsonPlanParser;
#[async_trait]
impl PlanParser for JsonPlanParser {
/// 期望 LLM 输出形如:
/// {"steps": [{"description": "..."}, ...]}
/// 的 JSON 文本。
/// 解析失败返回 AgentError::PlanParse。
async fn parse(&self, raw: &str, goal: &str) -> Result<Plan, AgentError> { /* ... */ }
}
```
**设计意图**
- **注入式**:上层应用可以注入自己的 `PlanParser`(如基于 XML / YAML / 自定义 DSL
- `JsonPlanParser` 是**参考实现**,不是默认实现——上层必须显式选择
- `JsonPlanParser` 大约 20 行:`serde_json::from_str` 解析 + 字段映射
#### 3.2.6 `AgentError`
```rust
pub enum AgentError {
Llm(LlmError),
Tool(ToolError),
Memory(MemoryError),
PlanParse(String),
HookBlocked(String),
LimitExceeded(String),
Config(String),
Other(String),
}
```
**设计意图**
- 聚合而非包装下层错误(避免 `Box<dyn Error>` 丢失类型)
- `PlanParse` / `HookBlocked` / `LimitExceeded` / `Config` 是 Agent 层特有的错误类型
- `is_recoverable()` 根据变体类型判定(如 `Memory(_)` 可恢复、`PlanParse(_)` 不可恢复)
#### 3.2.7 `AgentConfig` + `AgentBuilder`
```rust
pub struct AgentConfig {
pub max_turns: u32,
pub max_tool_turns: u32,
pub session_ttl: Option<Duration>,
pub compact_config: Option<CompactConfig>,
}
pub struct AgentBuilder { /* ... */ }
impl AgentBuilder {
pub fn new() -> Self;
pub fn provider(self, p: Arc<dyn LlmProvider>) -> Self;
pub fn tool_registry(self, r: Arc<ToolRegistry>) -> Self;
pub fn hook_executor(self, h: Arc<HookExecutor>) -> Self;
pub fn memory_store(self, m: Arc<dyn MemoryStore>) -> Self; // 选填
pub fn retriever(self, r: Arc<MemoryRetriever>) -> Self; // 选填
pub fn session_memory_backend(self, s: Arc<dyn MemoryStore>) -> Self; // 选填
pub fn config(self, c: AgentConfig) -> Self;
pub fn build(self) -> Result<RuntimeBundle, AgentError>;
}
```
**设计意图**
- `AgentBuilder` 是**唯一**的 `RuntimeBundle` 构造入口
- 必填字段在 `build()` 时校验(`provider` / `tool_registry` / `hook_executor` 不可缺)
- `memory_store` / `retriever` / `session_memory_backend` 选填
- `session_memory_backend` 不传时,`AgentSession` 内部用 `InMemoryStore` 兜底(进程级隔离)
#### 3.2.8 `SessionMemory` — 会话级记忆
```rust
pub struct SessionMemory {
store: Arc<dyn MemoryStore>,
namespace: String,
}
impl SessionMemory {
/// 创建新的 session 级记忆实例。
/// store:后端存储(可跨进程共享的 MemoryStore 实现)。
/// namespace:按 session_id 隔离,防止跨 session 泄漏。
pub fn new(store: Arc<dyn MemoryStore>, namespace: &str) -> Self;
/// 写入一条 key-value 条目。
pub async fn set(&self, key: &str, value: &str) -> Result<(), AgentError>;
/// 读取指定 key 的值。
pub async fn get(&self, key: &str) -> Result<Option<String>, AgentError>;
/// 返回所有条目的格式化快照,适合注入 system prompt。
/// 格式:
/// <session-context>
/// key1: value1
/// key2: value2
/// </session-context>
pub async fn snapshot(&self) -> Result<String, AgentError>;
/// 删除指定 key。
pub async fn remove(&self, key: &str) -> Result<(), AgentError>;
/// 清空当前 namespace 下所有条目。
pub async fn clear(&self) -> Result<(), AgentError>;
}
```
**设计意图**
- `SessionMemory` 是**会话级**记忆,不是持久层(`MemoryStore`)也不是对话历史(`ConversationMemory`)——它的定位是 session 内各 context 之间的信息桥接
- **复用 Phase 3 `MemoryStore` trait**:不引入新的存储后端机制。单进程场景用 `InMemoryStore`(零序列化开销),跨进程场景换 Redis / SQLite 等实现即可
- **按 `namespace` 隔离**:每个 session 一个独立命名空间(`"_session_{session_id}"`),避免跨 session 意外泄漏
- **`snapshot()` 格式化为标记文本**:专为注入 system prompt 设计,LLM 可以自然理解 `<session-context>` 标签中的内容
- **所有方法为 `async`**:因为后端可能是跨进程的(Redis / DB),虽然 `InMemoryStore` 本身是同步操作
- **不引入自己的错误类型**:错误通过 `AgentError::Memory` 传播(复用已有变体)
**三层记忆体系关系**
```
持久层(Phase 3 MemoryStore / KnowledgeStore ── 跨 session 持久,长期知识
会话层(新增) SessionMemory ── 单 session 内共享,context 桥接
对话层(Phase 3 ConversationMemory ── 单 context 内消息历史
```
**典型使用模式**v0.2+ context 切换场景):
```
context_a (build agent)
→ 在对话中决定某个关键结论值得记下来
→ 调用 session_memory.set("design_decision", "用 PostgreSQL")
→ 继续对话
创建 context_b (plan agent)
→ system_prompt 末尾追加 session_memory.snapshot()
→ LLM 看到 "<session-context>\ndesign_decision: 用 PostgreSQL\n</session-context>"
→ 无需看 context_a 的 50 轮完整历史,但知道关键上下文
```
### 3.3 状态机
#### 3.3.1 `StepStatus` 状态转换图
```
┌─────────────┐
│ Pending │ ◄── 初始状态
└──────┬──────┘
│ execute_plan() 进入
┌─────────────┐
│ Running │ ◄── 触发 OnPlanStepCompletestatus=Running
└──────┬──────┘
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│Completed│ │ Failed │ │ Skipped │
└─────────┘ └──────────┘ └──────────┘
触发 OnPlanStepCompletestatus=Completed
触发 OnPlanStepCompletestatus=Failed
触发 OnPlanStepCompletestatus=Skipped
```
**设计约束**
- 状态转换**单向**Pending → Running → 终态),不回退
- 终态(Completed / Failed / Skipped)触发 `OnPlanStepComplete` hook
- 重试由上层应用新建 `Plan` 实现(不在 `TaskAgent` 内做自动重试)
#### 3.3.2 Session 状态
`AgentSession` 的状态机比 `Step` 简单:
```
创建 (new) ──► turn_index=0 ──► submit_turn() ──► turn_index+=1 ──► ... ──► 销毁
```
`turn_index` 累加,`cost_so_far` 累加,无显式状态枚举(避免过度设计)。
### 3.4 Hook 扩展设计
`src/llm/hooks.rs` 中追加 3 个事件 + 2 个上下文字段:
```rust
pub enum HookEvent {
// ... 现有 4 个:PreRequest / PostRequest / OnRetry / OnError ...
// 新增 3 个(Phase 4):
OnTurnStart,
OnTurnEnd,
OnPlanStepComplete,
}
pub struct HookContext {
// ... 现有字段 ...
// 新增 2 个(Phase 4):
pub turn_index: Option<u32>, // OnTurnStart / OnTurnEnd 用
pub plan_step_index: Option<usize>, // OnPlanStepComplete 用
}
```
**设计意图**
- **不破坏现有 hook 兼容性**:3 个新事件是 enum 追加,2 个新字段是 `Option<T>` 默认 `None`
- 上层应用可通过监听 `OnTurnEnd` 实现"独立 task 回写 ConversationMemory"——呼应"记忆在独立 task 处理"原则
- `OnPlanStepComplete` 提供"步骤级别"的可观测性,与 Hermes 的"任务进度回调"对齐
### 3.5 错误体系
`AgentError` 与下层错误的关系:
```
┌──────────────────┐
│ AgentError │
├──────────────────┤
│ Llm(LlmError) │──► 透传 Phase 0 错误,含 is_recoverable()
│ Tool(ToolError) │──► 透传 Phase 2 错误,含 is_recoverable()
│ Memory(MemoryError)│─► 透传 Phase 3 错误
│ PlanParse(String) │─► Agent 层特有
│ HookBlocked(String)│─► Agent 层特有
│ LimitExceeded(String)│► Agent 层特有
│ Config(String) │──► Agent 层特有
│ Other(String) │──► 兜底
└──────────────────┘
is_recoverable(): 聚合判定
- Llm/Memory 可恢复(重试)
- PlanParse / Config 不可恢复(需人工介入)
- Tool / HookBlocked / LimitExceeded 按内层错误判定
```
**自动 From 转换**:通过 `#[from]` 宏实现 `From<LlmError>` / `From<ToolError>` / `From<MemoryError>`,让 `submit_turn` 内部可以用 `?` 运算符直接传播。
### 3.6 与 Phase 0-3 模块的集成
| Phase 4 组件 | 调用的下层 API | 调用位置 |
|-------------|--------------|---------|
| `AgentSession::submit_turn` | `LlmCycle::new` + `with_system_prompt` + `with_hook_executor` + `with_compact_config` + `with_messages` + `submit_with_tools` | session.rs |
| `AgentSession::submit_turn` | `CostTracker::add`(累计 cost | session.rs |
| `RuntimeBundle::new` | `ToolRegistry::register`(注册 retrieve tool | runtime.rs |
| `TaskAgent::execute_plan` | `AgentSession::submit_turn`(每步调一次) | task.rs |
| `JsonPlanParser::parse` | `serde_json::from_str` | task.rs |
| `AgentError::from` | `LlmError` / `ToolError` / `MemoryError` | error.rs |
| `HookContext` 扩展 | `HookEvent::OnTurnStart/End/OnPlanStepComplete` | llm/hooks.rs |
| `SessionMemory::set/get/snapshot` | `MemoryStore::save/load/search` | session_memory.rs |
**不调用的下层 API**(明确边界):
-`ConversationMemory`(由上层独立 task 管理)
-`KnowledgeStore`(由上层独立 task 管理)
-`McpClient`(已由 `ToolRegistry` 包装)
-`StreamEvents::submit_stream`v1 暂不暴露流式 `submit_turn`v0.2 再说)
- ❌ 多 context 切换管理(v0.2+ 实现,Phase 4 只预留 `SessionMemory` 桥接通道)
-`"session_memory_set"` 等 session memory tool 自动注册(v0.2+ 可选)
## 4. 实施计划
Phase 4 拆分为三个独立子阶段:**Phase 4a(核心胶水层)** → **Phase 4b(任务执行)****Phase 4c(会话级记忆)**。每个子阶段独立交付、独立验证,4b 与 4c 无相互依赖。
### 4.1 文件清单
#### 新增文件(9 个)
```
src/agent.rs # [4a] 模块根 + pub use 重导出
src/agent/agent.rs # [4a] Agent trait
src/agent/runtime.rs # [4a] RuntimeBundle + AgentConfig(不含 session_memory_backend
src/agent/session.rs # [4a] AgentSessionsubmit_turn + 内联 session_data HashMap
src/agent/task.rs # [4a] Plan / Step / StepStatus 纯数据 / [4b] TaskAgent + PlanParser + JsonPlanParser
src/agent/builder.rs # [4a] AgentBuilder(不含 session_memory_backend
src/agent/error.rs # [4a] AgentError(不含 PlanParse 变体)/ [4b] 补充 PlanParse 变体
src/agent/session_memory.rs # [4c] SessionMemory(基于 MemoryStore
```
#### 修改文件(2 个)
```
src/lib.rs # [4a] + pub mod agent;
src/llm/hooks.rs # [4a] + 2 事件(OnTurnStart/OnTurnEnd+ 1 字段(turn_index
# [4b] + 1 事件(OnPlanStepComplete+ 1 字段(plan_step_index
```
#### 关联文档(已完成)
```
docs/note-agent-harness-references.md # ✅ 已存在
docs/note-agent-runtime-design.md # ✅ 已存在(与本文件配套)
docs/7-agent-runtime.md # ✅ 本文件
```
---
### 4.2 Phase 4a — 核心胶水层(最小 Agent Runtime
**范围**Agent trait + AgentSession + submit_turn(内联 HashMap session_data+ RuntimeBundle/AgentBuilder + AgentError + Plan/Step 纯数据 + hooks 扩展(OnTurnStart/OnTurnEnd
**任务拆解**
| 顺序 | 任务 | 涉及文件 | 验证 |
|------|------|---------|------|
| a1 | 修改 `llm/hooks.rs` 追加 OnTurnStart / OnTurnEnd + turn_index 字段 | `src/llm/hooks.rs` | `cargo build` 通过;Phase 0 测试不挂 |
| a2 | 新建 `agent/error.rs` 定义 `AgentError`(不含 PlanParse 变体) | `src/agent/error.rs` | `cargo build` 通过 |
| a3 | 新建 `agent/agent.rs` 定义 `Agent` trait | `src/agent/agent.rs` | `cargo build` 通过 |
| a4 | 新建 `agent/runtime.rs` 定义 `RuntimeBundle` + `AgentConfig`(不含 session_memory_backend | `src/agent/runtime.rs` | `cargo build` 通过 |
| a5 | 新建 `agent/builder.rs` 定义 `AgentBuilder`(不含 session_memory_backend 方法) | `src/agent/builder.rs` | `cargo build` 通过 |
| a6 | 新建 `agent/session.rs` 定义 `AgentSession` + `submit_turn`(内联 `HashMap<String,String>` 做 session_data,不引 MemoryStore | `src/agent/session.rs` | `cargo build` 通过 |
| a7 | 新建 `agent/task.rs` 定义 `Plan` / `Step` / `StepStatus` 纯数据结构(不含 TaskAgent trait,不含 PlanParser | `src/agent/task.rs` | `cargo build` 通过 |
| a8 | 新建 `src/agent.rs` 模块根 + `pub use` 重导出 + 修改 `lib.rs` | `src/agent.rs` + `src/lib.rs` | `cargo build` 通过 |
| a9 | 编写烟雾测试 3-4 个(Agent trait 可装配 / RuntimeBundle 可构造 / submit_turn 跑通 mock / Plan 数据结构) | `src/agent/*.rs` 内联 | `cargo test` 通过 |
| a10 | 完整 `cargo test` 跑全量回归 + roadmap.md 状态更新 | — | 所有已有测试不挂 |
**依赖关系**
```
hooks扩展 (a1) ──┐
├──► agent/error.rs (a2) ──► agent/agent.rs (a3)
│ │
│ ▼
│ agent/runtime.rs (a4)
│ │
│ ▼
│ agent/builder.rs (a5)
│ │
│ ▼
│ agent/session.rs (a6)
│ │
│ ▼
│ agent/task.rs (a7) [纯数据]
│ │
└──────────────────► src/agent.rs + lib.rs (a8)
cargo test (a9 → a10)
```
---
### 4.3 Phase 4b — 任务执行
**范围**TaskAgent trait + PlanParser trait + JsonPlanParser 参考实现 + OnPlanStepComplete hook + AgentError PlanParse 变体
**前置条件**Phase 4a 已完成并交付。
**任务拆解**
| 顺序 | 任务 | 涉及文件 | 验证 |
|------|------|---------|------|
| b1 | 修改 `llm/hooks.rs` 追加 OnPlanStepComplete + plan_step_index 字段 | `src/llm/hooks.rs` | `cargo build` 通过;Phase 0 + 4a 测试不挂 |
| b2 | `agent/error.rs` 追加 PlanParse 变体 | `src/agent/error.rs` | `cargo build` 通过 |
| b3 | `agent/task.rs` 追加 `TaskAgent` trait + `PlanParser` trait + `JsonPlanParser` 参考实现 | `src/agent/task.rs` | `cargo build` 通过 |
| b4 | 更新 `agent.rs` 模块根重导出(如有新增公开类型) | `src/agent.rs` | `cargo build` 通过 |
| b5 | 编写烟雾测试 2-3 个(TaskAgent mock 执行 / JsonPlanParser 解析 / PlanParse 错误) | `src/agent/task.rs` 内联 | `cargo test` 通过 |
| b6 | 完整 `cargo test` 跑全量回归 + roadmap.md 状态更新 | — | 所有已有测试不挂 |
**依赖关系**
```
hooks扩展 (b1) ──┐
├──► error.rs 追加 (b2) ──► task.rs 追加 (b3)
agent.rs 更新 (b4)
cargo test (b5 → b6)
```
---
### 4.4 Phase 4c — 会话级记忆
**范围**SessionMemory struct(基于 MemoryStore+ RuntimeBundle/Build 扩展 session_memory_backend + AgentSession 接入(替换内联 HashMap
**前置条件**:Phase 4a 已完成并交付(可在 4b 之前、之后或并行实施)。
**任务拆解**
| 顺序 | 任务 | 涉及文件 | 验证 |
|------|------|---------|------|
| c1 | 新建 `agent/session_memory.rs` 定义 `SessionMemory`(基于 `MemoryStore`namespace 隔离) | `src/agent/session_memory.rs` | `cargo build` 通过 |
| c2 | `agent/runtime.rs` 追加 `session_memory_backend` 字段到 `RuntimeBundle` | `src/agent/runtime.rs` | `cargo build` 通过 |
| c3 | `agent/builder.rs` 追加 `.session_memory_backend()` 方法 | `src/agent/builder.rs` | `cargo build` 通过 |
| c4 | `agent/session.rs` 替换内联 HashMap 为完整 `SessionMemory` + 更新模块根重导出 | `src/agent/session.rs` + `src/agent.rs` | `cargo build` 通过 |
| c5 | 编写烟雾测试 2-3 个(SessionMemory set/get/snapshot 基于 InMemoryStore | `src/agent/session_memory.rs` 内联 | `cargo test` 通过 |
| c6 | 完整 `cargo test` 跑全量回归 + roadmap.md 状态更新 | — | 所有已有测试不挂 |
**依赖关系**
```
session_memory.rs (c1) ──► runtime.rs 追加 (c2) ──► builder.rs 追加 (c3)
session.rs 修改 (c4)
cargo test (c5 → c6)
```
---
### 4.5 预估工作量(按子阶段)
| 子阶段 | 文件 | 行数 | 说明 |
|--------|------|------|------|
| **Phase 4a** | hooks 扩展(2 事件 + 1 字段) | ~10 | 追加变体 + 字段 + 文档 |
| | agent/error.rs | ~40 | AgentError 枚举 + From + is_recoverable |
| | agent/agent.rs | ~30 | Agent trait + docs |
| | agent/runtime.rs | ~60 | RuntimeBundle + AgentConfig |
| | agent/builder.rs | ~60 | 链式构造 + build 校验 |
| | agent/session.rs | ~100 | AgentSession + submit_turn + 内联 HashMap |
| | agent/task.rs(纯数据) | ~40 | Plan / Step / StepStatus |
| | src/agent.rs + lib.rs | ~20 | 模块根 + 导出 |
| | 烟雾测试 | ~80 | 3-4 个测试 |
| | **小计** | **~440** | **核心胶水层** |
| **Phase 4b** | hooks 扩展(1 事件 + 1 字段) | ~5 | OnPlanStepComplete + plan_step_index |
| | error.rs 追加 PlanParse | ~5 | 1 个变体 |
| | task.rs 追加(TaskAgent + PlanParser + JsonPlanParser | ~130 | trait + 参考实现 + docs |
| | 烟雾测试 | ~60 | 2-3 个测试 |
| | **小计** | **~200** | **任务执行** |
| **Phase 4c** | session_memory.rs | ~40 | 5 个方法 + docs |
| | runtime.rs / builder.rs / session.rs 修改 | ~35 | 追加字段 + setter + 替换 HashMap |
| | 烟雾测试 | ~40 | 2-3 个测试 |
| | **小计** | **~115** | **会话级记忆** |
| **合计** | | **~755** | 与原始预估 ~800 基本持平 |
## 5. 风险评估
### 5.1 抽象化边界(核心风险)
**风险描述**Phase 4 容易"过度抽象"——参考了 OpenHarness / Hermes 后,倾向于把它们的核心能力都搬到 Rust core 库里。
**缓解措施**
- 严格遵循 §1.3 的 7 条设计原则
- 每次添加新 trait / struct 前,先问"这属于 core 库职责吗?"
- 业务能力(Plan 拆解、多 Agent 协同、技能加载)一律走 trait 注入或 v0.2+ 延后
### 5.2 对 Phase 0-3 的侵入风险
**风险描述**:为实现 Phase 4 需修改 `src/llm/hooks.rs`,可能破坏 Phase 0 的现有测试。
**缓解措施**
- 只追加 enum 变体和 `Option<T>` 字段(NF9
- 顺序:先跑 `cargo test` 确认 Phase 0 测试不挂,再开始 Phase 4
- 详细回归验证:实施完毕后跑全量 `cargo test`
### 5.3 参考项目语言差异
**风险描述**OpenClaw / Hermes / OpenHarness 均为 Python/TypeScriptOpenHuman 虽是 Rust + Tauri 但定位是桌面应用。直接照搬接口形状可能导致 Rust 借用检查问题、async 复杂度增加。
**缓解措施**
- §1.3 明确"借鉴不照搬"
- 反模式列表(见 `docs/note-agent-harness-references.md` §6)作为排除项
- 接口设计优先考虑 Rust 惯例(`Arc<dyn Trait>` / `async fn` / `Result<T, E>`
### 5.4 trait 设计的稳定性风险
**风险描述**Phase 4 是 v0.1 的第一个"复杂 trait 集合",如果 trait 形状不稳定,v0.2+ 添加新能力时会 breaking。
**缓解措施**
- §3.2 的所有 trait / struct 在 `docs/note-agent-runtime-design.md` §4 已固化草案
- 实施时如需调整,应先更新决策记录再改代码
- 预留扩展点:`Agent::tool_definitions` 的默认实现可被子 trait 覆盖
### 5.5 实施进度风险
**风险描述**:拆为 3 个子阶段后每个阶段任务量降低(4a 约 440 行、4b 约 200 行、4c 约 115 行),但阶段间衔接(4b/4c 对 4a 的依赖)可能产生等待。
**缓解措施**
- 每个子阶段独立验证,完成即交付,不阻塞后续阶段
- 4b 和 4c 无相互依赖,可并行开工
- 烟雾测试只验证"能跑通"不验证"业务正确"——避免陷入业务循环的细节
- 必要时先做 `MockProvider`(Phase 0 已有模式),不依赖真实 LLM
## 6. 验收标准
### 6.1 通用代码验收(每个子阶段必须满足)
- [ ] `cargo build --release` 0 错误 0 警告(clippy
- [ ] `cargo test` 所有已有测试 + 本阶段新增测试全部通过
- [ ] `cargo doc --no-deps` 所有公开 API 有 `///` 文档注释
- [ ] `src/llm/hooks.rs` 仅追加(不修改现有变体或字段)
### 6.2 Phase 4a 验收
#### 6.2a 代码验收
- [ ] 新增代码 ~440 行(含测试 + 文档注释),与 §4.5 预估一致
- [ ] `src/lib.rs` 新增一行 `pub mod agent;`
- [ ] 新增文件:`agent.rs` / `agent/agent.rs` / `agent/runtime.rs` / `agent/builder.rs` / `agent/session.rs` / `agent/task.rs` / `agent/error.rs`(共 7 个文件,不含 `agent/builder.rs` 之外的 builder 则 7 个)
#### 6.2b 接口验收
- [ ] `Agent` trait 包含 `name` / `system_prompt` / `tool_definitions` 三个方法
- [ ] `RuntimeBundle` 包含 5 个字段:provider / tool_registry / hook_executor / memory_store? / retriever? / config(不含 session_memory_backend
- [ ] `AgentBuilder` 提供 5 个 setter(不含 session_memory_backend+ `build()` 校验
- [ ] `AgentSession``Arc<dyn Agent>` 而非 `agent_name: String`
- [ ] `AgentSession::submit_turn` 实现约 30 行,含 OnTurnStart/End hook 触发
- [ ] `AgentSession` 用内联 `HashMap<String, String>` 做 session_data(不引 `MemoryStore`
- [ ] `Plan` / `Step` / `StepStatus` 纯数据结构存在,状态机正确
- [ ] `AgentError` 聚合 6 个变体:Llm / Tool / Memory / HookBlocked / LimitExceeded / Config / Other(不含 PlanParse
- [ ] `AgentError::is_recoverable()` 对各变体返回正确分类
- [ ] `HookEvent` 新增 2 个变体:`OnTurnStart` / `OnTurnEnd`
- [ ] `HookContext` 新增 1 个 `Option` 字段:`turn_index`
#### 6.2c 测试验收
- [ ] **测试 1**`Agent` trait 可实现 + `RuntimeBundle` 可构造(builder 链式调用)
- [ ] **测试 2**`AgentSession::submit_turn` 跑通 mock providerPhase 0 `MockProvider` 模式)
- [ ] **测试 3**`Plan` / `Step` / `StepStatus` 状态机转换正确
- [ ] **测试 4(可选)**session_data set/get 基本读写
#### 6.2d 行为验收
- [ ] `AgentSession::submit_turn` 不持有 `ConversationMemory`grep 验证无 `use crate::memory::ConversationMemory`
- [ ] `AgentSession``Arc<dyn Agent>`,可从 agent 获取 `system_prompt()` / `tool_definitions()`
- [ ] `RuntimeBundle::new``retriever``Some` 时自动注册 `"retrieve"` tool
- [ ] `AgentBuilder::build` 在必填字段缺失时返回 `AgentError::Config`(而非 panic
---
### 6.3 Phase 4b 验收
#### 6.3a 代码验收
- [ ] 追加代码 ~200 行(增量,在 Phase 4a 基础上),与 §4.5 预估一致
- [ ] `src/llm/hooks.rs` 追加 OnPlanStepComplete + plan_step_index(不修改 Phase 4a 新增内容)
#### 6.3b 接口验收
- [ ] `TaskAgent` trait 提供双入口 `run(goal)` + `execute_plan(plan)`
- [ ] `PlanParser` trait 可注入,`JsonPlanParser` 参考实现基于 `serde_json`~20 行)
- [ ] `AgentError` 追加 PlanParse 变体(共 7 个变体)
- [ ] `HookEvent` 追加 1 个变体:`OnPlanStepComplete`
- [ ] `HookContext` 追加 1 个 `Option` 字段:`plan_step_index`
#### 6.3c 测试验收
- [ ] **测试 1**`TaskAgent::execute_plan` 跑通 mock provider
- [ ] **测试 2**`JsonPlanParser::parse` 能解析合法 JSON,失败时返回 `AgentError::PlanParse`
- [ ] **测试 3(可选)**`OnPlanStepComplete` hook 触发正确
---
### 6.4 Phase 4c 验收
#### 6.4a 代码验收
- [ ] 追加代码 ~115 行(增量,在 Phase 4a 基础上),与 §4.5 预估一致
- [ ] 新增文件:`agent/session_memory.rs`
#### 6.4b 接口验收
- [ ] `SessionMemory` 包含 5 个方法(set / get / snapshot / remove / clear),基于 `MemoryStore` 实现
- [ ] `SessionMemory::snapshot` 返回 `<session-context>` 标签包裹的格式化文本
- [ ] `RuntimeBundle` 追加 `session_memory_backend: Option<Arc<dyn MemoryStore>>` 字段
- [ ] `AgentBuilder` 追加 `.session_memory_backend()` setter
- [ ] `AgentSession` 替换内联 HashMap 为完整 `SessionMemory`,含 `session_memory: SessionMemory` 字段
- [ ] `SessionMemory``session_memory_backend` 未传入时自动使用 `InMemoryStore` 兜底
#### 6.4c 测试验收
- [ ] **测试 1**`SessionMemory` set / get / snapshot 基本读写(基于 `InMemoryStore`
- [ ] **测试 2**session_data 内联 HashMap ↔ SessionMemory 替换后 submit_turn 行为不变
---
### 6.5 文档验收
- [ ] `docs/7-agent-runtime.md`(本文件)完整,6 段式结构齐备
- [ ] `docs/note-agent-runtime-design.md` 与本文件互相引用一致
- [ ] `docs/note-agent-harness-references.md` 与本文件互相引用一致
- [ ] `docs/roadmap.md` 各子阶段状态按阶段翻转
### 6.6 风险验收
- [ ] 5.1 抽象化边界:交付物列表中**不包含** Multi-Agent / Skills / TUI / Gateway 等应用层能力
- [ ] 5.2 Phase 0-3 侵入:`git diff` 显示 `src/llm/hooks.rs` 仅追加
- [ ] 5.3 语言差异:trait 形状符合 Rust 惯例(无 Python 风格的复杂继承)
- [ ] 5.4 trait 稳定性:决策记录与最终代码一致
- [ ] 5.5 实施进度:每个子阶段实际工作量与 §4.5 预估偏差 < 30%
## 7. 一句话总结
> **Phase 4 = 3 个子阶段:4a(核心胶水层:Agent + AgentSession + submit_turn + RuntimeBundle + Plan/Step 纯数据,~440 行)→ 4b(任务执行:TaskAgent + PlanParser/JsonPlanParser~200 行)→ 4c(会话级记忆:SessionMemory + 接入,~115 行),合计 ~755 行,分步交付、逐段验证,把 Phase 0-3 已有能力"装配"成"智能体"的概念。**