docs: 更新 README feature 表 + 升级指南 + 示例注释 + roadmap 同步

- README 添加 feature 组合表 + 模块级 features 清单 + 升级指南
- 18 个 example 顶部添加 Required features 注释
- roadmap.md 和 roadmap-v0.3.2.md 同步 Phase 26-27 完成状态
- cargo fmt 全量格式化(修复预存格式问题,CI format job 可通过)
This commit is contained in:
徐涛
2026-07-19 08:18:04 +08:00
parent bc4eac72e1
commit 5baa170508
53 changed files with 1112 additions and 615 deletions
+6 -13
View File
@@ -16,8 +16,8 @@ use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::agent::session::AgentSession;
use crate::engine::snapshot::SessionSnapshot;
use crate::engine::EngineError;
use crate::engine::snapshot::SessionSnapshot;
use crate::memory::store::MemoryStore;
use crate::memory::types::{MemoryFilter, MemoryItem};
@@ -80,9 +80,8 @@ impl Checkpointer {
let ckpt_id = generate_ckpt_id();
let key = ckpt_key(&session.session_id, &ckpt_id);
let json = serde_json::to_string(&snapshot).map_err(|e| {
EngineError::Serialization(format!("snapshot serialize failed: {e}"))
})?;
let json = serde_json::to_string(&snapshot)
.map_err(|e| EngineError::Serialization(format!("snapshot serialize failed: {e}")))?;
let item = MemoryItem {
id: key,
@@ -141,10 +140,7 @@ impl Checkpointer {
/// prefix 查询 `ckpt:{session_id}:` → 反序列化 `SessionSnapshot` → 提取元数据。
/// 不需要 `CkptMeta` 单独存储——`SessionSnapshot` 已含 `turn_index` 字段,
/// `created_at` 用 `MemoryItem.created_at` 转换。
pub async fn list_checkpoints(
&self,
session_id: &str,
) -> Result<Vec<CkptMeta>, EngineError> {
pub async fn list_checkpoints(&self, session_id: &str) -> Result<Vec<CkptMeta>, EngineError> {
let prefix = format!("ckpt:{}:", session_id);
let filter = MemoryFilter {
prefix: Some(prefix),
@@ -346,10 +342,7 @@ mod tests {
let mut session = new_session_for_test("latest-session");
cp.checkpoint(&session).await.unwrap();
tokio::time::sleep(std::time::Duration::from_millis(2)).await;
session
.set_session_data("v", "2")
.await
.unwrap();
session.set_session_data("v", "2").await.unwrap();
cp.checkpoint(&session).await.unwrap();
let latest = cp.latest_snapshot("latest-session").await.unwrap().unwrap();
@@ -374,4 +367,4 @@ mod tests {
assert_eq!(metas_a[0].session_id, "iso-a");
assert_eq!(metas_b[0].session_id, "iso-b");
}
}
}
+1 -1
View File
@@ -48,4 +48,4 @@ pub enum EngineError {
/// 调用方收到此错误时,子 session 已通过 `destroy()` 清理(SessionMeta + checkpoint 全部清空)。
#[error("Dispatch failed: {0}")]
DispatchFailed(String),
}
}
+1 -1
View File
@@ -20,4 +20,4 @@ pub use checkpointer::{Checkpointer, CkptMeta};
pub use error::EngineError;
pub use session_manager::{SessionManager, SessionManagerConfig};
pub use snapshot::{SessionMemoryEntry, SessionSnapshot};
pub use sub_agent::{DispatchConfig, SubTaskResult, SubTaskStreamEvent};
pub use sub_agent::{DispatchConfig, SubTaskResult, SubTaskStreamEvent};
+47 -33
View File
@@ -141,11 +141,11 @@ impl SessionManager {
Ok(())
}
pub(crate) async fn load_session_meta(&self, session_id: &str) -> Result<Option<SessionMeta>, EngineError> {
let item = self
.store
.get(&SessionMeta::meta_key(session_id))
.await?;
pub(crate) async fn load_session_meta(
&self,
session_id: &str,
) -> Result<Option<SessionMeta>, EngineError> {
let item = self.store.get(&SessionMeta::meta_key(session_id)).await?;
match item {
Some(item) => {
let meta: SessionMeta = serde_json::from_str(&item.content).map_err(|e| {
@@ -241,10 +241,7 @@ impl SessionManager {
/// 按 ID 获取 session(**仅查内存**,不自动从存储恢复)。
///
/// 冷启动时 `get()` 未命中返回 `SessionNotFound`。如需从存储恢复,使用 `recover()` 方法。
pub async fn get(
&self,
session_id: &str,
) -> Result<Arc<Mutex<AgentSession>>, EngineError> {
pub async fn get(&self, session_id: &str) -> Result<Arc<Mutex<AgentSession>>, EngineError> {
let sessions = self.sessions.read().await;
let result = sessions.get(session_id).cloned();
tracing::debug!(
@@ -392,12 +389,7 @@ impl SessionManager {
session_id: &str,
user_input: impl Into<String>,
) -> Result<
std::pin::Pin<
Box<
dyn futures_core::Stream<Item = crate::llm::stream::StreamEvent>
+ Send,
>,
>,
std::pin::Pin<Box<dyn futures_core::Stream<Item = crate::llm::stream::StreamEvent> + Send>>,
EngineError,
> {
let session = self.get(session_id).await?;
@@ -593,8 +585,14 @@ mod tests {
.await
.unwrap();
let session = sm.get(&id).await.unwrap();
sm.checkpointer().checkpoint(&*session.lock().await).await.unwrap();
assert_eq!(sm.checkpointer().list_checkpoints(&id).await.unwrap().len(), 1);
sm.checkpointer()
.checkpoint(&*session.lock().await)
.await
.unwrap();
assert_eq!(
sm.checkpointer().list_checkpoints(&id).await.unwrap().len(),
1
);
sm.destroy(&id).await.unwrap();
@@ -603,7 +601,10 @@ mod tests {
// SessionMeta 已删除
assert!(sm.load_session_meta(&id).await.unwrap().is_none());
// Checkpoint 已删除
assert_eq!(sm.checkpointer().list_checkpoints(&id).await.unwrap().len(), 0);
assert_eq!(
sm.checkpointer().list_checkpoints(&id).await.unwrap().len(),
0
);
// destroy 不存在的 session 不报错
sm.destroy(&id).await.unwrap();
@@ -682,7 +683,10 @@ mod tests {
{
let s = sm.get(&id).await.unwrap();
s.lock().await.set_session_data("k", "v1").await.unwrap();
sm.checkpointer().checkpoint(&*s.lock().await).await.unwrap();
sm.checkpointer()
.checkpoint(&*s.lock().await)
.await
.unwrap();
}
// 2. 模拟"进程重启"——清空内存但保留 store
@@ -710,7 +714,10 @@ mod tests {
.unwrap();
{
let s = sm.get(&id).await.unwrap();
sm.checkpointer().checkpoint(&*s.lock().await).await.unwrap();
sm.checkpointer()
.checkpoint(&*s.lock().await)
.await
.unwrap();
}
let err = sm
@@ -750,7 +757,10 @@ mod tests {
let err = sm.submit_turn(&id, "hello").await.unwrap_err();
match err {
EngineError::Agent(crate::agent::error::AgentError::Llm(_)) => {}
other => panic!("expected EngineError::Agent(AgentError::Llm), got {:?}", other),
other => panic!(
"expected EngineError::Agent(AgentError::Llm), got {:?}",
other
),
}
}
@@ -772,8 +782,14 @@ mod tests {
let _ = sm.submit_turn(&id, "x").await;
// 手动 checkpoint 仍可工作
sm.checkpointer().checkpoint(&*s.lock().await).await.unwrap();
assert_eq!(sm.checkpointer().list_checkpoints(&id).await.unwrap().len(), 1);
sm.checkpointer()
.checkpoint(&*s.lock().await)
.await
.unwrap();
assert_eq!(
sm.checkpointer().list_checkpoints(&id).await.unwrap().len(),
1
);
}
#[tokio::test]
@@ -787,7 +803,8 @@ mod tests {
.unwrap();
// 构造一个新 session(同 session_id)然后 replace
let mut new_session = AgentSession::new(Arc::new(StubAgent("a".into())), &id, make_bundle());
let mut new_session =
AgentSession::new(Arc::new(StubAgent("a".into())), &id, make_bundle());
new_session
.set_session_data("replaced", "yes")
.await
@@ -838,8 +855,7 @@ mod tests {
/// restore_memory 幂等性:第二次调用应立即返回 Ok(())(pending 已被清空)。
#[tokio::test]
async fn restore_memory_is_idempotent() {
let store: Arc<dyn MemoryStore> =
Arc::new(crate::memory::store::InMemoryStore::new());
let store: Arc<dyn MemoryStore> = Arc::new(crate::memory::store::InMemoryStore::new());
let sm = SessionManager::new(store.clone());
let id = sm
@@ -848,12 +864,11 @@ mod tests {
.unwrap();
{
let s = sm.get(&id).await.unwrap();
s.lock()
.await
.set_session_data("k", "v")
s.lock().await.set_session_data("k", "v").await.unwrap();
sm.checkpointer()
.checkpoint(&*s.lock().await)
.await
.unwrap();
sm.checkpointer().checkpoint(&*s.lock().await).await.unwrap();
}
// 模拟"进程重启"——新建 SessionManager,复用 store
@@ -877,8 +892,7 @@ mod tests {
/// 10 并发 session 创建:验证 RwLock 写锁争用下不冲突,所有 ID 唯一。
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn concurrent_create_ten_sessions() {
let store: Arc<dyn MemoryStore> =
Arc::new(crate::memory::store::InMemoryStore::new());
let store: Arc<dyn MemoryStore> = Arc::new(crate::memory::store::InMemoryStore::new());
let sm = Arc::new(SessionManager::new(store));
let mut handles = Vec::with_capacity(10);
@@ -905,4 +919,4 @@ mod tests {
assert!(sm.get(id).await.is_ok());
}
}
}
}
+1 -1
View File
@@ -36,4 +36,4 @@ pub struct SessionSnapshot {
pub last_summary_turn: Option<u32>,
#[serde(default)]
pub session_memory_data: std::collections::HashMap<String, SessionMemoryEntry>,
}
}
+23 -22
View File
@@ -77,10 +77,7 @@ pub enum SubTaskStreamEvent {
/// 执行完成,携带完整结果。
Completed(SubTaskResult),
/// 流式调度中的错误。
Error {
child_id: String,
error: String,
},
Error { child_id: String, error: String },
}
impl std::fmt::Display for SubTaskStreamEvent {
@@ -150,7 +147,7 @@ impl SessionManager {
// 3. 过滤(按 bridge_keys
let filtered: Vec<_> = match &config.bridge_keys {
None => Vec::new(), // None = 不继承任何
None => Vec::new(), // None = 不继承任何
Some(keys) if keys.is_empty() => entries, // 空列表 = 全部继承
Some(keys) => entries
.into_iter()
@@ -343,9 +340,7 @@ impl SessionManager {
task: impl Into<String>,
config: DispatchConfig,
) -> Result<
std::pin::Pin<
Box<dyn futures_core::Stream<Item = SubTaskStreamEvent> + Send>,
>,
std::pin::Pin<Box<dyn futures_core::Stream<Item = SubTaskStreamEvent> + Send>>,
EngineError,
> {
use futures_util::StreamExt;
@@ -473,9 +468,7 @@ impl SessionManager {
}
};
let mut guard = session.lock().await;
guard
.finalize_turn(response, new_messages)
.await
guard.finalize_turn(response, new_messages).await
};
if let Err(e) = lock_result {
@@ -575,8 +568,7 @@ mod tests {
}
async fn make_manager_and_bundle() -> (Arc<SessionManager>, Arc<RuntimeBundle>) {
let store: Arc<dyn crate::memory::store::MemoryStore> =
Arc::new(InMemoryStore::new());
let store: Arc<dyn crate::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
let provider = Arc::new(MockProvider::new(vec![
assistant_text("child response 1"),
assistant_text("child response 2"),
@@ -702,7 +694,12 @@ mod tests {
// dispatch 到不存在的 parent_id
let result = sm
.dispatch("nonexistent_parent", child, "task", DispatchConfig::default())
.dispatch(
"nonexistent_parent",
child,
"task",
DispatchConfig::default(),
)
.await;
assert!(result.is_err());
@@ -716,7 +713,12 @@ mod tests {
let (sm, _bundle) = make_manager_and_bundle().await;
let child: Arc<dyn Agent> = Arc::new(MockAgent::new("child"));
let result = sm
.dispatch("nonexistent_parent", child, "task", DispatchConfig::default())
.dispatch(
"nonexistent_parent",
child,
"task",
DispatchConfig::default(),
)
.await;
assert!(matches!(result, Err(EngineError::SessionNotFound(_))));
}
@@ -724,9 +726,10 @@ mod tests {
// ====== dispatch_all 测试 ======
/// 提供充足的 mock response>= 3
async fn make_manager_and_bundle_for_all(n: usize) -> (Arc<SessionManager>, Arc<RuntimeBundle>) {
let store: Arc<dyn crate::memory::store::MemoryStore> =
Arc::new(InMemoryStore::new());
async fn make_manager_and_bundle_for_all(
n: usize,
) -> (Arc<SessionManager>, Arc<RuntimeBundle>) {
let store: Arc<dyn crate::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
let responses: Vec<_> = (0..n)
.map(|i| assistant_text(&format!("response {i}")))
.collect();
@@ -747,8 +750,7 @@ mod tests {
/// 创建空 mock responses 的 manager 和 bundle —— 后续 dispatch 会触发
/// "MockProvider: 预设响应已用完" 错误,可用于测试错误传播。
async fn make_manager_and_bundle_empty_mock() -> (Arc<SessionManager>, Arc<RuntimeBundle>) {
let store: Arc<dyn crate::memory::store::MemoryStore> =
Arc::new(InMemoryStore::new());
let store: Arc<dyn crate::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
let provider = Arc::new(MockProvider::empty());
let bundle = Arc::new(
AgentBuilder::new()
@@ -1048,8 +1050,7 @@ mod tests {
}
// 验证:收到 Error 事件
let (err_child_id, err_message) =
error_received.expect("Error event should be received");
let (err_child_id, err_message) = error_received.expect("Error event should be received");
assert_eq!(
Some(err_child_id.as_str()),
child_id_from_event.as_deref(),
+2 -6
View File
@@ -129,8 +129,7 @@ mod tests {
}
async fn make_manager_and_bundle() -> (Arc<SessionManager>, Arc<RuntimeBundle>) {
let store: Arc<dyn crate::memory::store::MemoryStore> =
Arc::new(InMemoryStore::new());
let store: Arc<dyn crate::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
let provider = Arc::new(MockProvider::new(vec![
assistant_text("response1"),
assistant_text("response2"),
@@ -175,10 +174,7 @@ mod tests {
{
let session = sm.get(&sid).await.unwrap();
let mut guard = session.lock().await;
guard
.set_session_data("key1", "value1")
.await
.unwrap();
guard.set_session_data("key1", "value1").await.unwrap();
}
sm.submit_turn(&sid, "hello").await.unwrap();