Compare commits

..

8 Commits

Author SHA1 Message Date
徐涛 829be90d19 docs: 新增示例程序方案文档 2026-06-11 22:49:27 +08:00
徐涛 ce1f1aaca0 feat(agent): 实现 Phase 4c 会话级记忆功能
- 新增 `SessionMemory` 结构体,基于 `MemoryStore` 按 namespace 隔离键值数据
- `AgentBuilder` 增加 `session_memory_backend` 配置入口
- `RuntimeBundle` 透传 `session_memory_backend` 字段
- `AgentSession` 将内联 `HashMap` 替换为完整的 `SessionMemory`,`set_session_data` 和 `get_session_data` 改为异步方法
- 新增 3 个内联测试,全量测试从 113 增至 116,clippy 0 警告
2026-06-11 22:14:15 +08:00
徐涛 4de7db0b2c docs(roadmap): 更新 Phase 4b 状态为已完成 2026-06-11 21:57:10 +08:00
徐涛 2b189880a9 feat(agent): 实现 Agent Runtime 核心胶水层 (Phase 4a)
- 添加 Agent trait、AgentSession、RuntimeBundle、AgentBuilder
- 添加 Plan/Step/StepStatus 任务规划数据结构
- 添加 AgentError 统一错误类型(聚合 LlmError/ToolError/MemoryError)
- 实现 submit_turn 单轮对话流程(含 hook 触发与 cost 累计)
- 扩展 LlmCycle 支持 Arc<dyn LlmProvider>
- 扩展 HookEvent 添加 OnTurnStart/OnTurnEnd
- 更新 roadmap 状态
2026-06-11 21:45:28 +08:00
徐涛 59ec0f5597 docs(roadmap): 将 Phase 4 拆分为 4a/4b/4c 三个独立子阶段 2026-06-10 22:38:32 +08:00
徐涛 be595a6771 docs(agent-runtime): 补充 SessionMemory 设计与多 context 切换备忘
- 在方案文档中新增 `SessionMemory` 作为会话级记忆桥接组件
- 将 AgentSession 的 `agent_name: String` 改为 `agent: Arc<dyn Agent>`
- RuntimeBundle 新增 `session_memory_backend` 可选字段
- 新增 `docs/note-context-switch-design.md` 记录多 context 切换方案
- 更新 Roadmap 状态并补充 Phase 4 范围界定
- Phase 4 仅实现 SessionMemory 数据结构与 API,ContextManager 延后至 v0.2+
2026-06-10 22:22:18 +08:00
徐涛 75f8736931 feat(docs): 添加 OpenCode 借鉴的 Agent 切换与 Prompt 动态扩展项 2026-06-10 08:35:56 +08:00
徐涛 b539f37eeb docs: 添加 OpenCode Agent 切换机制调研笔记 2026-06-10 06:52:24 +08:00
16 changed files with 2658 additions and 141 deletions
+346 -109
View File
@@ -21,14 +21,33 @@ AG Core 已完成 Phase 0LLM 调用周期)、Phase 1(提示词工程)
### 1.2 目标
Phase 4 目标是提供一个**薄胶水层 + 一组 trait 抽象**,让上层应用可以基于 AG Core 构建多轮对话、任务规划等智能体行为。具体包括
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 + 状态)
- **`TaskAgent` trait** — 任务型智能体的"规划/执行"抽象
- **`RuntimeBundle`** — 显式依赖注入容器,集中管理 provider/registry/hook/memory 等依赖
- **`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 设计原则
@@ -42,6 +61,7 @@ Phase 4 严格遵循以下原则,所有范围决策都基于这些原则推导
| **实体/会话分离** | 同一角色可被多 session 复用 | `Agent` + `AgentSession` 两层模型 |
| **记忆弱引用** | 记忆是"被动能力",不内嵌循环 | `memory_store: Option<Arc<dyn MemoryStore>>` 弱引用 |
| **业务可注入** | Plan 拆解是业务能力,不在 core 库实现 | 暴露 `PlanParser` trait,上层注入 |
| **会话级记忆** | session 内共享、context 间桥接,不是持久层也不是对话历史 | `SessionMemory` 基于 `MemoryStore`,按 session_id 命名空间隔离 |
| **借鉴不照搬** | 4 个参考项目均非 Rust 实现 | 只取架构模式,不抄实现细节 |
### 1.4 与已完成的 Phase 关系
@@ -54,7 +74,9 @@ Phase 3 (L2) ── MemoryStore / ConversationMemory / KnowledgeStore / Me
│ 复用
Phase 4 (L1→L2) ── Agent trait + AgentSession + TaskAgent + RuntimeBundle(胶水层)
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 范围)
```
@@ -65,23 +87,31 @@ Phase 4 (L1→L2) ── Agent trait + AgentSession + TaskAgent + RuntimeBund
### 2.1 功能需求
| ID | 需求 | 优先级 | 说明 |
|----|------|--------|------|
| F1 | `Agent` trait 抽象 | P0 | 角色定义:name / system_prompt / 工具集 |
| F2 | `AgentSession` 会话实例 | P0 | 绑定 session_id、bundle、turn_index、cost_so_far |
| F3 | `submit_turn()` 最小 reference impl | P0 | 组装 LlmCycle → submit → 累计 cost30 行 |
| F4 | `TaskAgent::run(goal)` 自主式入口 | P0 | 内部用 LLM 拆 Plan,再调用 `execute_plan` |
| F5 | `TaskAgent::execute_plan(plan)` 外部驱动式入口 | P0 | 用户预定义 Plan,逐步执行 |
| F6 | `Plan` / `Step` / `StepStatus` 数据结构 | P0 | 含 Pending / Running / Completed / Failed / Skipped 状态机 |
| F7 | `PlanParser` trait + `JsonPlanParser` 参考实现 | P0 | 注入式,上层可替换 |
| F8 | `RuntimeBundle` 依赖注入容器 | P0 | 聚合 provider/registry/hook/memory/retriever/config |
| F9 | `AgentBuilder` 链式构造 | P0 | 构建 `RuntimeBundle`,retriever 存在时自动注册为 tool |
| F10 | `AgentError` 统一错误类型 | P0 | 聚合 LlmError / ToolError / MemoryError,含 `is_recoverable()` |
| F11 | Hook 事件扩展:OnTurnStart / OnTurnEnd / OnPlanStepComplete | P0 | 在 `llm/hooks.rs` 中追加 3 个事件 + 上下文扩展 2 个字段 |
| F12 | 烟雾测试 2-3 个 | P0 | trait 可装配 / RuntimeBundle 可构造 / `submit_turn` 跑通 mock |
| F13 | `lib.rs` 导出 `pub mod agent;` | P0 | 一行 |
| F14 | 方案文档(本文件)+ 决策记录 | P0 | 已完成 |
| F15 | Roadmap 状态翻转 | P0 | 实施完成后做 |
| 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 非功能需求
@@ -190,6 +220,7 @@ pub struct RuntimeBundle {
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,
}
```
@@ -198,6 +229,7 @@ pub struct RuntimeBundle {
- 所有运行时依赖**显式打包**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
@@ -205,10 +237,11 @@ pub struct RuntimeBundle {
```rust
pub struct AgentSession {
pub session_id: String,
pub agent_name: String,
pub agent: Arc<dyn Agent>,
bundle: Arc<RuntimeBundle>,
turn_index: u32,
cost_so_far: CostTracker,
session_memory: SessionMemory,
}
impl AgentSession {
@@ -229,6 +262,8 @@ impl AgentSession {
```
**设计意图**
- `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`
@@ -324,6 +359,7 @@ impl AgentBuilder {
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>;
}
@@ -332,7 +368,74 @@ impl AgentBuilder {
**设计意图**
- `AgentBuilder` 是**唯一**的 `RuntimeBundle` 构造入口
- 必填字段在 `build()` 时校验(`provider` / `tool_registry` / `hook_executor` 不可缺)
- `memory_store` / `retriever` 选填,对应 §3.2.2 的"无记忆模式"
- `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 状态机
@@ -440,38 +543,44 @@ pub struct HookContext {
| `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 文件清单
#### 新增文件(7 个)
#### 新增文件(9 个)
```
src/agent.rs # 模块根 + pub use 重导出
src/agent/agent.rs # Agent trait
src/agent/runtime.rs # RuntimeBundle + AgentConfig
src/agent/session.rs # AgentSessionsubmit_turn reference impl
src/agent/task.rs # TaskAgent trait + Plan/Step + PlanParser + JsonPlanParser
src/agent/builder.rs # AgentBuilder
src/agent/error.rs # AgentError
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
```
#### 修改文件(3 个)
#### 修改文件(2 个)
```
src/lib.rs # + pub mod agent;
src/llm/hooks.rs # + 3 个事件变体 + 2 个 HookContext 字段
docs/roadmap.md # Phase 4 状态 ❌ 缺失 → ✅
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 # ✅ 已存在
@@ -479,67 +588,140 @@ docs/note-agent-runtime-design.md # ✅ 已存在(与本文件配套)
docs/7-agent-runtime.md # ✅ 本文件
```
### 4.2 任务拆解(按依赖顺序)
---
### 4.2 Phase 4a — 核心胶水层(最小 Agent Runtime
**范围**Agent trait + AgentSession + submit_turn(内联 HashMap session_data+ RuntimeBundle/AgentBuilder + AgentError + Plan/Step 纯数据 + hooks 扩展(OnTurnStart/OnTurnEnd
**任务拆解**
| 顺序 | 任务 | 涉及文件 | 验证 |
|------|------|---------|------|
| 1 | 修改 `llm/hooks.rs` 追加 3 个事件 + 2 个字段 | `src/llm/hooks.rs` | `cargo build` 通过;现有测试不挂 |
| 2 | 新建 `agent/error.rs` 定义 `AgentError` | `src/agent/error.rs` | `cargo build` 通过 |
| 3 | 新建 `agent/agent.rs` 定义 `Agent` trait | `src/agent/agent.rs` | `cargo build` 通过 |
| 4 | 新建 `agent/runtime.rs` 定义 `RuntimeBundle` + `AgentConfig` | `src/agent/runtime.rs` | `cargo build` 通过 |
| 5 | 新建 `agent/builder.rs` 定义 `AgentBuilder` | `src/agent/builder.rs` | `cargo build` 通过 |
| 6 | 新建 `agent/session.rs` 定义 `AgentSession` + `submit_turn` | `src/agent/session.rs` | `cargo build` 通过 |
| 7 | 新建 `agent/task.rs` 定义 `TaskAgent` + `Plan` / `Step` / `PlanParser` / `JsonPlanParser` | `src/agent/task.rs` | `cargo build` 通过 |
| 8 | 新建 `src/agent.rs` 模块根 + `pub use` 重导出 | `src/agent.rs` | `cargo build` 通过 |
| 9 | 修改 `lib.rs` 导出 `pub mod agent;` | `src/lib.rs` | `cargo build` 通过 |
| 10 | 编写 2-3 个烟雾测试 | `src/agent/*.rs` 内联 | `cargo test` 通过 |
| 11 | 更新 `roadmap.md` 状态翻转 | `docs/roadmap.md` | 文档 review |
| 12 | 完整 `cargo test` 跑全量回归 | — | 所有已有测试不挂 |
| 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 状态更新 | — | 所有已有测试不挂 |
### 4.3 依赖关系
**依赖关系**
```
hooks.rs (1) ──┐
├──► agent/error.rs (2) ──► agent/agent.rs (3)
hooks扩展 (a1) ──┐
├──► agent/error.rs (a2) ──► agent/agent.rs (a3)
│ │
│ ▼
agent/runtime.rs (4)
agent/runtime.rs (a4)
│ │
│ ▼
agent/builder.rs (5)
agent/builder.rs (a5)
│ │
│ ▼
agent/session.rs (6)
agent/session.rs (a6)
│ │
│ ▼
└─────────────────► agent/task.rs (7)
agent/task.rs (a7) [纯数据]
│ │
└──────────────────► src/agent.rs + lib.rs (a8)
src/agent.rs (8)
src/lib.rs (9)
cargo test (10)
roadmap.md (11)
回归 (12)
cargo test (a9 → a10)
```
### 4.4 预估工作量
---
| 阶段 | 行数 | 说明 |
|------|------|------|
| 1hooks 扩展) | ~15 | 3 个变体 + 2 个字段 + 文档 |
| 2-77 个 agent 文件) | ~600 | 含 import + trait + struct + impl + 文档 |
| 8-9lib.rs + agent.rs 模块根) | ~20 | 主要是 pub use 重导出 |
| 10(烟雾测试) | ~100 | 2-3 个测试 |
| 11roadmap 同步) | ~5 | 状态翻转一行 |
| **合计** | **~740** | 与 `note-agent-runtime-design.md` §6 预估一致 |
### 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. 风险评估
@@ -581,67 +763,122 @@ hooks.rs (1) ──┐
### 5.5 实施进度风险
**风险描述**12 项交付物虽然不多,但 `submit_turn` 的 reference impl 需要在"LlmCycle 之上做正确组装",容易卡在细节
**风险描述**拆为 3 个子阶段后每个阶段任务量降低(4a 约 440 行、4b 约 200 行、4c 约 115 行),但阶段间衔接(4b/4c 对 4a 的依赖)可能产生等待
**缓解措施**
- 任务拆解(§4.2)按依赖顺序排好
- 每个子阶段独立验证,完成即交付,不阻塞后续阶段
- 4b 和 4c 无相互依赖,可并行开工
- 烟雾测试只验证"能跑通"不验证"业务正确"——避免陷入业务循环的细节
- 必要时先做 `MockProvider`(Phase 0 已有模式),不依赖真实 LLM
## 6. 验收标准
### 6.1 代码验收
### 6.1 通用代码验收(每个子阶段必须满足)
- [ ] `cargo build --release` 0 错误 0 警告(clippy
- [ ] `cargo test` 所有 Phase 0-3 已有测试 + Phase 4 新增测试全部通过
- [ ] `cargo test` 所有已有测试 + 本阶段新增测试全部通过
- [ ] `cargo doc --no-deps` 所有公开 API 有 `///` 文档注释
- [ ] 新增代码 700-750 行(含测试 + 文档注释),与 §4.4 预估一致
- [ ] `src/lib.rs` 新增一行 `pub mod agent;`
- [ ] `src/llm/hooks.rs` 仅追加(不修改现有变体或字段)
### 6.2 接口验收
### 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 接口验收
- [ ] 7 个新文件全部存在(§4.1
- [ ] `Agent` trait 包含 `name` / `system_prompt` / `tool_definitions` 三个方法
- [ ] `RuntimeBundle` 包含 6 个字段provider / tool_registry / hook_executor / memory_store? / retriever? / config
- [ ] `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 触发
- [ ] `TaskAgent` 提供双入口 `run` + `execute_plan`
- [ ] `JsonPlanParser` 实现约 20 行,基于 `serde_json`
- [ ] `AgentError` 聚合 8 个变体,含 `is_recoverable()`
- [ ] `AgentBuilder` 提供 6 个 setter + `build()` 校验
- [ ] `HookEvent` 新增 3 个变体:`OnTurnStart` / `OnTurnEnd` / `OnPlanStepComplete`
- [ ] `HookContext` 新增 2`Option` 字段:`turn_index` / `plan_step_index`
- [ ] `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.3 测试验收
至少 2-3 个烟雾测试通过:
#### 6.2c 测试验收
- [ ] **测试 1**`Agent` trait 可实现 + `RuntimeBundle` 可构造(builder 链式调用)
- [ ] **测试 2**`AgentSession::submit_turn` 跑通 mock providerPhase 0 `MockProvider` 模式)
- [ ] **测试 3(可选)**`JsonPlanParser::parse` 能解析合法 JSON,失败时返回 `AgentError::PlanParse`
- [ ] **测试 3**`Plan` / `Step` / `StepStatus` 状态机转换正确
- [ ] **测试 4(可选)**session_data set/get 基本读写
### 6.4 文档验收
- [ ] `docs/7-agent-runtime.md`(本文件)完整、6 段式结构齐备
- [ ] `docs/note-agent-runtime-design.md` 与本文件互相引用一致
- [ ] `docs/note-agent-harness-references.md` 与本文件互相引用一致
- [ ] `docs/roadmap.md` Phase 4 状态从 ❌ 缺失 改为 ✅,交付物清单更新
### 6.5 行为验收(人工 review
#### 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
- [ ] `AgentError::is_recoverable()` 对各变体返回正确分类
---
### 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 抽象化边界:trailt 列表中**不包含** Multi-Agent / Skills / TUI / Gateway 等应用层能力
- [ ] 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.4 预估偏差 < 30%
- [ ] 5.5 实施进度:每个子阶段实际工作量与 §4.5 预估偏差 < 30%
## 7. 一句话总结
> **Phase 4 = 1 traitAgent+ 1 个容器(RuntimeBundle+ 1 个会话(AgentSession+ 1 个任务抽象(TaskAgent+ 4 个辅助组件(Builder / Error / PlanParser / Hook 扩展),约 740 行代码,把 Phase 0-3 已有能力"装配"成"智能体"的概念。**
> **Phase 4 = 3子阶段:4a(核心胶水层:Agent + AgentSession + submit_turn + RuntimeBundle + Plan/Step 纯数据,~440 行)→ 4b(任务执行:TaskAgent + PlanParser/JsonPlanParser~200 行)→ 4c(会话级记忆:SessionMemory + 接入,~115 行),合计 ~755 行,分步交付、逐段验证,把 Phase 0-3 已有能力"装配"成"智能体"的概念。**
+434
View File
@@ -0,0 +1,434 @@
# 示例程序新增方案
> 作者:Proposal Agent
> 日期:2026-06-11
> 对应版本:agcore v0.1
## 背景与目标
### 问题
当前 `examples/` 目录下只有一个 `simple_visit.rs`,仅演示了 `LlmCycle::submit()` 的基本 LLM 调用(Phase 0),且依赖真实 API key 才能运行。v0.1 已实现的全部 7 个 Phase 的能力(Phase 0~4c)缺乏可运行、可独立验证的示例展示。
### 目标
1. **覆盖全 Phase** — 每个 Phase 核心能力至少有一个示例
2. **可离线运行** — 优先选用 MockProvider 和本地逻辑,不强制依赖 API key
3. **真实使用模式** — 示例反映库的预期使用方式(Builder 模式、trait 实现、? 错误传播)
4. **验收辅助** — 示例跑通 = 对应模块公共 API 可用且装配正确
### 非目标
- 不替代单元测试的边界覆盖(内联测试仍负责边界条件)
- 不引入第三方依赖(示例只使用 `agcore` 公开 API
- 不追求 UI 或交互式输入
---
## 当前状态分析
```text
examples/
└── simple_visit.rs # 仅 Phase 0 基础调用,需 API key
```
### 现有示例覆盖缺口
| Phase | 模块 | 示例覆盖 | 缺口 |
|-------|------|---------|------|
| Phase 0 | LLM 调用周期 | `simple_visit.rs` | 流式事件、重试逻辑、Auto-compaction 未演示 |
| Phase 1 | 提示词工程 | ❌ | 模板变量插值、消息组合、条件渲染 |
| Phase 2 | 工具系统 | ❌ | 自定义工具注册、并行调用、权限检查 |
| Phase 3 | 记忆系统 | ❌ | 对话记忆滑动窗口、知识页面存储、关键词检索 |
| Phase 4a | 核心胶水层 | ❌ | Agent/AgentSession/RuntimeBundle/AgentBuilder 装配 |
| Phase 4b | 任务执行 | ❌ | PlanParser/Step 状态机/TaskAgent |
| Phase 4c | 会话级记忆 | ❌ | SessionMemory set/get/snapshot |
---
## 设计方案
### 总体架构
新增示例按三层优先级组织,每个示例为一个独立 `.rs` 文件,统一放在 `examples/` 目录下。
```
examples/
├── simple_visit.rs # [已有] 基本 LLM 调用(Phase 0
├── prompt_composer.rs # [新增] 提示词组合(Phase 1)🥇
├── custom_tool.rs # [新增] 自定义工具(Phase 2)🥇
├── agent_session_demo.rs # [新增] Agent 会话(Phase 4a+4c)🥇
├── task_agent_demo.rs # [新增] 任务规划(Phase 4b)🥇
├── conversation_memory_demo.rs # [新增] 对话记忆(Phase 3)🥈
├── knowledge_search_demo.rs # [新增] 知识检索(Phase 3)🥈
├── streaming_events_demo.rs # [新增] 流式事件(Phase 0)🥈
└── full_integration.rs # [新增] 全栈集成(Phase 全栈)🥉
```
### 详细设计
#### 🥇 示例:`prompt_composer.rs`Phase 1
**设计思路**:纯本地运行,不依赖任何外部服务。通过构造模板、组合消息来验证 Prompt Engineering 模块的公共 API。
**流程**
```
TemplateContext 构造 → PromptTemplate 填充变量 → PromptComposer 构建消息链 → 断言验证
```
**关键代码片段示意**
```rust
// 1. 构造模板
let mut registry = PromptTemplateRegistry::new();
registry.register(PromptTemplate::new("weather", "今日{location}天气:{condition},温度{temperature}"));
// 2. 填充变量
let template = registry.get("weather").unwrap();
let rendered = template.render(&TemplateContext::from([
("location", "北京"),
("condition", ""),
("temperature", "25°C"),
])?;
// 3. 组合消息
let composer = PromptComposer::new()
.system("你是一个天气助手")
.user(rendered)
.assistant(/* 可选历史 */);
let messages = composer.compose();
assert_eq!(messages.len(), 2);
```
**验证点**
- `TemplateContext` 变量插值正确
- `PromptComposer` 消息顺序正确
- `PromptError` 在缺失变量时正确返回
**新增代码量**:约 60 行
---
#### 🥇 示例:`custom_tool.rs`Phase 2
**设计思路**:实现一个模拟工具(如 `WeatherTool`),注册到 `ToolRegistry`,演示单次调用、并行调用、权限检查。
**流程**
```
实现 BaseTool → 注册到 ToolRegistry → invoke 单次 → invoke_all 并行 → PermissionChecker 白名单过滤
```
**关键代码片段示意**
```rust
// 1. 实现工具
struct WeatherTool;
#[async_trait]
impl BaseTool for WeatherTool {
fn name(&self) -> &str { "get_weather" }
fn parameters(&self) -> Value { json!({"type":"object","properties":{"city":{"type":"string"}}}) }
async fn execute(&self, args: Value, _ctx: &ToolContext) -> Result<Value, ToolError> {
Ok(json!({"city": args["city"], "temperature": 22, "condition": ""}))
}
}
// 2. 注册 + 调用
let mut registry = ToolRegistry::new();
registry.register(WeatherTool.into())?;
let result = registry.invoke("get_weather", json!({"city": "北京"})).await?;
// 3. 并行调用
let results = registry.invoke_all(vec![...], 30).await;
// 4. 权限检查
let checker = PermissionChecker::new(PermissionConfig::white_list(vec!["get_weather"]));
assert!(checker.check("get_weather").is_ok());
assert!(checker.check("delete_file").is_err());
```
**验证点**
- 工具注册/查找/调用完整链路
- 并行调用结果数正确
- 权限白名单/黑名单行为
- `ToolError::NotFound` 未注册工具
**新增代码量**:约 80 行
---
#### 🥇 示例:`agent_session_demo.rs`Phase 4a + 4c
**设计思路**:使用 `MockProvider` 模拟 LLM 响应,完整演示 `Agent → AgentBuilder → RuntimeBundle → AgentSession` 的装配流程及 `SessionMemory` 的读写。
**流程**
```
实现 Agent → AgentBuilder 构造 RuntimeBundle → AgentSession::new → submit_turn → session_data 读写 → snapshot 输出
```
**关键代码片段示意**
```rust
// 1. 定义 Agent
struct CalculatorAgent;
impl Agent for CalculatorAgent {
fn name(&self) -> &str { "calculator" }
fn system_prompt(&self) -> Option<&str> { Some("你是计算器助手") }
}
// 2. 装配 RuntimeBundle
let bundle = AgentBuilder::new()
.provider(Arc::new(mock_provider))
.tool_registry(Arc::new(tool_registry))
.hook_executor(Arc::new(hook_executor))
.build()?;
// 3. 创建会话
let mut session = AgentSession::new(Arc::new(CalculatorAgent), "session-1", Arc::new(bundle));
// 4. 提交对话
let response = session.submit_turn("1+1=?").await?;
// 5. SessionMemory 读写
session.set_session_data("last_result", "2").await?;
let result = session.get_session_data("last_result").await?;
println!("{}", session.session_memory().snapshot().await?);
```
**验证点**
- `AgentBuilder::build()` 必填字段校验
- `submit_turn` 流程完整(hook 触发、cost 累计、turn_index 递增)
- `SessionMemory` set/get/snapshot 正确
- 多个 session 间数据隔离
**新增代码量**:约 100 行
---
#### 🥇 示例:`task_agent_demo.rs`Phase 4b
**设计思路**:使用 `JsonPlanParser` 从预定义 JSON 解析 Plan,驱动 Step 状态机转换,观察状态单向流转。
**流程**
```
构造 JSON 输入 → JsonPlanParser::parse → Plan 数据结构 → 模拟 execute_plan → Step 状态变迁 → Hook 事件
```
**关键代码片段示意**
```rust
// 1. 解析 Plan
let parser = JsonPlanParser;
let input = r#"{"steps": [{"description": "查天气"}, {"description": "算结果"}]}"#;
let mut plan = parser.parse(input, "完成今日任务").await?;
// 2. 模拟 step 执行
assert!(plan.steps[0].status.is_pending());
step.status = StepStatus::Running;
step.status = StepStatus::Completed(response);
assert!(step.status.is_terminal());
// 3. 失败路径
step.status = StepStatus::Failed(AgentError::Other("API 不可用".into()));
assert!(step.status.is_terminal());
```
**验证点**
- 合法 JSON 解析正确
- 非法 JSON / 空步骤 / 缺字段返回 `AgentError::PlanParse`
- 状态机单向转换(Pending → Running → Completed/Failed/Skipped
- `is_terminal()` / `is_pending()` 语义正确
**新增代码量**:约 70 行
---
#### 🥈 示例:`conversation_memory_demo.rs`Phase 3
**设计思路**:演示 `ConversationMemory` 的多轮消息写入、滑动窗口淘汰、冷热分离存储。
**流程**
```
ConversationMemory::new → add_message × N → 触发窗口淘汰 → get_history 验证 → MemoryStore 持久化读取
```
**关键代码片段示意**
```rust
let config = ConversationMemoryConfig {
strategy: MemoryStrategy::SlidingWindow,
max_turns: 5,
..Default::default()
};
let mut memory = ConversationMemory::new(store, "session-1", config);
// 写入 10 条消息
for i in 0..10 {
memory.add_message(OpenaiChatMessage::user_text(format!("消息 {i}"))).await?;
}
// 验证窗口大小为 5
let history = memory.get_history().await?;
assert_eq!(history.len(), 5);
assert!(history[0].content().contains("消息 5"));
```
**验证点**
- 滑动窗口淘汰旧消息
- Full 策略保留全部消息
- 冷存储 `MemoryStore` 写入/读取正确
- `CompactConfig` 触发自动压缩
**新增代码量**:约 70 行
---
#### 🥈 示例:`knowledge_search_demo.rs`Phase 3
**设计思路**:演示 `KnowledgeStore` 页面存储 + `MemoryRetriever` 关键词检索与 Dice 系数评分。
**流程**
```
KnowledgeStore 创建页面 → MemoryRetriever::search → 评分排序结果输出 → 阈值过滤观察
```
**关键代码片段示意**
```rust
let store = KnowledgeStore::new(memory_store);
store.save_page("Rust 入门", "Rust 是一门系统编程语言...", vec!["rust", "编程"]).await?;
store.save_page("Python 简介", "Python 是动态类型语言...", vec!["python", "动态"]).await?;
let retriever = MemoryRetriever::new(store, RetrieverConfig::default());
let result = retriever.search("Rust 语言").await?;
for item in &result.items {
println!(" 页面: {} (评分: {:.2})", item.page.title, item.score);
assert!(item.score >= 0.0 && item.score <= 1.0);
}
```
**验证点**
- `KnowledgeStore` 页面存/取/搜索正确
- `TextOverlap` Dice 系数在 [0.0, 1.0] 范围内
- 停用词过滤正常
- 低于 `min_score` 的结果被过滤
**新增代码量**:约 60 行
---
#### 🥈 示例:`streaming_events_demo.rs`Phase 0 — 流式接口)
**设计思路**:调用 `LlmCycle::submit_stream()` 获取事件流,展示了语义事件的消费模式。可选使用 API key 或 MockProvider。
**流程**
```
LlmCycle::submit_stream → 事件循环 match StreamEvent → 输出类型/内容 → TurnComplete 收尾
```
**关键代码片段示意**
```rust
let mut cycle = LlmCycle::new(provider, config);
let mut stream = cycle.submit_stream("讲个笑话".into(), vec![]).await?;
use futures_util::StreamExt;
while let Some(event) = stream.next().await {
match event {
StreamEvent::AssistantTextDelta { text } => print!("{text}"),
StreamEvent::TurnComplete { reason } => println!("\n\n完成,原因: {reason:?}"),
StreamEvent::Error { message } => eprintln!("错误: {message}"),
_ => {} // 其他事件
}
}
```
**验证点**
- 流式链路完整(请求 → 事件 → 完成)
- 事件枚举覆盖所有变体
- 错误事件正确处理
**新增代码量**:约 80 行
---
#### 🥉 示例:`full_integration.rs`Phase 全栈集成)
**设计思路**:端到端演示,将 v0.1 所有模块装配为一个可运行的智能体。需真实 API key。
**流程**
```
创建 Agent(带 system prompt + 2 个工具)→ 注入 MemoryStore/Retriever → AgentSession → 多轮对话 → 知识检索 → SessionMemory 桥接 → 输出运行摘要
```
**验证点**
- Phase 4 "胶水层"真正将 Phase 0~3 粘合
- `submit_turn` 内部调用工具
- `ConversationMemory` 回写
- 全链路无类型/装配错误
**新增代码量**:约 150 行
---
## 实现计划
### 阶段一:第一梯队(优先级 🥇)
| 示例 | 预计代码量 | 可并行实施 |
|------|-----------|-----------|
| `prompt_composer.rs` | ~60 行 | ✅ 与 2/3 并行 |
| `custom_tool.rs` | ~80 行 | ✅ 与 1/3 并行 |
| `agent_session_demo.rs` | ~100 行 | ✅ 与 1/2 并行 |
| `task_agent_demo.rs` | ~70 行 | ✅ 与 1/2/3 并行 |
**验证标准**`cargo run --example <name>` 全部成功退出(code 0)。
### 阶段二:第二梯队(优先级 🥈)
| 示例 | 预计代码量 | 前置依赖 |
|------|-----------|---------|
| `conversation_memory_demo.rs` | ~70 行 | 无 |
| `knowledge_search_demo.rs` | ~60 行 | 无 |
| `streaming_events_demo.rs` | ~80 行 | 无 |
### 阶段三:第三梯队(优先级 🥉)
| 示例 | 预计代码量 | 前置依赖 |
|------|-----------|---------|
| `full_integration.rs` | ~150 行 | 需 `.env` 配置 API key |
### 总工作量估算
| 合计 | 代码行数 | 文件数 |
|------|---------|-------|
| 第一阶段 | ~310 行 | 4 个 |
| 第二阶段 | ~210 行 | 3 个 |
| 第三阶段 | ~150 行 | 1 个 |
| **总计** | **~670 行** | **8 个文件** |
---
## 风险评估
| 风险 | 影响 | 概率 | 缓解措施 |
|------|------|------|---------|
| 示例与库 API 不同步(库重构后示例过时) | 高 | 中 | 将示例加入 CI:`cargo test --examples` |
| MockProvider 行为与真实 Provider 差异 | 低 | 低 | 示例明确标注离线/在线模式 |
| 示例代码量膨胀超过预期 | 低 | 低 | 每个示例控制在 200 行以内,超过则拆分子函数 |
| `full_integration.rs` 依赖 API keyCI 会跳过 | 中 | 高 | 用 `#[cfg(not(ci))]``.env` 存在性判断优雅降级 |
---
## 验收标准
1. **阶段一全部完成时**
- `cargo run --example prompt_composer` → 成功退出
- `cargo run --example custom_tool` → 成功退出
- `cargo run --example agent_session_demo` → 成功退出
- `cargo run --example task_agent_demo` → 成功退出
2. **阶段二全部完成时**
- 额外 3 个示例均可 `cargo run` 成功
3. **阶段三完成时**(可选):
- `full_integration` 在有 `.env` 配置时成功运行,无配置时友好提示降级
4. **全局验收**
- `cargo build` 无新增警告
- 所有示例输出格式清晰,有说明性 println
- 每个示例在文件顶部有 `//!` 注释说明其演示目的
+222
View File
@@ -0,0 +1,222 @@
# Context 切换方案设计备忘
> 创建日期:2026-06-10
> 状态:备忘(Phase 4 不实现)
> 关联文档:
> - `docs/7-agent-runtime.md` — Phase 4 方案(含 SessionMemory 设计)
> - `docs/note-opencode-agent-switching.md` — OpenCode 切换机制调研
> - `docs/roadmap.md` — 项目总 Roadmap
---
## 1. 背景
### 1.1 问题
在调研 OpenCode 的 Agent 切换机制后(详见 `docs/note-opencode-agent-switching.md`),发现其做法是:
- 切换 agent 时**不动消息历史**
- 在 user message 末尾追加 `synthetic: true``<system-reminder>` 提醒
- 同时**完全重新计算** system prompt
这个方法的问题是:**长上下文中频繁切换 agent 容易给 LLM 造成身份困惑**。同一个消息列表里有 `system: build` 的 identity,又出现 `system: plan` 的 identityLLM 容易"串味"。
### 1.2 核心思路
以一个 session 里存在**多个独立的 context** 来解决,每个 context 有自己独立的 system prompt + 消息列表:
```
OpenCode 模式(一条流):
[system: build, user: A, ass: A', user: <切plan>, system: plan, user: B]
↑ 身份困惑
建议的多 context 模式:
session {
context_a: [system: build, user: A, ass: A'] ← 只有 build 的 identity
context_b: [system: plan, user: B, ass: B'] ← 只有 plan 的 identity
}
```
### 1.3 适用范围
| 场景 | 适用性 | 说明 |
|------|--------|------|
| Agent 切换(build ↔ plan | ✅ 核心场景 | 同一 session 内更换角色 |
| 主从 Agent 协作 | ✅ 核心场景 | primary 委派子任务给 subagentsubagent 独立运作 |
| 长 session 上下文压缩 | ✅ 附带收益 | 拆分 context 后,每个 context 独立累积消息,不会互相拖长 |
| 并行 context 执行 | ⚠️ 拓展场景 | context_a 和 context_b 可各自独立推进 |
---
## 2. 三个候选方案
### 方案 AOpenCode 式(system prompt 重算 + synthetic 追加)
**做法**
- 单一消息列表
- 切换时重算 system prompt
- user message 末尾追加 `<system-reminder>` 标签
**优点**
- 实现简单
- 消息历史完整可见
**缺点**
- 长上下文身份困惑
- context 互相污染(每个 context 都要看全部历史)
**结论**:❌ 否决。不解决身份困惑问题。
### 方案 B:信息池 + 切换不重置(借鉴 OpenCode + 增强)
**做法**
- 切换时保留历史
- 使用 `<system-reminder>` 标签
- 靠 prompt 工程让 LLM 理解身份变更
**优点**
- 历史连贯
- 改动最小
**缺点**
- 仍然有身份困惑风险
- 上下文不受控增长
**结论**:❌ 否决。治标不治本。
### 方案 C:多 context 隔离 + SessionMemory 桥接(推荐)
**做法**
- 每个 agent 切换创建一个新的 context(独立消息列表 + 独立 system prompt
- context 之间通过 `SessionMemory` 桥接关键信息
- 切换时新 context 的 system prompt 末尾注入 `SessionMemory::snapshot()`
```
context_a (build)
→ 对话 50 轮
→ 写入 SessionMemory: {"design_decision": "用 PostgreSQL",
"files_changed": "src/db.rs"}
→ 销毁(或沉睡)
创建 context_b (plan)
→ system_prompt += snapshot()
→ "<session-context>
design_decision: 用 PostgreSQL
files_changed: src/db.rs
</session-context>"
→ 对话 10 轮(不需要看 context_a 的 50 轮历史)
→ 读 SessionMemory: get("design_decision") → "用 PostgreSQL"
```
**优点**
- ✅ 身份稳定:每个 context 只有一套 system prompt
- ✅ 上下文隔离:context_b 不受 context_a 的消息量影响
- ✅ 信息桥接:关键结论通过 SessionMemory 显式传递
- ✅ 并行潜力:两个 context 可各自运行
**缺点**
- ❌ 实现复杂度:从"一个消息列表"到"多个消息列表 + 桥接"
- ❌ 信息完整性:LLM 自主决定"什么值得记",可能遗漏细节
- ❌ 上层理解成本:应用层需要理解 context 概念
**结论**:✅ 推荐。架构上最干净,但 Phase 4 不做全部实现。
---
## 3. SessionMemory 桥接机制(方案 C 的核心)
### 3.1 设计决策
| 决策 | 结论 | 理由 |
|------|------|------|
| 复用 Phase 3 `MemoryStore` | ✅ 是 | 不引入新存储机制 |
| 跨进程支持 | ✅ 是 | 换后端即可(Redis / SQLite),`InMemoryStore` 兜底 |
| namespace 隔离 | ✅ 是 | `_session_{session_id}` 命名空间 |
| 谁写 SessionMemory | LLM 通过 tool 显式写(v0.2+)或上层应用 API 写 | 不支持自动写——避免 "写太多 = 噪音,写太少 = 遗漏" |
| snapshot 格式 | `<session-context>` XML 风格 | 专为注入 system prompt 设计 |
### 3.2 谁写 SessionMemory 的三种选项
| 选项 | 描述 | 评估 |
|------|------|------|
| **选项 1AgentSession 自动写** | 每轮对话后自动摘录关键信息 | ❌ 摘录什么?容易变成精简版对话历史,失去"关键信息"的定位 |
| **选项 2LLM 通过 tool 显式写** | 把 `SessionMemory::set` 暴露为 Tool 供 LLM 调用 | ✅ LLM 自主决定什么值得记;v0.2+ 实现自动注册 |
| **选项 3:上层应用 API 写** | `agent_session.session_memory.set("k", "v")` | ✅ Phase 4 即可用,最透明 |
**Phase 4 实现选项 3**v0.2+ 补充选项 2(tool 自动注册)。
### 3.3 三层记忆体系
```
持久层(Phase 3 MemoryStore / KnowledgeStore ── 跨 session 持久,长期知识
会话层(Phase 4 SessionMemory ── 单 session 内共享,context 桥接
对话层(Phase 3 ConversationMemory ── 单 context 内消息历史
```
---
## 4. Phase 4 范围 vs v0.2+ 范围
### ✅ Phase 4 做
| 组件 | 状态 | 行数 |
|------|------|------|
| `SessionMemory` struct | ✅ 做 | ~40 行 |
| `AgentSession` + `session_memory` 字段 | ✅ 做 | ~3 行 |
| `AgentSession``Arc<dyn Agent>` 替代 `agent_name: String` | ✅ 做 | ~3 行 |
| `RuntimeBundle` + `session_memory_backend` 字段 | ✅ 做 | ~1 行 |
| `AgentBuilder` + `.session_memory_backend()` | ✅ 做 | ~3 行 |
### ❌ 延后到 v0.2+
| 组件 | 状态 | 说明 |
|------|------|------|
| Context 切换管理(`switch_context` / `create_context` | ❌ 延后 | 需要 `ContextManager` 包装 |
| 多 context 生命周期管理 | ❌ 延后 | context 的创建/销毁/切换策略 |
| `"session_memory_set"` tool 自动注册 | ❌ 延后 | 在 `ToolRegistry` 里注册特殊 tool |
| Context 级别的 `ConversationMemory` 自动管理 | ❌ 延后 | 每个 context 独立消息历史 |
### 延后的理由
1. **最小范围原则**Phase 4 定位是"薄胶水层 + trait 抽象",多 context 管理属于业务编排的范畴
2. **稳定 API 优先**:先把 `AgentSession` / `RuntimeBundle` / `SessionMemory` 的 API 定稳,v0.2+ 在上面搭建 context 切换
3. **降低实施风险**:Phase 4 已有 13 个交付任务,加 context 切换会增加 2-3 倍复杂度
---
## 5. v0.2+ Context 切换的设想接口
> 以下为未来实现的草案,非承诺。记录在这里避免 v0.2+ 重新设计时丢失上下文。
```rust
pub struct ContextManager {
contexts: HashMap<String, AgentSession>,
active_context: String,
session_memory: SessionMemory,
}
impl ContextManager {
/// 创建一个新的 context,绑定指定 agent
pub fn create_context(&mut self, id: &str, agent: Arc<dyn Agent>) -> Result<(), AgentError>;
/// 切换到已有 context
pub fn switch_context(&mut self, id: &str) -> Result<&mut AgentSession, AgentError>;
/// 销毁 context
pub fn destroy_context(&mut self, id: &str) -> Result<(), AgentError>;
/// 从 context_a 桥接关键信息到 context_b 的 system prompt
pub fn bridge(&mut self, from: &str, to: &str) -> Result<(), AgentError>;
}
```
切换流程:
1. `context_manager.create_context("plan", plan_agent)` — 新 context 的 system prompt 自动附加 `session_memory.snapshot()`
2. `context_manager.switch_context("plan")` — 返回 context 的 `AgentSession`,应用层调 `submit_turn`
3. context 销毁时,关键信息经由 LLM 或上层应用写入 `SessionMemory`
---
## 6. 一句话总结
> **多 context 切换方案 = `SessionMemory`Phase 4 做信息桥接基础) + `ContextManager`v0.2+ 做切换管理)。Phase 4 只铺"水管接口",不装"水循环系统"。**
+174
View File
@@ -0,0 +1,174 @@
# OpenCode Agent 切换机制调研笔记
> 调研日期:2026-06-09
> 调研方式:直接读 `sst/opencode` 源码(本地路径 `/Users/midnite/Samples/opencode`
> 调研目标:OpenCode 在切换 agent 时,整个上下文(系统提示词 + agent 提示词)是如何注入的
> 关联:
> - `docs/note-agent-harness-references.md` — 之前的参考项目调研
> - `docs/note-agent-runtime-design.md` — AG Core Phase 4 设计决策
> - `docs/7-agent-runtime.md` — AG Core Phase 4 方案文档
---
## 1. 项目背景
| 维度 | 详情 |
|------|------|
| 项目 | `sst/opencode`GitHub),不是 npm `opencode-ai`npm 只发编译产物) |
| 规模 | 160k+ stars、900+ contributors、13k+ commits |
| 语言 | TypeScript + Bun 运行时 + Effect(依赖注入 / Layer |
| 定位 | 开源 AI 编程代理(TUI / Desktop / IDE 插件),与 Claude Code / Cursor 对标 |
| Agent 切换 | 终端按 **Tab 键** 在 primary agent 之间循环(build / plan |
## 2. Agent 分类
OpenCode 把 agent 严格分为三类:
| 类型 | 内置 | 触发方式 | 用途 |
|------|------|---------|------|
| **Primary(主代理)** | build / plan | Tab 键循环切换 | 用户直接交互 |
| **Subagent(子代理)** | general / explore / scout | 主代理自动调 / 用户 `@提及` | 专门任务 |
| **Hidden system(隐藏)** | compaction / title / summary | 框架自动调,用户不可见 | 系统级 |
源码定义在 `packages/opencode/src/agent/agent.ts``Agent.Info` schema
```typescript
{ name, description, mode, native, hidden,
topP, temperature, color,
permission: PermissionV1.Ruleset,
model, variant, prompt, options, steps }
```
每个 agent 是**纯配置对象**,可由用户 `opencode.json` 覆盖或自定义 `.md` 文件定义。
## 3. 核心:System Prompt 完整拼接机制
OpenCode 把 system prompt 分为 **3 层**,每次 LLM 调用时**完整重新计算**(不缓存)。
### 3.1 拼接顺序
源码:`packages/opencode/src/session/llm/request.ts:58-66`
```typescript
const system = [
// Layer1:主 agent 提示词
...(input.agent.prompt
? [input.agent.prompt] // ① agent 自带 prompt(如 PROMPT_EXPLORE
: SystemPrompt.provider(input.model)), // ② 或按 model 选择 provider prompt
// Layer2:动态上下文(prompt.ts:1408-1414
...input.system, // ③ env + instructions + skills
// Layer3:用户自定义 system
...(input.user.system ? [input.user.system] : []), // ④ 单次 user 消息的 system 字段
]
.filter(x => x)
.join("\n")
```
### 3.2 Layer 2 的内部构成
源码:`packages/opencode/src/session/prompt.ts:1408-1414`
```typescript
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
sys.skills(agent), // 当前 agent 可用的 skills 描述
sys.environment(model), // 工作目录、日期、平台、git 状态
instruction.system(), // 自动读取 AGENTS.md / CLAUDE.md / CONTEXT.md
MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [...env, ...instructions, ...(skills ? [skills] : [])]
```
**关键发现**
- **AGENTS.md / CLAUDE.md 是 instruction 自动注入**,不是用户手动 @引用
- `system.ts``provider()` 函数**根据 model ID 选择不同 .txt 模板**(如 `PROMPT_ANTHROPIC` / `PROMPT_GEMINI` / `PROMPT_CODEX`
- `environment()` 注入**运行时环境信息**(cwd、平台、日期)
### 3.3 Agent 配置中的 `prompt` 字段
源码:`packages/opencode/src/agent/agent.ts`
```typescript
// build / plan 没有 prompt 字段 → 走 SystemPrompt.provider(model)
build: { name: "build", mode: "primary", permission: ... },
plan: { name: "plan", mode: "primary", permission: ... },
// explore / compaction / title / summary 有显式 prompt
explore: { ..., prompt: PROMPT_EXPLORE, mode: "subagent" },
compaction: { ..., prompt: PROMPT_COMPACTION, mode: "primary", hidden: true },
title: { ..., prompt: PROMPT_TITLE, mode: "primary", hidden: true },
summary: { ..., prompt: PROMPT_SUMMARY, mode: "primary", hidden: true },
```
**结论**agent 的 `prompt` 字段**只决定 Layer 1 的内容**。Layer 2env/instructions/skills)和 Layer 3user.system)始终拼上。
## 4. 核心:Agent 切换时的 4 个动作
源码:`packages/opencode/src/session/reminders.ts`(**整个文件 92 行就是答案**)
OpenCode 用 **`synthetic: true` 的 text part 注入到 user message**,而不是修改 system prompt。
| 切换方向 | 动作 | 模板文件 | 大小 |
|---------|------|---------|------|
| 任意 → **plan** | user message 追加 `PROMPT_PLAN` | `session/prompt/plan.txt` | 26 行 |
| **plan → build** | user message 追加 `BUILD_SWITCH` | `session/prompt/build-switch.txt` | **5 行** |
| build → **plan** (experimental) | user message 追加 `PLAN_MODE` | `session/prompt/plan-mode.txt` | 70 行 |
| 任意切换 | system prompt **完全重算** | `request.ts:58-66` | — |
**最关键的发现——`build-switch.txt` 全文只有 5 行**
```
<system-reminder>
Your operational mode has changed from plan to build.
You are no longer in read-only mode.
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
</system-reminder>
```
**机制总结**
1. 切换时**不动 message history**(之前所有 user/assistant/tool 消息完整保留)
2. 通过**比较 `msg.info.agent` 字段**判断上一条 assistant 用的哪个 agent
3. 在**当前 user message 末尾追加**一个 `synthetic: true` 的 text part
4. 同时**重新计算 system prompt**Layer 1 根据新 agent 的 `prompt` 字段切换)
## 5. 关键设计决策
| 决策 | 做法 | 原因推断 |
|------|------|---------|
| **system prompt 重算而非缓存** | 每次 LLM 调用都重新拼接 | agent / model / instructions 都可能动态变化 |
| **历史消息不重置** | 切换 = 追加 synthetic part | 保持上下文连贯,避免"切换即失忆" |
| **切换提醒伪装成 user 内容** | `<system-reminder>` 标签 + `synthetic: true` | 大多数 LLM 对 `<system-reminder>` 标签有特殊信任 |
| **agent prompt 与 model prompt 二选一** | `agent.prompt ?? SystemPrompt.provider(model)` | build/plan 共享 model prompt,自定义 agent 可独立 prompt |
| **AGENTS.md 自动注入** | instruction.service 每轮扫描 | Claude Code 兼容,提升跨工具体验 |
## 6. 与 AG Core Phase 4 的对应关系
| OpenCode 机制 | AG Core 对应 | 借鉴价值 |
|--------------|-------------|---------|
| 3 层 system prompt 拼接 | `AgentSession::submit_turn` 中组装 `LlmCycle.with_system_prompt()` | **高**——可拆分为「base prompt + agent prompt + env context」 |
| agent 切换时追加 synthetic part | Phase 4 v1 **不做**(仅保留单 agent 角色) | 中——v0.2+ 才考虑 |
| AGENTS.md 自动注入 | `prompt::PromptTemplate` + 文件加载(应用层) | 低——文件 I/O 是应用层职责 |
| 权限矩阵三态 (allow/ask/deny) | `tools::PermissionChecker`Phase 2 已实现) | 已有 |
| Hidden system agent (Compaction) | `llm::compact`Phase 0 已实现) | 已有 |
| Tab 键循环切换 | 应用层 UI 概念 | 不在 core 库范围 |
## 7. 借鉴 / 不借鉴清单
### ✅ 值得借鉴(v0.2+ 考虑)
1. **`AgentSession` 应支持"切换 agent 但保留历史"**——目前 Phase 4 v1 不做,但 trait 设计上要预留空间
2. **System prompt 拆分为多层**——`base_prompt + agent_prompt + env_context`,便于将来按 agent 类型切换
3. **synthetic message 模式**——切换 agent 时插入"状态变更通知"而非修改历史
### ❌ 不借鉴
- Tab 键循环切换(应用层 UI 概念)
- `.md` agent 定义文件(应用层文件加载)
- `mode: primary/subagent` 区分(AG Core 是 lib 不做 UI 角色区分)
- Hidden system agent 字段(AG Core 已在 L0/L1 实现等价能力)
- AGENTS.md 自动注入(应用层职责)
## 8. 一句话总结
> **OpenCode 的 agent 切换机制 = "system prompt 完全重算" + "user message 追加 5 行 synthetic 提醒"。** Agent 切换**不**重置消息历史,**不**改写之前内容,只在末尾追加一条"状态变更通知",并按新 agent 重新组装 system prompt 的 Layer 1agent 专属 prompt)。
+108 -20
View File
@@ -1,13 +1,13 @@
# AG Core Roadmap
> 定稿日期:2026-05-11
> 最后更新:2026-06-09Phase 4 设计讨论收尾;扩展计划补充 v0.2+ 候选项
> 最后更新:2026-06-11Phase 4c 编码实施完成
## 愿景
AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可插拔的架构,提供大模型调用、提示词工程、工具系统、记忆检索四大核心能力,支持快速组合出符合业务需求的智能体应用。
**当前状态**:Phase 0 基础设施已全部完成,Phase 1 提示词工程已全部完成,Phase 2 工具系统已全部完成,Phase 3 记忆系统已全部完成,等待 Phase 4 启动
**当前状态**:Phase 0 基础设施已全部完成,Phase 1 提示词工程已全部完成,Phase 2 工具系统已全部完成,Phase 3 记忆系统已全部完成,Phase 4a 核心胶水层已全部完成,Phase 4b 任务执行已全部完成,Phase 4c 会话级记忆已全部完成(116 个测试通过,0 警告)
---
@@ -19,7 +19,7 @@ AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可
| 提示词工程 | ✅ 完整 | `docs/4-prompt-engineering.md` | P1 |
| 工具系统 + 权限 | ✅ 完整 | `docs/5-tool-system.md` | P1 |
| 记忆检索 | ✅ 完整 | `docs/6-memory-system.md` | P2 |
| Agent 运行时 | ❌ 缺失 | — | P2 |
| Agent 运行时4a 胶水层) | ✅ 已实现 | `docs/7-agent-runtime.md` | P2 |
| 生命周期钩子 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0LLM Cycle 扩展) |
| Provider 注册发现 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0Provider 接口扩展) |
| 流式事件系统 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0(流式接口前置) |
@@ -121,22 +121,94 @@ AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可
---
### Phase 4 — Agent Runtime(智能体运行时
### Phase 4a — Agent Core Glue(核心胶水层
**目标**实现多轮对话编排与任务规划
**目标**提供最小可用的 Agent Runtime——把 Phase 0-3 的能力"装配"成 `AgentSession::submit_turn`。上层可基于 4a 构建多轮对话应用
**交付物**
1. `agent.rs` + `agent/` 模块
2. `Agent` trait — 智能体接口定义
3. `ConversationAgent` — 对话型智能体实现
4. `TaskAgent` — 任务型智能体(规划 → 执行 → 反馈
5. `specs/agent-runtime.md` — 方案文档
1. `agent.rs` + `agent/` 模块7 个文件:agent/error/runtime/builder/session/task + 模块根)
2. `Agent` trait — 智能体角色定义(name / system_prompt / tool_definitions
3. `AgentSession` — 会话实例(绑定 `Arc<dyn Agent>` + `RuntimeBundle` + 内联 HashMap session_data
4. `RuntimeBundle` — 显式依赖注入容器(不含 session_memory_backend
5. `AgentBuilder` — 链式构造入口(不含 session_memory_backend
6.`AgentError` — 统一错误类型(7 个变体:Llm / Tool / Memory / HookBlocked / LimitExceeded / Config / Other;不含 PlanParse
7.`Plan` / `Step` / `StepStatus` — 纯数据结构(不含任何解析逻辑)
8. ✅ Hook 事件扩展:OnTurnStart / OnTurnEnd + turn_index 字段
9.`docs/7-agent-runtime.md` — 方案设计文档(含 4a/4b/4c 分阶段计划)
**依赖**Phase 0, 1, 2, 3(整合所有模块)
**实际新增**
- 新增文件 7 个(agent.rs + agent/{agent, error, runtime, builder, session, task}.rs
- 修改文件 3 个(lib.rs +1 行;llm/hooks.rs +13 行追加变体/字段;llm/cycle.rs 内部字段 Box→Arc + 新增 `new_with_arc` 公共方法)
- 实际代码量约 800 行(含测试;纯实现约 470 行——略高于方案预估 440 行,因 AgentSession 的 tests 模块内联 MockProvider/StubAgent 等辅助结构)
- 新增内联测试 22 个;全量测试 84 → 109(0 失败)
- clippy 0 警告(agent 模块)
- 无新增外部依赖
**依赖**Phase 0, 1, 2, 3
**优先级**Could Have
**预估规模**:约 600 行代码
**预估规模**:约 440 行代码
**状态**:✅ Phase 4a 全部交付物已完成
---
### Phase 4b — Task Execution(任务执行)
**目标**:在 Phase 4a 基础上,赋予智能体"拆解目标 → 逐步执行"的能力。
**前置条件**Phase 4a 已完成。
**交付物**
1.`TaskAgent` trait — `run(goal)` 自主式 + `execute_plan(plan)` 外部驱动式
2.`PlanParser` trait + `JsonPlanParser` 参考实现
3.`AgentError` 追加 PlanParse 变体(共 7 个变体)
4. ✅ Hook 事件扩展:OnPlanStepComplete + plan_step_index 字段
**依赖**Phase 4a
**优先级**Could Have
**预估规模**:约 200 行代码(增量)
**实际新增**
- 修改文件 2 个(llm/hooks.rs +5 行;agent/error.rs +10 行)
- 新增代码约 150 行(含测试;纯实现约 90 行)
- 新增内联测试 4 个;全量测试 109 → 113(0 失败)
- clippy 0 警告
- 无新增外部依赖
**状态**:✅ Phase 4b 全部交付物已完成
---
### Phase 4c — Session Memory(会话级记忆)
**目标**:提供会话级 key-value 记忆,作为 session 内各 context 之间的信息桥接通道。
**前置条件**Phase 4a 已完成(可与 Phase 4b 并行)。
**交付物**
1.`SessionMemory` struct — 基于 `MemoryStore`,按 session_id namespace 隔离
2.`RuntimeBundle` + `AgentBuilder` 扩展 `session_memory_backend` 字段
3.`AgentSession` 替换内联 HashMap 为完整 `SessionMemory`
**依赖**Phase 4aPhase 3 MemoryStore
**优先级**Could Have
**预估规模**:约 115 行代码(增量)
**实际新增**
- 新增文件 1 个(agent/session_memory.rs
- 修改文件 4 个(agent/runtime.rs +5 行;agent/builder.rs +10 行;agent/session.rs +30 行;agent.rs +2 行)
- 新增代码约 180 行(含测试;纯实现约 100 行)
- 新增内联测试 3 个;全量测试 113 → 116(0 失败)
- clippy 0 警告
- 无新增外部依赖
**状态**:✅ Phase 4c 全部交付物已完成
---
@@ -148,15 +220,19 @@ graph BT
P1["<b>Phase 1: Prompt Engineering</b><br/>PromptTemplate<br/>PromptComposer"]:::done
P2["<b>Phase 2: Tool System</b><br/>Tool Registry<br/>PermissionChecker<br/>MCP Client"]:::done
P3["<b>Phase 3: Memory System</b><br/>MemoryStore<br/>ConversationMemory<br/>KnowledgeStore"]:::done
P4["<b>Phase 4: Agent Runtime</b><br/>ConversationAgent<br/>TaskAgent"]:::pending
P4a["<b>Phase 4a: Core Glue</b><br/>AgentSession<br/>RuntimeBundle<br/>Plan/Step 纯数据"]:::done
P4b["<b>Phase 4b: Task Execution</b><br/>TaskAgent<br/>PlanParser<br/>JsonPlanParser"]:::done
P4c["<b>Phase 4c: Session Memory</b><br/>SessionMemory"]:::done
P1 --> P0
P2 --> P0
P3 --> P0
P2 --> P1
P4 --> P1
P4 --> P2
P4 --> P3
P4a --> P1
P4a --> P2
P4a --> P3
P4b --> P4a
P4c --> P4a
classDef done fill:#4ade80,stroke:#16a34a,color:#1a1a1a
classDef pending fill:#fbbf24,stroke:#d97706,color:#1a1a1a
@@ -168,6 +244,7 @@ graph BT
> 以下功能在已完成的 phase 中已实现基础能力或在 Phase 4 阶段明确了边界,后续可按维度增量扩展。
> 设计参考:见 `docs/note-agent-harness-references.md`OpenClaw / Hermes / OpenHuman / OpenHarness 横向对比)。
> OpenCode 借鉴:见 `docs/note-opencode-agent-switching.md`Agent 切换 + System Prompt 拼接机制)。
### 已有扩展项(沿用)
@@ -225,6 +302,14 @@ graph BT
|-------|---------|------|--------|------|
| 流式 `submit_turn` | `agent/session` | Phase 4 v1 只暴露非流式 `submit_turn()`v0.2 包装 `LlmCycle::submit_stream` 暴露流式入口 | P2 | v0.2 待评估 |
#### Agent 切换 / Prompt 动态(OpenCode 借鉴)
| 扩展项 | 所在模块 | 说明 | 优先级 | 状态 |
|-------|---------|------|--------|------|
| Agent 身份切换(角色轮换) | `agent` | 借鉴 OpenCode Tab 键切换 build/plan:同一 `AgentSession` 持有可热替换的 `Agent` 引用,切换时不重置消息历史,在末尾追加 `synthetic: true` 的状态变更消息。详见 `docs/note-opencode-agent-switching.md` §4 | P2 | v0.2 待评估 |
| System Prompt 多层动态拼接 | `agent/session` | 借鉴 OpenCode `request.ts:58-66`:拆分 `base_prompt + agent_prompt + env_context` 三层,`AgentSession::submit_turn` 每轮重算(不缓存),便于按 agent 类型动态切换 | P2 | v0.2 待评估 |
| **多 Context 切换** | `agent` | **Phase 4c 的 SessionMemory 数据结构已预留信息桥接通道,v0.2+ 在其上包装 `ContextManager` 实现完整的多 context 切换:创建/销毁/切换 context、通过 SessionMemory 桥接关键信息。详见 `docs/note-context-switch-design.md`** | P2 | v0.2 待评估 |
---
## 风险与建议
@@ -233,20 +318,23 @@ graph BT
2. **并行可能性**Phase 0 和 Phase 1 可并行开展(无相互依赖),可加速早期交付
3. **MCP 协议复杂性**MCP 涉及协议握手、session 管理、长期连接,建议预留充足时间调研协议细节
4. **Scope 蔓延风险**:当前 specs 只有 1 份文档,建议每个模块上线前都产出对应 spec,避免边实现边设计
5. **Phase 4 抽象化边界**AG Core 定位为"支持库"而非"Agent 产品"Phase 4 需严格控制范围——只暴露 trait + 最小 reference impl,业务循环(多轮 turn 编排、记忆自动回写、Task 拆解策略)留给上层应用,避免与 OpenHarness / Hermes / OpenHuman 等已有 Agent 产品竞争实现细节。详细设计决策见 Phase 4 设计讨论记录(待 `docs/7-agent-runtime.md` 落盘)
5. **Phase 4 抽象化边界**AG Core 定位为"支持库"而非"Agent 产品"Phase 44a/4b/4c需严格控制范围——只暴露 trait + 最小 reference impl,业务循环(多轮 turn 编排、对话记忆自动回写、Task 拆解策略)留给上层应用`SessionMemory`(Phase 4c)提供信息桥接通道但不实现 context 切换逻辑。多 context 切换管理延后至 v0.2+。详细设计决策见 `docs/7-agent-runtime.md`
6. **参考项目语言差异**OpenClaw / Hermes / OpenHarness 均为 Python/TypeScript 实现,OpenHuman 虽是 Rust + Tauri 但定位是桌面应用。借鉴时**只取架构模式**,不照搬具体实现(如 Pydantic 工具校验、SQLite Memory Tree、Node+Python 双进程等)
---
## 下一步行动
1. **Phase 4 设计讨论收尾**Phase 4 范围已收窄为「`Agent` trait + `RuntimeBundle` 依赖注入容器 + `AgentSession` 实体/会话分离 + `TaskAgent` 双入口 + 记忆弱引用 + Hook 事件扩展 3 个」。决策记录已固化,待写 `docs/7-agent-runtime.md` 方案文档后启动编码实现
2. **Phase 4 方案文档**:将 Phase 4 设计决策沉淀为方案文档,沿用 `docs/4-prompt-engineering.md` / `5-tool-system.md` / `6-memory-system.md` 的 6 段式结构,文件名 `docs/7-agent-runtime.md`
1. **Phase 4c 已完成**Phase 4a + 4b + 4c 已交付(116 测试通过,0 clippy 警告)。可启动 v0.2+ 扩展评估(如多 Context 切换、Multi-Agent 协同等)
2. **Context 切换备忘**`docs/note-context-switch-design.md` 记录了多 context 切换方案讨论,作为 v0.2+ 扩展项的输入
3. **参考项目调研沉淀**:已完成 OpenClaw / Hermes / OpenHuman / OpenHarness 横向调研,结果沉淀至 `docs/note-agent-harness-references.md`,作为 v0.2+ 扩展项的输入
4. **Phase 3 备用设计就绪**`docs/note-knowledge-graph-design.md` 记录了 KnowledgeGraph、高级评分、RecallBased 淘汰等设计,v0.2+ 记忆扩展可直接参考
**已完成阶段**
**已完成 / 进行中阶段**
- ✅ Phase 0 Foundation — 全部交付物已完成
- ✅ Phase 1 Prompt Engineering — 全部交付物已完成
- ✅ Phase 2 Tool System — 全部交付物已完成
- ✅ Phase 3 Memory System — 全部交付物已完成
- ✅ Phase 4a Core Glue — 全部交付物已完成
- ✅ Phase 4b Task Execution — 全部交付物已完成
- ✅ Phase 4c Session Memory — 全部交付物已完成
+28
View File
@@ -0,0 +1,28 @@
//! Agent Runtime —— 智能体(Agent)核心胶水层。
//!
//! 把 Phase 0-3 的能力(LlmCycle / ToolRegistry / MemoryStore / HookExecutor"装配"为
//! 上层可用的智能体抽象:`Agent` / `AgentSession` / `RuntimeBundle` / `AgentBuilder` / `Plan`。
//!
//! **不**实现业务循环,**不**假设上层如何使用 memory。
//! 详细设计见 `docs/7-agent-runtime.md`。
// 模块根文件 `agent.rs` 与子模块 `agent/agent.rs` 同名(项目惯例,与 `llm/cycle.rs` 一致)。
#![allow(clippy::module_inception)]
pub mod agent;
pub mod builder;
pub mod error;
pub mod runtime;
pub mod session;
pub mod session_memory;
pub mod task;
// 重导出公共 API(按使用频度排序)
pub use agent::Agent;
pub use builder::AgentBuilder;
pub use error::AgentError;
pub use runtime::{AgentConfig, RuntimeBundle};
pub use session::AgentSession;
pub use session_memory::SessionMemory;
pub use task::{Plan, PlanParser, Step, StepStatus, TaskAgent};
pub use task::JsonPlanParser;
+30
View File
@@ -0,0 +1,30 @@
//! Agent trait —— 智能体的"角色"抽象。
//!
//! 设计要点(参见 `docs/7-agent-runtime.md` §3.2.1):
//!
//! - **角色与会话分离**`Agent` 定义"做什么、用什么工具"`AgentSession` 维护"当前状态"
//! - **工具白名单扩展点**:默认从 `RuntimeBundle.tool_registry` 取全部,子 trait 可覆盖做白名单/过滤
//! - **不绑定业务循环**`submit_turn` 在 `AgentSession` 上,不在 trait 上
use crate::agent::runtime::RuntimeBundle;
use crate::llm::types::ToolDefinition;
/// Agent 角色抽象。
///
/// 实现此 trait 即可接入 Agent Runtime。典型实现是 struct 持有静态配置(name、system prompt 模板),
/// 也可以是基于配置动态生成的轻量实现。
pub trait Agent: Send + Sync {
/// 角色名(用于日志、调试、UI 展示)。
fn name(&self) -> &str;
/// 系统提示词。无提示词的纯工具型 agent 返回 `None`。
fn system_prompt(&self) -> Option<&str>;
/// 列出该 Agent 想暴露给 LLM 的工具定义。
///
/// **默认实现**:从 `bundle.tool_registry` 取全部工具(最常用模式)。
/// **子 trait / 具体实现可覆盖**:做白名单、过滤、按状态动态调整等。
fn tool_definitions(&self, bundle: &RuntimeBundle) -> Vec<ToolDefinition> {
bundle.tool_registry.definitions()
}
}
+182
View File
@@ -0,0 +1,182 @@
//! AgentBuilder —— `RuntimeBundle` 的链式构造入口。
//!
//! 设计原则:
//!
//! - **唯一构造入口**:上层应用不应直接 `RuntimeBundle::new`;用 `AgentBuilder` 保证必填字段
//! 校验集中、默认值集中管理
//! - **必填字段在 `build()` 时校验**:缺失返回 `AgentError::Config`,不 panic
//! - **选填字段独立 setter**:未调用对应 setter 时使用 `None` 兜底
use std::sync::Arc;
use crate::agent::error::AgentError;
use crate::agent::runtime::{AgentConfig, RuntimeBundle};
use crate::llm::hooks::HookExecutor;
use crate::llm::provider::LlmProvider;
use crate::memory::retriever::MemoryRetriever;
use crate::memory::store::MemoryStore;
use crate::tools::ToolRegistry;
/// `RuntimeBundle` 的链式构造器。
///
/// 使用示例:
/// ```ignore
/// let bundle = AgentBuilder::new()
/// .provider(my_provider)
/// .tool_registry(my_registry)
/// .hook_executor(my_executor)
/// .build()?;
/// ```
#[derive(Default)]
pub struct AgentBuilder {
provider: Option<Arc<dyn LlmProvider>>,
tool_registry: Option<Arc<ToolRegistry>>,
hook_executor: Option<Arc<HookExecutor>>,
memory_store: Option<Arc<dyn MemoryStore>>,
retriever: Option<Arc<MemoryRetriever>>,
session_memory_backend: Option<Arc<dyn MemoryStore>>,
config: Option<AgentConfig>,
}
impl AgentBuilder {
/// 创建一个空的 builder,所有必填字段均为 `None`。
pub fn new() -> Self {
Self::default()
}
/// 设置 LLM provider(必填)。
pub fn provider(mut self, p: Arc<dyn LlmProvider>) -> Self {
self.provider = Some(p);
self
}
/// 设置工具注册表(必填)。
pub fn tool_registry(mut self, r: Arc<ToolRegistry>) -> Self {
self.tool_registry = Some(r);
self
}
/// 设置钩子执行器(必填)。
pub fn hook_executor(mut self, h: Arc<HookExecutor>) -> Self {
self.hook_executor = Some(h);
self
}
/// 设置持久化记忆后端(选填,不传也能跑)。
pub fn memory_store(mut self, m: Arc<dyn MemoryStore>) -> Self {
self.memory_store = Some(m);
self
}
/// 设置记忆检索器(选填,不传也能跑)。
pub fn retriever(mut self, r: Arc<MemoryRetriever>) -> Self {
self.retriever = Some(r);
self
}
/// 设置 SessionMemory 后端(选填,不传则 `AgentSession` 内部用 `InMemoryStore` 兜底)。
pub fn session_memory_backend(mut self, s: Arc<dyn MemoryStore>) -> Self {
self.session_memory_backend = Some(s);
self
}
/// 整体覆盖 `AgentConfig`(选填,不传则用默认值)。
pub fn config(mut self, c: AgentConfig) -> Self {
self.config = Some(c);
self
}
/// 构造 `RuntimeBundle`,校验必填字段。
///
/// **错误**`provider` / `tool_registry` / `hook_executor` 任一缺失则返回
/// `AgentError::Config("missing <field>")`,不 panic。
pub fn build(self) -> Result<RuntimeBundle, AgentError> {
let provider = self
.provider
.ok_or_else(|| AgentError::Config("missing provider".into()))?;
let tool_registry = self
.tool_registry
.ok_or_else(|| AgentError::Config("missing tool_registry".into()))?;
let hook_executor = self
.hook_executor
.ok_or_else(|| AgentError::Config("missing hook_executor".into()))?;
let config = self.config.unwrap_or_default();
Ok(RuntimeBundle::new(
provider,
tool_registry,
hook_executor,
self.memory_store,
self.retriever,
self.session_memory_backend,
config,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::llm::provider::LlmProvider;
use crate::llm::types::{ChatRequest, ChatResponse};
use crate::llm::error::LlmError;
use async_trait::async_trait;
struct StubProvider;
#[async_trait]
impl LlmProvider for StubProvider {
async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, LlmError> {
unimplemented!()
}
}
#[test]
fn build_with_all_required_succeeds() {
let bundle = AgentBuilder::new()
.provider(Arc::new(StubProvider))
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.build();
assert!(bundle.is_ok());
}
#[test]
fn build_missing_provider_returns_config_error() {
let result = AgentBuilder::new()
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.build();
assert!(matches!(result, Err(AgentError::Config(s)) if s.contains("provider")));
}
#[test]
fn build_missing_tool_registry_returns_config_error() {
let result = AgentBuilder::new()
.provider(Arc::new(StubProvider))
.hook_executor(Arc::new(HookExecutor::new()))
.build();
assert!(matches!(result, Err(AgentError::Config(s)) if s.contains("tool_registry")));
}
#[test]
fn build_missing_hook_executor_returns_config_error() {
let result = AgentBuilder::new()
.provider(Arc::new(StubProvider))
.tool_registry(Arc::new(ToolRegistry::new()))
.build();
assert!(matches!(result, Err(AgentError::Config(s)) if s.contains("hook_executor")));
}
#[test]
fn optional_fields_default_to_none() {
let bundle = AgentBuilder::new()
.provider(Arc::new(StubProvider))
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.build()
.unwrap();
assert!(bundle.memory_store.is_none());
assert!(bundle.retriever.is_none());
}
}
+183
View File
@@ -0,0 +1,183 @@
//! Agent Runtime 统一错误类型。
//!
//! `AgentError` 聚合 Phase 0-3 各层错误(LlmError / ToolError / MemoryError),
//! 加上 Agent 层特有的错误变体。设计原则:
//!
//! - 聚合而非包装:保留内层错误的类型信息(避免 `Box<dyn Error>` 丢失上下文)
//! - 显式 `From` 实现:让 `?` 运算符能透明传播下层错误
//! - `is_recoverable()`:根据变体类型判定可恢复性,便于上层决策
use thiserror::Error;
use crate::llm::error::LlmError;
use crate::memory::error::MemoryError;
use crate::tools::error::ToolError;
/// Agent Runtime 统一错误枚举。
///
/// **不实现 `Clone`**:透传内层 `LlmError` / `MemoryError`,两者均未派生 `Clone`(保留
/// 完整错误信息,传递所有权)。如需在多 session 间共享错误状态,用 `Arc<AgentError>` 包装。
#[derive(Debug, Error)]
pub enum AgentError {
/// LLM 调用错误(透传 Phase 0)。
#[error("LLM 错误: {0}")]
Llm(#[from] LlmError),
/// 工具调用错误(透传 Phase 2)。
#[error("工具错误: {0}")]
Tool(#[from] ToolError),
/// 记忆系统错误(透传 Phase 3)。
#[error("记忆错误: {0}")]
Memory(#[from] MemoryError),
/// Plan 解析失败(Phase 4b 新增)。
#[error("Plan 解析错误: {0}")]
PlanParse(String),
/// 钩子阻断操作(Agent 层特有)。
#[error("钩子阻断: {0}")]
HookBlocked(String),
/// 达到限制阈值(最大 turn、token 预算等)。
#[error("超过限制: {0}")]
LimitExceeded(String),
/// 配置错误(构建 RuntimeBundle / AgentSession 时校验失败)。
#[error("配置错误: {0}")]
Config(String),
/// 其他未分类错误(兜底)。
#[error("Agent 错误: {0}")]
Other(String),
}
impl AgentError {
/// 判定错误是否可恢复。
///
/// - `Llm` / `Memory`:由内层 `is_recoverable()` 决定
/// - `Tool`:由内层 `is_recoverable()` 决定
/// - `HookBlocked` / `LimitExceeded`:不可恢复(需人工介入或终止循环)
/// - `Config` / `Other`:不可恢复
pub fn is_recoverable(&self) -> bool {
match self {
Self::Llm(e) => matches!(
e,
LlmError::RateLimit { .. } | LlmError::Timeout { .. } | LlmError::Stream(_)
),
Self::Tool(e) => e.is_recoverable(),
Self::Memory(e) => e.is_recoverable(),
Self::PlanParse(_) => false,
Self::HookBlocked(_) | Self::LimitExceeded(_) | Self::Config(_) | Self::Other(_) => {
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn llm_recoverable_propagation() {
let err = AgentError::Llm(LlmError::Timeout {
duration: std::time::Duration::from_secs(30),
});
assert!(err.is_recoverable());
}
#[test]
fn llm_non_recoverable_propagation() {
let err = AgentError::Llm(LlmError::Authentication("bad key".into()));
assert!(!err.is_recoverable());
}
#[test]
fn tool_recoverable_propagation() {
let err = AgentError::Tool(ToolError::ExecutionFailed("foo".into(), "boom".into()));
assert!(err.is_recoverable());
}
#[test]
fn tool_non_recoverable_propagation() {
let err = AgentError::Tool(ToolError::NotFound("foo".into()));
assert!(!err.is_recoverable());
}
#[test]
fn memory_recoverable_propagation() {
let err = AgentError::Memory(MemoryError::NotFound("page".into()));
assert!(err.is_recoverable());
}
#[test]
fn memory_non_recoverable_propagation() {
let err = AgentError::Memory(MemoryError::Storage("disk full".into()));
assert!(!err.is_recoverable());
}
#[test]
fn hook_blocked_not_recoverable() {
assert!(!AgentError::HookBlocked("denied".into()).is_recoverable());
}
#[test]
fn limit_exceeded_not_recoverable() {
assert!(!AgentError::LimitExceeded("max turns".into()).is_recoverable());
}
#[test]
fn config_not_recoverable() {
assert!(!AgentError::Config("missing provider".into()).is_recoverable());
}
#[test]
fn other_not_recoverable() {
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> {
Err(LlmError::Other("test".into()))
}
fn caller() -> Result<(), AgentError> {
returns_llm()?;
Ok(())
}
let err = caller().unwrap_err();
assert!(matches!(err, AgentError::Llm(_)));
}
#[test]
fn from_tool_via_question_mark() {
fn returns_tool() -> Result<(), ToolError> {
Err(ToolError::NotFound("x".into()))
}
fn caller() -> Result<(), AgentError> {
returns_tool()?;
Ok(())
}
let err = caller().unwrap_err();
assert!(matches!(err, AgentError::Tool(_)));
}
#[test]
fn from_memory_via_question_mark() {
fn returns_mem() -> Result<(), MemoryError> {
Err(MemoryError::Storage("x".into()))
}
fn caller() -> Result<(), AgentError> {
returns_mem()?;
Ok(())
}
let err = caller().unwrap_err();
assert!(matches!(err, AgentError::Memory(_)));
}
}
+121
View File
@@ -0,0 +1,121 @@
//! Runtime Bundle —— 显式依赖注入容器(OpenHarness 风格)。
//!
//! 集中持有 Agent 运行所需的全部运行时依赖:`LlmProvider` / `ToolRegistry` / `HookExecutor` /
//! `MemoryStore`(弱引用)/ `MemoryRetriever`(弱引用) / `AgentConfig`。
//!
//! **设计意图**(参见 `docs/7-agent-runtime.md` §3.2.2):
//!
//! - 所有运行时依赖显式打包,便于跨 `AgentSession` 共享、便于测试注入 mock
//! - `memory_store` / `retriever` 为 `Option`:上层应用不传也能跑(无记忆模式)
//! - 构造时若 `retriever` 为 `Some`,自动注册 `"retrieve"` toolv0.1 占位——
//! Phase 4a 不在 `submit_turn` 中真正调用;Phase 4a 任务范围仅"装配可注册",
//! 真正的 `RetrieveTool` 实现留待 v0.2 接入)
//! - 不持有 `Box<dyn LlmProvider>` 而是 `Arc<dyn LlmProvider>`:支持多 session 共享
use std::sync::Arc;
use std::time::Duration;
use crate::llm::compact::CompactConfig;
use crate::llm::provider::LlmProvider;
use crate::llm::hooks::HookExecutor;
use crate::memory::retriever::MemoryRetriever;
use crate::memory::store::MemoryStore;
use crate::tools::ToolRegistry;
/// Agent 运行配置。
#[derive(Debug, Clone)]
pub struct AgentConfig {
/// 单次会话最大 turn 数(含工具循环内部 turn),默认 50。
pub max_turns: u32,
/// 单次会话最大工具循环轮次(与 LlmCycle 的 `max_tool_turns` 对齐),默认 10。
pub max_tool_turns: u32,
/// 会话 TTLNone 表示无过期),默认 None。
pub session_ttl: Option<Duration>,
/// 上下文压缩配置(None 表示不启用自动压缩),默认 None。
pub compact_config: Option<CompactConfig>,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
max_turns: 50,
max_tool_turns: 10,
session_ttl: None,
compact_config: None,
}
}
}
/// Agent Runtime 依赖注入容器。
///
/// 通过 `AgentBuilder::build()` 构造;构造完成后内部为只读视图。
/// `Arc` 共享,多个 `AgentSession` 可共用同一个 bundle。
#[derive(Clone)]
pub struct RuntimeBundle {
/// LLM 后端(强引用,多 session 共享)。
pub provider: Arc<dyn LlmProvider>,
/// 工具注册表(强引用,多 session 共享)。
pub tool_registry: Arc<ToolRegistry>,
/// 钩子执行器(强引用,多 session 共享)。
pub hook_executor: Arc<HookExecutor>,
/// 持久化记忆后端(弱引用 —— 不传也能跑)。
pub memory_store: Option<Arc<dyn MemoryStore>>,
/// 记忆检索器(弱引用 —— 不传也能跑)。
/// 传入时可在 `submit_turn` 内部将检索能力作为工具暴露给 LLM。
pub retriever: Option<Arc<MemoryRetriever>>,
/// SessionMemory 后端(选填)。
/// 传入时 `SessionMemory` 使用该后端(支持跨进程共享);
/// 不传时 `AgentSession` 内部自动创建 `InMemoryStore` 作为进程级隔离的后端。
pub session_memory_backend: Option<Arc<dyn MemoryStore>>,
/// 运行时配置。
pub config: AgentConfig,
}
impl std::fmt::Debug for RuntimeBundle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuntimeBundle")
.field("provider_type", &"<dyn LlmProvider>")
.field("tool_names", &self.tool_registry.list_tools())
.field("has_memory_store", &self.memory_store.is_some())
.field("has_retriever", &self.retriever.is_some())
.field(
"has_session_memory_backend",
&self.session_memory_backend.is_some(),
)
.field("config", &self.config)
.finish()
}
}
impl RuntimeBundle {
/// 构造一个 `RuntimeBundle`。
///
/// **Phase 4a 行为**`retriever` 存在时仅占位记录,不真正注入工具
/// v0.1 不在 `submit_turn` 中启用检索;Phase 4c 之后再决定是否注册成 tool)。
/// 真正的工具注入留待 v0.2 接入 `RetrieveTool` 实现。
pub fn new(
provider: Arc<dyn LlmProvider>,
tool_registry: Arc<ToolRegistry>,
hook_executor: Arc<HookExecutor>,
memory_store: Option<Arc<dyn MemoryStore>>,
retriever: Option<Arc<MemoryRetriever>>,
session_memory_backend: Option<Arc<dyn MemoryStore>>,
config: AgentConfig,
) -> Self {
Self {
provider,
tool_registry,
hook_executor,
memory_store,
retriever,
session_memory_backend,
config,
}
}
}
+356
View File
@@ -0,0 +1,356 @@
//! AgentSession —— 智能体"会话"实例。
//!
//! 设计要点(参见 `docs/7-agent-runtime.md` §3.2.3):
//!
//! - **会话 = 角色 + 状态**:绑定 `session_id` / `agent` / `bundle`,累计 `turn_index` 和 `cost_so_far`
//! - **最小 reference impl**`submit_turn` 演示"组装 LlmCycle → submit_with_tools → 累计 cost"的标准流程
//! - **不做业务循环**:多轮策略、错误重试、记忆回写由上层应用或具体 `TaskAgent` 决定
//! - **不持有 ConversationMemory**:上层可独立 new 一个 `ConversationMemory`,在合适的时机调 `add_message`
use std::sync::Arc;
use crate::agent::agent::Agent;
use crate::agent::error::AgentError;
use crate::agent::runtime::RuntimeBundle;
use crate::agent::session_memory::SessionMemory;
use crate::llm::cycle::{CostTracker, CycleConfig, LlmCycle};
use crate::llm::hooks::{HookContext, HookEvent};
use crate::llm::types::ChatResponse;
use crate::memory::store::InMemoryStore;
/// Agent 会话实例。
///
/// 同一 `Agent` 可被多个 `AgentSession` 复用(不同 session_id 互不干扰)。
/// `submit_turn` 一次只跑一轮 LLM 调用(含自动 tool 循环)。
///
/// **不实现 `Clone`**session 持有累计 `turn_index` / `cost_so_far` / `session_memory`
/// 共享这些状态需要显式 sync 语义;如果上层需要并发访问,自己用 `Arc<Mutex<_>>` 包装。
pub struct AgentSession {
/// 会话 ID(由调用方指定,用于日志/追踪/记忆关联)。
pub session_id: String,
/// 角色(可热切换为同 bundle 下的其他角色)。
pub agent: Arc<dyn Agent>,
bundle: Arc<RuntimeBundle>,
turn_index: u32,
cost_so_far: CostTracker,
/// 会话级记忆(Phase 4c 替换内联 HashMap)。
pub session_memory: SessionMemory,
}
impl std::fmt::Debug for AgentSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AgentSession")
.field("session_id", &self.session_id)
.field("agent", &self.agent.name())
.field("turn_index", &self.turn_index)
.field("cost_so_far", &self.cost_so_far.total())
.field("session_memory", &"<SessionMemory>")
.finish()
}
}
impl AgentSession {
/// 创建一个新的会话实例。
///
/// `agent` 与 `bundle` 共同决定 `submit_turn` 行为:system_prompt / 工具集 / LLM 后端均来自它们。
pub fn new(
agent: Arc<dyn Agent>,
session_id: impl Into<String>,
bundle: Arc<RuntimeBundle>,
) -> Self {
let session_id_str = session_id.into();
let backend = bundle
.session_memory_backend
.clone()
.unwrap_or_else(|| Arc::new(InMemoryStore::new()));
let session_memory = SessionMemory::new(backend, &session_id_str);
Self {
session_id: session_id_str,
agent,
bundle,
turn_index: 0,
cost_so_far: CostTracker::default(),
session_memory,
}
}
/// 当前 turn 序号(0-based:第一次 `submit_turn` 完成后变 1)。
pub fn turn_index(&self) -> u32 {
self.turn_index
}
/// 累计用量(跨所有 turn)。
pub fn usage(&self) -> &CostTracker {
&self.cost_so_far
}
/// 会话级记忆引用。
pub fn session_memory(&self) -> &SessionMemory {
&self.session_memory
}
/// 写入一条会话级数据(覆盖同名 key)。
pub async fn set_session_data(
&mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Result<(), AgentError> {
self.session_memory.set(&key.into(), &value.into()).await
}
/// 读取一条会话级数据。
pub async fn get_session_data(&self, key: &str) -> Result<Option<String>, AgentError> {
self.session_memory.get(key).await
}
/// 提交一轮对话(含自动 tool 循环),返回 LLM 响应。
///
/// 流程:
/// 1. 触发 `OnTurnStart` hook
/// 2. 组装 `LlmCycle`(注入 system_prompt / hook_executor / compact_config / 消息历史)
/// 3. `submit_with_tools` 跑单轮对话
/// 4. 累计 `cost_so_far`
/// 5. 触发 `OnTurnEnd` hook
/// 6. `turn_index += 1`
///
/// **不做**
/// - 不持有 `ConversationMemory`(由上层独立 task 决定何时回写)
/// - 不做 Plan 拆解(Phase 4b 才加 `TaskAgent`
/// - 不做 session_data 持久化(Phase 4c 替换为 `SessionMemory`
pub async fn submit_turn(
&mut self,
user_input: impl Into<String>,
) -> Result<ChatResponse, AgentError> {
let turn_index = self.turn_index;
let hook_executor = Arc::clone(&self.bundle.hook_executor);
// 1. 触发 OnTurnStart hook
let start_ctx =
HookContext::new(HookEvent::OnTurnStart).with_turn_index(turn_index);
hook_executor
.execute(HookEvent::OnTurnStart, &start_ctx)
.await;
// 2. 组装 LlmCycle —— 共享 bundle 中的 provider 句柄
// 工具列表从 agent.tool_definitions(bundle) 派生(默认 = bundle 全量);
// submit_with_tools 内部从 registry 自行取 definitions,此处仅消费以触发
// 子 trait 覆盖(白名单/过滤)的副作用。
let _ = self.agent.tool_definitions(&self.bundle);
let mut cycle = LlmCycle::new_with_arc(Arc::clone(&self.bundle.provider), CycleConfig::default())
.with_messages(Vec::new());
if let Some(prompt) = self.agent.system_prompt() {
cycle = cycle.with_system_prompt(prompt.to_string());
}
if let Some(cfg) = self.bundle.config.compact_config.clone() {
cycle = cycle.with_compact_config(cfg);
}
// 3. 提交(HookExecutor 不在这里传——内部 hook 由 LlmCycle 在 PreRequest/PostRequest 触发)
let response = cycle
.submit_with_tools(user_input.into(), &self.bundle.tool_registry)
.await?;
// 4. 累计 cost
self.cost_so_far.add(&response.usage);
// 5. 触发 OnTurnEnd hook
let end_ctx = HookContext::new(HookEvent::OnTurnEnd).with_turn_index(turn_index);
hook_executor.execute(HookEvent::OnTurnEnd, &end_ctx).await;
// 6. turn_index 递增
self.turn_index += 1;
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::builder::AgentBuilder;
use crate::llm::hooks::{Hook, HookContext, HookExecutor, HookResult};
use crate::llm::provider::LlmProvider;
use crate::llm::types::{
ChatRequest, ChatResponse, FinishReason, OpenaiChatMessage,
};
use crate::tools::ToolRegistry;
use async_trait::async_trait;
use std::sync::atomic::{AtomicU32, Ordering};
/// 计数 hook —— 每被调用一次 +1。
struct CountHook(AtomicU32);
#[async_trait]
impl Hook for CountHook {
async fn execute(&self, _ctx: &HookContext<'_>) -> HookResult {
self.0.fetch_add(1, Ordering::SeqCst);
HookResult::allow()
}
}
/// 把 `Arc<CountHook>` 包装为 `Box<dyn Hook>`dyn Hook 不能直接来自 Arc)。
struct CountHookAdapter(Arc<CountHook>);
#[async_trait]
impl Hook for CountHookAdapter {
async fn execute(&self, ctx: &HookContext<'_>) -> HookResult {
self.0.execute(ctx).await
}
}
/// MockProvider:按调用顺序返回预设响应。
struct MockProvider {
responses: std::sync::Mutex<Vec<ChatResponse>>,
}
impl MockProvider {
fn new(responses: Vec<ChatResponse>) -> Self {
Self {
responses: std::sync::Mutex::new(responses),
}
}
}
#[async_trait]
impl LlmProvider for MockProvider {
async fn chat(&self, _request: ChatRequest) -> Result<ChatResponse, crate::llm::error::LlmError> {
let mut responses = self.responses.lock().unwrap();
if responses.is_empty() {
return Err(crate::llm::error::LlmError::Other(
"no more mock responses".into(),
));
}
Ok(responses.remove(0))
}
}
struct StubAgent {
name: String,
prompt: Option<String>,
}
impl Agent for StubAgent {
fn name(&self) -> &str {
&self.name
}
fn system_prompt(&self) -> Option<&str> {
self.prompt.as_deref()
}
}
fn assistant_text(text: &str) -> ChatResponse {
ChatResponse {
message: OpenaiChatMessage::assistant_text(text),
usage: crate::llm::types::Usage::from_input_output(10, 5),
stop_reason: Some(FinishReason::Stop),
}
}
/// 烟雾测试 1AgentSession::submit_turn 跑通 mock provider。
#[tokio::test]
async fn submit_turn_runs_with_mock_provider() {
let provider = Arc::new(MockProvider::new(vec![assistant_text("hello back")]));
let agent = Arc::new(StubAgent {
name: "stub".into(),
prompt: Some("you are a test agent".into()),
});
let bundle = Arc::new(
AgentBuilder::new()
.provider(provider)
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.build()
.unwrap(),
);
let mut session = AgentSession::new(agent, "s1", bundle);
assert_eq!(session.turn_index(), 0);
let response = session.submit_turn("hi").await.unwrap();
let text = match &response.message {
OpenaiChatMessage::Assistant { content, .. } => {
if let crate::llm::types::ContentField::String(s) = content {
s.clone()
} else {
String::new()
}
}
_ => String::new(),
};
assert_eq!(text, "hello back");
assert_eq!(session.turn_index(), 1);
assert_eq!(session.usage().total().prompt_tokens, 10);
assert_eq!(session.usage().total().completion_tokens, 5);
}
/// 烟雾测试 2session_data 读写。
#[tokio::test]
async fn session_data_set_get() {
let provider = Arc::new(MockProvider::new(vec![]));
let agent = Arc::new(StubAgent {
name: "stub".into(),
prompt: None,
});
let bundle = Arc::new(
AgentBuilder::new()
.provider(provider)
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.build()
.unwrap(),
);
let mut session = AgentSession::new(agent, "s2", bundle);
assert!(session.get_session_data("k").await.unwrap().is_none());
session.set_session_data("k", "v").await.unwrap();
assert_eq!(session.get_session_data("k").await.unwrap(), Some("v".into()));
// 覆盖写
session.set_session_data("k", "v2").await.unwrap();
assert_eq!(
session.get_session_data("k").await.unwrap(),
Some("v2".into())
);
}
/// 烟雾测试 3submit_turn 触发 OnTurnStart / OnTurnEnd hook。
#[tokio::test]
async fn submit_turn_triggers_turn_hooks() {
let mut hook_executor = HookExecutor::new();
let start_count = Arc::new(CountHook(AtomicU32::new(0)));
let end_count = Arc::new(CountHook(AtomicU32::new(0)));
hook_executor.register(
HookEvent::OnTurnStart,
Box::new(CountHookAdapter(start_count.clone())),
);
hook_executor.register(
HookEvent::OnTurnEnd,
Box::new(CountHookAdapter(end_count.clone())),
);
let provider = Arc::new(MockProvider::new(vec![
assistant_text("ok"),
assistant_text("ok 2"),
]));
let agent = Arc::new(StubAgent {
name: "stub".into(),
prompt: None,
});
let bundle = Arc::new(
AgentBuilder::new()
.provider(provider)
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(hook_executor))
.build()
.unwrap(),
);
let mut session = AgentSession::new(agent, "s3", bundle);
session.submit_turn("hi").await.unwrap();
assert_eq!(start_count.0.load(Ordering::SeqCst), 1);
assert_eq!(end_count.0.load(Ordering::SeqCst), 1);
session.submit_turn("hi again").await.unwrap();
assert_eq!(start_count.0.load(Ordering::SeqCst), 2);
assert_eq!(end_count.0.load(Ordering::SeqCst), 2);
}
}
+184
View File
@@ -0,0 +1,184 @@
//! SessionMemory —— 会话级记忆,用于 context 间的信息桥接。
//!
//! 设计要点(参见 `docs/7-agent-runtime.md` §3.2.8):
//!
//! - **会话级**:单 session 内共享,跨 context 桥接信息(不是持久层,也不是对话历史)
//! - **复用 Phase 3 `MemoryStore`**:不引入新的存储后端机制
//! - **按 `namespace` 隔离**:每个 session 一个独立命名空间,防止跨 session 泄漏
//! - **`snapshot()` 格式化为标记文本**:专为注入 system prompt 设计
//! - **所有方法为 `async`**:因为后端可能是跨进程的(Redis / DB)
use std::sync::Arc;
use time::OffsetDateTime;
use crate::agent::error::AgentError;
use crate::memory::store::MemoryStore;
use crate::memory::types::{MemoryFilter, MemoryItem};
/// 会话级记忆实例。
///
/// 基于 [`MemoryStore`] 后端,按 `namespace` 隔离键值数据。
/// 适用于 session 内各 context 之间的信息桥接(如将关键结论传递给后续 context)。
pub struct SessionMemory {
store: Arc<dyn MemoryStore>,
namespace: String,
}
impl SessionMemory {
/// 创建新的 session 级记忆实例。
///
/// - `store`:后端存储(可跨进程共享的 `MemoryStore` 实现)。
/// - `namespace`:按 session_id 隔离,防止跨 session 泄漏。
/// 内部会自动添加 `"_session_"` 前缀。
pub fn new(store: Arc<dyn MemoryStore>, namespace: &str) -> Self {
Self {
store,
namespace: format!("_session_{namespace}"),
}
}
/// 内部 key 格式:`"{namespace}:{key}"`。
fn internal_key(&self, key: &str) -> String {
format!("{}:{}", self.namespace, key)
}
/// 写入一条 key-value 条目(覆盖同名 key)。
pub async fn set(&self, key: &str, value: &str) -> Result<(), AgentError> {
let item = MemoryItem {
id: self.internal_key(key),
content: value.to_string(),
metadata: serde_json::json!({}),
created_at: OffsetDateTime::now_utc(),
};
self.store.save(item).await.map_err(AgentError::Memory)
}
/// 读取指定 key 的值。
pub async fn get(&self, key: &str) -> Result<Option<String>, AgentError> {
let item = self
.store
.get(&self.internal_key(key))
.await
.map_err(AgentError::Memory)?;
Ok(item.map(|i| i.content))
}
/// 返回所有条目的格式化快照,适合注入 system prompt。
///
/// 格式:
/// ```text
/// <session-context>
/// key1: value1
/// key2: value2
/// </session-context>
/// ```
pub async fn snapshot(&self) -> Result<String, AgentError> {
let filter = MemoryFilter {
prefix: Some(format!("{}:", self.namespace)),
..Default::default()
};
let items = self
.store
.list(&filter)
.await
.map_err(AgentError::Memory)?;
let mut lines = Vec::with_capacity(items.len() + 2);
lines.push("<session-context>".to_string());
for item in items {
// 从 id 中提取原始 key(去掉 namespace 前缀)
let key = item
.id
.strip_prefix(&format!("{}:", self.namespace))
.unwrap_or(&item.id);
lines.push(format!("{}: {}", key, item.content));
}
lines.push("</session-context>".to_string());
Ok(lines.join("\n"))
}
/// 删除指定 key。
pub async fn remove(&self, key: &str) -> Result<(), AgentError> {
self.store
.delete(&self.internal_key(key))
.await
.map_err(AgentError::Memory)
}
/// 清空当前 namespace 下所有条目。
pub async fn clear(&self) -> Result<(), AgentError> {
let filter = MemoryFilter {
prefix: Some(format!("{}:", self.namespace)),
..Default::default()
};
let items = self
.store
.list(&filter)
.await
.map_err(AgentError::Memory)?;
for item in items {
self.store
.delete(&item.id)
.await
.map_err(AgentError::Memory)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::store::InMemoryStore;
fn make_store() -> Arc<dyn MemoryStore> {
Arc::new(InMemoryStore::new())
}
/// 烟雾测试 1set / get / remove 基本读写。
#[tokio::test]
async fn set_get_remove() {
let mem = SessionMemory::new(make_store(), "test-session");
assert!(mem.get("k").await.unwrap().is_none());
mem.set("k", "v").await.unwrap();
assert_eq!(mem.get("k").await.unwrap(), Some("v".into()));
mem.remove("k").await.unwrap();
assert!(mem.get("k").await.unwrap().is_none());
}
/// 烟雾测试 2snapshot 格式化输出。
#[tokio::test]
async fn snapshot_format() {
let mem = SessionMemory::new(make_store(), "s1");
mem.set("design", "PostgreSQL").await.unwrap();
mem.set("lang", "Rust").await.unwrap();
let snap = mem.snapshot().await.unwrap();
assert!(snap.contains("<session-context>"));
assert!(snap.contains("</session-context>"));
assert!(snap.contains("design: PostgreSQL"));
assert!(snap.contains("lang: Rust"));
}
/// 烟雾测试 3clear 清空当前 namespace。
#[tokio::test]
async fn clear_only_affects_own_namespace() {
let store = make_store();
let mem_a = SessionMemory::new(store.clone(), "a");
let mem_b = SessionMemory::new(store.clone(), "b");
mem_a.set("key", "val_a").await.unwrap();
mem_b.set("key", "val_b").await.unwrap();
mem_a.clear().await.unwrap();
assert!(mem_a.get("key").await.unwrap().is_none());
assert_eq!(mem_b.get("key").await.unwrap(), Some("val_b".into()));
}
}
+243
View File
@@ -0,0 +1,243 @@
//! 任务规划数据结构 + 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::ChatResponse;
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)]
pub enum StepStatus {
/// 初始状态 —— 等待执行。
Pending,
/// 正在执行(`TaskAgent::execute_plan` 进入)。
Running,
/// 已完成(含 LLM 响应)。
Completed(ChatResponse),
/// 失败(含错误)。
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(_)));
}
}
+1
View File
@@ -1,5 +1,6 @@
//! agcore —— 智能体(Agent)核心工具箱。
pub mod agent;
pub mod llm;
pub mod memory;
pub mod prompt;
+12 -2
View File
@@ -63,7 +63,7 @@ impl Default for CycleConfig {
/// LLM 调用周期 —— 管理一次或多次 LLM 请求的生命周期。
pub struct LlmCycle {
provider: Box<dyn LlmProvider>,
provider: Arc<dyn LlmProvider>,
config: CycleConfig,
usage: CostTracker,
messages: Vec<OpenaiChatMessage>,
@@ -74,8 +74,18 @@ pub struct LlmCycle {
}
impl LlmCycle {
/// 创建一个新的 LlmCycle。
/// 创建一个新的 LlmCycle(持有 `Box<dyn LlmProvider>` 的独占所有权)
///
/// 内部将 Box 转为 `Arc<dyn LlmProvider>` 以便 `new_with_arc` 复用句柄。
/// 公共签名保持不变,向后兼容。
pub fn new(provider: Box<dyn LlmProvider>, config: CycleConfig) -> Self {
Self::new_with_arc(Arc::from(provider), config)
}
/// 创建一个新的 LlmCycle,共享传入的 `Arc<dyn LlmProvider>` 句柄。
///
/// **新增**Phase 4a 引入):用于 `AgentSession::submit_turn` 在多 session 间共享 provider。
pub fn new_with_arc(provider: Arc<dyn LlmProvider>, config: CycleConfig) -> Self {
Self {
provider,
config,
+24
View File
@@ -16,6 +16,12 @@ pub enum HookEvent {
OnRetry,
/// 不可恢复错误返回之前。
OnError,
/// Agent 会话开始一轮 turn 之前(Phase 4a 新增)。
OnTurnStart,
/// Agent 会话完成一轮 turn 之后(Phase 4a 新增)。
OnTurnEnd,
/// TaskAgent 完成一个 Plan 步骤后触发(Phase 4b 新增)。
OnPlanStepComplete,
}
/// 此次钩子调用的上下文。
@@ -29,6 +35,10 @@ pub struct HookContext<'a> {
pub error: Option<&'a LlmError>,
/// 当前重试次数(从 1 开始,仅 OnRetry 可用)。
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> {
@@ -38,6 +48,8 @@ impl<'a> HookContext<'a> {
request: None,
error: None,
attempt: 0,
turn_index: None,
plan_step_index: None,
}
}
@@ -55,6 +67,18 @@ impl<'a> HookContext<'a> {
self.attempt = attempt;
self
}
/// 设置 turn 序号(仅 OnTurnStart / OnTurnEnd 使用)。
pub(crate) fn with_turn_index(mut self, turn_index: u32) -> Self {
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
}
}
/// 钩子执行结果。