优化: 子代理截断信号与缓存失效
- max_tokens 截断标记穿透到完成卡 - 缓存写后失效补测试锁定 - 派发参数卡头信息补全
This commit is contained in:
@@ -257,6 +257,11 @@ pub(crate) fn readonly_cache_args_key(tool_name: &str, args: &serde_json::Value)
|
||||
///
|
||||
/// 与 find_cached_high_risk_result 区别:后者治"超时→重试→重新审批"循环( High risk),
|
||||
/// 本函数治"LLM 失忆→同参重调"死循环( Low risk 只读),问题形态相似但触发原因不同。
|
||||
///
|
||||
/// **写后失效**(治「只读结果缓存无写失效」缺陷):会话内 write_file/run_command 等
|
||||
/// 非只读工具成功执行后,历史只读结果已可能过期(读的是改前世界)。本函数扫描时按
|
||||
/// per_conv.readonly_cache_floor 过滤——floor 之后的索引才算可信,之前的旧调用全部
|
||||
/// 跳过(过界即脏),写后 LLM 重读同参工具必然 miss 缓存走真执行拿到新内容。
|
||||
pub(crate) async fn find_cached_readonly_result(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
@@ -281,7 +286,11 @@ pub(crate) async fn find_cached_readonly_result(
|
||||
let cached: Option<(String, String, u32)> = (async {
|
||||
let session = session_arc.lock().await;
|
||||
let Some(conv) = session.conv_read(conv_id) else { return None };
|
||||
let msgs: Vec<&ChatMessage> = conv.messages.iter().collect();
|
||||
// 写后失效:floor 之前的只读结果一律不可信(写过世界状态),只扫 [floor, len)。
|
||||
// floor > len 防御(pop_last_assistant_round/clear 缩短历史后索引漂移),钳到 len
|
||||
// 等于扫描区间空 → 无缓存可命中,走真执行(保守方向安全)。
|
||||
let floor = conv.readonly_cache_floor.min(conv.messages.len());
|
||||
let msgs: Vec<&ChatMessage> = conv.messages.iter().skip(floor).collect();
|
||||
|
||||
// 1) 反向扫描 assistant tool_calls,找最近一条同名同参调用 → 拿到旧 tool_call_id,
|
||||
// 同时累计同名同参调用总数(hit_count 供「第 N 次命中缓存」威慑)。
|
||||
@@ -342,6 +351,29 @@ pub(crate) async fn find_cached_readonly_result(
|
||||
/// 只读缓存白名单已收敛为注册元数据(readonly 维度,DRY 债 #5):
|
||||
/// find_cached_readonly_result 改查 df_ai::ai_tools::is_readonly,本文件不再持名单。
|
||||
|
||||
/// 写后失效:非只读工具成功回填后推进会话级缓存失效下界(readonly_cache_floor)。
|
||||
///
|
||||
/// 调用点 = 全部写工具真执行成功的回填处(Low 风险并行回填 / trust 放行回填 /
|
||||
/// 边界放行回填 / ai_approve 审批后执行回填)。非只读工具(写文件/跑命令/改 DB 等)
|
||||
/// 成功执行即视作世界状态变更,历史里的只读结果从此不可信——floor 推进到写结果
|
||||
/// push 之后,find_cached_readonly_result 扫描时跳过 floor 之前的全部旧调用(过界即脏)。
|
||||
///
|
||||
/// 单调不减(max):多写工具同轮/跨轮先后执行,floor 取最大(最后写),前段只读
|
||||
/// 结果全失效——保守方向安全(失效仅多一次真执行,误命中才喂脏数据)。
|
||||
///
|
||||
/// 须在持 session 锁的短临界区内调用(与写结果 push 同锁,防 floor 与 messages 漂移)。
|
||||
/// is_readonly 未注册的工具按非只读处理(保守推进 floor,防未知新写工具漏失效)。
|
||||
pub(crate) fn mark_readonly_cache_stale(conv: &mut super::super::PerConvState, tool_name: &str) {
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
if df_ai::ai_tools::is_readonly(tool_name) {
|
||||
return; // 只读工具不改世界状态,缓存照常有效
|
||||
}
|
||||
let new_floor = conv.messages.len();
|
||||
if conv.readonly_cache_floor < new_floor {
|
||||
conv.readonly_cache_floor = new_floor;
|
||||
}
|
||||
}
|
||||
|
||||
/// 递归对 JSON 对象的键做字典序排序(就地),数组成员也递归排序。
|
||||
fn sort_object_keys(v: &mut serde_json::Value) {
|
||||
match v {
|
||||
@@ -545,7 +577,53 @@ mod tests {
|
||||
assert_ne!(a, b, "read_file 不同 offset 应不同 key(分段读正常)");
|
||||
}
|
||||
|
||||
// ── AC-2 ③ 列举命令检测测试 ──
|
||||
// ── 写后失效测试 ──
|
||||
|
||||
/// mark_readonly_cache_stale:非只读工具推进 floor,只读工具不动。
|
||||
#[test]
|
||||
fn mark_stale_advances_floor_for_write_tools_only() {
|
||||
crate::commands::ai::tool_registry::ensure_tool_meta_seeded();
|
||||
let mut conv = super::super::super::PerConvState::new();
|
||||
// 三条历史消息(user + assistant + tool)
|
||||
conv.messages.push(df_ai::provider::ChatMessage::user("hi"));
|
||||
conv.messages.push(df_ai::provider::ChatMessage::assistant("ack"));
|
||||
conv.messages.push(df_ai::provider::ChatMessage::tool_result("t1", "r"));
|
||||
assert_eq!(conv.readonly_cache_floor, 0, "初值 0");
|
||||
|
||||
// read_file 只读:不推进
|
||||
mark_readonly_cache_stale(&mut conv, "read_file");
|
||||
assert_eq!(conv.readonly_cache_floor, 0, "只读工具不推进 floor");
|
||||
|
||||
// write_file 写:推进到当前 len(3)
|
||||
mark_readonly_cache_stale(&mut conv, "write_file");
|
||||
assert_eq!(conv.readonly_cache_floor, 3, "写工具推进 floor 到 len");
|
||||
|
||||
// 再只读:不动(单调不减由 max 保证)
|
||||
mark_readonly_cache_stale(&mut conv, "grep");
|
||||
assert_eq!(conv.readonly_cache_floor, 3);
|
||||
|
||||
// run_command(写语义):取 max 不回退
|
||||
conv.messages.push(df_ai::provider::ChatMessage::user("again"));
|
||||
mark_readonly_cache_stale(&mut conv, "run_command");
|
||||
assert_eq!(conv.readonly_cache_floor, 4, "第二个写工具推进到新 len");
|
||||
}
|
||||
|
||||
/// floor 过滤逻辑纯函数级锁定:floor=min 钳制语义(消息缩短后 floor 不越界)。
|
||||
#[test]
|
||||
fn readonly_cache_floor_clamped_by_len() {
|
||||
let mut conv = super::super::super::PerConvState::new();
|
||||
conv.messages.push(df_ai::provider::ChatMessage::user("a"));
|
||||
conv.messages.push(df_ai::provider::ChatMessage::user("b"));
|
||||
mark_readonly_cache_stale(&mut conv, "write_file");
|
||||
assert_eq!(conv.readonly_cache_floor, 2);
|
||||
// 模拟 pop_last_assistant_round/clear 缩短历史后:len=0,floor 语义钳到 0
|
||||
let floor = conv.readonly_cache_floor.min(conv.messages.len());
|
||||
conv.messages.clear();
|
||||
let floor2 = conv.readonly_cache_floor.min(conv.messages.len());
|
||||
assert_eq!(floor, 2, "len 未缩短时 floor 原样");
|
||||
assert_eq!(floor2, 0, "len 缩短后 floor 钳到 0(空扫描区间,安全)");
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn listing_command_detects_powershell_and_unix() {
|
||||
|
||||
Reference in New Issue
Block a user