修复: 消息三元组完整性(sanitize 补头自愈 orphan tool_result,治 AI 看不到工具结果致静默停)
This commit is contained in:
@@ -162,6 +162,63 @@ fn replace_tool_result_updates_tokens() {
|
||||
assert!(after > before);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 三元组完整性 P1(根治):裁剪/压缩必须保证三元组原子性,
|
||||
// 不出现 tool_result 残留但 tool_call 头被裁的 orphan(根因:AI Coding 静默停)。
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn trim_never_produces_orphan_tool_result_without_head() {
|
||||
// 三元组完整性 P1 回归:超预算裁剪后,发送视图中**任何 tool_result 的 tool_call_id 都必须
|
||||
// 有对应 assistant tool_call 头**(不残留 orphan result)。裁剪按 build_eviction_units 的
|
||||
// 三元组原子单元(Head+Tail+Standalone Assistant 同进同出),trim_end 始终落在单元边界,
|
||||
// 永不切断三元组。
|
||||
//
|
||||
// 布局:5 旧 user/assistant 文本(淘汰区) + 完整三元组(tc_mid) + 6 新(保护区) = 14 条
|
||||
// 强制小预算使裁剪切到三元组边界(整体保留或整体丢弃,不在中间切)。
|
||||
let mut mgr = ContextManager::new(cfg(120));
|
||||
for i in 0..5 {
|
||||
mgr.push(ChatMessage::user(&format!("旧消息 {} 用于撑爆预算的较长文本", i)));
|
||||
}
|
||||
// 完整三元组(裁剪边界附近)
|
||||
mgr.push(ChatMessage::assistant_with_tools(
|
||||
"调中间工具",
|
||||
vec![ToolCall::new("tc_mid", "read_file", "{}")],
|
||||
));
|
||||
mgr.push(ChatMessage::tool_result("tc_mid", "中间工具结果"));
|
||||
mgr.push(ChatMessage::assistant("中间完成"));
|
||||
// 保护区(最近 PROTECT_COUNT=6 条)
|
||||
for i in 0..6 {
|
||||
mgr.push(ChatMessage::user(&format!("新消息 {} 保护区", i)));
|
||||
}
|
||||
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(trimmed, "应触发裁剪");
|
||||
|
||||
// 收集发送视图中所有 assistant 头的 tool_call.id
|
||||
use std::collections::HashSet;
|
||||
let head_ids: HashSet<String> = msgs
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Assistant))
|
||||
.filter_map(|m| m.tool_calls.as_ref())
|
||||
.flatten()
|
||||
.map(|c| c.id.clone())
|
||||
.collect();
|
||||
|
||||
// 每个 tool_result 的 tool_call_id 都必须在 head_ids 内(不残留 orphan result)
|
||||
let orphans: Vec<&str> = msgs
|
||||
.iter()
|
||||
.filter(|m| matches!(m.role, MessageRole::Tool))
|
||||
.filter_map(|m| m.tool_call_id.as_deref())
|
||||
.filter(|id| !head_ids.contains(*id))
|
||||
.collect();
|
||||
assert!(
|
||||
orphans.is_empty(),
|
||||
"裁剪后不应有 orphan tool_result(无对应头), 实际 orphans={:?}, heads={:?}",
|
||||
orphans, head_ids
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_rebuilds_token_cache() {
|
||||
let mut mgr = ContextManager::new(cfg(100_000));
|
||||
|
||||
Reference in New Issue
Block a user