P1:窄投影恢复失败不再掐掉整轮 resume,plan_gdd blocker 改类型化判别
A. reconcile_plan_gdd_approval_projections_at 挂在 resume 的第一行却用 `?` 强传播,
一次 Fast GDD 投影失败会掐掉全项目所有 Agent 的恢复;而它本身正是 receipt 投影失败
后的重试入口,掐掉它等于连兜底一起废掉。改成把 fail-closed 收敛到策划根 Supervisor
这个 run,其余 Agent 照常恢复;无法归属时才退回全局上抛。
B. main_loop 原来用 contains("approvalPending=awaiting_decision") 区分 plan_gdd
blocker 的子状态,而三个 blocked 里只有一个含这个子串——尚未提交(下一步是
agent.delegate)和 receipt 锚点收尾都会掉进 else 被打成 needs-reconciliation,把最
正常的推进态当成故障停掉。改由构造方给出 PlanGddCompletionBlockerKind,消费方穷尽
match,判不出 kind 时保持 fail-closed。这两个推进态不再产生等待态,和
runtime.plan_update 一样让本轮循环继续。
映射抽成纯函数并建起 main_loop 至今没有的 mod tests:四个 kind × 两条映射全覆盖。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,86 @@
|
||||
use super::*;
|
||||
|
||||
struct PlanGddBlockerRuntimeProjection {
|
||||
phase: &'static str,
|
||||
current_action: &'static str,
|
||||
waiting_on: &'static str,
|
||||
next_step: &'static str,
|
||||
}
|
||||
|
||||
/// `runtime.plan_gdd` blocker 的类型化子状态 → 运行时投影。
|
||||
///
|
||||
/// 抽成纯函数是为了让这条分支可测:主循环整个文件此前没有 `mod tests`,而原来的
|
||||
/// 判别是对 detail 做 `contains("approvalPending=awaiting_decision")`——三个 blocked
|
||||
/// 子状态里只有一个含这个子串,另外两个会掉进 else 被打成 needs-reconciliation,
|
||||
/// 把最正常的早期推进态和收尾态当成故障停掉。判不出 kind 时保持 fail-closed。
|
||||
fn plan_gdd_blocker_runtime_projection(
|
||||
kind: Option<PlanGddCompletionBlockerKind>,
|
||||
) -> PlanGddBlockerRuntimeProjection {
|
||||
match kind {
|
||||
Some(PlanGddCompletionBlockerKind::SubmissionNotStarted) => {
|
||||
PlanGddBlockerRuntimeProjection {
|
||||
phase: "planning",
|
||||
current_action: "推进本根 Run 的 Fast GDD 提交",
|
||||
waiting_on: "策划子 Agent 完成本根 Run 的 plan.submit_gdd",
|
||||
next_step: "调用 agent.delegate 派出策划子 Agent;上一根 Run 遗留的 game/fast_gdd.md 或 Acceptance Graph 不能代替本根提交",
|
||||
}
|
||||
}
|
||||
Some(PlanGddCompletionBlockerKind::AwaitingApprovalDecision) => {
|
||||
PlanGddBlockerRuntimeProjection {
|
||||
phase: "waiting-for-user-input",
|
||||
current_action: "等待 Fast GDD 审批决定",
|
||||
waiting_on: "用户在审批卡选择批准、修改或退回",
|
||||
next_step: "等待 decide_game_creator_plan_gdd;不得重新提交同一 GDD 或自行创建审批 pending",
|
||||
}
|
||||
}
|
||||
Some(PlanGddCompletionBlockerKind::ReceiptAnchorCleanupPending) => {
|
||||
PlanGddBlockerRuntimeProjection {
|
||||
phase: "planning",
|
||||
current_action: "等待 Fast GDD 审批投影收尾",
|
||||
waiting_on: "原 plan.submit_gdd 恢复锚点由审批投影清理",
|
||||
next_step: "等待审批投影恢复清理锚点后继续;不得重新提交同一 GDD 或自行创建审批 pending",
|
||||
}
|
||||
}
|
||||
Some(PlanGddCompletionBlockerKind::NeedsReconciliation) | None => {
|
||||
PlanGddBlockerRuntimeProjection {
|
||||
phase: "needs-reconciliation",
|
||||
current_action: "Fast GDD 审批投影需要人工核对",
|
||||
waiting_on:
|
||||
"planning pending、receipt、原提交锚点、terminal observation、audit 与 session 的精确身份",
|
||||
next_step: "先恢复或核对现有 durable 事实,不能请求新 Provider 计划",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 只有「等用户决定」和「要人工核对」才终结本轮后台任务;尚未提交与锚点收尾都是
|
||||
/// 继续推进态,和 `runtime.plan_update` 一样不产生等待态,让本轮循环继续。
|
||||
fn plan_gdd_blocker_waiting_kind(
|
||||
kind: Option<PlanGddCompletionBlockerKind>,
|
||||
) -> Option<(
|
||||
&'static str,
|
||||
&'static str,
|
||||
&'static str,
|
||||
AgentBackgroundTaskOutcome,
|
||||
)> {
|
||||
match kind {
|
||||
Some(PlanGddCompletionBlockerKind::AwaitingApprovalDecision) => Some((
|
||||
"agent.runtime.plan.gdd.waiting",
|
||||
"waiting-for-user-input",
|
||||
"Fast GDD 审批等待状态持久化失败",
|
||||
AgentBackgroundTaskOutcome::WaitingForUserInput,
|
||||
)),
|
||||
Some(PlanGddCompletionBlockerKind::NeedsReconciliation) | None => Some((
|
||||
"agent.runtime.plan.gdd.reconciliation",
|
||||
"needs-reconciliation",
|
||||
"Fast GDD 审批投影需要人工核对",
|
||||
AgentBackgroundTaskOutcome::NeedsReconciliation,
|
||||
)),
|
||||
Some(PlanGddCompletionBlockerKind::SubmissionNotStarted)
|
||||
| Some(PlanGddCompletionBlockerKind::ReceiptAnchorCleanupPending) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn autonomous_registered_derived_visuals_need_repair_at(root: &Path) -> bool {
|
||||
let Ok(manifest) = read_manifest_for_project(root) else {
|
||||
return false;
|
||||
@@ -1889,6 +1970,9 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
}
|
||||
|
||||
if plan.actions.is_empty() {
|
||||
// blocked 的 plan_gdd blocker 有三种截然不同的继续推进态,phase 与
|
||||
// next_step 必须按类型化子状态选,不能回去猜 detail 字符串。
|
||||
let mut plan_gdd_blocker_kind: Option<PlanGddCompletionBlockerKind> = None;
|
||||
let completion_blocker = structured_plan_completion_blocker(&runtime)
|
||||
.or_else(|| {
|
||||
provider_action_batch_completion_blocker_at_locked(
|
||||
@@ -1898,7 +1982,11 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
plan_gdd_completion_blocker_at_locked(&root, &agent_id, &runtime.run_id)
|
||||
plan_gdd_typed_completion_blocker_at_locked(&root, &agent_id, &runtime.run_id)
|
||||
.map(|blocker| {
|
||||
plan_gdd_blocker_kind = Some(blocker.kind);
|
||||
blocker.observation
|
||||
})
|
||||
})
|
||||
.or_else(|| game_creator_agent_goal_completion_blocker_at_locked(&root, &runtime))
|
||||
.or_else(|| goal_contract_acceptance_completion_blocker_at_locked(&root, &runtime))
|
||||
@@ -1948,22 +2036,11 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
runtime.next_step = "先恢复原批次,不能请求新计划或提交最终回复".to_string();
|
||||
} else if blocker.tool == "runtime.plan_gdd" {
|
||||
runtime.status = "running".to_string();
|
||||
if blocker.status == "blocked"
|
||||
&& blocker.detail.as_deref().is_some_and(|detail| {
|
||||
detail.contains("approvalPending=awaiting_decision")
|
||||
})
|
||||
{
|
||||
runtime.phase = "waiting-for-user-input".to_string();
|
||||
runtime.current_action = "等待 Fast GDD 审批决定".to_string();
|
||||
runtime.waiting_on = "用户在审批卡选择批准、修改或退回".to_string();
|
||||
runtime.next_step = "等待 decide_game_creator_plan_gdd;不得重新提交同一 GDD 或自行创建审批 pending".to_string();
|
||||
} else {
|
||||
runtime.phase = "needs-reconciliation".to_string();
|
||||
runtime.current_action = "Fast GDD 审批投影需要人工核对".to_string();
|
||||
runtime.waiting_on = "planning pending、receipt、原提交锚点、terminal observation、audit 与 session 的精确身份".to_string();
|
||||
runtime.next_step =
|
||||
"先恢复或核对现有 durable 事实,不能请求新 Provider 计划".to_string();
|
||||
}
|
||||
let projection = plan_gdd_blocker_runtime_projection(plan_gdd_blocker_kind);
|
||||
runtime.phase = projection.phase.to_string();
|
||||
runtime.current_action = projection.current_action.to_string();
|
||||
runtime.waiting_on = projection.waiting_on.to_string();
|
||||
runtime.next_step = projection.next_step.to_string();
|
||||
} else if blocker.tool == "runtime.collaboration_policy" {
|
||||
runtime.status = "running".to_string();
|
||||
runtime.phase = "planning".to_string();
|
||||
@@ -2126,23 +2203,7 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
AgentBackgroundTaskOutcome::WaitingForDelegateReceipts,
|
||||
))
|
||||
} else if observation.tool == "runtime.plan_gdd" {
|
||||
if observation.status == "blocked"
|
||||
&& detail.contains("approvalPending=awaiting_decision")
|
||||
{
|
||||
Some((
|
||||
"agent.runtime.plan.gdd.waiting",
|
||||
"waiting-for-user-input",
|
||||
"Fast GDD 审批等待状态持久化失败",
|
||||
AgentBackgroundTaskOutcome::WaitingForUserInput,
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
"agent.runtime.plan.gdd.reconciliation",
|
||||
"needs-reconciliation",
|
||||
"Fast GDD 审批投影需要人工核对",
|
||||
AgentBackgroundTaskOutcome::NeedsReconciliation,
|
||||
))
|
||||
}
|
||||
plan_gdd_blocker_waiting_kind(plan_gdd_blocker_kind)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -4211,3 +4272,78 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod plan_gdd_blocker_projection_tests {
|
||||
use super::*;
|
||||
|
||||
/// 三个 blocked 子状态必须落到各自的 phase。旧的子串判别只认得
|
||||
/// AwaitingApprovalDecision,另外两个会被打成 needs-reconciliation。
|
||||
#[test]
|
||||
fn each_blocked_kind_projects_its_own_phase() {
|
||||
assert_eq!(
|
||||
plan_gdd_blocker_runtime_projection(Some(
|
||||
PlanGddCompletionBlockerKind::SubmissionNotStarted
|
||||
))
|
||||
.phase,
|
||||
"planning"
|
||||
);
|
||||
assert_eq!(
|
||||
plan_gdd_blocker_runtime_projection(Some(
|
||||
PlanGddCompletionBlockerKind::AwaitingApprovalDecision
|
||||
))
|
||||
.phase,
|
||||
"waiting-for-user-input"
|
||||
);
|
||||
assert_eq!(
|
||||
plan_gdd_blocker_runtime_projection(Some(
|
||||
PlanGddCompletionBlockerKind::ReceiptAnchorCleanupPending
|
||||
))
|
||||
.phase,
|
||||
"planning"
|
||||
);
|
||||
}
|
||||
|
||||
/// 判不出 kind 与显式的人工核对一样,保持 fail-closed。
|
||||
#[test]
|
||||
fn reconciliation_and_unknown_kind_stay_fail_closed() {
|
||||
assert_eq!(
|
||||
plan_gdd_blocker_runtime_projection(Some(
|
||||
PlanGddCompletionBlockerKind::NeedsReconciliation
|
||||
))
|
||||
.phase,
|
||||
"needs-reconciliation"
|
||||
);
|
||||
assert_eq!(
|
||||
plan_gdd_blocker_runtime_projection(None).phase,
|
||||
"needs-reconciliation"
|
||||
);
|
||||
assert!(matches!(
|
||||
plan_gdd_blocker_waiting_kind(None),
|
||||
Some((_, _, _, AgentBackgroundTaskOutcome::NeedsReconciliation))
|
||||
));
|
||||
}
|
||||
|
||||
/// 继续推进态不产生等待态,本轮后台任务不该在这里终结。
|
||||
#[test]
|
||||
fn only_user_decision_and_reconciliation_end_the_background_task() {
|
||||
assert!(matches!(
|
||||
plan_gdd_blocker_waiting_kind(Some(
|
||||
PlanGddCompletionBlockerKind::AwaitingApprovalDecision
|
||||
)),
|
||||
Some((_, "waiting-for-user-input", _, _))
|
||||
));
|
||||
assert!(matches!(
|
||||
plan_gdd_blocker_waiting_kind(Some(PlanGddCompletionBlockerKind::NeedsReconciliation)),
|
||||
Some((_, "needs-reconciliation", _, _))
|
||||
));
|
||||
assert!(plan_gdd_blocker_waiting_kind(Some(
|
||||
PlanGddCompletionBlockerKind::SubmissionNotStarted
|
||||
))
|
||||
.is_none());
|
||||
assert!(plan_gdd_blocker_waiting_kind(Some(
|
||||
PlanGddCompletionBlockerKind::ReceiptAnchorCleanupPending
|
||||
))
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -819,12 +819,79 @@ fn durable_process_session_recovery_exists_at(root: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Fast GDD approval 投影恢复失败时,把 fail-closed 收敛到受影响的那个 run。
|
||||
///
|
||||
/// 返回 `Ok(true)` 表示已经把策划根 Supervisor 标成 needs-reconciliation,调用方可以
|
||||
/// 继续扫描其余 Agent;`Ok(false)` 表示当前没有可归属的 run,无法精确收敛,调用方必须
|
||||
/// 把原错误照旧上抛,保持全局 fail-closed。
|
||||
fn contain_plan_gdd_approval_recovery_failure_at(root: &Path, error: &str) -> Result<bool, String> {
|
||||
let Some(_runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
)?
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
let mut runtime =
|
||||
read_game_creator_agent_runtime_at(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)?.state;
|
||||
if runtime.run_id.trim().is_empty()
|
||||
|| matches!(
|
||||
runtime.phase.as_str(),
|
||||
"completed" | "cancelled" | "needs-reconciliation"
|
||||
)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
let error = sanitize_agent_runtime_text(error, 500);
|
||||
runtime.status = "failed".to_string();
|
||||
runtime.phase = "needs-reconciliation".to_string();
|
||||
runtime.current_action = "Fast GDD 审批投影恢复需要人工核对".to_string();
|
||||
runtime.waiting_on = "开发者核对 planning pending、receipt 与原提交锚点".to_string();
|
||||
runtime.next_step = "修复审批投影后显式恢复该 run".to_string();
|
||||
runtime.error = Some(error.clone());
|
||||
runtime.updated_at = unix_timestamp();
|
||||
append_game_creator_agent_runtime_task(root, &runtime)?;
|
||||
refresh_game_creator_agent_runtime_task_queue(root, &mut runtime)?;
|
||||
write_game_creator_agent_runtime_state(root, &runtime)?;
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
root,
|
||||
&runtime,
|
||||
"plan.gdd.approval_recovery.needs_reconciliation",
|
||||
"failed",
|
||||
"needs-reconciliation",
|
||||
"Fast GDD 审批投影恢复已停止自动重放,等待开发者核对。",
|
||||
Some(&error),
|
||||
);
|
||||
let _ = append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.plan.gdd.approval_recovery.needs_reconciliation",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"error": error,
|
||||
}),
|
||||
);
|
||||
emit_game_creator_agent_runtime_update(root, &runtime.agent_id);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at(
|
||||
root: &Path,
|
||||
) -> Result<Vec<AgentRuntimeResult>, String> {
|
||||
validate_project_root(root)?;
|
||||
reconcile_plan_gdd_approval_projections_at(root)
|
||||
.map_err(|error| format!("恢复 GDD approval 投影失败:{error}"))?;
|
||||
// 这是一条只覆盖策划根 Supervisor 的窄投影恢复,却挂在整轮 resume 的最前面。
|
||||
// 原来用 `?` 强传播:一次 Fast GDD 投影失败会掐掉全项目所有 Agent 的恢复——而它
|
||||
// 本身正是 receipt 投影失败后的重试入口,掐掉它等于连兜底一起废掉。改成把
|
||||
// fail-closed 精确收敛到受影响的那个 run,其余 Agent 照常恢复;实在无法归属时
|
||||
// 才退回原来的全局上抛。
|
||||
if let Err(error) = reconcile_plan_gdd_approval_projections_at(root) {
|
||||
let error = format!("恢复 GDD approval 投影失败:{error}");
|
||||
if !contain_plan_gdd_approval_recovery_failure_at(root, &error)? {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
if external_agent_runner_owns_background_execution() {
|
||||
resume_external_agent_runner(root)?;
|
||||
return read_game_creator_agent_runtimes_at(root);
|
||||
@@ -1842,4 +1909,62 @@ mod plan_gdd_approval_wait_recovery_tests {
|
||||
"status 写成 waiting-for-user-input 才是恢复扫描让路的外部输入等待"
|
||||
);
|
||||
}
|
||||
|
||||
/// Fast GDD approval 投影恢复失败,不能再掐掉整轮 resume。
|
||||
///
|
||||
/// 它挂在 `resume_game_creator_agent_background_tasks_unredacted_at` 的第一行,
|
||||
/// 原来用 `?` 强传播;而这条 reconcile 本身正是 receipt 投影失败后的重试入口,
|
||||
/// 掐掉它等于连兜底一起废掉。现在 fail-closed 精确收敛到策划根 Supervisor 这个 run。
|
||||
#[test]
|
||||
fn failed_plan_gdd_approval_recovery_contains_itself_instead_of_aborting_the_scan() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("plan-gdd-approval-recovery-contain-");
|
||||
let root = temporary.path();
|
||||
init_local_game_project_at(root, "plan-gdd-approval-contain", "审批投影恢复失败收敛")
|
||||
.expect("init project");
|
||||
let runtime = start_game_creator_agent_runtime_task_at(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"推进立项策划",
|
||||
"plan-gdd-approval-contain-run",
|
||||
"agent-ready-task-scheduler",
|
||||
"准备推进立项策划",
|
||||
vec!["推进立项策划".to_string()],
|
||||
)
|
||||
.expect("start plan-root supervisor runtime");
|
||||
|
||||
let planning_directory = root.join(".agent/planning");
|
||||
fs::create_dir_all(&planning_directory).expect("create planning directory");
|
||||
fs::write(planning_directory.join("gdd.v1.json"), b"{")
|
||||
.expect("corrupt the GDD lineage so approval recovery fails");
|
||||
assert!(reconcile_plan_gdd_approval_projections_at(root).is_err());
|
||||
|
||||
let resumed = resume_game_creator_agent_background_tasks_at(root)
|
||||
.expect("窄投影恢复失败不能把整轮 resume 掐掉");
|
||||
assert!(resumed
|
||||
.iter()
|
||||
.all(|result| result.state.phase != "completed"));
|
||||
|
||||
let contained = read_game_creator_agent_runtime_at(root, &runtime.agent_id)
|
||||
.expect("read contained supervisor runtime");
|
||||
assert_eq!(contained.state.phase, "needs-reconciliation");
|
||||
assert!(
|
||||
contained
|
||||
.state
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|error| error.contains("恢复 GDD approval 投影失败")),
|
||||
"unexpected error: {:?}",
|
||||
contained.state.error
|
||||
);
|
||||
let audit_count = read_agent_db_records_bounded(root, 1024 * 1024)
|
||||
.expect("read approval recovery audit")
|
||||
.0
|
||||
.iter()
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(|value| value.as_str())
|
||||
== Some("agent.runtime.plan.gdd.approval_recovery.needs_reconciliation")
|
||||
})
|
||||
.count();
|
||||
assert_eq!(audit_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
+76
-12
@@ -1288,16 +1288,69 @@ pub(crate) fn decide_plan_gdd_at(
|
||||
|
||||
const PLAN_GDD_COMPLETION_BLOCKER_TOOL: &str = "runtime.plan_gdd";
|
||||
|
||||
/// 为什么 `runtime.plan_gdd` 的 blocker 需要一个类型化的子状态:
|
||||
///
|
||||
/// `status == "needs-reconciliation"` 已经把「要人工核对」和「继续推进」分开了,
|
||||
/// 但 `"blocked"` 一侧有三种彼此完全不同的继续推进态,驱动侧必须区分才能选对
|
||||
/// phase 与 next_step。原来的做法是在 `main_loop` 里对 detail 做
|
||||
/// `contains("approvalPending=awaiting_decision")`:三个 blocked 里只有一个含这个
|
||||
/// 子串,另外两个会掉进 else 被打成 needs-reconciliation——把最正常的早期推进态和
|
||||
/// 收尾态当成故障停掉。子状态判别必须由构造方给出,不能让消费方去猜字符串。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum PlanGddCompletionBlockerKind {
|
||||
/// 根 Run 还没提交 Fast GDD,下一步是 `agent.delegate`。
|
||||
SubmissionNotStarted,
|
||||
/// Fast GDD 已提交,等待用户在审批卡上做决定。
|
||||
AwaitingApprovalDecision,
|
||||
/// receipt 已落盘,原 `plan.submit_gdd` 恢复锚点还没清理完。
|
||||
ReceiptAnchorCleanupPending,
|
||||
/// 其余一律要人工核对。
|
||||
NeedsReconciliation,
|
||||
}
|
||||
|
||||
pub(crate) struct PlanGddCompletionBlocker {
|
||||
pub(crate) observation: AgentRuntimeToolObservation,
|
||||
pub(crate) kind: PlanGddCompletionBlockerKind,
|
||||
}
|
||||
|
||||
fn plan_gdd_completion_blocker(
|
||||
status: &str,
|
||||
summary: impl Into<String>,
|
||||
detail: impl Into<String>,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
AgentRuntimeToolObservation {
|
||||
tool: PLAN_GDD_COMPLETION_BLOCKER_TOOL.to_string(),
|
||||
status: status.to_string(),
|
||||
summary: summary.into(),
|
||||
detail: Some(detail.into()),
|
||||
) -> PlanGddCompletionBlocker {
|
||||
debug_assert_ne!(
|
||||
status, "blocked",
|
||||
"blocked 子状态必须走 plan_gdd_blocked_completion_blocker 显式给出 kind"
|
||||
);
|
||||
PlanGddCompletionBlocker {
|
||||
observation: AgentRuntimeToolObservation {
|
||||
tool: PLAN_GDD_COMPLETION_BLOCKER_TOOL.to_string(),
|
||||
status: status.to_string(),
|
||||
summary: summary.into(),
|
||||
detail: Some(detail.into()),
|
||||
},
|
||||
kind: PlanGddCompletionBlockerKind::NeedsReconciliation,
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_gdd_blocked_completion_blocker(
|
||||
kind: PlanGddCompletionBlockerKind,
|
||||
summary: impl Into<String>,
|
||||
detail: impl Into<String>,
|
||||
) -> PlanGddCompletionBlocker {
|
||||
debug_assert_ne!(
|
||||
kind,
|
||||
PlanGddCompletionBlockerKind::NeedsReconciliation,
|
||||
"blocked blocker 不能声明成人工核对"
|
||||
);
|
||||
PlanGddCompletionBlocker {
|
||||
observation: AgentRuntimeToolObservation {
|
||||
tool: PLAN_GDD_COMPLETION_BLOCKER_TOOL.to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: summary.into(),
|
||||
detail: Some(detail.into()),
|
||||
},
|
||||
kind,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1585,11 +1638,22 @@ fn plan_root_completion_identity_at(
|
||||
/// committed GDD therefore remains blocked until the pending/receipt,
|
||||
/// generic child anchors, terminal observation, decision audit and planning
|
||||
/// session all form one exact durable state.
|
||||
/// 只要 blocker 本身,不关心 blocked 的子状态。驱动侧(`main_loop`)必须改用
|
||||
/// `plan_gdd_typed_completion_blocker_at_locked`,否则又要去猜 detail 字符串。
|
||||
pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Option<AgentRuntimeToolObservation> {
|
||||
plan_gdd_typed_completion_blocker_at_locked(root, agent_id, run_id)
|
||||
.map(|blocker| blocker.observation)
|
||||
}
|
||||
|
||||
pub(crate) fn plan_gdd_typed_completion_blocker_at_locked(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
) -> Option<PlanGddCompletionBlocker> {
|
||||
let is_plan_root = match plan_root_completion_identity_at(root, agent_id, run_id) {
|
||||
Ok(value) => value,
|
||||
Err(error) => {
|
||||
@@ -1629,8 +1693,8 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
) {
|
||||
Ok(Some(_)) => Some(plan_gdd_completion_blocker(
|
||||
"blocked",
|
||||
Ok(Some(_)) => Some(plan_gdd_blocked_completion_blocker(
|
||||
PlanGddCompletionBlockerKind::SubmissionNotStarted,
|
||||
"当前立项策划根 Run 尚未提交 Fast GDD,不能收束任务",
|
||||
format!(
|
||||
"rootRunId={run_id} · nextRequiredAction=agent.delegate;上一根 Run 遗留的 game/fast_gdd.md 或 Acceptance Graph 不能代替本根提交"
|
||||
@@ -1818,8 +1882,8 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
),
|
||||
));
|
||||
}
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"blocked",
|
||||
return Some(plan_gdd_blocked_completion_blocker(
|
||||
PlanGddCompletionBlockerKind::AwaitingApprovalDecision,
|
||||
"Fast GDD 已提交,等待用户审批决定,不能收束任务",
|
||||
format!(
|
||||
"gddVersion={} · approvalPending=awaiting_decision · childPending={} · childBatch={};pending 只能由验收取证通过后的 acceptance-gate caller 创建",
|
||||
@@ -1852,8 +1916,8 @@ pub(crate) fn plan_gdd_completion_blocker_at_locked(
|
||||
}
|
||||
}
|
||||
if pending_anchor != PlanGddAnchorState::Absent || batch_anchor != PlanGddAnchorState::Absent {
|
||||
return Some(plan_gdd_completion_blocker(
|
||||
"blocked",
|
||||
return Some(plan_gdd_blocked_completion_blocker(
|
||||
PlanGddCompletionBlockerKind::ReceiptAnchorCleanupPending,
|
||||
"Fast GDD receipt 已提交,但原 plan.submit_gdd 恢复锚点尚未清理",
|
||||
format!(
|
||||
"gddVersion={} · terminalObservation={} · childPending={} · childBatch={}",
|
||||
|
||||
@@ -3267,6 +3267,17 @@ mod tests {
|
||||
.expect("awaiting approval must block completion");
|
||||
assert_eq!(blocker.status, "blocked");
|
||||
assert!(blocker.summary.contains("等待用户审批"));
|
||||
// 驱动侧按类型化子状态选 phase:这一条必须是「等用户决定」,不能被当成人工核对。
|
||||
assert_eq!(
|
||||
plan_gdd_typed_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("awaiting approval blocker")
|
||||
.kind,
|
||||
PlanGddCompletionBlockerKind::AwaitingApprovalDecision
|
||||
);
|
||||
|
||||
let decision_input = approval_input(
|
||||
&gdd,
|
||||
@@ -3377,6 +3388,18 @@ mod tests {
|
||||
.expect("current root without submission must block");
|
||||
assert_eq!(blocker.status, "blocked");
|
||||
assert!(blocker.summary.contains("尚未提交 Fast GDD"));
|
||||
// 这是策划最正常的早期推进态,下一步是 agent.delegate。它的 detail 里没有
|
||||
// approvalPending 字段,旧的子串判别会把它打成 needs-reconciliation 停掉整条 run。
|
||||
assert_eq!(
|
||||
plan_gdd_typed_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("submission-not-started blocker")
|
||||
.kind,
|
||||
PlanGddCompletionBlockerKind::SubmissionNotStarted
|
||||
);
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
@@ -3421,6 +3444,16 @@ mod tests {
|
||||
"unexpected blocker: {}",
|
||||
blocker.summary
|
||||
);
|
||||
assert_eq!(
|
||||
plan_gdd_typed_completion_blocker_at_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&context.root_run_id,
|
||||
)
|
||||
.expect("mismatched pending blocker")
|
||||
.kind,
|
||||
PlanGddCompletionBlockerKind::NeedsReconciliation
|
||||
);
|
||||
cleanup_fixture(root);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user