新增: 上下文管理演进设计文档(发散思考 + 任务技术设计)

发散思考文档涵盖:
- ContextManager 三维评估(效率/合理性/成本)
- 业界方案对标(Claude Code/OpenAI SDK/Mem0/LangGraph 等)
- 融合设计: 结构化分层上下文引擎(L1常驻/L2历史/L3摘要)
- 9 种方法交叉论证(决策矩阵/ROI/帕累托/Kano/风险矩阵等)
- 7 个发散方向与冲突分析,推荐派系 A 路线

任务技术设计文档涵盖:
- T2 工具命名空间: 大工具结果不进主队列,NamespaceStore 详细设计
- T4 工作流 DAG 注入: 活跃路径裁剪,结构化 DAG 进 system prompt
- T3 L3 结构化摘要: JSON+NL 双格式压缩摘要
- T1 动态压缩阈值: 精确到行的 3 处改动
- 4 种边界情况推演,AI coding 多 agent 并行策略
This commit is contained in:
lxy
2026-07-20 00:53:53 +08:00
parent e9e3578d26
commit b1d7deece1
3 changed files with 1616 additions and 0 deletions
@@ -0,0 +1,732 @@
# 上下文管理演进 — 任务技术设计
> 创建: 2026-07-20 | 状态: 设计阶段 | 关联文档: `上下文管理演进与发散思考-2026-07-20.md`
---
## 目录
- [T2 工具命名空间(详细设计)](#t2-工具命名空间详细设计)
- [T4 工作流 DAG 注入(详细设计)](#t4-工作流-dag-注入详细设计)
- [T3 L3 结构化摘要(设计概要)](#t3-l3-结构化摘要设计概要)
---
## T2 工具命名空间(详细设计)
### 现有流程 vs 新流程
```
现有流程:
AiToolRegistry.execute(name, args)
→ 工具执行 → 全量结果 String
→ push(ChatMessage::tool_result(全量内容)) ← 不管大小,全塞进消息队列
→ build_for_request 时,大 tool_result 撑爆预算
→ 压缩/裁剪 → 丢失细节
新流程:
AiToolRegistry.execute(name, args)
→ 工具执行 → 全量结果 String
→ if should_use_namespace(全量内容)
→ store_to_namespace(key, 全量内容) ← 存入独立存储
→ push(ChatMessage::tool_result(引用路径)) ← 主队列只插引用
else
→ push(ChatMessage::tool_result(全量内容)) ← 小结果不动
→ build_for_request 时,引用只占 ~20 tokens
→ 压缩不影响 namespace 内容
→ 模型通过引用路径按需读取(或系统自动补全)
```
### 数据结构
```rust
// crates/df-ai/src/namespace_store.rs
/// namespace 引用路径格式: "namespace://tool_name/args_hash"
/// 例: "namespace://read_file/a1b2c3d4"
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 存储
///
/// 内存中为 HashMap,提供常规 CRUD
/// 外部可注入持久化实现(接 SQLite / 内存 fallback)。
/// 设计为独立于 ContextManager 的结构——不被压缩/裁剪影响。
pub struct NamespaceStore {
/// key = 引用路径 hash, value = 原始工具结果
entries: HashMap<String, NamespaceEntry>,
/// 总字节数上限(防内存爆炸,超限淘汰最旧条目)
max_bytes: usize,
current_bytes: usize,
}
pub struct NamespaceEntry {
pub key: String, // 完整引用路径
pub tool_name: String,
pub args: serde_json::Value,
pub content: String, // 原始工具执行结果
pub content_summary: String, // extract_key_info 后的摘要(供 LLM 预览)
pub created_at: Instant,
pub access_count: u64,
}
```
### 关键决策点
#### 决策 1:何时进 namespace
```rust
pub fn should_use_namespace(content: &str, tool_name: &str) -> bool {
// 1. 明确标记为"大结果"的工具(read_file / list_directory 几乎总是大)
if is_always_large_tool(tool_name) {
return true;
}
// 2. 按大小阈值判定
content.len() > NAMESPACE_BYTE_THRESHOLD
|| content.lines().count() > NAMESPACE_LINE_THRESHOLD
}
fn is_always_large_tool(tool_name: &str) -> bool {
matches!(tool_name, "read_file" | "list_directory" | "grep" | "diff_files")
}
```
#### 决策 2:模型看到什么
LLM 在 tool_result 消息中看到的不是全量内容,而是:
```
当前(小结果原样,大结果原样):
[tool_result] 342 行文件内容...(占 2000 tokens
新流程(引用路径 + 摘要预览):
[tool_result] <namespace://read_file/a1b2c3>
[tool_result]
文件: config.rs
行数: 342
关键节点: timeout:15 (行15), pool_size:10 (行42)
```
**LLM 的理解能力验证**:如果模型需要读取完整内容,system prompt 需说明:
```
[上下文说明]
当工具结果以 <namespace://path> 格式返回时,表示完整内容已存储。
如果你需要查看完整内容,回复 /read_namespace path
系统将自动拉取完整内容替换当前引用。
```
或自动更激进——当 LLM 的回复引用某 namespace 路径时,系统在下一轮自动拉取:
```
LLM: "config.rs 第 15 行的 timeout 需要改"
系统检测: /read_namespace 触发 → 从 namespace 拉取 config.rs 完整内容
→ 下轮 tool_result 消息中替换为完整内容
→ LLM 看到原文,精确引用行号
```
#### 决策 3:持久化与生命周期
```rust
impl NamespaceStore {
/// 存入 namespace
pub fn store(&mut self, key: &str, tool_name: &str, args: &Value, content: &str) -> String {
let summary = extract_key_info(content); // 复用现有 extract_key_info
let entry = NamespaceEntry {
key: key.to_string(),
tool_name: tool_name.to_string(),
args: args.clone(),
content: content.to_string(),
content_summary: summary,
created_at: Instant::now(),
access_count: 0,
};
self.current_bytes += content.len();
// 超限淘汰:从最旧开始删
while self.current_bytes > self.max_bytes {
// ... LRU 淘汰
}
format!("{}{}/{}", NAMESPACE_REF_PREFIX, tool_name, key)
}
/// 读取完整内容
pub fn read(&mut self, full_path: &str) -> Option<&str> {
let key = self.parse_key(full_path)?;
let entry = self.entries.get_mut(&key)?;
entry.access_count += 1;
Some(entry.content.as_str())
}
/// 读取摘要(用于自动补全判断)
pub fn read_summary(&self, full_path: &str) -> Option<&str> {
let key = self.parse_key(full_path)?;
self.entries.get(&key).map(|e| e.content_summary.as_str())
}
}
```
### 接线点:process_tool_calls
改动点位于 `audit/mod.rs::process_tool_calls`,工具执行完成后的 push 路径:
```rust
// current (audit/mod.rs ~380行附近)
pub(crate) async fn process_tool_calls(..., conv_id: &str) -> usize {
// ... 现有审批/执行逻辑 ...
for (index, draft) in tc_list {
// ... 执行工具 ...
let result = tools_arc.execute(&draft.name, &draft.args).await;
// ★ 新增:namespace 判断
let content = if should_use_namespace(&result, &draft.name) {
let session = session_arc.lock().await;
let ns = &mut session.namespace_store; // AiSession 新增字段
let ref_path = ns.store(&draft.name, &draft.args, &result);
ref_path // ← 主队列只推引用路径
} else {
result // ← 小结果原样推
};
// 后续 push 不变
let msg = ChatMessage::tool_result(draft.tool_call_id.clone(), &content);
session.conv(conv_id).messages.push(msg);
}
}
```
### AiSession 新增字段
```rust
// src-tauri/src/commands/ai/mod.rs 或 state.rs
pub struct AiSession {
pub conversations: HashMap<String, AiConversation>,
pub current_conv: Option<String>,
pub generating: HashSet<String>,
// ★ 新增
pub namespace_store: NamespaceStore,
}
```
### 边界情况推演
#### 情况 1:模型不理解为 namespace 引用
```
LLM 收到 [tool_result] <namespace://read_file/a1b2c3>
LLM 回复: "读了 config.rs,但我不知道内容是什么"
→ 用户体验差
```
**对策**:引用路径不是结束。在 `process_tool_calls` push 引用时,额外 push 一条 assistant 消息或修改 tool_result 内容格式:
```
[tool_result] 文件 config.rs (342 行)
| 关键行: timeout:15 (行15), pool_size:10 (行42)
| 完整内容: <namespace://read_file/a1b2c3>
```
即:`extract_key_info(已有)+ 引用路径` 混合格式。LLM 可以靠摘要感知文件内容,只有需要精确行号时才触发 `/read_namespace`
#### 情况 2:同一文件在 2 轮内被多次引用
```
第 3 轮: read_file("config.rs") → namespace 存 342 行 → 引用
第 5 轮: 模型需要第 15 行 → /read_namespace → 系统拉取
```
第 5 轮拉取后,第 6 轮应该做什么?
**方案 A(推荐)**:拉取后仅在**当轮** `tool_result` 替换为完整内容。下一轮重新压缩时,如果内容大再次进入 namespace。
```
第 5 轮: [tool_result] config.rs 342 行全文(/read_namespace 触发的)
第 6 轮: 压缩 → config.rs 内容被压缩掉 → 正常
第 7 轮: 模型再次需要 → 再次 /read_namespace → namespace 还在
```
**方案 B**:拉取后一直保留在消息队列中。
→ 不推荐,回到了 tool_result 膨胀的老路。
#### 情况 3namespace 内存爆炸
```
最大对话: 500 轮,平均每轮存 2 个 namespace 条目,每条约 3k bytes
→ 500 × 2 × 3k = 3MB
```
`max_bytes = 10MB`(宽松上限),最旧条目自动淘汰。淘汰后如果有模型再次引用:
```
LLM: /read_namespace namespace://read_file/a1b2c3
系统: 条目已淘汰 → 重新执行工具 → 重新 namespace 存储 → 返回内容
```
等价于「cache miss」,对用户透明。
#### 情况 4DB 持久化需不需要存 namespace
**不需要**。namespace 是运行时缓存优化,不是持久化真相源。全量消息已在 `save_conversation` 写入 `ai_messages` 表(uncompressed),压缩后的摘要也在 system 消息中。namespace 淘汰后,可通过 DB 重新构建,但需要保证:
```rust
// save_conversation 中
if msg.content.starts_with(NAMESPACE_REF_PREFIX) {
// 写 DB 前将引用替换回原始内容(从 namespace 中取)
// 替代方案:DB 也存引用路径,恢复时从 namespace 重建
}
```
**建议**:DB 存引用路径。恢复时 namespace 可能已淘汰,此时:
1. 重新执行工具(不可行——工具可能有副作用)
2. DB 保留原文(`save_conversation` 时展开引用)
**结论**DB 存原文。save 时 namespace 条目肯定存在(刚执行完),展开引用写入 DB。恢复时直接读 DB 原文,namespace 只服务于运行时。
```rust
// save_conversation 中展开引用
let content = if msg.content.starts_with(NAMESPACE_REF_PREFIX) {
namespace_store.read(&msg.content)
.unwrap_or(&msg.content) // 兜底:用引用路径自身(可能性低)
} else {
&msg.content
};
record.content = content.to_string();
```
### 推演:T2 在 T-heavy 场景中的行为
```
第 3 轮: read_file("config.rs") → 342 行
应进 namespace ✓
主队列: [tool_result] 文件 config.rs (342行), 行15:timeout, 行42:pool
主队列 token: ~80(→原来 2000
节省: 1920 tokens
第 4-7 轮: 另有 3 次大工具结果 → 每次省 ~1500-2000 tokens
第 8 轮: 压缩触发
当前: 主队列总 history_tokens ≈ 5000(含 4 条引用)
→ 未达压缩阈值(原应为 20000+)
压缩不触发 ✓ 用户无感知
第 15 轮: 用户回来 "config.rs 的 timeout 在第几行?"
LLM 在压缩摘要中仍能找到"行15:timeout" ← 摘要来自 extract_key_info
→ 精确回答 ✓ 不需要 /read_namespace
第 16 轮: "删掉那行,改成 30"
LLM 需要完整文件结构来做 diff
→ /read_namespace namespace://read_file/xxx
→ 系统拉取完整 342 行
→ 第 16 轮 tool_result 出现完整内容
→ 第 17 轮压缩 → 完整内容又被 namespace 吸收 → 回到摘要+引用
```
**总节省**:15 轮对话中,原本平均每轮 4000 tokens 的大 tool_result 占用 → 现在每轮 ~100 tokens 引用 + 摘要。**累积节省 ~60k tokens,约 $0.18Sonnet)。**
---
## T4 工作流 DAG 注入(详细设计)
### 现有流程 vs 新流程
```
现有:
run_agentic_loop system_prompt 拼接:
[系统指令] + [工具定义] + [pinned_goals] + [知识注入]
→ build_for_request
→ LLM 只能从消息历史中推断"当前在做什么"
新流程:
run_agentic_loop system_prompt 拼接前:
if conv.workflow_id != None:
dag = df_workflow::Dag::load(conv.workflow_id)
dag_block = render_dag_to_system_block(dag)
system_prompt = dag_block + system_prompt_原有内容
→ build_for_request(system_prompt + ...)
→ LLM 看到结构化 DAG"任务进展"一目了然
```
### 数据结构
```rust
// src-tauri/src/commands/ai/workflow_context.rs
/// 工作流上下文块 — 注入 system prompt 的 DAG 摘要
pub struct WorkflowContextBlock {
pub workflow_id: String,
pub workflow_name: String,
pub total_nodes: usize,
pub completed_nodes: usize,
pub current_node: Option<WorkflowNodeSummary>,
pub next_nodes: Vec<WorkflowNodeSummary>,
}
/// DAG 节点摘要 — 不进完整 DAG,只进关键上下文
pub struct WorkflowNodeSummary {
pub node_id: String,
pub node_type: String, // "AINode" | "ScriptNode" | "HumanNode" | ...
pub label: String, // 用户或 LLM 设定的节点名称
pub status: String, // "completed" | "running" | "pending" | "blocked"
pub output_summary: Option<String>, // 节点输出的摘要(关键产出)
}
```
### 关键决策点
#### 决策 1:DAG 全量注入还是摘要注入
```
DAG 可能很大(50+ 节点)。全量注入 ≈ 2000+ tokens,不可接受。
```
**方案**:只注入「当前活跃路径」。从 DAG 的起始节点到当前节点 + 后续 2 层子节点。其余节点不注入。
```mermaid
graph LR
subgraph "DAG 全量(50 节点)"
N1["N1 ✅"] --> N2["N2 ✅"]
N1 --> N3["N3 ✅"]
N2 --> N4["N4 🚧 当前"]
N2 --> N5["N5 pending"]
N3 --> N6["N6 pending"]
N4 --> N7["N7 pending"]
N5 --> N8["N8 pending"]
N6 --> N9["N9 pending"]
N3 --> N10["N10 pending"]
N4 --> N11["N11 pending"]
end
subgraph "注入内容(当前活跃路径,5 节点)"
I1["N1: 读源码 ✅"]
I2["N2: 分析依赖 ✅"]
I3["N4: 改配置 🚧"]
I4["N5: 验证配置 pending"]
I5["N7: 提交变更 pending"]
end
```
```rust
pub fn build_active_path(dag: &Dag, current_node_id: &str) -> WorkflowContextBlock {
let path = dag.path_to_root(current_node_id); // 到根的全路径
let next = dag.children(current_node_id, 2); // 后续 2 层
WorkflowContextBlock {
workflow_id: dag.id.clone(),
workflow_name: dag.name.clone(),
total_nodes: dag.nodes.len(),
completed_nodes: dag.nodes.iter().filter(|n| n.status == "completed").count(),
current_node: summarize_node(dag.get_node(current_node_id)),
next_nodes: next.into_iter().map(|n| summarize_node(n)).collect(),
}
}
```
#### 决策 2:注入位置
注入 system prompt 的最前方(优先级高于工具定义):
```
[system]
[工作流] 审批模块改造 (ID: wf-abc)
✅ 1/3 读 config.rs 源码 — 产出: timeout:15 位于行15
🚧 2/3 改 timeout 配置 — 当前步骤
⬜ 3/3 验证配置生效
──────────────────────────────────────
[工具定义] 30 个工具的 JSON schema...
[目标提示] ...
[system prompt 原有内容]
```
**理由**:工作流状态是会话的最上层语境。模型先看到"我们在做什么",再看到"有什么工具可用"。
#### 决策 3:无工作流时的行为
```rust
if let Some(wf_id) = &conv.workflow_id {
if let Ok(dag) = df_workflow::Dag::load(db, wf_id) {
let block = build_active_path(&dag, &conv.current_node_id);
system_prompt = format!("{}\n{}", block.to_system_text(), system_prompt);
}
// Dag::load 失败 → 静默跳过(workflow 可能被删了)
}
// workflow_id == None → 行为完全不变
```
### 推演:T4 在 T-resume 场景中的行为
```
上午:
用户在工作流 wf-abc 中:
N1: 读源码 ✅
N2: 分析依赖 ✅
N3: 改配置 🚧(当前)
最后操作: 改了一半配置,离开
下午回来:
现有: 消息历史被压缩 → 摘要"用户讨论过配置修改"
→ LLM 答:"你讨论了配置,要继续吗?"
→ 用户需要重新说明
T4: system prompt 注入:
[工作流] 审批模块改造
✅ 1/3 读 config.rs — timeout:15
✅ 2/3 分析 db.rs 依赖 — 连接池:10
🚧 3/3 改 timeout 配置 — 已设为30,未验证
→ LLM: "你上午在改 timeout 配置,已经改了还没验证,
要继续验证还是改其他?"
→ 用户直接继续,不需要重新说明
```
**精度依赖**`WorkflowNodeSummary.output_summary` 是关键。如果工作流节点在完成时已有结构化的产出记录(`read_file → 行号:timeout:15`),LLM 就能精确回溯。这需要 df-workflow 的节点在完成时主动记录产出摘要——目前不一定有。
**缺口**:如果工作流节点跑完但没有产出摘要(`output_summary: None`),LLM 只能看到"节点已完成",不知道完成了什么。`output_summary` 需要在 `AiNode::execute` 完成时自动产生:
```rust
// df-nodes/src/ai_node.rs: 执行完成后
if let Some(workflow_node_id) = current_workflow_node {
let summary = extract_key_info(&result); // 复用
workflow::update_node_output(db, workflow_node_id, summary);
}
```
这个联动在 T4 之前需要确保。
### T4 + T2 组合推演
```
T2 提供了 namespace 精确行号回溯
T4 提供了工作流节点结构化映射
组合:
LLM: "config.rs 的 timeout 在哪一行?"
T4 DAG 节点: "N1: 读 config.rs"
T2 namespace: 关联 namespace://read_file/xxx 到 N1 的产出
→ LLM 从 DAG 节点取到 "timeout:15 位于行15"
→ 不需要调用 /read_namespace
→ 精确行号回答,零额外 token
如果 T2 没有、T4 没有:
LLM: 从压缩摘要猜 → 可能错
如果 T2 有、T4 没有:
LLM: 从 DAG 节点取出 "读 config.rs",但需要/read_namespace 拿行号
→ 多一次 tool round
如果 T2 没有、T4 有:
LLM: DAG 节点只有 "读源码 ✅",没有行号细节
→ 重新 read_file
组合后: 最大精度,最小 token 开销。
```
---
## T3 L3 结构化摘要(设计概要)
### 改 compress_via_llm 返回类型
```rust
// current
pub(crate) async fn compress_via_llm(...) -> Result<String, String>;
// new
pub struct CompressedSummary {
/// JSON 卡片序列化字符串
pub json_card: String,
/// 自然语言摘要(向前兼容)
pub nl_summary: String,
}
pub(crate) async fn compress_via_llm(...) -> Result<CompressedSummary, String>;
```
### 改 compress_prompt
```rust
pub(crate) fn compress_prompt(lang: &str) -> &'static str {
match lang {
"en" => "You are a conversation summarizer. Compress the following conversation \
into a structured summary that preserves the essential context for \
continuing the work. \
\n\
**You MUST output TWO parts separated by a delimiter:**\n\
\n\
Part 1 — JSON (between <<<JSON>>> and <<<END_JSON>>>):\n\
{\n\
\"topics\": [\"topic1\", \"topic2\"],\n\
\"decisions\": [{\"what\": \"...\", \"why\": \"...\"}],\n\
\"unresolved\": [\"question1\"],\n\
\"key_files\": [{\"path\": \"...\", \"change\": \"...\"}],\n\
\"token_saved\": <estimated_tokens>\n\
}\n\
\n\
Part 2 — Natural language summary (between <<<NL>>> and <<<END_NL>>>):\n\
A concise paragraph summarizing the conversation.\n\
\n\
Rules:\n\
- JSON must be valid.\n\
- Keep the natural language summary concise; prefer bullet points.\n\
- Preserve file paths, identifiers, and error messages verbatim in both parts.\n\
- Drop small talk; keep only technically load-bearing facts.\n\
- Do NOT invent facts.\n",
_ => {
// 中文版本同上,翻译为中文
}
}
}
```
### insert_at 处的消费
```rust
// context_lifecycle.rs: 压缩成功后
match compress_outcome {
Ok(Some(summary)) => {
// ★ 新格式:JSON + NL 双格式嵌入 system 消息
let system_text = format!(
"{}<<<STRUCTURED_SUMMARY>>>\n{}\n<<<END_STRUCTURED_SUMMARY>>>\n{}",
SUMMARY_MARKER,
summary.json_card,
summary.nl_summary,
);
conv.messages.insert_at(0, ChatMessage::system(&system_text));
// ...
}
Ok(None) => { /* noop */ }
Err(e) => {
// LLM 压缩失败 → 关键词摘要兜底(同前)
// 关键词摘要是纯文本格式,不需要 JSON 结构
// → insert_at(0, system(keyword_fallback)) ← 与旧行为一致
}
}
```
### 退化检测适配
当前 `clean_summary` 函数只处理纯文本退化。新格式引入后,退化检测需增加 JSON 解析验证:
```rust
fn clean_summary(raw: &str) -> Result<CompressedSummary, String> {
// 提取 JSON 段
let json = extract_between(raw, "<<<JSON>>>", "<<<END_JSON>>>")?;
let nl = extract_between(raw, "<<<NL>>>", "<<<END_NL>>>")?;
// 验证 JSON 合法性
let card: SummaryCard = serde_json::from_str(&json)
.map_err(|e| format!("JSON 解析失败: {}", e))?;
// 退化检测 NL 段
if is_degenerated_repetition(&nl) {
return Err("NL 段退化重复".to_string());
}
Ok(CompressedSummary {
json_card: json,
nl_summary: nl,
})
}
```
### 推演:T3 在检索场景中的行为
```
无 T3(当前):
压缩摘要: "用户讨论了审批配置,把 timeout 改成了 30"
LLM 读取后知道"改过",但具体决策原因不清晰
→ 需要推测或 ask user
有 T3:
system 消息中嵌入:
<<<STRUCTURED_SUMMARY>>>
{"decisions": [{"what": "timeout: 15→30", "why": "用户反馈15min太短"}]}
<<<END_STRUCTURED_SUMMARY>>>
自然语言: 用户讨论审批配置,决定超时改为 30 分钟
LLM 直接从 JSON 读取决策原因:
→ "15→30,原因是用户反馈太短"
→ 精确回答 ✓
→ 不需要 ask user
```
**收益量化**:每次压缩后,JSON 卡片占 ~300 tokens,自然语言占 ~200 tokens。比纯自然语言的 ~400 tokens 多了 ~100 tokens。但 JSON 的结构化让 LLM 的检索精度从「需要推理」变为「可以直接读」,减少了后续追问的轮次。
---
## T1 动态压缩阈值(设计概要)
代码改动精确到行:
```rust
// src-tauri/src/commands/ai/agentic/mod.rs: 调用处(line ~1117
if maybe_auto_compress(
&session_arc,
&conv_id,
&app_handle,
&provider,
&provider_config,
&llm_concurrency,
iteration,
sys_tokens, // ★ 新增参数
).await {
```
```rust
// src-tauri/src/commands/ai/agentic/context_lifecycle.rs: 函数签名(line ~72
pub(super) async fn maybe_auto_compress(
session_arc: &Arc<Mutex<AiSession>>,
conv_id: &str,
app_handle: &AppHandle,
provider: &Box<dyn LlmProvider>,
provider_config: &AiProviderRecord,
llm_concurrency: &LlmConcurrency,
iteration: usize,
sys_tokens: u32, // ★ 新增
) -> bool {
```
```rust
// 同上,line ~88-92 触发条件
let budget = mgr.budget_limit();
let available = budget.saturating_sub(sys_tokens); // ★ 新增
let should = protect_start > 0
&& (available as u64) * 6 / 10 < history_tokens as u64 // ★ 改 budget→available
&& mgr.has_compressible_messages(protect_start);
```
---
## 实施顺序验证(依赖关系)
```mermaid
graph LR
T1["T1: 动态阈值<br>~4 行改动"] --> T2["T2: 命名空间<br>~300 行"]
T1 -.->|可选前置| T3["T3: 结构化摘要<br>~100 行"]
T2 --> T4["T4: 工作流DAG<br>~500 行"]
T3 --> T5["T5: WorkingContext<br>~500 行"]
T2 -.-> T4
T3 -.-> T4
style T1 fill:#c8e6c9
style T2 fill:#c8e6c9
style T3 fill:#fff9c4
style T4 fill:#fff9c4
style T5 fill:#ffccbc
```
**并发策略(AI coding 多 agent 并行)**
| 并行流 | 任务 | 前置 | 可独立启动? |
|--------|------|------|------------|
| 流 A | T1 + T2 | T1 无前置,T2 无前置 | ✅ 立即,T1 与 T2 可并行编码 |
| 流 B | T3 | 无(与 T2 独立) | ✅ 可同时启动 |
| 流 C | T4 | 依赖 T2 的 namespace 接口签名(非实现) | ⚠️ 接口定义后即可启动 |
| 流 D | T5 | 依赖 T3 的 CompressedSummary 结构 | ⚠️ T3 验收后 |