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:
+74
-65
@@ -15,24 +15,22 @@ use std::sync::Arc;
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::agent::agent::Agent;
|
||||
use crate::agent::context::{
|
||||
ContextSlot, DeriveStrategy, SlotConfig, SlotMode,
|
||||
};
|
||||
use crate::agent::context::{ContextSlot, DeriveStrategy, SlotConfig, SlotMode};
|
||||
// SlotSource 仅在 `mod tests` 中使用(通过 `use super::*;` 引入),lib 主体保留以避免测试 import 变更。
|
||||
#[allow(unused_imports)]
|
||||
use crate::agent::context::SlotSource;
|
||||
use crate::agent::error::AgentError;
|
||||
use crate::agent::runtime::RuntimeBundle;
|
||||
use crate::agent::session_memory::SessionMemory;
|
||||
use crate::agent::summary::{format_messages_as_text, SummaryConfig};
|
||||
#[cfg(feature = "engine")]
|
||||
use crate::engine::snapshot::{SessionMemoryEntry, SessionSnapshot};
|
||||
use crate::agent::summary::{SummaryConfig, format_messages_as_text};
|
||||
#[cfg(feature = "engine")]
|
||||
use crate::engine::EngineError;
|
||||
#[cfg(feature = "engine")]
|
||||
use crate::engine::snapshot::{SessionMemoryEntry, SessionSnapshot};
|
||||
use crate::llm::LlmProvider;
|
||||
use crate::llm::cycle::{CostTracker, CycleConfig, LlmCycle};
|
||||
use crate::llm::error::LlmError;
|
||||
use crate::llm::hooks::{HookContext, HookEvent};
|
||||
use crate::llm::LlmProvider;
|
||||
use crate::llm::stream::StreamEvent;
|
||||
use crate::llm::types::message::Message;
|
||||
use crate::llm::types::response_v2::MessageResponse;
|
||||
@@ -112,11 +110,7 @@ impl AgentSession {
|
||||
let session_memory = SessionMemory::new(backend, &session_id_str);
|
||||
|
||||
// 自动创建 "default" slot
|
||||
let default_slot = ContextSlot::new(
|
||||
&session_id_str,
|
||||
"default",
|
||||
SlotConfig::default(),
|
||||
);
|
||||
let default_slot = ContextSlot::new(&session_id_str, "default", SlotConfig::default());
|
||||
let mut slots = HashMap::new();
|
||||
slots.insert("default".to_string(), default_slot);
|
||||
|
||||
@@ -162,9 +156,7 @@ impl AgentSession {
|
||||
key: impl Into<String>,
|
||||
value: impl Into<String>,
|
||||
) -> Result<(), AgentError> {
|
||||
self.session_memory
|
||||
.set(&key.into(), &value.into())
|
||||
.await
|
||||
self.session_memory.set(&key.into(), &value.into()).await
|
||||
}
|
||||
|
||||
/// 读取一条会话级数据。
|
||||
@@ -204,11 +196,7 @@ impl AgentSession {
|
||||
if self.slots.contains_key(&id) {
|
||||
return Err(AgentError::SlotAlreadyExists(id));
|
||||
}
|
||||
let slot = ContextSlot::new(
|
||||
&self.session_id,
|
||||
&id,
|
||||
config.unwrap_or_default(),
|
||||
);
|
||||
let slot = ContextSlot::new(&self.session_id, &id, config.unwrap_or_default());
|
||||
slot.save(&*self.resolve_store()).await?;
|
||||
self.slots.insert(id, slot);
|
||||
Ok(())
|
||||
@@ -264,7 +252,9 @@ impl AgentSession {
|
||||
/// - 已删除后再 load 返回 None
|
||||
pub async fn delete_slot(&mut self, id: &str) -> Result<(), AgentError> {
|
||||
if id == "default" {
|
||||
return Err(AgentError::Config("Cannot delete the 'default' slot".into()));
|
||||
return Err(AgentError::Config(
|
||||
"Cannot delete the 'default' slot".into(),
|
||||
));
|
||||
}
|
||||
if self.slots.len() <= 1 {
|
||||
return Err(AgentError::Config("Cannot delete the last slot".into()));
|
||||
@@ -354,12 +344,7 @@ impl AgentSession {
|
||||
// 6. 只将本轮新增消息追加到当前 slot(保留全量历史,确保 Focused 模式的"读时过滤"语义不丢失数据)
|
||||
// cycle.messages() 包含 [system_prompt?, history..., user_input, tool_calls..., final_response]
|
||||
// 新增消息 = cycle.messages()[input_len..](跳过 initial_messages,即跳过已被持久化的内容)
|
||||
let new_messages: Vec<Message> = cycle
|
||||
.messages()
|
||||
.iter()
|
||||
.skip(input_len)
|
||||
.cloned()
|
||||
.collect();
|
||||
let new_messages: Vec<Message> = cycle.messages().iter().skip(input_len).cloned().collect();
|
||||
let store = self.resolve_store();
|
||||
if let Some(slot) = self.slots.get_mut(&self.current_slot_id) {
|
||||
slot.append_messages(new_messages)?;
|
||||
@@ -437,10 +422,7 @@ impl AgentSession {
|
||||
|
||||
// 5. 调用流式工具循环
|
||||
let stream = cycle
|
||||
.submit_with_tools_stream(
|
||||
user_input.into(),
|
||||
Arc::clone(&self.bundle.tool_registry),
|
||||
)
|
||||
.submit_with_tools_stream(user_input.into(), Arc::clone(&self.bundle.tool_registry))
|
||||
.await?;
|
||||
|
||||
// 6. turn_index 递增 —— 配合 finalize_turn 用 (turn_index - 1) 传递正确的 OnTurnEnd 序号
|
||||
@@ -492,7 +474,8 @@ impl AgentSession {
|
||||
.await;
|
||||
|
||||
// Phase 16: 摘要检查点(流式路径 turn_index 已被 submit_turn_stream 提前 ++1)
|
||||
self.maybe_summarize(self.turn_index.saturating_sub(1)).await;
|
||||
self.maybe_summarize(self.turn_index.saturating_sub(1))
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -569,11 +552,7 @@ impl AgentSession {
|
||||
if slots.is_empty() {
|
||||
slots.insert(
|
||||
"default".to_string(),
|
||||
ContextSlot::new(
|
||||
&snapshot.session_id,
|
||||
"default",
|
||||
SlotConfig::default(),
|
||||
),
|
||||
ContextSlot::new(&snapshot.session_id, "default", SlotConfig::default()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -619,12 +598,7 @@ impl AgentSession {
|
||||
|
||||
for (key, entry) in entries {
|
||||
self.session_memory
|
||||
.set_with_meta(
|
||||
&key,
|
||||
&entry.value,
|
||||
entry.metadata.clone(),
|
||||
entry.created_at,
|
||||
)
|
||||
.set_with_meta(&key, &entry.value, entry.metadata.clone(), entry.created_at)
|
||||
.await
|
||||
.map_err(EngineError::Agent)?;
|
||||
}
|
||||
@@ -686,13 +660,22 @@ impl AgentSession {
|
||||
let model = cfg.summary_model.clone();
|
||||
let prompt = cfg.summary_prompt.clone();
|
||||
|
||||
let result =
|
||||
Self::generate_summary(&provider, &messages, &prompt, model.as_deref(), max_tool_result_chars)
|
||||
.await;
|
||||
let result = Self::generate_summary(
|
||||
&provider,
|
||||
&messages,
|
||||
&prompt,
|
||||
model.as_deref(),
|
||||
max_tool_result_chars,
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(text) => {
|
||||
tracing::info!(turn = current_turn, summary_len = text.len(), "摘要自动生成成功");
|
||||
tracing::info!(
|
||||
turn = current_turn,
|
||||
summary_len = text.len(),
|
||||
"摘要自动生成成功"
|
||||
);
|
||||
// Resolve store first (immutable borrow on self) before mutable borrow on slots.
|
||||
let store = self.resolve_store();
|
||||
if let Some(slot) = self.slots.get_mut(&self.current_slot_id)
|
||||
@@ -755,8 +738,8 @@ impl AgentSession {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::agent::builder::AgentBuilder;
|
||||
use crate::agent::FocusedConfig;
|
||||
use crate::agent::builder::AgentBuilder;
|
||||
use crate::llm::hooks::{Hook, HookContext, HookExecutor, HookResult};
|
||||
use crate::llm::mock::MockProvider;
|
||||
use crate::llm::stream::StreamEvent;
|
||||
@@ -815,7 +798,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_session(provider_responses: Vec<MessageResponse>) -> (AgentSession, Arc<CountHook>, Arc<CountHook>) {
|
||||
fn build_session(
|
||||
provider_responses: Vec<MessageResponse>,
|
||||
) -> (AgentSession, Arc<CountHook>, Arc<CountHook>) {
|
||||
let mut hook_executor = HookExecutor::new();
|
||||
let start_count = Arc::new(CountHook(AtomicU32::new(0)));
|
||||
let end_count = Arc::new(CountHook(AtomicU32::new(0)));
|
||||
@@ -849,7 +834,8 @@ mod tests {
|
||||
/// 烟雾测试 1:AgentSession::submit_turn 跑通 mock provider(向后兼容)。
|
||||
#[tokio::test]
|
||||
async fn submit_turn_runs_with_mock_provider() {
|
||||
let (mut session, start_count, end_count) = build_session(vec![assistant_text("hello back")]);
|
||||
let (mut session, start_count, end_count) =
|
||||
build_session(vec![assistant_text("hello back")]);
|
||||
assert_eq!(session.turn_index(), 0);
|
||||
|
||||
let response = session.submit_turn("hi").await.unwrap();
|
||||
@@ -884,10 +870,8 @@ mod tests {
|
||||
/// 烟雾测试 3:submit_turn 触发 OnTurnStart / OnTurnEnd hook。
|
||||
#[tokio::test]
|
||||
async fn submit_turn_triggers_turn_hooks() {
|
||||
let (mut session, start_count, end_count) = build_session(vec![
|
||||
assistant_text("ok"),
|
||||
assistant_text("ok 2"),
|
||||
]);
|
||||
let (mut session, start_count, end_count) =
|
||||
build_session(vec![assistant_text("ok"), assistant_text("ok 2")]);
|
||||
|
||||
session.submit_turn("hi").await.unwrap();
|
||||
assert_eq!(start_count.0.load(Ordering::SeqCst), 1);
|
||||
@@ -940,9 +924,15 @@ mod tests {
|
||||
// 即 [user_input, tool_results?, final_response](不含 system_prompt,system 由 agent 提供)
|
||||
assert!(slot.messages.len() >= 2, "应至少包含 user 和 assistant");
|
||||
// 验证 user 输入和 assistant 响应都已写入
|
||||
let has_user = slot.messages.iter().any(|m| extract_text(m) == "user input");
|
||||
let has_user = slot
|
||||
.messages
|
||||
.iter()
|
||||
.any(|m| extract_text(m) == "user input");
|
||||
let has_resp = slot.messages.iter().any(|m| extract_text(m) == "resp");
|
||||
assert!(has_user && has_resp, "slot 应包含 user input 和 assistant response");
|
||||
assert!(
|
||||
has_user && has_resp,
|
||||
"slot 应包含 user input 和 assistant response"
|
||||
);
|
||||
}
|
||||
|
||||
/// Phase 10: create_slot 创建新 slot。
|
||||
@@ -986,7 +976,11 @@ mod tests {
|
||||
// 3. 检查 slot_a 的消息数
|
||||
let slot_a = session.slots.get("slot_a").unwrap();
|
||||
let slot_a_count = slot_a.messages.len();
|
||||
assert!(slot_a_count >= 2, "slot_a 至少 2 条消息,实际 {}", slot_a_count);
|
||||
assert!(
|
||||
slot_a_count >= 2,
|
||||
"slot_a 至少 2 条消息,实际 {}",
|
||||
slot_a_count
|
||||
);
|
||||
|
||||
// 4. 切回 default,验证 default 不包含 slot_a 的消息
|
||||
session.switch_slot("default").await.unwrap();
|
||||
@@ -1281,7 +1275,10 @@ mod tests {
|
||||
.iter()
|
||||
.any(|m| matches!(m, Message::User { .. }));
|
||||
let has_resp = slot.messages.iter().any(|m| extract_text(m) == "hi back");
|
||||
assert!(has_user && has_resp, "default slot 应包含 user 和 assistant 消息");
|
||||
assert!(
|
||||
has_user && has_resp,
|
||||
"default slot 应包含 user 和 assistant 消息"
|
||||
);
|
||||
}
|
||||
|
||||
/// Phase 9 Step 5.2 — `submit_turn_stream` 触发 OnTurnStart / OnTurnEnd hook。
|
||||
@@ -1380,7 +1377,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn summary_not_generated_below_threshold() {
|
||||
let mut session = build_session_with_summary(
|
||||
vec![assistant_text("a"), assistant_text("b"), assistant_text("c")],
|
||||
vec![
|
||||
assistant_text("a"),
|
||||
assistant_text("b"),
|
||||
assistant_text("c"),
|
||||
],
|
||||
vec![assistant_text("should_not_appear")],
|
||||
SummaryConfig::default(),
|
||||
);
|
||||
@@ -1400,16 +1401,20 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn summary_generated_above_threshold() {
|
||||
let mut session = build_session_with_summary(
|
||||
vec![assistant_text("a"), assistant_text("b"), assistant_text("c")],
|
||||
vec![
|
||||
assistant_text("a"),
|
||||
assistant_text("b"),
|
||||
assistant_text("c"),
|
||||
],
|
||||
vec![
|
||||
assistant_text("summary-1"),
|
||||
assistant_text("summary-2"),
|
||||
assistant_text("summary-3"),
|
||||
],
|
||||
SummaryConfig {
|
||||
max_context_tokens: 20, // 阈值 20 * 0.5 = 10
|
||||
max_context_tokens: 20, // 阈值 20 * 0.5 = 10
|
||||
trigger_token_ratio: 0.5,
|
||||
debounce_turns: 0, // 关闭防抖便于测试
|
||||
debounce_turns: 0, // 关闭防抖便于测试
|
||||
..SummaryConfig::default()
|
||||
},
|
||||
);
|
||||
@@ -1459,7 +1464,11 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn summary_written_to_session_memory_but_full_mode_does_not_inject() {
|
||||
let mut session = build_session_with_summary(
|
||||
vec![assistant_text("a"), assistant_text("b"), assistant_text("c")],
|
||||
vec![
|
||||
assistant_text("a"),
|
||||
assistant_text("b"),
|
||||
assistant_text("c"),
|
||||
],
|
||||
vec![assistant_text("captured-summary")],
|
||||
SummaryConfig {
|
||||
max_context_tokens: 20,
|
||||
@@ -1485,7 +1494,7 @@ mod tests {
|
||||
// 故意只提供 1 个对话响应;摘要调用时队列耗尽,MockProvider 返回 LlmError::Other
|
||||
let mut session = build_session_with_summary(
|
||||
vec![assistant_text("only-one")], // 后续摘要会失败
|
||||
vec![], // 无摘要响应
|
||||
vec![], // 无摘要响应
|
||||
SummaryConfig {
|
||||
max_context_tokens: 20,
|
||||
trigger_token_ratio: 0.5,
|
||||
@@ -1639,4 +1648,4 @@ mod tests {
|
||||
let summary = session.get_conversation_summary().await.unwrap();
|
||||
assert!(summary.is_none(), "巨型 max_context_tokens 应永不触发");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user