修复自主构建收束回复与公共失败审计

为已通过完成门禁的自主构建总控补充确定性最终回复兜底。

保留 Provider 失败生命周期并沿用回复流与 finalization 幂等提交。

将公共失败事件和 Agent DB 审计改为哈希、长度与稳定分类。

补充私有诊断隔离回归、真实外部 E2E 证据与项目文档。
This commit is contained in:
AIGameCreator App
2026-07-22 17:48:00 +08:00
parent 759951ab12
commit e142ef5eac
10 changed files with 845 additions and 54 deletions
@@ -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] {