修复 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();