修复自主试玩失败状态持久化
持久化当前 revision 的试玩失败标记并驱动总控进入源码修复 在试玩通过后安全清除失败凭证并补充上下文压缩回归测试
This commit is contained in:
@@ -10726,6 +10726,8 @@ pub(crate) struct AgentRuntimeVerificationGate {
|
||||
pub(crate) last_mutation_tool: Option<String>,
|
||||
pub(crate) last_verification_tool: Option<String>,
|
||||
pub(crate) last_verification_status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) failed_playtest_revision: Option<u64>,
|
||||
pub(crate) updated_at: u64,
|
||||
}
|
||||
|
||||
@@ -17396,6 +17398,7 @@ fn default_agent_runtime_verification_gate(
|
||||
last_mutation_tool: None,
|
||||
last_verification_tool: None,
|
||||
last_verification_status: None,
|
||||
failed_playtest_revision: None,
|
||||
updated_at: 0,
|
||||
})
|
||||
}
|
||||
@@ -17460,6 +17463,18 @@ fn validate_agent_runtime_verification_gate(
|
||||
{
|
||||
return Err("Agent Runtime verification gate 的 verifiedRevision 早于修改".to_string());
|
||||
}
|
||||
if gate.failed_playtest_revision.is_some_and(|revision| revision == 0) {
|
||||
return Err("Agent Runtime verification gate 的 failedPlaytestRevision 必须大于 0".to_string());
|
||||
}
|
||||
if gate
|
||||
.failed_playtest_revision
|
||||
.zip(gate.mutation_revision)
|
||||
.is_some_and(|(failed_playtest, mutation)| failed_playtest > mutation)
|
||||
{
|
||||
return Err(
|
||||
"Agent Runtime verification gate 的 failedPlaytestRevision 晚于当前修改".to_string(),
|
||||
);
|
||||
}
|
||||
if gate.last_mutation_tool.as_deref().is_some_and(|tool| {
|
||||
!matches!(
|
||||
tool,
|
||||
@@ -21363,6 +21378,7 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
|
||||
gate.last_mutation_tool = Some(tool.to_string());
|
||||
gate.last_verification_tool = None;
|
||||
gate.last_verification_status = None;
|
||||
gate.failed_playtest_revision = None;
|
||||
gate.updated_at = now;
|
||||
if let Err(error) = write_game_creator_agent_runtime_verification_gate(root, &gate) {
|
||||
revision.revision = next_revision;
|
||||
@@ -21409,9 +21425,13 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
|
||||
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
let revision_unchanged = current_revision.revision == expected_revision.revision;
|
||||
let passed = passed && revision_unchanged;
|
||||
let verification_tool = gate.last_verification_tool.clone();
|
||||
gate.verified_revision = passed
|
||||
.then_some(current_revision.revision)
|
||||
.filter(|value| *value > 0);
|
||||
if passed && verification_tool.as_deref() == Some("preview.validate") {
|
||||
gate.failed_playtest_revision = None;
|
||||
}
|
||||
gate.last_verification_status = Some(
|
||||
if passed {
|
||||
AGENT_RUNTIME_VERIFICATION_STATUS_PASSED
|
||||
@@ -21431,6 +21451,30 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_agent_runtime_failed_playtest_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
expected_revision: u64,
|
||||
) -> Result<(), String> {
|
||||
let _lock =
|
||||
acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, "preview.validate")?;
|
||||
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
|
||||
if current_revision.revision != expected_revision {
|
||||
return Err(format!(
|
||||
"浏览器试玩通过后项目 revision 已从 {expected_revision} 变化为 {}",
|
||||
current_revision.revision
|
||||
));
|
||||
}
|
||||
let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
|
||||
if gate.mutation_revision != Some(expected_revision) {
|
||||
return Err("浏览器试玩通过时没有当前 revision 的项目修改凭证".to_string());
|
||||
}
|
||||
gate.failed_playtest_revision = None;
|
||||
gate.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_verification_gate(root, &gate)
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_agent_runtime_project_verification_after_preview_failure_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -21454,6 +21498,7 @@ pub(crate) fn invalidate_agent_runtime_project_verification_after_preview_failur
|
||||
gate.verified_revision = None;
|
||||
gate.last_verification_tool = Some("preview.validate".to_string());
|
||||
gate.last_verification_status = Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED.to_string());
|
||||
gate.failed_playtest_revision = Some(expected_revision);
|
||||
gate.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_verification_gate(root, &gate)
|
||||
}
|
||||
@@ -28217,6 +28262,21 @@ fn validate_agent_runtime_autonomous_plan_liveness(
|
||||
.iter()
|
||||
.any(|action| is_supervisor_orchestrator_project_mutation_tool(&action.tool));
|
||||
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||||
if verification_gate
|
||||
.failed_playtest_revision
|
||||
.zip(verification_gate.mutation_revision)
|
||||
.is_some_and(|(failed_playtest_revision, mutation_revision)| {
|
||||
failed_playtest_revision == mutation_revision
|
||||
})
|
||||
&& !has_mutation
|
||||
{
|
||||
return Err(format!(
|
||||
"{AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX};持久验证门仍标记 revision {} 的试玩失败,即使上下文压缩后也必须根据最近一次试玩诊断直接修改 game/index.html;不得继续只更新计划、读取、搜索、重复验证、查询状态、委派或返回最终回复",
|
||||
verification_gate
|
||||
.failed_playtest_revision
|
||||
.unwrap_or_default()
|
||||
));
|
||||
}
|
||||
let Some(failed_playtest_index) = observations.iter().rposition(|observation| {
|
||||
observation.tool == "preview.validate"
|
||||
&& observation.status == "failed"
|
||||
@@ -35421,7 +35481,7 @@ async fn observe_agent_runtime_preview_validate(
|
||||
detail: None,
|
||||
};
|
||||
};
|
||||
match write_autonomous_playtest_receipt_at(
|
||||
let receipt = match write_autonomous_playtest_receipt_at(
|
||||
root,
|
||||
contract,
|
||||
action_id,
|
||||
@@ -35438,7 +35498,21 @@ async fn observe_agent_runtime_preview_validate(
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
};
|
||||
}
|
||||
};
|
||||
if let Err(error) = clear_agent_runtime_failed_playtest_at(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
revision_after.revision,
|
||||
) {
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "preview.validate".to_string(),
|
||||
status: "failed".to_string(),
|
||||
summary: "浏览器验证已通过,但失败试玩凭证无法安全清除".to_string(),
|
||||
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
|
||||
};
|
||||
}
|
||||
receipt
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -7587,6 +7587,188 @@ async fn autonomous_game_build_repairs_supervisor_failed_playtest_stall_into_mut
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn autonomous_game_build_repairs_persisted_failed_playtest_after_context_compaction() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"project-autonomous-supervisor-persisted-playtest-liveness",
|
||||
"自主构建总控持久试玩失败活性测试",
|
||||
)
|
||||
.expect("project init");
|
||||
write_supervisor_collaboration_policy_at(&root, SupervisorCollaborationPolicy::default())
|
||||
.expect("disable unrelated collaboration preflight");
|
||||
let run_id = "autonomous-supervisor-persisted-playtest-liveness-run";
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("bind autonomous supervisor profile");
|
||||
let runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"修复试玩失败并完成可玩塔防",
|
||||
run_id,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
"根据试玩诊断直接修复",
|
||||
vec!["修复试玩失败".to_string(), "重新验证并交付".to_string()],
|
||||
)
|
||||
.expect("start autonomous supervisor runtime");
|
||||
|
||||
let revision = prepare_agent_runtime_project_mutation_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
"file.write",
|
||||
)
|
||||
.expect("record project mutation");
|
||||
let (expected_revision, gate) = begin_agent_runtime_project_verification_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
"game.static_smoke",
|
||||
)
|
||||
.expect("begin initial static smoke");
|
||||
finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true)
|
||||
.expect("finish initial static smoke");
|
||||
invalidate_agent_runtime_project_verification_after_preview_failure_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
revision,
|
||||
)
|
||||
.expect("persist failed browser playtest");
|
||||
|
||||
// Simulate a later context window that only remembers the generic static check.
|
||||
let (expected_revision, gate) = begin_agent_runtime_project_verification_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
"game.static_smoke",
|
||||
)
|
||||
.expect("begin static smoke after compaction");
|
||||
finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true)
|
||||
.expect("finish static smoke after compaction");
|
||||
let gate = read_game_creator_agent_runtime_verification_gate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
)
|
||||
.expect("read persisted failed playtest gate");
|
||||
assert_eq!(gate.failed_playtest_revision, Some(revision));
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let read_arguments = serde_json::json!({"reason": "继续读取而不修复", "input": {}}).to_string();
|
||||
let patch_arguments = serde_json::json!({
|
||||
"reason": "根据持久试玩诊断直接修复",
|
||||
"input": {
|
||||
"path": "game/index.html",
|
||||
"oldText": "ctx.",
|
||||
"newText": "ctx.fillRect(0,0,1,1);</script></body></html>"
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_raw_responses_with_capture(
|
||||
vec![
|
||||
native_agent_tool_plan_chat_response(
|
||||
"call-autonomous-supervisor-persisted-playtest-read",
|
||||
&native_runtime_function_name("project.index").expect("index function"),
|
||||
read_arguments,
|
||||
),
|
||||
native_agent_tool_plan_chat_response(
|
||||
"call-autonomous-supervisor-persisted-playtest-patch",
|
||||
&native_runtime_function_name("file.patch").expect("patch function"),
|
||||
patch_arguments,
|
||||
),
|
||||
],
|
||||
Some(sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"project-supervisor": {{
|
||||
"apiKey": "autonomous-supervisor-persisted-playtest-repair-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "autonomous-supervisor-persisted-playtest-repair-model",
|
||||
"apiKind": "openai_chat",
|
||||
"maxRetries": 0
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let observations = (0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT)
|
||||
.map(|_| AgentRuntimeToolObservation {
|
||||
tool: "runtime.plan_update".to_string(),
|
||||
status: "blocked".to_string(),
|
||||
summary: "结构化计划尚未完成".to_string(),
|
||||
detail: None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let plan = request_game_creator_agent_background_tool_plan_for_test(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&runtime.session_id,
|
||||
run_id,
|
||||
&runtime.current_task,
|
||||
&observations,
|
||||
30,
|
||||
0,
|
||||
)
|
||||
.await
|
||||
.expect("repair persisted failed playtest stall")
|
||||
.expect("repaired persisted supervisor mutation plan");
|
||||
assert_eq!(plan.actions.len(), 1);
|
||||
assert_eq!(plan.actions[0].tool, "file.patch");
|
||||
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("initial persisted stalled supervisor request");
|
||||
let repair_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("persisted failed playtest mutation repair request");
|
||||
assert!(repair_request.contains("持久验证门仍标记"));
|
||||
assert!(repair_request.contains("不得继续只更新计划、读取、搜索"));
|
||||
let repair_request_json = mock_http_request_json(&repair_request);
|
||||
let repair_function_names = repair_request_json["tools"]
|
||||
.as_array()
|
||||
.expect("restricted persisted failed playtest repair tools")
|
||||
.iter()
|
||||
.filter_map(|tool| {
|
||||
tool.get("name")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.or_else(|| {
|
||||
tool.get("function")
|
||||
.and_then(|function| function.get("name"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
})
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
repair_function_names,
|
||||
BTreeSet::from([
|
||||
native_runtime_function_name("file.write")
|
||||
.expect("write function")
|
||||
.as_str(),
|
||||
native_runtime_function_name("file.patch")
|
||||
.expect("patch function")
|
||||
.as_str(),
|
||||
native_runtime_function_name("file.delete")
|
||||
.expect("delete function")
|
||||
.as_str(),
|
||||
native_runtime_function_name("project.patchset")
|
||||
.expect("patchset function")
|
||||
.as_str(),
|
||||
])
|
||||
);
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_autonomous_preview_invalidates_static_smoke_verification_gate() {
|
||||
let root = unique_project_path();
|
||||
@@ -7633,6 +7815,25 @@ fn failed_autonomous_preview_invalidates_static_smoke_verification_gate() {
|
||||
Some("preview.validate")
|
||||
);
|
||||
assert_eq!(gate.last_verification_status.as_deref(), Some("failed"));
|
||||
assert_eq!(gate.failed_playtest_revision, Some(revision));
|
||||
|
||||
// A later static smoke pass must not erase the durable interactive failure.
|
||||
let (expected_revision, gate) = begin_agent_runtime_project_verification_locked(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
"game.static_smoke",
|
||||
)
|
||||
.expect("begin static smoke after failed playtest");
|
||||
finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true)
|
||||
.expect("finish static smoke after failed playtest");
|
||||
let gate = read_game_creator_agent_runtime_verification_gate(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
run_id,
|
||||
)
|
||||
.expect("read gate after static smoke");
|
||||
assert_eq!(gate.failed_playtest_revision, Some(revision));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user