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:
@@ -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
@@ -48,4 +48,4 @@ pub enum EngineError {
|
||||
/// 调用方收到此错误时,子 session 已通过 `destroy()` 清理(SessionMeta + checkpoint 全部清空)。
|
||||
#[error("Dispatch failed: {0}")]
|
||||
DispatchFailed(String),
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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};
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user