新增: 上下文管理演进 Wave1(契约定义 + T1动态阈值 + T2命名空间)

Agent 3: 共享契约类型定义
- CompressedSummary(T3输出格式)
- WorkflowContextBlock(T4工作流DAG上下文)
- WorkingContext + GoalItem/DecisionItem/StepInfo(T5常驻上下文)
- 脏标记版本追踪 + reset_all 压缩联动

T1: 动态压缩阈值
- maybe_auto_compress 加 sys_tokens 参数,触发条件从 budget*0.6 改为 available*0.6
- 调用处传 sys_tokens(loop顶部已缓存)
- 3 行核心改动

T2: 工具命名空间存储
- crates/df-ai/src/namespace_store.rs: 完整实现 + 单元测试
- process_tool_calls: Low/Med/High 路径 namespace 路由
- save_conversation: namespace 引用展开为原文再落 DB
- AiSession 新增 namespace_store 字段
This commit is contained in:
lxy
2026-07-20 01:05:20 +08:00
parent b1d7deece1
commit faa61a9ba5
10 changed files with 805 additions and 6 deletions
+251
View File
@@ -0,0 +1,251 @@
//! 工具命名空间存储 — 大工具结果不进主消息队列
//!
//! 当工具执行结果超过阈值时,存入 `NamespaceStore`,主队列只保留轻量引用路径。
//! 运行时为内存 HashMap(LRU 淘汰),持久化时由调用方展开引用写 DB。
//!
//! 存储格式: "namespace://tool_name/args_hash"
//! 例: namespace://read_file/a1b2c3d4
use std::collections::HashMap;
use std::time::Instant;
/// 引用路径前缀
pub const NAMESPACE_REF_PREFIX: &str = "namespace://";
/// namespace 字节阈值(> 2048 bytes 触发)
pub const NAMESPACE_BYTE_THRESHOLD: usize = 2048;
/// namespace 行数阈值(> 50 行 触发)
pub const NAMESPACE_LINE_THRESHOLD: usize = 50;
/// 始终进 namespace 的工具名(其输出几乎总是大结果)
pub const ALWAYS_LARGE_TOOLS: &[&str] = &["read_file", "list_directory", "grep", "diff_files"];
/// 判定工具结果是否应进入 namespace
pub fn should_use_namespace(content: &str, tool_name: &str) -> bool {
if ALWAYS_LARGE_TOOLS.contains(&tool_name) {
return true;
}
content.len() > NAMESPACE_BYTE_THRESHOLD
|| content.lines().count() > NAMESPACE_LINE_THRESHOLD
}
/// 判定 tool_result 内容是否为 namespace 引用路径
pub fn is_namespace_ref(content: &str) -> bool {
content.starts_with(NAMESPACE_REF_PREFIX)
}
/// 从引用路径中提取内部 key
pub fn parse_namespace_key(full_path: &str) -> Option<&str> {
full_path.strip_prefix(NAMESPACE_REF_PREFIX)
.and_then(|s| s.split('/').nth(1))
}
/// namespace 条目
#[derive(Debug, Clone)]
pub struct NamespaceEntry {
pub key: String,
pub tool_name: String,
pub content: String,
pub created_at: Instant,
pub access_count: u64,
}
/// namespace 存储(内存 HashMap + LRU 淘汰)
#[derive(Debug, Clone)]
pub struct NamespaceStore {
entries: HashMap<String, NamespaceEntry>,
max_bytes: usize,
current_bytes: usize,
}
impl NamespaceStore {
/// 创建 namespace 存储
pub fn new(max_bytes: usize) -> Self {
Self {
entries: HashMap::new(),
max_bytes,
current_bytes: 0,
}
}
/// 默认大小:10MB
pub fn default() -> Self {
Self::new(10 * 1024 * 1024)
}
/// 存入 namespace。返回引用路径字符串。
pub fn store(&mut self, tool_name: &str, content: &str) -> String {
let key = simple_hash(content);
let path = format!("{}{}/{}", NAMESPACE_REF_PREFIX, tool_name, key);
// 已存在 → 刷新 access_count,不重复占用空间
if self.entries.contains_key(&key) {
if let Some(entry) = self.entries.get_mut(&key) {
entry.access_count += 1;
}
return path;
}
// 检查容量,超限淘汰
let new_bytes = content.len();
self.evict_if_needed(new_bytes);
let entry = NamespaceEntry {
key: key.clone(),
tool_name: tool_name.to_string(),
content: content.to_string(),
created_at: Instant::now(),
access_count: 1,
};
self.current_bytes += new_bytes;
self.entries.insert(key, entry);
path
}
/// 读取完整内容
pub fn read(&mut self, full_path: &str) -> Option<&str> {
let key = parse_namespace_key(full_path)?;
let entry = self.entries.get_mut(key)?;
entry.access_count += 1;
Some(entry.content.as_str())
}
/// 读取(只读,不改 access_count
pub fn read_only(&self, full_path: &str) -> Option<&str> {
let key = parse_namespace_key(full_path)?;
self.entries.get(key).map(|e| e.content.as_str())
}
/// 检查 key 是否存在
pub fn contains(&self, full_path: &str) -> bool {
parse_namespace_key(full_path)
.and_then(|k| self.entries.get(k))
.is_some()
}
/// 条目数
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
/// LRU 淘汰:删除最旧条目直到腾出所需空间
fn evict_if_needed(&mut self, needed: usize) {
while self.current_bytes + needed > self.max_bytes && !self.entries.is_empty() {
// 找最旧的条目
let oldest_key = self.entries.iter()
.min_by_key(|(_, e)| e.created_at)
.map(|(k, _)| k.clone());
if let Some(key) = oldest_key {
if let Some(entry) = self.entries.remove(&key) {
self.current_bytes = self.current_bytes.saturating_sub(entry.content.len());
}
}
}
}
/// 清空
pub fn clear(&mut self) {
self.entries.clear();
self.current_bytes = 0;
}
}
/// 简单的基于长度的哈希(唯一性由 content 长度 + 前 16 字符保证)
/// 用于构建引用路径中的 key 段,不需要密码学强度
fn simple_hash(content: &str) -> String {
let prefix: String = content.chars().take(16).collect();
format!("{}_{}", content.len(), prefix)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_use_namespace_small_content() {
assert!(!should_use_namespace("short", "custom_tool"));
}
#[test]
fn should_use_namespace_large_bytes() {
let large = "x".repeat(3000);
assert!(should_use_namespace(&large, "custom_tool"));
}
#[test]
fn should_use_namespace_always_large_tool() {
assert!(should_use_namespace("short", "read_file"));
assert!(should_use_namespace("short", "list_directory"));
}
#[test]
fn should_use_namespace_many_lines() {
let many = (0..60).map(|i| format!("line {}", i)).collect::<Vec<_>>().join("\n");
assert!(should_use_namespace(&many, "custom_tool"));
}
#[test]
fn is_namespace_ref_detects_prefix() {
assert!(is_namespace_ref("namespace://read_file/abc"));
assert!(!is_namespace_ref("file content here"));
}
#[test]
fn parse_namespace_key_extracts_key() {
assert_eq!(parse_namespace_key("namespace://read_file/abc_123"), Some("abc_123"));
assert_eq!(parse_namespace_key("namespace://grep/xyz"), Some("xyz"));
assert_eq!(parse_namespace_key("no_prefix"), None);
}
#[test]
fn store_and_read() {
let mut ns = NamespaceStore::new(100_000);
let path = ns.store("read_file", "hello world content");
assert!(is_namespace_ref(&path));
assert!(path.contains("read_file"));
assert_eq!(ns.read(&path), Some("hello world content"));
}
#[test]
fn store_duplicate_key_does_not_double_count() {
let mut ns = NamespaceStore::new(100_000);
let content = "test content for duplicate check";
let p1 = ns.store("tool_a", content);
let p2 = ns.store("tool_b", content);
assert_eq!(ns.len(), 1, "相同内容应去重");
assert_eq!(ns.read(&p1), ns.read(&p2));
}
#[test]
fn eviction_oldest_removed_when_over_limit() {
let mut ns = NamespaceStore::new(100); // 极小容量
let small = "a"; // 1 byte
ns.store("t1", small);
assert_eq!(ns.len(), 1);
// 填充到超限
let big = "x".repeat(200);
ns.store("t2", &big);
// t1 可能被淘汰
if ns.current_bytes > 100 {
assert!(ns.len() < 2, "超限时应已淘汰旧条目");
}
}
#[test]
fn clear_resets_all() {
let mut ns = NamespaceStore::new(100_000);
ns.store("t1", "content");
assert!(!ns.is_empty());
ns.clear();
assert!(ns.is_empty());
assert_eq!(ns.current_bytes, 0);
}
}