Compare commits
8 Commits
336920554a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 829be90d19 | |||
| ce1f1aaca0 | |||
| 4de7db0b2c | |||
| 2b189880a9 | |||
| 59ec0f5597 | |||
| be595a6771 | |||
| 75f8736931 | |||
| b539f37eeb |
+346
-109
@@ -21,14 +21,33 @@ AG Core 已完成 Phase 0(LLM 调用周期)、Phase 1(提示词工程)
|
|||||||
|
|
||||||
### 1.2 目标
|
### 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)
|
- **`Agent` trait** — 智能体的"角色"抽象(不绑定 session)
|
||||||
- **`AgentSession` struct** — 智能体的"会话"实例(绑定 session_id + 状态)
|
- **`AgentSession` struct** — 智能体的"会话"实例(绑定 session_id + 状态)
|
||||||
- **`TaskAgent` trait** — 任务型智能体的"规划/执行"抽象
|
- **`RuntimeBundle`** — 显式依赖注入容器,集中管理 provider/registry/hook 等依赖
|
||||||
- **`RuntimeBundle`** — 显式依赖注入容器,集中管理 provider/registry/hook/memory 等依赖
|
|
||||||
- **`AgentBuilder`** — 链式构造入口
|
- **`AgentBuilder`** — 链式构造入口
|
||||||
- **`AgentError`** — 统一错误类型,聚合 LlmError / ToolError / MemoryError
|
- **`AgentError`** — 统一错误类型,聚合 LlmError / ToolError / MemoryError
|
||||||
|
- **`Plan` / `Step` / `StepStatus`** — 任务规划纯数据结构(不做解析逻辑)
|
||||||
|
|
||||||
|
Phase 4b 追加:
|
||||||
|
|
||||||
|
- **`TaskAgent` trait** — 任务型智能体的"规划/执行"抽象
|
||||||
|
- **`PlanParser` trait + `JsonPlanParser`** — Plan 解析接口与参考实现
|
||||||
|
|
||||||
|
Phase 4c 追加:
|
||||||
|
|
||||||
|
- **`SessionMemory`** — 会话级记忆,用于 context 间的信息桥接(基于 `MemoryStore` 后端)
|
||||||
|
|
||||||
### 1.3 设计原则
|
### 1.3 设计原则
|
||||||
|
|
||||||
@@ -42,6 +61,7 @@ Phase 4 严格遵循以下原则,所有范围决策都基于这些原则推导
|
|||||||
| **实体/会话分离** | 同一角色可被多 session 复用 | `Agent` + `AgentSession` 两层模型 |
|
| **实体/会话分离** | 同一角色可被多 session 复用 | `Agent` + `AgentSession` 两层模型 |
|
||||||
| **记忆弱引用** | 记忆是"被动能力",不内嵌循环 | `memory_store: Option<Arc<dyn MemoryStore>>` 弱引用 |
|
| **记忆弱引用** | 记忆是"被动能力",不内嵌循环 | `memory_store: Option<Arc<dyn MemoryStore>>` 弱引用 |
|
||||||
| **业务可注入** | Plan 拆解是业务能力,不在 core 库实现 | 暴露 `PlanParser` trait,上层注入 |
|
| **业务可注入** | Plan 拆解是业务能力,不在 core 库实现 | 暴露 `PlanParser` trait,上层注入 |
|
||||||
|
| **会话级记忆** | session 内共享、context 间桥接,不是持久层也不是对话历史 | `SessionMemory` 基于 `MemoryStore`,按 session_id 命名空间隔离 |
|
||||||
| **借鉴不照搬** | 4 个参考项目均非 Rust 实现 | 只取架构模式,不抄实现细节 |
|
| **借鉴不照搬** | 4 个参考项目均非 Rust 实现 | 只取架构模式,不抄实现细节 |
|
||||||
|
|
||||||
### 1.4 与已完成的 Phase 关系
|
### 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 范围)
|
应用层 (L4) ── 上层 crate / 二进制 / Gateway(不在 Phase 4 范围)
|
||||||
```
|
```
|
||||||
@@ -65,23 +87,31 @@ Phase 4 (L1→L2) ── Agent trait + AgentSession + TaskAgent + RuntimeBund
|
|||||||
|
|
||||||
### 2.1 功能需求
|
### 2.1 功能需求
|
||||||
|
|
||||||
| ID | 需求 | 优先级 | 说明 |
|
| ID | 需求 | 优先级 | 归属 | 说明 |
|
||||||
|----|------|--------|------|
|
|----|------|--------|------|------|
|
||||||
| F1 | `Agent` trait 抽象 | P0 | 角色定义:name / system_prompt / 工具集 |
|
| F1 | `Agent` trait 抽象 | P0 | 4a | 角色定义:name / system_prompt / 工具集 |
|
||||||
| F2 | `AgentSession` 会话实例 | P0 | 绑定 session_id、bundle、turn_index、cost_so_far |
|
| F2 | `AgentSession` 会话实例 | P0 | 4a | 绑定 session_id、bundle、turn_index、cost_so_far |
|
||||||
| F3 | `submit_turn()` 最小 reference impl | P0 | 组装 LlmCycle → submit → 累计 cost;约 30 行 |
|
| F3 | `submit_turn()` 最小 reference impl | P0 | 4a | 组装 LlmCycle → submit → 累计 cost;~30 行 |
|
||||||
| F4 | `TaskAgent::run(goal)` 自主式入口 | P0 | 内部用 LLM 拆 Plan,再调用 `execute_plan` |
|
| F6 | `Plan` / `Step` / `StepStatus` 数据结构 | P0 | 4a | 含 Pending / Running / Completed / Failed / Skipped 状态机 |
|
||||||
| F5 | `TaskAgent::execute_plan(plan)` 外部驱动式入口 | P0 | 用户预定义 Plan,逐步执行 |
|
| F8 | `RuntimeBundle` 依赖注入容器 | P0 | 4a | 聚合 provider/registry/hook/config(不含 session_memory_backend) |
|
||||||
| F6 | `Plan` / `Step` / `StepStatus` 数据结构 | P0 | 含 Pending / Running / Completed / Failed / Skipped 状态机 |
|
| F9 | `AgentBuilder` 链式构造 | P0 | 4a | 构建 `RuntimeBundle`,retriever 存在时自动注册为 tool |
|
||||||
| F7 | `PlanParser` trait + `JsonPlanParser` 参考实现 | P0 | 注入式,上层可替换 |
|
| F10 | `AgentError` 统一错误类型 | P0 | 4a | 聚合 LlmError / ToolError / MemoryError,含 `is_recoverable()` |
|
||||||
| F8 | `RuntimeBundle` 依赖注入容器 | P0 | 聚合 provider/registry/hook/memory/retriever/config |
|
| F11a | Hook 事件扩展:OnTurnStart / OnTurnEnd + turn_index 字段 | P0 | 4a | 在 `llm/hooks.rs` 中追加 2 个事件 + 1 个字段 |
|
||||||
| F9 | `AgentBuilder` 链式构造 | P0 | 构建 `RuntimeBundle`,retriever 存在时自动注册为 tool |
|
| F12a | 烟雾测试 3-4 个(Phase 4a) | P0 | 4a | trait 可装配 / RuntimeBundle 可构造 / submit_turn 跑通 mock / Plan 数据结构 |
|
||||||
| F10 | `AgentError` 统一错误类型 | P0 | 聚合 LlmError / ToolError / MemoryError,含 `is_recoverable()` |
|
| F13 | `lib.rs` 导出 `pub mod agent;` | P0 | 4a | 一行 |
|
||||||
| F11 | Hook 事件扩展:OnTurnStart / OnTurnEnd / OnPlanStepComplete | P0 | 在 `llm/hooks.rs` 中追加 3 个事件 + 上下文扩展 2 个字段 |
|
| F14 | 方案文档(本文件)+ 决策记录 | P0 | — | ✅ 已完成 |
|
||||||
| F12 | 烟雾测试 2-3 个 | P0 | trait 可装配 / RuntimeBundle 可构造 / `submit_turn` 跑通 mock |
|
| F4 | `TaskAgent::run(goal)` 自主式入口 | P0 | 4b | 内部用 LLM 拆 Plan,再调用 `execute_plan` |
|
||||||
| F13 | `lib.rs` 导出 `pub mod agent;` | P0 | 一行 |
|
| F5 | `TaskAgent::execute_plan(plan)` 外部驱动式入口 | P0 | 4b | 用户预定义 Plan,逐步执行 |
|
||||||
| F14 | 方案文档(本文件)+ 决策记录 | P0 | 已完成 |
|
| F7 | `PlanParser` trait + `JsonPlanParser` 参考实现 | P0 | 4b | 注入式,上层可替换 |
|
||||||
| F15 | Roadmap 状态翻转 | P0 | 实施完成后做 |
|
| 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 非功能需求
|
### 2.2 非功能需求
|
||||||
|
|
||||||
@@ -190,6 +220,7 @@ pub struct RuntimeBundle {
|
|||||||
pub hook_executor: Arc<HookExecutor>,
|
pub hook_executor: Arc<HookExecutor>,
|
||||||
pub memory_store: Option<Arc<dyn MemoryStore>>, // 弱引用
|
pub memory_store: Option<Arc<dyn MemoryStore>>, // 弱引用
|
||||||
pub retriever: Option<Arc<MemoryRetriever>>, // 弱引用
|
pub retriever: Option<Arc<MemoryRetriever>>, // 弱引用
|
||||||
|
pub session_memory_backend: Option<Arc<dyn MemoryStore>>, // SessionMemory 后端(选填)
|
||||||
pub config: AgentConfig,
|
pub config: AgentConfig,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -198,6 +229,7 @@ pub struct RuntimeBundle {
|
|||||||
- 所有运行时依赖**显式打包**(OpenHarness 风格)
|
- 所有运行时依赖**显式打包**(OpenHarness 风格)
|
||||||
- `memory_store` / `retriever` 均为 `Option`——上层应用**不传也能跑**(无记忆模式)
|
- `memory_store` / `retriever` 均为 `Option`——上层应用**不传也能跑**(无记忆模式)
|
||||||
- 当 `retriever` 存在时,`RuntimeBundle::new()` 内部自动注册一个名为 `"retrieve"` 的 tool(具体实现:在 `ToolRegistry` 里加一个 `RetrieveTool` 包装),让 LLM 在对话中**主动**调用检索能力
|
- 当 `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)
|
- `config` 集中管理所有可调参数(max_turns、max_tool_turns、session_ttl、compact_config)
|
||||||
|
|
||||||
#### 3.2.3 `AgentSession` 与最小 reference impl
|
#### 3.2.3 `AgentSession` 与最小 reference impl
|
||||||
@@ -205,10 +237,11 @@ pub struct RuntimeBundle {
|
|||||||
```rust
|
```rust
|
||||||
pub struct AgentSession {
|
pub struct AgentSession {
|
||||||
pub session_id: String,
|
pub session_id: String,
|
||||||
pub agent_name: String,
|
pub agent: Arc<dyn Agent>,
|
||||||
bundle: Arc<RuntimeBundle>,
|
bundle: Arc<RuntimeBundle>,
|
||||||
turn_index: u32,
|
turn_index: u32,
|
||||||
cost_so_far: CostTracker,
|
cost_so_far: CostTracker,
|
||||||
|
session_memory: SessionMemory,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentSession {
|
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" 只演示**最常见**的对话场景
|
- "最小 reference impl" 只演示**最常见**的对话场景
|
||||||
- 业务循环(多轮策略、错误重试、记忆回写时机)由上层应用或具体的 `TaskAgent` 实现决定
|
- 业务循环(多轮策略、错误重试、记忆回写时机)由上层应用或具体的 `TaskAgent` 实现决定
|
||||||
- `submit_turn` 不持有 `ConversationMemory`——上层应用可独立 new 一个 `ConversationMemory`,在合适的时机(如 OnTurnEnd hook)调 `add_message`
|
- `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 hook_executor(self, h: Arc<HookExecutor>) -> Self;
|
||||||
pub fn memory_store(self, m: Arc<dyn MemoryStore>) -> Self; // 选填
|
pub fn memory_store(self, m: Arc<dyn MemoryStore>) -> Self; // 选填
|
||||||
pub fn retriever(self, r: Arc<MemoryRetriever>) -> 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 config(self, c: AgentConfig) -> Self;
|
||||||
pub fn build(self) -> Result<RuntimeBundle, AgentError>;
|
pub fn build(self) -> Result<RuntimeBundle, AgentError>;
|
||||||
}
|
}
|
||||||
@@ -332,7 +368,74 @@ impl AgentBuilder {
|
|||||||
**设计意图**:
|
**设计意图**:
|
||||||
- `AgentBuilder` 是**唯一**的 `RuntimeBundle` 构造入口
|
- `AgentBuilder` 是**唯一**的 `RuntimeBundle` 构造入口
|
||||||
- 必填字段在 `build()` 时校验(`provider` / `tool_registry` / `hook_executor` 不可缺)
|
- 必填字段在 `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 状态机
|
### 3.3 状态机
|
||||||
|
|
||||||
@@ -440,38 +543,44 @@ pub struct HookContext {
|
|||||||
| `JsonPlanParser::parse` | `serde_json::from_str` | task.rs |
|
| `JsonPlanParser::parse` | `serde_json::from_str` | task.rs |
|
||||||
| `AgentError::from` | `LlmError` / `ToolError` / `MemoryError` | error.rs |
|
| `AgentError::from` | `LlmError` / `ToolError` / `MemoryError` | error.rs |
|
||||||
| `HookContext` 扩展 | `HookEvent::OnTurnStart/End/OnPlanStepComplete` | llm/hooks.rs |
|
| `HookContext` 扩展 | `HookEvent::OnTurnStart/End/OnPlanStepComplete` | llm/hooks.rs |
|
||||||
|
| `SessionMemory::set/get/snapshot` | `MemoryStore::save/load/search` | session_memory.rs |
|
||||||
|
|
||||||
**不调用的下层 API**(明确边界):
|
**不调用的下层 API**(明确边界):
|
||||||
- ❌ `ConversationMemory`(由上层独立 task 管理)
|
- ❌ `ConversationMemory`(由上层独立 task 管理)
|
||||||
- ❌ `KnowledgeStore`(由上层独立 task 管理)
|
- ❌ `KnowledgeStore`(由上层独立 task 管理)
|
||||||
- ❌ `McpClient`(已由 `ToolRegistry` 包装)
|
- ❌ `McpClient`(已由 `ToolRegistry` 包装)
|
||||||
- ❌ `StreamEvents::submit_stream`(v1 暂不暴露流式 `submit_turn`,v0.2 再说)
|
- ❌ `StreamEvents::submit_stream`(v1 暂不暴露流式 `submit_turn`,v0.2 再说)
|
||||||
|
- ❌ 多 context 切换管理(v0.2+ 实现,Phase 4 只预留 `SessionMemory` 桥接通道)
|
||||||
|
- ❌ `"session_memory_set"` 等 session memory tool 自动注册(v0.2+ 可选)
|
||||||
|
|
||||||
## 4. 实施计划
|
## 4. 实施计划
|
||||||
|
|
||||||
|
Phase 4 拆分为三个独立子阶段:**Phase 4a(核心胶水层)** → **Phase 4b(任务执行)** → **Phase 4c(会话级记忆)**。每个子阶段独立交付、独立验证,4b 与 4c 无相互依赖。
|
||||||
|
|
||||||
### 4.1 文件清单
|
### 4.1 文件清单
|
||||||
|
|
||||||
#### 新增文件(7 个)
|
#### 新增文件(9 个)
|
||||||
|
|
||||||
```
|
```
|
||||||
src/agent.rs # 模块根 + pub use 重导出
|
src/agent.rs # [4a] 模块根 + pub use 重导出
|
||||||
src/agent/agent.rs # Agent trait
|
src/agent/agent.rs # [4a] Agent trait
|
||||||
src/agent/runtime.rs # RuntimeBundle + AgentConfig
|
src/agent/runtime.rs # [4a] RuntimeBundle + AgentConfig(不含 session_memory_backend)
|
||||||
src/agent/session.rs # AgentSession(含 submit_turn reference impl)
|
src/agent/session.rs # [4a] AgentSession(submit_turn + 内联 session_data HashMap)
|
||||||
src/agent/task.rs # TaskAgent trait + Plan/Step + PlanParser + JsonPlanParser
|
src/agent/task.rs # [4a] Plan / Step / StepStatus 纯数据 / [4b] TaskAgent + PlanParser + JsonPlanParser
|
||||||
src/agent/builder.rs # AgentBuilder
|
src/agent/builder.rs # [4a] AgentBuilder(不含 session_memory_backend)
|
||||||
src/agent/error.rs # AgentError
|
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/lib.rs # [4a] + pub mod agent;
|
||||||
src/llm/hooks.rs # + 3 个事件变体 + 2 个 HookContext 字段
|
src/llm/hooks.rs # [4a] + 2 事件(OnTurnStart/OnTurnEnd)+ 1 字段(turn_index)
|
||||||
docs/roadmap.md # Phase 4 状态 ❌ 缺失 → ✅
|
# [4b] + 1 事件(OnPlanStepComplete)+ 1 字段(plan_step_index)
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 关联文档(已完成 / 待写)
|
#### 关联文档(已完成)
|
||||||
|
|
||||||
```
|
```
|
||||||
docs/note-agent-harness-references.md # ✅ 已存在
|
docs/note-agent-harness-references.md # ✅ 已存在
|
||||||
@@ -479,67 +588,140 @@ docs/note-agent-runtime-design.md # ✅ 已存在(与本文件配套)
|
|||||||
docs/7-agent-runtime.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` 通过;现有测试不挂 |
|
| a1 | 修改 `llm/hooks.rs` 追加 OnTurnStart / OnTurnEnd + turn_index 字段 | `src/llm/hooks.rs` | `cargo build` 通过;Phase 0 测试不挂 |
|
||||||
| 2 | 新建 `agent/error.rs` 定义 `AgentError` | `src/agent/error.rs` | `cargo build` 通过 |
|
| a2 | 新建 `agent/error.rs` 定义 `AgentError`(不含 PlanParse 变体) | `src/agent/error.rs` | `cargo build` 通过 |
|
||||||
| 3 | 新建 `agent/agent.rs` 定义 `Agent` trait | `src/agent/agent.rs` | `cargo build` 通过 |
|
| a3 | 新建 `agent/agent.rs` 定义 `Agent` trait | `src/agent/agent.rs` | `cargo build` 通过 |
|
||||||
| 4 | 新建 `agent/runtime.rs` 定义 `RuntimeBundle` + `AgentConfig` | `src/agent/runtime.rs` | `cargo build` 通过 |
|
| a4 | 新建 `agent/runtime.rs` 定义 `RuntimeBundle` + `AgentConfig`(不含 session_memory_backend) | `src/agent/runtime.rs` | `cargo build` 通过 |
|
||||||
| 5 | 新建 `agent/builder.rs` 定义 `AgentBuilder` | `src/agent/builder.rs` | `cargo build` 通过 |
|
| a5 | 新建 `agent/builder.rs` 定义 `AgentBuilder`(不含 session_memory_backend 方法) | `src/agent/builder.rs` | `cargo build` 通过 |
|
||||||
| 6 | 新建 `agent/session.rs` 定义 `AgentSession` + `submit_turn` | `src/agent/session.rs` | `cargo build` 通过 |
|
| a6 | 新建 `agent/session.rs` 定义 `AgentSession` + `submit_turn`(内联 `HashMap<String,String>` 做 session_data,不引 MemoryStore) | `src/agent/session.rs` | `cargo build` 通过 |
|
||||||
| 7 | 新建 `agent/task.rs` 定义 `TaskAgent` + `Plan` / `Step` / `PlanParser` / `JsonPlanParser` | `src/agent/task.rs` | `cargo build` 通过 |
|
| a7 | 新建 `agent/task.rs` 定义 `Plan` / `Step` / `StepStatus` 纯数据结构(不含 TaskAgent trait,不含 PlanParser) | `src/agent/task.rs` | `cargo build` 通过 |
|
||||||
| 8 | 新建 `src/agent.rs` 模块根 + `pub use` 重导出 | `src/agent.rs` | `cargo build` 通过 |
|
| a8 | 新建 `src/agent.rs` 模块根 + `pub use` 重导出 + 修改 `lib.rs` | `src/agent.rs` + `src/lib.rs` | `cargo build` 通过 |
|
||||||
| 9 | 修改 `lib.rs` 导出 `pub mod agent;` | `src/lib.rs` | `cargo build` 通过 |
|
| a9 | 编写烟雾测试 3-4 个(Agent trait 可装配 / RuntimeBundle 可构造 / submit_turn 跑通 mock / Plan 数据结构) | `src/agent/*.rs` 内联 | `cargo test` 通过 |
|
||||||
| 10 | 编写 2-3 个烟雾测试 | `src/agent/*.rs` 内联 | `cargo test` 通过 |
|
| a10 | 完整 `cargo test` 跑全量回归 + roadmap.md 状态更新 | — | 所有已有测试不挂 |
|
||||||
| 11 | 更新 `roadmap.md` 状态翻转 | `docs/roadmap.md` | 文档 review |
|
|
||||||
| 12 | 完整 `cargo test` 跑全量回归 | — | 所有已有测试不挂 |
|
|
||||||
|
|
||||||
### 4.3 依赖关系
|
**依赖关系**:
|
||||||
|
|
||||||
```
|
```
|
||||||
hooks.rs (1) ──┐
|
hooks扩展 (a1) ──┐
|
||||||
├──► agent/error.rs (2) ──► agent/agent.rs (3)
|
├──► 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)
|
cargo test (a9 → a10)
|
||||||
│
|
|
||||||
▼
|
|
||||||
src/lib.rs (9)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
cargo test (10)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
roadmap.md (11)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
回归 (12)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4.4 预估工作量
|
---
|
||||||
|
|
||||||
| 阶段 | 行数 | 说明 |
|
### 4.3 Phase 4b — 任务执行
|
||||||
|------|------|------|
|
|
||||||
| 1(hooks 扩展) | ~15 | 3 个变体 + 2 个字段 + 文档 |
|
**范围**:TaskAgent trait + PlanParser trait + JsonPlanParser 参考实现 + OnPlanStepComplete hook + AgentError PlanParse 变体
|
||||||
| 2-7(7 个 agent 文件) | ~600 | 含 import + trait + struct + impl + 文档 |
|
|
||||||
| 8-9(lib.rs + agent.rs 模块根) | ~20 | 主要是 pub use 重导出 |
|
**前置条件**:Phase 4a 已完成并交付。
|
||||||
| 10(烟雾测试) | ~100 | 2-3 个测试 |
|
|
||||||
| 11(roadmap 同步) | ~5 | 状态翻转一行 |
|
**任务拆解**:
|
||||||
| **合计** | **~740** | 与 `note-agent-runtime-design.md` §6 预估一致 |
|
|
||||||
|
| 顺序 | 任务 | 涉及文件 | 验证 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| 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. 风险评估
|
||||||
|
|
||||||
@@ -581,67 +763,122 @@ hooks.rs (1) ──┐
|
|||||||
|
|
||||||
### 5.5 实施进度风险
|
### 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
|
- 必要时先做 `MockProvider`(Phase 0 已有模式),不依赖真实 LLM
|
||||||
|
|
||||||
## 6. 验收标准
|
## 6. 验收标准
|
||||||
|
|
||||||
### 6.1 代码验收
|
### 6.1 通用代码验收(每个子阶段必须满足)
|
||||||
|
|
||||||
- [ ] `cargo build --release` 0 错误 0 警告(clippy)
|
- [ ] `cargo build --release` 0 错误 0 警告(clippy)
|
||||||
- [ ] `cargo test` 所有 Phase 0-3 已有测试 + Phase 4 新增测试全部通过
|
- [ ] `cargo test` 所有已有测试 + 本阶段新增测试全部通过
|
||||||
- [ ] `cargo doc --no-deps` 所有公开 API 有 `///` 文档注释
|
- [ ] `cargo doc --no-deps` 所有公开 API 有 `///` 文档注释
|
||||||
- [ ] 新增代码 700-750 行(含测试 + 文档注释),与 §4.4 预估一致
|
|
||||||
- [ ] `src/lib.rs` 新增一行 `pub mod agent;`
|
|
||||||
- [ ] `src/llm/hooks.rs` 仅追加(不修改现有变体或字段)
|
- [ ] `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` 三个方法
|
- [ ] `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 触发
|
- [ ] `AgentSession::submit_turn` 实现约 30 行,含 OnTurnStart/End hook 触发
|
||||||
- [ ] `TaskAgent` 提供双入口 `run` + `execute_plan`
|
- [ ] `AgentSession` 用内联 `HashMap<String, String>` 做 session_data(不引 `MemoryStore`)
|
||||||
- [ ] `JsonPlanParser` 实现约 20 行,基于 `serde_json`
|
- [ ] `Plan` / `Step` / `StepStatus` 纯数据结构存在,状态机正确
|
||||||
- [ ] `AgentError` 聚合 8 个变体,含 `is_recoverable()`
|
- [ ] `AgentError` 聚合 6 个变体:Llm / Tool / Memory / HookBlocked / LimitExceeded / Config / Other(不含 PlanParse)
|
||||||
- [ ] `AgentBuilder` 提供 6 个 setter + `build()` 校验
|
- [ ] `AgentError::is_recoverable()` 对各变体返回正确分类
|
||||||
- [ ] `HookEvent` 新增 3 个变体:`OnTurnStart` / `OnTurnEnd` / `OnPlanStepComplete`
|
- [ ] `HookEvent` 新增 2 个变体:`OnTurnStart` / `OnTurnEnd`
|
||||||
- [ ] `HookContext` 新增 2 个 `Option` 字段:`turn_index` / `plan_step_index`
|
- [ ] `HookContext` 新增 1 个 `Option` 字段:`turn_index`
|
||||||
|
|
||||||
### 6.3 测试验收
|
#### 6.2c 测试验收
|
||||||
|
|
||||||
至少 2-3 个烟雾测试通过:
|
|
||||||
|
|
||||||
- [ ] **测试 1**:`Agent` trait 可实现 + `RuntimeBundle` 可构造(builder 链式调用)
|
- [ ] **测试 1**:`Agent` trait 可实现 + `RuntimeBundle` 可构造(builder 链式调用)
|
||||||
- [ ] **测试 2**:`AgentSession::submit_turn` 跑通 mock provider(Phase 0 `MockProvider` 模式)
|
- [ ] **测试 2**:`AgentSession::submit_turn` 跑通 mock provider(Phase 0 `MockProvider` 模式)
|
||||||
- [ ] **测试 3(可选)**:`JsonPlanParser::parse` 能解析合法 JSON,失败时返回 `AgentError::PlanParse`
|
- [ ] **测试 3**:`Plan` / `Step` / `StepStatus` 状态机转换正确
|
||||||
|
- [ ] **测试 4(可选)**:session_data set/get 基本读写
|
||||||
|
|
||||||
### 6.4 文档验收
|
#### 6.2d 行为验收
|
||||||
|
|
||||||
- [ ] `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)
|
|
||||||
|
|
||||||
- [ ] `AgentSession::submit_turn` 不持有 `ConversationMemory`(grep 验证无 `use crate::memory::ConversationMemory`)
|
- [ ] `AgentSession::submit_turn` 不持有 `ConversationMemory`(grep 验证无 `use crate::memory::ConversationMemory`)
|
||||||
|
- [ ] `AgentSession` 持 `Arc<dyn Agent>`,可从 agent 获取 `system_prompt()` / `tool_definitions()`
|
||||||
- [ ] `RuntimeBundle::new` 当 `retriever` 为 `Some` 时自动注册 `"retrieve"` tool
|
- [ ] `RuntimeBundle::new` 当 `retriever` 为 `Some` 时自动注册 `"retrieve"` tool
|
||||||
- [ ] `AgentBuilder::build` 在必填字段缺失时返回 `AgentError::Config`(而非 panic)
|
- [ ] `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 风险验收
|
### 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.2 Phase 0-3 侵入:`git diff` 显示 `src/llm/hooks.rs` 仅追加
|
||||||
- [ ] 5.3 语言差异:trait 形状符合 Rust 惯例(无 Python 风格的复杂继承)
|
- [ ] 5.3 语言差异:trait 形状符合 Rust 惯例(无 Python 风格的复杂继承)
|
||||||
- [ ] 5.4 trait 稳定性:决策记录与最终代码一致
|
- [ ] 5.4 trait 稳定性:决策记录与最终代码一致
|
||||||
- [ ] 5.5 实施进度:实际工作量与 §4.4 预估偏差 < 30%
|
- [ ] 5.5 实施进度:每个子阶段实际工作量与 §4.5 预估偏差 < 30%
|
||||||
|
|
||||||
## 7. 一句话总结
|
## 7. 一句话总结
|
||||||
|
|
||||||
> **Phase 4 = 1 个 trait(Agent)+ 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 已有能力"装配"成"智能体"的概念。**
|
||||||
|
|||||||
@@ -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 key,CI 会跳过 | 中 | 高 | 用 `#[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
|
||||||
|
- 每个示例在文件顶部有 `//!` 注释说明其演示目的
|
||||||
@@ -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` 的 identity,LLM 容易"串味"。
|
||||||
|
|
||||||
|
### 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 委派子任务给 subagent,subagent 独立运作 |
|
||||||
|
| 长 session 上下文压缩 | ✅ 附带收益 | 拆分 context 后,每个 context 独立累积消息,不会互相拖长 |
|
||||||
|
| 并行 context 执行 | ⚠️ 拓展场景 | context_a 和 context_b 可各自独立推进 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 三个候选方案
|
||||||
|
|
||||||
|
### 方案 A:OpenCode 式(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 的三种选项
|
||||||
|
|
||||||
|
| 选项 | 描述 | 评估 |
|
||||||
|
|------|------|------|
|
||||||
|
| **选项 1:AgentSession 自动写** | 每轮对话后自动摘录关键信息 | ❌ 摘录什么?容易变成精简版对话历史,失去"关键信息"的定位 |
|
||||||
|
| **选项 2:LLM 通过 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 只铺"水管接口",不装"水循环系统"。**
|
||||||
@@ -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 2(env/instructions/skills)和 Layer 3(user.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 1(agent 专属 prompt)。
|
||||||
+108
-20
@@ -1,13 +1,13 @@
|
|||||||
# AG Core Roadmap
|
# AG Core Roadmap
|
||||||
|
|
||||||
> 定稿日期:2026-05-11
|
> 定稿日期:2026-05-11
|
||||||
> 最后更新:2026-06-09(Phase 4 设计讨论收尾;扩展计划补充 v0.2+ 候选项)
|
> 最后更新:2026-06-11(Phase 4c 编码实施完成)
|
||||||
|
|
||||||
## 愿景
|
## 愿景
|
||||||
|
|
||||||
AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可插拔的架构,提供大模型调用、提示词工程、工具系统、记忆检索四大核心能力,支持快速组合出符合业务需求的智能体应用。
|
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/4-prompt-engineering.md` | P1 |
|
||||||
| 工具系统 + 权限 | ✅ 完整 | `docs/5-tool-system.md` | P1 |
|
| 工具系统 + 权限 | ✅ 完整 | `docs/5-tool-system.md` | P1 |
|
||||||
| 记忆检索 | ✅ 完整 | `docs/6-memory-system.md` | P2 |
|
| 记忆检索 | ✅ 完整 | `docs/6-memory-system.md` | P2 |
|
||||||
| Agent 运行时 | ❌ 缺失 | — | P2 |
|
| Agent 运行时(4a 胶水层) | ✅ 已实现 | `docs/7-agent-runtime.md` | P2 |
|
||||||
| 生命周期钩子 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0(LLM Cycle 扩展) |
|
| 生命周期钩子 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0(LLM Cycle 扩展) |
|
||||||
| Provider 注册发现 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0(Provider 接口扩展) |
|
| Provider 注册发现 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0(Provider 接口扩展) |
|
||||||
| 流式事件系统 | ✅ 完整 | `docs/3-phase0-remaining.md` | P0(流式接口前置) |
|
| 流式事件系统 | ✅ 完整 | `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/` 模块
|
1. ✅ `agent.rs` + `agent/` 模块(7 个文件:agent/error/runtime/builder/session/task + 模块根)
|
||||||
2. `Agent` trait — 智能体接口定义
|
2. ✅ `Agent` trait — 智能体角色定义(name / system_prompt / tool_definitions)
|
||||||
3. `ConversationAgent` — 对话型智能体实现
|
3. ✅ `AgentSession` — 会话实例(绑定 `Arc<dyn Agent>` + `RuntimeBundle` + 内联 HashMap session_data)
|
||||||
4. `TaskAgent` — 任务型智能体(规划 → 执行 → 反馈)
|
4. ✅ `RuntimeBundle` — 显式依赖注入容器(不含 session_memory_backend)
|
||||||
5. `specs/agent-runtime.md` — 方案文档
|
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
|
**优先级**: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 4a(Phase 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
|
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
|
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
|
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
|
P1 --> P0
|
||||||
P2 --> P0
|
P2 --> P0
|
||||||
P3 --> P0
|
P3 --> P0
|
||||||
P2 --> P1
|
P2 --> P1
|
||||||
P4 --> P1
|
P4a --> P1
|
||||||
P4 --> P2
|
P4a --> P2
|
||||||
P4 --> P3
|
P4a --> P3
|
||||||
|
P4b --> P4a
|
||||||
|
P4c --> P4a
|
||||||
|
|
||||||
classDef done fill:#4ade80,stroke:#16a34a,color:#1a1a1a
|
classDef done fill:#4ade80,stroke:#16a34a,color:#1a1a1a
|
||||||
classDef pending fill:#fbbf24,stroke:#d97706,color:#1a1a1a
|
classDef pending fill:#fbbf24,stroke:#d97706,color:#1a1a1a
|
||||||
@@ -168,6 +244,7 @@ graph BT
|
|||||||
|
|
||||||
> 以下功能在已完成的 phase 中已实现基础能力或在 Phase 4 阶段明确了边界,后续可按维度增量扩展。
|
> 以下功能在已完成的 phase 中已实现基础能力或在 Phase 4 阶段明确了边界,后续可按维度增量扩展。
|
||||||
> 设计参考:见 `docs/note-agent-harness-references.md`(OpenClaw / Hermes / OpenHuman / OpenHarness 横向对比)。
|
> 设计参考:见 `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 待评估 |
|
| 流式 `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 可并行开展(无相互依赖),可加速早期交付
|
2. **并行可能性**:Phase 0 和 Phase 1 可并行开展(无相互依赖),可加速早期交付
|
||||||
3. **MCP 协议复杂性**:MCP 涉及协议握手、session 管理、长期连接,建议预留充足时间调研协议细节
|
3. **MCP 协议复杂性**:MCP 涉及协议握手、session 管理、长期连接,建议预留充足时间调研协议细节
|
||||||
4. **Scope 蔓延风险**:当前 specs 只有 1 份文档,建议每个模块上线前都产出对应 spec,避免边实现边设计
|
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 4(4a/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 双进程等)
|
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` 方案文档后启动编码实现
|
1. **Phase 4c 已完成**:Phase 4a + 4b + 4c 已交付(116 测试通过,0 clippy 警告)。可启动 v0.2+ 扩展评估(如多 Context 切换、Multi-Agent 协同等)
|
||||||
2. **Phase 4 方案文档**:将 Phase 4 设计决策沉淀为方案文档,沿用 `docs/4-prompt-engineering.md` / `5-tool-system.md` / `6-memory-system.md` 的 6 段式结构,文件名 `docs/7-agent-runtime.md`
|
2. **Context 切换备忘**:`docs/note-context-switch-design.md` 记录了多 context 切换方案讨论,作为 v0.2+ 扩展项的输入
|
||||||
3. **参考项目调研沉淀**:已完成 OpenClaw / Hermes / OpenHuman / OpenHarness 横向调研,结果沉淀至 `docs/note-agent-harness-references.md`,作为 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+ 记忆扩展可直接参考
|
4. **Phase 3 备用设计就绪**:`docs/note-knowledge-graph-design.md` 记录了 KnowledgeGraph、高级评分、RecallBased 淘汰等设计,v0.2+ 记忆扩展可直接参考
|
||||||
|
|
||||||
**已完成阶段**:
|
**已完成 / 进行中阶段**:
|
||||||
- ✅ Phase 0 Foundation — 全部交付物已完成
|
- ✅ Phase 0 Foundation — 全部交付物已完成
|
||||||
- ✅ Phase 1 Prompt Engineering — 全部交付物已完成
|
- ✅ Phase 1 Prompt Engineering — 全部交付物已完成
|
||||||
- ✅ Phase 2 Tool System — 全部交付物已完成
|
- ✅ Phase 2 Tool System — 全部交付物已完成
|
||||||
- ✅ Phase 3 Memory System — 全部交付物已完成
|
- ✅ Phase 3 Memory System — 全部交付物已完成
|
||||||
|
- ✅ Phase 4a Core Glue — 全部交付物已完成
|
||||||
|
- ✅ Phase 4b Task Execution — 全部交付物已完成
|
||||||
|
- ✅ Phase 4c Session Memory — 全部交付物已完成
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"` tool(v0.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,
|
||||||
|
/// 会话 TTL(None 表示无过期),默认 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 1:AgentSession::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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 2:session_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())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 3:submit_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 1:set / 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 2:snapshot 格式化输出。
|
||||||
|
#[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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 3:clear 清空当前 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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 1:JsonPlanParser 解析合法 JSON。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_plan_parser_success() {
|
||||||
|
let parser = JsonPlanParser;
|
||||||
|
let input = r#"{"steps": [{"description": "step one"}, {"description": "step two"}]}"#;
|
||||||
|
let plan = parser.parse(input, "my goal").await.unwrap();
|
||||||
|
assert_eq!(plan.goal, "my goal");
|
||||||
|
assert_eq!(plan.steps.len(), 2);
|
||||||
|
assert_eq!(plan.steps[0].description, "step one");
|
||||||
|
assert_eq!(plan.steps[1].description, "step two");
|
||||||
|
assert!(plan.steps.iter().all(|s| s.status.is_pending()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 2:JsonPlanParser 解析失败返回 AgentError::PlanParse。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_plan_parser_invalid_json() {
|
||||||
|
let parser = JsonPlanParser;
|
||||||
|
let err = parser.parse("not json", "goal").await.unwrap_err();
|
||||||
|
assert!(matches!(err, AgentError::PlanParse(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 烟雾测试 3:JsonPlanParser 空步骤返回错误。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn json_plan_parser_empty_steps() {
|
||||||
|
let parser = JsonPlanParser;
|
||||||
|
let input = r#"{"steps": []}"#;
|
||||||
|
let err = parser.parse(input, "goal").await.unwrap_err();
|
||||||
|
assert!(matches!(err, AgentError::PlanParse(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! agcore —— 智能体(Agent)核心工具箱。
|
//! agcore —— 智能体(Agent)核心工具箱。
|
||||||
|
|
||||||
|
pub mod agent;
|
||||||
pub mod llm;
|
pub mod llm;
|
||||||
pub mod memory;
|
pub mod memory;
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
|
|||||||
+12
-2
@@ -63,7 +63,7 @@ impl Default for CycleConfig {
|
|||||||
|
|
||||||
/// LLM 调用周期 —— 管理一次或多次 LLM 请求的生命周期。
|
/// LLM 调用周期 —— 管理一次或多次 LLM 请求的生命周期。
|
||||||
pub struct LlmCycle {
|
pub struct LlmCycle {
|
||||||
provider: Box<dyn LlmProvider>,
|
provider: Arc<dyn LlmProvider>,
|
||||||
config: CycleConfig,
|
config: CycleConfig,
|
||||||
usage: CostTracker,
|
usage: CostTracker,
|
||||||
messages: Vec<OpenaiChatMessage>,
|
messages: Vec<OpenaiChatMessage>,
|
||||||
@@ -74,8 +74,18 @@ pub struct LlmCycle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
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 {
|
Self {
|
||||||
provider,
|
provider,
|
||||||
config,
|
config,
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ pub enum HookEvent {
|
|||||||
OnRetry,
|
OnRetry,
|
||||||
/// 不可恢复错误返回之前。
|
/// 不可恢复错误返回之前。
|
||||||
OnError,
|
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>,
|
pub error: Option<&'a LlmError>,
|
||||||
/// 当前重试次数(从 1 开始,仅 OnRetry 可用)。
|
/// 当前重试次数(从 1 开始,仅 OnRetry 可用)。
|
||||||
pub attempt: u32,
|
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> {
|
impl<'a> HookContext<'a> {
|
||||||
@@ -38,6 +48,8 @@ impl<'a> HookContext<'a> {
|
|||||||
request: None,
|
request: None,
|
||||||
error: None,
|
error: None,
|
||||||
attempt: 0,
|
attempt: 0,
|
||||||
|
turn_index: None,
|
||||||
|
plan_step_index: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,6 +67,18 @@ impl<'a> HookContext<'a> {
|
|||||||
self.attempt = attempt;
|
self.attempt = attempt;
|
||||||
self
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 钩子执行结果。
|
/// 钩子执行结果。
|
||||||
|
|||||||
Reference in New Issue
Block a user