合并主分支
合入主分支的智能体根任务可观测性、队列恢复与前端状态更新。 保留角色动作正式序列、后台预览换签和画布导出修复。 完成合并树验证并保持现有契约测试。
This commit is contained in:
@@ -156,6 +156,13 @@ pub(crate) fn render_local_conversation_prompt_context_for_session(
|
||||
agent_label: &str,
|
||||
) {
|
||||
for message in conversation.messages {
|
||||
if message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id
|
||||
.trim()
|
||||
.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let content = sanitize_prompt_context(&message.content)
|
||||
.split_whitespace()
|
||||
.collect::<Vec<_>>()
|
||||
|
||||
@@ -240,7 +240,7 @@ pub(in crate::agent) use pending_execution::*;
|
||||
pub(in crate::agent) use pending_recovery::*;
|
||||
pub(in crate::agent) use provider_recovery::*;
|
||||
pub(in crate::agent) use recovery_scan::*;
|
||||
pub(in crate::agent) use task_queue::*;
|
||||
pub(crate) use task_queue::*;
|
||||
pub(in crate::agent) use task_start::*;
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -353,10 +353,139 @@ fn latest_game_chat_deadline_runtime_at(
|
||||
.unwrap_or(fallback)
|
||||
}
|
||||
|
||||
struct GameChatAbsoluteDeadlinePublicStates {
|
||||
root: AgentRuntimeState,
|
||||
child: Option<AgentRuntimeState>,
|
||||
}
|
||||
|
||||
fn resolve_game_chat_absolute_deadline_public_states_at(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
phase: &str,
|
||||
) -> Result<GameChatAbsoluteDeadlinePublicStates, String> {
|
||||
let binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 绝对硬截止缺少当前 Run Profile 绑定".to_string())?;
|
||||
if binding.agent_id != runtime.agent_id
|
||||
|| binding.run_id != runtime.run_id
|
||||
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| binding.root_run_id.trim().is_empty()
|
||||
|| binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
{
|
||||
return Err("game-chat 绝对硬截止当前 Run Profile 绑定身份不一致".to_string());
|
||||
}
|
||||
let root_binding = read_game_creator_agent_runtime_run_profile_binding(
|
||||
root,
|
||||
&binding.root_agent_id,
|
||||
&binding.root_run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 绝对硬截止缺少根 Run Profile 绑定".to_string())?;
|
||||
if root_binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| root_binding.run_id != binding.root_run_id
|
||||
|| root_binding.root_agent_id != root_binding.agent_id
|
||||
|| root_binding.root_run_id != root_binding.run_id
|
||||
|| root_binding.parent_agent_id.is_some()
|
||||
|| root_binding.parent_run_id.is_some()
|
||||
|| root_binding.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|
||||
|| root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|
||||
{
|
||||
return Err("game-chat 绝对硬截止根 Run Profile 绑定身份不一致".to_string());
|
||||
}
|
||||
let root_task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_binding.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 绝对硬截止缺少根 Supervisor 任务".to_string())?;
|
||||
if root_task.parent_agent_id.is_some()
|
||||
|| root_task.parent_run_id.is_some()
|
||||
|| root_task.session_id.trim().is_empty()
|
||||
|| root_task.run_profile_binding_fingerprint != root_binding.binding_fingerprint
|
||||
{
|
||||
return Err("game-chat 绝对硬截止根 Supervisor 任务身份不一致".to_string());
|
||||
}
|
||||
let mut root_state = agent_runtime_state_from_task_record(&root_task);
|
||||
root_state.status = "failed".to_string();
|
||||
root_state.phase = phase.to_string();
|
||||
|
||||
if runtime.agent_id == root_state.agent_id && runtime.run_id == root_state.run_id {
|
||||
if runtime.session_id != root_state.session_id
|
||||
|| runtime.parent_agent_id.is_some()
|
||||
|| runtime.parent_run_id.is_some()
|
||||
|| binding.parent_agent_id.is_some()
|
||||
|| binding.parent_run_id.is_some()
|
||||
|| runtime.run_profile_binding_fingerprint != root_binding.binding_fingerprint
|
||||
{
|
||||
return Err("game-chat 绝对硬截止当前根 Runtime 身份不一致".to_string());
|
||||
}
|
||||
return Ok(GameChatAbsoluteDeadlinePublicStates {
|
||||
root: root_state,
|
||||
child: None,
|
||||
});
|
||||
}
|
||||
|
||||
if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| runtime.session_id.trim().is_empty()
|
||||
|| runtime.parent_agent_id.as_deref() != Some(root_state.agent_id.as_str())
|
||||
|| runtime.parent_run_id.as_deref() != Some(root_state.run_id.as_str())
|
||||
|| binding.parent_agent_id.as_deref() != Some(root_state.agent_id.as_str())
|
||||
|| binding.parent_run_id.as_deref() != Some(root_state.run_id.as_str())
|
||||
|| runtime.run_profile_binding_fingerprint != binding.binding_fingerprint
|
||||
{
|
||||
return Err("game-chat 绝对硬截止当前专业 Agent Runtime 身份不一致".to_string());
|
||||
}
|
||||
let child_task = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
root,
|
||||
&runtime.agent_id,
|
||||
&runtime.run_id,
|
||||
)?
|
||||
.ok_or_else(|| "game-chat 绝对硬截止缺少当前专业 Agent 任务".to_string())?;
|
||||
if child_task.session_id != runtime.session_id
|
||||
|| child_task.parent_agent_id != runtime.parent_agent_id
|
||||
|| child_task.parent_run_id != runtime.parent_run_id
|
||||
|| child_task.run_profile_binding_fingerprint != binding.binding_fingerprint
|
||||
{
|
||||
return Err("game-chat 绝对硬截止当前专业 Agent 任务身份不一致".to_string());
|
||||
}
|
||||
|
||||
let mut child_state = agent_runtime_state_from_task_record(&child_task);
|
||||
child_state.status = "failed".to_string();
|
||||
child_state.phase = phase.to_string();
|
||||
Ok(GameChatAbsoluteDeadlinePublicStates {
|
||||
root: root_state,
|
||||
child: Some(child_state),
|
||||
})
|
||||
}
|
||||
|
||||
fn append_resolved_game_chat_absolute_deadline_public_messages_at(
|
||||
root: &Path,
|
||||
states: &GameChatAbsoluteDeadlinePublicStates,
|
||||
error: &str,
|
||||
) -> Result<(), String> {
|
||||
append_game_creator_agent_runtime_terminal_public_message_at(root, &states.root, error)?;
|
||||
if let Some(child) = states.child.as_ref() {
|
||||
append_game_creator_agent_runtime_terminal_public_message_at(root, child, error)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn append_game_chat_absolute_deadline_public_messages_at(
|
||||
root: &Path,
|
||||
runtime: &AgentRuntimeState,
|
||||
phase: &str,
|
||||
error: &str,
|
||||
) -> Result<(), String> {
|
||||
let states = resolve_game_chat_absolute_deadline_public_states_at(root, runtime, phase)?;
|
||||
append_resolved_game_chat_absolute_deadline_public_messages_at(root, &states, error)
|
||||
}
|
||||
|
||||
pub(super) fn finish_game_chat_absolute_deadline_timeout_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
_agent_id: &str,
|
||||
_session_id: &str,
|
||||
fallback: AgentRuntimeState,
|
||||
) -> AgentBackgroundTaskOutcome {
|
||||
let mut runtime = latest_game_chat_deadline_runtime_at(root, fallback);
|
||||
@@ -387,6 +516,31 @@ pub(super) fn finish_game_chat_absolute_deadline_timeout_at(
|
||||
GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_SECONDS
|
||||
)
|
||||
};
|
||||
// The absolute deadline may itself be handling a broken Runtime state
|
||||
// projection. Commit the stable user-facing terminal outcome before any
|
||||
// reconciliation/failure projection, then retry the same idempotent
|
||||
// message after cleanup below in case the first conversation write was
|
||||
// transient.
|
||||
let public_terminal_phase = if preserves_external_reconciliation {
|
||||
"needs-reconciliation"
|
||||
} else {
|
||||
"failed"
|
||||
};
|
||||
let public_states = match resolve_game_chat_absolute_deadline_public_states_at(
|
||||
root,
|
||||
&runtime,
|
||||
public_terminal_phase,
|
||||
) {
|
||||
Ok(states) => states,
|
||||
Err(_) => return AgentBackgroundTaskOutcome::Finished,
|
||||
};
|
||||
let initial_public_status_error =
|
||||
append_resolved_game_chat_absolute_deadline_public_messages_at(
|
||||
root,
|
||||
&public_states,
|
||||
&error,
|
||||
)
|
||||
.err();
|
||||
let terminal_failure_error = if let Some(pending) = pending_action
|
||||
.as_ref()
|
||||
.filter(|_| preserves_external_reconciliation)
|
||||
@@ -448,6 +602,9 @@ pub(super) fn finish_game_chat_absolute_deadline_timeout_at(
|
||||
if let Some(error) = terminal_failure_error {
|
||||
cleanup_errors.push(sanitize_agent_runtime_text(&error, 160));
|
||||
}
|
||||
if let Some(error) = initial_public_status_error {
|
||||
cleanup_errors.push(sanitize_agent_runtime_text(&error, 160));
|
||||
}
|
||||
let _ = append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
@@ -465,15 +622,10 @@ pub(super) fn finish_game_chat_absolute_deadline_timeout_at(
|
||||
"cleanupErrorCount": cleanup_errors.len(),
|
||||
}),
|
||||
);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
let _ = append_resolved_game_chat_absolute_deadline_public_messages_at(
|
||||
root,
|
||||
Some(agent_id),
|
||||
Some(session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(agent_id, &error),
|
||||
agent_id: None,
|
||||
},
|
||||
&public_states,
|
||||
&error,
|
||||
);
|
||||
AgentBackgroundTaskOutcome::Finished
|
||||
}
|
||||
@@ -1168,19 +1320,24 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
}
|
||||
if error.starts_with(GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX) {
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let public_runtime = runtime.clone();
|
||||
let public_states =
|
||||
match resolve_game_chat_absolute_deadline_public_states_at(
|
||||
&root,
|
||||
&public_runtime,
|
||||
"budget-exhausted",
|
||||
) {
|
||||
Ok(states) => states,
|
||||
Err(_) => return AgentBackgroundTaskOutcome::Finished,
|
||||
};
|
||||
let initial_public_status =
|
||||
append_resolved_game_chat_absolute_deadline_public_messages_at(
|
||||
&root,
|
||||
&public_states,
|
||||
&error,
|
||||
);
|
||||
let failed_runtime =
|
||||
fail_game_creator_agent_runtime_budget_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: "首版素材生成已达到七十五分钟硬上限,本轮已停止,不会继续在后台运行。"
|
||||
.to_string(),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
@@ -1188,23 +1345,19 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
AGENT_RUNTIME_BACKGROUND_FAILURE_KIND_BUDGET,
|
||||
);
|
||||
}
|
||||
if initial_public_status.is_err() {
|
||||
let _ =
|
||||
append_resolved_game_chat_absolute_deadline_public_messages_at(
|
||||
&root,
|
||||
&public_states,
|
||||
&error,
|
||||
);
|
||||
}
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let failed_runtime =
|
||||
fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(
|
||||
&agent_id, &error,
|
||||
),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
@@ -2388,18 +2541,6 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
Err(error) => {
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(
|
||||
&agent_id, &error,
|
||||
),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
}
|
||||
@@ -2514,18 +2655,6 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
{
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(
|
||||
&agent_id, &error,
|
||||
),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
let pre_execution_drift =
|
||||
@@ -2779,18 +2908,6 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
{
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(
|
||||
&agent_id, &error,
|
||||
),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
if let Err(error) =
|
||||
@@ -3298,16 +3415,6 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
};
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let failed_runtime = fail_game_creator_agent_runtime_budget_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: format!("后台任务未完成:{error}"),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
@@ -3598,18 +3705,6 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
||||
let error = redact_agent_runtime_error(&root, &error, 500);
|
||||
let failed_runtime =
|
||||
fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
Some(&session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(
|
||||
&agent_id, &error,
|
||||
),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_game_creator_agent_background_task_failed_audit(
|
||||
&root,
|
||||
|
||||
+337
-7
@@ -1,6 +1,6 @@
|
||||
use super::main_loop::{
|
||||
await_game_chat_absolute_deadline_at, finish_game_chat_absolute_deadline_timeout_at,
|
||||
game_chat_absolute_deadline_from_bound_at,
|
||||
append_game_chat_absolute_deadline_public_messages_at, await_game_chat_absolute_deadline_at,
|
||||
finish_game_chat_absolute_deadline_timeout_at, game_chat_absolute_deadline_from_bound_at,
|
||||
};
|
||||
use super::*;
|
||||
|
||||
@@ -62,21 +62,248 @@ async fn game_chat_absolute_deadline_returns_an_in_flight_result_before_expiry()
|
||||
assert_eq!(result.expect("in-flight action completes"), "completed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_absolute_deadline_rejects_forged_root_runtime_identity() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-forged-root-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "deadline-forged-root", "硬截止伪根身份测试")
|
||||
.expect("project init");
|
||||
let root_session = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve game-chat root session");
|
||||
let root_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_session,
|
||||
"生成首版可玩游戏",
|
||||
"game-chat-deadline-forged-root-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue autonomous game-chat root");
|
||||
|
||||
let mut forged_session = agent_runtime_state_from_task_record(&root_record);
|
||||
forged_session.session_id = "forged-root-session".to_string();
|
||||
let error = append_game_chat_absolute_deadline_public_messages_at(
|
||||
&root,
|
||||
&forged_session,
|
||||
"failed",
|
||||
GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX,
|
||||
)
|
||||
.expect_err("forged root session must fail closed");
|
||||
assert!(error.contains("当前根 Runtime 身份不一致"));
|
||||
let outcome = finish_game_chat_absolute_deadline_timeout_at(
|
||||
&root,
|
||||
&forged_session.agent_id,
|
||||
&forged_session.session_id,
|
||||
forged_session.clone(),
|
||||
);
|
||||
assert!(matches!(outcome, AgentBackgroundTaskOutcome::Finished));
|
||||
let root_after_forged_finish = read_latest_game_creator_agent_runtime_task_by_run_id(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_record.run_id,
|
||||
)
|
||||
.expect("read authoritative root after forged finish")
|
||||
.expect("authoritative root task remains present");
|
||||
assert_eq!(root_after_forged_finish.session_id, root_session);
|
||||
assert_eq!(root_after_forged_finish.status, "pending");
|
||||
|
||||
let mut forged_parent = agent_runtime_state_from_task_record(&root_record);
|
||||
forged_parent.parent_run_id = Some("forged-parent-run".to_string());
|
||||
let error = append_game_chat_absolute_deadline_public_messages_at(
|
||||
&root,
|
||||
&forged_parent,
|
||||
"failed",
|
||||
GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX,
|
||||
)
|
||||
.expect_err("root runtime carrying a parent identity must fail closed");
|
||||
assert!(error.contains("当前根 Runtime 身份不一致"));
|
||||
|
||||
let child_session = resolve_agent_conversation_session_id_at(&root, "art-director", None, true)
|
||||
.expect("resolve child session");
|
||||
let child_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
"art-director",
|
||||
&child_session,
|
||||
"执行美术审计",
|
||||
"game-chat-deadline-forged-child-run",
|
||||
"agent-ready-task-scheduler",
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(root_record.agent_id.clone()),
|
||||
parent_run_id: Some(root_record.run_id.clone()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
.expect("queue autonomous game-chat child");
|
||||
let mut forged_child_session = agent_runtime_state_from_task_record(&child_record);
|
||||
forged_child_session.session_id = "forged-child-session".to_string();
|
||||
let error = append_game_chat_absolute_deadline_public_messages_at(
|
||||
&root,
|
||||
&forged_child_session,
|
||||
"failed",
|
||||
GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX,
|
||||
)
|
||||
.expect_err("child runtime with the wrong session must fail closed");
|
||||
assert!(error.contains("当前专业 Agent 任务身份不一致"));
|
||||
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read forged root conversation");
|
||||
assert_eq!(
|
||||
project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.count(),
|
||||
0,
|
||||
"identity conflicts must not create a second project terminal message"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn game_chat_absolute_deadline_deduplicates_root_terminal_across_children() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-multi-child-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "deadline-multi-child", "硬截止多 child 幂等测试")
|
||||
.expect("project init");
|
||||
let root_session = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve game-chat root session");
|
||||
let root_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_session,
|
||||
"生成首版可玩游戏",
|
||||
"game-chat-deadline-multi-child-root-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue autonomous game-chat root");
|
||||
let task_link = AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(root_record.agent_id.clone()),
|
||||
parent_run_id: Some(root_record.run_id.clone()),
|
||||
delegation_id: None,
|
||||
};
|
||||
let mut children = Vec::new();
|
||||
for (agent_id, run_id) in [
|
||||
("art-director", "game-chat-deadline-multi-child-art"),
|
||||
("code-prototype", "game-chat-deadline-multi-child-code"),
|
||||
] {
|
||||
let session_id = resolve_agent_conversation_session_id_at(&root, agent_id, None, true)
|
||||
.expect("resolve child session");
|
||||
let record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
agent_id,
|
||||
&session_id,
|
||||
"执行专业任务",
|
||||
run_id,
|
||||
"agent-ready-task-scheduler",
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
Some(&task_link),
|
||||
)
|
||||
.expect("queue autonomous game-chat child");
|
||||
children.push(agent_runtime_state_from_task_record(&record));
|
||||
}
|
||||
let private_error = format!(
|
||||
"{GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX}: operationId=secret-operation-id pendingAction=private-action"
|
||||
);
|
||||
for child in &children {
|
||||
append_game_chat_absolute_deadline_public_messages_at(
|
||||
&root,
|
||||
child,
|
||||
"needs-reconciliation",
|
||||
&private_error,
|
||||
)
|
||||
.expect("append idempotent root and child deadline terminals");
|
||||
}
|
||||
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read multi-child root conversation");
|
||||
let root_public_statuses = project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(root_public_statuses.len(), 1);
|
||||
assert_eq!(
|
||||
root_public_statuses[0].content,
|
||||
"项目总控 Agent 执行失败,请稍后重试"
|
||||
);
|
||||
assert!(!root_public_statuses[0]
|
||||
.content
|
||||
.contains("secret-operation-id"));
|
||||
assert!(!root_public_statuses[0].content.contains("pendingAction"));
|
||||
for child in &children {
|
||||
assert!(!root_public_statuses[0].content.contains(&child.session_id));
|
||||
assert!(!root_public_statuses[0].content.contains(&child.run_id));
|
||||
let child_conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(&child.agent_id),
|
||||
Some(&child.session_id),
|
||||
)
|
||||
.expect("read child deadline conversation");
|
||||
assert_eq!(
|
||||
child_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn game_chat_absolute_deadline_preserves_external_generation_for_same_action_resume() {
|
||||
let temporary = crate::tests::canonical_test_tempdir("game-chat-deadline-reconciliation-");
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "deadline-reconciliation", "硬截止收尾测试")
|
||||
.expect("project init");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
let root_session = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve game-chat root session");
|
||||
let root_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_session,
|
||||
"生成首版可玩游戏",
|
||||
"game-chat-deadline-reconciliation-root-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("bind autonomous game-chat root profile");
|
||||
.expect("queue autonomous game-chat root");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
"art-director",
|
||||
@@ -84,8 +311,8 @@ async fn game_chat_absolute_deadline_preserves_external_generation_for_same_acti
|
||||
"agent-ready-task-scheduler",
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()),
|
||||
parent_run_id: Some("game-chat-deadline-reconciliation-root-run".to_string()),
|
||||
parent_agent_id: Some(root_record.agent_id.clone()),
|
||||
parent_run_id: Some(root_record.run_id.clone()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
@@ -100,6 +327,8 @@ async fn game_chat_absolute_deadline_preserves_external_generation_for_same_acti
|
||||
vec!["执行外部图片生成".to_string()],
|
||||
)
|
||||
.expect("start runtime");
|
||||
runtime.parent_agent_id = Some(root_record.agent_id.clone());
|
||||
runtime.parent_run_id = Some(root_record.run_id.clone());
|
||||
runtime.loop_iteration = 1;
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "canvas.asset_generate".to_string(),
|
||||
@@ -227,6 +456,42 @@ async fn game_chat_absolute_deadline_preserves_external_generation_for_same_acti
|
||||
assert!(agent_db.contains("\"externalGenerationRecordPreserved\":true"));
|
||||
assert!(agent_db.contains("agent.runtime.tool_action.needs_reconciliation"));
|
||||
assert!(!agent_db.contains("test-operation-id"));
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(&runtime.agent_id),
|
||||
Some(&runtime.session_id),
|
||||
)
|
||||
.expect("read reconciliation deadline conversation");
|
||||
let public_statuses = conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(public_statuses.len(), 1);
|
||||
assert_eq!(
|
||||
public_statuses[0].content,
|
||||
"专业 Agent 执行失败,请稍后重试"
|
||||
);
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read root deadline public conversation");
|
||||
let root_public_statuses = project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(root_public_statuses.len(), 1);
|
||||
assert_eq!(
|
||||
root_public_statuses[0].content,
|
||||
"项目总控 Agent 执行失败,请稍后重试"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
@@ -237,6 +502,37 @@ fn game_chat_absolute_deadline_still_cleans_local_action_recovery() {
|
||||
let root = temporary.path().join("project");
|
||||
init_local_game_project_at(&root, "deadline-local-cleanup", "硬截止本地清理测试")
|
||||
.expect("project init");
|
||||
let root_session = resolve_agent_conversation_session_id_at(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.expect("resolve local deadline root session");
|
||||
let root_record = append_unique_game_creator_agent_runtime_pending_task(
|
||||
&root,
|
||||
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||
&root_session,
|
||||
"生成首版可玩游戏",
|
||||
"game-chat-deadline-local-root-run",
|
||||
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
None,
|
||||
)
|
||||
.expect("queue local deadline game-chat root");
|
||||
bind_game_creator_agent_runtime_run_profile_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
"game-chat-deadline-local-cleanup-run",
|
||||
"agent-ready-task-scheduler",
|
||||
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
|
||||
Some(&AgentRuntimeTaskLink {
|
||||
parent_agent_id: Some(root_record.agent_id.clone()),
|
||||
parent_run_id: Some(root_record.run_id.clone()),
|
||||
delegation_id: None,
|
||||
}),
|
||||
)
|
||||
.expect("bind local deadline child profile");
|
||||
let mut runtime = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"code-prototype",
|
||||
@@ -247,6 +543,8 @@ fn game_chat_absolute_deadline_still_cleans_local_action_recovery() {
|
||||
vec!["执行首版写入".to_string()],
|
||||
)
|
||||
.expect("start runtime");
|
||||
runtime.parent_agent_id = Some(root_record.agent_id.clone());
|
||||
runtime.parent_run_id = Some(root_record.run_id.clone());
|
||||
runtime.loop_iteration = 1;
|
||||
let action = AgentRuntimeToolAction {
|
||||
tool: "file.write".to_string(),
|
||||
@@ -311,6 +609,38 @@ fn game_chat_absolute_deadline_still_cleans_local_action_recovery() {
|
||||
));
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(agent_db.contains("\"reconciliationPreserved\":false"));
|
||||
let conversation = read_local_conversation_for_session_at(
|
||||
&root,
|
||||
Some(&runtime.agent_id),
|
||||
Some(&runtime.session_id),
|
||||
)
|
||||
.expect("read local deadline conversation");
|
||||
assert_eq!(
|
||||
conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
let project_conversation =
|
||||
read_local_conversation_at(&root, None).expect("read local root deadline conversation");
|
||||
assert_eq!(
|
||||
project_conversation
|
||||
.messages
|
||||
.iter()
|
||||
.filter(|message| {
|
||||
message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
})
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
@@ -9,17 +9,15 @@ pub(in crate::agent) fn game_creator_agent_background_task_default_plan() -> Vec
|
||||
]
|
||||
}
|
||||
|
||||
pub(in crate::agent) fn agent_runtime_background_task_message_id(
|
||||
pub(crate) fn agent_runtime_background_task_message_id(
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
run_id: &str,
|
||||
source: &str,
|
||||
_source: &str,
|
||||
) -> String {
|
||||
let identity = format!("{agent_id}\n{session_id}\n{run_id}\n{source}");
|
||||
let fingerprint = format!("{:x}", Sha256::digest(identity.as_bytes()));
|
||||
format!(
|
||||
"runtime-task-{}",
|
||||
fingerprint.chars().take(32).collect::<String>()
|
||||
game_creator_agent_runtime_message_correlation_id(agent_id, session_id, run_id)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -279,7 +277,7 @@ pub(crate) fn spawn_started_game_creator_agent_background_task_drain_with_lock(
|
||||
pub(in crate::agent) fn fail_game_creator_agent_background_context_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
_session_id: &str,
|
||||
runtime: AgentRuntimeState,
|
||||
error: &str,
|
||||
) -> AgentBackgroundTaskOutcome {
|
||||
@@ -295,16 +293,6 @@ pub(in crate::agent) fn fail_game_creator_agent_background_context_at(
|
||||
}
|
||||
let error = redact_agent_runtime_error(root, error, 500);
|
||||
let failed_runtime = fail_game_creator_agent_runtime_turn_at(root, runtime, &error);
|
||||
let _ = append_local_conversation_message_for_session_at(
|
||||
root,
|
||||
Some(agent_id),
|
||||
Some(session_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: game_creator_agent_runtime_failure_conversation_message(agent_id, &error),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
root,
|
||||
|
||||
@@ -278,7 +278,18 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_in_se
|
||||
.as_deref()
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
});
|
||||
let (pending_task, pending_task_created) = if is_static_supervisor_delegate {
|
||||
let requires_public_start_status = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
&& task_link
|
||||
.and_then(|link| link.parent_agent_id.as_deref())
|
||||
.is_none()
|
||||
&& task_link
|
||||
.and_then(|link| link.parent_run_id.as_deref())
|
||||
.is_none()
|
||||
&& !matches!(
|
||||
source,
|
||||
AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE | AGENT_RUNTIME_ISOLATED_JOIN_SOURCE
|
||||
);
|
||||
let (mut pending_task, pending_task_created) = if is_static_supervisor_delegate {
|
||||
append_or_read_exact_game_creator_agent_runtime_pending_task(
|
||||
root,
|
||||
&agent_id,
|
||||
@@ -291,16 +302,29 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_in_se
|
||||
)?
|
||||
} else {
|
||||
(
|
||||
append_unique_game_creator_agent_runtime_pending_task(
|
||||
root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
task,
|
||||
run_id,
|
||||
source,
|
||||
run_profile,
|
||||
task_link,
|
||||
)?,
|
||||
if requires_public_start_status {
|
||||
append_unique_game_creator_agent_runtime_public_status_preparing_task(
|
||||
root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
task,
|
||||
run_id,
|
||||
source,
|
||||
run_profile,
|
||||
task_link,
|
||||
)?
|
||||
} else {
|
||||
append_unique_game_creator_agent_runtime_pending_task(
|
||||
root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
task,
|
||||
run_id,
|
||||
source,
|
||||
run_profile,
|
||||
task_link,
|
||||
)?
|
||||
},
|
||||
true,
|
||||
)
|
||||
};
|
||||
@@ -332,7 +356,17 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_in_se
|
||||
)
|
||||
.map(|_| ())
|
||||
};
|
||||
if let Err(error) = conversation_result {
|
||||
let conversation_error = match conversation_result {
|
||||
Ok(()) => None,
|
||||
Err(error) => {
|
||||
match game_creator_agent_runtime_task_has_user_message_at(root, &pending_task) {
|
||||
Ok(true) => None,
|
||||
Ok(false) => Some(error),
|
||||
Err(identity_error) => Some(identity_error),
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(error) = conversation_error {
|
||||
let error = redact_agent_runtime_project_paths(root, &error, 500);
|
||||
let failed_task = AgentRuntimeTaskRecord {
|
||||
status: "failed".to_string(),
|
||||
@@ -359,6 +393,37 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_in_se
|
||||
return Err(format!("后台任务用户消息落盘失败,任务未执行:{error}"));
|
||||
}
|
||||
}
|
||||
if requires_public_start_status {
|
||||
if let Err(error) =
|
||||
ensure_game_creator_agent_runtime_accepted_public_status_at(root, &pending_task)
|
||||
{
|
||||
let error = redact_agent_runtime_project_paths(root, &error, 500);
|
||||
let terminal_result =
|
||||
fail_game_creator_agent_runtime_public_start_status_at(root, &pending_task, &error);
|
||||
let _ = append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.queue_warning",
|
||||
"agentId": pending_task.agent_id,
|
||||
"sessionId": pending_task.session_id,
|
||||
"runId": pending_task.run_id,
|
||||
"warningKind": "public-status-write-failed",
|
||||
"error": sanitize_agent_runtime_text(&error, 240),
|
||||
}),
|
||||
);
|
||||
terminal_result?;
|
||||
return Err(format!("后台任务启动确认落盘失败,任务未执行:{error}"));
|
||||
}
|
||||
let queued_task = AgentRuntimeTaskRecord {
|
||||
status: "pending".to_string(),
|
||||
phase: "queued".to_string(),
|
||||
current_action: "等待当前后台任务完成".to_string(),
|
||||
updated_at: unix_timestamp(),
|
||||
..pending_task.clone()
|
||||
};
|
||||
append_game_creator_agent_runtime_task_record(root, &queued_task)?;
|
||||
pending_task = queued_task;
|
||||
}
|
||||
if !pending_task_created && game_creator_agent_runtime_terminal_status(&pending_task).is_some()
|
||||
{
|
||||
return read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -44,7 +44,11 @@ pub(crate) fn game_creator_agent_runtime_terminal_status(
|
||||
"completed" => Some("completed"),
|
||||
"budget-exhausted" => Some("budget-exhausted"),
|
||||
"cancelled" => Some("cancelled"),
|
||||
"failed" | "conversation-write-failed" if task.status == "failed" => Some("failed"),
|
||||
"failed" | "conversation-write-failed" | "public-status-write-failed"
|
||||
if task.status == "failed" =>
|
||||
{
|
||||
Some("failed")
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +378,13 @@ fn render_conversation_messages(
|
||||
messages
|
||||
.iter()
|
||||
.filter_map(|message| {
|
||||
if message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id
|
||||
.trim()
|
||||
.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
}) {
|
||||
return None;
|
||||
}
|
||||
let content = normalize_conversation_content(&message.content);
|
||||
(!content.is_empty()).then(|| format!("- [{label} / {}] {content}", message.role))
|
||||
})
|
||||
|
||||
@@ -987,6 +987,29 @@ fn take_agent_db_record_failure_injection(
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn take_conversation_audit_failure_injection(root: &Path, message_id: &str) -> Result<(), String> {
|
||||
let failure_path = root.join(".agent/runtime/test-fail-next-agent-db-record");
|
||||
match fs::read_to_string(&failure_path) {
|
||||
Ok(expected) => {
|
||||
let expected = expected.trim();
|
||||
let Some(message_id_prefix) = expected.strip_prefix("conversation.message:") else {
|
||||
return Ok(());
|
||||
};
|
||||
if !message_id.starts_with(message_id_prefix) {
|
||||
return Ok(());
|
||||
}
|
||||
fs::remove_file(&failure_path)
|
||||
.map_err(|error| format!("清理 Agent DB 测试失败注入标记失败:{error}"))?;
|
||||
Err(format!(
|
||||
"测试注入 conversation.message 审计失败:{message_id_prefix}"
|
||||
))
|
||||
}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
|
||||
Err(error) => Err(format!("读取 Agent DB 测试失败注入标记失败:{error}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn append_agent_db_record_internal(root: &Path, record: serde_json::Value) -> Result<(), String> {
|
||||
#[cfg(test)]
|
||||
take_agent_db_record_failure_injection(
|
||||
@@ -4153,6 +4176,15 @@ pub(super) fn ensure_conversation_message_audit_at(
|
||||
)? {
|
||||
return Ok(());
|
||||
}
|
||||
#[cfg(test)]
|
||||
take_conversation_audit_failure_injection(root, message_id)?;
|
||||
#[cfg(test)]
|
||||
take_agent_db_record_failure_injection(
|
||||
root,
|
||||
audit_record
|
||||
.get("recordType")
|
||||
.and_then(serde_json::Value::as_str),
|
||||
)?;
|
||||
let append_class = agent_db_record_append_class(&audit_record);
|
||||
let line = serialize_agent_db_record(audit_record)?;
|
||||
validate_agent_db_append_class_record_size(append_class, &line)?;
|
||||
|
||||
@@ -1055,6 +1055,29 @@ pub(crate) fn read_local_conversation_message_by_id_for_session_at(
|
||||
agent_id: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
message_id: &str,
|
||||
) -> Result<Option<LocalConversationMessageRecord>, String> {
|
||||
read_local_conversation_message_by_id_for_session_internal_at(
|
||||
root, agent_id, session_id, message_id, true,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn read_local_conversation_message_by_id_for_session_without_touch_at(
|
||||
root: &Path,
|
||||
agent_id: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
message_id: &str,
|
||||
) -> Result<Option<LocalConversationMessageRecord>, String> {
|
||||
read_local_conversation_message_by_id_for_session_internal_at(
|
||||
root, agent_id, session_id, message_id, false,
|
||||
)
|
||||
}
|
||||
|
||||
fn read_local_conversation_message_by_id_for_session_internal_at(
|
||||
root: &Path,
|
||||
agent_id: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
message_id: &str,
|
||||
touch_session: bool,
|
||||
) -> Result<Option<LocalConversationMessageRecord>, String> {
|
||||
let message_id = normalize_local_conversation_message_id(message_id)?;
|
||||
let (path, normalized_agent_id, normalized_session_id) =
|
||||
@@ -1068,17 +1091,19 @@ pub(crate) fn read_local_conversation_message_by_id_for_session_at(
|
||||
normalized_session_id.as_deref(),
|
||||
&message_id,
|
||||
)?;
|
||||
if let (Some(agent_id), Some(session_id)) = (
|
||||
normalized_agent_id.as_deref(),
|
||||
normalized_session_id.as_deref(),
|
||||
) {
|
||||
touch_agent_conversation_session_at(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
records.len() as u64,
|
||||
false,
|
||||
)?;
|
||||
if touch_session {
|
||||
if let (Some(agent_id), Some(session_id)) = (
|
||||
normalized_agent_id.as_deref(),
|
||||
normalized_session_id.as_deref(),
|
||||
) {
|
||||
touch_agent_conversation_session_at(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
records.len() as u64,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Ok(matched_index.map(|index| records[index].to_public_record()))
|
||||
}
|
||||
|
||||
@@ -5578,9 +5578,10 @@ async fn background_agent_runtime_marks_unconverged_loop_budget_exhausted() {
|
||||
.expect("read budget conversation");
|
||||
assert!(conversation.messages.iter().any(|message| {
|
||||
message.role == "assistant"
|
||||
&& message
|
||||
.content
|
||||
.contains("后台任务未完成:loop-budget-exhausted")
|
||||
&& message.content == "专业 Agent 执行失败,请稍后重试"
|
||||
&& message.message_id.as_deref().is_some_and(|message_id| {
|
||||
message_id.starts_with(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX)
|
||||
})
|
||||
}));
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(agent_db.contains("\"failureKind\":\"loop-budget-exhausted\""));
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2422,6 +2422,30 @@ fn local_conversation_prompt_context_scopes_agent_messages() {
|
||||
},
|
||||
)
|
||||
.expect("append other agent conversation");
|
||||
append_local_conversation_message_for_session_idempotent_at(
|
||||
&root,
|
||||
None,
|
||||
None,
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: "任务已接收,项目总控 Agent 正在启动处理。".to_string(),
|
||||
agent_id: None,
|
||||
},
|
||||
"runtime-public-status-project-accepted",
|
||||
)
|
||||
.expect("append Runtime-owned project status");
|
||||
append_local_conversation_message_for_session_idempotent_at(
|
||||
&root,
|
||||
Some("art-asset-plan"),
|
||||
None,
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: "专业 Agent 执行失败,请稍后重试".to_string(),
|
||||
agent_id: None,
|
||||
},
|
||||
"runtime-public-status-agent-failed",
|
||||
)
|
||||
.expect("append Runtime-owned Agent status");
|
||||
|
||||
let project_context =
|
||||
render_local_conversation_prompt_context(&root, None).expect("project context");
|
||||
@@ -2430,6 +2454,7 @@ fn local_conversation_prompt_context_scopes_agent_messages() {
|
||||
assert!(project_context.contains("[project / user] 希望主角用月光厨房做弹幕躲避"));
|
||||
assert!(!project_context.contains("[art-asset-plan / assistant]"));
|
||||
assert!(!project_context.contains("美术建议:霓虹锅铲和月亮灶台"));
|
||||
assert!(!project_context.contains("任务已接收"));
|
||||
|
||||
let art_context = render_local_conversation_prompt_context(&root, Some("art-asset-plan"))
|
||||
.expect("art agent context");
|
||||
@@ -2439,6 +2464,8 @@ fn local_conversation_prompt_context_scopes_agent_messages() {
|
||||
assert!(art_context.contains("[redacted sensitive context]"));
|
||||
assert!(!art_context.contains("secret-token"));
|
||||
assert!(!art_context.contains("策划建议:只保留三种输入"));
|
||||
assert!(!art_context.contains("任务已接收"));
|
||||
assert!(!art_context.contains("专业 Agent 执行失败"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
@@ -21,6 +21,28 @@ import type {
|
||||
TauriInvoke,
|
||||
} from '../../app/types';
|
||||
|
||||
const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX = 'runtime-public-status-';
|
||||
const AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX = 'runtime-task-';
|
||||
const AGENT_RUNTIME_MESSAGE_CORRELATION_PATTERN = /^[0-9a-f]{32}$/;
|
||||
|
||||
function agentRuntimeMessageCorrelationId(messageId: string | null | undefined) {
|
||||
const normalized = messageId?.trim() ?? '';
|
||||
const prefix = normalized.startsWith(
|
||||
AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX,
|
||||
)
|
||||
? AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX
|
||||
: normalized.startsWith(AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX)
|
||||
? AGENT_RUNTIME_TASK_MESSAGE_ID_PREFIX
|
||||
: null;
|
||||
if (!prefix) {
|
||||
return null;
|
||||
}
|
||||
const correlationId = normalized.slice(prefix.length).split('-', 1)[0] ?? '';
|
||||
return AGENT_RUNTIME_MESSAGE_CORRELATION_PATTERN.test(correlationId)
|
||||
? correlationId
|
||||
: null;
|
||||
}
|
||||
|
||||
export function createLocalConversationDraftMessage(
|
||||
content: string,
|
||||
updatedAt = Date.now(),
|
||||
@@ -1221,17 +1243,56 @@ export function mergeProjectSupervisorConversation(
|
||||
projectRecords: LocalConversationMessageRecord[],
|
||||
supervisorRecords: LocalConversationMessageRecord[],
|
||||
): ChatMessage[] {
|
||||
const supervisorRecordIndexByCorrelation = new Map<string, number>();
|
||||
supervisorRecords.forEach((record, index) => {
|
||||
if (record.role !== 'user') {
|
||||
return;
|
||||
}
|
||||
const correlationId = agentRuntimeMessageCorrelationId(record.messageId);
|
||||
if (correlationId && !supervisorRecordIndexByCorrelation.has(correlationId)) {
|
||||
supervisorRecordIndexByCorrelation.set(correlationId, index);
|
||||
}
|
||||
});
|
||||
const records = [
|
||||
...projectRecords.map((record, index) => ({
|
||||
record,
|
||||
runtimeOwned: false,
|
||||
stableIndex: index,
|
||||
})),
|
||||
...supervisorRecords.map((record, index) => ({
|
||||
record,
|
||||
runtimeOwned: true,
|
||||
stableIndex: projectRecords.length + index,
|
||||
})),
|
||||
...projectRecords.map((record, index) => {
|
||||
const runtimeOwned = Boolean(
|
||||
record.messageId
|
||||
?.trim()
|
||||
.startsWith(AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX),
|
||||
);
|
||||
const accepted =
|
||||
runtimeOwned &&
|
||||
record.content === '任务已接收,项目总控 Agent 正在启动处理。';
|
||||
const correlationId = agentRuntimeMessageCorrelationId(record.messageId);
|
||||
const supervisorRecordIndex = correlationId
|
||||
? supervisorRecordIndexByCorrelation.get(correlationId)
|
||||
: undefined;
|
||||
const runtimeRecordIndex =
|
||||
supervisorRecordIndex ?? supervisorRecords.length + index;
|
||||
return {
|
||||
record,
|
||||
runtimeOwned,
|
||||
sameSecondLane: runtimeOwned ? 1 : 0,
|
||||
sameSecondOrder: runtimeOwned
|
||||
? runtimeRecordIndex * 4 + (accepted ? 1 : 2)
|
||||
: index,
|
||||
stableIndex: index,
|
||||
};
|
||||
}),
|
||||
...supervisorRecords.map((record, index) => {
|
||||
const userMessage = record.role === 'user';
|
||||
return {
|
||||
record,
|
||||
runtimeOwned: true,
|
||||
sameSecondLane: 1,
|
||||
// Conversation timestamps have second precision. Runtime task and
|
||||
// public status IDs carry the same opaque run correlation digest, so
|
||||
// status ordering follows its actual task record instead of guessing
|
||||
// from independent user/accepted/terminal category counters.
|
||||
sameSecondOrder: index * 4 + (userMessage ? 0 : 3),
|
||||
stableIndex: projectRecords.length + index,
|
||||
};
|
||||
}),
|
||||
]
|
||||
.filter(
|
||||
({ record }) => record.role === 'user' || record.role === 'assistant',
|
||||
@@ -1239,6 +1300,8 @@ export function mergeProjectSupervisorConversation(
|
||||
.sort(
|
||||
(left, right) =>
|
||||
left.record.updatedAt - right.record.updatedAt ||
|
||||
left.sameSecondLane - right.sameSecondLane ||
|
||||
left.sameSecondOrder - right.sameSecondOrder ||
|
||||
left.stableIndex - right.stableIndex,
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
|
||||
+17
-1
@@ -12,6 +12,7 @@ import type {
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { PROJECT_SUPERVISOR_AGENT_ID } from '../../app/constants';
|
||||
import type {
|
||||
AgentRuntimeEventRecord,
|
||||
AgentRuntimeResponseStream,
|
||||
@@ -54,6 +55,7 @@ type RuntimeControlProps = ComponentProps<
|
||||
export type GameChatRuntimeEvent = {
|
||||
key: string;
|
||||
agentLabel: string;
|
||||
rootSupervisorRuntime: boolean;
|
||||
event: AgentRuntimeEventRecord;
|
||||
};
|
||||
|
||||
@@ -76,6 +78,14 @@ const GAME_CHAT_INTERNAL_RUNTIME_EVENT_TYPES = new Set([
|
||||
'agent.runtime.tool.response',
|
||||
'agent.runtime.tool.result',
|
||||
]);
|
||||
const GAME_CHAT_ROOT_RUNTIME_OWNED_EVENT_TYPES = new Set([
|
||||
// Runtime-owned accepted status is persisted once by the backend. Keeping
|
||||
// turn.started in the event transcript would show a second synthetic start
|
||||
// message and make a single accepted task look like two user-visible turns.
|
||||
'turn.started',
|
||||
'turn.failed',
|
||||
'turn.budget_exhausted',
|
||||
]);
|
||||
const GAME_CHAT_RUNTIME_CLOCK_INTERVAL_MS = 10_000;
|
||||
const GAME_CHAT_RUNTIME_STALL_THRESHOLD_MS = 5 * 60 * 1000;
|
||||
const GAME_CHAT_EARLIEST_RUNTIME_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
|
||||
@@ -674,6 +684,10 @@ function collectGameChatRuntimeEventsInternal(
|
||||
deduplicated.set(key, {
|
||||
key,
|
||||
agentLabel: source.label,
|
||||
rootSupervisorRuntime:
|
||||
source.runtime.agentId === PROJECT_SUPERVISOR_AGENT_ID &&
|
||||
source.runtime.parentAgentId == null &&
|
||||
source.runtime.parentRunId == null,
|
||||
event,
|
||||
});
|
||||
}
|
||||
@@ -705,7 +719,9 @@ function gameChatRuntimeEventMessageText(item: GameChatRuntimeEvent) {
|
||||
if (
|
||||
!eventId ||
|
||||
!publicText ||
|
||||
GAME_CHAT_INTERNAL_RUNTIME_EVENT_TYPES.has(eventType)
|
||||
GAME_CHAT_INTERNAL_RUNTIME_EVENT_TYPES.has(eventType) ||
|
||||
(item.rootSupervisorRuntime &&
|
||||
GAME_CHAT_ROOT_RUNTIME_OWNED_EVENT_TYPES.has(eventType))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import type {
|
||||
AgentRuntimeResult,
|
||||
AgentRuntimeState,
|
||||
ChatMessage,
|
||||
LocalConversationMessageRecord,
|
||||
} from '../src/app/types';
|
||||
import {
|
||||
formatAgentRuntimeEvent,
|
||||
mergeGameChatRuntimeResponseMessagesIntoHistory,
|
||||
mergeProjectSupervisorConversation,
|
||||
MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE,
|
||||
projectRuntimeVisibleCurrentWork,
|
||||
projectRuntimeVisibleError,
|
||||
@@ -18,6 +20,184 @@ import {
|
||||
submitProjectSupervisorRuntimeTask,
|
||||
} from '../src/features/agent-runtime/model';
|
||||
|
||||
describe('Runtime-owned public statuses', () => {
|
||||
test('keeps backend status messages visible without treating them as client-authored conversation', () => {
|
||||
const projectRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '任务已接收,项目总控 Agent 正在启动处理。',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-accepted',
|
||||
updatedAt: 2,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '普通项目消息',
|
||||
agentId: null,
|
||||
messageId: 'project-message-1',
|
||||
updatedAt: 3,
|
||||
},
|
||||
];
|
||||
const supervisorRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'user',
|
||||
content: '请继续完成当前游戏',
|
||||
agentId: 'project-supervisor',
|
||||
messageId: 'runtime-task-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
updatedAt: 2,
|
||||
},
|
||||
];
|
||||
|
||||
const messages = mergeProjectSupervisorConversation(
|
||||
projectRecords,
|
||||
supervisorRecords,
|
||||
);
|
||||
|
||||
expect(messages).toEqual([
|
||||
expect.objectContaining({
|
||||
text: '请继续完成当前游戏',
|
||||
runtimeOwned: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: '任务已接收,项目总控 Agent 正在启动处理。',
|
||||
runtimeOwned: true,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
text: '普通项目消息',
|
||||
runtimeOwned: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
test('interleaves rapid same-second tasks with their accepted and terminal statuses', () => {
|
||||
const projectRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '任务已接收,项目总控 Agent 正在启动处理。',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-accepted',
|
||||
updatedAt: 9,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '项目总控 Agent 执行失败,请稍后重试',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-failed',
|
||||
updatedAt: 9,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '任务已接收,项目总控 Agent 正在启动处理。',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-accepted',
|
||||
updatedAt: 9,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '项目总控 Agent 已达到执行预算,请缩小任务范围后重试',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-budget',
|
||||
updatedAt: 9,
|
||||
},
|
||||
];
|
||||
const supervisorRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'user',
|
||||
content: '第一条任务',
|
||||
agentId: 'project-supervisor',
|
||||
messageId: 'runtime-task-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
updatedAt: 9,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'user',
|
||||
content: '第二条任务',
|
||||
agentId: 'project-supervisor',
|
||||
messageId: 'runtime-task-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
updatedAt: 9,
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
mergeProjectSupervisorConversation(projectRecords, supervisorRecords).map(
|
||||
(message) => message.text,
|
||||
),
|
||||
).toEqual([
|
||||
'第一条任务',
|
||||
'任务已接收,项目总控 Agent 正在启动处理。',
|
||||
'项目总控 Agent 执行失败,请稍后重试',
|
||||
'第二条任务',
|
||||
'任务已接收,项目总控 Agent 正在启动处理。',
|
||||
'项目总控 Agent 已达到执行预算,请缩小任务范围后重试',
|
||||
]);
|
||||
});
|
||||
|
||||
test('keeps an older sparse terminal before a new same-second task', () => {
|
||||
const projectRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '项目总控 Agent 执行失败,请稍后重试',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-failed',
|
||||
updatedAt: 9,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
content: '任务已接收,项目总控 Agent 正在启动处理。',
|
||||
agentId: null,
|
||||
messageId:
|
||||
'runtime-public-status-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-accepted',
|
||||
updatedAt: 9,
|
||||
},
|
||||
];
|
||||
const supervisorRecords: LocalConversationMessageRecord[] = [
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'user',
|
||||
content: '旧任务',
|
||||
agentId: 'project-supervisor',
|
||||
messageId: 'runtime-task-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
updatedAt: 8,
|
||||
},
|
||||
{
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'user',
|
||||
content: '新任务',
|
||||
agentId: 'project-supervisor',
|
||||
messageId: 'runtime-task-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
||||
updatedAt: 9,
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
mergeProjectSupervisorConversation(projectRecords, supervisorRecords).map(
|
||||
(message) => message.text,
|
||||
),
|
||||
).toEqual([
|
||||
'旧任务',
|
||||
'项目总控 Agent 执行失败,请稍后重试',
|
||||
'新任务',
|
||||
'任务已接收,项目总控 Agent 正在启动处理。',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Game Chat stream identity and source', () => {
|
||||
test('response stream identity binds run, slot, and revision', () => {
|
||||
const base: AgentRuntimeResponseStream = {
|
||||
|
||||
@@ -3689,6 +3689,20 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
const runtime = gameChatRuntimeState({
|
||||
runId,
|
||||
recentEvents: [
|
||||
gameChatRuntimeEvent({
|
||||
runId,
|
||||
eventType: 'turn.started',
|
||||
summary: 'Synthetic start event must stay out of chat',
|
||||
updatedAt: 30,
|
||||
}),
|
||||
gameChatRuntimeEvent({
|
||||
runId,
|
||||
eventType: 'turn.failed',
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
summary: 'Root terminal event is covered by durable public status',
|
||||
updatedAt: 35,
|
||||
}),
|
||||
gameChatRuntimeEvent({
|
||||
runId,
|
||||
eventType: 'turn.progress',
|
||||
@@ -3737,11 +3751,35 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
}),
|
||||
],
|
||||
});
|
||||
const designRuntime = gameChatRuntimeState({
|
||||
agentId: 'design-director',
|
||||
taskId: 'design-director',
|
||||
sessionId: 'design-director-session',
|
||||
runId: 'design-director-child-run',
|
||||
source: 'agent-delegate',
|
||||
parentAgentId: 'project-supervisor',
|
||||
parentRunId: runId,
|
||||
recentEvents: [
|
||||
gameChatRuntimeEvent({
|
||||
agentId: 'design-director',
|
||||
runId: 'design-director-child-run',
|
||||
eventType: 'turn.started',
|
||||
summary: 'Design Agent started and must stay visible',
|
||||
updatedAt: 32,
|
||||
}),
|
||||
],
|
||||
updatedAt: 32,
|
||||
});
|
||||
|
||||
const messages = gameChatRuntimeEventMessages(runtime, {});
|
||||
const messages = gameChatRuntimeEventMessages(runtime, {
|
||||
'design-director': designRuntime,
|
||||
});
|
||||
|
||||
expect(messages.map((message) => message.updatedAt)).toEqual([40, 60, 80]);
|
||||
expect(messages.map((message) => message.updatedAt)).toEqual([
|
||||
32, 40, 60, 80,
|
||||
]);
|
||||
expect(messages.map((message) => message.text)).toEqual([
|
||||
expect.stringContaining('Design Agent started and must stay visible'),
|
||||
expect.stringContaining('Generated prototype progress'),
|
||||
expect.stringContaining('code agent repair collision'),
|
||||
expect.stringContaining('preview.validate:ok'),
|
||||
@@ -3760,6 +3798,47 @@ export function registerProjectSupervisorSurfaceTests() {
|
||||
expect(messages.map((message) => message.text).join('\n')).not.toContain(
|
||||
'agent.runtime.tool.request',
|
||||
);
|
||||
|
||||
for (const continuation of [
|
||||
{ kind: 'receipt', source: 'agent-delegate-receipt' },
|
||||
{ kind: 'isolated join', source: 'agent-isolated-join' },
|
||||
]) {
|
||||
const continuationRunId = `supervisor-${continuation.kind.replaceAll(' ', '-')}-continuation`;
|
||||
const supervisorContinuation = gameChatRuntimeState({
|
||||
runId: continuationRunId,
|
||||
source: continuation.source,
|
||||
parentAgentId: null,
|
||||
parentRunId: runId,
|
||||
recentEvents: [
|
||||
gameChatRuntimeEvent({
|
||||
runId: continuationRunId,
|
||||
eventType: 'turn.started',
|
||||
summary: `Supervisor ${continuation.kind} continuation started`,
|
||||
updatedAt: 100,
|
||||
}),
|
||||
gameChatRuntimeEvent({
|
||||
runId: continuationRunId,
|
||||
eventType: 'turn.failed',
|
||||
status: 'failed',
|
||||
phase: 'failed',
|
||||
summary: `Supervisor ${continuation.kind} continuation failed`,
|
||||
updatedAt: 110,
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(
|
||||
gameChatRuntimeEventMessages(supervisorContinuation, {}).map(
|
||||
(message) => message.text,
|
||||
),
|
||||
).toEqual([
|
||||
expect.stringContaining(
|
||||
`Supervisor ${continuation.kind} continuation started`,
|
||||
),
|
||||
expect.stringContaining(
|
||||
`Supervisor ${continuation.kind} continuation failed`,
|
||||
),
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('turns only professional final-reply streams into labeled game-chat messages', () => {
|
||||
|
||||
@@ -79,6 +79,17 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-06 AGC 根长任务启动与终态失败采用 Runtime 公开消息硬门
|
||||
|
||||
- 背景:AGC 已有模型 final-reply、Runtime `eventId/publicText` 和进度卡,但根任务“已入队”没有后端持久公开回执;失败消息又散落在 main loop 多个 `let _ = append conversation` 分支。Provider 或 Runtime 在首条公开事件前失败时可能零消息,同一次失败也可能被 `turn.failed` 和 conversation 重复播报。
|
||||
- 决策:Supervisor / 专业 Agent 继续负责业务语义,Runtime 只增加两项不依赖模型的最低可观测性。用户直接投递的 Project Supervisor 根后台任务先落为不可执行的 `preparing / public-status-pending`,再以 run 绑定的 `runtime-public-status-*` message ID 幂等写入启动确认,成功后才转为 `pending / queued`;恢复预检只验证业务状态且不改写 task/conversation,仅真实 resume 持有 Agent 锁后才能把已验证的 accepted 任务持久提升为 `pending / queued`,写入失败则把 task 落为 `failed / public-status-write-failed`。根 Supervisor 通过正式失败 / 预算耗尽收束或 game-chat 绝对硬期限进入 reconciliation 时,在其它终态投影前先写一条脱敏失败消息;专业 Agent 命中同一全局硬期限时,必须通过权威 Run Profile 与根 task 在项目 conversation 幂等写根终态,同时只在精确匹配 agent/run/session、两个 parent 和 binding 的专业 Agent Session 保留 child 终态。自称根 agent/run 但 session 或两个 parent 不符的 Runtime 必须失败关闭,不得生成第二条项目终态。该前缀的 Runtime 公开状态只供 UI 展示,不合入任何 Agent prompt。前端把它视为 Runtime-owned,同秒时排在触发它的 Supervisor 用户消息之后,并仅过滤重复的根 `turn.started / turn.failed / turn.budget_exhausted`,专业 Agent 启动事件仍可见。
|
||||
- 恢复补充(以本条为准):`preparing` 只要同 run 的用户消息或 accepted 任一已完整持久即属可恢复;preflight 不改写业务文件,resume 才在 Agent 锁内补写 accepted 并入队。用户消息或 accepted conversation 已存在而审计补写失败不得把任务改判为启动失败。根终态首次公开写入遇到瞬时失败时,完成终态投影后必须用同 message ID 再幂等写一次。带 parent 的 Supervisor receipt / isolated-join 续跑只保留一份 Runtime 公开终态,不再追加 Session 重复消息;`runtime-task-*` 与 `runtime-public-status-*` 共享同 run 的不透明关联摘要,多个同秒任务在 UI 中按实际 run 关联的 `user -> accepted -> terminal` 交错排序。
|
||||
- 边界:不改变 Supervisor 条件 Graph、`agent.route_manifest`、美术路由、Provider tool-plan / final-reply 协议、response stream 或最终 assistant 唯一性;不把 tool 参数、原始错误、路径、fingerprint、凭据和专业 Agent 私有失败诊断公开。Runtime 也不根据工具事件推断业务进展,后续若引入模型 `commentary/final` phase 应沿独立消息协议扩展。
|
||||
- 验证方式:覆盖 `preparing -> accepted -> pending` 顺序与崩溃恢复、启动确认幂等、启动确认无法持久化时任务零执行、公开状态不进入实际 Agent prompt、状态文件写入本身失败时仍先产生公开失败、根终态事件不重复进聊天、专业 Agent 启动事件仍可见、普通项目消息不被误标 Runtime-owned,并运行 Runtime 定向 Rust、AppSurface、模型测试、typecheck、编码和 diff 门禁。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/project-memory/shared-memory/development-workflow.md`、`docs/project-memory/shared-memory/pitfalls.md`。
|
||||
|
||||
---
|
||||
|
||||
## 2026-08-05 game-chat 当前 ready child 对 manifest Pending 漂移保持活性
|
||||
|
||||
> 本条只保留 ready child 的 Pending 漂移处理;历史试玩类型继承与旧三 Director 首波验收已由 `2026-08-06 game-chat 固定规则只提供上下文,Supervisor 决定条件 Graph` 取代。
|
||||
|
||||
@@ -83,6 +83,16 @@ cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml real_
|
||||
|
||||
game-chat 迭代还必须确认以下行为:Supervisor ready 输出、本轮实际启动的 `design-director / art-director / art-asset-plan / code-director / code-prototype / preview-readiness / preview-playtest` 专业 Agent 各自产生 durable `final-reply`,条件路由跳过的节点只要求 manifest 正确投影为 completed,不伪造 Agent 回复;Rust 明确生成 `eventId + publicText` 的每条公开 Runtime 输出都作为独立 assistant 消息逐条固化在项目聊天。消息通过顶层 `messageId` 幂等追加,事件 / 轮询 / hydration 重放不重复。前端禁止从原始 `summary / detail`、tool plan、Provider / Runner 元数据、命令输出或路径自行拼接持久消息;UI 把一个父 Run 统一显示为“本轮生成进度”,最新状态也不得暴露内部“第 N 轮”,完整 GUI / CLI 任务图可显示 `x/14`,首版快车道显示 `x/7`,终态不保留运行中进度卡;可信 `project-supervisor-game-chat` 一轮完成 `preview-playtest` 后直接收束,不请求下一次 Provider tool-plan;所有调度入口均不得暴露或启动 `publish-strategy` / `publish-package`。固定关键词与资产探测只进入 Supervisor 的 advisory context;没有持久 Supervisor 路由时不得启动任何 child。只有程序侧审计判定存在真实缺口或 Supervisor 明确选择整体重做时才要求可用 External Editor API,并按 `art-spec.png -> icon-spritesheet -> art-spritesheet.png + iconImageSrcs 本地切片 -> code-prototype Canvas 使用四类切片` 补齐;`use-existing-art` 不得重复生成或扣费,但仍须通过相同正式资产和代码可见使用门禁。
|
||||
|
||||
Project Supervisor 根后台任务还必须验证公开消息硬门:任务先落为不可执行的 `preparing / public-status-pending`,项目 conversation 中存在且仅存在一条同 run 的 `runtime-public-status-* / accepted` 后才能转为 `pending / queued`;恢复 preflight 只验证并临时分类可恢复状态,不改写 task/conversation,真实 resume 持有 Agent 锁后才可把 accepted preparing 持久提升为 `pending / queued`;否则不得执行。若该消息无法持久化,task 必须进入 `failed / public-status-write-failed` 且 Provider、工具与 child 调度均为零。这类 Runtime 公开状态不得合入 Agent prompt。根 Supervisor 通过正式失败 / 预算耗尽收束或 game-chat 绝对硬期限进入 reconciliation 时,即使后续 task/event/state 写入失败,也必须已经存在一条脱敏终态失败消息;专业 Agent 命中全局硬期限时,项目 conversation 仍必须幂等保留根终态,但 child 终态只能写入与权威 task 的 agent/run/session、两个 parent 和 binding 全部一致的私有 Session;自称根 agent/run 却 session 或 parent 不符时失败关闭。只有根 Supervisor 的 `turn.started / turn.failed / turn.budget_exhausted` 不得再形成第二条聊天消息,专业 Agent 的公开启动事件仍应显示。非 Supervisor 专业 Agent 的失败状态留在对应 Agent Session,不得把私有诊断送进项目 conversation。定向复验至少运行:
|
||||
|
||||
提交前还要覆盖启动崩溃窗口:仅 `preparing` 时 preflight/resume 均不执行;用户消息已持久但 accepted 缺失时,preflight 只报可恢复且 task/conversation 不变,resume 才补写 accepted 并转为 `pending / queued`。还要验证用户消息或 accepted 已落盘但审计失败均继续入队、根终态首次公开写入瞬时失败会用同 ID 重试、receipt / isolated-join 失败只有一条公开结果,以及同秒连续任务通过共享的 run 关联摘要按 `user -> accepted -> terminal` 逐任务展示。
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml public_start_status -- --test-threads=1
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_state_write_failure_stops_before_context_and_audit -- --test-threads=1
|
||||
npm run test -- apps/ai-game-creator-shell/tests/agentRuntimeModel.test.ts apps/ai-game-creator-shell/tests/appSurface.test.ts
|
||||
```
|
||||
|
||||
game-chat 条件快车道仍采用七任务口径,但执行顺序由持久合同控制。父 Run 与全部 child Run 共用 4200 秒软预算和 4500 秒累计硬上限;Supervisor 首轮先调用 `agent.route_manifest`,决策前零 child。`audit-existing-first` 的首波仅为 `design-director + code-director`,已登记但无效的旧派生视觉不得阻断这两项审计启动;code-director 必须先 `asset.list`,读取 Supervisor 已持久化的 authoritative 决策,再提交正式资产覆盖合同。覆盖完整时跳过图片生成,存在缺口时只开放缺口对应 owner,`regenerate-art` 才强制重新开放两个美术 owner;已登记但校验失败的固定资产只允许当前路由绑定的 canonical owner 原位替换。新生成仍按 `art-spec.png -> art-spritesheet.png + 独立切片` 推进,最后才允许 code-prototype 写入或局部修复入口。软预算后只允许使用已登记图集、当前 resourceId 对应切片清单的确定性本地 fallback、`game.static_smoke` 和 `preview.validate`;不得退回普通生图、猜测 atlas 网格或纯代码核心画面。完成门要求活动 Canvas 分别绘制 player、blocks-and-targets、obstacles-and-scene、feedback-effects 四类不同切片;整图 `<img>`、CSS background、完整图集直绘、单个猜测裁切和路径诱饵均失败。对应定向测试至少包括:决策前零 child、旧无效视觉不阻断首波、首波无美术 child、code-director 收到真实持久策略、完整覆盖零生成、仅缺图集只运行 `art-asset-plan`、无效已登记资产由 owner 原位替换、art spec 缺失导致引用合同失效时同时补齐两个槽位、显式重做原位替换两个正式资产、旧 root/fingerprint/缺口或重复路由失败关闭,以及补齐后恢复 code-prototype。
|
||||
|
||||
失败续跑还必须覆盖同 Session 同 source 继承、跨 Session / 跨 source 不继承、首次与连续 successor 的 effective task / contract / scheduler 一致性,以及中英文纯继续短语使用同一识别函数。非占位入口的新 `code-prototype` 必须先产生本人 mutation 再 smoke;连续只读 smoke 不得收束。占位 fallback 只允许显式支持的真实玩法模板,俄罗斯方块必须验证棋盘、下落、旋转、锁定和消行语义,未知玩法必须失败关闭。
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
- 关联:相关文件、文档、提交或 Issue
|
||||
```
|
||||
|
||||
## Runtime 状态写失败不能发生在公开失败消息之前
|
||||
|
||||
- 现象:用户提交长任务后只看到运行失败或任务直接消失,聊天里一条有用消息都没有;另一些失败又同时出现 Runtime event 和 conversation 两条近似提示。
|
||||
- 原因:启动确认依赖实际 `turn.started`,任务只入队或 Runner 在 start transition 前失败时没有公开回执;终态失败先写 task/event/state,最后才由各 main-loop 分支尽力追加 assistant。坏掉的若正是状态文件,流程会在公开消息前返回;散落的 `let _` 又无法提供幂等身份。
|
||||
- 处理:用户直接投递的 Project Supervisor 根任务先落为不可执行的 `preparing / public-status-pending`,持久化同 run 的 `runtime-public-status-* / accepted` 后才转为 `pending / queued`;恢复 preflight 只验证业务状态,不改写 task/conversation,仅 resume 持有 Agent 锁后才可持久提升。根 Supervisor 通过正式失败 / 预算耗尽收束或 game-chat 绝对硬期限进入 reconciliation 时,先写相同协议的终态消息,再处理 Runtime 其它投影。专业 Agent 命中全局硬期限时,应由权威根 task 在项目 conversation 中幂等写根终态,child 仅在身份完整匹配时写私有 Session;根 agent/run 的 session 或两个 parent 冲突时必须失败关闭。消息正文只能来自封闭脱敏映射,prompt 构建必须按稳定前缀排除;前端按前缀标记 Runtime-owned,只过滤根 Supervisor 的 `turn.started / turn.failed / turn.budget_exhausted`,不吞掉专业 Agent 启动进度。模型仍负责业务 commentary/final,Runtime 的两条硬门不扩展成业务判断器。
|
||||
- 恢复补充:不能把“accepted 还没写完”等同于“用户从未投递”。用户消息已持久时,真实 resume 必须补写 accepted 后才入队;用户消息或 accepted conversation 已存在时,后续审计失败不得留下“正在启动”但永不执行的假状态,但同 message ID 的 role/content 冲突必须把 task 明确收束为 `conversation-write-failed`。根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。带 parent 的 Supervisor continuation 不得同时产生 Session 终态和 Runtime 事件两条公开消息;秒级时间戳下必须以 task/status message ID 共享的 run 关联摘要排序,不能用不同消息类别的计数猜测顺序。
|
||||
- 验证:任务 journal 必须显示 `preparing -> pending`,仅有 accepted 时恢复才可提升;破坏项目 conversation 时断言任务为 `public-status-write-failed` 且无可运行 pending;破坏 Runtime state 路径时断言公开失败已经存在;重复写同一 run/status 只有一条 message ID;渲染实际 prompt 断言不包含 Runtime 公开状态;AppSurface 证明根启动/失败事件不重复,专业 Agent 启动仍可见。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs`、`agent/runtime_driver/task_start.rs`、`src/features/agent-runtime/model.ts`、`src/features/project-workspace/SupervisorChatOnlyView.tsx`。
|
||||
|
||||
## manifest 被旧快照写回 Pending 时不能让父 Run 抛下真实运行中的 child
|
||||
|
||||
- 现象:`code-prototype` 已有确定性 child run、running journal 和工具事件,父 Supervisor 却在几秒后以 fixed graph stalled 失败;child 随后完成代码与静态检查,但 completion gate 持续报告 `task=code-prototype status=pending`,最后 `loop-budget-exhausted`。
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
- Windows Runner 私有文件初始化:父 AppData 已归当前 `TokenUser` 后,新建 `agent-runner.lock`、endpoint 临时文件、project-owner 诊断临时文件与 real-E2E 私有文件的 owner 仍可能采用 token 默认 owner `Administrators`。固定 stale lock 只有在父目录已验证为当前用户 protected 私有 DACL、Windows 不共享独占句柄已取得、且句柄确认普通文件、非 reparse point、链接数为一时才允许修复;其它三类文件只允许在本进程 `create_new` 成功且仍持有同一独占句柄时初始化 `TokenUser` owner / DACL,再写入、原子安装并严格复核,初始化失败必须清理刚创建的文件。既有 durable endpoint / diagnostic 读取不得自动接管;活锁不得截断,只有 sharing / lock violation `32/33` 表示占用,access denied 等其它错误立即返回。父进程观察到 Runner 子进程退出后立即返回错误,不等待完整 30 秒 deadline。
|
||||
- 启动诊断:独立 release 的 `startup.log` 和 `agent-runner.log` 只记录有界、脱敏的阶段与 stdout / stderr 摘要,凭据、AppData 路径和其它绝对路径不得原样落盘;单文件达到 256 KiB 后只轮转保留一份 `.previous.log`。`startup.log` 优先写独立 AppData,目录不可写时回退到系统 TEMP 下的 `Genarrative-Game-Chat-Diagnostics`;Tauri context、窗口 URL、AppData、Runner 或 `.setup()` / `.build()` 初始化失败时,Windows 必须显示可见错误对话框并给出诊断日志位置,不能只在无控制台 release 中静默退出。
|
||||
- 对话与事件:窗口固定使用 `project-supervisor + autonomous-game-build`,继续复用 active Session、External Runner、持久 conversation、流式回复、same-run steer、工具确认与用户追问。以 `/` 开头的输入必须继续走现有内置命令解析,例如 `/preview` 只能生成 `preview.start` 确认卡,不得作为自主构建任务投递给 Supervisor。界面聚合当前 Supervisor 父 run 及其直接委派专业 Agent 的最新原始事件,按时间倒序稳定去重并标注 Agent;默认显示 4 条,可展开至最新 20 条。原始 `summary / detail` 仍只作 Runtime 状态投影,不直接写入 conversation。需要进入聊天的事件必须由 Rust 同步生成唯一 `eventId` 与安全 `publicText`;前端只按这两个字段形成独立 assistant 消息,无 `eventId`、空 `publicText`、legacy 事件和内部 tool / Provider / Runner 协议一律忽略。
|
||||
- 公开消息硬门:模型仍负责 Supervisor / 专业 Agent 回复的业务语义,Runtime 不根据 tool 或 Provider 事件自行补写业务结论;但用户直接投递的 Project Supervisor 根后台任务必须先落为不可执行的 `preparing / public-status-pending`,再以 `runtime-public-status-*` 稳定 message ID 把“任务已接收,正在启动处理”写入项目 conversation,成功后才转为 `pending / queued`;恢复预检只读,只能在验证到同 run accepted 消息后把该任务临时分类为可恢复,真实 resume 持有 Agent 锁后才可持久提升为 `pending / queued`;写入失败则落为 `failed / public-status-write-failed`,不得继续执行。这些 Runtime 公开状态只供 UI 展示,prompt 构建器必须按稳定前缀排除。根 Supervisor 通过正式失败 / 预算耗尽收束或 game-chat 绝对硬期限进入 reconciliation 时,必须在 task、event、state 等其它终态投影之前先幂等写入一条脱敏、用户可理解的失败消息;专业 Agent 命中该全局硬期限时,也必须通过权威 Run Profile 和根 task 将同一根终态写入项目 conversation,同时保留 child 私有 Session 状态;状态文件本身写坏也不能导致零公开结果。当前 Runtime 自称根 agent/run 时,其 session 和两个 parent 字段必须与权威根 task 一致;任一身份冲突必须失败关闭,不得以另一 session 派生第二条项目终态。前端把该前缀识别为 Runtime-owned,同秒时排在触发它的 Supervisor 用户消息之后,不二次持久化;仅根 Supervisor 的 `turn.started / turn.failed / turn.budget_exhausted` 只保留在 Runtime 详情和进度投影中,不能再生成第二条聊天消息,专业 Agent 的公开启动事件仍可见。该硬门不改变 final-reply 的唯一性;非 Supervisor 专业 Agent 的失败消息继续留在对应 Agent Session,不把私有诊断写进项目 conversation。
|
||||
- 启动恢复和续跑边界:本条取代上一条中“只有 accepted 才可恢复”的窄口径。若进程在 Supervisor 用户消息已持久、accepted 未持久之间崩溃,只读 preflight 可以把该 `preparing` 识别为可恢复,但不改写 task/conversation;真实 resume 持有 Agent 锁后必须先幂等补写 accepted,再提升为 `pending / queued`。用户消息或 accepted conversation 已落盘而辅助审计失败时,以 conversation 为公开真相继续入队,不留下“已接收但永不执行”的任务;根终态首次公开写入的瞬时失败必须在终态投影后用相同 message ID 重试。receipt / isolated-join 等带 parent 的 Supervisor continuation 不再另写 Session 终态,只保留单一后端公开事件;`runtime-task-*` 与 `runtime-public-status-*` 共享同 run 的不透明关联摘要,秒级时间戳下多个连续任务必须按实际 run 对应的 `user -> accepted -> terminal` 顺序交错展示。
|
||||
- Supervisor 进度播报:聊天消息流内保留且只保留一条当前 run 的 Runtime-owned 播报卡,由客户端从 manifest 任务图、Supervisor 结构化计划、`loopIteration`、当前动作、直接委派专业 Agent 及其持久事件确定性整理;显示当前轮次、任务 / 计划进度、活跃 Agent、最近试玩与静态检查、返工决定、代码修改和截图检查证据。同一 run 原位更新,切换 run 时替换,不调用额外模型、不追加持久 conversation,也不改变最终 assistant 回复的唯一性;任意详情必须有界且不展示绝对路径、Provider 元数据或内部指纹。
|
||||
- ready-task 启动活性:`background_task.queued`、`autonomous_ready_task.scheduled`、Runner heartbeat 或执行锁已移交都不等于 child 已启动。实际持有执行权的 Runner 必须在释放项目写锁后同步写入 child 的 running task、`turn.started` 与 started journal,再把已启动 state 和 per-Agent 执行锁交给已确认开始轮询的独立 execution worker;同步启动或 worker 接管失败时,要在仍持有执行锁期间依次把 child 和 manifest Graph 节点明确落为 failed,再释放锁并让 parent 收到调度错误。`autonomous_ready_task.scheduled` 只作诊断审计,其写入失败不能阻断 durable child 启动;external client 只 wake Runner,不在客户端抢占执行。Supervisor 进度卡通过 durable `startedAt`(旧 Run 从完整 task journal 恢复,最新 task-record fallback 保持 0)显示真实持续时间,并以父 Run 与当前关联专业 Agent 的最大事件时间计算运行态活跃度:运行超过 5 分钟无新事件时显示“运行中 · 疑似停滞”和静默时长;等待用户、等待确认、Provider retry、视觉资产、进程会话、pausing 与 paused 不误报。父 Run terminal 后,持续时间冻结在父 Run 自身最后活动,不随 child 晚到收口事件增长。消息时间统一校验为 JavaScript 可表示的 Date;越界值显示“时间未知”且不写无效 `datetime`。实时回复只显示 response stream 自己的 `updatedAt`,缺失时同样显示“时间未知”,不能借用其它 Runtime 活动时间或随前端时钟漂移。该提示只提供可观测性,不改变 Runtime/manifest 正式状态。
|
||||
- ready-task manifest 漂移:父 Supervisor 必须分别判断“能否调度新节点”和“是否存在必须等待的工作”。派生视觉需要父规划修复时不再调度新 child,但当前最新且活跃的根 Run 下,只要存在确定性 runId、scheduler source、正确父绑定且 durable journal 为 queued/running 的 ready child,父 Run 就保持 `waiting-for-manifest-tasks`,不能因旧 hydration 快照把 manifest running 覆盖成 pending 而提前 fixed-graph-stalled。game-chat child 可在相同严格身份下容忍 pending 漂移;正式产物、Canvas、revision、`game.static_smoke` 与 `preview.validate` 门禁不放宽。GUI/CLI、旧父 Run、终态、确认/用户输入/reconciliation、伪造绑定或非确定性 runId 全部失败关闭;更新根 Run 后旧 child 不得继续维持新 DAG 或投影完成。
|
||||
@@ -58,6 +60,7 @@
|
||||
## 2026-07-31 game-chat 输出、单轮预览与平台美术资源
|
||||
|
||||
- 对话输出:game-chat 的 Supervisor `ready` response stream 继续以稳定身份显示;七任务目录中本轮实际启动的专业 Agent,其 `requestKind=final-reply` 且 `status=ready|committed` 的非空安全回复分别以 Agent、Session、run、request slot 和 response revision 形成 durable message ID,并带 Agent 标签逐条追加到项目聊天。条件路由跳过的美术节点只要求 manifest 正确投影为 completed,不伪造 Agent 回复。每条 Rust `eventId + publicText` 公开输出同样形成独立 durable 消息。所有这些消息通过 `append_local_conversation_message` 的顶层 `messageId` 幂等写入,事件、轮询、React StrictMode 和 hydration 重放不重复;tool-plan、半成品 stream、原始事件 detail、命令正文、绝对路径、Provider / Runner 元数据、哈希和凭据不得进入聊天。普通 `supervisor-chat` 保持原有 transient response 行为。
|
||||
- 对话输出中的 `eventId + publicText` 只指需要独立进入聊天的进度事件;`turn.started` 和根 Run 终态失败事件由上一条 `runtime-public-status-*` 硬门覆盖,不得同时转成事件消息。专业 Agent child 的失败消息继续留在其 Agent Session,根项目聊天只接收 Supervisor 终态失败、明确公开进度和安全 final-reply,避免一项失败被 Runtime event 与 conversation 各播报一次。
|
||||
- 单轮收束:game-chat source 只生成至 `preview-playtest` 的 manifest seed task,试玩完成后父 Run 直接进入完成门,不再调度 `publish-strategy` / `publish-package`;`agent.schedule_ready` 必须按当前 Supervisor Run 的持久 source/profile 选择同一 source-aware scheduler,不能绕过该边界。`task.list` 对同一 root source 必须从任务行、readyTaskIds 和统计中排除两个发布节点,`agent.delegate` 也必须按 root binding 拒绝直接委派这两个节点,不能让 Provider 用“读取完整 DAG 后手工委派”恢复已裁掉的发布阶段。完成门满足且 collaboration、Provider batch、进程会话、视觉资源等非验证屏障全部清零后,Runtime 必须用确定性回复直接收束结构化计划并结束父 Run,不再请求下一次 Provider 工具计划。普通 GUI / CLI 仍执行完整发布 DAG。
|
||||
- 轮次展示:`loopIteration` 只是同一父 Run 内的 Provider / 工具规划循环,用于委派、回执、返工和验收,不是用户发起的游戏生成轮次。game-chat 的进度卡、当前工作和“最新状态”事件统一显示“本轮”,整个页面不向用户显示“第 N 轮”;完整 GUI / CLI pre-publish 任务图仍可显示 `x/14`,但首版只按七项任务显示 `x/7`(详见 2026-08-03 小节),不得把两个发布节点计入任一分母。父 Run 终态后移除运行中进度卡,只保留终态阶段记录与预览。
|
||||
- 平台美术资源:game-chat 保留正式视觉 DAG,但是否进入生成节点由 2026-08-06 的持久条件路由决定。现有正式资源覆盖完整时直接复用;存在真实缺口或 Supervisor 明确选择整体重做时,`art-director` 才通过平台 `images/generations(kind=spec)` 生成并登记 `assets/art-spec.png`,`art-asset-plan` 再以该规范图的稳定 resourceId 调用 `icon-spritesheets/generations` 生成真实透明 `assets/art-spritesheet.png`,并把响应中的 `iconImageSrcs` 下载为本地独立切片,写入 `assets/art-spritesheet-slices/manifest.json`。`code-prototype` 必须等待复用或补齐后的图集与切片清单,并在活动 Canvas 中分别绘制玩家、方块/目标、障碍/场景与反馈四类切片;规范图只作 reference,纯代码核心画面、猜测图集等分网格、整图 `<img>` / CSS 背景、完整图集直绘或只出现路径均不得完成。
|
||||
|
||||
Reference in New Issue
Block a user