修复游戏创作编排恢复与进度可观测性

提取并固化 Supervisor 工作流与任务图约束
修复 game-chat 计划收敛、当前 Run 写入归属和素材完成门
补强 parent wake 持久恢复、幂等投影与 deferred 竞态
改进 Agent 进度消息时间戳、等待态和滚动可见性
补齐 Rust、App Surface 回归及项目文档
This commit is contained in:
2026-08-05 23:47:46 +08:00
parent 17f8f4b48b
commit bc1fe7d8fd
43 changed files with 8483 additions and 423 deletions
@@ -106,6 +106,7 @@ pub(crate) use project_gates::{
}; };
#[cfg(test)] #[cfg(test)]
pub(crate) use project_gates::{ pub(crate) use project_gates::{
ensure_current_autonomous_ready_child_mutation_at_locked,
supervisor_collaboration_policy_completion_blocker_for_test_at, supervisor_collaboration_policy_completion_blocker_for_test_at,
supervisor_orchestrator_mutation_block_after_dispatch_for_test, supervisor_orchestrator_mutation_block_after_dispatch_for_test,
}; };
@@ -50,12 +50,17 @@ pub(crate) fn append_agent_runtime_tool_call_record(
action: &AgentRuntimeToolAction, action: &AgentRuntimeToolAction,
observation: &AgentRuntimeToolObservation, observation: &AgentRuntimeToolObservation,
action_id: Option<&str>, action_id: Option<&str>,
action_fingerprint: Option<&str>,
) { ) {
let record = AgentRuntimeToolCallRecord { let record = AgentRuntimeToolCallRecord {
action_id: action_id.map(ToString::to_string), action_id: action_id.map(ToString::to_string),
tool: observation.tool.clone(), tool: observation.tool.clone(),
status: observation.status.clone(), status: observation.status.clone(),
action_fingerprint: Some(agent_runtime_tool_action_fingerprint(action, task)), action_fingerprint: Some(
action_fingerprint
.map(ToString::to_string)
.unwrap_or_else(|| agent_runtime_tool_action_fingerprint(action, task)),
),
input_summary: agent_runtime_tool_action_input_summary(root, action), input_summary: agent_runtime_tool_action_input_summary(root, action),
reason: action reason: action
.reason .reason
@@ -132,7 +132,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
false, false,
|| observe_agent_runtime_memory(root, agent_id, &action.input), || observe_agent_runtime_memory(root, agent_id, &action.input),
), ),
"memory.write" => observe_agent_runtime_memory_write(root, agent_id, &action.input), "memory.write" => observe_agent_runtime_memory_write(root, agent_id, run_id, &action.input),
"conversation.read" => observe_agent_runtime_project_snapshot_with_lock( "conversation.read" => observe_agent_runtime_project_snapshot_with_lock(
root, root,
agent_id, agent_id,
@@ -285,8 +285,8 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
false, false,
|| observe_agent_runtime_task_list(root, agent_id, run_id), || observe_agent_runtime_task_list(root, agent_id, run_id),
), ),
"task.create" => observe_agent_runtime_task_create(root, agent_id, &action.input), "task.create" => observe_agent_runtime_task_create(root, agent_id, run_id, &action.input),
"task.update" => observe_agent_runtime_task_update(root, agent_id, &action.input), "task.update" => observe_agent_runtime_task_update(root, agent_id, run_id, &action.input),
"command.exec" => { "command.exec" => {
observe_agent_runtime_command_exec( observe_agent_runtime_command_exec(
root, root,
@@ -386,7 +386,9 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
) )
.await .await
} }
"blackboard.write" => observe_agent_runtime_blackboard_write(root, agent_id, &action.input), "blackboard.write" => {
observe_agent_runtime_blackboard_write(root, agent_id, run_id, &action.input)
}
"agent.message" => { "agent.message" => {
observe_agent_runtime_agent_message(root, agent_id, run_id, &action.input) observe_agent_runtime_agent_message(root, agent_id, run_id, &action.input)
} }
@@ -38,6 +38,7 @@ pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at(
&pending.action, &pending.action,
&waiting_observation, &waiting_observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
runtime.pending_tool_action = Some(pending.summary()); runtime.pending_tool_action = Some(pending.summary());
runtime.status = "waiting-for-user-input".to_string(); runtime.status = "waiting-for-user-input".to_string();
@@ -536,34 +536,13 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
root: &Path, root: &Path,
) -> Result<bool, String> { ) -> Result<bool, String> {
let manifest = read_manifest_for_project(root)?; let manifest = read_manifest_for_project(root)?;
let source = read_game_creator_agent_runtime_at(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) let current_root = current_autonomous_game_build_root_task_at(root)?.ok_or_else(|| {
.ok() "无法解析当前自主构建根 Run 的可信 source,拒绝按 GUI 完整 DAG 回退".to_string()
.filter(|runtime| { })?;
runtime.state.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD if !autonomous_game_build_root_task_is_active(&current_root) {
&& agent_runtime_supervisor_source_is_trusted(&runtime.state.source) return Ok(false);
}) }
.map(|runtime| runtime.state.source) let seed_task_ids = autonomous_manifest_seed_tasks_for_source(&current_root.source)
.or_else(|| {
let path = game_creator_agent_runtime_task_path(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
);
read_all_game_creator_agent_runtime_tasks(&path)
.ok()
.map(latest_game_creator_agent_runtime_tasks)
.and_then(|records| {
records.into_iter().rev().find_map(|record| {
(record.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& record.parent_run_id.is_none()
&& agent_runtime_supervisor_source_is_trusted(&record.source))
.then_some(record.source)
})
})
})
.ok_or_else(|| {
"无法解析当前自主构建根 Run 的可信 source,拒绝按 GUI 完整 DAG 回退".to_string()
})?;
let seed_task_ids = autonomous_manifest_seed_tasks_for_source(&source)
.into_iter() .into_iter()
.map(|task| task.id) .map(|task| task.id)
.collect::<BTreeSet<_>>(); .collect::<BTreeSet<_>>();
@@ -572,19 +551,58 @@ pub(in crate::agent) fn autonomous_manifest_dag_in_progress_at(
.iter() .iter()
.filter(|task| seed_task_ids.contains(&task.id)) .filter(|task| seed_task_ids.contains(&task.id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let started = seed_tasks
.iter()
.any(|task| task.status != GameCreationAppTaskStatus::Pending);
let running = seed_tasks let running = seed_tasks
.iter() .iter()
.any(|task| task.status == GameCreationAppTaskStatus::Running); .any(|task| task.status == GameCreationAppTaskStatus::Running);
let completed = seed_tasks
.iter()
.all(|task| task.status == GameCreationAppTaskStatus::Completed);
let failed = seed_tasks let failed = seed_tasks
.iter() .iter()
.any(|task| task.status == GameCreationAppTaskStatus::Failed); .any(|task| task.status == GameCreationAppTaskStatus::Failed);
Ok(started && running && !completed && !failed) let active_child = autonomous_manifest_parent_has_active_ready_task_at(
root,
&current_root.run_id,
&seed_task_ids,
)?;
Ok((running || active_child) && !failed)
}
fn autonomous_manifest_parent_has_active_ready_task_at(
root: &Path,
parent_run_id: &str,
seed_task_ids: &BTreeSet<String>,
) -> Result<bool, String> {
for task_id in seed_task_ids {
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(
&game_creator_agent_runtime_task_path(root, task_id),
)?);
for record in records {
if record.source != "agent-ready-task-scheduler"
|| record.parent_agent_id.as_deref()
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|| record.parent_run_id.as_deref() != Some(parent_run_id)
|| game_creator_agent_runtime_terminal_status(&record).is_some()
{
continue;
}
if record.run_id != autonomous_manifest_ready_task_run_id(parent_run_id, task_id) {
return Err(format!(
"当前自主构建父 Run 的活跃 child runId 不符合确定性绑定:taskId={task_id}"
));
}
let state = agent_runtime_state_from_task_record(&record);
let binding = autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
.ok_or_else(|| {
format!("当前自主构建父 Run 的活跃 child 缺少父绑定:taskId={task_id}")
})?;
if binding.root_run_id != parent_run_id {
return Err(format!(
"当前自主构建父 Run 的活跃 child rootRunId 不一致:taskId={task_id}"
));
}
return Ok(true);
}
}
Ok(false)
} }
pub(crate) fn validate_agent_runtime_autonomous_plan_liveness( pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
@@ -776,6 +776,7 @@ pub(in crate::agent) fn project_game_creator_agent_runtime_parallel_read_batch(
&pending.action, &pending.action,
&observation, &observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
runtime.pending_tool_action = None; runtime.pending_tool_action = None;
runtime.status = "running".to_string(); runtime.status = "running".to_string();
@@ -142,6 +142,7 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
blocker.detail.unwrap_or_default() blocker.detail.unwrap_or_default()
)); ));
} }
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)?;
let mut revision = read_game_creator_agent_runtime_project_revision(root)?; let mut revision = read_game_creator_agent_runtime_project_revision(root)?;
let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?;
let next_revision = revision let next_revision = revision
@@ -176,6 +177,159 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
Ok(next_revision) Ok(next_revision)
} }
pub(crate) fn ensure_current_autonomous_ready_child_mutation_at_locked(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<(), String> {
let normalized_agent_id = match normalize_game_creator_runtime_agent_id(agent_id) {
Ok(agent_id) => agent_id,
Err(_) => return Ok(()),
};
let binding =
read_game_creator_agent_runtime_run_profile_binding(root, &normalized_agent_id, run_id)?;
let Some(binding) = binding else {
let task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&normalized_agent_id,
run_id,
)?;
if task
.as_ref()
.is_some_and(|task| task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD)
{
return Err("autonomous Run 项目修改缺少 Run Profile binding,已失败关闭".to_string());
}
return Ok(());
};
if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Ok(());
}
if binding.agent_id != normalized_agent_id
|| binding.run_id != run_id
|| binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
{
return Err("autonomous Run 项目修改的 Run Profile 绑定身份不一致".to_string());
}
let task =
read_latest_game_creator_agent_runtime_task_by_run_id(root, &normalized_agent_id, run_id)?
.ok_or_else(|| {
"autonomous Run 项目修改缺少 durable task journal,已失败关闭".to_string()
})?;
if task.agent_id != binding.agent_id
|| task.run_id != binding.run_id
|| task.source != binding.source
|| task.run_profile != binding.profile
|| task.run_profile_binding_fingerprint != binding.binding_fingerprint
|| task.parent_agent_id != binding.parent_agent_id
|| task.parent_run_id != binding.parent_run_id
{
return Err("autonomous Run 项目修改的 durable task journal 与绑定不一致".to_string());
}
let is_root = binding.agent_id == binding.root_agent_id
&& binding.run_id == binding.root_run_id
&& binding.parent_agent_id.is_none()
&& binding.parent_run_id.is_none();
if is_root {
if task.parent_agent_id.is_some()
|| task.parent_run_id.is_some()
|| !agent_runtime_supervisor_source_is_trusted(&task.source)
{
return Err("autonomous 根 Run 项目修改的 durable identity 不一致".to_string());
}
} else {
let parent_agent_id = binding
.parent_agent_id
.as_deref()
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentAgentId".to_string())?;
let parent_run_id = binding
.parent_run_id
.as_deref()
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少 parentRunId".to_string())?;
let parent_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
parent_agent_id,
parent_run_id,
)?
.ok_or_else(|| "autonomous 派生 Run 项目修改缺少父 Run Profile binding".to_string())?;
if binding.parent_binding_fingerprint.as_deref()
!= Some(parent_binding.binding_fingerprint.as_str())
|| parent_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| parent_binding.root_agent_id != binding.root_agent_id
|| parent_binding.root_run_id != binding.root_run_id
{
return Err(
"autonomous 派生 Run 项目修改的父 binding 或 root identity 不一致".to_string(),
);
}
if binding.source == "agent-ready-task-scheduler" {
let state = agent_runtime_state_from_task_record(&task);
let ready_binding =
autonomous_manifest_ready_task_parent_binding_for_state_at(root, &state)?
.ok_or_else(|| {
"autonomous ready-task 项目修改缺少确定性父 Run 绑定".to_string()
})?;
if ready_binding != binding
|| state.run_id
!= autonomous_manifest_ready_task_run_id(
&binding.root_run_id,
&normalized_agent_id,
)
{
return Err("autonomous ready-task 项目修改的确定性父子身份不一致".to_string());
}
} else if binding.source == "agent-delegate"
&& task
.delegation_id
.as_deref()
.is_none_or(|delegation_id| delegation_id.trim().is_empty())
{
return Err("autonomous agent-delegate 项目修改缺少 delegationId".to_string());
}
}
if task.status != "running" || game_creator_agent_runtime_terminal_status(&task).is_some() {
return Err("autonomous Run 项目修改要求当前 durable task 仍为 running".to_string());
}
let current_root = current_autonomous_game_build_root_task_at(root)?
.ok_or_else(|| "autonomous Run 项目修改时当前根 Run 已不存在".to_string())?;
if current_root.run_id != binding.root_run_id {
return Err(format!(
"autonomous Run 已被更新根 Run 取代:currentRunId={}",
current_root.run_id
));
}
let current_root_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&current_root.agent_id,
&current_root.run_id,
)?
.ok_or_else(|| "autonomous Run 当前根缺少 Run Profile binding".to_string())?;
if current_root.agent_id != binding.root_agent_id
|| current_root.source != current_root_binding.source
|| current_root.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| current_root.parent_agent_id.is_some()
|| current_root.parent_run_id.is_some()
|| current_root.delegation_id.is_some()
|| current_root_binding.agent_id != binding.root_agent_id
|| current_root_binding.run_id != binding.root_run_id
|| current_root_binding.root_agent_id != current_root_binding.agent_id
|| current_root_binding.root_run_id != current_root_binding.run_id
|| current_root_binding.parent_agent_id.is_some()
|| current_root_binding.parent_run_id.is_some()
|| current_root_binding.binding_fingerprint != current_root.run_profile_binding_fingerprint
|| (is_root && current_root_binding.binding_fingerprint != binding.binding_fingerprint)
{
return Err("autonomous Run 当前根 journal 与 binding 不一致".to_string());
}
if !autonomous_game_build_root_task_is_active(&current_root) {
return Err(format!(
"autonomous Run 当前根已不再活跃:status={} phase={}",
current_root.status, current_root.phase
));
}
Ok(())
}
pub(crate) fn begin_agent_runtime_project_verification_locked( pub(crate) fn begin_agent_runtime_project_verification_locked(
root: &Path, root: &Path,
agent_id: &str, agent_id: &str,
@@ -689,6 +689,7 @@ pub(in crate::agent) fn persist_game_creator_agent_runtime_provider_batch_waitin
&pending.action, &pending.action,
observation, observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
complete_agent_runtime_active_plan_step( complete_agent_runtime_active_plan_step(
runtime, runtime,
@@ -788,6 +789,7 @@ pub(in crate::agent) fn project_game_creator_agent_runtime_provider_batch_abort(
&pending.action, &pending.action,
&observation, &observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
complete_agent_runtime_active_plan_step(runtime, "failed", &observation_summary); complete_agent_runtime_active_plan_step(runtime, "failed", &observation_summary);
runtime.pending_tool_action = None; runtime.pending_tool_action = None;
@@ -101,6 +101,8 @@ pub(crate) const AGENT_RUNTIME_ISOLATED_JOIN_SOURCE: &str = "agent-isolated-join
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE: &str = "project-supervisor-gui"; pub(crate) const AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE: &str = "project-supervisor-gui";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE: &str = "project-supervisor-cli"; pub(crate) const AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE: &str = "project-supervisor-cli";
pub(crate) const AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE: &str = "project-supervisor-game-chat"; pub(crate) const AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE: &str = "project-supervisor-game-chat";
pub(super) const GAME_CHAT_FIXED_TASK_GRAPH_STALLED_ERROR: &str =
"game-chat 首版固定任务图无法继续推进,拒绝回退到普通 Provider 协作波";
pub(crate) fn agent_runtime_supervisor_source_is_trusted(source: &str) -> bool { pub(crate) fn agent_runtime_supervisor_source_is_trusted(source: &str) -> bool {
matches!( matches!(
@@ -277,8 +279,12 @@ pub(crate) use provider_recovery::{
}; };
#[cfg(test)] #[cfg(test)]
pub(crate) use provider_recovery::{ pub(crate) use provider_recovery::{
drive_waiting_autonomous_manifest_parent_wake_budget_for_test,
ensure_waiting_provider_retry_records_for_test, ensure_waiting_provider_retry_records_for_test,
mark_autonomous_manifest_parent_wake_needs_reconciliation_for_test,
prepare_waiting_autonomous_manifest_parent_for_test,
probe_static_delegate_parent_wake_singleflight_coalescing, probe_static_delegate_parent_wake_singleflight_coalescing,
repair_autonomous_manifest_parent_wake_reconciliation_projection_for_test,
}; };
pub(crate) use recovery_scan::{ pub(crate) use recovery_scan::{
cleanup_game_creator_agent_runtime_completed_finalizations_at, cleanup_game_creator_agent_runtime_completed_finalizations_at,
@@ -293,10 +299,12 @@ pub(crate) use task_queue::{
run_game_creator_agent_background_task_with_context, run_game_creator_agent_background_task_with_context,
spawn_next_game_creator_agent_background_task_drain, spawn_next_game_creator_agent_background_task_drain,
spawn_next_game_creator_agent_background_task_drain_with_lock, spawn_next_game_creator_agent_background_task_drain_with_lock,
spawn_started_game_creator_agent_background_task_drain_with_lock,
}; };
#[cfg(test)] #[cfg(test)]
pub(crate) use task_start::start_game_creator_agent_background_task_with_session_lane_hook_at; pub(crate) use task_start::start_game_creator_agent_background_task_with_session_lane_hook_at;
pub(crate) use task_start::{ pub(crate) use task_start::{
autonomous_game_build_root_task_is_active, current_autonomous_game_build_root_task_at,
notify_external_agent_runner_after_background_task_enqueue, notify_external_agent_runner_after_background_task_enqueue,
project_autonomous_manifest_ready_task_terminal_at, project_autonomous_manifest_ready_task_terminal_at,
schedule_autonomous_game_build_ready_tasks_at, schedule_game_creator_agent_ready_tasks_at, schedule_autonomous_game_build_ready_tasks_at, schedule_game_creator_agent_ready_tasks_at,
@@ -584,8 +584,14 @@ pub(in crate::agent) fn read_game_creator_agent_runtime_with_session_filter_at(
} }
let recent_events = let recent_events =
read_recent_game_creator_agent_runtime_events_for_session(&event_path, session_id)?; read_recent_game_creator_agent_runtime_events_for_session(&event_path, session_id)?;
let task_snapshot = let task_snapshot = read_game_creator_agent_runtime_task_snapshot_for_session(
read_game_creator_agent_runtime_task_snapshot_for_session(&task_path, session_id)?; &task_path,
session_id,
(!state.run_id.trim().is_empty()).then_some(state.run_id.as_str()),
)?;
if state.started_at == 0 {
state.started_at = task_snapshot.run_started_at.unwrap_or(state.updated_at);
}
state.task_queue = task_snapshot.task_queue.clone(); state.task_queue = task_snapshot.task_queue.clone();
let response_stream = let response_stream =
visible_game_creator_agent_runtime_response_stream_at(root, &state).unwrap_or(None); visible_game_creator_agent_runtime_response_stream_at(root, &state).unwrap_or(None);
@@ -12,6 +12,7 @@ pub(crate) const GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX: &str =
"game-chat-first-playable-hard-budget-exhausted"; "game-chat-first-playable-hard-budget-exhausted";
const FALLBACK_THEME_MARKER: &str = "__GAME_CHAT_THEME__"; const FALLBACK_THEME_MARKER: &str = "__GAME_CHAT_THEME__";
pub(super) const GAME_CHAT_CODE_COMPLETION_REPAIR_STEP: &str = "修复 Runtime 完成门诊断并重新验证";
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct GameChatFastPathBudget { pub(crate) struct GameChatFastPathBudget {
@@ -313,6 +314,134 @@ fn game_chat_fast_path_has_art_manifest(root: &Path) -> bool {
}) })
} }
fn game_chat_english_words(task: &str) -> Vec<&str> {
task.split(|character: char| !character.is_ascii_alphanumeric())
.filter(|word| !word.is_empty())
.collect()
}
fn game_chat_english_words_contain_phrase(words: &[&str], phrase: &[&str]) -> bool {
words
.windows(phrase.len())
.any(|candidate| candidate == phrase)
}
fn game_chat_english_application_is_negated(words: &[&str], application_index: usize) -> bool {
let prefix = &words[application_index.saturating_sub(4)..application_index];
prefix
.iter()
.any(|word| matches!(*word, "not" | "never" | "dont"))
|| game_chat_english_words_contain_phrase(prefix, &["don", "t"])
|| ["refuse", "refuses", "refused"]
.iter()
.any(|refusal| prefix.ends_with(&[*refusal]) || prefix.ends_with(&[*refusal, "to"]))
}
fn game_chat_chinese_application_is_negated(clause: &str, application_index: usize) -> bool {
let prefix = clause[..application_index].trim_end();
[
"不要", "", "", "", "请勿", "禁止", "拒绝", "避免", "无需", "无须", "不能", "不可",
"不得", "不应",
]
.iter()
.any(|negation| prefix.ends_with(negation))
}
fn game_chat_chinese_clause_requests_existing_art_application(clause: &str) -> bool {
let names_art = ["美术资源", "美术素材", "已有素材", "现有素材"]
.iter()
.any(|marker| clause.contains(marker));
if !names_art {
return false;
}
let explicitly_names_existing_art = ["已有美术", "现有美术", "已有素材", "现有素材"]
.iter()
.any(|marker| clause.contains(marker));
let requests_new_art = [
"全新美术",
"新的美术",
"新美术",
"全新素材",
"新的素材",
"新素材",
"重新生成美术",
"重做美术",
]
.iter()
.any(|marker| clause.contains(marker));
if requests_new_art && !explicitly_names_existing_art {
return false;
}
["替换", "换成", "接入", "使用", "应用", "复用"]
.iter()
.any(|application| {
clause.match_indices(application).any(|(index, _)| {
// “换成” is also a suffix of “替换成”; the latter must be
// judged once at the beginning of the complete action.
!(*application == "换成" && clause[..index].ends_with('替'))
&& !game_chat_chinese_application_is_negated(clause, index)
})
})
}
fn game_chat_explicit_existing_art_reuse_intent(task: &str) -> bool {
let normalized = task.trim().to_ascii_lowercase();
let english_words = game_chat_english_words(&normalized);
let requests_existing_art_in_chinese = normalized
.split(|character: char| {
matches!(
character,
'' | '。' | '' | ';' | ',' | '.' | '' | '!' | '' | '?' | '\n' | '\r'
)
})
.any(game_chat_chinese_clause_requests_existing_art_application);
let requests_new_art_in_english = [["new", "art"], ["fresh", "art"], ["regenerate", "art"]]
.iter()
.any(|phrase| game_chat_english_words_contain_phrase(&english_words, phrase));
let requests_application_in_english = english_words.iter().enumerate().any(|(index, word)| {
matches!(*word, "replace" | "use" | "apply" | "reuse")
&& !game_chat_english_application_is_negated(&english_words, index)
});
let names_art_in_english = [
&["art", "asset"][..],
&["art", "assets"][..],
&["spritesheet"][..],
&["spritesheets"][..],
&["sprite", "sheet"][..],
&["sprite", "sheets"][..],
]
.iter()
.any(|phrase| game_chat_english_words_contain_phrase(&english_words, phrase));
requests_existing_art_in_chinese
|| (!requests_new_art_in_english && requests_application_in_english && names_art_in_english)
}
pub(in crate::agent) fn game_chat_existing_art_reuse_refinement_intent_at(
root: &Path,
task: &str,
) -> Result<bool, String> {
if !game_chat_explicit_existing_art_reuse_intent(task)
|| game_chat_fallback_targets_initial_placeholder(root)?
{
return Ok(false);
}
Ok(true)
}
pub(in crate::agent) fn game_chat_existing_art_reuse_refinement_is_valid_at(
root: &Path,
task: &str,
) -> Result<bool, String> {
if !game_chat_existing_art_reuse_refinement_intent_at(root, task)? {
return Ok(false);
}
Ok(game_chat_fast_path_has_visual_asset(root, "art-director")
&& game_chat_fast_path_has_visual_asset(root, "art-asset-plan")
&& game_chat_fast_path_has_art_manifest(root))
}
pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at(
root: &Path, root: &Path,
agent_id: &str, agent_id: &str,
@@ -727,6 +856,165 @@ fn game_chat_fast_path_verified_delivery_plan(
} }
} }
fn game_chat_fast_path_completion_repair_plan(
runtime: &AgentRuntimeState,
blocker: &AgentRuntimeToolObservation,
) -> Option<AgentRuntimeToolPlan> {
if !agent_runtime_has_structured_plan(runtime)
|| runtime
.plan_steps
.iter()
.any(|step| step.title == GAME_CHAT_CODE_COMPLETION_REPAIR_STEP)
|| runtime
.plan_steps
.iter()
.any(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED)
{
return None;
}
let first_non_terminal_index = runtime.plan_steps.iter().position(|step| {
step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED
&& step.status != AGENT_RUNTIME_PLAN_STATUS_FAILED
});
if first_non_terminal_index.is_none()
&& runtime.plan_steps.len() >= AGENT_RUNTIME_PLAN_STEP_LIMIT
{
return None;
}
let mut repair_inserted = false;
let mut steps = runtime
.plan_steps
.iter()
.enumerate()
.map(|(index, step)| {
if Some(index) == first_non_terminal_index {
repair_inserted = true;
return AgentRuntimePlanUpdateStep {
step: GAME_CHAT_CODE_COMPLETION_REPAIR_STEP.to_string(),
status: AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS.to_string(),
};
}
AgentRuntimePlanUpdateStep {
step: step.title.clone(),
status: if step.status == AGENT_RUNTIME_PLAN_STATUS_COMPLETED {
AGENT_RUNTIME_PLAN_STATUS_COMPLETED
} else {
AGENT_RUNTIME_PLAN_STATUS_PENDING
}
.to_string(),
}
})
.collect::<Vec<_>>();
if !repair_inserted {
steps.push(AgentRuntimePlanUpdateStep {
step: GAME_CHAT_CODE_COMPLETION_REPAIR_STEP.to_string(),
status: AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS.to_string(),
});
}
Some(AgentRuntimeToolPlan {
thinking_summary: "静态检查已通过,但 Runtime 完成门仍有明确诊断;重新打开修复计划并交给 Code Agent 处理。"
.to_string(),
plan_update: Some(AgentRuntimePlanUpdate {
explanation: format!(
"{}{}",
blocker.summary,
blocker.detail.as_deref().unwrap_or("请按完成门诊断继续修复")
),
steps,
}),
plan: Vec::new(),
actions: Vec::new(),
response: String::new(),
})
}
pub(super) fn game_chat_fast_path_external_repair_observation_at(
root: &Path,
runtime: &AgentRuntimeState,
) -> Option<AgentRuntimeToolObservation> {
if runtime.agent_id != "code-prototype"
|| !agent_runtime_has_structured_plan(runtime)
|| runtime
.plan_steps
.iter()
.any(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED)
|| (!runtime.plan_steps.iter().any(|step| {
step.title == GAME_CHAT_CODE_COMPLETION_REPAIR_STEP
|| step
.title
.starts_with(&format!("{GAME_CHAT_CODE_COMPLETION_REPAIR_STEP}"))
}) && runtime.plan_steps.iter().any(|step| {
step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED
&& step.status != AGENT_RUNTIME_PLAN_STATUS_FAILED
}))
{
return None;
}
autonomous_game_build_completion_blocker_at_locked(root, runtime).map(|mut blocker| {
blocker.summary = format!(
"{};结构化计划窗口已终态,进入外部 repair lane,只执行读取、实际 mutation 与重新验证",
blocker.summary
);
blocker
})
}
fn game_chat_fast_path_current_run_owns_mutation(
root: &Path,
runtime: &AgentRuntimeState,
gate: &AgentRuntimeVerificationGate,
revision: u64,
) -> Result<bool, String> {
let Some(tool) = gate
.last_mutation_tool
.as_deref()
.filter(|_| gate.mutation_revision == Some(revision))
else {
return Ok(false);
};
let Some(last_mutation_call) = runtime
.recent_tool_calls
.iter()
.rev()
.find(|call| call.tool == tool)
else {
return Ok(false);
};
let (Some(action_id), Some(action_fingerprint)) = (
last_mutation_call.action_id.as_deref(),
last_mutation_call.action_fingerprint.as_deref(),
) else {
return Ok(false);
};
if last_mutation_call.status != "ok"
|| !is_valid_agent_runtime_action_id(action_id)
|| !is_valid_agent_runtime_action_fingerprint(action_fingerprint)
{
return Ok(false);
}
let (records, _) =
read_agent_db_records_bounded(root, AGENT_RUNTIME_ACTION_HISTORY_MAX_DB_BYTES)?;
Ok(records.iter().rev().any(|record| {
record.get("recordType").and_then(serde_json::Value::as_str)
== Some(AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE)
&& record.get("agentId").and_then(serde_json::Value::as_str)
== Some(runtime.agent_id.as_str())
&& record.get("taskId").and_then(serde_json::Value::as_str)
== Some(runtime.task_id.as_str())
&& record.get("sessionId").and_then(serde_json::Value::as_str)
== Some(runtime.session_id.as_str())
&& record.get("runId").and_then(serde_json::Value::as_str)
== Some(runtime.run_id.as_str())
&& record.get("actionId").and_then(serde_json::Value::as_str) == Some(action_id)
&& record
.get("actionFingerprint")
.and_then(serde_json::Value::as_str)
== Some(action_fingerprint)
&& record.get("tool").and_then(serde_json::Value::as_str) == Some(tool)
&& record.get("status").and_then(serde_json::Value::as_str) == Some("ok")
}))
}
fn game_chat_fast_path_current_revision_is_verified( fn game_chat_fast_path_current_revision_is_verified(
root: &Path, root: &Path,
runtime: &AgentRuntimeState, runtime: &AgentRuntimeState,
@@ -898,13 +1186,29 @@ pub(crate) fn game_chat_fast_path_plan_at(
} }
} }
"code-prototype" => { "code-prototype" => {
if agent_runtime_has_structured_plan(runtime)
&& runtime
.plan_steps
.iter()
.any(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED)
{
return Err(
"game-chat code-prototype 结构化计划已包含 failed 步骤,拒绝继续空转"
.to_string(),
);
}
let revision = read_game_creator_agent_runtime_project_revision(root)?; let revision = read_game_creator_agent_runtime_project_revision(root)?;
let gate = read_game_creator_agent_runtime_verification_gate( let gate = read_game_creator_agent_runtime_verification_gate(
root, root,
&runtime.agent_id, &runtime.agent_id,
&runtime.run_id, &runtime.run_id,
)?; )?;
let owns_current_mutation = gate.mutation_revision == Some(revision.revision); let owns_current_mutation = game_chat_fast_path_current_run_owns_mutation(
root,
runtime,
&gate,
revision.revision,
)?;
let current_revision_verified = owns_current_mutation let current_revision_verified = owns_current_mutation
&& gate.verified_revision == Some(revision.revision) && gate.verified_revision == Some(revision.revision)
&& gate.last_verification_status.as_deref() && gate.last_verification_status.as_deref()
@@ -914,6 +1218,13 @@ pub(crate) fn game_chat_fast_path_plan_at(
== Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED); == Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED);
if current_revision_verified { if current_revision_verified {
if let Some(blocker) =
autonomous_game_build_completion_blocker_at_locked(root, runtime)
{
return Ok(game_chat_fast_path_completion_repair_plan(
runtime, &blocker,
));
}
return Ok(Some(game_chat_fast_path_verified_delivery_plan( return Ok(Some(game_chat_fast_path_verified_delivery_plan(
runtime, runtime,
"首个可玩版本代码已生成并通过静态自检。", "首个可玩版本代码已生成并通过静态自检。",
@@ -1650,6 +1961,53 @@ mod tests {
); );
} }
#[test]
fn existing_art_reuse_intent_requires_whole_english_action_words_and_rejects_negation() {
let accepted = [
"Use existing art assets in the current game.",
"Please REUSE the existing art assets.",
"Replace placeholders with current art assets.",
"Apply the existing spritesheet to the UI.",
"Reuse the sprite-sheet for the falling blocks.",
"Use existing art assets; do not generate new ones.",
"使用现有素材,不要重新生成。",
"请复用已有美术资源。",
"不要重新生成美术,继续接入现有素材。",
];
for task in accepted {
assert!(
game_chat_explicit_existing_art_reuse_intent(task),
"expected existing-art reuse intent: {task}"
);
}
let rejected = [
"Do not use existing art assets.",
"Do NOT use the existing art assets.",
"Don't reuse existing art assets.",
"Never apply the existing spritesheet.",
"Do not replace the UI with existing art assets.",
"We refuse to use existing art assets.",
"Refuse art assets.",
"Misuse art assets.",
"These are useful art assets.",
"Discuss art assets because they exist.",
"Create new art assets.",
"Regenerate art assets.",
"不要复用已有素材。",
"不要接入现有素材。",
"不应用已有美术资源。",
"别替换成现有素材。",
"不要使用现有素材,改为重做美术。",
];
for task in rejected {
assert!(
!game_chat_explicit_existing_art_reuse_intent(task),
"expected no existing-art reuse intent: {task}"
);
}
}
#[test] #[test]
fn art_slice_completion_validation_rejects_tampering_and_duplicate_pixels() { fn art_slice_completion_validation_rejects_tampering_and_duplicate_pixels() {
let temporary = tempfile::tempdir().expect("create slice validation project"); let temporary = tempfile::tempdir().expect("create slice validation project");
@@ -891,6 +891,7 @@ pub(in crate::agent) fn advance_game_creator_agent_runtime_provider_batch_gate(
&next_pending.action, &next_pending.action,
&observation, &observation,
Some(&next_pending.action_id), Some(&next_pending.action_id),
Some(&next_pending.action_fingerprint),
); );
activate_agent_runtime_plan_step( activate_agent_runtime_plan_step(
runtime, runtime,
@@ -112,9 +112,7 @@ async fn request_game_creator_agent_tool_plan_with_game_chat_budget_at(
.map(RequestedAgentRuntimeToolPlanOutcome::Ready); .map(RequestedAgentRuntimeToolPlanOutcome::Ready);
} }
if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
return Err( return Err(GAME_CHAT_FIXED_TASK_GRAPH_STALLED_ERROR.to_string());
"game-chat 首版固定任务图无法继续推进,拒绝回退到普通 Provider 协作波".to_string(),
);
} }
let Some(timeout) = game_chat_fast_path_provider_timeout(&budget) else { let Some(timeout) = game_chat_fast_path_provider_timeout(&budget) else {
if runtime.agent_id == "code-prototype" { if runtime.agent_id == "code-prototype" {
@@ -127,13 +125,17 @@ async fn request_game_creator_agent_tool_plan_with_game_chat_budget_at(
} }
return Err("game-chat 首版软预算已耗尽,拒绝继续请求 Provider".to_string()); return Err("game-chat 首版软预算已耗尽,拒绝继续请求 Provider".to_string());
}; };
let mut provider_observations = observations.to_vec();
if let Some(blocker) = game_chat_fast_path_external_repair_observation_at(root, runtime) {
provider_observations.push(blocker);
}
let provider_request = request_game_creator_agent_background_tool_plan_at( let provider_request = request_game_creator_agent_background_tool_plan_at(
root, root,
&runtime.agent_id, &runtime.agent_id,
&runtime.session_id, &runtime.session_id,
&runtime.run_id, &runtime.run_id,
task, task,
observations, &provider_observations,
loop_index, loop_index,
runtime.applied_steer_cursor, runtime.applied_steer_cursor,
); );
@@ -714,10 +716,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
} else { } else {
false false
}; };
let autonomous_manifest_can_wait = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID let autonomous_manifest_parent_can_wait = agent_id
== GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& !game_chat_hard_budget_expired && !game_chat_hard_budget_expired
&& !autonomous_registered_derived_visuals_need_repair_at(&root)
&& !game_creator_agent_runtime_provider_action_batch_exists( && !game_creator_agent_runtime_provider_action_batch_exists(
&root, &root,
&agent_id, &agent_id,
@@ -732,32 +734,47 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
&& isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id).is_none() && isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id).is_none()
&& static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id) && static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
.is_none(); .is_none();
if autonomous_manifest_can_wait { if autonomous_manifest_parent_can_wait {
if let Err(error) = schedule_autonomous_game_build_ready_tasks_at( // 已登记但损坏的派生视觉需要先由父 Run 规划修复,因此此时不再调度新的
&root, // manifest child;但已经持久化并运行的 child 仍是当前 DAG 的活跃工作,
&agent_id, // 父 Run 必须继续等待,不能提前落入 game-chat fixed-graph-stalled。
&runtime.run_id, let scheduled_ready_tasks =
3, if autonomous_registered_derived_visuals_need_repair_at(&root) {
) { Vec::new()
return fail_game_creator_agent_background_context_at( } else {
&root, match schedule_autonomous_game_build_ready_tasks_at(
&agent_id,
&session_id,
runtime,
&format!("调度自主构建 manifest 任务失败:{error}"),
);
}
let manifest_in_progress = match autonomous_manifest_dag_in_progress_at(&root) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root, &root,
&agent_id, &agent_id,
&session_id, &runtime.run_id,
runtime, 3,
&format!("读取自主构建 manifest 等待屏障失败:{error}"), ) {
); Ok(tasks) => tasks,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("调度自主构建 manifest 任务失败:{error}"),
);
}
}
};
let manifest_in_progress = if scheduled_ready_tasks.is_empty() {
match autonomous_manifest_dag_in_progress_at(&root) {
Ok(value) => value,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取自主构建 manifest 等待屏障失败:{error}"),
);
}
} }
} else {
true
}; };
if manifest_in_progress { if manifest_in_progress {
let blocker = let blocker =
@@ -1264,9 +1281,12 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
&runtime.run_id, &runtime.run_id,
); );
let verified_delivery = match verification_gate { let verified_delivery = match verification_gate {
Ok(gate) => agent_runtime_autonomous_verified_delivery_allows_plan_completion( Ok(gate) => {
&agent_id, &gate, agent_runtime_autonomous_verified_delivery_allows_plan_completion(
), &agent_id, &gate,
) && autonomous_game_build_completion_blocker_at_locked(&root, &runtime)
.is_none()
}
Err(error) => { Err(error) => {
return fail_game_creator_agent_background_context_at( return fail_game_creator_agent_background_context_at(
&root, &root,
@@ -2700,6 +2720,9 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
observation_action_identity observation_action_identity
.as_ref() .as_ref()
.map(|identity| identity.0.as_str()), .map(|identity| identity.0.as_str()),
observation_action_identity
.as_ref()
.map(|identity| identity.1.as_str()),
); );
if observation.is_waiting_for_confirmation() { if observation.is_waiting_for_confirmation() {
let mut pending_action = durable_action let mut pending_action = durable_action
File diff suppressed because it is too large Load Diff
@@ -606,6 +606,7 @@ async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary(
&action, &action,
&observation, &observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
complete_agent_runtime_active_plan_step( complete_agent_runtime_active_plan_step(
&mut runtime, &mut runtime,
@@ -662,6 +663,7 @@ async fn continue_game_creator_agent_pending_tool_action_within_stack_boundary(
&action, &action,
&observation, &observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
complete_agent_runtime_active_plan_step( complete_agent_runtime_active_plan_step(
&mut runtime, &mut runtime,
@@ -1093,6 +1095,7 @@ pub(in crate::agent) fn mark_game_creator_agent_runtime_tool_observation_needs_r
&pending.action, &pending.action,
observation, observation,
Some(&pending.action_id), Some(&pending.action_id),
Some(&pending.action_fingerprint),
); );
// needs-reconciliation 是外部结果未知边界,不是结构化计划步骤的确定失败。 // needs-reconciliation 是外部结果未知边界,不是结构化计划步骤的确定失败。
// 保持 active,后续同一 action 对账成功时才能完成该步骤,并让持久 context // 保持 active,后续同一 action 对账成功时才能完成该步骤,并让持久 context
File diff suppressed because it is too large Load Diff
@@ -670,6 +670,19 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
resume_external_agent_runner(root)?; resume_external_agent_runner(root)?;
return read_game_creator_agent_runtimes_at(root); return read_game_creator_agent_runtimes_at(root);
} }
if let Some(runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)? {
if let Some(result) = repair_autonomous_manifest_parent_wake_reconciliation_projection_at(
root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
)? {
drop(runtime_lock);
return Ok(vec![result]);
}
drop(runtime_lock);
}
cleanup_orphaned_platform_art_generation_runtime_states_at(root)?; cleanup_orphaned_platform_art_generation_runtime_states_at(root)?;
let agent_ids = collect_game_creator_agent_runtime_agent_ids(root)?; let agent_ids = collect_game_creator_agent_runtime_agent_ids(root)?;
if !current_game_creator_agent_runtime_finalization_exists_at(root, &agent_ids)? { if !current_game_creator_agent_runtime_finalization_exists_at(root, &agent_ids)? {
@@ -977,16 +990,26 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
} }
} }
if task.phase == "waiting-for-manifest-tasks" { if task.phase == "waiting-for-manifest-tasks" {
if let Err(error) = let scheduled_ready_tasks = match schedule_autonomous_game_build_ready_tasks_at(
schedule_autonomous_game_build_ready_tasks_at(root, &task.agent_id, &task.run_id, 3) root,
{ &task.agent_id,
drop(runtime_lock); &task.run_id,
mark_autonomous_manifest_parent_wake_needs_reconciliation_at( 3,
root, ) {
&agent_id, Ok(tasks) => tasks,
&task.run_id, Err(error) => {
&format!("Runner 重启恢复 manifest 任务图失败:{error}"), drop(runtime_lock);
)?; mark_autonomous_manifest_parent_wake_needs_reconciliation_at(
root,
&agent_id,
&task.run_id,
&format!("Runner 重启恢复 manifest 任务图失败:{error}"),
)?;
resumed.push(read_game_creator_agent_runtime_at(root, &agent_id)?);
continue;
}
};
if !scheduled_ready_tasks.is_empty() {
resumed.push(read_game_creator_agent_runtime_at(root, &agent_id)?); resumed.push(read_game_creator_agent_runtime_at(root, &agent_id)?);
continue; continue;
} }
@@ -203,6 +203,79 @@ pub(crate) fn spawn_next_game_creator_agent_background_task_drain_with_lock(
}); });
} }
pub(crate) fn spawn_started_game_creator_agent_background_task_drain_with_lock(
root: &Path,
agent_id: &str,
first_task: String,
first_state: AgentRuntimeState,
runtime_lock: AgentRuntimeTaskLock,
) -> Result<(), (String, AgentRuntimeTaskLock)> {
debug_assert!(!external_agent_runner_owns_background_execution());
#[cfg(test)]
let first_poll_delay = {
let path = root.join(format!(
".agent/runtime/test-delay-started-task-first-poll-{agent_id}"
));
let delay = fs::read_to_string(&path)
.ok()
.and_then(|value| value.trim().parse::<u64>().ok())
.map(|milliseconds| Duration::from_millis(milliseconds.min(5_000)))
.unwrap_or_default();
let _ = fs::remove_file(path);
delay
};
let root = root.to_path_buf();
let agent_id = agent_id.to_string();
let (first_poll_sender, first_poll_receiver) = std::sync::mpsc::sync_channel(1);
let (runtime_lock_sender, runtime_lock_receiver) = std::sync::mpsc::sync_channel(1);
let worker_name = format!(
"agent-ready-task-{}",
sanitize_agent_runtime_text(&agent_id, 48)
);
if let Err(error) = std::thread::Builder::new()
.name(worker_name)
.stack_size(16 * 1024 * 1024)
.spawn(move || {
#[cfg(test)]
if !first_poll_delay.is_zero() {
std::thread::sleep(first_poll_delay);
}
tauri::async_runtime::block_on(async move {
if first_poll_sender.send(()).is_err() {
return;
}
let Ok(runtime_lock) = runtime_lock_receiver.recv() else {
return;
};
let _runtime_lock = runtime_lock;
drain_game_creator_agent_background_tasks(root, agent_id, first_task, first_state)
.await;
});
})
{
return Err((
format!("创建 Agent Runtime child execution worker 失败:{error}"),
runtime_lock,
));
}
if first_poll_receiver
.recv_timeout(Duration::from_secs(2))
.is_err()
{
return Err((
"Agent Runtime child execution future 未在 2 秒内开始轮询".to_string(),
runtime_lock,
));
}
if let Err(error) = runtime_lock_sender.send(runtime_lock) {
return Err((
"Agent Runtime child execution future 在接管执行锁前已退出".to_string(),
error.0,
));
}
Ok(())
}
pub(in crate::agent) fn fail_game_creator_agent_background_context_at( pub(in crate::agent) fn fail_game_creator_agent_background_context_at(
root: &Path, root: &Path,
agent_id: &str, agent_id: &str,
@@ -616,7 +616,7 @@ fn validate_autonomous_game_build_ready_task_parent_at(
Ok(binding) Ok(binding)
} }
fn autonomous_game_build_root_task_is_active(task: &AgentRuntimeTaskRecord) -> bool { pub(crate) fn autonomous_game_build_root_task_is_active(task: &AgentRuntimeTaskRecord) -> bool {
matches!( matches!(
task.status.as_str(), task.status.as_str(),
"pending" | "running" | "waiting-for-confirmation" | "waiting-for-user-input" "pending" | "running" | "waiting-for-confirmation" | "waiting-for-user-input"
@@ -626,7 +626,7 @@ fn autonomous_game_build_root_task_is_active(task: &AgentRuntimeTaskRecord) -> b
) )
} }
fn current_autonomous_game_build_root_task_at( pub(crate) fn current_autonomous_game_build_root_task_at(
root: &Path, root: &Path,
) -> Result<Option<AgentRuntimeTaskRecord>, String> { ) -> Result<Option<AgentRuntimeTaskRecord>, String> {
let path = game_creator_agent_runtime_task_path(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID); let path = game_creator_agent_runtime_task_path(root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID);
@@ -1042,8 +1042,11 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
} }
let mut results = Vec::new(); let mut results = Vec::new();
for (task, result, record, needs_notification, runtime_lock) in scheduled { for (task, mut result, record, needs_notification, runtime_lock) in scheduled {
append_agent_db_record( // This record is diagnostic only. The durable child task and manifest
// transition already exist, so an audit sink failure must not strand
// the child in queued before its execution worker is started.
let _ = append_agent_db_record(
root, root,
serde_json::json!({ serde_json::json!({
"recordType": "agent.runtime.autonomous_ready_task.scheduled", "recordType": "agent.runtime.autonomous_ready_task.scheduled",
@@ -1059,14 +1062,117 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at(
"role": task.role.clone(), "role": task.role.clone(),
"recovered": needs_notification, "recovered": needs_notification,
}), }),
)?; );
if game_creator_agent_runtime_terminal_status(&record).is_none() { if game_creator_agent_runtime_terminal_status(&record).is_none() {
if let Some(runtime_lock) = runtime_lock { if let Some(runtime_lock) = runtime_lock {
spawn_next_game_creator_agent_background_task_drain_with_lock( if external_agent_runner_owns_background_execution() {
root, spawn_next_game_creator_agent_background_task_drain_with_lock(
&record.agent_id, root,
runtime_lock, &record.agent_id,
); runtime_lock,
);
} else {
match (|| {
#[cfg(test)]
{
let injected_failure = root.join(format!(
".agent/runtime/test-fail-autonomous-ready-task-start-{}",
record.agent_id
));
if fs::remove_file(injected_failure).is_ok() {
return Err(
"测试注入 autonomous ready-task child 启动失败".to_string()
);
}
}
start_game_creator_agent_runtime_task_for_session_at(
root,
&record.agent_id,
Some(&record.session_id),
&record.task,
&record.run_id,
&record.source,
"后台任务从队列开始执行",
game_creator_agent_background_task_default_plan(),
)
})() {
Ok(state) => {
let _ = append_game_creator_agent_background_task_started_record(
root, &state,
);
let launch =
spawn_started_game_creator_agent_background_task_drain_with_lock(
root,
&record.agent_id,
record.task.clone(),
state.clone(),
runtime_lock,
);
match launch {
Ok(()) => {
result.state = state.clone();
result.task_queue = state.task_queue.clone();
}
Err((error, runtime_lock)) => {
let error = format!(
"autonomous ready-task child execution 启动失败:taskId={}{error}",
task.id
);
let failure = fail_game_creator_agent_runtime_turn_at(
root, state, &error,
);
let error = match failure.and_then(|failed| {
project_autonomous_manifest_ready_task_terminal_at(
root, &failed,
)?;
Ok(failed)
}) {
Ok(failed) => {
result.task_queue = failed.task_queue.clone();
result.state = failed;
error
}
Err(persistence_error) => format!(
"{error};失败状态或任务图收口失败:{persistence_error}"
),
};
drop(runtime_lock);
if first_error.is_none() {
first_error = Some(error);
}
}
}
}
Err(error) => {
let error = format!(
"autonomous ready-task child 启动失败:taskId={}{error}",
task.id
);
let fallback = agent_runtime_state_from_task_record(&record);
let failure =
fail_game_creator_agent_runtime_turn_at(root, fallback, &error);
let error = match failure.and_then(|failed| {
project_autonomous_manifest_ready_task_terminal_at(root, &failed)?;
Ok(failed)
}) {
Ok(failed) => {
result.task_queue = failed.task_queue.clone();
result.state = failed;
error
}
Err(persistence_error) => {
format!(
"{error};失败状态或任务图收口失败:{persistence_error}"
)
}
};
drop(runtime_lock);
if first_error.is_none() {
first_error = Some(error);
}
}
}
}
} }
} }
if needs_notification if needs_notification
@@ -251,6 +251,11 @@ pub(super) fn start_game_creator_agent_runtime_task_for_session_in_session_lane_
.ok() .ok()
.map(|result| result.state); .map(|result| result.state);
let mut state = default_game_creator_agent_runtime_state(&agent_id, &run_id); let mut state = default_game_creator_agent_runtime_state(&agent_id, &run_id);
state.started_at = queued_task_record
.as_ref()
.map(|record| record.updated_at)
.filter(|updated_at| *updated_at > 0)
.unwrap_or_else(unix_timestamp);
state.session_id = session_id; state.session_id = session_id;
state.source = source.trim().to_string(); state.source = source.trim().to_string();
let (run_profile, run_profile_binding_fingerprint) = agent_runtime_run_profile_identity_at( let (run_profile, run_profile_binding_fingerprint) = agent_runtime_run_profile_identity_at(
@@ -286,6 +291,9 @@ pub(super) fn start_game_creator_agent_runtime_task_for_session_in_session_lane_
&& previous_state.source == state.source && previous_state.source == state.source
&& previous_state.current_task == state.current_task; && previous_state.current_task == state.current_task;
if same_runtime_run { if same_runtime_run {
if previous_state.started_at > 0 {
state.started_at = previous_state.started_at;
}
state.loop_iteration = previous_state.loop_iteration; state.loop_iteration = previous_state.loop_iteration;
state.max_loop_iterations = previous_state.max_loop_iterations; state.max_loop_iterations = previous_state.max_loop_iterations;
state.tool_action_budget = previous_state.tool_action_budget; state.tool_action_budget = previous_state.tool_action_budget;
@@ -1453,6 +1461,7 @@ pub(crate) fn default_game_creator_agent_runtime_state(
context_usage: AgentRuntimeContextUsage::default(), context_usage: AgentRuntimeContextUsage::default(),
last_response: None, last_response: None,
error: None, error: None,
started_at: 0,
updated_at: unix_timestamp(), updated_at: unix_timestamp(),
} }
} }
@@ -2823,6 +2832,17 @@ pub(super) fn append_unique_game_creator_agent_runtime_pending_task(
requested_run_profile: Option<&str>, requested_run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>, task_link: Option<&AgentRuntimeTaskLink>,
) -> Result<AgentRuntimeTaskRecord, String> { ) -> Result<AgentRuntimeTaskRecord, String> {
let autonomous_root_project_lock = (agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& task_link.is_none()
&& requested_run_profile
.is_some_and(|profile| profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD))
.then(|| {
acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.autonomous-root.create",
)
})
.transpose()?;
let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?; let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?;
let run_id = unique_game_creator_agent_runtime_run_id(root, agent_id, requested_run_id)?; let run_id = unique_game_creator_agent_runtime_run_id(root, agent_id, requested_run_id)?;
let run_profile_binding = bind_game_creator_agent_runtime_run_profile_at( let run_profile_binding = bind_game_creator_agent_runtime_run_profile_at(
@@ -2865,6 +2885,8 @@ pub(super) fn append_unique_game_creator_agent_runtime_pending_task(
updated_at: unix_timestamp(), updated_at: unix_timestamp(),
}; };
append_game_creator_agent_runtime_task_record_unlocked(root, &record)?; append_game_creator_agent_runtime_task_record_unlocked(root, &record)?;
drop(_journal_lock);
drop(autonomous_root_project_lock);
if let Err(error) = ensure_autonomous_completion_contract_for_task_at(root, &record) { if let Err(error) = ensure_autonomous_completion_contract_for_task_at(root, &record) {
let public_error = redact_agent_runtime_project_paths(root, &error, 500); let public_error = redact_agent_runtime_project_paths(root, &error, 500);
let failed = AgentRuntimeTaskRecord { let failed = AgentRuntimeTaskRecord {
@@ -2882,7 +2904,7 @@ pub(super) fn append_unique_game_creator_agent_runtime_pending_task(
Ok(record) Ok(record)
} }
pub(super) fn append_or_read_exact_game_creator_agent_runtime_pending_task( pub(crate) fn append_or_read_exact_game_creator_agent_runtime_pending_task(
root: &Path, root: &Path,
agent_id: &str, agent_id: &str,
session_id: &str, session_id: &str,
@@ -3111,19 +3133,32 @@ pub(super) fn read_recent_game_creator_agent_runtime_events_for_session(
pub(super) struct AgentRuntimeTaskSnapshot { pub(super) struct AgentRuntimeTaskSnapshot {
pub(super) task_queue: AgentRuntimeTaskQueueSummary, pub(super) task_queue: AgentRuntimeTaskQueueSummary,
pub(super) recent_tasks: Vec<AgentRuntimeTaskRecord>, pub(super) recent_tasks: Vec<AgentRuntimeTaskRecord>,
pub(super) run_started_at: Option<u64>,
} }
pub(super) fn read_game_creator_agent_runtime_task_snapshot( pub(super) fn read_game_creator_agent_runtime_task_snapshot(
path: &Path, path: &Path,
) -> Result<AgentRuntimeTaskSnapshot, String> { ) -> Result<AgentRuntimeTaskSnapshot, String> {
read_game_creator_agent_runtime_task_snapshot_for_session(path, None) read_game_creator_agent_runtime_task_snapshot_for_session(path, None, None)
} }
pub(super) fn read_game_creator_agent_runtime_task_snapshot_for_session( pub(super) fn read_game_creator_agent_runtime_task_snapshot_for_session(
path: &Path, path: &Path,
session_id: Option<&str>, session_id: Option<&str>,
run_id: Option<&str>,
) -> Result<AgentRuntimeTaskSnapshot, String> { ) -> Result<AgentRuntimeTaskSnapshot, String> {
let records = read_all_game_creator_agent_runtime_tasks(path)?; let records = read_all_game_creator_agent_runtime_tasks(path)?;
let run_started_at = run_id.and_then(|run_id| {
records
.iter()
.filter(|record| {
record.run_id == run_id
&& session_id.map_or(true, |session_id| record.session_id == session_id)
})
.map(|record| record.updated_at)
.filter(|updated_at| *updated_at > 0)
.min()
});
let latest = latest_game_creator_agent_runtime_tasks(records) let latest = latest_game_creator_agent_runtime_tasks(records)
.into_iter() .into_iter()
.filter(|record| session_id.map_or(true, |session_id| record.session_id == session_id)) .filter(|record| session_id.map_or(true, |session_id| record.session_id == session_id))
@@ -3136,6 +3171,7 @@ pub(super) fn read_game_creator_agent_runtime_task_snapshot_for_session(
Ok(AgentRuntimeTaskSnapshot { Ok(AgentRuntimeTaskSnapshot {
task_queue, task_queue,
recent_tasks: recent, recent_tasks: recent,
run_started_at,
}) })
} }
@@ -26,6 +26,7 @@ pub(in crate::agent) fn observe_agent_runtime_memory(
pub(in crate::agent) fn observe_agent_runtime_memory_write( pub(in crate::agent) fn observe_agent_runtime_memory_write(
root: &Path, root: &Path,
agent_id: &str, agent_id: &str,
run_id: &str,
input: &serde_json::Value, input: &serde_json::Value,
) -> AgentRuntimeToolObservation { ) -> AgentRuntimeToolObservation {
let scope = input let scope = input
@@ -105,6 +106,16 @@ pub(in crate::agent) fn observe_agent_runtime_memory_write(
}; };
} }
}; };
if let Err(error) =
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)
{
return AgentRuntimeToolObservation {
tool: "memory.write".to_string(),
status: "failed".to_string(),
summary: sanitize_agent_runtime_text(&error, 240),
detail: None,
};
}
if let Err(error) = advance_agent_runtime_project_revision_locked(root) { if let Err(error) = advance_agent_runtime_project_revision_locked(root) {
return agent_runtime_revision_advance_failure_observation(root, "memory.write", &error); return agent_runtime_revision_advance_failure_observation(root, "memory.write", &error);
} }
@@ -3,6 +3,7 @@ use super::*;
pub(in crate::agent) fn observe_agent_runtime_blackboard_write( pub(in crate::agent) fn observe_agent_runtime_blackboard_write(
root: &Path, root: &Path,
agent_id: &str, agent_id: &str,
run_id: &str,
input: &serde_json::Value, input: &serde_json::Value,
) -> AgentRuntimeToolObservation { ) -> AgentRuntimeToolObservation {
let content = agent_runtime_tool_input_text(input, &["content", "summary", "message"]); let content = agent_runtime_tool_input_text(input, &["content", "summary", "message"]);
@@ -25,6 +26,16 @@ pub(in crate::agent) fn observe_agent_runtime_blackboard_write(
}; };
} }
}; };
if let Err(error) =
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)
{
return AgentRuntimeToolObservation {
tool: "blackboard.write".to_string(),
status: "failed".to_string(),
summary: sanitize_agent_runtime_text(&error, 240),
detail: None,
};
}
if let Err(error) = advance_agent_runtime_project_revision_locked(root) { if let Err(error) = advance_agent_runtime_project_revision_locked(root) {
return agent_runtime_revision_advance_failure_observation( return agent_runtime_revision_advance_failure_observation(
root, root,
@@ -112,6 +123,16 @@ pub(crate) fn observe_agent_runtime_agent_message(
}; };
} }
}; };
if let Err(error) =
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)
{
return AgentRuntimeToolObservation {
tool: "agent.message".to_string(),
status: "failed".to_string(),
summary: sanitize_agent_runtime_text(&error, 240),
detail: None,
};
}
let content = truncate_agent_runtime_text(sanitize_prompt_context(&content).as_str(), 1_200); let content = truncate_agent_runtime_text(sanitize_prompt_context(&content).as_str(), 1_200);
let message = format!("来自 {agent_id} 的定向消息:{content}"); let message = format!("来自 {agent_id} 的定向消息:{content}");
let result = resolve_agent_conversation_session_id_at(root, &target_agent_id, None, true) let result = resolve_agent_conversation_session_id_at(root, &target_agent_id, None, true)
@@ -397,6 +418,16 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
detail: None, detail: None,
}; };
} }
if let Err(error) =
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, parent_run_id)
{
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: sanitize_agent_runtime_text(&error, 240),
detail: None,
};
}
let action_identity = action_id let action_identity = action_id
.filter(|value| !value.trim().is_empty()) .filter(|value| !value.trim().is_empty())
.map(str::to_string) .map(str::to_string)
@@ -359,13 +359,13 @@ pub(in crate::agent) fn wake_waiting_autonomous_manifest_parent_run_at(
return Err("manifest parent-wake 只允许自主构建根 Supervisor".to_string()); return Err("manifest parent-wake 只允许自主构建根 Supervisor".to_string());
} }
schedule_autonomous_game_build_ready_tasks_at( let scheduled_ready_tasks = schedule_autonomous_game_build_ready_tasks_at(
root, root,
&current_task.agent_id, &current_task.agent_id,
&current_task.run_id, &current_task.run_id,
3, 3,
)?; )?;
if autonomous_manifest_dag_in_progress_at(root)? { if !scheduled_ready_tasks.is_empty() || autonomous_manifest_dag_in_progress_at(root)? {
return Ok(false); return Ok(false);
} }

Some files were not shown because too many files have changed in this diff Show More