Merge remote-tracking branch 'origin/master' into codex/fix-main-canvas-reference-images
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
+106
-1
@@ -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 Graph;isolated child 还必须实际收到自己的 `acceptanceCriteria / expectedArtifacts / writeScopes`。继承上下文不扩大工具、目录、写入权限或 expectedArtifacts。专业 Agent 只能报告局部结果、证据和剩余风险,不能修改根合同、根验收图或宣布用户总目标完成。
|
||||
- Acceptance Graph 节点由 Supervisor 针对当前任务动态生成,不来自玩法模板。节点记录 required/optional、依赖、状态、证据引用与摘要;只有同一可信根 Supervisor 能调用 `agent.acceptance_update`,且该动作必须独占一轮。failed 与 not-observed 节点形成下一轮定向返工集合,未提交的 passed 节点保持不变。
|
||||
|
||||
Reference in New Issue
Block a user