feat(agent): 实现 Phase 4c 会话级记忆功能
- 新增 `SessionMemory` 结构体,基于 `MemoryStore` 按 namespace 隔离键值数据 - `AgentBuilder` 增加 `session_memory_backend` 配置入口 - `RuntimeBundle` 透传 `session_memory_backend` 字段 - `AgentSession` 将内联 `HashMap` 替换为完整的 `SessionMemory`,`set_session_data` 和 `get_session_data` 改为异步方法 - 新增 3 个内联测试,全量测试从 113 增至 116,clippy 0 警告
This commit is contained in:
+17
-9
@@ -1,13 +1,13 @@
|
|||||||
# AG Core Roadmap
|
# AG Core Roadmap
|
||||||
|
|
||||||
> 定稿日期:2026-05-11
|
> 定稿日期:2026-05-11
|
||||||
> 最后更新:2026-06-11(Phase 4b 编码实施完成;Phase 4c 仍待启动)
|
> 最后更新:2026-06-11(Phase 4c 编码实施完成)
|
||||||
|
|
||||||
## 愿景
|
## 愿景
|
||||||
|
|
||||||
AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可插拔的架构,提供大模型调用、提示词工程、工具系统、记忆检索四大核心能力,支持快速组合出符合业务需求的智能体应用。
|
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 并行)。
|
**前置条件**:Phase 4a 已完成(可与 Phase 4b 并行)。
|
||||||
|
|
||||||
**交付物**:
|
**交付物**:
|
||||||
1. `SessionMemory` struct — 基于 `MemoryStore`,按 session_id namespace 隔离
|
1. ✅ `SessionMemory` struct — 基于 `MemoryStore`,按 session_id namespace 隔离
|
||||||
2. `RuntimeBundle` + `AgentBuilder` 扩展 `session_memory_backend` 字段
|
2. ✅ `RuntimeBundle` + `AgentBuilder` 扩展 `session_memory_backend` 字段
|
||||||
3. `AgentSession` 替换内联 HashMap 为完整 `SessionMemory`
|
3. ✅ `AgentSession` 替换内联 HashMap 为完整 `SessionMemory`
|
||||||
|
|
||||||
**依赖**:Phase 4a(Phase 3 MemoryStore)
|
**依赖**:Phase 4a(Phase 3 MemoryStore)
|
||||||
|
|
||||||
@@ -200,7 +200,15 @@ AG Core 定位为构建 AI 智能体的底层工具箱,通过模块化、可
|
|||||||
|
|
||||||
**预估规模**:约 115 行代码(增量)
|
**预估规模**:约 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["<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
|
||||||
P4a["<b>Phase 4a: Core Glue</b><br/>AgentSession<br/>RuntimeBundle<br/>Plan/Step 纯数据"]:::done
|
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
|
P4b["<b>Phase 4b: Task Execution</b><br/>TaskAgent<br/>PlanParser<br/>JsonPlanParser"]:::done
|
||||||
P4c["<b>Phase 4c: Session Memory</b><br/>SessionMemory"]:::pending
|
P4c["<b>Phase 4c: Session Memory</b><br/>SessionMemory"]:::done
|
||||||
|
|
||||||
P1 --> P0
|
P1 --> P0
|
||||||
P2 --> 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+ 扩展项的输入
|
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+ 记忆扩展可直接参考
|
||||||
@@ -329,4 +337,4 @@ graph BT
|
|||||||
- ✅ Phase 3 Memory System — 全部交付物已完成
|
- ✅ Phase 3 Memory System — 全部交付物已完成
|
||||||
- ✅ Phase 4a Core Glue — 全部交付物已完成
|
- ✅ Phase 4a Core Glue — 全部交付物已完成
|
||||||
- ✅ Phase 4b Task Execution — 全部交付物已完成
|
- ✅ Phase 4b Task Execution — 全部交付物已完成
|
||||||
- ⏳ Phase 4c Session Memory — 依赖 4a
|
- ✅ Phase 4c Session Memory — 全部交付物已完成
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub mod builder;
|
|||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
pub mod session_memory;
|
||||||
pub mod task;
|
pub mod task;
|
||||||
|
|
||||||
// 重导出公共 API(按使用频度排序)
|
// 重导出公共 API(按使用频度排序)
|
||||||
@@ -22,5 +23,6 @@ pub use builder::AgentBuilder;
|
|||||||
pub use error::AgentError;
|
pub use error::AgentError;
|
||||||
pub use runtime::{AgentConfig, RuntimeBundle};
|
pub use runtime::{AgentConfig, RuntimeBundle};
|
||||||
pub use session::AgentSession;
|
pub use session::AgentSession;
|
||||||
|
pub use session_memory::SessionMemory;
|
||||||
pub use task::{Plan, PlanParser, Step, StepStatus, TaskAgent};
|
pub use task::{Plan, PlanParser, Step, StepStatus, TaskAgent};
|
||||||
pub use task::JsonPlanParser;
|
pub use task::JsonPlanParser;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ pub struct AgentBuilder {
|
|||||||
hook_executor: Option<Arc<HookExecutor>>,
|
hook_executor: Option<Arc<HookExecutor>>,
|
||||||
memory_store: Option<Arc<dyn MemoryStore>>,
|
memory_store: Option<Arc<dyn MemoryStore>>,
|
||||||
retriever: Option<Arc<MemoryRetriever>>,
|
retriever: Option<Arc<MemoryRetriever>>,
|
||||||
|
session_memory_backend: Option<Arc<dyn MemoryStore>>,
|
||||||
config: Option<AgentConfig>,
|
config: Option<AgentConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +74,12 @@ impl AgentBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 设置 SessionMemory 后端(选填,不传则 `AgentSession` 内部用 `InMemoryStore` 兜底)。
|
||||||
|
pub fn session_memory_backend(mut self, s: Arc<dyn MemoryStore>) -> Self {
|
||||||
|
self.session_memory_backend = Some(s);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// 整体覆盖 `AgentConfig`(选填,不传则用默认值)。
|
/// 整体覆盖 `AgentConfig`(选填,不传则用默认值)。
|
||||||
pub fn config(mut self, c: AgentConfig) -> Self {
|
pub fn config(mut self, c: AgentConfig) -> Self {
|
||||||
self.config = Some(c);
|
self.config = Some(c);
|
||||||
@@ -102,6 +109,7 @@ impl AgentBuilder {
|
|||||||
hook_executor,
|
hook_executor,
|
||||||
self.memory_store,
|
self.memory_store,
|
||||||
self.retriever,
|
self.retriever,
|
||||||
|
self.session_memory_backend,
|
||||||
config,
|
config,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,11 @@ pub struct RuntimeBundle {
|
|||||||
/// 传入时可在 `submit_turn` 内部将检索能力作为工具暴露给 LLM。
|
/// 传入时可在 `submit_turn` 内部将检索能力作为工具暴露给 LLM。
|
||||||
pub retriever: Option<Arc<MemoryRetriever>>,
|
pub retriever: Option<Arc<MemoryRetriever>>,
|
||||||
|
|
||||||
|
/// SessionMemory 后端(选填)。
|
||||||
|
/// 传入时 `SessionMemory` 使用该后端(支持跨进程共享);
|
||||||
|
/// 不传时 `AgentSession` 内部自动创建 `InMemoryStore` 作为进程级隔离的后端。
|
||||||
|
pub session_memory_backend: Option<Arc<dyn MemoryStore>>,
|
||||||
|
|
||||||
/// 运行时配置。
|
/// 运行时配置。
|
||||||
pub config: AgentConfig,
|
pub config: AgentConfig,
|
||||||
}
|
}
|
||||||
@@ -79,6 +84,10 @@ impl std::fmt::Debug for RuntimeBundle {
|
|||||||
.field("tool_names", &self.tool_registry.list_tools())
|
.field("tool_names", &self.tool_registry.list_tools())
|
||||||
.field("has_memory_store", &self.memory_store.is_some())
|
.field("has_memory_store", &self.memory_store.is_some())
|
||||||
.field("has_retriever", &self.retriever.is_some())
|
.field("has_retriever", &self.retriever.is_some())
|
||||||
|
.field(
|
||||||
|
"has_session_memory_backend",
|
||||||
|
&self.session_memory_backend.is_some(),
|
||||||
|
)
|
||||||
.field("config", &self.config)
|
.field("config", &self.config)
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
@@ -96,6 +105,7 @@ impl RuntimeBundle {
|
|||||||
hook_executor: Arc<HookExecutor>,
|
hook_executor: Arc<HookExecutor>,
|
||||||
memory_store: Option<Arc<dyn MemoryStore>>,
|
memory_store: Option<Arc<dyn MemoryStore>>,
|
||||||
retriever: Option<Arc<MemoryRetriever>>,
|
retriever: Option<Arc<MemoryRetriever>>,
|
||||||
|
session_memory_backend: Option<Arc<dyn MemoryStore>>,
|
||||||
config: AgentConfig,
|
config: AgentConfig,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -104,6 +114,7 @@ impl RuntimeBundle {
|
|||||||
hook_executor,
|
hook_executor,
|
||||||
memory_store,
|
memory_store,
|
||||||
retriever,
|
retriever,
|
||||||
|
session_memory_backend,
|
||||||
config,
|
config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-21
@@ -7,22 +7,23 @@
|
|||||||
//! - **不做业务循环**:多轮策略、错误重试、记忆回写由上层应用或具体 `TaskAgent` 决定
|
//! - **不做业务循环**:多轮策略、错误重试、记忆回写由上层应用或具体 `TaskAgent` 决定
|
||||||
//! - **不持有 ConversationMemory**:上层可独立 new 一个 `ConversationMemory`,在合适的时机调 `add_message`
|
//! - **不持有 ConversationMemory**:上层可独立 new 一个 `ConversationMemory`,在合适的时机调 `add_message`
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::agent::agent::Agent;
|
use crate::agent::agent::Agent;
|
||||||
use crate::agent::error::AgentError;
|
use crate::agent::error::AgentError;
|
||||||
use crate::agent::runtime::RuntimeBundle;
|
use crate::agent::runtime::RuntimeBundle;
|
||||||
|
use crate::agent::session_memory::SessionMemory;
|
||||||
use crate::llm::cycle::{CostTracker, CycleConfig, LlmCycle};
|
use crate::llm::cycle::{CostTracker, CycleConfig, LlmCycle};
|
||||||
use crate::llm::hooks::{HookContext, HookEvent};
|
use crate::llm::hooks::{HookContext, HookEvent};
|
||||||
use crate::llm::types::ChatResponse;
|
use crate::llm::types::ChatResponse;
|
||||||
|
use crate::memory::store::InMemoryStore;
|
||||||
|
|
||||||
/// Agent 会话实例。
|
/// Agent 会话实例。
|
||||||
///
|
///
|
||||||
/// 同一 `Agent` 可被多个 `AgentSession` 复用(不同 session_id 互不干扰)。
|
/// 同一 `Agent` 可被多个 `AgentSession` 复用(不同 session_id 互不干扰)。
|
||||||
/// `submit_turn` 一次只跑一轮 LLM 调用(含自动 tool 循环)。
|
/// `submit_turn` 一次只跑一轮 LLM 调用(含自动 tool 循环)。
|
||||||
///
|
///
|
||||||
/// **不实现 `Clone`**:session 持有累计 `turn_index` / `cost_so_far` / `session_data`,
|
/// **不实现 `Clone`**:session 持有累计 `turn_index` / `cost_so_far` / `session_memory`,
|
||||||
/// 共享这些状态需要显式 sync 语义;如果上层需要并发访问,自己用 `Arc<Mutex<_>>` 包装。
|
/// 共享这些状态需要显式 sync 语义;如果上层需要并发访问,自己用 `Arc<Mutex<_>>` 包装。
|
||||||
pub struct AgentSession {
|
pub struct AgentSession {
|
||||||
/// 会话 ID(由调用方指定,用于日志/追踪/记忆关联)。
|
/// 会话 ID(由调用方指定,用于日志/追踪/记忆关联)。
|
||||||
@@ -32,8 +33,8 @@ pub struct AgentSession {
|
|||||||
bundle: Arc<RuntimeBundle>,
|
bundle: Arc<RuntimeBundle>,
|
||||||
turn_index: u32,
|
turn_index: u32,
|
||||||
cost_so_far: CostTracker,
|
cost_so_far: CostTracker,
|
||||||
/// 会话级键值数据(Phase 4a 用内联 HashMap;Phase 4c 替换为 `SessionMemory`)。
|
/// 会话级记忆(Phase 4c 替换内联 HashMap)。
|
||||||
session_data: HashMap<String, String>,
|
pub session_memory: SessionMemory,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Debug for AgentSession {
|
impl std::fmt::Debug for AgentSession {
|
||||||
@@ -43,7 +44,7 @@ impl std::fmt::Debug for AgentSession {
|
|||||||
.field("agent", &self.agent.name())
|
.field("agent", &self.agent.name())
|
||||||
.field("turn_index", &self.turn_index)
|
.field("turn_index", &self.turn_index)
|
||||||
.field("cost_so_far", &self.cost_so_far.total())
|
.field("cost_so_far", &self.cost_so_far.total())
|
||||||
.field("session_data_keys", &self.session_data.keys().collect::<Vec<_>>())
|
.field("session_memory", &"<SessionMemory>")
|
||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,13 +58,19 @@ impl AgentSession {
|
|||||||
session_id: impl Into<String>,
|
session_id: impl Into<String>,
|
||||||
bundle: Arc<RuntimeBundle>,
|
bundle: Arc<RuntimeBundle>,
|
||||||
) -> Self {
|
) -> 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 {
|
Self {
|
||||||
session_id: session_id.into(),
|
session_id: session_id_str,
|
||||||
agent,
|
agent,
|
||||||
bundle,
|
bundle,
|
||||||
turn_index: 0,
|
turn_index: 0,
|
||||||
cost_so_far: CostTracker::default(),
|
cost_so_far: CostTracker::default(),
|
||||||
session_data: HashMap::new(),
|
session_memory,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,19 +84,23 @@ impl AgentSession {
|
|||||||
&self.cost_so_far
|
&self.cost_so_far
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 会话级数据快照引用。
|
/// 会话级记忆引用。
|
||||||
pub fn session_data(&self) -> &HashMap<String, String> {
|
pub fn session_memory(&self) -> &SessionMemory {
|
||||||
&self.session_data
|
&self.session_memory
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 写入一条会话级数据(覆盖同名 key)。
|
/// 写入一条会话级数据(覆盖同名 key)。
|
||||||
pub fn set_session_data(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
pub async fn set_session_data(
|
||||||
self.session_data.insert(key.into(), value.into());
|
&mut self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
value: impl Into<String>,
|
||||||
|
) -> Result<(), AgentError> {
|
||||||
|
self.session_memory.set(&key.into(), &value.into()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 读取一条会话级数据。
|
/// 读取一条会话级数据。
|
||||||
pub fn get_session_data(&self, key: &str) -> Option<&str> {
|
pub async fn get_session_data(&self, key: &str) -> Result<Option<String>, AgentError> {
|
||||||
self.session_data.get(key).map(String::as_str)
|
self.session_memory.get(key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 提交一轮对话(含自动 tool 循环),返回 LLM 响应。
|
/// 提交一轮对话(含自动 tool 循环),返回 LLM 响应。
|
||||||
@@ -273,8 +284,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 烟雾测试 2:session_data 读写。
|
/// 烟雾测试 2:session_data 读写。
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn session_data_set_get() {
|
async fn session_data_set_get() {
|
||||||
let provider = Arc::new(MockProvider::new(vec![]));
|
let provider = Arc::new(MockProvider::new(vec![]));
|
||||||
let agent = Arc::new(StubAgent {
|
let agent = Arc::new(StubAgent {
|
||||||
name: "stub".into(),
|
name: "stub".into(),
|
||||||
@@ -290,12 +301,15 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let mut session = AgentSession::new(agent, "s2", bundle);
|
let mut session = AgentSession::new(agent, "s2", bundle);
|
||||||
|
|
||||||
assert!(session.get_session_data("k").is_none());
|
assert!(session.get_session_data("k").await.unwrap().is_none());
|
||||||
session.set_session_data("k", "v");
|
session.set_session_data("k", "v").await.unwrap();
|
||||||
assert_eq!(session.get_session_data("k"), Some("v"));
|
assert_eq!(session.get_session_data("k").await.unwrap(), Some("v".into()));
|
||||||
// 覆盖写
|
// 覆盖写
|
||||||
session.set_session_data("k", "v2");
|
session.set_session_data("k", "v2").await.unwrap();
|
||||||
assert_eq!(session.get_session_data("k"), Some("v2"));
|
assert_eq!(
|
||||||
|
session.get_session_data("k").await.unwrap(),
|
||||||
|
Some("v2".into())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 烟雾测试 3:submit_turn 触发 OnTurnStart / OnTurnEnd hook。
|
/// 烟雾测试 3:submit_turn 触发 OnTurnStart / OnTurnEnd hook。
|
||||||
|
|||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user