diff --git a/docs/roadmap.md b/docs/roadmap.md index fdbdbc8..6a38530 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,13 +1,13 @@ # AG Core Roadmap > 定稿日期:2026-05-11 -> 最后更新:2026-06-11(Phase 4b 编码实施完成;Phase 4c 仍待启动) +> 最后更新:2026-06-11(Phase 4c 编码实施完成) ## 愿景 AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可插拔的架构,提供大模型调用、提示词工程、工具系统、记忆检索四大核心能力,支持快速组合出符合业务需求的智能体应用。 -**当前状态**:Phase 0 基础设施已全部完成,Phase 1 提示词工程已全部完成,Phase 2 工具系统已全部完成,Phase 3 记忆系统已全部完成,Phase 4a 核心胶水层已全部完成,Phase 4b 任务执行已全部完成(113 个测试通过,0 警告),Phase 4c 待启动。 +**当前状态**:Phase 0 基础设施已全部完成,Phase 1 提示词工程已全部完成,Phase 2 工具系统已全部完成,Phase 3 记忆系统已全部完成,Phase 4a 核心胶水层已全部完成,Phase 4b 任务执行已全部完成,Phase 4c 会话级记忆已全部完成(116 个测试通过,0 警告)。 --- @@ -190,9 +190,9 @@ AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可 **前置条件**:Phase 4a 已完成(可与 Phase 4b 并行)。 **交付物**: -1. `SessionMemory` struct — 基于 `MemoryStore`,按 session_id namespace 隔离 -2. `RuntimeBundle` + `AgentBuilder` 扩展 `session_memory_backend` 字段 -3. `AgentSession` 替换内联 HashMap 为完整 `SessionMemory` +1. ✅ `SessionMemory` struct — 基于 `MemoryStore`,按 session_id namespace 隔离 +2. ✅ `RuntimeBundle` + `AgentBuilder` 扩展 `session_memory_backend` 字段 +3. ✅ `AgentSession` 替换内联 HashMap 为完整 `SessionMemory` **依赖**:Phase 4a(Phase 3 MemoryStore) @@ -200,7 +200,15 @@ AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可 **预估规模**:约 115 行代码(增量) -**状态**:⏳ 待 Phase 4a 完成后启动 +**实际新增**: +- 新增文件 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 全部交付物已完成 --- @@ -214,7 +222,7 @@ graph BT P3["Phase 3: Memory System
MemoryStore
ConversationMemory
KnowledgeStore"]:::done P4a["Phase 4a: Core Glue
AgentSession
RuntimeBundle
Plan/Step 纯数据"]:::done P4b["Phase 4b: Task Execution
TaskAgent
PlanParser
JsonPlanParser"]:::done - P4c["Phase 4c: Session Memory
SessionMemory"]:::pending + P4c["Phase 4c: Session Memory
SessionMemory"]:::done P1 --> P0 P2 --> P0 @@ -317,7 +325,7 @@ graph BT ## 下一步行动 -1. **Phase 4c 启动评估**:Phase 4a + 4b 已交付(113 测试通过,0 clippy 警告)。可启动 Phase 4c(会话级记忆:SessionMemory + RuntimeBundle/Builder 扩展 + AgentSession 接入) +1. **Phase 4c 已完成**:Phase 4a + 4b + 4c 已交付(116 测试通过,0 clippy 警告)。可启动 v0.2+ 扩展评估(如多 Context 切换、Multi-Agent 协同等) 2. **Context 切换备忘**:`docs/note-context-switch-design.md` 记录了多 context 切换方案讨论,作为 v0.2+ 扩展项的输入 3. **参考项目调研沉淀**:已完成 OpenClaw / Hermes / OpenHuman / OpenHarness 横向调研,结果沉淀至 `docs/note-agent-harness-references.md`,作为 v0.2+ 扩展项的输入 4. **Phase 3 备用设计就绪**:`docs/note-knowledge-graph-design.md` 记录了 KnowledgeGraph、高级评分、RecallBased 淘汰等设计,v0.2+ 记忆扩展可直接参考 @@ -329,4 +337,4 @@ graph BT - ✅ Phase 3 Memory System — 全部交付物已完成 - ✅ Phase 4a Core Glue — 全部交付物已完成 - ✅ Phase 4b Task Execution — 全部交付物已完成 -- ⏳ Phase 4c Session Memory — 依赖 4a +- ✅ Phase 4c Session Memory — 全部交付物已完成 diff --git a/src/agent.rs b/src/agent.rs index df0d5e8..fede02b 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -14,6 +14,7 @@ pub mod builder; pub mod error; pub mod runtime; pub mod session; +pub mod session_memory; pub mod task; // 重导出公共 API(按使用频度排序) @@ -22,5 +23,6 @@ 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; diff --git a/src/agent/builder.rs b/src/agent/builder.rs index d8d2b02..9f2ae70 100644 --- a/src/agent/builder.rs +++ b/src/agent/builder.rs @@ -34,6 +34,7 @@ pub struct AgentBuilder { hook_executor: Option>, memory_store: Option>, retriever: Option>, + session_memory_backend: Option>, config: Option, } @@ -73,6 +74,12 @@ impl AgentBuilder { self } + /// 设置 SessionMemory 后端(选填,不传则 `AgentSession` 内部用 `InMemoryStore` 兜底)。 + pub fn session_memory_backend(mut self, s: Arc) -> Self { + self.session_memory_backend = Some(s); + self + } + /// 整体覆盖 `AgentConfig`(选填,不传则用默认值)。 pub fn config(mut self, c: AgentConfig) -> Self { self.config = Some(c); @@ -102,6 +109,7 @@ impl AgentBuilder { hook_executor, self.memory_store, self.retriever, + self.session_memory_backend, config, )) } diff --git a/src/agent/runtime.rs b/src/agent/runtime.rs index cdc0fa1..bd20f33 100644 --- a/src/agent/runtime.rs +++ b/src/agent/runtime.rs @@ -68,6 +68,11 @@ pub struct RuntimeBundle { /// 传入时可在 `submit_turn` 内部将检索能力作为工具暴露给 LLM。 pub retriever: Option>, + /// SessionMemory 后端(选填)。 + /// 传入时 `SessionMemory` 使用该后端(支持跨进程共享); + /// 不传时 `AgentSession` 内部自动创建 `InMemoryStore` 作为进程级隔离的后端。 + pub session_memory_backend: Option>, + /// 运行时配置。 pub config: AgentConfig, } @@ -79,6 +84,10 @@ impl std::fmt::Debug for RuntimeBundle { .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() } @@ -96,6 +105,7 @@ impl RuntimeBundle { hook_executor: Arc, memory_store: Option>, retriever: Option>, + session_memory_backend: Option>, config: AgentConfig, ) -> Self { Self { @@ -104,6 +114,7 @@ impl RuntimeBundle { hook_executor, memory_store, retriever, + session_memory_backend, config, } } diff --git a/src/agent/session.rs b/src/agent/session.rs index 843c5cd..cd62657 100644 --- a/src/agent/session.rs +++ b/src/agent/session.rs @@ -7,22 +7,23 @@ //! - **不做业务循环**:多轮策略、错误重试、记忆回写由上层应用或具体 `TaskAgent` 决定 //! - **不持有 ConversationMemory**:上层可独立 new 一个 `ConversationMemory`,在合适的时机调 `add_message` -use std::collections::HashMap; 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_data`, +/// **不实现 `Clone`**:session 持有累计 `turn_index` / `cost_so_far` / `session_memory`, /// 共享这些状态需要显式 sync 语义;如果上层需要并发访问,自己用 `Arc>` 包装。 pub struct AgentSession { /// 会话 ID(由调用方指定,用于日志/追踪/记忆关联)。 @@ -32,8 +33,8 @@ pub struct AgentSession { bundle: Arc, turn_index: u32, cost_so_far: CostTracker, - /// 会话级键值数据(Phase 4a 用内联 HashMap;Phase 4c 替换为 `SessionMemory`)。 - session_data: HashMap, + /// 会话级记忆(Phase 4c 替换内联 HashMap)。 + pub session_memory: SessionMemory, } impl std::fmt::Debug for AgentSession { @@ -43,7 +44,7 @@ impl std::fmt::Debug for AgentSession { .field("agent", &self.agent.name()) .field("turn_index", &self.turn_index) .field("cost_so_far", &self.cost_so_far.total()) - .field("session_data_keys", &self.session_data.keys().collect::>()) + .field("session_memory", &"") .finish() } } @@ -57,13 +58,19 @@ impl AgentSession { session_id: impl Into, bundle: Arc, ) -> 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.into(), + session_id: session_id_str, agent, bundle, turn_index: 0, cost_so_far: CostTracker::default(), - session_data: HashMap::new(), + session_memory, } } @@ -77,19 +84,23 @@ impl AgentSession { &self.cost_so_far } - /// 会话级数据快照引用。 - pub fn session_data(&self) -> &HashMap { - &self.session_data + /// 会话级记忆引用。 + pub fn session_memory(&self) -> &SessionMemory { + &self.session_memory } /// 写入一条会话级数据(覆盖同名 key)。 - pub fn set_session_data(&mut self, key: impl Into, value: impl Into) { - self.session_data.insert(key.into(), value.into()); + pub async fn set_session_data( + &mut self, + key: impl Into, + value: impl Into, + ) -> Result<(), AgentError> { + self.session_memory.set(&key.into(), &value.into()).await } /// 读取一条会话级数据。 - pub fn get_session_data(&self, key: &str) -> Option<&str> { - self.session_data.get(key).map(String::as_str) + pub async fn get_session_data(&self, key: &str) -> Result, AgentError> { + self.session_memory.get(key).await } /// 提交一轮对话(含自动 tool 循环),返回 LLM 响应。 @@ -273,8 +284,8 @@ mod tests { } /// 烟雾测试 2:session_data 读写。 - #[test] - fn session_data_set_get() { + #[tokio::test] + async fn session_data_set_get() { let provider = Arc::new(MockProvider::new(vec![])); let agent = Arc::new(StubAgent { name: "stub".into(), @@ -290,12 +301,15 @@ mod tests { ); let mut session = AgentSession::new(agent, "s2", bundle); - assert!(session.get_session_data("k").is_none()); - session.set_session_data("k", "v"); - assert_eq!(session.get_session_data("k"), Some("v")); + 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"); - assert_eq!(session.get_session_data("k"), Some("v2")); + 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。 diff --git a/src/agent/session_memory.rs b/src/agent/session_memory.rs new file mode 100644 index 0000000..2e2bf53 --- /dev/null +++ b/src/agent/session_memory.rs @@ -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, + namespace: String, +} + +impl SessionMemory { + /// 创建新的 session 级记忆实例。 + /// + /// - `store`:后端存储(可跨进程共享的 `MemoryStore` 实现)。 + /// - `namespace`:按 session_id 隔离,防止跨 session 泄漏。 + /// 内部会自动添加 `"_session_"` 前缀。 + pub fn new(store: Arc, 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, AgentError> { + let item = self + .store + .get(&self.internal_key(key)) + .await + .map_err(AgentError::Memory)?; + Ok(item.map(|i| i.content)) + } + + /// 返回所有条目的格式化快照,适合注入 system prompt。 + /// + /// 格式: + /// ```text + /// + /// key1: value1 + /// key2: value2 + /// + /// ``` + pub async fn snapshot(&self) -> Result { + 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("".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("".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 { + 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("")); + assert!(snap.contains("")); + 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())); + } +} \ No newline at end of file