新增: 上下文管理演进 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
+167 -1
View File
@@ -1,4 +1,4 @@
//! 上下文管理纯函数与数据类型 — 从 context.rs 抽离的无 `self` 依赖部分
//! 上下文管理纯函数与数据类型 — 从 context.rs 抽离的无 `self` 依赖部分
//!
//! 职责:
//! - Token 粗估器(字符级近似,无 tokenizer 依赖)
@@ -553,6 +553,9 @@ pub const TOOL_MISSING_PREFIX: &str = "tool_missing_";
/// 兜底:sanitize view-only 不改持久化,失败只影响单请求,flag 关→行为完全等价改动前。
pub const PLACEHOLDER_INTEGRITY_ENABLED: bool = true;
/// namespace 引用路径前缀(转发至 namespace_store,供 context.rs 统一路径引用)
pub const NAMESPACE_REF_PREFIX: &str = crate::namespace_store::NAMESPACE_REF_PREFIX;
/// 占位 tool_result 内容中嵌入的唯一标记前缀(供 sanitize 识别"审批挂起占位,不可裁")。
///
/// 写入处(audit/cache.rs:PENDING_APPROVAL_PLACEHOLDER)格式:`{占位文本}{__PENDING__}{tc_id}`,
@@ -604,6 +607,169 @@ pub fn extract_pending_tc_id(content: &str) -> Option<&str> {
.filter(|id| !id.is_empty())
}
// ============================================================
// Wave 共享契约类型(由 上下文管理演进-执行计划 Agent 3 定义)
// ============================================================
/// 压缩摘要双格式(T3 输出, T5 消费)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CompressedSummary {
/// JSON 卡片序列化(主题/决策/待办/token节省)
pub json_card: String,
/// 自然语言摘要(向前兼容关键词兜底降级)
pub nl_summary: String,
}
/// 工作流 DAG 上下文块(T4 输出 → 注入 system prompt
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkflowContextBlock {
pub workflow_id: String,
pub workflow_name: String,
pub total_nodes: usize,
pub completed_nodes: usize,
pub current_node: Option<String>,
pub next_nodes: Vec<String>,
}
impl WorkflowContextBlock {
/// 格式化为 system prompt 可读文本
pub fn to_system_text(&self) -> String {
let mut s = format!("工作流: {} (ID: {})", self.workflow_name, self.workflow_id);
s.push_str(&format!("\n进度: {}/{}", self.completed_nodes, self.total_nodes));
if let Some(ref cur) = self.current_node {
s.push_str(&format!("\n当前步骤: {}", cur));
}
if !self.next_nodes.is_empty() {
s.push_str(&format!("\n后续: {}", self.next_nodes.join("")));
}
s
}
}
/// 目标状态(T5 WorkingContext
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum GoalStatus {
Active,
Completed,
Cancelled,
}
/// 步骤状态
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum StepStatus {
Pending,
InProgress,
Done,
Blocked,
}
/// 目标条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GoalItem {
pub id: String,
pub text: String,
pub status: GoalStatus,
}
/// 决策条目
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DecisionItem {
pub question: String,
pub decision: String,
pub reason: String,
}
/// 步骤信息
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StepInfo {
pub step_name: String,
pub status: StepStatus,
}
/// WorkingContext 版本号(脏标记追踪)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkingContextVersion {
pub goals_changed_at: u32,
pub decisions_changed_at: u32,
pub unresolved_changed_at: u32,
pub step_changed_at: u32,
pub current_turn: u32,
}
impl WorkingContextVersion {
pub fn new() -> Self {
Self {
goals_changed_at: 0,
decisions_changed_at: 0,
unresolved_changed_at: 0,
step_changed_at: 0,
current_turn: 0,
}
}
/// 压缩后重置:将所有字段标记为本轮变更
pub fn reset_all(&mut self, _summary: &str, current_turn: u32) {
self.goals_changed_at = current_turn;
self.decisions_changed_at = current_turn;
self.unresolved_changed_at = current_turn;
self.current_turn = current_turn;
}
}
/// WorkingContext 常驻上下文(T5 实现)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkingContext {
pub goals: Vec<GoalItem>,
pub decisions: Vec<DecisionItem>,
pub unresolved: Vec<String>,
pub current_step: Option<StepInfo>,
pub version: WorkingContextVersion,
}
impl WorkingContext {
pub fn new() -> Self {
Self {
goals: Vec::new(),
decisions: Vec::new(),
unresolved: Vec::new(),
current_step: None,
version: WorkingContextVersion::new(),
}
}
/// 构建本轮需要注入的 L1 内容(L1a 必带 + L1b 条件注入)
pub fn build_injection(&self, recent_turns: u32) -> String {
let mut parts: Vec<String> = Vec::new();
// L1a: 当前步骤(每轮必带)
if let Some(ref step) = self.current_step {
parts.push(format!("当前步骤: {} [{:?}]", step.step_name, step.status));
}
// L1b: 过去 N 轮内有变更才注入
let threshold = self.version.current_turn.saturating_sub(recent_turns);
let has_active = self.goals.iter().any(|g| g.status == GoalStatus::Active);
if has_active && self.version.goals_changed_at >= threshold {
let active: Vec<&str> = self.goals.iter()
.filter(|g| g.status == GoalStatus::Active)
.map(|g| g.text.as_str())
.collect();
parts.push(format!("目标({}): {}", active.len(), active.join("; ")));
}
if self.version.decisions_changed_at >= threshold && !self.decisions.is_empty() {
let recent: Vec<&str> = self.decisions.iter().map(|d| d.decision.as_str()).collect();
parts.push(format!("近期决策: {}", recent.join("; ")));
}
if self.version.unresolved_changed_at >= threshold && !self.unresolved.is_empty() {
parts.push(format!("待解: {}", self.unresolved.join("; ")));
}
parts.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;