取证覆盖校验先折叠重复分页,并让 Fast GDD 尽量一页读完
run 21 的 GDD 是 133 行,而 file.read 的默认 maxLines 是 120,于是被迫分成 1-120 / 121-133 两页。模型把第二页读了两遍,把三个 actionId 全提交上去, validate_fast_gdd_file_read_coverage 排序后严格走 `start_line == next_line`: 1-120 → next_line = 121 ✓ 121-133 → next_line = 134 ✓ 121-133 → 121 != 134 ✗「必须从第 1 行无缺口、无重叠地覆盖到文件末尾」 连吃三次 agent.acceptance_update 拒绝,第四次才猜对该交哪两个 id。 同一页读两遍不削弱证据,不该判成重叠。覆盖检查前先按完全相同的 (startLine, endLine, contentSha256) 折叠——内容 SHA 在上一步已经要求全体一致, 折叠掉的确实是同一页的重复回执。真缺口与部分重叠照旧拒绝,有用例钉住。 顺带把取证指令和 playbook 第 5 步改成「每次都传 maxLines=240(上限),尽量一页 读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段」,让典型 GDD 根本 不进分页逻辑。 另记一笔本次排查暴露的可观测性缺口(未修):agent.acceptance_update 的拒绝理由只 回灌给模型,durable receipt 是 detailUnavailable=true / safeDetail=null,事后在 日志和 agent.db 里都查不到,这类拒绝无法复盘。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
2. 冻结后立即用一次 `agent.delegate` 把任务委派给 `project-planning`,`expectedArtifacts` 写 `game/fast_gdd.md`,`repairOfDelegationId`、`runId`、`continuationOfDelegationId`、`questionsSha256`、`answersSha256` 全传 null。已有委派尚未收束时不要重复委派。
|
||||
3. 等待子 Agent 期间不得调用 `respond_to_user`。Runtime 会通过 delegate 完成屏障保持同一父 run,回执到达后再继续。
|
||||
4. 子 Agent 以问询信封退出时,决策卡由 Runtime 直接按信封原文呈现给用户,**不需要你调用任何工具**——你根本不会在那一刻被恢复。用户答完之后你才会拿到答案,届时为该原 delivery 创建且仅创建一次 continuation 委派,`continuationOfDelegationId` 与 `repairOfDelegationId` 都指向该原 delivery,并提交 observation 给出的 `questionsSha256`、`answersSha256`。
|
||||
5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行分页读到 `game/fast_gdd.md` 末尾取证。每次 `file.read` 的 observation 末尾都带着自己的 `sourceActionId`,直接把这些 id 收集起来,用一次 `agent.acceptance_update` 完整写进 evidence 即可——不要为了取 id 再去查动作历史。取证完成前审批卡不会出现。
|
||||
5. 回执 contractStatus=evidence-ready 且 GDD 已提交时,用 `file.read` 从第 1 行读到 `game/fast_gdd.md` 末尾取证,每次都传 `maxLines: 240`(上限),尽量一页读完;确实需要第二页时从上一页的下一行开始,不要重复读同一段。每次 `file.read` 的 observation 末尾都带着自己的 `sourceActionId`,直接把这些 id 收集起来,用一次 `agent.acceptance_update` 完整写进 evidence 即可——不要为了取 id 再去查动作历史。取证完成前审批卡不会出现。
|
||||
6. 用户在审批卡上选择修改或退回时,先用 `agent.run_status` 按原 delegationId 取回已认领的权威委派合同,把其中的 acceptanceCriteria 与 expectedArtifacts 逐字照抄进返工委派(`runId` 传 null),再把用户原话完整附在 task 里;同一原委派只能返工一次。用户通过后只做一句简短收尾。
|
||||
|
||||
【转达的规则】
|
||||
|
||||
+1
-1
@@ -392,7 +392,7 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
|
||||
)
|
||||
} else if plan_root {
|
||||
format!(
|
||||
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId——每次 file.read 的 observation 末尾都带着自己的 sourceActionId,直接用它,不要为取得 actionId 额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
|
||||
"{prompt}\n\n立项策划 Goal Contract 已冻结且不可重写。先完成 project-planning 委派;收到其 Fast GDD 提交后,由当前 Supervisor 根 Run 从 startLine=1 开始分页、无缺口且无重叠地读取 game/fast_gdd.md 直到 EOF,每次都用 maxLines=240(上限)以尽量一页读完、少分页,各页必须来自同一内容 SHA-256,并在 agent.acceptance_update.evidence 中提交全部分页 file.read 的 actionId——每次 file.read 的 observation 末尾都带着自己的 sourceActionId,直接用它,不要为取得 actionId 额外查询动作历史。取证未通过时不得展示或创建审批卡,只能针对原策划 delivery 返工。"
|
||||
)
|
||||
} else if root_control_authority {
|
||||
format!(
|
||||
|
||||
@@ -349,6 +349,17 @@ fn validate_fast_gdd_file_read_coverage(
|
||||
return Err("Fast GDD file.read evidence 不属于同一 Markdown 内容".to_string());
|
||||
}
|
||||
coverage.sort_by_key(|item| (item.start_line, item.end_line));
|
||||
// 同一页被读了两遍不削弱证据,却会让下面那趟严格的 `start_line == next_line`
|
||||
// walk 判成重叠:实测一次 133 行的 GDD 分成 1-120 / 121-133 两页,模型把第二页
|
||||
// 读了两次、把三个 actionId 全提交上来,于是连吃三次「必须从第 1 行无缺口、无
|
||||
// 重叠地覆盖到文件末尾」,试到第四次才猜对该交哪两个。这里先按完全相同的
|
||||
// (startLine, endLine, contentSha256) 折叠——内容 SHA 在上面已经要求全体一致,
|
||||
// 所以折叠掉的确实是同一页的重复回执,不是两段不同内容。
|
||||
coverage.dedup_by(|left, right| {
|
||||
left.start_line == right.start_line
|
||||
&& left.end_line == right.end_line
|
||||
&& left.content_sha256 == right.content_sha256
|
||||
});
|
||||
if total_lines == 0 {
|
||||
if coverage.len() != 1 || coverage[0].start_line != 0 || coverage[0].end_line != 0 {
|
||||
return Err("空 Fast GDD 的 file.read evidence 覆盖不唯一".to_string());
|
||||
@@ -1218,6 +1229,52 @@ pub(crate) fn render_game_creator_agent_runtime_acceptance_graph_for_prompt_at(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn page(start: usize, end: usize, total: usize) -> AcceptanceEvidenceReceipt {
|
||||
AcceptanceEvidenceReceipt {
|
||||
tool: "file.read".to_string(),
|
||||
project_revision_before: 1,
|
||||
project_revision_after: 1,
|
||||
input_summary: Some(format!(
|
||||
"path=game/fast_gdd.md · startLine={start} · maxLines=240"
|
||||
)),
|
||||
safe_detail: Some(
|
||||
serde_json::json!({
|
||||
"path": "game/fast_gdd.md",
|
||||
"contentSha256": "b".repeat(64),
|
||||
"lines": format!("{start}-{end} of {total}"),
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// 同一页被读了两遍不削弱证据。实测 133 行的 GDD 被默认 maxLines=120 逼成两页,
|
||||
/// 模型把第二页读了两次并提交了三个 actionId,于是连吃三次「无缺口、无重叠」。
|
||||
#[test]
|
||||
fn repeating_an_identical_page_is_not_an_overlap() {
|
||||
let first = page(1, 120, 133);
|
||||
let second = page(121, 133, 133);
|
||||
let duplicate = page(121, 133, 133);
|
||||
let receipts = vec![&first, &second, &duplicate];
|
||||
let (sha, total) =
|
||||
validate_fast_gdd_file_read_coverage(&receipts).expect("duplicate page is tolerated");
|
||||
assert_eq!(sha, "b".repeat(64));
|
||||
assert_eq!(total, 133);
|
||||
}
|
||||
|
||||
/// 真的缺口与真的重叠照旧拒绝——去重只折叠完全相同的分页。
|
||||
#[test]
|
||||
fn real_gaps_and_partial_overlaps_are_still_rejected() {
|
||||
let first = page(1, 120, 133);
|
||||
let gapped = page(122, 133, 133);
|
||||
let receipts = vec![&first, &gapped];
|
||||
validate_fast_gdd_file_read_coverage(&receipts).expect_err("a real gap must fail");
|
||||
|
||||
let overlapping = page(100, 133, 133);
|
||||
let receipts = vec![&first, &overlapping];
|
||||
validate_fast_gdd_file_read_coverage(&receipts).expect_err("a partial overlap must fail");
|
||||
}
|
||||
|
||||
fn root_fixture() -> (tempfile::TempDir, PathBuf, AgentRuntimeRunProfileBinding) {
|
||||
let temporary = crate::tests::canonical_test_tempdir("acceptance-graph-");
|
||||
let root = temporary.path().join("project");
|
||||
|
||||
Reference in New Issue
Block a user