修复自主构建收束回复与公共失败审计
为已通过完成门禁的自主构建总控补充确定性最终回复兜底。 保留 Provider 失败生命周期并沿用回复流与 finalization 幂等提交。 将公共失败事件和 Agent DB 审计改为哈希、长度与稳定分类。 补充私有诊断隔离回归、真实外部 E2E 证据与项目文档。
This commit is contained in:
+2
-6
@@ -7,6 +7,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
run_id: &str,
|
||||
task: &str,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
fallback_response: Option<&str>,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
applied_steer_cursor: u64,
|
||||
request_slot: &str,
|
||||
@@ -116,12 +117,7 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
|
||||
let stream_snapshot = provider_snapshot.clone();
|
||||
let suppress_private_process_output =
|
||||
agent_runtime_observations_contain_private_process_output(observations);
|
||||
let fallback_response = (!plan.response.trim().is_empty())
|
||||
.then(|| strip_llm_thinking_blocks(&plan.response))
|
||||
.map(|response| {
|
||||
redact_agent_runtime_private_process_output_from_response(&response, observations)
|
||||
})
|
||||
.filter(|response| !response.trim().is_empty());
|
||||
let fallback_response = fallback_response.filter(|response| !response.trim().is_empty());
|
||||
let stream_response = llm.stream;
|
||||
let request_stream_snapshot = stream_snapshot.clone();
|
||||
let response_result =
|
||||
|
||||
@@ -192,6 +192,8 @@ mod finalization;
|
||||
mod interaction;
|
||||
mod lifecycle_control;
|
||||
mod main_loop;
|
||||
#[cfg(test)]
|
||||
mod main_loop_tests;
|
||||
mod pending_execution;
|
||||
mod pending_recovery;
|
||||
mod provider_recovery;
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
use super::*;
|
||||
|
||||
pub(super) fn game_creator_agent_background_final_reply_fallback(
|
||||
plan_response: &str,
|
||||
run_profile: &str,
|
||||
agent_id: &str,
|
||||
response_revision: u64,
|
||||
) -> Option<String> {
|
||||
if !plan_response.trim().is_empty() {
|
||||
let response = strip_llm_thinking_blocks(plan_response);
|
||||
return (!response.trim().is_empty()).then_some(response);
|
||||
}
|
||||
(run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
&& agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
.then(|| {
|
||||
format!(
|
||||
"项目已完成生成,并通过当前 revision {response_revision} 的静态检查和桌面、移动端交互试玩验证。"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_TOOL_PLAN: &str = "tool-plan-failed";
|
||||
const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_BUDGET: &str = "loop-budget-exhausted";
|
||||
const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_FINAL_REPLY: &str = "final-reply-failed";
|
||||
const AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_FINALIZATION: &str = "finalization-failed";
|
||||
|
||||
pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_context(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
@@ -505,17 +529,10 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"source": runtime.source,
|
||||
"error": runtime.error,
|
||||
}),
|
||||
&runtime,
|
||||
AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_TOOL_PLAN,
|
||||
);
|
||||
}
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
@@ -2596,18 +2613,10 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"source": runtime.source,
|
||||
"failureKind": "loop-budget-exhausted",
|
||||
"error": runtime.error,
|
||||
}),
|
||||
&runtime,
|
||||
AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_BUDGET,
|
||||
);
|
||||
}
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
@@ -2664,6 +2673,16 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
);
|
||||
let response_next_loop_index =
|
||||
usize::try_from(runtime.loop_iteration).unwrap_or(usize::MAX);
|
||||
let final_reply_fallback = game_creator_agent_background_final_reply_fallback(
|
||||
&plan.response,
|
||||
&runtime.run_profile,
|
||||
&agent_id,
|
||||
response_revision,
|
||||
)
|
||||
.map(|response| {
|
||||
redact_agent_runtime_private_process_output_from_response(&response, &observations)
|
||||
})
|
||||
.filter(|response| !response.trim().is_empty());
|
||||
if let Err(error) = persist_game_creator_agent_runtime_context(
|
||||
&root,
|
||||
&runtime,
|
||||
@@ -2688,6 +2707,7 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
&runtime.run_id,
|
||||
&task,
|
||||
&plan,
|
||||
final_reply_fallback.as_deref(),
|
||||
&observations,
|
||||
runtime.applied_steer_cursor,
|
||||
&final_reply_request_slot,
|
||||
@@ -2730,10 +2750,6 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
);
|
||||
}
|
||||
}
|
||||
let plan_fallback_response = redact_agent_runtime_private_process_output_from_response(
|
||||
&strip_llm_thinking_blocks(&plan.response),
|
||||
&observations,
|
||||
);
|
||||
let reply = match final_reply_result {
|
||||
Ok(RequestedAgentRuntimeFinalReplyOutcome::Ready(Some(requested_reply))) => {
|
||||
runtime.context_usage.estimated_input_tokens =
|
||||
@@ -2879,7 +2895,9 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
{
|
||||
return AgentBackgroundTaskOutcome::NeedsReconciliation;
|
||||
}
|
||||
Err(_) if !plan_fallback_response.trim().is_empty() => plan_fallback_response,
|
||||
Err(_) if final_reply_fallback.is_some() => {
|
||||
final_reply_fallback.expect("checked final reply fallback")
|
||||
}
|
||||
Err(error) => {
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let failed_runtime =
|
||||
@@ -2895,17 +2913,10 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"source": runtime.source,
|
||||
"error": runtime.error,
|
||||
}),
|
||||
&runtime,
|
||||
AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_FINAL_REPLY,
|
||||
);
|
||||
}
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
@@ -2976,17 +2987,10 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
|
||||
Err(error) => {
|
||||
let failed_runtime = fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"source": runtime.source,
|
||||
"error": runtime.error,
|
||||
}),
|
||||
&runtime,
|
||||
AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_FINALIZATION,
|
||||
);
|
||||
}
|
||||
AgentBackgroundTaskOutcome::Finished
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
use super::*;
|
||||
|
||||
fn prepare_autonomous_completion_evidence(root: &Path, state: &AgentRuntimeState) -> u64 {
|
||||
let contract = read_autonomous_completion_contract(root, &state.agent_id, &state.run_id)
|
||||
.expect("read autonomous completion contract")
|
||||
.expect("autonomous completion contract exists");
|
||||
let revision = {
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"test.autonomous.final_reply.mutate",
|
||||
)
|
||||
.expect("acquire autonomous mutation lock");
|
||||
let revision = prepare_agent_runtime_project_mutation_locked(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
"file.write",
|
||||
)
|
||||
.expect("advance autonomous project revision");
|
||||
write_local_project_file_at(
|
||||
root,
|
||||
AGENT_RUNTIME_GAME_INDEX_PATH,
|
||||
"<!doctype html><title>可玩塔防</title><canvas></canvas>",
|
||||
)
|
||||
.expect("write autonomous game index");
|
||||
revision
|
||||
};
|
||||
{
|
||||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||
root,
|
||||
"test.autonomous.final_reply.verify",
|
||||
)
|
||||
.expect("acquire autonomous verification lock");
|
||||
let (revision, gate) = begin_agent_runtime_project_verification_locked(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
"game.static_smoke",
|
||||
)
|
||||
.expect("begin autonomous static smoke");
|
||||
finish_agent_runtime_project_verification_locked(root, &revision, gate, true)
|
||||
.expect("finish autonomous static smoke");
|
||||
}
|
||||
bind_supervisor_collaboration_policy_snapshot_at(
|
||||
root,
|
||||
&state.agent_id,
|
||||
&state.run_id,
|
||||
&SupervisorCollaborationPolicy::default(),
|
||||
"legacy-current-project-policy",
|
||||
)
|
||||
.expect("bind empty collaboration policy");
|
||||
|
||||
let evidence_root = root
|
||||
.join(".agent/runtime/browser-validations")
|
||||
.join(agent_runtime_confirmation_path_component(
|
||||
&state.agent_id,
|
||||
"agent",
|
||||
))
|
||||
.join(agent_runtime_confirmation_path_component(
|
||||
&state.run_id,
|
||||
"run",
|
||||
))
|
||||
.join(revision.to_string());
|
||||
fs::create_dir_all(&evidence_root).expect("create browser evidence root");
|
||||
let screenshots = [
|
||||
evidence_root.join("desktop.png"),
|
||||
evidence_root.join("mobile.png"),
|
||||
];
|
||||
for screenshot in &screenshots {
|
||||
fs::write(screenshot, b"\x89PNG\r\n\x1a\nfixture")
|
||||
.expect("write browser screenshot fixture");
|
||||
}
|
||||
let viewport = |viewport, screenshot_path| BrowserViewportValidationResult {
|
||||
viewport,
|
||||
width: if viewport == BrowserValidationViewport::Desktop {
|
||||
1280
|
||||
} else {
|
||||
390
|
||||
},
|
||||
height: if viewport == BrowserValidationViewport::Desktop {
|
||||
720
|
||||
} else {
|
||||
844
|
||||
},
|
||||
final_url: "http://127.0.0.1:34567/".to_string(),
|
||||
title: "自主试玩测试".to_string(),
|
||||
ready_state: "complete".to_string(),
|
||||
visible_text_summary: "可试玩项目".to_string(),
|
||||
visible_text_character_count: 5,
|
||||
dom_character_count: 100,
|
||||
expected_text: Vec::new(),
|
||||
console_errors: Vec::new(),
|
||||
console_warnings: Vec::new(),
|
||||
exceptions: Vec::new(),
|
||||
failed_requests: Vec::new(),
|
||||
canvases: Vec::new(),
|
||||
blocked_popup_count: 0,
|
||||
blocked_dialog_count: 0,
|
||||
blocked_download_count: 0,
|
||||
blocked_permission_count: 0,
|
||||
blocked_service_worker_count: 0,
|
||||
screenshot_path,
|
||||
passed: true,
|
||||
diagnostics: Vec::new(),
|
||||
};
|
||||
let scenario = BrowserPlaytestScenario::LaneDefenseV1;
|
||||
let result = BrowserValidationResult {
|
||||
schema_version: "browser-validation.v1".to_string(),
|
||||
url: "http://127.0.0.1:34567/".to_string(),
|
||||
browser: BrowserIdentity {
|
||||
kind: DiscoveredBrowserKind::Chrome,
|
||||
product: "test-browser".to_string(),
|
||||
protocol_version: "1".to_string(),
|
||||
},
|
||||
passed: true,
|
||||
viewport_results: vec![
|
||||
viewport(BrowserValidationViewport::Desktop, screenshots[0].clone()),
|
||||
viewport(BrowserValidationViewport::Mobile, screenshots[1].clone()),
|
||||
],
|
||||
playtest: Some(BrowserPlaytestResult {
|
||||
scenario,
|
||||
scenario_fingerprint: browser_playtest_scenario_fingerprint(scenario),
|
||||
passed: true,
|
||||
initial_sequence: Some(1),
|
||||
initial_phase: Some(BrowserPlaytestPhase::Ready),
|
||||
initial_level: Some(1),
|
||||
final_sequence: Some(8),
|
||||
final_phase: Some(BrowserPlaytestPhase::Playing),
|
||||
final_level: Some(2),
|
||||
assertions: vec![BrowserPlaytestAssertion {
|
||||
name: "fixture-passed".to_string(),
|
||||
passed: true,
|
||||
}],
|
||||
diagnostics: Vec::new(),
|
||||
}),
|
||||
diagnostics: Vec::new(),
|
||||
evidence: BrowserValidationEvidencePaths {
|
||||
root: evidence_root.clone(),
|
||||
report_path: evidence_root.join("validation.json"),
|
||||
},
|
||||
completed_at_unix_ms: 1,
|
||||
};
|
||||
fs::write(
|
||||
&result.evidence.report_path,
|
||||
serde_json::to_vec_pretty(&result).expect("serialize browser evidence"),
|
||||
)
|
||||
.expect("write browser evidence report");
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "preview.validate".to_string(),
|
||||
reason: Some("验证真实可玩闭环".to_string()),
|
||||
input: serde_json::json!({}),
|
||||
};
|
||||
let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task);
|
||||
let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint);
|
||||
write_autonomous_playtest_receipt_at(
|
||||
root,
|
||||
&contract,
|
||||
&action_id,
|
||||
&action_fingerprint,
|
||||
revision,
|
||||
&result,
|
||||
)
|
||||
.expect("write autonomous playtest receipt");
|
||||
assert!(autonomous_game_build_completion_blocker_at_locked(root, state).is_none());
|
||||
revision
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autonomous_supervisor_empty_plan_uses_deterministic_final_reply_fallback() {
|
||||
assert_eq!(
|
||||
game_creator_agent_background_final_reply_fallback(
|
||||
"",
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
7,
|
||||
)
|
||||
.as_deref(),
|
||||
Some("项目已完成生成,并通过当前 revision 7 的静态检查和桌面、移动端交互试玩验证。")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_agent_empty_plan_has_no_deterministic_final_reply_fallback() {
|
||||
assert!(game_creator_agent_background_final_reply_fallback(
|
||||
"",
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
"code-prototype",
|
||||
7,
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standard_supervisor_empty_plan_has_no_deterministic_final_reply_fallback() {
|
||||
assert!(game_creator_agent_background_final_reply_fallback(
|
||||
"",
|
||||
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
7,
|
||||
)
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_response_precedes_autonomous_supervisor_deterministic_fallback() {
|
||||
assert_eq!(
|
||||
game_creator_agent_background_final_reply_fallback(
|
||||
"沿用现有计划回复。",
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
7,
|
||||
)
|
||||
.as_deref(),
|
||||
Some("沿用现有计划回复。")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallback_once() {
|
||||
const RUN_ID: &str = "autonomous-final-reply-fallback-run";
|
||||
const TASK: &str = "生成一个可完成静态检查和双视口试玩的塔防游戏";
|
||||
const TEST_KEY: &str = "autonomous-final-reply-fallback-key";
|
||||
|
||||
let temporary = tempfile::tempdir().expect("create autonomous fallback root");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "autonomous-fallback-project", TASK)
|
||||
.expect("init autonomous fallback project");
|
||||
let planning_response = serde_json::json!({
|
||||
"thinkingSummary": "当前 revision 的完成证据已经齐全",
|
||||
"planUpdate": null,
|
||||
"plan": [],
|
||||
"actions": [],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url =
|
||||
crate::tests::spawn_mock_llm_tool_plan_then_invalid_final_reply(planning_response);
|
||||
let _config_guard = crate::tests::write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"project-supervisor": {{
|
||||
"apiKey": "{TEST_KEY}",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "autonomous-fallback-model",
|
||||
"apiKind": "openai_responses",
|
||||
"stream": false,
|
||||
"maxRetries": 0,
|
||||
"retryBackoffMs": 1
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let lane_lock = try_acquire_game_creator_agent_runtime_task_lock(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
)
|
||||
.expect("acquire Supervisor lane")
|
||||
.expect("Supervisor lane available");
|
||||
start_game_creator_supervisor_background_task_for_session_at(
|
||||
&root,
|
||||
None,
|
||||
TASK,
|
||||
RUN_ID,
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
|
||||
)
|
||||
.expect("queue autonomous Supervisor task");
|
||||
let task_record = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read queued Supervisor task")
|
||||
.expect("queued Supervisor task exists");
|
||||
let state = agent_runtime_state_from_task_record(&task_record);
|
||||
let revision = prepare_autonomous_completion_evidence(&root, &state);
|
||||
drop(lane_lock);
|
||||
|
||||
resume_game_creator_agent_background_tasks_at(&root)
|
||||
.expect("resume autonomous Supervisor task");
|
||||
let mut runtime =
|
||||
read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
.expect("read autonomous Supervisor runtime")
|
||||
.state;
|
||||
for _ in 0..500 {
|
||||
if runtime.status == "idle" || runtime.status == "failed" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
runtime =
|
||||
read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|
||||
.expect("poll autonomous Supervisor runtime")
|
||||
.state;
|
||||
}
|
||||
let fallback = format!(
|
||||
"项目已完成生成,并通过当前 revision {revision} 的静态检查和桌面、移动端交互试玩验证。"
|
||||
);
|
||||
assert_eq!(runtime.status, "idle");
|
||||
assert_eq!(runtime.phase, "completed");
|
||||
assert_eq!(runtime.last_response.as_deref(), Some(fallback.as_str()));
|
||||
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
Some(&task_record.session_id),
|
||||
)
|
||||
.expect("read autonomous Supervisor conversation");
|
||||
assert_eq!(
|
||||
conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| message.role == "assistant")
|
||||
.map(|message| message.content.as_str())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![fallback.as_str()]
|
||||
);
|
||||
let stream = read_game_creator_agent_runtime_response_stream_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read autonomous fallback response stream")
|
||||
.expect("autonomous fallback response stream exists");
|
||||
assert_eq!(stream.status, "committed");
|
||||
assert_eq!(stream.accumulated_text, fallback);
|
||||
assert!(read_game_creator_agent_runtime_finalization_journal(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read finalization residue")
|
||||
.is_none());
|
||||
assert!(provider_retry::read_for_run_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read retry residue")
|
||||
.is_none());
|
||||
assert!(provider_handoff::read_for_run_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read handoff residue")
|
||||
.is_none());
|
||||
assert!(tool_plan_handoff::read_for_run_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
RUN_ID,
|
||||
)
|
||||
.expect("read tool-plan handoff residue")
|
||||
.is_none());
|
||||
|
||||
let public_paths = [
|
||||
root.join(".agent/agent.db"),
|
||||
game_creator_agent_runtime_event_path(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID),
|
||||
root.join(".agent/activity.jsonl"),
|
||||
root.join(".agent/output.jsonl"),
|
||||
];
|
||||
let project_path = root.to_string_lossy().into_owned();
|
||||
for path in public_paths.iter().filter(|path| path.exists()) {
|
||||
let content = fs::read_to_string(path).expect("read autonomous fallback public audit");
|
||||
for forbidden in [fallback.as_str(), TEST_KEY, project_path.as_str()] {
|
||||
assert!(!content.contains(forbidden));
|
||||
}
|
||||
}
|
||||
let public_records =
|
||||
fs::read_to_string(&public_paths[0]).expect("read autonomous fallback audit");
|
||||
let final_reply_lifecycle_statuses = public_records
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str::<serde_json::Value>(line).ok())
|
||||
.filter(|record| {
|
||||
record["recordType"] == "agent.runtime.provider_request.lifecycle"
|
||||
&& record["runId"] == RUN_ID
|
||||
&& record["requestKind"] == "final-reply"
|
||||
})
|
||||
.filter_map(|record| record["status"].as_str().map(str::to_string))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(final_reply_lifecycle_statuses, vec!["started", "failed"]);
|
||||
}
|
||||
@@ -1061,6 +1061,44 @@ pub(crate) fn finish_game_creator_agent_background_runtime_turn_at(
|
||||
)
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_failure_metadata(error: &str) -> (String, usize) {
|
||||
(
|
||||
format!("{:x}", Sha256::digest(error.as_bytes())),
|
||||
error.chars().count(),
|
||||
)
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_public_failure_detail(error: &str) -> String {
|
||||
let (error_sha256, error_chars) = game_creator_agent_runtime_failure_metadata(error);
|
||||
format!("errorSha256={error_sha256} · errorChars={error_chars}")
|
||||
}
|
||||
|
||||
pub(crate) fn append_game_creator_agent_background_task_failed_audit(
|
||||
root: &Path,
|
||||
state: &AgentRuntimeState,
|
||||
failure_kind: &'static str,
|
||||
) -> Result<(), String> {
|
||||
let error = state
|
||||
.error
|
||||
.as_deref()
|
||||
.ok_or_else(|| "后台任务失败审计缺少私有错误诊断".to_string())?;
|
||||
let (error_sha256, error_chars) = game_creator_agent_runtime_failure_metadata(error);
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": state.agent_id,
|
||||
"taskId": state.task_id,
|
||||
"sessionId": state.session_id,
|
||||
"runId": state.run_id,
|
||||
"source": state.source,
|
||||
"failureKind": failure_kind,
|
||||
"errorSha256": error_sha256,
|
||||
"errorChars": error_chars,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn fail_game_creator_agent_runtime_turn_at(
|
||||
root: &Path,
|
||||
mut state: AgentRuntimeState,
|
||||
@@ -2278,6 +2316,12 @@ pub(super) fn append_game_creator_agent_runtime_event_with_action(
|
||||
|| summary.starts_with("command.output_read:")))
|
||||
})
|
||||
.map(|value| {
|
||||
if matches!(
|
||||
event_type,
|
||||
"error" | "turn.failed" | "turn.budget_exhausted"
|
||||
) {
|
||||
return game_creator_agent_runtime_public_failure_detail(value);
|
||||
}
|
||||
let max_chars = if event_type == "observation"
|
||||
&& summary.starts_with("agent.action_history:")
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ fn valid_test_png_bytes() -> Vec<u8> {
|
||||
.expect("valid 1x1 test png")
|
||||
}
|
||||
|
||||
struct TestConfigGuard {
|
||||
pub(crate) struct TestConfigGuard {
|
||||
_lock: StdMutexGuard<'static, ()>,
|
||||
path: PathBuf,
|
||||
previous: Option<Vec<u8>>,
|
||||
@@ -702,7 +702,7 @@ fn replace_test_local_config(path: &Path, content: impl AsRef<[u8]>) {
|
||||
fs::rename(&temp_path, path).expect("replace local config");
|
||||
}
|
||||
|
||||
fn write_test_local_config(content: String) -> TestConfigGuard {
|
||||
pub(crate) fn write_test_local_config(content: String) -> TestConfigGuard {
|
||||
let lock = TEST_CONFIG_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner());
|
||||
@@ -1079,6 +1079,46 @@ fn spawn_mock_llm_server_responses(response_contents: Vec<String>) -> String {
|
||||
spawn_mock_llm_server_responses_with_capture(response_contents, None)
|
||||
}
|
||||
|
||||
pub(crate) fn spawn_mock_llm_tool_plan_then_invalid_final_reply(
|
||||
planning_response: String,
|
||||
) -> String {
|
||||
let listener = bind_test_tcp_listener("mock invalid final reply bind");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr"));
|
||||
std::thread::spawn(move || {
|
||||
let (mut planning_stream, _) = listener.accept().expect("mock tool plan accept");
|
||||
drop(read_mock_http_request(&mut planning_stream));
|
||||
let planning_body = serde_json::json!({
|
||||
"id": "resp_invalid_final_reply_planning",
|
||||
"model": "mock-game-model",
|
||||
"output_text": planning_response,
|
||||
"status": "completed",
|
||||
"usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 }
|
||||
})
|
||||
.to_string();
|
||||
let planning_response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
planning_body.len(),
|
||||
planning_body
|
||||
);
|
||||
planning_stream
|
||||
.write_all(planning_response.as_bytes())
|
||||
.expect("mock tool plan response");
|
||||
|
||||
let (mut final_stream, _) = listener.accept().expect("mock final reply accept");
|
||||
drop(read_mock_http_request(&mut final_stream));
|
||||
let invalid_body = "{invalid-json";
|
||||
let final_response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
invalid_body.len(),
|
||||
invalid_body
|
||||
);
|
||||
final_stream
|
||||
.write_all(final_response.as_bytes())
|
||||
.expect("mock invalid final reply response");
|
||||
});
|
||||
base_url
|
||||
}
|
||||
|
||||
fn final_tool_plan_response(response: impl Into<String>) -> String {
|
||||
serde_json::json!({
|
||||
"thinkingSummary": "已有工具观察足够,可以收束后台任务",
|
||||
|
||||
@@ -541,6 +541,280 @@ fn structured_plan_failure_after_all_steps_completed_preserves_terminal_snapshot
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_failure_public_audits_hash_private_delivery_diagnostics() {
|
||||
for (
|
||||
agent_id,
|
||||
child_run_id,
|
||||
parent_run_id,
|
||||
parent_action_id,
|
||||
private_error,
|
||||
budget_exhausted,
|
||||
terminal_event_type,
|
||||
failure_kind,
|
||||
) in [
|
||||
(
|
||||
"code-prototype",
|
||||
"public-failure-audit-code-run",
|
||||
"public-failure-audit-code-parent-run",
|
||||
"action-111111111111111111111111",
|
||||
"private deliveryStructuredResult:程序 Agent 失败诊断正文仅供私有返工使用",
|
||||
false,
|
||||
"turn.failed",
|
||||
"tool-plan-failed",
|
||||
),
|
||||
(
|
||||
"quality-review",
|
||||
"public-failure-audit-quality-run",
|
||||
"public-failure-audit-quality-parent-run",
|
||||
"action-222222222222222222222222",
|
||||
"private deliveryResult:质量 Agent 预算耗尽诊断正文仅供私有返工使用",
|
||||
true,
|
||||
"turn.budget_exhausted",
|
||||
"loop-budget-exhausted",
|
||||
),
|
||||
] {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "公共失败审计正文隔离测试")
|
||||
.expect("project init");
|
||||
let parent = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
"等待专业 Agent 私有失败回执",
|
||||
parent_run_id,
|
||||
"agent-chat",
|
||||
"等待专业 Agent",
|
||||
vec!["处理专业 Agent 回执".to_string()],
|
||||
)
|
||||
.expect("start parent runtime");
|
||||
let mut child = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
agent_id,
|
||||
"执行专业 Agent 任务并保留私有失败诊断",
|
||||
child_run_id,
|
||||
"agent-delegate",
|
||||
"执行专业任务",
|
||||
vec!["返回私有诊断".to_string()],
|
||||
)
|
||||
.expect("start child runtime");
|
||||
let delegation_id = agent_runtime_delegation_id(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
parent_run_id,
|
||||
agent_id,
|
||||
parent_action_id,
|
||||
);
|
||||
child.parent_agent_id = Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string());
|
||||
child.parent_run_id = Some(parent_run_id.to_string());
|
||||
child.delegation_id = Some(delegation_id.clone());
|
||||
let delivery = new_static_delegate_delivery(
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&parent.session_id,
|
||||
parent_run_id,
|
||||
parent_action_id,
|
||||
&delegation_id,
|
||||
agent_id,
|
||||
&child.session_id,
|
||||
child_run_id,
|
||||
);
|
||||
create_or_read_static_delegate_delivery_at(&root, &delivery)
|
||||
.expect("create static delivery");
|
||||
|
||||
let failed = if budget_exhausted {
|
||||
fail_game_creator_agent_runtime_budget_at(&root, child, private_error)
|
||||
} else {
|
||||
fail_game_creator_agent_runtime_turn_at(&root, child, private_error)
|
||||
}
|
||||
.expect("persist private failure state");
|
||||
append_game_creator_agent_background_task_failed_audit(&root, &failed, failure_kind)
|
||||
.expect("append public failure audit");
|
||||
|
||||
assert_eq!(failed.error.as_deref(), Some(private_error));
|
||||
let private_delivery = read_static_delegate_delivery_at(&root, &delegation_id)
|
||||
.expect("read private delivery")
|
||||
.expect("private delivery exists");
|
||||
assert_eq!(private_delivery.status, StaticDelegateDeliveryStatus::Ready);
|
||||
assert_eq!(
|
||||
private_delivery.result_summary.as_deref(),
|
||||
Some(private_error)
|
||||
);
|
||||
assert_eq!(
|
||||
private_delivery
|
||||
.structured_result
|
||||
.as_ref()
|
||||
.and_then(|result| result.error.as_deref()),
|
||||
Some(private_error)
|
||||
);
|
||||
|
||||
let error_sha256 = format!("{:x}", Sha256::digest(private_error.as_bytes()));
|
||||
let error_chars = private_error.chars().count();
|
||||
let expected_public_detail =
|
||||
format!("errorSha256={error_sha256} · errorChars={error_chars}");
|
||||
let result = read_game_creator_agent_runtime_at(&root, agent_id)
|
||||
.expect("read failed runtime projection");
|
||||
let public_failure_events = result
|
||||
.recent_events
|
||||
.iter()
|
||||
.filter(|event| {
|
||||
event.run_id == child_run_id
|
||||
&& matches!(
|
||||
event.event_type.as_str(),
|
||||
"error" | "turn.failed" | "turn.budget_exhausted"
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(public_failure_events.len(), 2);
|
||||
assert!(public_failure_events
|
||||
.iter()
|
||||
.any(|event| event.event_type == "error"));
|
||||
assert!(public_failure_events
|
||||
.iter()
|
||||
.any(|event| event.event_type == terminal_event_type));
|
||||
assert!(public_failure_events.iter().all(|event| {
|
||||
event.detail.as_deref() == Some(expected_public_detail.as_str())
|
||||
&& !event.summary.contains(private_error)
|
||||
}));
|
||||
let event_log = fs::read_to_string(game_creator_agent_runtime_event_path(&root, agent_id))
|
||||
.expect("read public event log");
|
||||
assert!(!event_log.contains(private_error));
|
||||
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db");
|
||||
assert!(!agent_db.contains(private_error));
|
||||
let failure_records = agent_db
|
||||
.lines()
|
||||
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some("agent.runtime.background_task.failed")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(child_run_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(failure_records.len(), 1);
|
||||
let failure_record = &failure_records[0];
|
||||
assert_eq!(failure_record["agentId"], agent_id);
|
||||
assert_eq!(failure_record["taskId"], agent_id);
|
||||
assert_eq!(failure_record["sessionId"], failed.session_id);
|
||||
assert_eq!(failure_record["runId"], child_run_id);
|
||||
assert_eq!(failure_record["source"], "agent-delegate");
|
||||
assert_eq!(failure_record["failureKind"], failure_kind);
|
||||
assert_eq!(failure_record["errorSha256"], error_sha256);
|
||||
assert_eq!(
|
||||
failure_record["errorChars"].as_u64(),
|
||||
Some(error_chars as u64)
|
||||
);
|
||||
assert!(failure_record.get("error").is_none());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_final_reply_failure_keeps_private_conversation_and_hashes_public_audits() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "最终回复公共失败审计测试")
|
||||
.expect("project init");
|
||||
let planning_response = serde_json::json!({
|
||||
"thinkingSummary": "已有上下文足够,准备生成最终回复",
|
||||
"plan": ["回复开发者"],
|
||||
"actions": [],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_tool_plan_then_invalid_final_reply(planning_response);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "design-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "design-runtime-model",
|
||||
"apiKind": "openai_responses",
|
||||
"maxRetries": 0
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let run_id = "background-final-reply-public-failure-audit-run";
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"验证最终回复失败的私有诊断和公共审计边界",
|
||||
run_id,
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read initial runtime")
|
||||
.state;
|
||||
for _ in 0..250 {
|
||||
if runtime.status == "failed" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read failed runtime")
|
||||
.state;
|
||||
}
|
||||
assert_eq!(runtime.status, "failed");
|
||||
assert_eq!(runtime.phase, "failed");
|
||||
let private_error = runtime.error.clone().expect("private runtime error");
|
||||
assert!(!private_error.trim().is_empty());
|
||||
|
||||
let error_sha256 = format!("{:x}", Sha256::digest(private_error.as_bytes()));
|
||||
let error_chars = private_error.chars().count();
|
||||
let expected_public_detail = format!("errorSha256={error_sha256} · errorChars={error_chars}");
|
||||
let result = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read public failure projections");
|
||||
for event_type in ["error", "turn.failed"] {
|
||||
let event = result
|
||||
.recent_events
|
||||
.iter()
|
||||
.find(|event| event.run_id == run_id && event.event_type == event_type)
|
||||
.expect("public failure event");
|
||||
assert_eq!(
|
||||
event.detail.as_deref(),
|
||||
Some(expected_public_detail.as_str())
|
||||
);
|
||||
}
|
||||
let event_log = fs::read_to_string(game_creator_agent_runtime_event_path(
|
||||
&root,
|
||||
"design-director",
|
||||
))
|
||||
.expect("read public event log");
|
||||
assert!(!event_log.contains(&private_error));
|
||||
|
||||
let failure_records = read_agent_db_records_for_test(&root)
|
||||
.into_iter()
|
||||
.filter(|record| {
|
||||
record.get("recordType").and_then(Value::as_str)
|
||||
== Some("agent.runtime.background_task.failed")
|
||||
&& record.get("runId").and_then(Value::as_str) == Some(run_id)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(failure_records.len(), 1);
|
||||
let failure_record = &failure_records[0];
|
||||
assert_eq!(failure_record["failureKind"], "final-reply-failed");
|
||||
assert_eq!(failure_record["errorSha256"], error_sha256);
|
||||
assert_eq!(
|
||||
failure_record["errorChars"].as_u64(),
|
||||
Some(error_chars as u64)
|
||||
);
|
||||
assert!(failure_record.get("error").is_none());
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("read agent db");
|
||||
assert!(!agent_db.contains(&private_error));
|
||||
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some("design-director"),
|
||||
Some(&runtime.session_id),
|
||||
)
|
||||
.expect("read private failure conversation");
|
||||
assert!(conversation.messages.iter().any(|message| {
|
||||
message.role == "assistant" && message.content == format!("后台任务失败:{private_error}")
|
||||
}));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_plan_failure_and_budget_preserve_last_trusted_progress() {
|
||||
for budget_exhausted in [false, true] {
|
||||
|
||||
@@ -5234,3 +5234,21 @@
|
||||
- 后续收口:`App.tsx` 从 `12983` 行降到 `9794` 行,项目工作区下沉到 `src/features/project-workspace/` 的 11 个模块;`runtime_actions.rs` 从 `12519` 行降到 `139` 行,拆为 19 个生产模块和 2 个测试模块;`runtime_driver.rs` 从 `10510` 行降到 `394` 行并拆为 11 个模块;`runtime_protocol.rs` 从 `8623` 行降到 `105` 行并拆为 14 个模块,单模块不超过 `1319` 行。`runtime_driver/main_loop.rs` 仍约 `2995` 行,因为它承载现有单一主循环函数;后续必须先按运行状态阶段建立边界再拆分,不能继续机械切割。
|
||||
- Rust 可见性与兼容性:嵌套子模块会改变 `pub(super)` 的直接父级语义。`runtime_tools` 中原本要供 `crate::agent` 兄弟模块使用的符号最小化调整为 `pub(in crate::agent)`;同一 facade 下跨子模块 helper 保持直接父级 `pub(super)`,`runner` 和 `process_session` 的内部兄弟调用仍经父模块 facade,不扩大到 crate 公共 API。新增 `runtime_protocol::provider_retry` 后,访问 crate 根同名模块必须写为 `crate::provider_retry`;原 facade 的兼容重导出继续保留,编译器仅因当前文件未直接消费而告警时使用局部 `#[allow(unused_imports)]`,不得机械删除。
|
||||
- 验收:稳定共享树的客户端 `cargo fmt --check`、typecheck、Prettier、ESLint 和编码检查通过;Rust 串行全量 `1139 passed / 5 ignored / 0 failed`,客户端前端 `329/329` 通过。确定性 E2E self-test 与完整 E2E 均通过;完整链路继续满足 17 次 Provider lifecycle、revision `0 -> 2`、真实浏览器 `37/37`,重复、残留和泄漏均为 `0`。
|
||||
|
||||
## 2026-07-22 AI 游戏创作自主构建完成后由 Supervisor 确定性收束回复
|
||||
|
||||
- 背景:最新真实外部 E2E 已推进到 revision 5,最终浏览器试玩 `37/37`、Supervisor 计划 `8/8`;`image.inspect` 视觉请求与最终回复却先后命中同一 deserialize fingerprint。终局 `114` 个 Provider request identity 中 `113 completed / 1 final-reply failed`,因没有 Supervisor assistant,整轮仍是 **FAIL**,不得记为外部 Provider 全链路 PASS。
|
||||
- 决策:确定性最终回复只适用于 `autonomous-game-build` profile、规范 Agent `project-supervisor`,并且当前 revision 的 completion gates 已全部通过之后发生的 `final-reply` 收束。优先使用非空 `plan.response`;只有它为空时,才生成“当前 revision 已完成生成并通过静态、桌面和移动试玩”的确定性回复。
|
||||
- 失败关闭:普通 Agent、尚未收敛的自主构建、任一完成门禁未通过或存在 reconciliation 时,继续沿用原失败路径,不得生成成功回复。该兜底不放宽工具、协作、验证、试玩或恢复门禁,也不从中间 planning 或失败 observation 推断项目已完成。
|
||||
- 证据边界:Provider lifecycle 必须保留真实 final-reply failed identity、fingerprint 和终态,不得为了写 assistant 把失败请求改成 completed、隐藏或重编号。兜底只解决已完成项目缺少用户收束的问题,不能成为 Provider 成功证据。
|
||||
- 验证:代码修复完成后必须重新运行独立真实外部 E2E,并在同一轮核对当前 revision、completion gates、唯一 Supervisor assistant、Provider lifecycle、残留、重复与泄漏。新一轮完整通过前,外部 Provider 全链路状态继续记为未 PASS。
|
||||
- 最新真实轮次:新 fallback 已命中,父 Supervisor 终局为 `idle / completed`,`turn.report` 为 `settled`,只产生 `1` 条 `44` 字符的 Supervisor assistant;pending、retry、handoff、finalization、reconciliation、重复、API Key 和路径泄漏均为 `0`。因此“完成后不回复”已在该轮解决。
|
||||
- 轮次结论:该轮仍是 **FAIL**,不能记为 PASS。`105` 个 Provider identity 中 `103 completed / 2 failed`;两个原始专业 Agent 失败均已由 repair 恢复,但最终验收命中 `supervisor-swarm-private-body-public-event-leak`。
|
||||
- 脱敏定位:两个专业 Agent 的失败正文分别为 `149 / 123` 字符,对应 SHA-256 前缀 `494ce8 / 3089ad`,共进入 `4` 条 `event.detail` 和 `2` 条 `agent.runtime.background_task.failed.error`。六处内容均属于 delivery result,不是 userTask、委派任务或对话正文,与 final-reply fallback 无直接关系。
|
||||
- 修复原则:私有 `state.error` 和私有 delivery 保留诊断正文;公共 event 与 agentDb 只写 `errorSha256 / errorChars /` 稳定 `failureKind`。不得依赖正文黑名单,也不得为通过验收把真实失败改写为成功。
|
||||
- 后续验收:完成上述公共投影脱敏后,必须另起一轮独立真实外部 E2E;在该轮完整通过前,当前外部 Provider 全链路状态仍为未 PASS。
|
||||
- 最终独立真实外部轮次:公共投影脱敏修复后另起的新轮次独立取得完整证据,`status=PASS`、`evidence=complete`、`privacy scan=complete`。上述 `114` identity 与 `105` identity 两个 **FAIL** 继续保留为独立历史失败,不与本轮拼接;最终 PASS 是单个新轮次的完整证据,当前外部 Provider 全链路状态据此更新为 **PASS**。
|
||||
- Provider 与任务终态:本轮共有 `84` 个 Provider identity,`started / terminal / completed` 均为 `84`,`failed / retry / open / duplicate` 均为 `0`。`1` 个原专业任务以 `budget-exhausted` 终止,唯一 repair 已 `completed` 并标记 `recovered`;最终 child 为 `2 completed + 1 historical failed`,所有任务均处于终态。
|
||||
- 父级收束:父 Supervisor 为 `idle / completed`,`turn.report` 为 `settled`;唯一 Supervisor assistant 为 `297` 字符,`completed audit=1`,finalization 完成 `4` 个 stages。
|
||||
- 项目与试玩:revision 从 `0 -> 4`,`game/index.html` 为 `7639` bytes 且内容已变化,`game.static_smoke` passed;`lane-defense-v1` 的 desktop / mobile 浏览器验证均通过,固定试玩为 `37/37`。
|
||||
- 零值、隐私与清理:pending / confirmation / user-input / provider batch / retry / handoff / tool-plan handoff / finalization 残留 / reconciliation / duplicate 全为 `0`;Provider payload / private body / API Key / project path / config path / log / browser report leak 全为 `0`;人工 approve / answer / steer 全为 `0`。Runner 与 AppData 已清理,项目因 `--keep-project` 暂留后由主线程清理。
|
||||
|
||||
@@ -3512,3 +3512,19 @@
|
||||
- 原因:多个 Agent 虽然拥有互不重叠的写入文件,但全 crate 编译会同时读取所有模块。某个入口刚写入 `mod`、对应子文件尚未全部落盘时启动构建,会读到合法的中间态半成品。
|
||||
- 处理:并行 Agent 只做各自 scoped 格式和测试;主 Agent 等所有写入方正式完成并关闭后,再在稳定共享树统一运行 crate fmt、全量测试和真实 E2E。编译前失败且 `stdin/provider/task=0` 的轮次只能算 harness 准备失败,不能归因给 Runtime 行为。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/runner.rs`、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/`。
|
||||
|
||||
## 确定性最终回复不能掩盖外部 Provider 的真实失败
|
||||
|
||||
- 现象:自主构建已经推进到 revision 5,浏览器试玩 `37/37`、Supervisor 计划 `8/8`,但 `image.inspect` 视觉请求与最终回复命中同一 deserialize fingerprint;`114` 个 Provider request identity 中仍有 `1` 个 final-reply failed,且没有 Supervisor assistant。只看项目已完成或后续确定性回复,容易把这轮误写成 PASS。
|
||||
- 原因:项目 completion gates、用户是否收到收束回复和 Provider lifecycle 是否全成功是三个独立事实。确定性回复可以补齐已完成项目的用户出口,但不能反向证明失败的 Provider 请求成功,也不能覆盖失败 identity。
|
||||
- 处理:兜底条件必须同时锁定 `autonomous-game-build`、`project-supervisor`、当前 revision completion gates 全通过和 final-reply 阶段;优先使用非空 `plan.response`,为空时才生成当前 revision 已完成生成并通过静态、桌面和移动试玩的固定回复。普通 Agent、未收敛、门禁未通过或 reconciliation 一律继续失败关闭,并原样保留 Provider failed lifecycle 证据。
|
||||
- 验证:把已有外部轮次继续标记为 **FAIL**。修复后另起独立真实外部 E2E,在同一轮同时证明唯一 Supervisor assistant、完成门禁、Provider lifecycle、零残留、零重复和零泄漏;复验完成前不得宣称外部 Provider 全链路 PASS,也不得与旧失败轮拼接。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/`、`apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e/`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
- 最新复验:新 fallback 已真实命中,父 Supervisor 为 `idle / completed`,`turn.report` 为 `settled`,唯一 assistant 为 `44` 字符;pending、retry、handoff、finalization、reconciliation、重复、API Key 和路径泄漏均为 `0`。“完成后不回复”已解决,但该轮仍是 **FAIL**:`105` 个 Provider identity 只有 `103 completed / 2 failed`,两个原始专业 Agent 失败虽均由 repair 恢复,最终仍因 `supervisor-swarm-private-body-public-event-leak` 未通过。
|
||||
- 泄漏定位:两个专业 Agent 的 `149 / 123` 字符失败正文,分别对应 SHA-256 前缀 `494ce8 / 3089ad`,进入 `4` 条 `event.detail` 和 `2` 条 `agent.runtime.background_task.failed.error`。这些内容是 delivery result,不是 userTask、委派任务或对话正文;不要把该问题误归因到 final-reply fallback。
|
||||
- 脱敏处理:私有 `state.error` 与私有 delivery 应保留完整诊断;公共 event / agentDb 只投影 `errorSha256 / errorChars /` 稳定 `failureKind`。禁止按正文黑名单打补丁,也禁止把失败状态伪装为成功来消除泄漏。
|
||||
- 复验要求:公共投影修复后必须另起独立真实外部 E2E,重新核对同轮 Provider lifecycle、唯一回复、残留、重复和泄漏;当前仍未 PASS。
|
||||
- 最终复验:修复后另起的独立真实外部轮次已取得 `status=PASS`、`evidence=complete`、`privacy scan=complete`。此前 `114` identity 和 `105` identity 两个 **FAIL** 仍是各自独立的历史失败,未与本轮拼接;最终 PASS 仅由这个单个新轮次的完整证据构成,当前状态现已 **PASS**。
|
||||
- Provider 与恢复证据:`84` 个 Provider identity 的 `started / terminal / completed` 均为 `84`,`failed / retry / open / duplicate` 均为 `0`。`1` 个原专业任务为 `budget-exhausted`,唯一 repair `completed` 且 `recovered`;最终 child 为 `2 completed + 1 historical failed`,全部任务均已终态。
|
||||
- 收束与项目证据:父 Supervisor 为 `idle / completed`,`turn.report=settled`,唯一 assistant 为 `297` 字符,`completed audit=1`,finalization stages 为 `4`。项目 revision `0 -> 4`,`game/index.html` 为 `7639` bytes 且已变化,static smoke passed;`lane-defense-v1` 的 desktop / mobile 浏览器验证均通过并取得 `37/37`。
|
||||
- 零值与清理证据:pending / confirmation / user-input / provider batch / retry / handoff / tool-plan handoff / finalization 残留 / reconciliation / duplicate 全为 `0`;Provider payload / private body / API Key / project path / config path / log / browser report leak 全为 `0`;人工 approve / answer / steer 全为 `0`。Runner 与 AppData 已清,项目因 `--keep-project` 暂留后由主线程清理。
|
||||
|
||||
@@ -66,6 +66,22 @@ V1.11 的受保护仓库控制目录同时包含 `.git / .agent / .agents / .cod
|
||||
|
||||
2026-07-22 补充:自主构建的最终 `preview.validate` 在当前 revision 失败后,修复责任必须继续服从 Project Supervisor 的只编排边界。父 run 尚无协作事实时可沿用总控直接修复兼容路径;一旦 durable 协作事实已建立且 `orchestratorOnlyAfterDelegation=true`,活性门不得再强迫 Supervisor 调用 `file.write / file.patch / project.patchset`。没有 ready 回执且委派容量未满时,可向 `code-prototype` 创建 `repairOfDelegationId=null / runId=null` 的新后续修复任务,并把最新浏览器诊断和 `game/index.html` 验收产物写入合同;已有 ready 未认领回执或 active delivery 已达 3 个时,原生工具目录必须只保留 `agent.run_status`,先原子认领既有交付,不得创建第四次委派。专业 Agent 推进到更高 revision 后,固定顺序为“认领 ready delivery -> 取得当前 revision 的静态通过凭证 -> 父 Supervisor 重跑固定试玩”。每个固定 `data-playtest-id` 在对应动作发生时必须恰好匹配一个可见、启用且真实可点击的 HTMLElement;缺失、重复、隐藏或 disabled 都失败关闭。失败 revision、专业修改、总控复验与最终试玩之间不得用伪造 mutation 衔接。
|
||||
|
||||
2026-07-22 外部自主构建 E2E 的最新失败轮已推进到 revision 5,最终浏览器试玩 `37/37`、Supervisor 持久计划 `8/8`;但 `image.inspect` 视觉请求与最终回复先后命中同一 deserialize fingerprint。终局 `114` 个 Provider request identity 中 `113 completed / 1 final-reply failed`,没有产生 Supervisor assistant,整轮因此仍为 **FAIL**,不得写成外部 Provider 全链路 PASS。
|
||||
|
||||
该问题的收束修复严格限制为 `autonomous-game-build + project-supervisor + completion gates 已通过` 后的 `final-reply`。优先使用非空 `plan.response`;为空时才生成确定性回复,明确当前 revision 已完成生成并通过静态、桌面和移动试玩。普通 Agent、尚未收敛、任一完成门禁未通过或存在 reconciliation 时继续失败关闭。Provider lifecycle 中真实的 final-reply 失败证据必须保留,兜底只保证已完成项目能向用户收束,不把失败请求改写为 completed。修复后必须重新运行一轮独立真实外部 E2E;该轮完成前不能宣称外部 Provider 全链路 PASS。
|
||||
|
||||
2026-07-22 修复后的最新独立真实外部轮次确认新 fallback 已命中:父 Supervisor 终局为 `idle / completed`,`turn.report` 为 `settled`,只产生 `1` 条 `44` 字符的 Supervisor assistant;pending、retry、handoff、finalization、reconciliation、重复、API Key 和路径泄漏均为 `0`。因此“完成后不回复”已在该轮解决。
|
||||
|
||||
该轮整体仍为 **FAIL**,不能称为 PASS:`105` 个 Provider identity 中 `103 completed / 2 failed`;两个原始专业 Agent 失败均由 repair 恢复,最终验收却命中 `supervisor-swarm-private-body-public-event-leak`。脱敏定位共 `6` 处:两个专业 Agent 的失败正文分别为 `149 / 123` 字符、对应 SHA-256 前缀 `494ce8 / 3089ad`,进入 `4` 条 `event.detail` 和 `2` 条 `agent.runtime.background_task.failed.error`;这些内容均属于 delivery result,不是 userTask、委派任务或对话正文,与 final-reply fallback 无直接关系。
|
||||
|
||||
修复必须保留私有 `state.error` 和私有 delivery 的诊断正文,公共 event / agentDb 只写 `errorSha256 / errorChars /` 稳定 `failureKind`;禁止采用正文黑名单,也禁止把真实失败改写为成功。修复后必须另起独立真实外部 E2E,当前外部 Provider 全链路仍未 PASS。
|
||||
|
||||
2026-07-22 最终独立真实外部轮次已完整 **PASS**:`status=PASS`、`evidence=complete`、`privacy scan=complete`。本轮共有 `84` 个 Provider identity,`started / terminal / completed` 均为 `84`,`failed / retry / open / duplicate` 均为 `0`;`1` 个原专业任务以 `budget-exhausted` 终止,唯一 repair 已 `completed` 并 `recovered`,最终 child 为 `2 completed + 1 historical failed`,所有任务均处于终态。
|
||||
|
||||
父 Supervisor 最终为 `idle / completed`,`turn.report` 为 `settled`,只产生 `1` 条 `297` 字符的 Supervisor assistant,`completed audit=1`,finalization 完成 `4` 个 stages。项目 revision 从 `0 -> 4`,`game/index.html` 为 `7639` bytes 且内容已变化,`game.static_smoke` passed;`lane-defense-v1` 的 desktop / mobile 浏览器验证均通过,固定试玩为 `37/37`。
|
||||
|
||||
终局 pending / confirmation / user-input / provider batch / retry / handoff / tool-plan handoff / finalization 残留 / reconciliation / duplicate 全为 `0`;Provider payload / private body / API Key / project path / config path / log / browser report leak 全为 `0`;人工 approve / answer / steer 全为 `0`。Runner 与 AppData 已清理,项目因 `--keep-project` 暂留后由主线程清理。此前 `114` identity 与 `105` identity 两个 **FAIL** 继续保留为独立历史失败,证据未与本轮拼接;最终 PASS 是这个单个新轮次的完整证据,当前外部 Provider 全链路状态现已 **PASS**。
|
||||
|
||||
2026-07-15 起,Runtime V1.1 文档的“V1.17 单 Agent 持久计划”作为后台工具规划进度的新事实源。`submit_agent_tool_plan` 新增 nullable `planUpdate={explanation,steps[{step,status}]}`;步骤只接受 `pending / in_progress / completed`,最多 8 步且至多一个 `in_progress`。结构化计划一旦建立,legacy `plan` 只作旧协议 fallback;终态步骤必须保留,`planRevision` 只在真实变化时单调递增,工具 action 下标不得自动完成结构化步骤,存在未完成步骤时不得写最终回复或 completed。
|
||||
|
||||
V1.17 计划快照随 `game-creator-runtime-context-bundle.v3` 持久化,v2 在通过原身份、revision 和 verification gate 校验后从当前 Runtime state 补齐计划字段继续恢复;计划元数据本身不推进项目 revision、不改变 verification gate,也不触发项目权限确认。开发 UI 和 CLI 有界展示 revision、说明与完整 8 步;正式用户的 Supervisor 只展示完成数、当前步骤、等待对象、下一步和协作数量的紧凑摘要。恢复、same-run steer 和真实 Provider 的完整验收矩阵以 Runtime V1.17 章节为准;2026-07-16 已在当前 v5 context 上完成正式 `openai_chat / gpt-5.5` 的同 run steer + Runner 强杀恢复专项,门禁状态为 PASS。
|
||||
|
||||
Reference in New Issue
Block a user