修复自主构建收束回复与公共失败审计
为已通过完成门禁的自主构建总控补充确定性最终回复兜底。 保留 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] {
|
||||
|
||||
Reference in New Issue
Block a user