feat(engine): 实现 Agent 执行引擎(SessionManager + Checkpointer + SessionSnapshot)

Phase 17 主体交付:解决 v0.2 中 session 在变量里、无父子关系、无 checkpoint、
不可序列化的空白。

新增模块 src/engine/(5 文件,约 1300 行纯实现 + 21 个内联测试):

- error.rs(46 行)—— EngineError 枚举(6 变体)
  - SessionNotFound / SessionAlreadyExists / CheckpointNotFound
  - Memory(#[from] MemoryError) 透传(与 AgentError 风格一致)
  - Serialization / Agent(#[from] AgentError)

- snapshot.rs(36 行)—— SessionSnapshot + SessionMemoryEntry
  - 独立 struct 避开 Arc<dyn Agent> 不可序列化限制
  - SessionMemoryEntry 保留 value/metadata/created_at 完整信息
  - 所有字段 #[serde(default)] 宽松反序列化保证前向兼容

- checkpointer.rs(377 行)—— Time-travel 检查点管理器
  - checkpoint() / rollback_load() / list_checkpoints() / delete_all() / latest_snapshot()
  - 存储 key:ckpt:{session_id}:{ckpt_id}
  - ckpt_id 纳秒+计数器(无外部依赖,ponytail)
  - CkptMeta.created_at_nanos 字段确保同秒内精确降序
  - 6 个内联测试覆盖 roundtrip / 不存在 ckpt / 降序排序 / delete_all 幂等 /
    latest_snapshot / 跨 session 隔离

- session_manager.rs(906 行)—— SessionManager 会话树管理
  - 内部 RwLock<HashMap> + Arc<tokio::sync::Mutex<AgentSession>> 双重锁
  - create / create_child / get / recover / replace / children / parent /
    destroy / submit_turn / submit_turn_stream / finalize_turn_stream 共 11 个公开方法
  - SessionManagerConfig.auto_checkpoint 默认 true(同步写入 +
    tracing::error! 失败不阻断主流程,不提供强持久化保证)
  - 孤儿策略:destroy 不递归删除子 session;父被销毁后 parent() 返回 None
  - create_child 限制:父 session 必须先 get/recover 到内存(bundle 不可序列化)
  - 15 个内联测试覆盖 CRUD / recover / replace / 树形 / 孤儿 / auto_checkpoint 开关 /
    序列化兼容性 / 幂等 / 10 并发创建

- mod.rs(20 行)—— 统一 pub use 重导出 EngineError / SessionManager /
  SessionManagerConfig / Checkpointer / CkptMeta / SessionSnapshot /
  SessionMemoryEntry

AgentSession 扩展(src/agent/session.rs,+152 行):
- to_snapshot() pub async —— 从 MemoryStore 拍平 session_memory 全量数据
- from_snapshot() pub fn Result —— 纯同步构造器(pending_memory_restore 暂存)
- restore_memory() pub async &mut self —— 写回持久层并清空 pending
- has_pending_memory_restore() —— 查询 pending 状态
- pub(crate) fn bundle() —— accessor 供 SessionManager::create_child 继承

SessionMemory 扩展(src/agent/session_memory.rs,+57 行):
- list_entries() —— 返回 Vec<(key, value, metadata, created_at_unix_secs)>
- set_with_meta() —— 保留 metadata/created_at 写入(供 restore_memory 完整恢复)

src/lib.rs —— pub mod engine 声明

examples/engine_demo.rs(+251 行):
端到端演示 create → submit_turn → checkpoint → list_checkpoints →
rollback_load → from_snapshot → restore_memory → replace → destroy 完整链路,
含 rollback 一致性 assert(turn_index 和 cost 恢复到 checkpoint 时刻)。

零新外部依赖(serde_json 已有)。全量 353 → 374(+21 新测试)。
clippy 0 警告,doc 0 warning,example exit 0。

Phase 17: Agent 执行引擎 — Step 2-7(合并提交)
This commit is contained in:
徐涛
2026-07-15 08:59:46 +08:00
parent 1d51dcdfe0
commit 34eec9f546
9 changed files with 1845 additions and 2 deletions
+252
View File
@@ -0,0 +1,252 @@
//! engine_demo —— SessionManager + Checkpointer 端到端示例。
//!
//! 演示:
//! 1. SessionManager::create 创建 session
//! 2. SessionManager::submit_turnauto_checkpoint=true 自动写 checkpoint
//! 3. SessionManager::create_child 创建子 session
//! 4. children() / parent() 树形查询
//! 5. Checkpointer::list_checkpoints 列出所有 checkpoint
//! 6. SessionManager::recover 从 checkpoint 恢复(模拟进程重启)
//! 7. AgentSession::to_snapshot + SessionManager::replace 演示 rollback 流程
//! 8. SessionManager::destroy 清理
//!
//! 运行:`cargo run --example engine_demo`
use std::sync::Arc;
use agcore::agent::{Agent, AgentBuilder, AgentSession};
use agcore::engine::SessionManager;
use agcore::llm::hooks::HookExecutor;
use agcore::llm::mock::MockProvider;
use agcore::llm::types::Usage;
use agcore::llm::types::message::{ContentBlock, Message};
use agcore::llm::types::response_v2::{MessageResponse, StopReason};
use agcore::memory::store::InMemoryStore;
use agcore::tools::ToolRegistry;
struct DemoAgent;
impl Agent for DemoAgent {
fn name(&self) -> &str {
"demo"
}
fn system_prompt(&self) -> Option<&str> {
Some("你是 demo agent,每轮回复一句话。")
}
}
fn assistant_text(text: &str) -> MessageResponse {
MessageResponse {
id: String::new(),
model: String::new(),
message: Message::Assistant {
content: vec![ContentBlock::Text { text: text.into() }],
},
usage: Usage::from_input_output(8, 4),
stop_reason: StopReason::Stop,
extra: Default::default(),
}
}
#[tokio::main]
async fn main() {
// 1. 准备底层组件
let store: Arc<dyn agcore::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
let provider = Arc::new(MockProvider::new(vec![
assistant_text("turn 1 response"),
assistant_text("turn 2 response"),
assistant_text("turn 3 response"),
assistant_text("child turn 1 response"),
assistant_text("recovered turn response"),
]));
let bundle = Arc::new(
AgentBuilder::new()
.provider(provider.clone())
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.session_memory_backend(store.clone())
.build()
.expect("RuntimeBundle 装配失败"),
);
let agent: Arc<dyn Agent> = Arc::new(DemoAgent);
// 2. 构造 SessionManagerauto_checkpoint 默认 true
let sm = SessionManager::new(store.clone());
println!("=== SessionManager 创建 ===");
// 3. create + submit_turnauto_checkpoint 触发)
println!("\n=== 创建根 session + 跑 3 轮 ===");
let parent_id = sm
.create(agent.clone(), bundle.clone())
.await
.expect("create 失败");
println!("parent_id = {parent_id}");
for i in 1..=3 {
let _resp = sm
.submit_turn(&parent_id, format!("turn {i}"))
.await
.expect("submit_turn 失败");
}
// 写入自定义 session memory 数据(演示持久层往返)
sm.get(&parent_id)
.await
.unwrap()
.lock()
.await
.set_session_data("design", "PostgreSQL")
.await
.unwrap();
// 4. 显式 checkpoint(覆盖 auto_checkpoint 的 turn-level,写入额外快照)
println!("\n=== Checkpointer 显式 checkpoint ===");
let ckpt_id = sm
.checkpointer()
.checkpoint(&*sm.get(&parent_id).await.unwrap().lock().await)
.await
.expect("checkpoint 失败");
println!("explicit ckpt_id = {ckpt_id}");
// 5. list_checkpoints
let metas = sm
.checkpointer()
.list_checkpoints(&parent_id)
.await
.expect("list_checkpoints 失败");
println!("parent session 有 {} 个 checkpoint", metas.len());
for m in &metas {
println!(
" - ckpt_id={}, turn_index={}, created_at={}",
m.ckpt_id, m.turn_index, m.created_at
);
}
// 6. create_child
println!("\n=== 创建子 session ===");
let child_id = sm
.create_child(&parent_id, agent.clone())
.await
.expect("create_child 失败");
println!("child_id = {child_id}");
let children = sm.children(&parent_id).await.expect("children 失败");
assert_eq!(children, vec![child_id.clone()]);
println!("children(parent) = {children:?}");
let parent_of_child = sm.parent(&child_id).await.expect("parent 失败");
assert_eq!(parent_of_child, Some(parent_id.clone()));
println!("parent({child_id}) = {parent_of_child:?}");
// 7. recover(模拟"进程重启"——新建 SessionManager 实例,但 store 复用)
println!("\n=== 从存储恢复 session(模拟进程重启)===");
let sm2 = SessionManager::new(store.clone());
let recovered = sm2
.recover(&parent_id, agent.clone(), bundle.clone())
.await
.expect("recover 失败");
let recovered_session = recovered.lock().await;
let v = recovered_session
.get_session_data("design")
.await
.expect("get_session_data 失败");
println!("recovered session_memory['design'] = {v:?}");
assert_eq!(v, Some("PostgreSQL".into()));
// 8. 演示 rollback 流程:先记录当前 turn_index,再 rollback 到一个早期 checkpoint
// 验证 session_memory 和 turn_index 已恢复到 checkpoint 时刻
println!("\n=== rollback 流程 ===");
let (before_turn, before_cost) = {
let s = sm.get(&parent_id).await.unwrap();
let g = s.lock().await;
(g.turn_index(), g.usage().total().total_tokens)
};
println!(
"rollback 前 turn_index={}, total_tokens={}",
before_turn, before_cost
);
let metas = sm
.checkpointer()
.list_checkpoints(&parent_id)
.await
.expect("list_checkpoints 失败");
assert!(metas.len() >= 2, "至少 2 个 checkpoint 才能演示 rollback");
// 取第二个 checkpoint(不是最新的)作为 rollback 目标
let rollback_ckpt = &metas[metas.len() - 2];
println!("rollback 到 ckpt_id={}", rollback_ckpt.ckpt_id);
let snapshot = sm
.checkpointer()
.rollback_load(&parent_id, &rollback_ckpt.ckpt_id)
.await
.expect("rollback_load 失败");
let snapshot_turn = snapshot.turn_index;
let snapshot_data_count = snapshot.session_memory_data.len();
println!(
"checkpoint 时刻 turn_index={}, session_memory 条目数={}",
snapshot_turn, snapshot_data_count
);
let mut rolled_back =
AgentSession::from_snapshot(snapshot, agent.clone(), bundle.clone()).expect("from_snapshot");
rolled_back
.restore_memory()
.await
.expect("restore_memory 失败");
sm.replace(&parent_id, rolled_back)
.await
.expect("replace 失败");
// 验证 rollback 后状态与 checkpoint 一致
let (after_turn, after_cost) = {
let s = sm.get(&parent_id).await.unwrap();
let g = s.lock().await;
(g.turn_index(), g.usage().total().total_tokens)
};
println!(
"rollback 后 turn_index={}, total_tokens={}",
after_turn, after_cost
);
assert!(
after_turn <= before_turn,
"rollback 后 turn_index({}) 应 ≤ rollback 前({})",
after_turn,
before_turn
);
assert_eq!(after_turn, snapshot_turn, "rollback 后 turn_index 应等于 checkpoint 时刻值");
assert!(after_cost <= before_cost, "rollback 后 cost 应 ≤ rollback 前");
println!("✓ rollback + replace 一致性验证通过");
// 9. destroy 父子 session
println!("\n=== 销毁 session ===");
sm.destroy(&child_id).await.expect("destroy child 失败");
sm.destroy(&parent_id).await.expect("destroy parent 失败");
// 验证清理
assert!(sm.get(&parent_id).await.is_err());
assert!(
sm.checkpointer()
.list_checkpoints(&parent_id)
.await
.unwrap()
.is_empty()
);
println!("✓ parent 已彻底清理(内存 + meta + checkpoints");
// 验证孤儿语义:父被销毁后子仍存在但 parent() 返回 None
println!("\n=== 孤儿策略演示(先创建父子,再仅销毁父)===");
let p_id = sm.create(agent.clone(), bundle.clone()).await.unwrap();
let c_id = sm.create_child(&p_id, agent.clone()).await.unwrap();
sm.destroy(&p_id).await.unwrap();
let p_of_c = sm.parent(&c_id).await.expect("parent 失败");
assert_eq!(p_of_c, None, "父被销毁后 child.parent() 应为 None");
println!("✓ child({c_id}) 仍是孤儿 sessionparent() = None");
// 清理孤儿
sm.destroy(&c_id).await.unwrap();
println!("\n✓ engine_demo 完成");
}