修复游戏智能体计划拒绝后的重复规划
Project CI / Repository checks (push) Successful in 1m5s
Project CI / Frontend tests (push) Successful in 3m22s
Project CI / Native shell tests (push) Failing after 12m42s
Project CI / Backend tests (push) Successful in 3m55s

在 runtime.plan_update 被拒绝后按角色收窄下一轮工具目录

恢复 Supervisor 首次项目修改的活性门禁

补充请求目录与活性门禁回归测试

同步更新游戏创作智能体实施规则
This commit is contained in:
2026-08-12 17:00:50 +08:00
parent 8abeb333bb
commit effdaefc63
3 changed files with 158 additions and 1 deletions
@@ -702,6 +702,21 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
.actions
.iter()
.any(|action| action.tool.trim() == "agent.route_manifest");
// A Supervisor without a mutation must still hit the same first-mutation
// liveness gate as every other autonomous Agent. Previously the presence
// of the Supervisor role alone could bypass this check.
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& loop_index > AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT
&& verification_gate.mutation_revision.is_none()
&& !has_mutation
&& !has_code_asset_route
&& plan.response.trim().is_empty()
&& !has_specialist_delegation
{
return Err(format!(
"{AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX}Supervisor 尚未提交首次项目 mutation 或有效协作动作,禁止继续只规划、读取、验证或空转"
));
}
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
if let Some(failed_playtest_revision) = verification_gate.failed_playtest_revision {
if project_revision < failed_playtest_revision {
@@ -1911,4 +1926,39 @@ mod tests {
.is_err());
}
}
#[test]
fn autonomous_supervisor_without_playtest_still_hits_pre_mutation_liveness_gate() {
let verification_gate = AgentRuntimeVerificationGate {
schema_version: "test".to_string(),
project_id: "test".to_string(),
agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
run_id: "supervisor-pre-mutation".to_string(),
requires_verification: false,
mutation_revision: None,
verified_revision: None,
last_mutation_tool: None,
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
failed_playtest_revision: None,
updated_at: 0,
};
let plan = AgentRuntimeToolPlan {
thinking_summary: "继续只读规划".to_string(),
..AgentRuntimeToolPlan::default()
};
let error = validate_agent_runtime_autonomous_plan_liveness(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1,
0,
&verification_gate,
&[],
&plan,
false,
false,
)
.expect_err("Supervisor without a playtest must not bypass pre-mutation liveness");
assert!(error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX));
}
}
@@ -316,6 +316,43 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request(
tool.name != goal_contract_function && tool.name != acceptance_update_function
});
}
// A rejected structured-plan update is a request-scoped liveness signal.
// The next Provider turn must perform the concrete mutation (or deliver a
// read-only result) instead of entering another planning loop.
let latest_plan_rejection = observations.iter().rposition(|observation| {
observation.tool == "runtime.plan_update" && observation.status == "rejected"
});
let plan_rejection_needs_repair = latest_plan_rejection.is_some_and(|index| {
!observations[index + 1..]
.iter()
.any(is_agent_runtime_project_mutation_observation)
});
if autonomous_game_build && plan_rejection_needs_repair {
let supervisor_orchestrator_repair = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
let policy = resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)?.policy;
let state = read_supervisor_collaboration_state_at(root, agent_id, run_id)?;
policy.orchestrator_only_after_delegation && state.has_collaboration()
} else {
false
};
let repair_tools: &[&str] = if supervisor_orchestrator_repair {
&["agent.delegate", "agent.run_status"]
} else {
&["file.write", "file.patch", "file.delete", "project.patchset", "project.restore", "canvas.asset_generate"]
};
let mut allowed_function_names = BTreeSet::from([AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string()]);
for tool in repair_tools {
if let Some(name) = native_runtime_function_name(tool) {
allowed_function_names.insert(name);
}
}
request.function_tools.retain(|tool| allowed_function_names.contains(&tool.name));
request.messages.push(LlmMessage::user(if supervisor_orchestrator_repair {
"上一轮 runtime.plan_update 被拒绝。本轮 Supervisor 已进入协作编排模式,只能调用 agent.run_status 或 agent.delegate 继续收束,或在证据足够时 respond_to_user;禁止再次规划、读取、搜索、验证或直接修改项目。"
} else {
"上一轮 runtime.plan_update 被拒绝。本轮必须立即提交当前 in_progress 步骤对应的实际项目 mutation,或在只读合同已满足时 respond_to_user;禁止再次规划、读取、搜索、验证、委派或普通文本解释。"
}));
}
if autonomous_game_build && !editor_api_key_is_configured() {
let canvas_function = native_runtime_function_name("canvas.asset_generate")
.ok_or_else(|| "无法生成画布素材工具函数名".to_string())?;
@@ -530,11 +567,14 @@ mod tests {
game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at,
provider_command_exec_contract, provider_command_start_contract,
required_runtime_prompt_section, resolve_agent_conversation_session_id_at,
start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolPlan,
start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolObservation,
AgentRuntimeToolPlan,
GameCreatorMcpCatalog, GameCreatorMcpCatalogTool,
AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL,
AGENT_RUNTIME_RESPOND_FUNCTION_NAME,
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, RUNTIME_PROMPT_SUPERVISOR_CHAT_COMPOSITION,
};
@@ -556,6 +596,71 @@ mod tests {
.collect()
}
#[test]
fn rejected_plan_update_forces_request_scoped_mutation_catalog() {
let directory = crate::tests::canonical_test_tempdir("provider-plan-rejection-repair-");
let root = directory.path().join("project");
init_local_game_project_at(&root, "plan-rejection-repair", "修复现有游戏")
.expect("project init");
let binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"plan-rejection-repair-root",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind root");
let state = start_game_creator_agent_runtime_task_at(
&root,
&binding.agent_id,
"修复现有游戏",
&binding.run_id,
&binding.source,
"执行当前计划中的项目修改",
vec!["立即修改 game/index.html".to_string()],
)
.expect("start task");
let catalog = GameCreatorMcpCatalog {
fingerprint: String::new(),
servers: Vec::new(),
tools: Vec::new(),
};
let rejected = AgentRuntimeToolObservation {
tool: "runtime.plan_update".to_string(),
status: "rejected".to_string(),
summary: "结构化计划更新被 Runtime 拒绝".to_string(),
detail: Some("计划状态回退".to_string()),
};
let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request(
&root,
&state.agent_id,
&state.session_id,
&state.run_id,
&state.current_task,
&[rejected],
1,
&catalog,
)
.expect("build request");
let names = request
.function_tools
.iter()
.map(|tool| tool.name.as_str())
.collect::<std::collections::BTreeSet<_>>();
assert!(names.contains(
crate::agent_native_tools::native_runtime_function_name("file.patch")
.expect("file.patch function")
.as_str()
));
assert!(names.contains(AGENT_RUNTIME_RESPOND_FUNCTION_NAME));
assert!(!names.contains(AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME));
assert!(request
.messages
.iter()
.any(|message| message.content.contains("runtime.plan_update 被拒绝")));
}
fn build_request_system_prompt_for_root_source(
agent_id: &str,
root_source: &str,
@@ -1051,6 +1051,8 @@ game-project/
## 2026-08-11 通用 Goal Contract 与动态 Acceptance Graph
- 2026-08-12 计划拒绝恢复:结构化 `runtime.plan_update` 被 Runtime 拒绝后,下一轮 Provider 请求按请求级目录收窄到实际项目 mutation 与 `respond_to_user`(已进入协作编排的 Supervisor 保留 `agent.delegate / agent.run_status`),并明确禁止再次规划、读取、搜索或验证;后续已有真实 mutation observation 后解除临时目录,不改变持久 executable policy。
- Goal Contract 绑定 project、可信根 Run Profile、source task SHA-256 和不可变 fingerprint;同一根 Run 只允许幂等重放完全相同的合同,语义变化必须进入新根 Run。已有合同的根 Supervisor 收到 steer 时,Runtime 必须按旧 rootRunId 串行化转换并在持锁后重验 Session 当前权威 Run,再取消并确认旧 rootRunId 的静态、ready、isolated 整棵树已进入终态或 `needs-reconciliation`;旧树未停稳时拒绝启动 replacement,停稳后才在同一 Session、source 和 Run Profile 创建唯一的新根 Run,不能把新增要求塞进旧合同继续完成。合同摘要作为 `decision` 投影到共享黑板,JSON sidecar 才是权威源;黑板冲突条目和专家事实仍追加保留。
- 所有静态 delegate、ready child 和 isolated child 都只读继承根合同与当前 Acceptance Graphisolated child 还必须实际收到自己的 `acceptanceCriteria / expectedArtifacts / writeScopes`。继承上下文不扩大工具、目录、写入权限或 expectedArtifacts。专业 Agent 只能报告局部结果、证据和剩余风险,不能修改根合同、根验收图或宣布用户总目标完成。
- Acceptance Graph 节点由 Supervisor 针对当前任务动态生成,不来自玩法模板。节点记录 required/optional、依赖、状态、证据引用与摘要;只有同一可信根 Supervisor 能调用 `agent.acceptance_update`,且该动作必须独占一轮。failed 与 not-observed 节点形成下一轮定向返工集合,未提交的 passed 节点保持不变。