合并主分支
合入主分支的智能体根任务可观测性、队列恢复与前端状态更新。 保留角色动作正式序列、后台预览换签和画布导出修复。 完成合并树验证并保持现有契约测试。
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', () => {
|
||||
|
||||
Reference in New Issue
Block a user