优化: 狗粮短板收口与patch_file增强
- patch_file old_text 精确未中时空白归一化回退(行首缩进归一+多空白折叠,唯一命中按整行区间替换,warning 注明) - patch_file anchor end 唯一性校验:多处命中报错并列前两处位置,治实测×2 锚点错位;三模式同传按优先级仲裁(anchor>old_text>replace_lines),warning 注明 - tool_result 压缩 memoization:PerConvState 加 tool_compress_cache(内容哈希→压缩结果),主 loop+子代理双调用点接线,治 369 组同内容反复重压 - 标题采样改两段式:user 首条必进锚定主题+其余按序补足 6 条,治 assistant 首句被当主题 - prompt 注入文件读取纪律段(父会话+子代理同源):read_file 分页/search 直读,禁 run_command 切片读文件;read_file 工具描述与 truncated 提示补大 limit 建议 - 顺带 DRY#1:agentic/mod.rs 三处裸 emit 换正典双写(心跳/破坏性回环熔断/stall 熔断),miniapp 跨端盲区补齐
This commit is contained in:
@@ -187,10 +187,12 @@ pub(crate) fn truncate_output(s: &str, max: usize) -> (String, bool) {
|
||||
// ============================================================
|
||||
// patch_file 三模式共用底层
|
||||
//
|
||||
// 三模式互斥(old_text 精确匹配 / replace_lines 行号区间 / anchor 锚点),
|
||||
// 三模式定位(old_text 精确匹配 / replace_lines 行号区间 / anchor 锚点),
|
||||
// 后两者均归约为「按行号区间 [start, end](1-based 含首尾)替换为 new_text」,
|
||||
// 由 apply_line_range 统一执行 splice。锚点经 resolve_anchor_to_lines 转行号区间。
|
||||
// 提取为纯函数便于直接单测(不依赖 async/fs/锁),handler 闭包内复用。
|
||||
// 由 apply_line_range 统一执行 splice。锚点经 resolve_anchor_to_lines 转行号区间
|
||||
// (end 子串须唯一命中,多处命中报错)。多模式同时传入由 handler 层按优先级仲裁
|
||||
// (见 tools/file.rs arbitrate_patch_mode)。提取为纯函数便于直接单测(不依赖 async/fs/锁),
|
||||
// handler 闭包内复用。
|
||||
// ============================================================
|
||||
|
||||
/// 按行号区间替换:将 content 第 [start, end] 行(1-based, 含首尾)替换为 new_text。
|
||||
@@ -242,11 +244,14 @@ pub(crate) fn apply_line_range(content: &str, start: usize, end: usize, new_text
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 锚点解析为行号区间:在 content 中找含 `start` 子串的首行 → 其后第一个含 `end` 子串的行。
|
||||
/// 锚点解析为行号区间:在 content 中找含 `start` 子串的首行 → 其后含 `end` 子串的行。
|
||||
///
|
||||
/// - 子串匹配(大小写敏感,与 search_files 一致)。
|
||||
/// - start/end 任一找不到返 Err;start 行号 > end 行号返 Err(即 end 必须在 start 之后或同行)。
|
||||
/// - 同一行同时命中 start/end:start==end,区间退化为单行替换。
|
||||
/// - end 唯一性校验(治实测 ×2 多处命中替换错位):end 子串在 start 行(含)之后多处命中
|
||||
/// 时返 Err,列出前两处命中行上下文,提示缩小 end 锚点或改用 old_text 模式。
|
||||
/// 旧实现静默取第一个命中行,LLM 意图往往是更靠后的行,错位后返工。
|
||||
/// - 返回 (start_line, end_line)(1-based 含首尾)。
|
||||
pub(crate) fn resolve_anchor_to_lines(content: &str, start: &str, end: &str) -> anyhow::Result<(usize, usize)> {
|
||||
if start.is_empty() {
|
||||
@@ -265,22 +270,44 @@ pub(crate) fn resolve_anchor_to_lines(content: &str, start: &str, end: &str) ->
|
||||
let start_line = start_line
|
||||
.ok_or_else(|| anyhow::anyhow!("anchor.start 子串「{}」未在文件中找到", start))?;
|
||||
|
||||
// end 在 start_line 之后(含 start_line 同行)第一个命中的行
|
||||
let mut end_line: Option<usize> = None;
|
||||
for (i, line) in content.lines().enumerate() {
|
||||
let lineno = i + 1;
|
||||
if lineno < start_line {
|
||||
continue;
|
||||
// end 在 start_line 之后(含 start_line 同行)收集全部命中行,唯一才放行
|
||||
let all_lines: Vec<&str> = content.lines().collect();
|
||||
let end_hits: Vec<usize> = all_lines
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, line)| i + 1 >= start_line && line.contains(end))
|
||||
.map(|(i, _)| i + 1)
|
||||
.collect();
|
||||
let end_line = match end_hits.len() {
|
||||
0 => anyhow::bail!(
|
||||
"anchor.end 子串「{}」未在 start 行(第 {} 行)之后找到",
|
||||
end, start_line
|
||||
),
|
||||
1 => end_hits[0],
|
||||
_ => {
|
||||
// 列出前两处命中行原文(截断防超长行刷屏),供 LLM 对照改锚
|
||||
let ctx = end_hits
|
||||
.iter()
|
||||
.take(2)
|
||||
.map(|&ln| {
|
||||
let raw = all_lines[ln - 1];
|
||||
let shown: String = if raw.chars().count() > 80 {
|
||||
raw.chars().take(80).collect::<String>() + "…"
|
||||
} else {
|
||||
raw.to_string()
|
||||
};
|
||||
format!("第 {} 行: {}", ln, shown)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
anyhow::bail!(
|
||||
"anchor.end 子串「{}」在 start 行(第 {} 行)之后命中 {} 处,定位歧义。前两处:\n{}\n请缩小 end 锚点(加长子串使其唯一)或改用 old_text 模式",
|
||||
end, start_line, end_hits.len(), ctx
|
||||
)
|
||||
}
|
||||
if line.contains(end) {
|
||||
end_line = Some(lineno);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let end_line = end_line
|
||||
.ok_or_else(|| anyhow::anyhow!("anchor.end 子串「{}」未在 start 行(第 {} 行)之后找到", end, start_line))?;
|
||||
};
|
||||
|
||||
// start_line <= end_line 已由搜索顺序保证,此处断言兜底
|
||||
// start_line <= end_line 已由命中过滤条件保证,此处断言兜底
|
||||
debug_assert!(start_line <= end_line, "anchor 解析顺序异常: start={} end={}", start_line, end_line);
|
||||
Ok((start_line, end_line))
|
||||
}
|
||||
@@ -1995,12 +2022,12 @@ mod tests {
|
||||
assert_eq!((s, e), (1, 4));
|
||||
}
|
||||
|
||||
/// resolve_anchor_to_lines: 子串匹配会在 start 之后命中第一个含该子串的行
|
||||
/// (含行内出现,如 println!("{}") 内的 })——锁定此行为语义,避免后续误改成「整行相等」
|
||||
/// resolve_anchor_to_lines: 子串匹配含行内出现(如 println!("{}") 内的 })——
|
||||
/// end 唯一命中时放行,锁定「子串包含」语义(非「整行相等」)
|
||||
#[test]
|
||||
fn test_resolve_anchor_substring_matches_inside_line() {
|
||||
// end 锚 "}" 在 start 行之后第一个命中 = 第 3 行 println!("{}") 的 },而非第 4 行的 }
|
||||
let content = "fn foo() {\n let x = 1;\n println!(\"{}\");\n}\n";
|
||||
// end 锚 "}" 唯一命中第 3 行 println!("{}") 的 }(第 4 行换成无 } 的行)
|
||||
let content = "fn foo() {\n let x = 1;\n println!(\"{}\");\nEND\n";
|
||||
let (s, e) = resolve_anchor_to_lines(content, "fn foo", "}").unwrap();
|
||||
assert_eq!((s, e), (1, 3));
|
||||
}
|
||||
@@ -2014,13 +2041,20 @@ mod tests {
|
||||
assert_eq!((s, e), (1, 1));
|
||||
}
|
||||
|
||||
/// resolve_anchor_to_lines: end 在 start 之后第一个命中行(跳过中间同子串)
|
||||
/// resolve_anchor_to_lines: end 子串多处命中 → Err 列前两处命中行 + 修复提示
|
||||
/// (契约变更:旧实现静默取第一个命中行,实测 ×2 LLM 短 end 锚点多处命中替换错位,
|
||||
/// 根修改为唯一性校验——多处命中报错,提示缩小 end 锚点或改用 old_text)
|
||||
#[test]
|
||||
fn test_resolve_anchor_picks_first_end_after_start() {
|
||||
// 第 2、4 行都含 "}",应取 start(第1行) 之后第一个 "}" → 第 2 行
|
||||
fn test_resolve_anchor_end_multi_hit_rejected() {
|
||||
// 第 2、4 行都含 "}",多处命中 → Err
|
||||
let content = "START\n}\nx\n}";
|
||||
let (s, e) = resolve_anchor_to_lines(content, "START", "}").unwrap();
|
||||
assert_eq!((s, e), (1, 2));
|
||||
let err = resolve_anchor_to_lines(content, "START", "}").unwrap_err();
|
||||
let msg = format!("{}", err);
|
||||
assert!(msg.contains("命中 2 处"), "应报多处命中数,实际: {msg}");
|
||||
assert!(msg.contains("第 2 行"), "应列第一处命中行,实际: {msg}");
|
||||
assert!(msg.contains("第 4 行"), "应列第二处命中行,实际: {msg}");
|
||||
assert!(msg.contains("缩小 end 锚点"), "应含缩小锚点提示,实际: {msg}");
|
||||
assert!(msg.contains("old_text"), "应含改用 old_text 建议,实际: {msg}");
|
||||
}
|
||||
|
||||
/// resolve_anchor_to_lines: 大小写敏感(与 search_files 一致)
|
||||
|
||||
Reference in New Issue
Block a user