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:
+35
-21
@@ -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<Mutex<_>>` 包装。
|
||||
pub struct AgentSession {
|
||||
/// 会话 ID(由调用方指定,用于日志/追踪/记忆关联)。
|
||||
@@ -32,8 +33,8 @@ pub struct AgentSession {
|
||||
bundle: Arc<RuntimeBundle>,
|
||||
turn_index: u32,
|
||||
cost_so_far: CostTracker,
|
||||
/// 会话级键值数据(Phase 4a 用内联 HashMap;Phase 4c 替换为 `SessionMemory`)。
|
||||
session_data: HashMap<String, String>,
|
||||
/// 会话级记忆(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::<Vec<_>>())
|
||||
.field("session_memory", &"<SessionMemory>")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
@@ -57,13 +58,19 @@ impl AgentSession {
|
||||
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.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<String, String> {
|
||||
&self.session_data
|
||||
/// 会话级记忆引用。
|
||||
pub fn session_memory(&self) -> &SessionMemory {
|
||||
&self.session_memory
|
||||
}
|
||||
|
||||
/// 写入一条会话级数据(覆盖同名 key)。
|
||||
pub fn set_session_data(&mut self, key: impl Into<String>, value: impl Into<String>) {
|
||||
self.session_data.insert(key.into(), value.into());
|
||||
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 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<Option<String>, 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。
|
||||
|
||||
Reference in New Issue
Block a user