修复 GDD 修订后的审批取证链路
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / Backend tests (pull_request) Failing after 12s
Project CI / Frontend tests (pull_request) Successful in 3m11s
Project CI / Native shell tests (pull_request) Successful in 12m30s

新增 plan Supervisor 的 AwaitingAcceptanceEvidence 阶段与无副作用 durable 状态判定

在证据不足时收窄工具面,阻止重复 agent.delegate 并保留 file.read、acceptance_update、run_status

补充一条阶段工具面回归及项目排障记录
This commit is contained in:
2026-08-26 13:18:12 +00:00
parent 0e84ea1ed9
commit 41228b0cda
6 changed files with 140 additions and 5 deletions
@@ -158,6 +158,9 @@ pub(crate) enum PlanRootSupervisorStage {
GoalContract,
/// 合同已冻结但本根 run 还没有任何委派:唯一能推进的动作是派出策划子 Agent。
Delegate,
/// 最新 GDD 已提交但尚未完成当前根 Run 的 Acceptance Graph 取证:只能读取
/// `game/fast_gdd.md`、更新验收图或重放状态,不能抢先创建重复策划 delivery。
AwaitingAcceptanceEvidence,
/// 已有委派:取证、返工与审批相关工具全部开放。
Delegated,
}
@@ -168,6 +171,9 @@ pub(crate) fn agent_runtime_plan_root_supervisor_tools_for_stage(
match stage {
PlanRootSupervisorStage::GoalContract => &["agent.goal_contract"],
PlanRootSupervisorStage::Delegate => &["agent.delegate"],
PlanRootSupervisorStage::AwaitingAcceptanceEvidence => {
&["file.read", "agent.acceptance_update", "agent.run_status"]
}
// 合同已冻结且不可重写,再广告 agent.goal_contract 只会诱导一次必被拒的调用。
PlanRootSupervisorStage::Delegated => &[
"file.read",
@@ -194,6 +200,7 @@ mod plan_root_stage_tests {
let union = [
PlanRootSupervisorStage::GoalContract,
PlanRootSupervisorStage::Delegate,
PlanRootSupervisorStage::AwaitingAcceptanceEvidence,
PlanRootSupervisorStage::Delegated,
]
.into_iter()
@@ -234,11 +241,13 @@ pub(crate) fn plan_root_supervisor_stage_at(
let delegated = list_static_delegate_deliveries_at(root)?
.into_iter()
.any(|delivery| delivery.parent_agent_id == agent_id && delivery.parent_run_id == run_id);
Ok(if delegated {
PlanRootSupervisorStage::Delegated
} else {
PlanRootSupervisorStage::Delegate
})
if !delegated {
return Ok(PlanRootSupervisorStage::Delegate);
}
if plan_root_supervisor_acceptance_evidence_required_at(root, agent_id, run_id)? {
return Ok(PlanRootSupervisorStage::AwaitingAcceptanceEvidence);
}
Ok(PlanRootSupervisorStage::Delegated)
}
pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> {
@@ -338,6 +338,93 @@ fn latest_plan_gdd_for_root<'a>(gdds: &'a [PlanGddV1], root_run_id: &str) -> Opt
})
}
/// Return whether the plan-root Supervisor must collect the current GDD
/// acceptance evidence before it can dispatch another planning child.
///
/// This is deliberately a read-only projection of the existing acceptance
/// gate. It does not create approval pending or mutate any planning sidecar;
/// the actual pending projection remains owned by
/// `ensure_plan_gdd_approval_pending_after_acceptance_locked` after a successful
/// `agent.acceptance_update`.
pub(crate) fn plan_root_supervisor_acceptance_evidence_required_at(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<bool, String> {
if !crate::config::game_creator_planning_capability_enabled()? {
return Ok(false);
}
if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || run_id.trim().is_empty() {
return Ok(false);
}
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.supervisor-stage",
)
.map_err(|error| format!("取得 plan Supervisor 阶段判定项目锁失败:{error}"))?;
plan_root_supervisor_acceptance_evidence_required_locked(root, run_id.trim())
}
fn plan_root_supervisor_acceptance_evidence_required_locked(
root: &Path,
run_id: &str,
) -> Result<bool, String> {
let gdds = read_plan_gdd_chain_locked(root).map_err(|error| error.to_string())?;
let Some(gdd) = latest_plan_gdd_for_root(&gdds, run_id) else {
return Ok(false);
};
let Some(global_latest) = gdds.last() else {
return Ok(false);
};
if global_latest.gdd_id != gdd.gdd_id
|| global_latest.version != gdd.version
|| global_latest.fingerprint != gdd.fingerprint
{
return Ok(false);
}
validate_plan_gdd(gdd).map_err(|error| error.to_string())?;
let approvals = read_plan_gdd_approvals_locked(root).map_err(|error| error.to_string())?;
validate_plan_gdd_approvals_against_gdds(&gdds, &approvals)
.map_err(|error| error.to_string())?;
if read_plan_gdd_approval_for_version_locked(root, gdd.version)
.map_err(|error| error.to_string())?
.is_some()
{
return Ok(false);
}
if let Some(pending) =
read_plan_gdd_approval_pending_locked(root).map_err(|error| error.to_string())?
{
if !pending_matches_gdd(&pending, gdd) {
return Err("plan Supervisor 阶段判定发现 approval pending identity 冲突".to_string());
}
return Ok(false);
}
let Some(session) =
read_plan_session_with_recovery_locked(root).map_err(|error| error.to_string())?
else {
return Ok(false);
};
if !plan_gdd_session_matches_submission(&session, gdd) {
return Ok(false);
}
let Some(delivery) = read_static_delegate_delivery_at(root, &gdd.delegation_id)? else {
return Ok(false);
};
if delivery.status != StaticDelegateDeliveryStatus::ClaimedByParent
|| delivery.terminal_status.as_deref() != Some("completed")
{
return Ok(false);
}
Ok(matches!(
plan_fast_gdd_acceptance_status_at_locked(root, gdd)?,
PlanFastGddAcceptanceStatus::NeedsEvidence
))
}
pub(crate) fn ensure_plan_gdd_approval_pending_after_acceptance_locked(
root: &Path,
agent_id: &str,
@@ -4721,6 +4721,29 @@ mod tests {
cleanup_fixture(root);
}
#[test]
fn m1c2a_unapproved_gdd_requires_acceptance_evidence_before_delegate() {
let (root, _gdd, root_runtime) = acceptance_gate_fixture(true);
assert_eq!(
plan_root_supervisor_stage_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
&root_runtime.run_id,
)
.expect("classify plan root stage"),
PlanRootSupervisorStage::AwaitingAcceptanceEvidence
);
assert_eq!(
agent_runtime_plan_root_supervisor_tools_for_stage(
PlanRootSupervisorStage::AwaitingAcceptanceEvidence
),
&["file.read", "agent.acceptance_update", "agent.run_status"]
);
cleanup_fixture(root);
}
#[test]
fn m1c2a_failed_acceptance_requires_claim_before_repair_dispatch() {
let (root, gdd, root_runtime) = acceptance_gate_fixture(false);
@@ -2256,6 +2256,7 @@ mod tests {
for stage in [
PlanRootSupervisorStage::GoalContract,
PlanRootSupervisorStage::Delegate,
PlanRootSupervisorStage::AwaitingAcceptanceEvidence,
PlanRootSupervisorStage::Delegated,
] {
let mut staged = functions.clone();
@@ -2290,6 +2291,7 @@ mod tests {
for stage in [
PlanRootSupervisorStage::GoalContract,
PlanRootSupervisorStage::Delegate,
PlanRootSupervisorStage::AwaitingAcceptanceEvidence,
PlanRootSupervisorStage::Delegated,
] {
let mut staged = functions.clone();
@@ -1,5 +1,12 @@
# 决策记录
## 2026-08-26 Fast GDD 修订后先取证再允许再次委派
- **现象**:GDD v1 经用户选择“修改”后,策划子 Agent 正确提交 v2,但 plan 根 Supervisor 的 `Delegated` 阶段仍同时广告 `agent.delegate` 与审批前置工具;模型可能在 Acceptance Graph 重新取证前重复创建修订 delivery,随后被 `PLAN_PROVIDER_USAGE_DEFERRED` 拦停。
- **决策**plan 根阶段增加轻量的 `AwaitingAcceptanceEvidence` 状态。当前根最新 GDD 无 approval receipt/pending、session `latestSubmittedRef` 精确指向该提交、delivery 已由根认领且 Acceptance Graph 返回 `NeedsEvidence` 时,只广告 `file.read``agent.acceptance_update``agent.run_status`;只有用户真正对最新审批卡选择修改/退回后,才恢复 `agent.delegate`
- **边界**:不放宽 Provider usage 门禁,不重构 delegation/repair lineage,不自动生成证据或审批 pending;审批 pending 仍只由既有 acceptance gate 在 `agent.acceptance_update` 成功后创建。
- **验证**:新增一条阶段工具面回归,并通过 15 条 M1C-2a acceptance gate 定向测试、plan root 原生工具目录测试、`cargo check --all-targets`、格式与 diff 检查。
## 2026-08-24 AGC Direct 媒体能力只通过客户端语义工具开放
- 背景:资源页已经补齐视频、角色动画、音效和背景音乐的 create/derive 能力,但 Direct Codex 只能准备标准美术包,无法查询已登记源资源或表达新增媒体意图。直接开放 Tauri invoke 会把项目路径、revision、operation、幂等键、登录态和事务权力交给模型。
@@ -1,5 +1,12 @@
# 踩坑与排障记录
## 2026-08-26 GDD 新版本提交后不能沿用“已有委派”工具面
- **现象**`plan_root_supervisor_stage_at` 只按是否存在 delivery 判定 `Delegated`。用户修订产生的新 GDD 仍未完成当前根 Run 的 `file.read → agent.acceptance_update` 取证时,模型会看到 `agent.delegate`,可能重复派发同一条策划链。
- **原因**:自然语言 playbook 已规定“证据不足先取证、用户修改后才返工”,但阶段工具白名单没有把这条 durable 状态固化。
- **处理**:阶段判定复用现有 acceptance gate 的 GDD/session/delivery/graph identity 检查,增加无副作用的 `AwaitingAcceptanceEvidence` 阶段;`PLAN_PROVIDER_USAGE_DEFERRED` 保持 fail-closed,不通过放宽 Provider 使用量门禁解决。
- **排查顺序**:先看最新 `gdd.vN.json``session.latestSubmittedRef`、delivery 是否 `ClaimedByParent`,再看 Acceptance Graph 是否 `NeedsEvidence`;若仍可见 `agent.delegate`,优先检查 plan root 阶段快照,而不是修改 acceptance gate 或 Provider 门禁。
## 2026-08-15 把校验往链路前面挪,改的不是严格程度而是作用域
- 现象:CI 全量 5 条失败,看上去毫不相干(两条 Goal 续跑停在 `needs-reconciliation`、一条交接用例断言错误文案、一条恢复用例把不可读 state 的错误抛了出来、一条 Linux-only 用例错误码对不上),实际只有 3 个根因,且三者是**同一个形状**:新增或既有的检查被放在了链路更靠前的位置,于是它的语义作用域被悄悄放大或提前,而不是「变严」。