diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs index 6ad293d45..45b158597 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs @@ -1,12519 +1,138 @@ use super::*; -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(rename_all = "camelCase")] -pub(crate) struct AgentRuntimePendingToolAction { - pub(crate) schema_version: String, - pub(crate) fingerprint_version: String, - pub(crate) agent_id: String, - pub(crate) task_id: String, - pub(crate) session_id: String, - pub(crate) run_id: String, - pub(crate) source: String, - #[serde(default = "default_agent_runtime_run_profile")] - pub(crate) run_profile: String, - #[serde(default)] - pub(crate) run_profile_binding_fingerprint: String, - pub(crate) task: String, - #[serde(default)] - pub(crate) goal_id: Option, - #[serde(default)] - pub(crate) goal_revision: u64, - #[serde(default)] - pub(crate) goal_snapshot_fingerprint: String, - pub(crate) loop_iteration: u32, - pub(crate) action_index: u32, - pub(crate) occurrence_nonce: u64, - pub(crate) thinking_summary: String, - pub(crate) plan: Vec, - pub(crate) fallback_response: String, - pub(crate) observations: Vec, - pub(crate) project_revision_before: AgentRuntimeProjectRevision, - pub(crate) verification_gate_before: AgentRuntimeVerificationGate, - pub(crate) planned_repository_context_fingerprint: String, - #[serde(default)] - pub(crate) planned_steer_cursor: u64, - pub(crate) action: AgentRuntimeToolAction, - pub(crate) action_id: String, - pub(crate) action_fingerprint: String, - pub(crate) input_summary: Option, - #[serde(default)] - pub(crate) execution_mode: String, - pub(crate) status: String, - pub(crate) observation: Option, - pub(crate) created_at: u64, - pub(crate) updated_at: u64, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] -pub(super) struct AgentRuntimeParallelReadTiming { - pub(super) action_id: String, - pub(super) started_at_nanos: u64, - pub(super) finished_at_nanos: u64, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] -pub(super) struct AgentRuntimeParallelReadBatch { - pub(super) schema_version: String, - pub(super) batch_id: String, - pub(super) project_id: String, - pub(super) agent_id: String, - pub(super) task_id: String, - pub(super) session_id: String, - pub(super) run_id: String, - pub(super) source: String, - pub(super) loop_iteration: u32, - pub(super) planned_steer_cursor: u64, - pub(super) status: String, - pub(super) actions: Vec, - pub(super) timings: Vec, - pub(super) created_at: u64, - pub(super) updated_at: u64, -} - -#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] -pub(crate) struct AgentRuntimeProviderActionBatch { - pub(crate) schema_version: String, - pub(crate) batch_id: String, - pub(crate) project_id: String, - pub(crate) agent_id: String, - pub(crate) task_id: String, - pub(crate) session_id: String, - pub(crate) run_id: String, - pub(crate) source: String, - #[serde(default = "default_agent_runtime_run_profile")] - pub(crate) run_profile: String, - #[serde(default)] - pub(crate) run_profile_binding_fingerprint: String, - pub(crate) loop_iteration: u32, - pub(crate) planned_steer_cursor: u64, - pub(crate) status: String, - pub(crate) next_action_index: u32, - pub(crate) plan: AgentRuntimeToolPlan, - pub(crate) actions: Vec, - #[serde(default)] - pub(crate) collaboration_contract: Option, - pub(crate) project_revision_before: AgentRuntimeProjectRevision, - pub(crate) planned_repository_context_fingerprint: String, - pub(crate) created_at: u64, - pub(crate) updated_at: u64, -} - -impl AgentRuntimePendingToolAction { - pub(crate) fn summary(&self) -> AgentRuntimePendingToolActionSummary { - AgentRuntimePendingToolActionSummary { - action_id: self.action_id.clone(), - action_fingerprint: self.action_fingerprint.clone(), - tool: self.action.tool.clone(), - input_summary: self.input_summary.clone(), - reason: self - .action - .reason - .as_deref() - .map(|value| sanitize_agent_runtime_text(value, 240)) - .filter(|value| !value.trim().is_empty()), - requested_at: self.created_at, - } - } - - pub(super) fn tool_plan(&self) -> AgentRuntimeToolPlan { - AgentRuntimeToolPlan { - thinking_summary: self.thinking_summary.clone(), - plan_update: None, - plan: self.plan.clone(), - actions: Vec::new(), - response: self.fallback_response.clone(), - } - } - - pub(super) fn approved(&self) -> bool { - matches!( - self.status.as_str(), - AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED - | AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING - | AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED - ) - } - - pub(super) fn is_auto(&self) -> bool { - self.execution_mode == AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO - } -} - -pub(super) fn build_game_creator_agent_runtime_pending_tool_action( - root: &Path, - runtime: &AgentRuntimeState, - task: &str, - plan: &AgentRuntimeToolPlan, - observations: &[AgentRuntimeToolObservation], - project_revision_before: &AgentRuntimeProjectRevision, - planned_repository_context_fingerprint: &str, - action: &AgentRuntimeToolAction, - action_index: usize, - execution_mode: &str, - status: &str, - observation: Option, -) -> Result { - let task = sanitize_agent_runtime_text(task, AGENT_RUNTIME_TASK_MAX_CHARS); - let action_fingerprint = - agent_runtime_pending_tool_action_fingerprint(action, &task, runtime.applied_steer_cursor); - let occurrence_nonce = unix_timestamp_nanos().min(u128::from(u64::MAX)) as u64; - let action_index = u32::try_from(action_index).unwrap_or(u32::MAX); - let action_id = agent_runtime_tool_action_id( - &runtime.run_id, - runtime.loop_iteration, - action_index, - occurrence_nonce, - &action_fingerprint, - ); - let now = unix_timestamp(); - Ok(AgentRuntimePendingToolAction { - schema_version: AGENT_RUNTIME_PENDING_ACTION_SCHEMA_VERSION.to_string(), - fingerprint_version: AGENT_RUNTIME_ACTION_FINGERPRINT_VERSION.to_string(), - agent_id: runtime.agent_id.clone(), - task_id: runtime.task_id.clone(), - session_id: runtime.session_id.clone(), - run_id: runtime.run_id.clone(), - source: runtime.source.clone(), - run_profile: runtime.run_profile.clone(), - run_profile_binding_fingerprint: runtime.run_profile_binding_fingerprint.clone(), - task, - goal_id: runtime.goal_id.clone(), - goal_revision: runtime.goal_revision, - goal_snapshot_fingerprint: agent_goal_snapshot_fingerprint_for_state_at(root, runtime)?, - loop_iteration: runtime.loop_iteration, - action_index, - occurrence_nonce, - thinking_summary: sanitize_agent_runtime_text(&plan.thinking_summary, 240), - plan: plan - .plan - .iter() - .map(|item| sanitize_agent_runtime_text(item, 180)) - .collect(), - fallback_response: sanitize_agent_runtime_text(&plan.response, 1_200), - observations: observations.to_vec(), - project_revision_before: project_revision_before.clone(), - verification_gate_before: read_game_creator_agent_runtime_verification_gate( - root, - &runtime.agent_id, - &runtime.run_id, - )?, - planned_repository_context_fingerprint: planned_repository_context_fingerprint.to_string(), - planned_steer_cursor: runtime.applied_steer_cursor, - action: action.clone(), - action_id, - action_fingerprint, - input_summary: agent_runtime_tool_action_input_summary(root, action), - execution_mode: execution_mode.to_string(), - status: status.to_string(), - observation, - created_at: now, - updated_at: now, - }) -} - -pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch( - root: &Path, - runtime: &AgentRuntimeState, - task: &str, - plan: &AgentRuntimeToolPlan, - observations: &[AgentRuntimeToolObservation], - project_revision_before: &AgentRuntimeProjectRevision, - planned_repository_context_fingerprint: &str, -) -> Result { - let mut batch_plan = plan.clone(); - batch_plan - .actions - .truncate(AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT); - if game_creator_agent_runtime_provider_action_batch_exists( - root, - &runtime.agent_id, - &runtime.run_id, - ) { - return Err("同一 run 已存在未收束的 Provider action 批次".to_string()); - } - let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at( - root, - &runtime.agent_id, - &runtime.run_id, - Some(&runtime.run_profile), - Some(&runtime.run_profile_binding_fingerprint), - )?; - if run_profile != runtime.run_profile - || binding_fingerprint != runtime.run_profile_binding_fingerprint - { - return Err("Provider action 批次的 Run Profile 快照已漂移".to_string()); - } - if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && batch_plan - .actions - .iter() - .any(|action| action.tool.trim() == GAME_CREATOR_USER_INPUT_REQUEST_TOOL) - { - return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked( - AgentRuntimeToolObservation { - tool: GAME_CREATOR_USER_INPUT_REQUEST_TOOL.to_string(), - status: "blocked".to_string(), - summary: "自主构建 Run 禁止中途请求用户输入".to_string(), - detail: Some( - "请采用可逆、保守且可试玩的默认值继续;不得进入 waiting-for-user-input。" - .to_string(), - ), - }, - )); - } - - let (collaboration_policy, collaboration_state, collaboration_preflight) = - if runtime.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - let collaboration_policy = resolve_supervisor_collaboration_policy_for_run_at( - root, - &runtime.agent_id, - &runtime.run_id, - )? - .policy; - let collaboration_state = - read_supervisor_collaboration_state_at(root, &runtime.agent_id, &runtime.run_id)?; - let collaboration_preflight = preflight_supervisor_collaboration_plan( - &runtime.agent_id, - &batch_plan.actions, - &collaboration_policy, - &collaboration_state, - )?; - ( - Some(collaboration_policy), - Some(collaboration_state), - collaboration_preflight, - ) - } else { - (None, None, SupervisorCollaborationPreflight::default()) - }; - if let Some(violation) = collaboration_preflight.violation { - return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked( - AgentRuntimeToolObservation { - tool: "runtime.collaboration_policy".to_string(), - status: "blocked".to_string(), - summary: violation.summary, - detail: Some(violation.detail), - }, - )); - } - if let (Some(policy), Some(state)) = - (collaboration_policy.as_ref(), collaboration_state.as_ref()) - { - let mut destructive_mcp = None; - for action in &batch_plan.actions { - if action.tool.trim() != GAME_CREATOR_MCP_CALL_TOOL { - continue; - } - match game_creator_mcp_action_is_strictly_read_only_at(root, action).await { - Ok(true) => {} - Ok(false) => { - destructive_mcp = Some( - "MCP 工具未同时声明 readOnlyHint=true 与 destructiveHint=false".to_string(), - ); - break; - } - Err(error) => { - destructive_mcp = Some(format!("无法确认 MCP 工具只读身份:{error}")); - break; - } - } - } - if let Some(detail) = destructive_mcp { - let starts_collaboration = collaboration_preflight.contract.is_some(); - if !state.has_collaboration() - && supervisor_collaboration_policy_has_initial_requirements(policy) - && !starts_collaboration - { - let gap = supervisor_collaboration_initial_wave_gap( - policy, - &SupervisorCollaborationState::default(), - ) - .unwrap_or_else(|| "首批协作合同不完整".to_string()); - return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked( - AgentRuntimeToolObservation { - tool: "runtime.collaboration_policy".to_string(), - status: "blocked".to_string(), - summary: "Project Supervisor 首批协作不满足项目合同".to_string(), - detail: Some(format!("{gap};{detail}")), - }, - )); - } - if policy.orchestrator_only_after_delegation - && (state.has_collaboration() || starts_collaboration) - { - return Ok(AgentRuntimeProviderActionBatchPreparation::Blocked( - AgentRuntimeToolObservation { - tool: "runtime.collaboration_policy".to_string(), - status: "blocked".to_string(), - summary: "Project Supervisor 已进入协作编排,不能调用非只读 MCP" - .to_string(), - detail: Some(detail), - }, - )); - } - } - } - if batch_plan.actions.len() < 2 && !collaboration_preflight.force_durable_batch { - return Ok(AgentRuntimeProviderActionBatchPreparation::NotNeeded); - } - - let mut actions = Vec::with_capacity(batch_plan.actions.len()); - let mut first_confirmation = None; - let mut first_denied = None; - for (action_index, action) in batch_plan.actions.iter().enumerate() { - let mut pending = build_game_creator_agent_runtime_pending_tool_action( - root, - runtime, - task, - &batch_plan, - observations, - project_revision_before, - planned_repository_context_fingerprint, - action, - action_index, - AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO, - AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED, - None, - )?; - let command_id = game_creator_agent_runtime_tool_command_id(action.tool.trim()); - let isolated_scope_block = if runtime.agent_id.starts_with("child-") { - validate_isolated_agent_tool_scope_at( - root, - &runtime.agent_id, - action.tool.trim(), - &action.input, - ) - .err() - .map(AgentRuntimeToolPolicyBlock::Denied) - } else { - None - }; - let local_policy_block = isolated_scope_block.or_else(|| { - command_id - .map(|command_id| { - game_creator_agent_runtime_tool_policy_rule_for_run( - root, - &runtime.agent_id, - &runtime.run_id, - Some(&runtime.run_profile), - Some(&runtime.run_profile_binding_fingerprint), - command_id, - ) - }) - .unwrap_or_else(|| { - Some(AgentRuntimeToolPolicyBlock::Denied( - "工具不在 Agent Runtime 白名单中".to_string(), - )) - }) - }); - let mcp_policy_block = if matches!( - local_policy_block, - Some(AgentRuntimeToolPolicyBlock::Denied(_)) - ) { - None - } else { - game_creator_mcp_action_policy_block_at(root, &runtime.agent_id, action, false).await - }; - match strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block) { - Some(blocked @ AgentRuntimeToolPolicyBlock::Denied(_)) => { - let observation = - agent_runtime_tool_policy_block_observation(action.tool.trim(), blocked); - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string(); - pending.observation = Some(observation.clone()); - pending.updated_at = unix_timestamp(); - if first_denied.is_none() { - first_denied = Some((action_index, observation)); - } - } - Some(blocked @ AgentRuntimeToolPolicyBlock::RequiresConfirmation(_)) => { - let observation = - agent_runtime_tool_policy_block_observation(action.tool.trim(), blocked); - pending.execution_mode = - AGENT_RUNTIME_ACTION_EXECUTION_MODE_CONFIRMATION.to_string(); - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING.to_string(); - pending.observation = None; - pending.updated_at = unix_timestamp(); - if first_confirmation.is_none() { - first_confirmation = Some((action_index, observation)); - } - } - None => {} - } - actions.push(pending); - } - - let project_id = game_creator_agent_runtime_context_project_id(root)?; - let status = if first_denied.is_some() { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_ABORTED - } else if first_confirmation.is_some() { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_WAITING_CONFIRMATION - } else { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_READY - }; - let batch_id = agent_runtime_provider_action_batch_id( - &project_id, - &runtime.agent_id, - &runtime.task_id, - &runtime.session_id, - &runtime.run_id, - runtime.loop_iteration, - runtime.applied_steer_cursor, - &batch_plan, - project_revision_before, - planned_repository_context_fingerprint, - &actions, - collaboration_preflight.contract.as_ref(), - )?; - let now = unix_timestamp(); - let batch = AgentRuntimeProviderActionBatch { - schema_version: AGENT_RUNTIME_PROVIDER_ACTION_BATCH_SCHEMA_VERSION.to_string(), - batch_id, - project_id, - agent_id: runtime.agent_id.clone(), - task_id: runtime.task_id.clone(), - session_id: runtime.session_id.clone(), - run_id: runtime.run_id.clone(), - source: runtime.source.clone(), - run_profile: runtime.run_profile.clone(), - run_profile_binding_fingerprint: runtime.run_profile_binding_fingerprint.clone(), - loop_iteration: runtime.loop_iteration, - planned_steer_cursor: runtime.applied_steer_cursor, - status: status.to_string(), - next_action_index: 0, - plan: batch_plan, - actions, - collaboration_contract: collaboration_preflight.contract, - project_revision_before: project_revision_before.clone(), - planned_repository_context_fingerprint: planned_repository_context_fingerprint.to_string(), - created_at: now, - updated_at: now, - }; - write_game_creator_agent_runtime_provider_action_batch(root, &batch)?; - - if let Some((index, observation)) = first_denied { - let pending = batch.actions[index].clone(); - return Ok(AgentRuntimeProviderActionBatchPreparation::Aborted { - batch, - pending, - observation, - }); - } - if let Some((index, observation)) = first_confirmation { - let pending = batch.actions[index].clone(); - return Ok(AgentRuntimeProviderActionBatchPreparation::Waiting { - batch, - pending, - observation, - }); - } - Ok(AgentRuntimeProviderActionBatchPreparation::Ready(batch)) -} - -pub(super) fn project_game_creator_agent_runtime_collaboration_block( - root: &Path, - runtime: &mut AgentRuntimeState, - observations: &mut Vec, - context_tracker: &mut AgentRuntimeContextWindowTracker, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - let summary = observation.summary(); - runtime.status = "running".to_string(); - runtime.phase = "planning".to_string(); - runtime.current_action = "重新规划 Project Supervisor 协作波".to_string(); - runtime.waiting_on = "项目协作策略要求的完整 static / isolated 组成".to_string(); - runtime.next_step = - "根据 collaboration policy 在一个 native planning 批次内提交完整协作,且不要混入总控项目修改" - .to_string(); - runtime.observations.push(summary.clone()); - runtime.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_state(root, runtime)?; - append_game_creator_agent_runtime_event( - root, - runtime, - "observation", - runtime.status.as_str(), - runtime.phase.as_str(), - &summary, - observation.detail.as_deref(), - )?; - let _ = append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.collaboration_policy.blocked", - "agentId": runtime.agent_id, - "taskId": runtime.task_id, - "sessionId": runtime.session_id, - "runId": runtime.run_id, - "status": observation.status, - }), - ); - context_tracker.record(observation); - observations.push(observation.clone()); - Ok(()) -} - -pub(super) fn persist_game_creator_agent_runtime_provider_batch_waiting_confirmation( - root: &Path, - runtime: &mut AgentRuntimeState, - task: &str, - plan: &AgentRuntimeToolPlan, - observations: &mut Vec, - loop_index: usize, - context_tracker: &mut AgentRuntimeContextWindowTracker, - batch: &AgentRuntimeProviderActionBatch, - pending: &AgentRuntimePendingToolAction, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - write_game_creator_agent_runtime_pending_tool_action(root, pending)?; - let action_index = usize::try_from(pending.action_index).unwrap_or(usize::MAX); - activate_agent_runtime_plan_step( - runtime, - action_index, - pending - .action - .reason - .as_deref() - .unwrap_or(pending.action.tool.as_str()), - ); - let observation_summary = observation.summary(); - runtime.observations.push(observation_summary.clone()); - append_agent_runtime_tool_call_record( - root, - runtime, - &pending.task, - &pending.action, - observation, - Some(&pending.action_id), - ); - complete_agent_runtime_active_plan_step( - runtime, - "waiting-for-confirmation", - &observation_summary, - ); - runtime.pending_tool_action = Some(pending.summary()); - runtime.status = "waiting-for-confirmation".to_string(); - runtime.phase = "waiting-for-confirmation".to_string(); - runtime.current_action = format!("等待确认工具 {}", observation.tool); - runtime.waiting_on = "开发者确认 Provider action 批次中的全部受控动作".to_string(); - runtime.next_step = - "确认或拒绝后继续原 Provider action 批次,确认前不执行 auto 前缀".to_string(); - runtime.updated_at = unix_timestamp(); - let public_observation_detail = agent_runtime_public_observation_detail(root, observation); - append_game_creator_agent_runtime_task_projection_once(root, runtime, &pending.action_id)?; - refresh_game_creator_agent_runtime_task_queue(root, runtime)?; - write_game_creator_agent_runtime_state(root, runtime)?; - append_game_creator_agent_runtime_action_event( - root, - runtime, - "observation", - "waiting-for-confirmation", - "waiting-for-confirmation", - &observation_summary, - public_observation_detail.as_deref(), - &pending.action_id, - )?; - context_tracker.record(observation); - observations.push(observation.clone()); - persist_game_creator_agent_runtime_context( - root, - runtime, - task, - plan, - observations, - loop_index, - context_tracker, - )?; - let public_input_summary = agent_runtime_public_action_input_summary( - root, - &pending.action.tool, - pending.input_summary.as_deref(), - ); - append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.provider_action_batch.confirmation_required", - "agentId": runtime.agent_id, - "taskId": runtime.task_id, - "sessionId": runtime.session_id, - "runId": runtime.run_id, - "batchId": batch.batch_id, - "actionCount": batch.actions.len(), - "actionIndex": pending.action_index, - "actionId": pending.action_id, - "actionFingerprint": pending.action_fingerprint, - "tool": pending.action.tool, - "inputSummary": public_input_summary, - }), - )?; - emit_game_creator_agent_runtime_update(root, &runtime.agent_id); - Ok(()) -} - -pub(super) fn project_game_creator_agent_runtime_provider_batch_abort( - root: &Path, - runtime: &mut AgentRuntimeState, - task: &str, - plan: &AgentRuntimeToolPlan, - observations: &mut Vec, - loop_index: usize, - context_tracker: &mut AgentRuntimeContextWindowTracker, - batch: &AgentRuntimeProviderActionBatch, - pending: &AgentRuntimePendingToolAction, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - write_game_creator_agent_runtime_pending_tool_action(root, pending)?; - append_game_creator_agent_runtime_auto_tool_action_observed_record(root, pending, observation)?; - let action_index = usize::try_from(pending.action_index).unwrap_or(usize::MAX); - activate_agent_runtime_plan_step( - runtime, - action_index, - pending - .action - .reason - .as_deref() - .unwrap_or(pending.action.tool.as_str()), - ); - let observation = observation.clone(); - let observation_summary = observation.summary(); - runtime.observations.push(observation_summary.clone()); - append_agent_runtime_tool_call_record( - root, - runtime, - &pending.task, - &pending.action, - &observation, - Some(&pending.action_id), - ); - complete_agent_runtime_active_plan_step(runtime, "failed", &observation_summary); - runtime.pending_tool_action = None; - runtime.status = "running".to_string(); - runtime.phase = "observation".to_string(); - runtime.current_action = "Provider action 批次已在执行前中止".to_string(); - runtime.waiting_on = "Agent 根据整批拒绝观察重新规划".to_string(); - runtime.next_step = "下一轮 Provider planning 必须重新选择允许执行的动作".to_string(); - runtime.updated_at = unix_timestamp(); - let public_observation_detail = agent_runtime_public_observation_detail(root, &observation); - append_game_creator_agent_runtime_task_projection_once(root, runtime, &pending.action_id)?; - refresh_game_creator_agent_runtime_task_queue(root, runtime)?; - write_game_creator_agent_runtime_state(root, runtime)?; - append_game_creator_agent_runtime_action_event( - root, - runtime, - "observation", - "running", - "observation", - &observation_summary, - public_observation_detail.as_deref(), - &pending.action_id, - )?; - append_agent_runtime_action_receipt( - root, - runtime, - &pending.action_id, - &pending.action_fingerprint, - &observation.tool, - AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO, - pending.input_summary.as_deref(), - &observation, - )?; - append_agent_db_terminal_observation_if_missing_for_action( - root, - &runtime.agent_id, - &runtime.run_id, - &pending.action_id, - serde_json::json!({ - "recordType": "agent.runtime.tool_observation", - "agentId": runtime.agent_id, - "taskId": runtime.task_id, - "runId": runtime.run_id, - "tool": observation.tool, - "status": observation.status, - "summary": observation.summary, - "actionId": pending.action_id, - "actionFingerprint": pending.action_fingerprint, - "decision": "batch-denied", - "providerActionBatchId": batch.batch_id, - }), - )?; - context_tracker.record(&observation); - observations.push(observation); - persist_game_creator_agent_runtime_context( - root, - runtime, - task, - plan, - observations, - loop_index, - context_tracker, - )?; - append_game_creator_agent_runtime_action_event( - root, - runtime, - "provider_action_batch.aborted", - "running", - "observation", - "Provider action 批次因预检拒绝而在零工具执行状态下中止。", - Some(&format!( - "batchId={} · actionCount={} · rejectedActionIndex={}", - batch.batch_id, - batch.actions.len(), - pending.action_index - )), - &pending.action_id, - )?; - remove_game_creator_agent_runtime_confirmations(root, &runtime.agent_id, &runtime.run_id)?; - remove_game_creator_agent_runtime_pending_tool_action( - root, - &runtime.agent_id, - &runtime.run_id, - )?; - remove_game_creator_agent_runtime_provider_action_batch( - root, - &runtime.agent_id, - &runtime.run_id, - )?; - runtime.pending_tool_action = None; - runtime.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_state(root, runtime) -} - -pub(super) fn mark_game_creator_agent_runtime_provider_action_batch_superseded( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Result, String> { - if !game_creator_agent_runtime_provider_action_batch_exists(root, agent_id, run_id) { - return Ok(None); - } - let mut batch = read_game_creator_agent_runtime_provider_action_batch(root, agent_id, run_id)?; - match batch.status.as_str() { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_READY => { - batch.status = AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_SUPERSEDED.to_string(); - batch.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_provider_action_batch(root, &batch)?; - } - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_SUPERSEDED - | AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_COMPLETED => {} - _ => { - return Err(format!( - "当前 Provider action 批次状态不能作废剩余动作:{}", - batch.status - )); - } - } - Ok(Some(batch)) -} - -pub(super) fn game_creator_agent_runtime_provider_batch_terminal_member_matches( - stored: &AgentRuntimePendingToolAction, - pending: &AgentRuntimePendingToolAction, - observations: &[AgentRuntimeToolObservation], -) -> bool { - let mut expected = pending.clone(); - expected.observations = observations.to_vec(); - expected.updated_at = stored.updated_at; - *stored == expected -} - -pub(crate) fn update_game_creator_agent_runtime_provider_batch_member( - root: &Path, - pending: &AgentRuntimePendingToolAction, -) -> Result { - if !game_creator_agent_runtime_provider_action_batch_exists( - root, - &pending.agent_id, - &pending.run_id, - ) { - return Ok(false); - } - let mut batch = read_game_creator_agent_runtime_provider_action_batch( - root, - &pending.agent_id, - &pending.run_id, - )?; - let action_index = usize::try_from(pending.action_index).unwrap_or(usize::MAX); - let next_action_index = usize::try_from(batch.next_action_index).unwrap_or(usize::MAX); - let stored = batch - .actions - .get(action_index) - .ok_or_else(|| "Provider action 批次成员 cursor 超出范围".to_string())?; - if stored.action_id != pending.action_id - || stored.action_fingerprint != pending.action_fingerprint - || stored.action != pending.action - { - return Err("Provider action 批次成员 action identity 已变化".to_string()); - } - let mut durable_pending = pending.clone(); - durable_pending.observations = batch - .actions - .first() - .map(|first| first.observations.clone()) - .unwrap_or_default(); - if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING { - if action_index != next_action_index { - return Err("Provider action 批次只能在当前 cursor 上进入确认等待".to_string()); - } - batch.actions[action_index] = durable_pending; - batch.status = AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_WAITING_CONFIRMATION.to_string(); - batch.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_provider_action_batch(root, &batch)?; - return Ok(false); - } - if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING { - if action_index != next_action_index { - return Err(format!( - "Provider action 批次只能在当前 cursor 上进入 executing:expected={next_action_index} actual={action_index}" - )); - } - if !matches!( - stored.status.as_str(), - AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED - | AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING - ) { - return Err("Provider action 批次当前成员不能进入 executing".to_string()); - } - batch.actions[action_index] = durable_pending; - batch.status = AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_READY.to_string(); - batch.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_provider_action_batch(root, &batch)?; - return Ok(false); - } - if !matches!( - pending.status.as_str(), - AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED - | AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED - ) || pending.observation.is_none() - { - return Err("Provider action 批次成员尚未形成可推进 cursor 的终态".to_string()); - } - if action_index < next_action_index { - if game_creator_agent_runtime_provider_batch_terminal_member_matches( - stored, - pending, - &durable_pending.observations, - ) { - return Ok(batch.status == AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_COMPLETED); - } - return Err("Provider action 批次已推进成员的终态内容发生冲突".to_string()); - } - if action_index != next_action_index { - return Err(format!( - "Provider action 批次终态顺序冲突:expected={next_action_index} actual={action_index}" - )); - } - batch.actions[action_index] = durable_pending; - batch.next_action_index = u32::try_from(action_index.saturating_add(1)).unwrap_or(u32::MAX); - let completed = action_index.saturating_add(1) == batch.actions.len(); - let observation_failed = pending - .observation - .as_ref() - .is_some_and(|observation| observation.status != "ok"); - if !completed && !observation_failed { - let next_pending = batch - .actions - .get_mut(action_index.saturating_add(1)) - .ok_or_else(|| "Provider action 批次缺少下一个 cursor 成员".to_string())?; - next_pending.verification_gate_before = read_game_creator_agent_runtime_verification_gate( - root, - &pending.agent_id, - &pending.run_id, - )?; - next_pending.updated_at = unix_timestamp(); - } - batch.status = if completed { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_COMPLETED - } else if observation_failed { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_SUPERSEDED - } else { - AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_READY - } - .to_string(); - batch.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_provider_action_batch(root, &batch)?; - Ok(completed) -} - -pub(super) fn update_game_creator_agent_runtime_provider_batch_parallel_members( - root: &Path, - parallel_batch: &AgentRuntimeParallelReadBatch, -) -> Result<(), String> { - if !game_creator_agent_runtime_provider_action_batch_exists( - root, - ¶llel_batch.agent_id, - ¶llel_batch.run_id, - ) { - return Ok(()); - } - for pending in ¶llel_batch.actions { - update_game_creator_agent_runtime_provider_batch_member(root, pending)?; - } - Ok(()) -} - -pub(super) fn persist_game_creator_agent_user_input_wait_at( - root: &Path, - runtime: &mut AgentRuntimeState, - pending: &mut AgentRuntimePendingToolAction, -) -> Result<(), String> { - if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { - return Err("自主构建 Run 禁止进入 waiting-for-user-input".to_string()); - } - pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string(); - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT.to_string(); - pending.observation = None; - pending.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_pending_tool_action(root, pending)?; - let request = match prepare_game_creator_agent_user_input_request_at(root, pending)? { - AgentRuntimeUserInputRecovery::Waiting(request) => request, - AgentRuntimeUserInputRecovery::Answered { .. } => { - return Err("新建用户输入等待时 sidecar 已进入 answered,需由恢复路径继续".to_string()); - } - AgentRuntimeUserInputRecovery::Cancelled => { - return Err("新建用户输入等待时 sidecar 已取消".to_string()); - } - }; - let waiting_observation = AgentRuntimeToolObservation { - tool: GAME_CREATOR_USER_INPUT_REQUEST_TOOL.to_string(), - status: "waiting-for-user-input".to_string(), - summary: format!( - "Agent 正在等待用户回答 {} 个澄清问题", - request.questions.len() - ), - detail: None, - }; - append_agent_runtime_tool_call_record( - root, - runtime, - &pending.task, - &pending.action, - &waiting_observation, - Some(&pending.action_id), - ); - runtime.pending_tool_action = Some(pending.summary()); - runtime.status = "waiting-for-user-input".to_string(); - runtime.phase = "waiting-for-user-input".to_string(); - runtime.current_action = "等待用户补充关键信息".to_string(); - runtime.waiting_on = "用户回答 Agent 的结构化澄清问题".to_string(); - runtime.next_step = "提交全部回答后在同一 run 继续当前计划".to_string(); - runtime.error = None; - runtime.updated_at = unix_timestamp(); - append_game_creator_agent_runtime_task_projection_once(root, runtime, &pending.action_id)?; - refresh_game_creator_agent_runtime_task_queue(root, runtime)?; - write_game_creator_agent_runtime_state(root, runtime)?; - append_game_creator_agent_runtime_action_event( - root, - runtime, - "user_input.required", - "waiting-for-user-input", - "waiting-for-user-input", - "Agent 已暂停当前 run,等待用户补充关键信息。", - pending.input_summary.as_deref(), - &pending.action_id, - )?; - if let Err(error) = append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.user_input.required", - "agentId": pending.agent_id, - "taskId": pending.task_id, - "sessionId": pending.session_id, - "runId": pending.run_id, - "actionId": pending.action_id, - "actionFingerprint": pending.action_fingerprint, - "requestId": request.request_id, - "questionCount": request.questions.len(), - "inputSummary": pending.input_summary, - }), - ) { - let _ = append_game_creator_agent_runtime_event( - root, - runtime, - "user_input.audit_failed", - "waiting-for-user-input", - "waiting-for-user-input", - "用户输入请求已安全暂停,但公共审计记录写入失败。", - Some(&sanitize_agent_runtime_text(&error, 240)), - ); - } - emit_game_creator_agent_runtime_update(root, &runtime.agent_id); - Ok(()) -} - -pub(crate) fn mark_game_creator_agent_runtime_auto_action_executing_if_current( - root: &Path, - pending: &mut AgentRuntimePendingToolAction, -) -> Result { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.tool_action.executing", - )?; - if game_creator_agent_runtime_cancel_requested_for(root, &pending.agent_id, &pending.run_id) { - return Ok(false); - } - if game_creator_agent_runtime_has_queued_steer_after_cursor( - root, - &pending.agent_id, - &pending.run_id, - pending.planned_steer_cursor, - )? { - return Ok(false); - } - if validate_agent_runtime_pending_current_goal_snapshot(root, pending).is_err() { - return Ok(false); - } - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING.to_string(); - pending.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_pending_tool_action(root, pending)?; - update_game_creator_agent_runtime_provider_batch_member(root, pending)?; - Ok(true) -} - -pub(super) fn append_game_creator_agent_runtime_auto_tool_action_executing_record( - root: &Path, - pending: &AgentRuntimePendingToolAction, -) -> Result<(), String> { - let public_input_summary = agent_runtime_public_action_input_summary( - root, - &pending.action.tool, - pending.input_summary.as_deref(), - ); - append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.tool_action.executing", - "agentId": pending.agent_id, - "taskId": pending.task_id, - "runId": pending.run_id, - "actionId": pending.action_id, - "actionFingerprint": pending.action_fingerprint, - "tool": pending.action.tool, - "executionMode": AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO, - "inputSummary": public_input_summary, - }), - ) -} - -pub(super) fn append_game_creator_agent_runtime_auto_tool_action_observed_record( - root: &Path, - pending: &AgentRuntimePendingToolAction, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.tool_action.observed", - "agentId": pending.agent_id, - "taskId": pending.task_id, - "runId": pending.run_id, - "actionId": pending.action_id, - "actionFingerprint": pending.action_fingerprint, - "tool": pending.action.tool, - "executionMode": AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO, - "observationStatus": observation.status, - }), - ) -} - -pub(super) fn append_game_creator_agent_runtime_parallel_action_executing_record( - root: &Path, - pending: &AgentRuntimePendingToolAction, -) -> Result<(), String> { - let record_type = "agent.runtime.tool_action.executing"; - if agent_db_record_exists_for_action( - root, - record_type, - &pending.agent_id, - &pending.run_id, - &pending.action_id, - )? { - return Ok(()); - } - append_game_creator_agent_runtime_auto_tool_action_executing_record(root, pending) -} - -pub(super) fn append_game_creator_agent_runtime_parallel_action_observed_record( - root: &Path, - pending: &AgentRuntimePendingToolAction, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - let record_type = "agent.runtime.tool_action.observed"; - if agent_db_record_exists_for_action( - root, - record_type, - &pending.agent_id, - &pending.run_id, - &pending.action_id, - )? { - return Ok(()); - } - append_game_creator_agent_runtime_auto_tool_action_observed_record(root, pending, observation) -} - -pub(super) fn agent_runtime_timestamp_nanos_u64() -> u64 { - unix_timestamp_nanos().min(u128::from(u64::MAX)) as u64 -} - -pub(super) fn agent_runtime_parallel_read_overlap_nanos( - timings: &[AgentRuntimeParallelReadTiming], -) -> u64 { - let mut maximum_overlap = 0_u64; - for (index, left) in timings.iter().enumerate() { - for right in timings.iter().skip(index + 1) { - let overlap_start = left.started_at_nanos.max(right.started_at_nanos); - let overlap_end = left.finished_at_nanos.min(right.finished_at_nanos); - maximum_overlap = maximum_overlap.max(overlap_end.saturating_sub(overlap_start)); - } - } - maximum_overlap -} +mod action_audit; +mod action_execution; +mod action_projection; +mod autonomous_policy; +mod context_compaction; +mod parallel_ledger; +mod parallel_read; +mod pending_confirmation_ledger; +mod project_gates; +mod provider_action_batch; +mod provider_batch_ledger; +mod provider_final_reply; +mod provider_request_builders; +mod provider_tool_plan; +mod response_stream; +mod run_status_observation; +mod structured_plan; +mod tool_plan_protocol; +mod tool_policy_snapshot; #[cfg(test)] -pub(super) fn wait_on_agent_runtime_parallel_read_test_barrier(root: &Path) { - static BARRIERS: OnceLock< - std::sync::Mutex>>, - > = OnceLock::new(); - let marker = root.join(".agent/runtime/test-parallel-read-barrier-count"); - let count = fs::read_to_string(marker) - .ok() - .and_then(|value| value.trim().parse::().ok()) - .filter(|count| (2..=AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT).contains(count)); - let Some(count) = count else { - return; - }; - let key = format!("{}\n{count}", root.to_string_lossy()); - let barrier = { - let mut barriers = BARRIERS - .get_or_init(|| std::sync::Mutex::new(std::collections::BTreeMap::new())) - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - barriers - .entry(key) - .or_insert_with(|| Arc::new(std::sync::Barrier::new(count))) - .clone() - }; - barrier.wait(); - let hold = root.join(".agent/runtime/test-parallel-read-hold"); - if hold.exists() { - let release = root.join(".agent/runtime/test-parallel-read-release"); - let started = std::time::Instant::now(); - while !release.exists() { - assert!( - started.elapsed() < Duration::from_secs(5), - "parallel read test release marker timed out" - ); - std::thread::sleep(Duration::from_millis(5)); - } - } -} - -#[cfg(not(test))] -pub(super) fn wait_on_agent_runtime_parallel_read_test_barrier(_root: &Path) {} - +mod response_stream_tests; #[cfg(test)] -pub(super) fn delay_agent_runtime_parallel_read_test_action( - root: &Path, - pending: &AgentRuntimePendingToolAction, -) { - if pending.action_index != 0 { - return; - } - let delay_millis = - fs::read_to_string(root.join(".agent/runtime/test-parallel-read-delay-first-ms")) - .ok() - .and_then(|value| value.trim().parse::().ok()) - .filter(|value| *value <= 1_000) - .unwrap_or_default(); - if delay_millis > 0 { - std::thread::sleep(Duration::from_millis(delay_millis)); - } -} +mod run_status_observation_tests; -#[cfg(not(test))] -pub(super) fn delay_agent_runtime_parallel_read_test_action( - _root: &Path, - _pending: &AgentRuntimePendingToolAction, -) { -} - -pub(super) fn execute_game_creator_agent_runtime_parallel_safe_read_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, - pending: &AgentRuntimePendingToolAction, -) -> AgentRuntimeToolObservation { - let action = &pending.action; - let tool = action.tool.trim(); - if agent_id.trim().starts_with("child-") { - if let Err(error) = - validate_isolated_agent_tool_scope_at(root, agent_id.trim(), tool, &action.input) - { - return AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "rejected".to_string(), - summary: redact_agent_runtime_project_paths(root, &error, 240), - detail: None, - }; - } - } - match tool { - "memory.read" => observe_agent_runtime_memory(root, agent_id, &action.input), - "conversation.read" => observe_agent_runtime_conversation(root, agent_id, run_id), - "asset.list" => observe_agent_runtime_assets(root), - "project.search" => observe_agent_runtime_project_search(root, &action.input), - "project.diff" => observe_agent_runtime_project_diff(root, &action.input), - "git.inspect" => observe_agent_runtime_git_inspect(root, &action.input), - "file.list" => observe_agent_runtime_file_list(root, &action.input), - "file.read" => observe_agent_runtime_file(root, &action.input), - "task.list" => observe_agent_runtime_task_list(root), - _ => AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "rejected".to_string(), - summary: "工具不属于 Agent Runtime 只读并行白名单".to_string(), - detail: None, - }, - } -} - -pub(super) fn validate_game_creator_agent_runtime_parallel_read_pending_at_locked( - root: &Path, - runtime: &AgentRuntimeState, - pending: &AgentRuntimePendingToolAction, -) -> Result, String> { - validate_agent_runtime_pending_tool_action_record(root, pending)?; - validate_agent_runtime_pending_context(root, runtime, pending)?; - if !pending.is_auto() || !agent_runtime_tool_is_parallel_safe_read(&pending.action.tool) { - return Err("Agent Runtime 只读并行成员不属于自动只读动作".to_string()); - } - if let Err(error) = validate_agent_runtime_pending_current_goal_snapshot(root, pending) { - return Ok(Some(agent_runtime_pending_goal_stale_observation( - root, &error, - ))); - } - if let Some(observation) = pending_repository_context_drift_observation(root, pending)? { - return Ok(Some(observation)); - } - let Some(command_id) = game_creator_agent_runtime_tool_command_id(&pending.action.tool) else { - return Err("Agent Runtime 只读并行成员不在工具白名单中".to_string()); - }; - if let Some(blocked) = game_creator_agent_runtime_tool_policy_block_after_lock( - root, - &pending.agent_id, - command_id, - Some(pending), - ) { - return Ok(Some(agent_runtime_tool_policy_block_observation( - &pending.action.tool, - blocked, - ))); - } - Ok(None) -} - -pub(super) fn mark_game_creator_agent_runtime_parallel_read_batch_observed_at_locked( - root: &Path, - mut batch: AgentRuntimeParallelReadBatch, - results: Vec<(AgentRuntimeToolObservation, AgentRuntimeParallelReadTiming)>, -) -> Result { - if results.len() != batch.actions.len() { - return Err("Agent Runtime 只读并行批次返回数量不匹配".to_string()); - } - batch.timings.clear(); - for (pending, (observation, timing)) in batch.actions.iter_mut().zip(results) { - if observation.tool != pending.action.tool - || observation.is_waiting_for_confirmation() - || observation.requires_reconciliation() - || timing.action_id != pending.action_id - { - return Err(format!( - "Agent Runtime 只读并行成员返回了不可持久化的 observation:{} / {}", - pending.action.tool, observation.status - )); - } - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED.to_string(); - pending.observation = Some(observation); - pending.updated_at = unix_timestamp(); - batch.timings.push(timing); - } - batch.status = AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED.to_string(); - batch.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_parallel_read_batch(root, &batch)?; - for pending in &batch.actions { - if let Some(observation) = pending.observation.as_ref() { - let _ = append_game_creator_agent_runtime_parallel_action_observed_record( - root, - pending, - observation, - ); - } - } - Ok(batch) -} - -pub(super) fn append_game_creator_agent_runtime_parallel_read_batch_completed_audit( - root: &Path, - batch: &AgentRuntimeParallelReadBatch, -) -> Result<(), String> { - let overlap_nanos = agent_runtime_parallel_read_overlap_nanos(&batch.timings); - let started_at_nanos = batch - .timings - .iter() - .map(|timing| timing.started_at_nanos) - .min() - .unwrap_or_default(); - let finished_at_nanos = batch - .timings - .iter() - .map(|timing| timing.finished_at_nanos) - .max() - .unwrap_or_default(); - let first_action_id = batch - .actions - .first() - .map(|pending| pending.action_id.as_str()) - .unwrap_or_default(); - let audit = serde_json::json!({ - "recordType": "agent.runtime.parallel_read_batch.completed", - "agentId": batch.agent_id, - "taskId": batch.task_id, - "sessionId": batch.session_id, - "runId": batch.run_id, - "batchId": batch.batch_id, - "actionId": first_action_id, - "actionIds": batch.actions.iter().map(|pending| pending.action_id.as_str()).collect::>(), - "tools": batch.actions.iter().map(|pending| pending.action.tool.as_str()).collect::>(), - "actionCount": batch.actions.len(), - "startedAtNanos": started_at_nanos, - "finishedAtNanos": finished_at_nanos, - "overlapNanos": overlap_nanos, - "overlapped": overlap_nanos > 0, - }); - if !agent_db_record_exists_for_action( - root, - "agent.runtime.parallel_read_batch.completed", - &batch.agent_id, - &batch.run_id, - first_action_id, - )? { - append_agent_db_record(root, audit)?; - } - Ok(()) -} - -pub(super) fn execute_game_creator_agent_runtime_parallel_read_members_at_locked( - root: &Path, - batch: AgentRuntimeParallelReadBatch, -) -> Result { - let results = std::thread::scope(|scope| { - let handles = batch - .actions - .iter() - .map(|pending| { - scope.spawn(move || { - let started_at_nanos = agent_runtime_timestamp_nanos_u64(); - wait_on_agent_runtime_parallel_read_test_barrier(root); - delay_agent_runtime_parallel_read_test_action(root, pending); - let observation = - execute_game_creator_agent_runtime_parallel_safe_read_at_locked( - root, - &pending.agent_id, - &pending.run_id, - pending, - ); - let finished_at_nanos = agent_runtime_timestamp_nanos_u64(); - ( - observation, - AgentRuntimeParallelReadTiming { - action_id: pending.action_id.clone(), - started_at_nanos, - finished_at_nanos, - }, - ) - }) - }) - .collect::>(); - handles - .into_iter() - .map(|handle| { - handle - .join() - .map_err(|_| "Agent Runtime 只读并行工作线程异常退出".to_string()) - }) - .collect::, String>>() - })?; - mark_game_creator_agent_runtime_parallel_read_batch_observed_at_locked(root, batch, results) -} - -pub(super) fn prepare_and_execute_game_creator_agent_runtime_parallel_read_batch_blocking( - root: PathBuf, - runtime: AgentRuntimeState, - task: String, - plan: AgentRuntimeToolPlan, - observations: Vec, - project_revision_before: AgentRuntimeProjectRevision, - repository_context_fingerprint: String, - action_start_index: usize, - actions: Vec, -) -> Result { - if actions.len() < 2 - || actions.len() > AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT - || !actions - .iter() - .all(|action| agent_runtime_tool_is_parallel_safe_read(&action.tool)) - { - return Ok(AgentRuntimeParallelReadBatchExecution::NotEligible); - } - let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - &root, - "runtime.parallel_read_batch", - )?; - if game_creator_agent_runtime_pending_tool_action_exists( - &root, - &runtime.agent_id, - &runtime.run_id, - ) || game_creator_agent_runtime_parallel_read_batch_exists( - &root, - &runtime.agent_id, - &runtime.run_id, - ) { - return Err("Agent Runtime 建立只读并行批次时发现未清理动作账本".to_string()); - } - let durable_runtime = read_game_creator_agent_runtime_for_session_at( - &root, - &runtime.agent_id, - Some(&runtime.session_id), - )? - .state; - if durable_runtime.agent_id != runtime.agent_id - || durable_runtime.task_id != runtime.task_id - || durable_runtime.session_id != runtime.session_id - || durable_runtime.run_id != runtime.run_id - || durable_runtime.source != runtime.source - || durable_runtime.loop_iteration != runtime.loop_iteration - || durable_runtime.applied_steer_cursor != runtime.applied_steer_cursor - { - return Err("Agent Runtime 只读并行批次与当前持久 run 身份不匹配".to_string()); - } - if game_creator_agent_runtime_cancel_requested(&root, &durable_runtime) - || game_creator_agent_runtime_has_queued_steer_after_cursor( - &root, - &runtime.agent_id, - &runtime.run_id, - runtime.applied_steer_cursor, - )? - { - return Ok(AgentRuntimeParallelReadBatchExecution::Stale); - } - if !agent_runtime_parallel_read_batch_is_auto_at( - &root, - &runtime.agent_id, - &runtime.run_id, - &runtime.run_profile, - &runtime.run_profile_binding_fingerprint, - &actions, - ) { - return Ok(AgentRuntimeParallelReadBatchExecution::NotEligible); - } - let provider_batch = if game_creator_agent_runtime_provider_action_batch_exists( - &root, - &runtime.agent_id, - &runtime.run_id, - ) { - Some(read_game_creator_agent_runtime_provider_action_batch( - &root, - &runtime.agent_id, - &runtime.run_id, - )?) - } else { - None - }; - let mut pending_actions = Vec::with_capacity(actions.len()); - for (offset, action) in actions.iter().enumerate() { - let expected_index = action_start_index.saturating_add(offset); - let mut pending = if let Some(provider_batch) = provider_batch.as_ref() { - if provider_batch.status != AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_READY - || usize::try_from(provider_batch.next_action_index).unwrap_or(usize::MAX) - != action_start_index - { - return Err( - "只读并行批次对应的 Provider action 批次尚未进入当前 cursor".to_string() - ); - } - let pending = provider_batch - .actions - .get(expected_index) - .ok_or_else(|| "Provider action 批次缺少只读并行成员".to_string())?; - if pending.action != *action - || pending.execution_mode != AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO - || pending.status != AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED - { - return Err("Provider action 批次只读成员身份或预检状态已变化".to_string()); - } - pending.clone() - } else { - build_game_creator_agent_runtime_pending_tool_action( - &root, - &durable_runtime, - &task, - &plan, - &observations, - &project_revision_before, - &repository_context_fingerprint, - action, - expected_index, - AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO, - AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING, - None, - )? - }; - pending.observations = observations.clone(); - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING.to_string(); - pending.observation = None; - pending.updated_at = unix_timestamp(); - if validate_game_creator_agent_runtime_parallel_read_pending_at_locked( - &root, - &durable_runtime, - &pending, - )? - .is_some() - { - return Ok(AgentRuntimeParallelReadBatchExecution::NotEligible); - } - pending_actions.push(pending); - } - if provider_batch.is_some() { - let current_cursor_gate = pending_actions - .first() - .ok_or_else(|| "Provider action 批次缺少只读并行 cursor 成员".to_string())? - .verification_gate_before - .clone(); - for pending in &mut pending_actions { - pending.verification_gate_before = current_cursor_gate.clone(); - } - } - let project_id = game_creator_agent_runtime_context_project_id(&root)?; - let batch_id = agent_runtime_parallel_read_batch_id( - &project_id, - &runtime.agent_id, - &runtime.task_id, - &runtime.session_id, - &runtime.run_id, - runtime.loop_iteration, - &pending_actions, - )?; - let now = unix_timestamp(); - let batch = AgentRuntimeParallelReadBatch { - schema_version: AGENT_RUNTIME_PARALLEL_READ_BATCH_SCHEMA_VERSION.to_string(), - batch_id, - project_id, - agent_id: runtime.agent_id.clone(), - task_id: runtime.task_id.clone(), - session_id: runtime.session_id.clone(), - run_id: runtime.run_id.clone(), - source: runtime.source.clone(), - loop_iteration: runtime.loop_iteration, - planned_steer_cursor: runtime.applied_steer_cursor, - status: AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_EXECUTING.to_string(), - actions: pending_actions, - timings: Vec::new(), - created_at: now, - updated_at: now, - }; - write_game_creator_agent_runtime_parallel_read_batch(&root, &batch)?; - for pending in &batch.actions { - let _ = append_game_creator_agent_runtime_parallel_action_executing_record(&root, pending); - } - execute_game_creator_agent_runtime_parallel_read_members_at_locked(&root, batch) - .map(AgentRuntimeParallelReadBatchExecution::Executed) -} - -pub(super) async fn execute_game_creator_agent_runtime_parallel_read_batch( - root: &Path, - runtime: &AgentRuntimeState, - task: &str, - plan: &AgentRuntimeToolPlan, - observations: &[AgentRuntimeToolObservation], - project_revision_before: &AgentRuntimeProjectRevision, - repository_context_fingerprint: &str, - action_start_index: usize, - actions: &[AgentRuntimeToolAction], -) -> Result { - let root = root.to_path_buf(); - let runtime = runtime.clone(); - let task = task.to_string(); - let plan = plan.clone(); - let observations = observations.to_vec(); - let project_revision_before = project_revision_before.clone(); - let repository_context_fingerprint = repository_context_fingerprint.to_string(); - let actions = actions.to_vec(); - tokio::task::spawn_blocking(move || { - prepare_and_execute_game_creator_agent_runtime_parallel_read_batch_blocking( - root, - runtime, - task, - plan, - observations, - project_revision_before, - repository_context_fingerprint, - action_start_index, - actions, - ) - }) - .await - .map_err(|error| format!("Agent Runtime 只读并行批次调度失败:{error}"))? -} +pub(in crate::agent) use action_audit::*; +pub(in crate::agent) use action_execution::*; +pub(in crate::agent) use action_projection::*; +pub(in crate::agent) use autonomous_policy::*; +pub(in crate::agent) use context_compaction::*; +pub(in crate::agent) use parallel_ledger::*; +pub(in crate::agent) use parallel_read::*; +pub(in crate::agent) use pending_confirmation_ledger::*; +pub(in crate::agent) use project_gates::*; +pub(in crate::agent) use provider_action_batch::*; +pub(in crate::agent) use provider_batch_ledger::*; +pub(in crate::agent) use provider_final_reply::*; +pub(in crate::agent) use provider_request_builders::*; +pub(in crate::agent) use provider_tool_plan::*; +pub(in crate::agent) use response_stream::*; +pub(in crate::agent) use run_status_observation::*; +pub(in crate::agent) use structured_plan::*; +pub(in crate::agent) use tool_plan_protocol::*; +pub(crate) use action_audit::{ + agent_runtime_git_commit_safe_detail_value, agent_runtime_tool_action_fingerprint, + agent_runtime_tool_action_id, agent_runtime_tool_action_input_summary, + append_agent_runtime_action_receipt, append_agent_runtime_tool_call_record, + AgentRuntimeToolPolicyBlock, +}; +pub(crate) use action_execution::{ + execute_game_creator_agent_runtime_tool_action, + execute_game_creator_agent_runtime_tool_action_with_action_id, + execute_game_creator_agent_runtime_tool_action_with_pending_action, +}; +pub(crate) use action_projection::mark_game_creator_agent_runtime_auto_action_executing_if_current; +#[allow(unused_imports)] +pub(crate) use autonomous_policy::{ + agent_runtime_read_only_delivery_completion_plan_update, + agent_runtime_verified_delivery_completion_plan_update, + refresh_agent_runtime_autonomous_convergence_snapshot_after_provider_at, + validate_agent_runtime_autonomous_plan_liveness, + validate_agent_runtime_autonomous_source_payload, AgentRuntimeAutonomousSourcePayloadStats, +}; +pub(crate) use context_compaction::compact_game_creator_agent_runtime_session_at; +pub(crate) use parallel_ledger::{ + agent_runtime_confirmation_path_component, agent_runtime_parallel_read_batch_len, + agent_runtime_tool_is_parallel_safe_read, game_creator_agent_runtime_parallel_read_batch_path, + game_creator_agent_runtime_pending_tool_action_path, + game_creator_agent_runtime_provider_action_batch_path, +}; #[cfg(test)] -pub(crate) fn execute_game_creator_agent_runtime_parallel_read_batch_for_test_at( - root: &Path, - agent_id: &str, - actions: Vec, -) -> Result<(), String> { - let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state; - let task = runtime.current_task.clone(); - let plan = AgentRuntimeToolPlan { - thinking_summary: "并行读取测试".to_string(), - plan_update: None, - plan: actions - .iter() - .map(|action| format!("读取 {}", action.tool)) - .collect(), - actions: actions.clone(), - response: String::new(), - }; - let project_revision = read_game_creator_agent_runtime_project_revision(root)?; - let repository_context_fingerprint = build_repository_startup_context_at(root)?.fingerprint; - match prepare_and_execute_game_creator_agent_runtime_parallel_read_batch_blocking( - root.to_path_buf(), - runtime, - task, - plan, - Vec::new(), - project_revision, - repository_context_fingerprint, - 0, - actions, - )? { - AgentRuntimeParallelReadBatchExecution::Executed(_) => Ok(()), - AgentRuntimeParallelReadBatchExecution::NotEligible => { - Err("测试动作未形成只读并行批次".to_string()) - } - AgentRuntimeParallelReadBatchExecution::Stale => { - Err("测试动作在只读并行批次建立前已过期".to_string()) - } - } -} - +pub(crate) use parallel_read::{ + execute_game_creator_agent_runtime_parallel_read_batch_for_test_at, + project_game_creator_agent_runtime_parallel_read_batch_for_test_at, + recover_game_creator_agent_runtime_parallel_read_batch_for_test_at, + rewind_game_creator_agent_runtime_parallel_read_batch_for_test_at, +}; +pub(crate) use pending_confirmation_ledger::{ + agent_runtime_contains_secret_key_prefix, + game_creator_agent_runtime_pending_tool_action_exists, + read_game_creator_agent_runtime_pending_tool_action, + write_game_creator_agent_runtime_pending_tool_action, + write_game_creator_agent_runtime_tool_confirmation, +}; +pub(crate) use project_gates::{ + acquire_game_creator_agent_runtime_project_write_lock_with_wait, + advance_agent_runtime_project_revision_locked, + agent_runtime_observation_advances_project_revision, + agent_runtime_tool_requires_pending_revision_gate, + begin_agent_runtime_project_verification_locked, clear_agent_runtime_failed_playtest_at, + finish_agent_runtime_project_verification_locked, + invalidate_agent_runtime_project_verification_after_preview_failure_at, + is_agent_runtime_project_mutation_observation, isolated_join_completion_blocker_at, + prepare_agent_runtime_project_mutation_locked, process_session_completion_blocker_at, + project_verification_completion_blocker, project_verification_completion_blocker_at, + static_delegate_completion_blocker_at, structured_plan_completion_blocker, + validate_agent_runtime_pending_verification_gate_before, +}; #[cfg(test)] -pub(crate) fn project_game_creator_agent_runtime_parallel_read_batch_for_test_at( - root: &Path, - agent_id: &str, -) -> Result { - let mut runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state; - let batch = - read_game_creator_agent_runtime_parallel_read_batch(root, agent_id, &runtime.run_id)?; - let pending = batch - .actions - .first() - .ok_or_else(|| "测试只读并行批次缺少动作".to_string())?; - let task = pending.task.clone(); - let context_bundle = read_game_creator_agent_runtime_context_bundle_with_superseded_goal( - root, - &runtime, - Some(pending), - false, - )?; - let continuation = context_bundle - .map(continuation_from_game_creator_agent_runtime_context_bundle) - .unwrap_or_else(|| AgentRuntimeContinuationContext { - plan: pending.tool_plan(), - observations: pending.observations.clone(), - next_loop_index: usize::try_from(pending.loop_iteration).unwrap_or(usize::MAX), - ..AgentRuntimeContinuationContext::default() - }); - let plan = continuation.plan.clone(); - let mut observations = continuation.observations.clone(); - let mut tracker = AgentRuntimeContextWindowTracker::from_continuation(&continuation); - project_game_creator_agent_runtime_parallel_read_batch( - root, - &mut runtime, - &task, - &plan, - &mut observations, - continuation.next_loop_index, - &mut tracker, - &batch, - )?; - Ok(runtime) -} - +pub(crate) use project_gates::{ + supervisor_collaboration_policy_completion_blocker_for_test_at, + supervisor_orchestrator_mutation_block_after_dispatch_for_test, +}; +pub(crate) use provider_action_batch::{ + prepare_game_creator_agent_runtime_provider_action_batch, + update_game_creator_agent_runtime_provider_batch_member, AgentRuntimePendingToolAction, + AgentRuntimeProviderActionBatch, +}; +pub(crate) use provider_batch_ledger::{ + read_game_creator_agent_runtime_provider_action_batch, + recover_supervisor_collaboration_policy_snapshot_from_pending_batch_if_any_at, + write_game_creator_agent_runtime_provider_action_batch, +}; #[cfg(test)] -pub(crate) fn rewind_game_creator_agent_runtime_parallel_read_batch_for_test_at( - root: &Path, - agent_id: &str, -) -> Result, String> { - let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state; - let mut batch = - read_game_creator_agent_runtime_parallel_read_batch(root, agent_id, &runtime.run_id)?; - let action_ids = batch - .actions - .iter() - .map(|pending| pending.action_id.clone()) - .collect::>(); - for pending in &mut batch.actions { - pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING.to_string(); - pending.observation = None; - pending.updated_at = unix_timestamp(); - } - batch.status = AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_EXECUTING.to_string(); - batch.timings.clear(); - batch.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_parallel_read_batch(root, &batch)?; - Ok(action_ids) -} - +pub(crate) use provider_tool_plan::{ + request_game_creator_agent_background_tool_plan_for_test, + request_game_creator_agent_background_tool_plan_waiting_retry_for_test, +}; #[cfg(test)] -pub(crate) fn recover_game_creator_agent_runtime_parallel_read_batch_for_test_at( - root: &Path, - agent_id: &str, -) -> Result<(), String> { - let runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state; - let batch = - read_game_creator_agent_runtime_parallel_read_batch(root, agent_id, &runtime.run_id)?; - recover_game_creator_agent_runtime_parallel_read_batch_blocking(root.to_path_buf(), batch) - .map(|_| ()) -} - -pub(super) fn project_game_creator_agent_runtime_parallel_read_batch( - root: &Path, - runtime: &mut AgentRuntimeState, - task: &str, - plan: &AgentRuntimeToolPlan, - observations: &mut Vec, - loop_index: usize, - context_tracker: &mut AgentRuntimeContextWindowTracker, - batch: &AgentRuntimeParallelReadBatch, -) -> Result { - validate_game_creator_agent_runtime_parallel_read_batch(root, batch)?; - if batch.status != AGENT_RUNTIME_PARALLEL_READ_BATCH_STATUS_OBSERVED { - return Err("Agent Runtime 只读并行批次尚未形成终态 observation".to_string()); - } - let first_pending = batch - .actions - .first() - .ok_or_else(|| "Agent Runtime 只读并行批次缺少动作".to_string())?; - if first_pending.task != task - || runtime.agent_id != batch.agent_id - || runtime.task_id != batch.task_id - || runtime.session_id != batch.session_id - || runtime.run_id != batch.run_id - || runtime.source != batch.source - || runtime.loop_iteration != batch.loop_iteration - { - return Err("Agent Runtime 只读并行批次投影上下文与当前 run 不匹配".to_string()); - } - let stored_batch_start_observations = - sanitize_game_creator_agent_runtime_context_observations_for_storage( - root, - &first_pending.observations, - ); - let stored_observations = - sanitize_game_creator_agent_runtime_context_observations_for_storage(root, observations); - if stored_observations.len() < stored_batch_start_observations.len() - || stored_observations[..stored_batch_start_observations.len()] - != stored_batch_start_observations - { - return Err("Agent Runtime 只读并行批次 context 前缀与批次起点不匹配".to_string()); - } - let context_projected_count = stored_observations.len() - stored_batch_start_observations.len(); - if context_projected_count > batch.actions.len() { - return Err("Agent Runtime 只读并行批次 context 包含批次之外的 observation".to_string()); - } - for (index, pending) in batch - .actions - .iter() - .take(context_projected_count) - .enumerate() - { - let observation = pending - .observation - .as_ref() - .ok_or_else(|| "Agent Runtime 只读并行批次成员缺少 observation".to_string())?; - let stored_observation = - sanitize_game_creator_agent_runtime_context_observations_for_storage( - root, - std::slice::from_ref(observation), - ) - .into_iter() - .next() - .ok_or_else(|| "Agent Runtime 只读并行批次 observation 无法持久化".to_string())?; - if stored_observations[stored_batch_start_observations.len() + index] != stored_observation - { - return Err(format!( - "Agent Runtime 只读并行批次 context observation 顺序或内容冲突:{}", - pending.action_id - )); - } - } - *observations = stored_observations; - - let mut runtime_projected_count = 0_usize; - let mut missing_projection_seen = false; - for pending in &batch.actions { - let observation = pending - .observation - .as_ref() - .ok_or_else(|| "Agent Runtime 只读并行批次成员缺少 observation".to_string())?; - let existing_call = runtime - .recent_tool_calls - .iter() - .find(|call| call.action_id.as_deref() == Some(pending.action_id.as_str())); - if let Some(existing_call) = existing_call { - if missing_projection_seen - || existing_call.tool != observation.tool - || existing_call.status != observation.status - || existing_call.summary != sanitize_agent_runtime_text(&observation.summary, 240) - || existing_call.action_fingerprint.as_deref() - != Some( - agent_runtime_tool_action_fingerprint(&pending.action, &pending.task) - .as_str(), - ) - { - return Err(format!( - "Agent Runtime 只读并行批次已有投影顺序或内容冲突:{}", - pending.action_id - )); - } - runtime_projected_count = runtime_projected_count.saturating_add(1); - } else { - missing_projection_seen = true; - } - } - if context_projected_count > runtime_projected_count - || runtime_projected_count.saturating_sub(context_projected_count) > 1 - { - return Err(format!( - "Agent Runtime 只读并行批次状态与 context 投影前缀不一致:state={runtime_projected_count} context={context_projected_count}" - )); - } - - let mut repository_context_drifted = false; - for (index, pending) in batch.actions.iter().enumerate() { - let observation = pending - .observation - .as_ref() - .ok_or_else(|| "Agent Runtime 只读并行批次成员缺少 observation".to_string())? - .clone(); - let already_projected_to_runtime = index < runtime_projected_count; - let already_projected_to_context = index < context_projected_count; - if !already_projected_to_runtime { - let action_index = usize::try_from(pending.action_index).unwrap_or(usize::MAX); - activate_agent_runtime_plan_step( - runtime, - action_index, - pending - .action - .reason - .as_deref() - .unwrap_or(pending.action.tool.as_str()), - ); - let observation_summary = observation.summary(); - runtime.observations.push(observation_summary.clone()); - append_agent_runtime_tool_call_record( - root, - runtime, - &pending.task, - &pending.action, - &observation, - Some(&pending.action_id), - ); - runtime.pending_tool_action = None; - runtime.status = "running".to_string(); - runtime.phase = "observation".to_string(); - runtime.current_action = format!("并行读取工具 {}", observation.tool); - runtime.waiting_on = "Agent 根据只读工具观察修正计划".to_string(); - runtime.next_step = if observation.is_repository_context_drift() { - "回到同一 run 的下一轮 planning,重新确认适用仓库规范".to_string() - } else { - "按 Provider 顺序整合只读工具观察".to_string() - }; - complete_agent_runtime_active_plan_step( - runtime, - if observation.status == "ok" { - "completed" - } else { - "failed" - }, - &observation_summary, - ); - runtime.updated_at = unix_timestamp(); - let public_observation_detail = - agent_runtime_public_observation_detail(root, &observation); - append_game_creator_agent_runtime_task_projection_once( - root, - runtime, - &pending.action_id, - )?; - refresh_game_creator_agent_runtime_task_queue(root, runtime)?; - write_game_creator_agent_runtime_state(root, runtime)?; - append_game_creator_agent_runtime_action_event( - root, - runtime, - "observation", - runtime.status.as_str(), - "observation", - &observation_summary, - public_observation_detail.as_deref(), - &pending.action_id, - )?; - } else if !already_projected_to_context { - // The runtime state is written before the remaining public projections. Only the - // final projected prefix member can be in this crash window. - append_game_creator_agent_runtime_task_projection_once( - root, - runtime, - &pending.action_id, - )?; - refresh_game_creator_agent_runtime_task_queue(root, runtime)?; - write_game_creator_agent_runtime_state(root, runtime)?; - let observation_summary = observation.summary(); - let public_observation_detail = - agent_runtime_public_observation_detail(root, &observation); - append_game_creator_agent_runtime_action_event( - root, - runtime, - "observation", - runtime.status.as_str(), - "observation", - &observation_summary, - public_observation_detail.as_deref(), - &pending.action_id, - )?; - } - fail_game_creator_agent_runtime_parallel_projection_for_test_at( - root, - "after-public-projection", - index, - )?; - append_agent_runtime_action_receipt( - root, - runtime, - &pending.action_id, - &pending.action_fingerprint, - &observation.tool, - AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO, - pending.input_summary.as_deref(), - &observation, - )?; - append_agent_db_terminal_observation_if_missing_for_action( - root, - &runtime.agent_id, - &runtime.run_id, - &pending.action_id, - serde_json::json!({ - "recordType": "agent.runtime.tool_observation", - "agentId": runtime.agent_id, - "taskId": runtime.task_id, - "runId": runtime.run_id, - "tool": observation.tool, - "status": observation.status, - "summary": observation.summary, - "actionId": pending.action_id, - "actionFingerprint": pending.action_fingerprint, - "decision": "auto", - "parallelBatchId": batch.batch_id, - }), - )?; - repository_context_drifted |= observation.is_repository_context_drift(); - if !already_projected_to_context { - context_tracker.record(&observation); - observations.push(observation); - *observations = sanitize_game_creator_agent_runtime_context_observations_for_storage( - root, - observations, - ); - persist_game_creator_agent_runtime_context( - root, - runtime, - task, - plan, - observations, - loop_index, - context_tracker, - )?; - } - fail_game_creator_agent_runtime_parallel_projection_for_test_at( - root, - "after-context", - index, - )?; - } - append_game_creator_agent_runtime_parallel_read_batch_completed_audit(root, batch)?; - let overlap_nanos = agent_runtime_parallel_read_overlap_nanos(&batch.timings); - append_game_creator_agent_runtime_action_event( - root, - runtime, - "parallel_read_batch.completed", - runtime.status.as_str(), - "observation", - &format!( - "Agent 已完成 {} 个只读工具动作并按 Provider 顺序持久化。", - batch.actions.len() - ), - Some(&format!( - "batchId={} · actionCount={} · overlapNanos={} · overlapped={}", - batch.batch_id, - batch.actions.len(), - overlap_nanos, - overlap_nanos > 0 - )), - &first_pending.action_id, - )?; - fail_game_creator_agent_runtime_parallel_projection_for_test_at( - root, - "before-sidecar-remove", - batch.actions.len(), - )?; - update_game_creator_agent_runtime_provider_batch_parallel_members(root, batch)?; - remove_game_creator_agent_runtime_parallel_read_batch(root, &batch.agent_id, &batch.run_id)?; - Ok(repository_context_drifted) -} - -#[cfg(test)] -pub(super) fn fail_game_creator_agent_runtime_parallel_projection_for_test_at( - root: &Path, - stage: &str, - action_index: usize, -) -> Result<(), String> { - let marker = root.join(".agent/runtime/test-parallel-read-projection-failpoint"); - let expected = format!("{stage}:{action_index}"); - if fs::read_to_string(marker) - .ok() - .is_some_and(|value| value.trim() == expected) - { - return Err(format!("测试只读并行投影故障点:{stage}:{action_index}")); - } - Ok(()) -} - -#[cfg(not(test))] -pub(super) fn fail_game_creator_agent_runtime_parallel_projection_for_test_at( - _root: &Path, - _stage: &str, - _action_index: usize, -) -> Result<(), String> { - Ok(()) -} - -pub(super) fn mark_supervisor_delivery_claims_observed_for_pending_action_at( - root: &Path, - pending: &AgentRuntimePendingToolAction, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - if pending.action.tool != "agent.run_status" || observation.status != "ok" { - return Ok(()); - } - let observed_delegate_receipt_ids = - observed_delegate_receipt_ids_from_run_status(observation.detail.as_deref())?; - let observed_isolated_groups = - observed_isolated_join_group_ids_from_run_status(observation.detail.as_deref())?; - if pending.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - mark_static_delegate_claim_observed_for_receipts_at( - root, - &pending.agent_id, - &pending.run_id, - &pending.action_id, - &observed_delegate_receipt_ids, - )?; - } - mark_unobserved_isolated_join_claims_for_parent_at( - root, - &pending.agent_id, - &pending.run_id, - &observed_isolated_groups, - )?; - Ok(()) -} - -pub(super) fn run_status_prefixed_payload<'a>( - detail: Option<&'a str>, - prefix: &str, -) -> Result, String> { - let Some(mut remaining) = detail else { - return Ok(None); - }; - let mut found = None; - loop { - let (block, next) = remaining - .split_once("\n\n") - .map_or((remaining, ""), |(block, next)| (block, next)); - if let Some(payload) = block.strip_prefix(prefix) { - if found.replace(payload).is_some() { - return Err(format!( - "agent.run_status observation 含重复前置区块:{}", - prefix.trim_end() - )); - } - } - if !matches!( - block.split_once(':').map(|(name, _)| name), - Some( - "readyIsolatedJoins" - | "readyDelegateReceipts" - | "claimedDelegateContracts" - | "claimedIsolatedJoins" - ) - ) { - return Ok(found); - } - if next.is_empty() { - return Ok(found); - } - remaining = next; - } -} - -pub(super) fn observed_delegate_receipt_ids_from_run_status( - detail: Option<&str>, -) -> Result, String> { - let Some(payload) = run_status_prefixed_payload(detail, "readyDelegateReceipts: ")? else { - return Ok(BTreeSet::new()); - }; - let payload = serde_json::from_str::(payload) - .map_err(|error| format!("解析已观察专业 Agent ready receipts 失败:{error}"))?; - if payload.get("ready").and_then(serde_json::Value::as_bool) != Some(true) { - return Err("已观察专业 Agent ready receipts 结果未标记 ready=true".to_string()); - } - let receipts = payload - .get("receipts") - .and_then(serde_json::Value::as_array) - .ok_or_else(|| "已观察专业 Agent ready receipts 结果缺少 receipts".to_string())?; - let mut delegation_ids = BTreeSet::new(); - for receipt in receipts { - let delegation_id = receipt - .get("delegationId") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| "已观察专业 Agent ready receipts 结果缺少 delegationId".to_string())?; - if !delegation_ids.insert(delegation_id.to_string()) { - return Err(format!( - "已观察专业 Agent ready receipts 结果含重复 delegationId:{delegation_id}" - )); - } - } - Ok(delegation_ids) -} - -pub(super) fn observed_isolated_join_group_ids_from_run_status( - detail: Option<&str>, -) -> Result, String> { - let Some(payload) = run_status_prefixed_payload(detail, "readyIsolatedJoins: ")? else { - return Ok(BTreeSet::new()); - }; - let payload = serde_json::from_str::(payload) - .map_err(|error| format!("解析已观察动态隔离 Agent join 结果失败:{error}"))?; - if payload.get("ready").and_then(serde_json::Value::as_bool) != Some(true) { - return Err("已观察动态隔离 Agent join 结果未标记 ready=true".to_string()); - } - let joins = payload - .get("joins") - .and_then(serde_json::Value::as_array) - .ok_or_else(|| "已观察动态隔离 Agent join 结果缺少 joins".to_string())?; - let mut group_ids = BTreeSet::new(); - for join in joins { - let group_id = join - .get("delegationGroupId") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| "已观察动态隔离 Agent join 结果缺少 delegationGroupId".to_string())?; - if !group_ids.insert(group_id.to_string()) { - return Err(format!( - "已观察动态隔离 Agent join 结果含重复 group:{group_id}" - )); - } - } - Ok(group_ids) -} - -#[cfg(test)] -mod run_status_observation_tests { - use super::*; - - #[test] - fn mixed_ready_prefixes_parse_exact_static_and_isolated_ids() { - let detail = concat!( - "readyIsolatedJoins: {\"ready\":true,\"joins\":[{\"delegationGroupId\":\"group-a\"}]}\n\n", - "readyDelegateReceipts: {\"ready\":true,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}\n\n", - "agentId: project-supervisor" - ); - assert_eq!( - observed_isolated_join_group_ids_from_run_status(Some(detail)) - .expect("parse mixed isolated prefix"), - BTreeSet::from(["group-a".to_string()]) - ); - assert_eq!( - observed_delegate_receipt_ids_from_run_status(Some(detail)) - .expect("parse mixed static prefix"), - BTreeSet::from(["delivery-a".to_string()]) - ); - } - - #[test] - fn ready_prefix_parser_rejects_false_and_duplicate_blocks() { - let not_ready = - "readyDelegateReceipts: {\"ready\":false,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}"; - assert!( - observed_delegate_receipt_ids_from_run_status(Some(not_ready)) - .expect_err("ready=false must fail closed") - .contains("ready=true") - ); - - let duplicate = concat!( - "readyDelegateReceipts: {\"ready\":true,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}\n\n", - "readyDelegateReceipts: {\"ready\":true,\"receipts\":[{\"delegationId\":\"delivery-a\"}]}" - ); - assert!( - observed_delegate_receipt_ids_from_run_status(Some(duplicate)) - .expect_err("duplicate ready receipt blocks must fail closed") - .contains("重复前置区块") - ); - } -} - -impl AgentRuntimeToolObservation { - pub(crate) fn summary(&self) -> String { - format!("{}:{} · {}", self.tool, self.status, self.summary) - } - - pub(super) fn is_waiting_for_confirmation(&self) -> bool { - self.status == "waiting-for-confirmation" - } - - pub(super) fn requires_reconciliation(&self) -> bool { - self.status == AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION - } - - pub(super) fn is_repository_context_drift(&self) -> bool { - self.status == "blocked" - && self - .detail - .as_deref() - .is_some_and(|detail| detail.starts_with("repositoryContextDrift=true")) - } -} - -pub(super) fn agent_runtime_local_observation_detail( - observation: &AgentRuntimeToolObservation, -) -> Option<&str> { - if matches!( - observation.tool.as_str(), - "command.poll" - | "command.stdin" - | GAME_CREATOR_MCP_CALL_TOOL - | GAME_CREATOR_USER_INPUT_REQUEST_TOOL - ) { - None - } else { - observation.detail.as_deref() - } -} - -pub(super) fn agent_runtime_public_observation_detail( - root: &Path, - observation: &AgentRuntimeToolObservation, -) -> Option { - if observation.tool == "agent.action_history" && observation.status == "ok" { - let detail = observation.detail.as_deref()?; - validate_agent_runtime_pending_serialized_content(root, detail).ok()?; - return Some(detail.to_string()); - } - agent_runtime_action_receipt_safe_detail(root, observation) -} - -pub(crate) fn advance_agent_runtime_project_revision_locked(root: &Path) -> Result { - let mut revision = read_game_creator_agent_runtime_project_revision(root)?; - let next_revision = revision - .revision - .checked_add(1) - .ok_or_else(|| "Agent Runtime 项目 revision 已达到上限".to_string())?; - revision.revision = next_revision; - revision.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_project_revision(root, &revision) - .map_err(|error| format!("项目 revision 持久化失败,禁止继续写入:{error}"))?; - Ok(next_revision) -} - -pub(super) fn supervisor_orchestrator_mutation_block_at( - root: &Path, - agent_id: &str, - run_id: &str, - tool: &str, -) -> Option { - if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - || !is_supervisor_orchestrator_project_mutation_tool(tool) - { - return None; - } - let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) { - Ok(resolution) => resolution.policy, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: "无法确认 Project Supervisor 协作策略,未执行项目修改".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - if !policy.orchestrator_only_after_delegation { - return None; - } - match read_supervisor_collaboration_state_at(root, agent_id, run_id) { - Ok(state) if !state.has_collaboration() => None, - Ok(state) => Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: "Project Supervisor 已进入协作编排,未执行项目修改".to_string(), - detail: Some(format!( - "initialStaticAgents={} · isolatedGroups={} · isolatedChildren={};请把修改交给专业 Agent,Supervisor 只继续委派、读取、认领回执和验证。", - state.initial_static_agent_ids.len(), - state.isolated_group_count, - state.isolated_child_count, - )), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: "无法确认 Project Supervisor 协作事实,未执行项目修改".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -#[cfg(test)] -pub(crate) fn supervisor_orchestrator_mutation_block_after_dispatch_for_test( - root: &Path, - agent_id: &str, - run_id: &str, - tool: &str, -) -> Option { - supervisor_orchestrator_mutation_block_at(root, agent_id, run_id, tool) -} - -pub(super) async fn supervisor_orchestrator_mcp_mutation_block_at( - root: &Path, - agent_id: &str, - run_id: &str, - action: &AgentRuntimeToolAction, -) -> Option { - if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - || action.tool.trim() != GAME_CREATOR_MCP_CALL_TOOL - { - return None; - } - let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) { - Ok(resolution) => resolution.policy, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: GAME_CREATOR_MCP_CALL_TOOL.to_string(), - status: "blocked".to_string(), - summary: "无法确认 Project Supervisor 协作策略,未执行 MCP".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - if !policy.orchestrator_only_after_delegation { - return None; - } - let state = match read_supervisor_collaboration_state_at(root, agent_id, run_id) { - Ok(state) if !state.has_collaboration() => return None, - Ok(state) => state, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: GAME_CREATOR_MCP_CALL_TOOL.to_string(), - status: "blocked".to_string(), - summary: "无法确认 Project Supervisor 协作事实,未执行 MCP".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - match game_creator_mcp_action_is_strictly_read_only_at(root, action).await { - Ok(true) => None, - Ok(false) => Some(AgentRuntimeToolObservation { - tool: GAME_CREATOR_MCP_CALL_TOOL.to_string(), - status: "blocked".to_string(), - summary: "Project Supervisor 已进入协作编排,未执行非只读 MCP".to_string(), - detail: Some(format!( - "initialStaticAgents={} · isolatedGroups={} · isolatedChildren={};MCP 工具必须同时声明 readOnlyHint=true 与 destructiveHint=false。", - state.initial_static_agent_ids.len(), - state.isolated_group_count, - state.isolated_child_count, - )), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: GAME_CREATOR_MCP_CALL_TOOL.to_string(), - status: "blocked".to_string(), - summary: "无法确认 MCP 工具只读身份,未执行调用".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(crate) fn prepare_agent_runtime_project_mutation_locked( - root: &Path, - agent_id: &str, - run_id: &str, - tool: &str, -) -> Result { - if let Some(blocker) = supervisor_orchestrator_mutation_block_at(root, agent_id, run_id, tool) { - return Err(format!( - "{}:{}", - blocker.summary, - blocker.detail.unwrap_or_default() - )); - } - 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 next_revision = revision - .revision - .checked_add(1) - .ok_or_else(|| "Agent Runtime 项目 revision 已达到上限".to_string())?; - let now = unix_timestamp(); - gate.requires_verification = true; - gate.mutation_revision = Some(next_revision); - gate.verified_revision = None; - gate.last_mutation_tool = Some(tool.to_string()); - gate.last_verification_tool = None; - gate.last_verification_status = None; - gate.failed_playtest_revision = None; - gate.updated_at = now; - if let Err(error) = write_game_creator_agent_runtime_verification_gate(root, &gate) { - revision.revision = next_revision; - revision.updated_at = now; - let fallback = write_game_creator_agent_runtime_project_revision(root, &revision); - return Err(match fallback { - Ok(()) => format!("项目验证门禁持久化失败;已保守推进 revision 使旧凭证失效:{error}"), - Err(revision_error) => format!( - "项目验证门禁和 revision 均无法持久化,禁止继续修改:{error};{revision_error}" - ), - }); - } - revision.revision = next_revision; - revision.updated_at = now; - write_game_creator_agent_runtime_project_revision(root, &revision).map_err(|error| { - format!("项目 revision 持久化失败,当前 run 已保持待验证状态并禁止继续修改:{error}") - })?; - Ok(next_revision) -} - -pub(crate) fn begin_agent_runtime_project_verification_locked( - root: &Path, - agent_id: &str, - run_id: &str, - tool: &str, -) -> Result<(AgentRuntimeProjectRevision, AgentRuntimeVerificationGate), String> { - let revision = read_game_creator_agent_runtime_project_revision(root)?; - let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - gate.verified_revision = None; - gate.last_verification_tool = Some(tool.to_string()); - gate.last_verification_status = Some(AGENT_RUNTIME_VERIFICATION_STATUS_RUNNING.to_string()); - gate.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_verification_gate(root, &gate) - .map_err(|error| format!("清除旧验证凭证失败,未执行 {tool}:{error}"))?; - Ok((revision, gate)) -} - -pub(crate) fn finish_agent_runtime_project_verification_locked( - root: &Path, - expected_revision: &AgentRuntimeProjectRevision, - mut gate: AgentRuntimeVerificationGate, - passed: bool, -) -> Result<(), String> { - let current_revision = read_game_creator_agent_runtime_project_revision(root)?; - let revision_unchanged = current_revision.revision == expected_revision.revision; - let passed = passed && revision_unchanged; - let verification_tool = gate.last_verification_tool.clone(); - gate.verified_revision = passed - .then_some(current_revision.revision) - .filter(|value| *value > 0); - if passed - && gate - .failed_playtest_revision - .is_some_and(|failed_revision| failed_revision < current_revision.revision) - { - gate.failed_playtest_revision = None; - } - if passed && verification_tool.as_deref() == Some("preview.validate") { - gate.failed_playtest_revision = None; - } - gate.last_verification_status = Some( - if passed { - AGENT_RUNTIME_VERIFICATION_STATUS_PASSED - } else { - AGENT_RUNTIME_VERIFICATION_STATUS_FAILED - } - .to_string(), - ); - gate.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_verification_gate(root, &gate)?; - if !revision_unchanged { - return Err(format!( - "验证期间项目 revision 已从 {} 变化为 {},结果不再有效", - expected_revision.revision, current_revision.revision - )); - } - Ok(()) -} - -pub(crate) fn clear_agent_runtime_failed_playtest_at( - root: &Path, - agent_id: &str, - run_id: &str, - expected_revision: u64, -) -> Result<(), String> { - let _lock = - acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, "preview.validate")?; - let current_revision = read_game_creator_agent_runtime_project_revision(root)?; - if current_revision.revision != expected_revision { - return Err(format!( - "浏览器试玩通过后项目 revision 已从 {expected_revision} 变化为 {}", - current_revision.revision - )); - } - let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - let owns_current_mutation = gate.mutation_revision == Some(expected_revision); - let supervisor_verified_current_revision = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - && gate.verified_revision == Some(expected_revision) - && gate.last_verification_status.as_deref() - == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED); - if !owns_current_mutation && !supervisor_verified_current_revision { - return Err("浏览器试玩通过时没有当前 revision 的项目修改凭证".to_string()); - } - gate.failed_playtest_revision = None; - gate.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_verification_gate(root, &gate) -} - -pub(crate) fn invalidate_agent_runtime_project_verification_after_preview_failure_at( - root: &Path, - agent_id: &str, - run_id: &str, - expected_revision: u64, -) -> Result<(), String> { - let _lock = - acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, "preview.validate")?; - let current_revision = read_game_creator_agent_runtime_project_revision(root)?; - if current_revision.revision != expected_revision { - return Err(format!( - "浏览器试玩失败后项目 revision 已从 {expected_revision} 变化为 {}", - current_revision.revision - )); - } - let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - let owns_current_mutation = gate.mutation_revision == Some(expected_revision); - let supervisor_verified_current_revision = agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - && gate.verified_revision == Some(expected_revision) - && gate.last_verification_status.as_deref() - == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED); - if !owns_current_mutation && !supervisor_verified_current_revision { - return Err("浏览器试玩失败时没有当前 revision 的项目修改凭证".to_string()); - } - if owns_current_mutation { - gate.requires_verification = true; - } else { - gate.requires_verification = false; - gate.mutation_revision = None; - gate.last_mutation_tool = None; - } - gate.verified_revision = None; - gate.last_verification_tool = Some("preview.validate".to_string()); - gate.last_verification_status = Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED.to_string()); - gate.failed_playtest_revision = Some(expected_revision); - gate.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_verification_gate(root, &gate) -} - -pub(super) fn validate_agent_runtime_verification_gate_snapshot( - root: &Path, - snapshot: &AgentRuntimeVerificationGate, -) -> Result<(), String> { - validate_agent_runtime_verification_gate(root, snapshot, &snapshot.agent_id, &snapshot.run_id)?; - let current = read_game_creator_agent_runtime_verification_gate( - root, - &snapshot.agent_id, - &snapshot.run_id, - )?; - if snapshot.requires_verification && !current.requires_verification { - return Err("Agent Runtime verification gate sidecar 早于持久化快照".to_string()); - } - if current.mutation_revision.unwrap_or(0) < snapshot.mutation_revision.unwrap_or(0) { - return Err( - "Agent Runtime verification gate sidecar 的 mutationRevision 已回退".to_string(), - ); - } - Ok(()) -} - -pub(crate) fn agent_runtime_tool_requires_pending_revision_gate(tool: &str) -> bool { - !matches!( - tool, - "memory.read" - | "conversation.read" - | "asset.list" - | "project.index" - | "project.search" - | "project.diff" - | "git.inspect" - | "file.list" - | "file.read" - | "task.list" - | "agent.action_history" - | "agent.run_status" - | "command.output_read" - | "command.poll" - | "command.stdin" - | "command.terminate" - | "image.inspect" - ) -} - -pub(crate) fn validate_agent_runtime_pending_verification_gate_before( - root: &Path, - pending: &AgentRuntimePendingToolAction, -) -> Result<(), String> { - validate_agent_runtime_project_revision(root, &pending.project_revision_before)?; - let prior_action_count = usize::try_from(pending.action_index).unwrap_or(usize::MAX); - let prior_revision_advances = pending - .observations - .iter() - .rev() - .take(prior_action_count) - .filter(|observation| agent_runtime_observation_advances_project_revision(observation)) - .count(); - let expected_revision = pending - .project_revision_before - .revision - .checked_add(u64::try_from(prior_revision_advances).unwrap_or(u64::MAX)) - .ok_or_else(|| "Agent Runtime 待执行动作的预期项目 revision 已达到上限".to_string())?; - let current_revision = read_game_creator_agent_runtime_project_revision(root)?; - if current_revision.revision != expected_revision { - return Err(format!( - "Agent Runtime 待执行动作创建后的项目 revision 已变化,禁止执行旧动作:预期 {expected_revision},当前 {}", - current_revision.revision - )); - } - validate_agent_runtime_verification_gate( - root, - &pending.verification_gate_before, - &pending.agent_id, - &pending.run_id, - )?; - let current = read_game_creator_agent_runtime_verification_gate( - root, - &pending.agent_id, - &pending.run_id, - )?; - if current != pending.verification_gate_before { - return Err( - "Agent Runtime 待执行动作创建后的 verification gate 已变化,禁止自动重放".to_string(), - ); - } - Ok(()) -} - -pub(super) fn agent_runtime_verification_blocker( - summary: impl Into, - detail: impl Into, -) -> AgentRuntimeToolObservation { - AgentRuntimeToolObservation { - tool: "runtime.verification".to_string(), - status: "blocked".to_string(), - summary: summary.into(), - detail: Some(detail.into()), - } -} - -pub(super) fn agent_runtime_mutation_gate_failure_observation( - root: &Path, - tool: &str, - error: &str, -) -> AgentRuntimeToolObservation { - AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "verification-failed".to_string(), - summary: "项目验证门禁准备失败,未执行修改".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, error, 500)), - } -} - -pub(super) fn agent_runtime_revision_advance_failure_observation( - root: &Path, - tool: &str, - error: &str, -) -> AgentRuntimeToolObservation { - AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "failed".to_string(), - summary: "项目 revision 推进失败,未执行写入".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, error, 500)), - } -} - -pub(crate) fn is_agent_runtime_project_mutation_observation( - observation: &AgentRuntimeToolObservation, -) -> bool { - if observation.tool == "command.start" { - return agent_runtime_command_start_advanced_project_revision(observation); - } - if observation.tool == "command.exec" { - return agent_runtime_command_exec_advanced_project_revision(observation); - } - if observation.tool == "project.patchset" { - return agent_runtime_patchset_advanced_project_revision(observation); - } - observation.status == "ok" - && matches!( - observation.tool.as_str(), - "file.write" | "file.patch" | "file.delete" | "project.patchset" | "project.restore" - ) -} - -pub(super) fn agent_runtime_command_start_advanced_project_revision( - observation: &AgentRuntimeToolObservation, -) -> bool { - observation.tool == "command.start" - && observation - .detail - .as_deref() - .and_then(|detail| serde_json::from_str::(detail).ok()) - .and_then(|detail| { - detail - .get("revisionAdvanced") - .and_then(serde_json::Value::as_bool) - }) - .unwrap_or(false) -} - -pub(super) fn agent_runtime_patchset_advanced_project_revision( - observation: &AgentRuntimeToolObservation, -) -> bool { - observation.tool == "project.patchset" - && (observation.status == "ok" - || observation - .detail - .as_deref() - .is_some_and(|detail| detail.starts_with("revisionAdvanced=true"))) -} - -pub(super) fn agent_runtime_command_exec_advanced_project_revision( - observation: &AgentRuntimeToolObservation, -) -> bool { - observation.tool == "command.exec" - && (matches!( - observation.status.as_str(), - "ok" | "command-failed" | AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION - ) || (observation.status == "verification-failed" - && observation - .detail - .as_deref() - .is_some_and(|detail| detail.starts_with("revisionAdvanced=true")))) -} - -pub(super) fn agent_runtime_command_exec_is_verification_eligible( - observation: &AgentRuntimeToolObservation, -) -> bool { - observation.detail.as_deref().is_some_and(|detail| { - detail.starts_with("verificationEligible=true ·") - || detail.starts_with("revisionAdvanced=true · verificationEligible=true ·") - }) -} - -pub(crate) fn agent_runtime_observation_advances_project_revision( - observation: &AgentRuntimeToolObservation, -) -> bool { - if agent_runtime_command_start_advanced_project_revision(observation) { - return true; - } - if agent_runtime_command_exec_advanced_project_revision(observation) { - return true; - } - if agent_runtime_patchset_advanced_project_revision(observation) { - return true; - } - if observation.status != "ok" { - return false; - } - match observation.tool.as_str() { - "file.write" - | "file.patch" - | "file.delete" - | "project.patchset" - | "project.restore" - | "blackboard.write" - | "canvas.asset_generate" => true, - "memory.write" => true, - _ => false, - } -} - -pub(super) fn is_agent_runtime_static_smoke_observation( - observation: &AgentRuntimeToolObservation, -) -> bool { - observation.tool == "command.run_limited" - && matches!( - observation.summary.as_str(), - "game.static_smoke 已完成" - | "game.static_smoke 执行失败" - | "game.static_smoke 无法取得项目验证锁" - | "game.static_smoke 无法清除旧验证凭证" - | "game.static_smoke 结果无法形成有效验证凭证" - ) -} - -pub(super) fn is_agent_runtime_project_verification_observation( - observation: &AgentRuntimeToolObservation, -) -> bool { - observation.tool == "project.verify" - || (observation.tool == "command.exec" - && agent_runtime_command_exec_is_verification_eligible(observation)) - || is_agent_runtime_static_smoke_observation(observation) -} - -pub(super) fn agent_runtime_project_verification_label( - observation: &AgentRuntimeToolObservation, -) -> &'static str { - if is_agent_runtime_static_smoke_observation(observation) { - "game.static_smoke" - } else if observation.tool == "command.exec" { - "command.exec" - } else { - "project.verify" - } -} - -pub(super) fn isolated_join_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - match isolated_join_completion_barrier_at(root, agent_id, run_id) { - Ok(None) => None, - Ok(Some(detail)) => Some(AgentRuntimeToolObservation { - tool: "runtime.isolated_join".to_string(), - status: "blocked".to_string(), - summary: "动态隔离 Agent 的 all-join 尚未完成并认领,不能收束当前任务".to_string(), - detail: Some(detail), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: "runtime.isolated_join".to_string(), - status: "blocked".to_string(), - summary: "无法确认动态隔离 Agent 的 all-join 状态,不能收束当前任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(super) fn static_delegate_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - return None; - } - match static_delegate_completion_barrier_at(root, agent_id, run_id) { - Ok(barrier) if barrier.is_clear() => None, - Ok(barrier) => Some(AgentRuntimeToolObservation { - tool: "runtime.delegate_receipts".to_string(), - status: "blocked".to_string(), - summary: "专业 Agent 委派尚未完成并认领,不能收束当前用户任务".to_string(), - detail: Some(barrier.detail()), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: "runtime.delegate_receipts".to_string(), - status: "blocked".to_string(), - summary: "无法确认专业 Agent 委派回执状态,不能收束当前用户任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(super) fn supervisor_collaboration_policy_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - if agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { - return None; - } - let policy = match resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id) { - Ok(resolution) => resolution.policy, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: "runtime.collaboration_policy".to_string(), - status: "blocked".to_string(), - summary: "无法读取 Project Supervisor 协作策略,不能收束当前任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - let state = match read_supervisor_collaboration_state_at(root, agent_id, run_id) { - Ok(state) => state, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: "runtime.collaboration_policy".to_string(), - status: "blocked".to_string(), - summary: "无法读取 Project Supervisor durable 协作事实,不能收束当前任务" - .to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - supervisor_collaboration_completion_gap(&policy, &state).map(|detail| { - AgentRuntimeToolObservation { - tool: "runtime.collaboration_policy".to_string(), - status: "blocked".to_string(), - summary: "Project Supervisor 尚未满足项目要求的协作合同".to_string(), - detail: Some(detail), - } - }) -} - -#[cfg(test)] -pub(crate) fn supervisor_collaboration_policy_completion_blocker_for_test_at( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - supervisor_collaboration_policy_completion_blocker_at_locked(root, agent_id, run_id) -} - -pub(super) fn isolated_join_barrier_has_waiting_groups(detail: &str) -> bool { - detail - .split_whitespace() - .find_map(|part| part.strip_prefix("waitingGroups=")) - .and_then(|value| value.parse::().ok()) - .is_some_and(|count| count > 0) -} - -pub(super) fn static_delegate_barrier_has_waiting_deliveries(detail: &str) -> bool { - detail - .split_whitespace() - .find_map(|part| part.strip_prefix("waitingDelegations=")) - .and_then(|value| value.parse::().ok()) - .is_some_and(|count| count > 0) -} - -pub(super) fn static_delegate_barrier_requires_repair(detail: &str) -> bool { - detail - .split_whitespace() - .find_map(|part| part.strip_prefix("repairRequired=")) - .and_then(|value| value.parse::().ok()) - .is_some_and(|count| count > 0) -} - -pub(super) fn process_session_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - match active_process_session_records_at(root, Some(agent_id), Some(run_id)) { - Ok(records) if records.is_empty() => None, - Ok(records) => { - let needs_reconciliation = records.iter().any(|record| { - record.needs_reconciliation || record.status == "needs-reconciliation" - }); - let sessions = records - .iter() - .map(|record| format!("{}:{}", record.process_id, record.status)) - .collect::>() - .join(","); - Some(AgentRuntimeToolObservation { - tool: "runtime.process_session".to_string(), - status: "blocked".to_string(), - summary: if needs_reconciliation { - "当前 run 存在待人工核对的进程会话,不能收束任务".to_string() - } else { - "当前 run 仍有活跃进程会话,不能收束任务".to_string() - }, - detail: Some(format!( - "processSessions={sessions} · 请先用 command.poll 读取状态,并在需要时调用 command.terminate" - )), - }) - } - Err(error) => Some(AgentRuntimeToolObservation { - tool: "runtime.process_session".to_string(), - status: "blocked".to_string(), - summary: "无法确认当前 run 的进程会话状态,不能收束任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(super) fn agent_runtime_non_verification_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - provider_retry_completion_blocker_at_locked(root, agent_id, run_id) - .or_else(|| provider_action_batch_completion_blocker_at_locked(root, agent_id, run_id)) - .or_else(|| { - supervisor_collaboration_policy_completion_blocker_at_locked(root, agent_id, run_id) - }) - .or_else(|| process_session_completion_blocker_at_locked(root, agent_id, run_id)) - .or_else(|| isolated_join_completion_blocker_at_locked(root, agent_id, run_id)) - .or_else(|| static_delegate_completion_blocker_at_locked(root, agent_id, run_id)) - .or_else(|| visual_asset_completion_blocker_at_locked(root, agent_id, Some(run_id))) -} - -pub(super) fn ui_prototype_visual_inspection_blocker_detail_at_locked( - root: &Path, - agent_id: &str, - required_run_id: Option<&str>, - expected_path: &str, -) -> Result, String> { - let inspection_run_id = required_run_id.unwrap_or("task_update_current_image"); - let mut images = load_agent_runtime_inspection_images( - root, - agent_id, - inspection_run_id, - &[expected_path.to_string()], - )?; - let image = images - .pop() - .ok_or_else(|| "UI 原型图片读取结果为空".to_string())?; - let (records, scan_truncated) = - read_agent_db_records_bounded(root, AGENT_RUNTIME_ACTION_HISTORY_MAX_DB_BYTES)?; - let matching = records.iter().rev().find(|record| { - record.get("recordType").and_then(serde_json::Value::as_str) - == Some("agent.runtime.image.inspect") - && record.get("agentId").and_then(serde_json::Value::as_str) == Some(agent_id) - && required_run_id.is_none_or(|run_id| { - record.get("runId").and_then(serde_json::Value::as_str) == Some(run_id) - }) - && record - .get("inspectionKind") - .and_then(serde_json::Value::as_str) - == Some(AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND) - && record - .get("validationProfile") - .and_then(serde_json::Value::as_str) - == Some(AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE) - && record - .get("images") - .and_then(serde_json::Value::as_array) - .is_some_and(|items| { - items.len() == 1 - && items[0].get("path").and_then(serde_json::Value::as_str) - == Some(expected_path) - && items[0].get("sha256").and_then(serde_json::Value::as_str) - == Some(image.sha256.as_str()) - }) - }); - let Some(record) = matching else { - return Ok(Some(format!( - "expectedPath={expected_path} · currentSha256={} · requiredInspection=image.inspect · inspectionRunId={} · scanTruncated={scan_truncated}", - image.sha256, - required_run_id.unwrap_or("latest-current-image") - ))); - }; - let checks = serde_json::from_value::( - record - .get("checks") - .cloned() - .ok_or_else(|| "UI 原型视觉检查审计缺少 checks".to_string())?, - ) - .map_err(|error| format!("解析 UI 原型视觉检查 checks 失败:{error}"))?; - let issues = serde_json::from_value::>( - record - .get("issues") - .cloned() - .ok_or_else(|| "UI 原型视觉检查审计缺少 issues".to_string())?, - ) - .map_err(|error| format!("解析 UI 原型视觉检查 issues 失败:{error}"))?; - let assessment = AgentRuntimeUiPrototypeAssessment { - checks, - issues, - summary: "结构化 UI 视觉检查审计".to_string(), - } - .validate()?; - let recorded_passed = record - .get("passed") - .and_then(serde_json::Value::as_bool) - .ok_or_else(|| "UI 原型视觉检查审计缺少 passed".to_string())?; - if recorded_passed != assessment.passed() { - return Err("UI 原型视觉检查审计的 passed 与结构化字段冲突".to_string()); - } - if recorded_passed { - return Ok(None); - } - Ok(Some(format!( - "expectedPath={expected_path} · resourceBar={} · unitCardTray={} · battlefieldGrid={} · enemyEntryDirection={} · waveStatus={} · primaryControls={} · implementationClarity={} · originalTheme={} · issues={}", - assessment.checks.resource_bar, - assessment.checks.unit_card_tray, - assessment.checks.battlefield_grid, - assessment.checks.enemy_entry_direction, - assessment.checks.wave_status, - assessment.checks.primary_controls, - assessment.checks.implementation_clarity, - assessment.checks.original_theme, - assessment.issues.join(";"), - ))) -} - -pub(super) fn visual_asset_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - required_run_id: Option<&str>, -) -> Option { - let (expected_path, expected_kind, label) = match agent_id { - "design-foundation" => ("assets/ui-prototype.png", "ui-prototype", "策划界面原型图"), - "art-asset-plan" => ( - "assets/art-spritesheet.png", - "art-spritesheet", - "首版美术素材图", - ), - _ => return None, - }; - let manifest = match read_manifest_for_project(root) { - Ok(manifest) => manifest, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: "runtime.visual_asset".to_string(), - status: "blocked".to_string(), - summary: format!("无法核对{label},不能完成任务"), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - let registered = manifest.assets.iter().any(|asset| { - asset.local_path == expected_path - && asset.kind == expected_kind - && asset.media_type.starts_with("image/") - && asset.source.kind == GameCreationAppAssetSourceKind::Canvas - && resolve_local_project_path(root, &asset.local_path) - .ok() - .is_some_and(|path| path.is_file()) - }); - if !registered { - return Some(AgentRuntimeToolObservation { - tool: "runtime.visual_asset".to_string(), - status: "blocked".to_string(), - summary: format!("{label}尚未生成并登记,不能完成任务"), - detail: Some(format!( - "expectedPath={expected_path} · expectedKind={expected_kind} · editorApiKeyConfigured={}", - editor_api_key_is_configured() - )), - }); - } - if agent_id != "design-foundation" { - return None; - } - match ui_prototype_visual_inspection_blocker_detail_at_locked( - root, - agent_id, - required_run_id, - expected_path, - ) { - Ok(None) => None, - Ok(Some(detail)) => Some(AgentRuntimeToolObservation { - tool: "runtime.visual_asset".to_string(), - status: "blocked".to_string(), - summary: "策划界面原型图尚未通过结构化 UI 视觉检查,不能完成任务".to_string(), - detail: Some(detail), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: "runtime.visual_asset".to_string(), - status: "blocked".to_string(), - summary: "无法核对策划界面原型图的结构化 UI 视觉证据,不能完成任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(super) fn provider_retry_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - match provider_retry::read_for_run_at(root, agent_id, run_id) { - Ok(None) => None, - Ok(Some(retry)) => Some(AgentRuntimeToolObservation { - tool: "runtime.provider_retry".to_string(), - status: "blocked".to_string(), - summary: "Provider retry 尚未完成持久等待与请求收束,不能提交最终回复".to_string(), - detail: Some(format!( - "requestKind={} · nextAttempt={} · maxRetries={} · remainingMs={}", - retry.identity.request_kind, - retry.next_attempt, - retry.max_retries, - provider_retry::remaining_ms(&retry), - )), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: "runtime.provider_retry".to_string(), - status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(), - summary: "Provider retry sidecar 无法通过校验,不能提交最终回复".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(super) fn provider_action_batch_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - if !game_creator_agent_runtime_provider_action_batch_exists(root, agent_id, run_id) { - return None; - } - match read_game_creator_agent_runtime_provider_action_batch(root, agent_id, run_id) { - Ok(batch) => Some(AgentRuntimeToolObservation { - tool: "runtime.provider_action_batch".to_string(), - status: "blocked".to_string(), - summary: "Provider action 批次尚未完成幂等收束,不能提交最终回复".to_string(), - detail: Some(format!( - "batchId={} · status={} · nextActionIndex={} · actionCount={}", - batch.batch_id, - batch.status, - batch.next_action_index, - batch.actions.len() - )), - }), - Err(error) => Some(AgentRuntimeToolObservation { - tool: "runtime.provider_action_batch".to_string(), - status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(), - summary: "Provider action 批次 sidecar 无法通过校验,不能提交最终回复".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }), - } -} - -pub(crate) fn structured_plan_completion_blocker( - runtime: &AgentRuntimeState, -) -> Option { - if !agent_runtime_has_structured_plan(runtime) { - return None; - } - if let Err(error) = validate_agent_runtime_structured_plan_snapshot( - runtime.plan_revision, - &runtime.plan_explanation, - &runtime.plan, - &runtime.plan_steps, - runtime.active_plan_step_index, - ) { - return Some(AgentRuntimeToolObservation { - tool: "runtime.plan_update".to_string(), - status: "needs-reconciliation".to_string(), - summary: "结构化计划快照无效,不能收束当前任务".to_string(), - detail: Some(format!( - "planRevision={} · snapshotErrorSha256={:x}", - runtime.plan_revision, - Sha256::digest(error.as_bytes()) - )), - }); - } - let incomplete = runtime - .plan_steps - .iter() - .filter(|step| step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED) - .collect::>(); - if incomplete.is_empty() { - return None; - } - let completed = runtime - .plan_steps - .iter() - .filter(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_COMPLETED) - .count(); - let pending = incomplete - .iter() - .filter(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_PENDING) - .count(); - let in_progress = incomplete - .iter() - .filter(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS) - .count(); - let failed = incomplete - .iter() - .filter(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_FAILED) - .count(); - let step_status_hashes = incomplete - .iter() - .take(AGENT_RUNTIME_PLAN_STEP_LIMIT) - .map(|step| { - format!( - "{}:{:x}", - step.status, - Sha256::digest(step.title.as_bytes()) - ) - }) - .collect::>() - .join(","); - Some(AgentRuntimeToolObservation { - tool: "runtime.plan_update".to_string(), - status: "blocked".to_string(), - summary: format!( - "结构化计划尚未完成:{completed}/{}", - runtime.plan_steps.len() - ), - detail: Some(format!( - "planRevision={} · completed={} · pending={} · inProgress={} · failed={} · stepStatusSha256={};只有步骤或状态真实变化时才单独提交 planUpdate;当前 in_progress 步骤已具备执行条件时必须在同一响应调用具体 action,不能只改计划解释。Runtime 不会按工具数组下标自动完成步骤。", - runtime.plan_revision, - completed, - pending, - in_progress, - failed, - step_status_hashes - )), - }) -} - -pub(crate) fn process_session_completion_blocker_at( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.process_session.completion", - ) { - Ok(lock) => lock, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: "runtime.process_session".to_string(), - status: "blocked".to_string(), - summary: "无法取得进程会话一致性锁,不能收束任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - process_session_completion_blocker_at_locked(root, agent_id, run_id) -} - -pub(crate) fn isolated_join_completion_blocker_at( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.isolated_join.complete", - ) { - Ok(lock) => lock, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: "runtime.isolated_join".to_string(), - status: "blocked".to_string(), - summary: "无法取得 all-join 完成复核锁,不能收束当前任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - isolated_join_completion_blocker_at_locked(root, agent_id, run_id) -} - -pub(crate) fn static_delegate_completion_blocker_at( - root: &Path, - agent_id: &str, - run_id: &str, -) -> Option { - let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.delegate_receipts.complete", - ) { - Ok(lock) => lock, - Err(error) => { - return Some(AgentRuntimeToolObservation { - tool: "runtime.delegate_receipts".to_string(), - status: "blocked".to_string(), - summary: "无法取得专业 Agent 回执完成复核锁,不能收束当前任务".to_string(), - detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), - }); - } - }; - static_delegate_completion_blocker_at_locked(root, agent_id, run_id) -} - -pub(crate) fn project_verification_completion_blocker( - observations: &[AgentRuntimeToolObservation], -) -> Option { - let latest_mutation_index = observations - .iter() - .rposition(is_agent_runtime_project_mutation_observation); - let latest_verification_index = observations - .iter() - .rposition(is_agent_runtime_project_verification_observation); - - let Some(latest_mutation_index) = latest_mutation_index else { - let latest_verification = latest_verification_index - .and_then(|index| observations.get(index)) - .filter(|observation| observation.status != "ok")?; - let verification_label = agent_runtime_project_verification_label(latest_verification); - return Some(AgentRuntimeToolObservation { - tool: "runtime.verification".to_string(), - status: "blocked".to_string(), - summary: format!("最新 {verification_label} 未通过,不能把任务标记为完成"), - detail: Some(format!( - "请根据验证诊断继续处理并再次执行 {verification_label}:{}", - latest_verification.summary - )), - }); - }; - - let Some(latest_verification_index) = latest_verification_index else { - let mutation = &observations[latest_mutation_index]; - return Some(AgentRuntimeToolObservation { - tool: "runtime.verification".to_string(), - status: "blocked".to_string(), - summary: "项目在最近一次修改后尚未验证,不能把任务标记为完成".to_string(), - detail: Some(format!( - "最后一次成功修改是 {};请执行并通过 project.verify、可验证 command.exec 或 game.static_smoke。", - mutation.tool - )), - }); - }; - - if latest_verification_index < latest_mutation_index { - let mutation = &observations[latest_mutation_index]; - return Some(AgentRuntimeToolObservation { - tool: "runtime.verification".to_string(), - status: "blocked".to_string(), - summary: "项目在最近一次修改后尚未验证,不能把任务标记为完成".to_string(), - detail: Some(format!( - "最后一次成功修改是 {},它发生在旧验证之后;请重新执行并通过 project.verify、可验证 command.exec 或 game.static_smoke。", - mutation.tool - )), - }); - } - - let latest_verification = &observations[latest_verification_index]; - if latest_verification.status != "ok" { - let verification_label = agent_runtime_project_verification_label(latest_verification); - return Some(AgentRuntimeToolObservation { - tool: "runtime.verification".to_string(), - status: "blocked".to_string(), - summary: format!("最新 {verification_label} 未通过,不能把任务标记为完成"), - detail: Some(format!( - "请根据验证诊断继续修复,并在最后一次修改后再次执行 {verification_label}:{}", - latest_verification.summary - )), - }); - } - - None -} - -pub(super) fn project_verification_completion_blocker_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, - observations: &[AgentRuntimeToolObservation], -) -> Option { - match evaluate_project_verification_completion_at_locked(root, agent_id, run_id, observations) { - Ok(blocker) => blocker, - Err(error) => Some(agent_runtime_verification_blocker( - "无法读取项目 revision 或当前 run 的 verification gate,不能把任务标记为完成", - error, - )), - } -} - -pub(super) fn evaluate_project_verification_completion_at_locked( - root: &Path, - agent_id: &str, - run_id: &str, - observations: &[AgentRuntimeToolObservation], -) -> Result, String> { - if let Some(blocker) = project_verification_completion_blocker(observations) { - return Ok(Some(blocker)); - } - let revision = read_game_creator_agent_runtime_project_revision(root)?; - let gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - if let Some(status) = gate.last_verification_status.as_deref() { - if status == AGENT_RUNTIME_VERIFICATION_STATUS_RUNNING { - return Ok(Some(agent_runtime_verification_blocker( - "项目验证尚未形成可用结果,不能把任务标记为完成", - "请等待当前验证结束,或重新执行并通过 project.verify / 可验证 command.exec / game.static_smoke。", - ))); - } - if status == AGENT_RUNTIME_VERIFICATION_STATUS_FAILED { - return Ok(Some(agent_runtime_verification_blocker( - "最近一次项目验证未通过,不能把任务标记为完成", - "请根据验证诊断继续修复,并重新执行 project.verify / 可验证 command.exec / game.static_smoke。", - ))); - } - } - if !gate.requires_verification { - return Ok(None); - } - if gate.last_verification_status.as_deref() != Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) - || gate.verified_revision != Some(revision.revision) - { - let verified_revision = gate - .verified_revision - .map(|value| value.to_string()) - .unwrap_or_else(|| "none".to_string()); - return Ok(Some(agent_runtime_verification_blocker( - "项目在当前 revision 上尚未通过验证,不能把任务标记为完成", - format!( - "currentRevision={}, mutationRevision={}, verifiedRevision={};请重新执行并通过 project.verify、可验证 command.exec 或 game.static_smoke。", - revision.revision, - gate.mutation_revision - .map(|value| value.to_string()) - .unwrap_or_else(|| "none".to_string()), - verified_revision - ), - ))); - } - Ok(None) -} - -pub(crate) fn project_verification_completion_blocker_at( - root: &Path, - agent_id: &str, - run_id: &str, - observations: &[AgentRuntimeToolObservation], -) -> Option { - let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.verification.complete", - ) { - Ok(lock) => lock, - Err(error) => { - return Some(agent_runtime_verification_blocker( - "无法取得项目完成复核锁,不能把任务标记为完成", - error, - )); - } - }; - project_verification_completion_blocker_at_locked(root, agent_id, run_id, observations) -} - -pub(crate) fn acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root: &Path, - command_id: &str, -) -> Result { - const MAX_ATTEMPTS: usize = 200; - for attempt in 0..MAX_ATTEMPTS { - match acquire_project_write_lock(root, command_id) { - Err(error) - if error.starts_with("项目正在被其他写操作占用:") - && attempt + 1 < MAX_ATTEMPTS => - { - std::thread::sleep(Duration::from_millis(5)); - } - result => return result, - } - } - unreachable!("project write lock retry loop always returns") -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum AgentRuntimeToolPolicyBlock { - Denied(String), - RequiresConfirmation(String), -} - -pub(super) fn strictest_agent_runtime_tool_policy_block( - left: Option, - right: Option, -) -> Option { - match (left, right) { - (Some(AgentRuntimeToolPolicyBlock::Denied(reason)), _) - | (_, Some(AgentRuntimeToolPolicyBlock::Denied(reason))) => { - Some(AgentRuntimeToolPolicyBlock::Denied(reason)) - } - (Some(blocked), _) => Some(blocked), - (_, Some(blocked)) => Some(blocked), - (None, None) => None, - } -} - -pub(super) fn agent_runtime_tool_policy_block_observation( - tool: &str, - blocked: AgentRuntimeToolPolicyBlock, -) -> AgentRuntimeToolObservation { - let (status, summary) = match blocked { - AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary), - AgentRuntimeToolPolicyBlock::RequiresConfirmation(summary) => { - ("waiting-for-confirmation", summary) - } - }; - AgentRuntimeToolObservation { - tool: tool.to_string(), - status: status.to_string(), - summary, - detail: None, - } -} - -pub(crate) fn append_agent_runtime_tool_call_record( - root: &Path, - runtime: &mut AgentRuntimeState, - task: &str, - action: &AgentRuntimeToolAction, - observation: &AgentRuntimeToolObservation, - action_id: Option<&str>, -) { - let record = AgentRuntimeToolCallRecord { - action_id: action_id.map(ToString::to_string), - tool: observation.tool.clone(), - status: observation.status.clone(), - action_fingerprint: Some(agent_runtime_tool_action_fingerprint(action, task)), - input_summary: agent_runtime_tool_action_input_summary(root, action), - reason: action - .reason - .as_deref() - .map(|value| sanitize_agent_runtime_text(value, 240)) - .filter(|value| !value.trim().is_empty()), - summary: sanitize_agent_runtime_text(&observation.summary, 240), - detail: agent_runtime_local_observation_detail(observation) - .map(|value| sanitize_agent_runtime_text(value, 500)) - .filter(|value| !value.trim().is_empty()), - updated_at: unix_timestamp(), - }; - if let Some(index) = action_id.and_then(|action_id| { - runtime - .recent_tool_calls - .iter() - .position(|existing| existing.action_id.as_deref() == Some(action_id)) - }) { - runtime.recent_tool_calls[index] = record; - return; - } - runtime.recent_tool_calls.push(record); - if runtime.recent_tool_calls.len() > AGENT_RUNTIME_RECENT_TOOL_CALL_LIMIT { - let keep_from = runtime - .recent_tool_calls - .len() - .saturating_sub(AGENT_RUNTIME_RECENT_TOOL_CALL_LIMIT); - runtime.recent_tool_calls = runtime.recent_tool_calls.split_off(keep_from); - } -} - -pub(crate) fn append_agent_runtime_action_receipt( - root: &Path, - runtime: &AgentRuntimeState, - action_id: &str, - action_fingerprint: &str, - tool: &str, - execution_mode: &str, - input_summary: Option<&str>, - observation: &AgentRuntimeToolObservation, -) -> Result<(), String> { - let agent_id = - agent_runtime_action_receipt_identity_text(root, &runtime.agent_id, 96, "agentId")?; - let task_id = agent_runtime_action_receipt_identity_text(root, &runtime.task_id, 96, "taskId")?; - let session_id = - agent_runtime_action_receipt_identity_text(root, &runtime.session_id, 160, "sessionId")?; - let run_id = agent_runtime_action_receipt_identity_text(root, &runtime.run_id, 160, "runId")?; - let tool = agent_runtime_action_receipt_identity_text(root, tool, 80, "tool")?; - let observation_tool = - agent_runtime_action_receipt_identity_text(root, &observation.tool, 80, "tool")?; - if tool != observation_tool { - return Err("Agent 持久动作回执的工具身份与 observation 不一致".to_string()); - } - if !is_valid_agent_runtime_action_id(action_id) { - return Err("Agent 持久动作回执的 actionId 无效".to_string()); - } - if !is_valid_agent_runtime_action_fingerprint(action_fingerprint) { - return Err("Agent 持久动作回执的 actionFingerprint 无效".to_string()); - } - if !is_valid_agent_runtime_action_execution_mode(execution_mode) { - return Err("Agent 持久动作回执的 executionMode 无效".to_string()); - } - if !is_terminal_agent_runtime_action_status(&observation.status) { - return Err("Agent 持久动作回执只能记录终态 observation".to_string()); - } - let safe_detail = agent_runtime_action_receipt_safe_detail(root, observation); - let detail_unavailable = observation.detail.is_some() && safe_detail.is_none(); - let input_summary = agent_runtime_public_action_input_summary(root, &tool, input_summary); - let summary = agent_runtime_action_receipt_safe_text( - root, - &observation.summary, - 320, - Some("工具动作已结束,敏感摘要已省略"), - ) - .unwrap_or_else(|| "工具动作已结束,敏感摘要已省略".to_string()); - let record = serde_json::json!({ - "recordType": AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE, - "agentId": agent_id, - "taskId": task_id, - "sessionId": session_id, - "runId": run_id, - "actionId": action_id, - "actionFingerprint": action_fingerprint, - "tool": tool, - "executionMode": execution_mode, - "status": observation.status, - "inputSummary": input_summary, - "summary": summary, - "safeDetail": safe_detail, - "detailUnavailable": detail_unavailable, - }); - append_agent_db_record_if_missing_for_action( - root, - AGENT_RUNTIME_ACTION_RECEIPT_RECORD_TYPE, - &runtime.agent_id, - &run_id, - action_id, - record, - )?; - Ok(()) -} - -pub(super) fn agent_runtime_action_receipt_safe_detail( - root: &Path, - observation: &AgentRuntimeToolObservation, -) -> Option { - if observation.tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL { - return game_creator_agent_user_input_public_observation_metadata( - observation.detail.as_deref().unwrap_or_default(), - ); - } - if observation.tool == "project.git_commit" { - let detail = agent_runtime_git_commit_safe_detail_value( - root, - observation.detail.as_deref().unwrap_or_default(), - )?; - return serde_json::to_string(&detail).ok(); - } - if observation.tool == "command.exec" { - let detail = agent_runtime_command_exec_safe_detail_value( - observation.detail.as_deref().unwrap_or_default(), - )?; - return serde_json::to_string(&detail).ok(); - } - if observation.tool == "command.output_read" { - let detail = agent_runtime_command_output_read_safe_detail_value( - root, - observation.detail.as_deref().unwrap_or_default(), - )?; - return serde_json::to_string(&detail).ok(); - } - if matches!( - observation.tool.as_str(), - "command.start" | "command.poll" | "command.terminate" - ) { - let detail = agent_runtime_process_session_safe_detail_value( - observation.detail.as_deref().unwrap_or_default(), - )?; - return serde_json::to_string(&detail).ok(); - } - if observation.tool == "command.stdin" { - let detail = agent_runtime_process_stdin_safe_detail_value( - observation.detail.as_deref().unwrap_or_default(), - )?; - return serde_json::to_string(&detail).ok(); - } - if observation.tool == "image.inspect" { - let detail = serde_json::from_str::( - observation.detail.as_deref().unwrap_or_default(), - ) - .ok()?; - let images = detail.get("images")?.as_array()?; - if images.is_empty() || images.len() > AGENT_RUNTIME_IMAGE_INSPECT_MAX_IMAGES { - return None; - } - let mut safe_images = Vec::with_capacity(images.len()); - let mut total_bytes = 0_u64; - for image in images { - let path = normalize_relative_path(image.get("path")?.as_str()?).ok()?; - let runtime_screenshot = path.starts_with(".agent/runtime/browser-validations/") - && matches!(path.rsplit('/').next(), Some("desktop.png" | "mobile.png")); - if !(path.starts_with("game/") || path.starts_with("assets/") || runtime_screenshot) { - return None; - } - let sha256 = image.get("sha256")?.as_str()?; - if sha256.len() != 64 - || !sha256 - .chars() - .all(|character| character.is_ascii_hexdigit()) - { - return None; - } - let bytes = image.get("bytes")?.as_u64()?; - if bytes == 0 || bytes > AGENT_RUNTIME_IMAGE_INSPECT_MAX_FILE_BYTES { - return None; - } - total_bytes = total_bytes.checked_add(bytes)?; - if total_bytes > AGENT_RUNTIME_IMAGE_INSPECT_MAX_TOTAL_BYTES { - return None; - } - safe_images.push(serde_json::json!({ - "path": path, - "sha256": sha256, - "bytes": bytes, - })); - } - let conclusion_chars = detail.get("conclusionChars")?.as_u64()?; - if conclusion_chars == 0 || conclusion_chars > 7_000 { - return None; - } - let response_id = detail - .get("responseId") - .and_then(serde_json::Value::as_str) - .and_then(|value| agent_runtime_action_receipt_safe_text(root, value, 160, None)); - let inspection_kind = detail - .get("inspectionKind") - .and_then(serde_json::Value::as_str) - .filter(|value| *value == AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND); - let validation_profile = detail - .get("validationProfile") - .and_then(serde_json::Value::as_str) - .filter(|value| *value == AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE); - let (passed, checks, issues) = if validation_profile.is_some() { - if inspection_kind != Some(AGENT_RUNTIME_UI_PROTOTYPE_INSPECTION_KIND) { - return None; - } - let passed = detail.get("passed")?.as_bool()?; - let assessment = AgentRuntimeUiPrototypeAssessment { - checks: serde_json::from_value(detail.get("checks")?.clone()).ok()?, - issues: serde_json::from_value(detail.get("issues")?.clone()).ok()?, - summary: detail.get("conclusion")?.as_str()?.to_string(), - } - .validate() - .ok()?; - if passed != assessment.passed() || passed != (observation.status == "ok") { - return None; - } - ( - Some(passed), - Some(serde_json::to_value(&assessment.checks).ok()?), - Some(serde_json::to_value(&assessment.issues).ok()?), - ) - } else { - if inspection_kind.is_some() - || !matches!(detail.get("passed"), None | Some(serde_json::Value::Null)) - || !matches!(detail.get("checks"), None | Some(serde_json::Value::Null)) - || !matches!(detail.get("issues"), None | Some(serde_json::Value::Null)) - { - return None; - } - (None, None, None) - }; - return serde_json::to_string(&serde_json::json!({ - "images": safe_images, - "responseId": response_id, - "conclusionChars": conclusion_chars, - "inspectionKind": inspection_kind, - "validationProfile": validation_profile, - "passed": passed, - "checks": checks, - "issues": issues, - })) - .ok(); - } - if observation.tool == GAME_CREATOR_MCP_CALL_TOOL { - return game_creator_mcp_public_result_metadata( - observation.detail.as_deref().unwrap_or_default(), - ) - .and_then(|value| serde_json::to_string(&value).ok()); - } - if observation.tool != "project.patchset" { - return None; - } - let allowed_keys = [ - "checkpointId", - "revision", - "changeCount", - "revisionAdvanced", - ]; - let fields = observation - .detail - .as_deref() - .unwrap_or_default() - .split('·') - .filter_map(|field| { - let (key, value) = field.trim().split_once('=')?; - let key = key.trim(); - let value = value.trim(); - if !allowed_keys.contains(&key) || value.is_empty() || value.contains(['\n', '\r']) { - return None; - } - let value = agent_runtime_action_receipt_safe_text(root, value, 160, None)?; - Some(format!("{key}={value}")) - }) - .collect::>(); - if fields.is_empty() { - None - } else { - Some(sanitize_agent_runtime_text( - &fields.join(" · "), - AGENT_RUNTIME_ACTION_RECEIPT_SAFE_DETAIL_MAX_CHARS, - )) - } -} - -pub(crate) fn agent_runtime_git_commit_safe_detail_value( - root: &Path, - detail: &str, -) -> Option { - let value = serde_json::from_str::(detail).ok()?; - let valid_object_id = |value: &str| { - matches!(value.len(), 40 | 64) && value.bytes().all(|byte| byte.is_ascii_hexdigit()) - }; - let parent_head = value.get("parentHead")?.as_str()?; - let commit_head = value.get("commitHead")?.as_str()?; - let message_sha256 = value.get("messageSha256")?.as_str()?; - if !valid_object_id(parent_head) - || !valid_object_id(commit_head) - || message_sha256.len() != 64 - || !message_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) - { - return None; - } - let branch = agent_runtime_action_receipt_identity_text( - root, - value.get("branch")?.as_str()?, - 255, - "branch", - ) - .ok()?; - let paths = value.get("paths")?.as_array()?; - if paths.is_empty() || paths.len() > 12 { - return None; - } - let mut safe_paths = Vec::with_capacity(paths.len()); - for path in paths { - let path = normalize_relative_path(path.as_str()?).ok()?; - if should_skip_project_snapshot_path(&path) { - return None; - } - safe_paths.push(path); - } - let path_count = value.get("pathCount")?.as_u64()?; - if path_count != safe_paths.len() as u64 { - return None; - } - Some(serde_json::json!({ - "parentHead": parent_head, - "commitHead": commit_head, - "branch": branch, - "pathCount": path_count, - "paths": safe_paths, - "messageSha256": message_sha256, - "remainingChangedCount": value.get("remainingChangedCount")?.as_u64()?, - })) -} - -pub(super) fn is_valid_agent_runtime_process_id(process_id: &str) -> bool { - process_id.len() == 37 - && process_id.starts_with("proc-") - && process_id[5..].bytes().all(|byte| byte.is_ascii_hexdigit()) -} - -pub(super) fn is_valid_agent_runtime_process_cursor(process_id: &str, cursor: &str) -> bool { - let mut parts = cursor.split(':'); - parts.next() == Some("v1") - && parts.next() == Some(process_id) - && parts - .next() - .is_some_and(|offset| offset.parse::().is_ok()) - && parts.next().is_none() -} - -pub(super) fn agent_runtime_process_session_safe_detail_value( - detail: &str, -) -> Option { - let value = serde_json::from_str::(detail).ok()?; - let process_id = value.get("processId")?.as_str()?; - let status = value.get("status")?.as_str()?; - let cursor = value.get("cursor")?.as_str()?; - let next_cursor = value.get("nextCursor")?.as_str()?; - let output_sha256 = value.get("outputSha256")?.as_str()?; - if !is_valid_agent_runtime_process_id(process_id) - || !matches!( - status, - "prepared" - | "launching" - | "running" - | "terminating" - | "exited" - | "terminated" - | "timed-out" - | "output-limit-exceeded" - | "needs-reconciliation" - | "failed" - ) - || !is_valid_agent_runtime_process_cursor(process_id, cursor) - || !is_valid_agent_runtime_process_cursor(process_id, next_cursor) - || output_sha256.len() != 64 - || !output_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) - { - return None; - } - let exit_code = match value.get("exitCode")? { - serde_json::Value::Null => serde_json::Value::Null, - value => serde_json::Value::Number(value.as_i64()?.into()), - }; - let signal = match value.get("signal")? { - serde_json::Value::Null => serde_json::Value::Null, - serde_json::Value::String(signal) - if signal.chars().count() <= 80 && !signal.chars().any(char::is_control) => - { - serde_json::Value::String(signal.clone()) - } - _ => return None, - }; - let source_changed = match value.get("sourceChanged")? { - serde_json::Value::Null => serde_json::Value::Null, - serde_json::Value::Bool(changed) => serde_json::Value::Bool(*changed), - _ => return None, - }; - Some(serde_json::json!({ - "processId": process_id, - "status": status, - "cursor": cursor, - "nextCursor": next_cursor, - "hasMore": value.get("hasMore")?.as_bool()?, - "stdinOpen": value.get("stdinOpen")?.as_bool()?, - "exitCode": exit_code, - "signal": signal, - "outputBytes": value.get("outputBytes")?.as_u64()?, - "outputSha256": output_sha256, - "sourceChanged": source_changed, - "needsReconciliation": value.get("needsReconciliation")?.as_bool()?, - })) -} - -pub(super) fn agent_runtime_process_stdin_safe_detail_value( - detail: &str, -) -> Option { - let value = serde_json::from_str::(detail).ok()?; - let process_id = value.get("processId")?.as_str()?; - let content_sha256 = value.get("contentSha256")?.as_str()?; - if !is_valid_agent_runtime_process_id(process_id) - || content_sha256.len() != 64 - || !content_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) - { - return None; - } - Some(serde_json::json!({ - "processId": process_id, - "bytesWritten": value.get("bytesWritten")?.as_u64()?, - "contentSha256": content_sha256, - "stdinOpen": value.get("stdinOpen")?.as_bool()?, - "eof": value.get("eof")?.as_bool()?, - })) -} - -pub(super) fn agent_runtime_command_exec_safe_detail_value( - detail: &str, -) -> Option { - let value = serde_json::from_str::(detail) - .ok() - .filter(serde_json::Value::is_object) - .unwrap_or_else(|| { - let mut fields = serde_json::Map::new(); - for field in detail.split(" · ") { - let Some((key, value)) = field.trim().split_once('=') else { - continue; - }; - if !matches!( - key, - "verificationEligible" - | "outputRef" - | "outputSha256" - | "totalLines" - | "captureTruncated" - | "exitCode" - | "timedOut" - | "sourceChanged" - ) || fields.contains_key(key) - { - continue; - } - fields.insert( - key.to_string(), - serde_json::Value::String(value.to_string()), - ); - } - serde_json::Value::Object(fields) - }); - let text = |key: &str| { - value - .get(key) - .and_then(serde_json::Value::as_str) - .map(str::trim) - }; - let boolean = |key: &str| { - value.get(key).and_then(|value| { - value - .as_bool() - .or_else(|| value.as_str().and_then(|value| value.parse::().ok())) - }) - }; - let unsigned = |key: &str| { - value.get(key).and_then(|value| { - value - .as_u64() - .or_else(|| value.as_str().and_then(|value| value.parse::().ok())) - }) - }; - let output_ref = normalize_relative_path(text("outputRef")?).ok()?; - if !output_ref.starts_with(".agent/runtime/command-outputs/") || !output_ref.ends_with(".json") - { - return None; - } - let output_sha256 = text("outputSha256")?; - if output_sha256.len() != 64 || !output_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) { - return None; - } - let exit_code = match value.get("exitCode")? { - serde_json::Value::Null => None, - serde_json::Value::Number(number) => Some(number.as_i64()?), - serde_json::Value::String(value) if value == "none" => None, - serde_json::Value::String(value) => Some(value.parse::().ok()?), - _ => return None, - }; - Some(serde_json::json!({ - "verificationEligible": boolean("verificationEligible")?, - "outputRef": output_ref, - "outputSha256": output_sha256, - "totalLines": unsigned("totalLines")?, - "captureTruncated": boolean("captureTruncated")?, - "exitCode": exit_code, - "timedOut": boolean("timedOut")?, - "sourceChanged": boolean("sourceChanged")?, - })) -} - -pub(super) fn agent_runtime_command_output_read_safe_detail_value( - root: &Path, - detail: &str, -) -> Option { - let value = serde_json::from_str::(detail).ok()?; - let source_action_id = value.get("sourceActionId")?.as_str()?; - let source_run_id = value.get("sourceRunId")?.as_str()?; - let source_fingerprint = value.get("sourceActionFingerprint")?.as_str()?; - if !is_valid_agent_runtime_action_id(source_action_id) - || !is_valid_agent_runtime_action_fingerprint(source_fingerprint) - || agent_runtime_action_receipt_identity_text(root, source_run_id, 160, "runId").is_err() - { - return None; - } - let output_ref = normalize_relative_path(value.get("outputRef")?.as_str()?).ok()?; - if !output_ref.starts_with(".agent/runtime/command-outputs/") || !output_ref.ends_with(".json") - { - return None; - } - let output_sha256 = value.get("outputSha256")?.as_str()?; - if output_sha256.len() != 64 || !output_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()) { - return None; - } - let start_line = value.get("startLine")?.as_u64()?; - let total_lines = value.get("totalLines")?.as_u64()?; - let next_line = match value.get("nextLine")? { - serde_json::Value::Null => None, - value => Some(value.as_u64()?), - }; - if start_line == 0 || start_line > total_lines.max(1) { - return None; - } - let exit_code = match value.get("exitCode")? { - serde_json::Value::Null => None, - value => Some(value.as_i64()?), - }; - Some(serde_json::json!({ - "sourceActionId": source_action_id, - "sourceRunId": source_run_id, - "sourceActionFingerprint": source_fingerprint, - "outputRef": output_ref, - "outputSha256": output_sha256, - "startLine": start_line, - "nextLine": next_line, - "totalLines": total_lines, - "hasMore": value.get("hasMore")?.as_bool()?, - "captureTruncated": value.get("captureTruncated")?.as_bool()?, - "exitCode": exit_code, - "timedOut": value.get("timedOut")?.as_bool()?, - "sourceChanged": value.get("sourceChanged")?.as_bool()?, - })) -} - -pub(super) fn agent_runtime_action_receipt_safe_text( - root: &Path, - value: &str, - max_chars: usize, - fallback: Option<&str>, -) -> Option { - let sanitized = - redact_absolute_path_tokens(&redact_agent_runtime_project_paths(root, value, max_chars)); - if sanitized.trim().is_empty() { - return fallback.map(ToString::to_string); - } - let serialized = serde_json::to_string(&sanitized).unwrap_or_default(); - if validate_agent_runtime_pending_serialized_content(root, &serialized).is_err() { - return fallback.map(ToString::to_string); - } - Some(sanitized) -} - -pub(super) fn agent_runtime_public_action_input_summary( - root: &Path, - tool: &str, - input_summary: Option<&str>, -) -> Option { - let input_summary = input_summary?.trim(); - if input_summary.is_empty() { - return None; - } - let public_shape_only = matches!( - tool, - "memory.read" - | "project.restore" - | "project.diff" - | "git.inspect" - | "project.patchset" - | "project.search" - | "project.verify" - | "file.list" - | "file.read" - | "file.write" - | "file.patch" - | "file.delete" - | "task.update" - | "command.exec" - | "command.start" - | "command.poll" - | "command.stdin" - | "command.terminate" - | "command.output_read" - | "command.run_limited" - | "preview.validate" - | "image.inspect" - | "canvas.asset_generate" - | "agent.message" - | "agent.delegate" - | "agent.schedule_ready" - | "agent.action_history" - | "agent.run_status" - | GAME_CREATOR_MCP_CALL_TOOL - ); - if public_shape_only { - return agent_runtime_action_receipt_safe_text(root, input_summary, 320, None); - } - Some(format!( - "inputSummarySha256={:x} · inputSummaryChars={}", - Sha256::digest(input_summary.as_bytes()), - input_summary.chars().count() - )) -} - -pub(super) fn agent_runtime_action_receipt_identity_text( - root: &Path, - value: &str, - max_chars: usize, - field: &str, -) -> Result { - let value = value.trim(); - if value.is_empty() || value.chars().count() > max_chars || value.chars().any(char::is_control) - { - return Err(format!("Agent 持久动作回执的 {field} 无效")); - } - let Some(safe) = agent_runtime_action_receipt_safe_text(root, value, max_chars, None) else { - return Err(format!("Agent 持久动作回执的 {field} 包含敏感内容")); - }; - if safe != value { - return Err(format!( - "Agent 持久动作回执的 {field} 不能包含 file URI 或绝对路径" - )); - } - Ok(safe) -} - -pub(crate) fn agent_runtime_tool_action_fingerprint( - action: &AgentRuntimeToolAction, - task: &str, -) -> String { - let sandbox_policy = matches!( - action.tool.trim(), - "command.exec" - | "command.start" - | "command.poll" - | "command.stdin" - | "command.terminate" - | "project.verify" - ) - .then(|| { - let metadata = command_sandbox_platform_metadata(); - serde_json::json!({ - "requirement": "required", - "backend": metadata.backend, - "mode": metadata.mode, - "networkAccess": metadata.network, - "profileVersion": metadata.profile_version, - }) - }); - let payload = serde_json::json!({ - "tool": action.tool.trim(), - "input": &action.input, - "taskContext": task, - "sandboxPolicy": sandbox_policy, - }); - let encoded = serde_json::to_vec(&payload).unwrap_or_default(); - format!("{:x}", Sha256::digest(encoded)) -} - -pub(super) fn agent_runtime_pending_tool_action_fingerprint( - action: &AgentRuntimeToolAction, - task: &str, - planned_steer_cursor: u64, -) -> String { - let base = agent_runtime_tool_action_fingerprint(action, task); - if planned_steer_cursor == 0 { - return base; - } - let payload = serde_json::json!({ - "fingerprintVersion": AGENT_RUNTIME_ACTION_FINGERPRINT_VERSION, - "baseActionFingerprint": base, - "plannedSteerCursor": planned_steer_cursor, - }); - let encoded = serde_json::to_vec(&payload).unwrap_or_default(); - format!("{:x}", Sha256::digest(encoded)) -} - -pub(crate) fn agent_runtime_tool_action_id( - run_id: &str, - loop_iteration: u32, - action_index: u32, - occurrence_nonce: u64, - action_fingerprint: &str, -) -> String { - let encoded = format!( - "{run_id}\n{loop_iteration}\n{action_index}\n{occurrence_nonce}\n{action_fingerprint}" - ); - let occurrence_fingerprint = format!("{:x}", Sha256::digest(encoded.as_bytes())); - format!( - "action-{}", - occurrence_fingerprint.chars().take(24).collect::() - ) -} - -pub(crate) fn agent_runtime_tool_action_input_summary( - root: &Path, - action: &AgentRuntimeToolAction, -) -> Option { - let input = &action.input; - let tool = action.tool.trim(); - let text = |keys: &[&str]| agent_runtime_tool_input_text(input, keys); - let relative_path = |keys: &[&str]| { - let value = text(keys); - if Path::new(&value).is_absolute() { - "[absolute path rejected]".to_string() - } else { - value - } - }; - let chars = |keys: &[&str]| { - keys.iter() - .find_map(|key| input.get(*key).and_then(|value| value.as_str())) - .map(|value| value.chars().count()) - .unwrap_or(0) - }; - let list_len = |keys: &[&str]| { - keys.iter() - .find_map(|key| input.get(*key).and_then(|value| value.as_array())) - .map(Vec::len) - .unwrap_or(0) - }; - let summary = match tool { - GAME_CREATOR_USER_INPUT_REQUEST_TOOL => { - game_creator_agent_user_input_action_input_summary(input).unwrap_or_default() - } - "memory.read" => format!("scope={}", text(&["scope"])), - "memory.write" => format!( - "scope={} · mode={} · title={} · contentChars={}", - text(&["scope"]), - text(&["mode"]), - text(&["title"]), - chars(&["content"]) - ), - "project.restore" => format!( - "checkpointId={}", - text(&["checkpointId", "checkpoint_id", "id"]) - ), - "project.diff" => format!( - "checkpointId={} · includeContent={} · maxFiles={} · maxChars={}", - text(&["checkpointId", "checkpoint_id", "id"]), - input - .get("includeContent") - .or_else(|| input.get("include_content")) - .and_then(serde_json::Value::as_bool) - .unwrap_or(false), - input - .get("maxFiles") - .or_else(|| input.get("max_files")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(AGENT_RUNTIME_PROJECT_DIFF_CONTENT_DEFAULT_FILES as u64), - input - .get("maxChars") - .or_else(|| input.get("max_chars")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(AGENT_RUNTIME_PROJECT_DIFF_CONTENT_DEFAULT_CHARS as u64) - ), - "git.inspect" => format!( - "includeDiff={} · maxFiles={} · maxChars={}", - input - .get("includeDiff") - .or_else(|| input.get("include_diff")) - .and_then(serde_json::Value::as_bool) - .unwrap_or(true), - input - .get("maxFiles") - .or_else(|| input.get("max_files")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(AGENT_RUNTIME_GIT_INSPECT_DEFAULT_FILES as u64), - input - .get("maxChars") - .or_else(|| input.get("max_chars")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(AGENT_RUNTIME_GIT_INSPECT_DEFAULT_CHARS as u64) - ), - "project.git_commit" => { - let paths = input - .get("paths") - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default(); - let visible_paths = paths - .iter() - .take(6) - .filter_map(serde_json::Value::as_str) - .filter_map(|path| normalize_relative_path(path).ok()) - .collect::>() - .join(","); - let message = text(&["message"]); - let title = message.lines().next().unwrap_or_default(); - let title = sanitize_agent_runtime_text(title, 100); - let message_sha256 = format!("{:x}", Sha256::digest(message.as_bytes())); - let expected_head = text(&["expectedHead", "expected_head"]); - let expected_snapshot = text(&[ - "expectedSnapshotFingerprint", - "expected_snapshot_fingerprint", - ]); - format!( - "title={} · pathCount={} · paths={} · expectedHead={} · snapshot={} · messageSha256={}", - title, - paths.len(), - visible_paths, - expected_head.chars().take(12).collect::(), - expected_snapshot.chars().take(12).collect::(), - message_sha256, - ) - } - "project.patchset" => { - let changes = input - .get("changes") - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default(); - let paths = changes - .iter() - .take(6) - .filter_map(|change| { - let operation = change - .get("operation") - .and_then(serde_json::Value::as_str)?; - let path = change.get("path").and_then(serde_json::Value::as_str)?; - (!Path::new(path).is_absolute()).then(|| format!("{operation}:{path}")) - }) - .collect::>() - .join(","); - let encoded = serde_json::to_vec(&changes).unwrap_or_default(); - format!( - "changeCount={} · changesSha256={:x} · paths={}", - changes.len(), - Sha256::digest(&encoded), - paths - ) - } - "project.search" => format!( - "path={} · queryChars={} · maxResults={} · caseSensitive={}", - relative_path(&["path"]), - chars(&["query"]), - input - .get("maxResults") - .or_else(|| input.get("max_results")) - .and_then(|value| value.as_u64()) - .unwrap_or(AGENT_RUNTIME_PROJECT_SEARCH_DEFAULT_RESULTS as u64), - input - .get("caseSensitive") - .or_else(|| input.get("case_sensitive")) - .and_then(|value| value.as_bool()) - .unwrap_or(false) - ), - "project.verify" => { - let expected_command = text(&["expectedCommand", "expected_command"]); - let command_chars = expected_command.chars().count(); - format!( - "script={} · expectedCommandSha256={:x} · expectedCommandChars={} · timeoutSeconds={}", - text(&["script"]), - Sha256::digest(expected_command.as_bytes()), - command_chars, - input - .get("timeoutSeconds") - .or_else(|| input.get("timeout_seconds")) - .and_then(|value| value.as_u64()) - .unwrap_or(AGENT_RUNTIME_PROJECT_VERIFY_DEFAULT_TIMEOUT_SECONDS as u64) - ) - } - "file.list" => format!("path={}", relative_path(&["path"])), - "file.read" => format!( - "path={} · startLine={} · maxLines={}", - relative_path(&["path"]), - input - .get("startLine") - .or_else(|| input.get("start_line")) - .and_then(|value| value.as_u64()) - .unwrap_or(1), - input - .get("maxLines") - .or_else(|| input.get("max_lines")) - .and_then(|value| value.as_u64()) - .unwrap_or(AGENT_RUNTIME_FILE_READ_DEFAULT_LINES as u64) - ), - "file.write" => format!( - "path={} · contentChars={}", - relative_path(&["path"]), - chars(&["content"]) - ), - "file.patch" => format!( - "path={} · oldTextChars={} · newTextChars={} · expectedReplacements={}", - relative_path(&["path"]), - chars(&["oldText", "old_text"]), - chars(&["newText", "new_text"]), - input - .get("expectedReplacements") - .or_else(|| input.get("expected_replacements")) - .and_then(|value| value.as_u64()) - .unwrap_or(1) - ), - "file.delete" => format!("path={}", relative_path(&["path"])), - "task.create" => format!( - "taskId={} · title={} · group={} · role={} · dependencies={} · artifacts={} · criteria={}", - text(&["taskId", "task_id", "id"]), - text(&["title"]), - text(&["group"]), - text(&["role"]), - list_len(&["dependencies"]), - list_len(&["artifacts"]), - list_len(&["acceptanceCriteria", "acceptance_criteria"]) - ), - "task.update" => format!( - "taskId={} · status={}", - text(&["taskId", "task_id", "id"]), - text(&["status"]) - ), - "command.exec" | "command.start" => { - let arguments = input - .get("args") - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default(); - let argument_bytes = serde_json::to_vec(&arguments).unwrap_or_default(); - format!( - "program={} · argsCount={} · argsSha256={:x} · cwd={} · timeoutSeconds={}", - text(&["program"]), - arguments.len(), - Sha256::digest(&argument_bytes), - relative_path(&["cwd"]), - input - .get("timeoutSeconds") - .or_else(|| input.get("timeout_seconds")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(120) - ) - } - "command.poll" => format!( - "processId={} · cursor={} · maxChars={} · waitMs={}", - text(&["processId"]), - text(&["cursor"]), - input - .get("maxChars") - .and_then(serde_json::Value::as_u64) - .unwrap_or(8_000), - input - .get("waitMs") - .and_then(serde_json::Value::as_u64) - .unwrap_or(0) - ), - "command.stdin" => { - let append_newline = input - .get("appendNewline") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false); - let eof = input - .get("eof") - .and_then(serde_json::Value::as_bool) - .unwrap_or(false); - let mut bytes = input - .get("data") - .and_then(serde_json::Value::as_str) - .unwrap_or_default() - .as_bytes() - .to_vec(); - if append_newline { - bytes.push(b'\n'); - } - format!( - "processId={} · bytes={} · contentSha256={:x} · eof={} · appendNewline={}", - text(&["processId"]), - bytes.len(), - Sha256::digest(&bytes), - eof, - append_newline - ) - } - "command.terminate" => format!("processId={}", text(&["processId"])), - "command.output_read" => format!( - "sourceActionId={} · startLine={} · maxLines={}", - text(&["actionId", "action_id"]), - input - .get("startLine") - .or_else(|| input.get("start_line")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(1), - input - .get("maxLines") - .or_else(|| input.get("max_lines")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(160) - ), - "command.run_limited" => format!( - "commandId={}", - text(&["commandId", "command_id", "id"]) - ), - "preview.validate" => { - let viewports = input - .get("viewports") - .and_then(serde_json::Value::as_array) - .map(|items| { - items - .iter() - .filter_map(serde_json::Value::as_str) - .collect::>() - .join(",") - }) - .filter(|value| !value.is_empty()) - .unwrap_or_else(|| "desktop,mobile".to_string()); - let expected_text = input - .get("expectedText") - .or_else(|| input.get("expected_text")) - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default(); - let expected_text_json = - serde_json::to_vec(&expected_text).unwrap_or_else(|_| b"[]".to_vec()); - format!( - "viewports={} · expectedTextCount={} · expectedTextSha256={:x} · settleMs={} · failOnConsoleError={} · playtestScenario={}", - viewports, - expected_text.len(), - Sha256::digest(&expected_text_json), - input - .get("settleMs") - .or_else(|| input.get("settle_ms")) - .and_then(serde_json::Value::as_u64) - .unwrap_or(800), - input - .get("failOnConsoleError") - .or_else(|| input.get("fail_on_console_error")) - .and_then(serde_json::Value::as_bool) - .unwrap_or(true), - text(&["playtestScenario", "playtest_scenario"]), - ) - } - "image.inspect" => { - let paths = input - .get("paths") - .and_then(serde_json::Value::as_array) - .cloned() - .unwrap_or_default(); - let safe_paths = paths - .iter() - .filter_map(serde_json::Value::as_str) - .filter(|path| !Path::new(path).is_absolute()) - .take(AGENT_RUNTIME_IMAGE_INSPECT_MAX_IMAGES) - .collect::>(); - let encoded = serde_json::to_vec(&safe_paths).unwrap_or_default(); - format!( - "pathCount={} · pathsSha256={:x} · paths={} · questionChars={}", - paths.len(), - Sha256::digest(&encoded), - safe_paths.join(","), - chars(&["question"]) - ) - } - "canvas.asset_generate" => format!( - "promptChars={} · outputPath={} · aspectRatio={} · imageSize={} · assetKind={} · assetLabel={}", - chars(&["prompt"]), - text(&["outputPath", "output_path"]), - text(&["aspectRatio", "aspect_ratio"]), - text(&["imageSize", "image_size"]), - text(&["assetKind", "asset_kind"]), - text(&["assetLabel", "asset_label"]), - ), - "blackboard.write" => format!( - "title={} · contentChars={}", - text(&["title"]), - chars(&["content"]) - ), - "agent.message" => format!( - "agentId={} · contentChars={}", - text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]), - chars(&["content"]) - ), - "agent.delegate" => format!( - "agentId={} · runId={} · taskChars={} · criteria={} · expectedArtifacts={} · repairOf={}", - text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]), - text(&["runId", "run_id"]), - chars(&["task"]), - list_len(&["acceptanceCriteria", "acceptance_criteria"]), - list_len(&["expectedArtifacts", "expected_artifacts"]), - text(&["repairOfDelegationId", "repair_of_delegation_id"]) - ), - "agent.schedule_ready" => format!( - "limit={}", - input.get("limit").and_then(|value| value.as_u64()).unwrap_or(1) - ), - "agent.action_history" => format!( - "runId={} · actionId={} · tool={} · status={} · limit={}", - text(&["runId", "run_id"]), - text(&["actionId", "action_id"]), - text(&["tool"]), - text(&["status"]), - input - .get("limit") - .and_then(serde_json::Value::as_u64) - .unwrap_or(AGENT_RUNTIME_ACTION_HISTORY_DEFAULT_LIMIT as u64) - ), - "agent.run_status" => format!( - "scope={} · agentId={} · delegationId={}", - text(&["scope"]), - text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]), - text(&["delegationId", "delegation_id"]) - ), - GAME_CREATOR_MCP_CALL_TOOL => { - let arguments = input - .get("arguments") - .and_then(serde_json::Value::as_object) - .cloned() - .unwrap_or_default(); - let arguments_json = serde_json::to_string(&arguments).unwrap_or_default(); - let catalog_fingerprint = text(&["catalogFingerprint"]); - let tool_fingerprint = text(&["toolFingerprint"]); - format!( - "server={} · tool={} · argumentKeys={} · argumentsChars={} · argumentsSha256={:x} · catalog={} · toolFingerprint={}", - text(&["server"]), - text(&["tool"]), - arguments.len(), - arguments_json.chars().count(), - Sha256::digest(arguments_json.as_bytes()), - catalog_fingerprint.chars().take(12).collect::(), - tool_fingerprint.chars().take(12).collect::(), - ) - } - _ => String::new(), - }; - let summary = redact_agent_runtime_project_paths(root, &summary, 320); - (!summary.trim().is_empty()).then_some(summary) -} - -pub(super) fn agent_runtime_has_structured_plan(runtime: &AgentRuntimeState) -> bool { - runtime.plan_revision > 0 -} - -pub(super) fn validate_agent_runtime_structured_plan_snapshot( - plan_revision: u64, - plan_explanation: &str, - plan: &[String], - plan_steps: &[AgentRuntimePlanStep], - active_plan_step_index: Option, -) -> Result<(), String> { - if plan_revision == 0 { - if !plan_explanation.trim().is_empty() { - return Err("Agent 旧计划不能携带结构化计划说明".to_string()); - } - return Ok(()); - } - if plan_explanation.trim().is_empty() { - return Err("Agent 结构化计划缺少 explanation".to_string()); - } - if plan_steps.is_empty() || plan_steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT { - return Err(format!( - "Agent 结构化计划步骤数量必须在 1..={AGENT_RUNTIME_PLAN_STEP_LIMIT}" - )); - } - if plan.len() != plan_steps.len() { - return Err("Agent 结构化计划文本与步骤数量不匹配".to_string()); - } - - let mut seen_steps = std::collections::BTreeSet::new(); - let mut in_progress_index = None; - for (index, step) in plan_steps.iter().enumerate() { - if step.index != index as u32 - || step.title.trim().is_empty() - || plan.get(index) != Some(&step.title) - { - return Err("Agent 结构化计划步骤索引或标题不匹配".to_string()); - } - if !seen_steps.insert(step.title.as_str()) { - return Err("Agent 结构化计划包含重复步骤".to_string()); - } - if !matches!( - step.status.as_str(), - AGENT_RUNTIME_PLAN_STATUS_PENDING - | AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS - | AGENT_RUNTIME_PLAN_STATUS_COMPLETED - | AGENT_RUNTIME_PLAN_STATUS_FAILED - ) { - return Err("Agent 结构化计划持久状态无效".to_string()); - } - if step.status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS { - if in_progress_index.replace(step.index).is_some() { - return Err("Agent 结构化计划同时存在多个 in_progress 步骤".to_string()); - } - } - } - if active_plan_step_index != in_progress_index { - return Err("Agent 结构化计划 activePlanStepIndex 与 in_progress 步骤不匹配".to_string()); - } - Ok(()) -} - -pub(crate) fn sanitize_agent_runtime_plan_update( - update: &AgentRuntimePlanUpdate, -) -> Result { - let explanation = sanitize_agent_runtime_text(update.explanation.trim(), 240); - if explanation.is_empty() { - return Err("Agent 结构化计划更新的 explanation 不能为空".to_string()); - } - if update.steps.is_empty() { - return Err("Agent 结构化计划更新必须包含至少一个步骤".to_string()); - } - if update.steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT { - return Err(format!( - "Agent 结构化计划更新最多包含 {AGENT_RUNTIME_PLAN_STEP_LIMIT} 个步骤" - )); - } - - let mut seen_steps = std::collections::BTreeSet::new(); - let mut in_progress_count = 0usize; - let mut steps = Vec::with_capacity(update.steps.len()); - for raw_step in &update.steps { - let step = sanitize_agent_runtime_text(raw_step.step.trim(), 180); - if step.is_empty() { - return Err("Agent 结构化计划更新不能包含空步骤".to_string()); - } - if !seen_steps.insert(step.clone()) { - return Err("Agent 结构化计划更新包含重复步骤".to_string()); - } - let status = raw_step.status.trim(); - if !matches!( - status, - AGENT_RUNTIME_PLAN_STATUS_PENDING - | AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS - | AGENT_RUNTIME_PLAN_STATUS_COMPLETED - ) { - return Err( - "Agent 结构化计划步骤状态无效,只允许 pending、in_progress、completed".to_string(), - ); - } - if status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS { - in_progress_count += 1; - if in_progress_count > 1 { - return Err("Agent 结构化计划同时最多只能有一个 in_progress 步骤".to_string()); - } - } - steps.push(AgentRuntimePlanUpdateStep { - step, - status: status.to_string(), - }); - } - - Ok(AgentRuntimePlanUpdate { explanation, steps }) -} - -pub(crate) fn apply_agent_runtime_plan_update( - runtime: &mut AgentRuntimeState, - update: &AgentRuntimePlanUpdate, -) -> Result { - let update = sanitize_agent_runtime_plan_update(update)?; - let mut terminal_steps = std::collections::BTreeMap::new(); - if agent_runtime_has_structured_plan(runtime) { - for step in &runtime.plan_steps { - if matches!( - step.status.as_str(), - AGENT_RUNTIME_PLAN_STATUS_COMPLETED | AGENT_RUNTIME_PLAN_STATUS_FAILED - ) { - terminal_steps.insert(step.title.clone(), step.clone()); - } - } - } - - for step in &update.steps { - let Some(existing) = terminal_steps.get(&step.step) else { - continue; - }; - if existing.status == AGENT_RUNTIME_PLAN_STATUS_COMPLETED - && step.status != AGENT_RUNTIME_PLAN_STATUS_COMPLETED - { - return Err("Agent 结构化计划不能让已完成步骤回退".to_string()); - } - if existing.status == AGENT_RUNTIME_PLAN_STATUS_FAILED { - return Err("Agent 结构化计划不能改写已失败步骤".to_string()); - } - } - - let incoming_titles = update - .steps - .iter() - .map(|step| step.step.as_str()) - .collect::>(); - let mut merged = runtime - .plan_steps - .iter() - .filter(|step| { - matches!( - step.status.as_str(), - AGENT_RUNTIME_PLAN_STATUS_COMPLETED | AGENT_RUNTIME_PLAN_STATUS_FAILED - ) && !incoming_titles.contains(step.title.as_str()) - }) - .map(|step| (step.title.clone(), step.status.clone())) - .collect::>(); - merged.extend(update.steps.iter().map(|step| { - let status = terminal_steps - .get(&step.step) - .map(|existing| existing.status.clone()) - .unwrap_or_else(|| step.status.clone()); - (step.step.clone(), status) - })); - if merged.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT { - return Err(format!( - "Agent 结构化计划保留终态步骤后超过 {AGENT_RUNTIME_PLAN_STEP_LIMIT} 步上限" - )); - } - - let unchanged = agent_runtime_has_structured_plan(runtime) - && runtime.plan_explanation == update.explanation - && runtime.plan_steps.len() == merged.len() - && runtime - .plan_steps - .iter() - .zip(merged.iter()) - .all(|(existing, (title, status))| { - existing.title == *title && existing.status == *status - }); - if unchanged { - return Ok(false); - } - - let now = unix_timestamp(); - let previous_steps = runtime - .plan_steps - .iter() - .map(|step| (step.title.clone(), step.clone())) - .collect::>(); - runtime.plan_steps = merged - .into_iter() - .enumerate() - .map(|(index, (title, status))| { - let previous = previous_steps.get(&title); - let unchanged_status = previous.is_some_and(|step| step.status == status); - AgentRuntimePlanStep { - index: index as u32, - title, - status, - detail: previous.and_then(|step| step.detail.clone()), - updated_at: if unchanged_status { - previous.map(|step| step.updated_at).unwrap_or(now) - } else { - now - }, - } - }) - .collect(); - runtime.plan = runtime - .plan_steps - .iter() - .map(|step| step.title.clone()) - .collect(); - runtime.active_plan_step_index = runtime - .plan_steps - .iter() - .find(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_IN_PROGRESS) - .map(|step| step.index); - runtime.plan_explanation = update.explanation; - runtime.plan_revision = runtime.plan_revision.saturating_add(1).max(1); - Ok(true) -} - -pub(super) fn update_agent_runtime_plan_steps(runtime: &mut AgentRuntimeState, plan: Vec) { - if agent_runtime_has_structured_plan(runtime) { - return; - } - runtime.plan = plan - .into_iter() - .filter(|item| !item.trim().is_empty()) - .take(AGENT_RUNTIME_PLAN_STEP_LIMIT) - .map(|item| sanitize_agent_runtime_text(&item, 180)) - .collect(); - runtime.plan_steps = runtime - .plan - .iter() - .enumerate() - .map(|(index, title)| AgentRuntimePlanStep { - index: index as u32, - title: title.clone(), - status: if index == 0 { "active" } else { "pending" }.to_string(), - detail: None, - updated_at: unix_timestamp(), - }) - .collect(); - runtime.active_plan_step_index = if runtime.plan_steps.is_empty() { - None - } else { - Some(0) - }; -} - -pub(crate) fn activate_agent_runtime_plan_step( - runtime: &mut AgentRuntimeState, - step_index: usize, - detail: &str, -) { - if agent_runtime_has_structured_plan(runtime) || runtime.plan_steps.is_empty() { - return; - } - let target_index = step_index.min(runtime.plan_steps.len().saturating_sub(1)); - let now = unix_timestamp(); - for step in runtime.plan_steps.iter_mut() { - if step.index as usize == target_index { - step.status = "active".to_string(); - step.detail = Some(sanitize_agent_runtime_text(detail, 180)); - step.updated_at = now; - } else if step.status == "active" { - step.status = "pending".to_string(); - step.updated_at = now; - } - } - runtime.active_plan_step_index = Some(target_index as u32); -} - -pub(crate) fn complete_agent_runtime_active_plan_step( - runtime: &mut AgentRuntimeState, - status: &str, - detail: &str, -) { - if agent_runtime_has_structured_plan(runtime) { - return; - } - let Some(active_index) = runtime.active_plan_step_index else { - return; - }; - let status = match status { - "failed" => "failed", - "waiting-for-confirmation" => "waiting-for-confirmation", - _ => "completed", - }; - let now = unix_timestamp(); - for step in runtime.plan_steps.iter_mut() { - if step.index == active_index { - step.status = status.to_string(); - step.detail = Some(sanitize_agent_runtime_text(detail, 220)); - step.updated_at = now; - break; - } - } - runtime.active_plan_step_index = None; -} - -pub(crate) fn retry_agent_runtime_active_plan_step(runtime: &mut AgentRuntimeState, detail: &str) { - if agent_runtime_has_structured_plan(runtime) { - return; - } - let Some(active_index) = runtime.active_plan_step_index else { - return; - }; - let now = unix_timestamp(); - for step in runtime.plan_steps.iter_mut() { - if step.index == active_index { - step.status = "pending".to_string(); - step.detail = Some(sanitize_agent_runtime_text(detail, 220)); - step.updated_at = now; - break; - } - } - runtime.active_plan_step_index = None; -} - -pub(crate) fn activate_agent_runtime_response_plan_step( - runtime: &mut AgentRuntimeState, - detail: &str, -) { - if agent_runtime_has_structured_plan(runtime) { - return; - } - let target_index = runtime - .plan_steps - .iter() - .find(|step| step.status == "pending" || step.status == "active") - .map(|step| step.index as usize); - if let Some(target_index) = target_index { - activate_agent_runtime_plan_step(runtime, target_index, detail); - return; - } - - if runtime.plan_steps.len() >= AGENT_RUNTIME_PLAN_STEP_LIMIT { - return; - } - - let index = runtime.plan_steps.len() as u32; - let title = "生成最终回复".to_string(); - runtime.plan.push(title.clone()); - runtime.plan_steps.push(AgentRuntimePlanStep { - index, - title, - status: "active".to_string(), - detail: Some(sanitize_agent_runtime_text(detail, 180)), - updated_at: unix_timestamp(), - }); - runtime.active_plan_step_index = Some(index); -} - -pub(super) fn fail_agent_runtime_remaining_plan_steps( - runtime: &mut AgentRuntimeState, - detail: &str, -) { - if agent_runtime_has_structured_plan(runtime) { - return; - } - if !agent_runtime_has_structured_plan(runtime) && runtime.active_plan_step_index.is_some() { - complete_agent_runtime_active_plan_step(runtime, "failed", detail); - return; - } - - let now = unix_timestamp(); - let detail = sanitize_agent_runtime_text(detail, 220); - let mut marked_failed = false; - for step in runtime.plan_steps.iter_mut() { - if matches!( - step.status.as_str(), - "pending" | "active" | "in_progress" | "waiting-for-confirmation" - ) { - step.status = "failed".to_string(); - step.detail = Some(detail.clone()); - step.updated_at = now; - marked_failed = true; - } - } - if !marked_failed { - if let Some(step) = runtime - .plan_steps - .iter_mut() - .rev() - .find(|step| step.status != "failed") - { - step.status = "failed".to_string(); - step.detail = Some(detail); - step.updated_at = now; - } - } - runtime.active_plan_step_index = None; -} - -pub(crate) fn complete_agent_runtime_remaining_plan_steps( - runtime: &mut AgentRuntimeState, - detail: &str, -) { - if agent_runtime_has_structured_plan(runtime) { - return; - } - let now = unix_timestamp(); - for step in runtime.plan_steps.iter_mut() { - if step.status != "failed" { - step.status = "completed".to_string(); - if step - .detail - .as_deref() - .map_or(true, |value| value.trim().is_empty()) - { - step.detail = Some(sanitize_agent_runtime_text(detail, 180)); - } - step.updated_at = now; - } - } - runtime.active_plan_step_index = None; -} - -pub(super) async fn compact_game_creator_agent_runtime_context_at( - root: &Path, - agent_id: &str, - session_id: &str, - run_id: &str, - observations: &[AgentRuntimeToolObservation], - trigger: &str, - estimated_tokens_before: u64, - applied_steer_cursor: u64, - allow_idle_context_compaction: bool, - persist_transient_retry: bool, - request_kind: &str, -) -> Result { - if !matches!( - request_kind, - "context-compaction" | "final-reply-context-compaction" - ) || (allow_idle_context_compaction && request_kind != "context-compaction") - { - return Err("上下文压缩 Provider requestKind 无效".to_string()); - } - let (snapshot, source, llm, config_path, request) = { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.context_compaction.build", - )?; - let source = build_game_creator_agent_runtime_context_compaction_source( - root, - agent_id, - session_id, - run_id, - observations, - trigger, - )?; - let request_slot = format!( - "source-{}", - source - .source_fingerprint - .chars() - .take(32) - .collect::() - ); - if !source.has_new_source { - provider_handoff::remove_consumed_context_at( - root, - agent_id, - run_id, - request_kind, - &request_slot, - )?; - let previous = source - .previous - .as_ref() - .map(|sidecar| Some(context_compaction_result(sidecar, true))) - .ok_or_else(|| "当前 Session 没有可压缩的旧上下文".to_string()); - return previous.map(AgentRuntimeContextCompactionOutcome::Completed); - } - let template_agent_id = game_creator_runtime_template_agent_id_at(root, agent_id)?; - let app_config = load_game_creator_app_config()?; - let llm = resolve_game_creator_llm_config_for_agent(&app_config, &template_agent_id); - let config_path = format!("agentLlm.{template_agent_id}"); - let request = build_game_creator_agent_runtime_context_compaction_request(&source, &llm)?; - let estimated_request_tokens = estimate_game_creator_llm_request_tokens(&request)?; - validate_game_creator_llm_request_context_budget( - &llm, - &request, - estimated_request_tokens, - "上下文压缩请求", - )?; - let snapshot = if allow_idle_context_compaction { - capture_idle_game_creator_agent_runtime_context_compaction_snapshot_at_locked( - root, - agent_id, - session_id, - run_id, - &request_slot, - applied_steer_cursor, - )? - } else { - capture_game_creator_agent_runtime_provider_request_snapshot_at_locked( - root, - agent_id, - session_id, - run_id, - request_kind, - &request_slot, - applied_steer_cursor, - )? - }; - (snapshot, source, llm, config_path, request) - }; - let handoff_identity = - game_creator_agent_runtime_provider_retry_identity(&snapshot, &llm, &request)?; - let response = if !persist_transient_retry { - AgentRuntimePersistedProviderRequestOutcome::Response( - request_game_creator_agent_runtime_llm_with_transient_retries( - root, - &snapshot, - &llm, - &config_path, - "上下文压缩", - &request, - ) - .await?, - ) - } else { - request_game_creator_agent_runtime_llm_with_persisted_transient_retry( - root, - &snapshot, - &llm, - &config_path, - "上下文压缩", - &request, - ) - .await? - }; - let response = match response { - AgentRuntimePersistedProviderRequestOutcome::Response(Some(response)) => response, - AgentRuntimePersistedProviderRequestOutcome::Response(None) => { - return Ok(AgentRuntimeContextCompactionOutcome::Completed(None)); - } - AgentRuntimePersistedProviderRequestOutcome::Waiting(record) => { - return Ok(AgentRuntimeContextCompactionOutcome::Waiting(record)); - } - AgentRuntimePersistedProviderRequestOutcome::HandoffPrepared => { - return Ok(AgentRuntimeContextCompactionOutcome::HandoffPrepared); - } - AgentRuntimePersistedProviderRequestOutcome::Superseded => { - return Ok(AgentRuntimeContextCompactionOutcome::Superseded); - } - }; - - let control_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.context_compaction.commit", - )?; - let current_source = build_game_creator_agent_runtime_context_compaction_source( - root, - agent_id, - session_id, - run_id, - observations, - trigger, - )?; - let source_stable = current_source.source_fingerprint == source.source_fingerprint - && current_source.covered_agent_messages == source.covered_agent_messages - && current_source.covered_project_messages == source.covered_project_messages - && current_source.covered_observations == source.covered_observations - && current_source.previous.as_ref().map(|value| value.revision) - == source.previous.as_ref().map(|value| value.revision); - if !source_stable { - let base_request_id = game_creator_agent_runtime_provider_request_id(&snapshot); - let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked( - root, - &base_request_id, - ) - .map(|value| value.0) - .unwrap_or(base_request_id); - let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked( - root, - &snapshot, - &request_id, - ); - return Err(format!( - "{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: context source drift" - )); - } - let sidecar = finalize_game_creator_agent_runtime_context_compaction( - root, - &source, - &response, - estimated_tokens_before, - )?; - if let Err(error) = write_game_creator_agent_runtime_context_compaction(root, &sidecar) { - let base_request_id = game_creator_agent_runtime_provider_request_id(&snapshot); - let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked( - root, - &base_request_id, - ) - .map(|value| value.0) - .unwrap_or(base_request_id); - let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked( - root, - &snapshot, - &request_id, - ); - return Err(format!( - "{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: {}", - redact_agent_runtime_error(root, &error, 320) - )); - } - let persisted_sidecar = read_game_creator_agent_runtime_context_compaction( - root, - &sidecar.agent_id, - &sidecar.session_id, - )? - .ok_or_else(|| { - format!( - "{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: context compaction 写入后不存在" - ) - })?; - if persisted_sidecar != sidecar { - return Err(format!( - "{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: context compaction 写入后内容冲突" - )); - } - if let Err(error) = provider_handoff::remove_matching_at(root, &handoff_identity) { - let base_request_id = game_creator_agent_runtime_provider_request_id(&snapshot); - let request_id = resolve_game_creator_agent_runtime_provider_request_attempt_at_locked( - root, - &base_request_id, - ) - .map(|value| value.0) - .unwrap_or(base_request_id); - let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked( - root, - &snapshot, - &request_id, - ); - return Err(format!( - "{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: {}", - redact_agent_runtime_error(root, &error, 320) - )); - } - drop(control_lock); - - if let Ok(runtime) = - read_game_creator_agent_runtime_for_session_at(root, agent_id, Some(session_id)) - .map(|result| result.state) - { - let summary = format!("Agent 已完成第 {} 次持久上下文压缩。", sidecar.revision); - let detail = format!( - "trigger={} · agentMessages={} · projectMessages={} · observations={} · estimatedBefore={} · estimatedAfter={}", - sidecar.trigger, - sidecar.covered_agent_messages, - sidecar.covered_project_messages, - sidecar.covered_observations, - sidecar.estimated_tokens_before, - sidecar.estimated_tokens_after, - ); - let _ = append_game_creator_agent_runtime_event( - root, - &runtime, - "context.compacted", - runtime.status.as_str(), - runtime.phase.as_str(), - &summary, - Some(&detail), - ); - let _ = append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.context.compacted", - "agentId": runtime.agent_id, - "taskId": runtime.task_id, - "sessionId": runtime.session_id, - "runId": runtime.run_id, - "trigger": sidecar.trigger, - "revision": sidecar.revision, - "coveredAgentMessages": sidecar.covered_agent_messages, - "coveredProjectMessages": sidecar.covered_project_messages, - "coveredObservations": sidecar.covered_observations, - "estimatedTokensBefore": sidecar.estimated_tokens_before, - "estimatedTokensAfter": sidecar.estimated_tokens_after, - "promptTokens": sidecar.prompt_tokens, - "completionTokens": sidecar.completion_tokens, - "totalTokens": sidecar.total_tokens, - }), - ); - } - Ok(AgentRuntimeContextCompactionOutcome::Completed(Some( - context_compaction_result(&sidecar, false), - ))) -} - -pub(crate) async fn compact_game_creator_agent_runtime_session_at( - root: &Path, - agent_id: &str, - session_id: Option<&str>, -) -> Result { - validate_project_root(root)?; - let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; - let session_id = resolve_agent_conversation_session_id_at(root, &agent_id, session_id, false)?; - let runtime_lock = try_acquire_game_creator_agent_runtime_task_lock_with_wait(root, &agent_id)? - .ok_or_else(|| "Agent 正在运行,当前不能手动压缩上下文".to_string())?; - let runtime = - read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id))?; - if runtime.state.session_id != session_id - || runtime.state.status != "idle" - || !matches!(runtime.state.phase.as_str(), "idle" | "completed") - || runtime.state.pending_tool_action.is_some() - || runtime.task_queue.pending > 0 - || runtime.task_queue.running > 0 - || runtime.task_queue.waiting_for_confirmation > 0 - || runtime.task_queue.waiting_for_user_input > 0 - || game_creator_agent_runtime_has_pending_action_ledger( - root, - &agent_id, - &runtime.state.run_id, - ) - || game_creator_agent_runtime_provider_request_is_active_at( - root, - &agent_id, - &runtime.state.run_id, - )? - { - return Err( - "只有没有运行任务、Provider 请求或待确认动作的空闲 Session 才能手动压缩".to_string(), - ); - } - let observations = - read_game_creator_agent_runtime_context_bundle_for_idle_compaction(root, &runtime.state)? - .map(|bundle| bundle.observations) - .unwrap_or_default(); - let source = build_game_creator_agent_runtime_context_compaction_source( - root, - &agent_id, - &session_id, - &runtime.state.run_id, - &observations, - "manual", - )?; - let estimated_tokens_before = source.source_prompt_tokens.saturating_add(128); - let result = match compact_game_creator_agent_runtime_context_at( - root, - &agent_id, - &session_id, - &runtime.state.run_id, - &observations, - "manual", - estimated_tokens_before, - runtime.state.applied_steer_cursor, - true, - false, - "context-compaction", - ) - .await? - { - AgentRuntimeContextCompactionOutcome::Completed(Some(result)) => result, - AgentRuntimeContextCompactionOutcome::Completed(None) => { - return Err("手动上下文压缩被新的控制指令中断".to_string()); - } - AgentRuntimeContextCompactionOutcome::Waiting(_) - | AgentRuntimeContextCompactionOutcome::HandoffPrepared - | AgentRuntimeContextCompactionOutcome::Superseded => { - return Err("手动上下文压缩不应进入后台 Provider 重试等待".to_string()); - } - }; - - let mut state = - read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id))?.state; - if state.run_id != runtime.state.run_id || state.status != "idle" { - return Err("手动压缩完成前 Runtime 身份或状态发生变化".to_string()); - } - let app_config = load_game_creator_app_config()?; - let template_agent_id = game_creator_runtime_template_agent_id_at(root, &agent_id)?; - let llm = resolve_game_creator_llm_config_for_agent(&app_config, &template_agent_id); - state.context_usage.auto_compact_token_limit = llm.auto_compact_token_limit; - state.context_usage.estimated_input_tokens = result.estimated_tokens_after; - state.context_usage.last_prompt_tokens = result.prompt_tokens; - state.context_usage.last_completion_tokens = result.completion_tokens; - state.context_usage.last_total_tokens = result.total_tokens; - state.context_usage.compaction_revision = result.revision; - state.context_usage.compaction_count = result.revision; - state.context_usage.last_compaction_trigger = Some(result.trigger.clone()); - state.context_usage.last_compacted_at = Some(result.compacted_at); - state.updated_at = unix_timestamp(); - write_game_creator_agent_runtime_state(root, &state)?; - drop(runtime_lock); - emit_game_creator_agent_runtime_update(root, &agent_id); - Ok(result) -} - -pub(super) fn append_game_creator_agent_tool_plan_audit_idempotent( - root: &Path, - record: serde_json::Value, -) -> Result<(), String> { - let record_type = record - .get("recordType") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| "tool-plan 审计缺少 recordType".to_string())?; - if !matches!( - record_type, - "agent.runtime.tool_plan.protocol" | "agent.runtime.tool_plan.repair" - ) { - return Err("tool-plan 幂等审计 recordType 无效".to_string()); - } - let request_slot = record - .get("requestSlot") - .and_then(serde_json::Value::as_str) - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| "tool-plan 审计缺少 requestSlot".to_string())?; - let loop_iteration = record - .get("loopIteration") - .and_then(serde_json::Value::as_u64) - .ok_or_else(|| "tool-plan 审计缺少 loopIteration".to_string())?; - let repair_attempt = record - .get("repairAttempt") - .and_then(serde_json::Value::as_u64) - .ok_or_else(|| "tool-plan 审计缺少 repairAttempt".to_string())?; - if request_slot != format!("loop-{loop_iteration}-repair-{repair_attempt}") { - return Err("tool-plan 审计 requestSlot 与 loop/repair 身份不匹配".to_string()); - } - record - .get("responseFingerprint") - .and_then(serde_json::Value::as_str) - .filter(|value| value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit())) - .ok_or_else(|| "tool-plan 审计缺少有效 responseFingerprint".to_string())?; - append_agent_db_tool_plan_audit_idempotent(root, record).map(|_| ()) -} - -pub(super) async fn request_game_creator_agent_background_tool_plan_at( - root: &Path, - agent_id: &str, - session_id: &str, - run_id: &str, - task: &str, - observations: &[AgentRuntimeToolObservation], - loop_index: usize, - applied_steer_cursor: u64, -) -> Result { - let initial_request_slot = format!("loop-{loop_index}-repair-0"); - let (run_profile, _) = - agent_runtime_run_profile_identity_at(root, agent_id, run_id, None, None)?; - let mcp_catalog = read_game_creator_mcp_catalog_at(root).await?; - let mut built_request = { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.provider_request.build.tool_plan", - )?; - build_game_creator_agent_background_tool_plan_request( - root, - agent_id, - session_id, - run_id, - task, - observations, - loop_index, - &mcp_catalog, - )? - }; - let mut estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?; - let mut compaction = None; - if estimated_input_tokens > built_request.0.auto_compact_token_limit { - match compact_game_creator_agent_runtime_context_at( - root, - agent_id, - session_id, - run_id, - observations, - "auto", - estimated_input_tokens, - applied_steer_cursor, - false, - true, - "context-compaction", - ) - .await? - { - AgentRuntimeContextCompactionOutcome::Completed(Some(result)) => { - compaction = Some(result); - } - AgentRuntimeContextCompactionOutcome::Completed(None) => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::Ready(None)); - } - AgentRuntimeContextCompactionOutcome::Waiting(record) => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::Waiting(record)); - } - AgentRuntimeContextCompactionOutcome::HandoffPrepared => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::HandoffPrepared); - } - AgentRuntimeContextCompactionOutcome::Superseded => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::Superseded); - } - } - built_request = { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.provider_request.rebuild.tool_plan", - )?; - build_game_creator_agent_background_tool_plan_request( - root, - agent_id, - session_id, - run_id, - task, - observations, - loop_index, - &mcp_catalog, - )? - }; - estimated_input_tokens = estimate_game_creator_llm_request_tokens(&built_request.2)?; - if estimated_input_tokens > built_request.0.auto_compact_token_limit { - return Err(format!( - "上下文压缩后 tool-plan 预计输入仍为 {estimated_input_tokens} tokens,超过 autoCompactTokenLimit={};请提高阈值或新建 Session", - built_request.0.auto_compact_token_limit - )); - } - } - validate_game_creator_llm_request_context_budget( - &built_request.0, - &built_request.2, - estimated_input_tokens, - "tool-plan 请求", - )?; - if compaction.is_none() { - compaction = - read_game_creator_agent_runtime_context_compaction(root, agent_id, session_id)? - .as_ref() - .map(|sidecar| context_compaction_result(sidecar, true)); - } - let provider_snapshot = { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.provider_request.capture.tool_plan", - )?; - capture_game_creator_agent_runtime_provider_request_snapshot_at_locked( - root, - agent_id, - session_id, - run_id, - "tool-plan", - &initial_request_slot, - applied_steer_cursor, - )? - }; - let (llm, config_path, mut request, repository_context_fingerprint) = built_request; - let auto_compact_token_limit = llm.auto_compact_token_limit; - let format_repair_attempts = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { - AGENT_RUNTIME_AUTONOMOUS_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS - } else { - AGENT_RUNTIME_TOOL_PLAN_FORMAT_REPAIR_ATTEMPTS - }; - let read_only_delivery = run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && agent_runtime_task_requires_read_only_delivery(agent_id, task); - let verified_delivery = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { - let verification_gate = - read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - agent_runtime_autonomous_verified_delivery_allows_plan_completion( - agent_id, - &verification_gate, - ) - } else { - false - }; - let allow_runtime_plan_completion = read_only_delivery || verified_delivery; - let mut autonomous_scaffold_repair_active = false; - for repair_attempt in 0..=format_repair_attempts { - if game_creator_agent_runtime_cancel_requested_for(root, agent_id, run_id) { - return Err("Agent 后台任务已收到取消请求".to_string()); - } - let operation = if repair_attempt == 0 { - "后台 Agent 工具计划".to_string() - } else { - format!( - "后台 Agent 工具计划格式修复 {repair_attempt}/{}", - format_repair_attempts - ) - }; - let request_slot = format!("loop-{loop_index}-repair-{repair_attempt}"); - estimated_input_tokens = estimate_game_creator_llm_request_tokens(&request)?; - validate_game_creator_llm_request_context_budget( - &llm, - &request, - estimated_input_tokens, - &operation, - )?; - let request_snapshot = provider_snapshot - .with_request_slot(&request_slot) - .with_web_search_enabled(request.enable_web_search); - let response = request_game_creator_agent_runtime_llm_with_persisted_transient_retry( - root, - &request_snapshot, - &llm, - &config_path, - &operation, - &request, - ) - .await?; - let response = match response { - AgentRuntimePersistedProviderRequestOutcome::Response(Some(response)) => response, - AgentRuntimePersistedProviderRequestOutcome::Response(None) => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::Ready(None)); - } - AgentRuntimePersistedProviderRequestOutcome::Waiting(record) => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::Waiting(record)); - } - AgentRuntimePersistedProviderRequestOutcome::HandoffPrepared => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::HandoffPrepared); - } - AgentRuntimePersistedProviderRequestOutcome::Superseded => { - return Ok(RequestedAgentRuntimeToolPlanOutcome::Superseded); - } - }; - let response_identity = - game_creator_agent_runtime_provider_retry_identity(&request_snapshot, &llm, &request)?; - let response_handoff = - match tool_plan_handoff::lookup_at(root, agent_id, run_id, &response_identity).map_err( - |error| { - game_creator_agent_runtime_provider_handoff_reconciliation_error( - root, - &request_snapshot, - &error, - ) - }, - )? { - tool_plan_handoff::AgentRuntimeToolPlanHandoffLookup::Exact(handoff) => handoff, - tool_plan_handoff::AgentRuntimeToolPlanHandoffLookup::Missing => { - return Err( - game_creator_agent_runtime_provider_handoff_reconciliation_error( - root, - &request_snapshot, - "tool-plan Provider 成功后缺少持久交接记录", - ), - ); - } - tool_plan_handoff::AgentRuntimeToolPlanHandoffLookup::IdentityConflict(_) => { - return Err( - game_creator_agent_runtime_provider_handoff_reconciliation_error( - root, - &request_snapshot, - "tool-plan Provider 成功交接身份冲突", - ), - ); - } - }; - if response_handoff.to_llm_response() != response { - return Err( - game_creator_agent_runtime_provider_handoff_reconciliation_error( - root, - &request_snapshot, - "tool-plan Provider 成功响应与持久交接内容冲突", - ), - ); - } - let response_fingerprint = response_handoff.response_fingerprint.clone(); - let provider_request_id_sha256 = format!( - "{:x}", - Sha256::digest(response_handoff.provider_request_id.as_bytes()) - ); - let parsed = parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified( - &response, - &mcp_catalog, - ) - .and_then(|parsed| { - let source_payload = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { - let source_payload = validate_agent_runtime_autonomous_source_payload(&parsed.plan) - .map_err(|error| { - AgentRuntimeToolPlanProtocolError::new( - AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, - error, - ) - })?; - if autonomous_scaffold_repair_active - && source_payload.max_field_chars - > AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS - { - return Err(AgentRuntimeToolPlanProtocolError::new( - AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, - format!( - "自主构建首次 scaffold 源码字段为 {} 字符,超过 {} 字符修复上限;必须先提交闭合且可运行的紧凑 HTML,保留 与 ,后续再用 patch 扩展", - source_payload.max_field_chars, - AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS - ), - )); - } - validate_agent_runtime_autonomous_response_plan_completion( - root, - agent_id, - session_id, - allow_runtime_plan_completion, - &parsed.plan, - ) - .map_err(|error| { - AgentRuntimeToolPlanProtocolError::new( - AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, - error, - ) - })?; - Some(source_payload) - } else { - None - }; - Ok((parsed, source_payload)) - }); - let parsed = match parsed { - Ok((parsed, source_payload)) - if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD => - { - let verification_gate = - read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - let project_revision = - read_game_creator_agent_runtime_project_revision(root)?.revision; - let supervisor_requires_delegated_repair = if agent_id - == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - { - let collaboration_policy = - resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)? - .policy; - let collaboration_state = - read_supervisor_collaboration_state_at(root, agent_id, run_id)?; - collaboration_policy.orchestrator_only_after_delegation - && collaboration_state.has_collaboration() - } else { - false - }; - match validate_agent_runtime_autonomous_plan_liveness_at( - root, - agent_id, - run_id, - loop_index, - project_revision, - &verification_gate, - observations, - &parsed.plan, - supervisor_requires_delegated_repair, - ) { - Ok(()) => Ok((parsed, source_payload)), - Err(error) => Err(AgentRuntimeToolPlanProtocolError::new( - AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, - error, - )), - } - } - parsed => parsed, - }; - let parsed = match parsed { - Ok((parsed, source_payload)) - if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID => - { - let collaboration_policy = - resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)? - .policy; - let collaboration_state = - read_supervisor_collaboration_state_at(root, agent_id, run_id)?; - let preflight = preflight_supervisor_collaboration_plan( - agent_id, - &parsed.plan.actions, - &collaboration_policy, - &collaboration_state, - )?; - if let Some(violation) = preflight.violation { - Err(AgentRuntimeToolPlanProtocolError::new( - AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, - format!("{}:{}", violation.summary, violation.detail), - )) - } else if loop_index > AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT - && supervisor_collaboration_policy_has_initial_requirements( - &collaboration_policy, - ) - && !collaboration_state.has_collaboration() - && preflight.contract.is_none() - { - Err(AgentRuntimeToolPlanProtocolError::new( - AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, - format!( - "{AGENT_RUNTIME_SUPERVISOR_INITIAL_COLLABORATION_LIVENESS_ERROR_PREFIX};当前已到第 {loop_index} 轮,父 run 仍无协作事实,本响应也未提交满足项目 policy 的完整 agent.delegate / agent.spawn_isolated 协作批次。请在本次修复一次性建立完整首批协作,不得继续只更新计划、读取、搜索、查询状态或返回最终回复" - ), - )) - } else { - Ok((parsed, source_payload)) - } - } - parsed => parsed, - }; - match parsed { - Ok((parsed, source_payload)) => { - if provider_retry::read_for_run_at(root, agent_id, run_id)?.is_some() { - return Err( - game_creator_agent_runtime_provider_handoff_reconciliation_error( - root, - &request_snapshot, - "有效 tool-plan 响应命中不应存在的后继 repair retry", - ), - ); - } - let ParsedAgentRuntimeToolPlan { - mut plan, - protocol, - call_id: _, - function_name: _, - call_ids, - function_names, - mut normalization_kinds, - mut normalization_count, - mut normalized_text_chars, - mut normalized_text_sha256, - } = parsed; - if let Some((count, source_chars, source_sha256)) = - response_handoff.thinking_normalization_metadata() - { - if !normalization_kinds.contains(&"complete-think-block") { - normalization_kinds.insert(0, "complete-think-block"); - } - normalization_count = normalization_count.saturating_add(count); - normalized_text_chars = source_chars; - normalized_text_sha256 = Some(source_sha256.to_string()); - } - enrich_game_creator_mcp_actions(&mut plan, &mcp_catalog)?; - let call_id_sha256s = call_ids - .iter() - .map(|value| format!("{:x}", Sha256::digest(value.as_bytes()))) - .collect::>(); - let response_id_sha256 = response - .response_id - .as_deref() - .map(|value| format!("{:x}", Sha256::digest(value.as_bytes()))); - let response_id_chars = response - .response_id - .as_deref() - .map(|value| value.chars().count()) - .unwrap_or(0); - append_game_creator_agent_tool_plan_audit_idempotent( - root, - serde_json::json!({ - "recordType": "agent.runtime.tool_plan.protocol", - "agentId": agent_id, - "taskId": provider_snapshot.task_id, - "sessionId": session_id, - "runId": run_id, - "source": provider_snapshot.source, - "loopIteration": loop_index, - "repairAttempt": repair_attempt, - "requestSlot": request_slot, - "responseFingerprint": response_fingerprint, - "providerRequestIdSha256": provider_request_id_sha256, - "protocol": protocol, - "functionCallCount": call_ids.len(), - "callIdSha256s": call_id_sha256s, - "functionNames": function_names, - "normalizationKinds": normalization_kinds, - "normalizationCount": normalization_count, - "normalizedTextChars": normalized_text_chars, - "normalizedTextSha256": normalized_text_sha256, - "responseIdSha256": response_id_sha256, - "responseIdChars": response_id_chars, - "autonomousSourcePayloadValidated": source_payload.is_some(), - "autonomousSourceMutationActionCount": source_payload.map(|value| value.mutation_action_count), - "autonomousSourceMaxFieldChars": source_payload.map(|value| value.max_field_chars), - "autonomousSourceTotalChars": source_payload.map(|value| value.total_chars), - }), - )?; - return Ok(RequestedAgentRuntimeToolPlanOutcome::Ready(Some( - RequestedAgentRuntimeToolPlan { - plan, - repository_context_fingerprint, - mcp_catalog_fingerprint: mcp_catalog.fingerprint.clone(), - estimated_input_tokens, - auto_compact_token_limit, - usage: response.usage, - compaction, - }, - ))); - } - Err(error) if error.kind() == AgentRuntimeToolPlanProtocolErrorKind::CatalogBinding => { - return Err(error.to_string()); - } - Err(error) if repair_attempt < format_repair_attempts => { - if game_creator_agent_runtime_cancel_requested_for(root, agent_id, run_id) { - return Err("Agent 后台任务已收到取消请求".to_string()); - } - let next_attempt = repair_attempt + 1; - let response_preview = - game_creator_agent_tool_plan_response_preview(&response, 2_400); - let protocol_error = sanitize_agent_runtime_text(&error.to_string(), 400); - let protocol = if response.tool_calls.is_empty() { - "text_json" - } else if response.tool_calls.len() == 1 - && response.tool_calls[0].name == AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME - { - "native_function" - } else { - "native_runtime_tools" - }; - let call_id = response.tool_calls.first().map(|call| call.id.clone()); - let function_name = response.tool_calls.first().map(|call| call.name.clone()); - append_game_creator_agent_tool_plan_audit_idempotent( - root, - serde_json::json!({ - "recordType": "agent.runtime.tool_plan.repair", - "agentId": agent_id, - "taskId": provider_snapshot.task_id, - "sessionId": session_id, - "runId": run_id, - "source": provider_snapshot.source, - "loopIteration": loop_index, - "repairAttempt": repair_attempt, - "requestSlot": request_slot, - "responseFingerprint": response_fingerprint, - "providerRequestIdSha256": provider_request_id_sha256, - "attempt": next_attempt, - "maxAttempts": format_repair_attempts, - "protocolErrorKind": error.kind().as_str(), - "protocolErrorSha256": format!( - "{:x}", - Sha256::digest(protocol_error.as_bytes()) - ), - "protocolErrorChars": protocol_error.chars().count(), - "responsePreviewSha256": format!( - "{:x}", - Sha256::digest(response_preview.as_bytes()) - ), - "responsePreviewChars": response_preview.chars().count(), - "protocol": protocol, - "callIdSha256": call_id.as_deref().map(|value| format!( - "{:x}", - Sha256::digest(value.as_bytes()) - )), - "functionNameSha256": function_name.as_deref().map(|value| format!( - "{:x}", - Sha256::digest(value.as_bytes()) - )), - }), - )?; - request - .messages - .push(LlmMessage::assistant(response_preview)); - let force_autonomous_pre_mutation = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX) - && !request.function_tools.is_empty(); - let force_autonomous_read_only_delivery = - force_autonomous_pre_mutation && read_only_delivery; - let force_autonomous_pending_verification = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_PENDING_VERIFICATION_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_reverify_after_mutation = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_REVERIFY_AFTER_MUTATION_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_supervisor_delivery_convergence = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_preview_after_static = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_PREVIEW_AFTER_STATIC_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_verified_delivery = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_VERIFIED_DELIVERY_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_failed_playtest = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_delegated_playtest_repair = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error.starts_with( - AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX, - ) - && !request.function_tools.is_empty(); - let force_autonomous_response_plan_completion = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error - .starts_with(AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX) - && !request.function_tools.is_empty(); - let force_autonomous_truncated_scaffold = run_profile - == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && protocol_error - .starts_with(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX) - && !request.function_tools.is_empty(); - let force_supervisor_initial_collaboration = - agent_runtime_protocol_error_requires_supervisor_collaboration_repair( - &protocol_error, - ) && !request.function_tools.is_empty(); - if force_supervisor_initial_collaboration - || force_autonomous_response_plan_completion - || force_autonomous_delegated_playtest_repair - || force_autonomous_failed_playtest - || force_autonomous_pending_verification - || force_autonomous_reverify_after_mutation - || force_autonomous_supervisor_delivery_convergence - || force_autonomous_preview_after_static - || force_autonomous_verified_delivery - || force_autonomous_truncated_scaffold - || force_autonomous_pre_mutation - { - request.function_tools = - build_agent_runtime_native_function_tools(&mcp_catalog)?; - } - if force_supervisor_initial_collaboration { - restrict_agent_runtime_supervisor_collaboration_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须根据当前 Project Supervisor 协作策略,在同一响应中一次性调用完整的 agent.delegate / agent.spawn_isolated 批次,使首批协作合同全部成立。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_response_plan_completion { - restrict_agent_runtime_autonomous_response_plan_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留 update_agent_plan 与 respond_to_user。必须在同一响应先调用 update_agent_plan,保留原步骤标题并把已经真实完成的全部步骤标为 completed,再调用 respond_to_user 交付刚才已经形成的结论。不得只调用其中一个函数,不得新增步骤、继续读取、修改项目或解释。" - ))); - } else if force_autonomous_supervisor_delivery_convergence { - restrict_agent_runtime_autonomous_supervisor_delivery_convergence_repair_tools( - &mut request, - )?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已有 ready 未认领回执或 3 个 active delivery,本次修复的原生工具目录只保留 agent.run_status。必须立即以 agentId=null、scope=all、delegationId=null 查询状态并原子认领 readyDelegateReceipts;不得创建第四次 agent.delegate、更新计划、读取、搜索、修改项目、重复验证或 respond_to_user。认领完成后再依据最新 project revision 重新规划验证。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_preview_after_static { - restrict_agent_runtime_autonomous_preview_after_static_repair_tools( - &mut request, - )?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n当前 revision 已通过 game.static_smoke,本次修复的原生工具目录只保留 preview.validate。必须立即对 desktop 与 mobile 执行当前完成合同绑定的真实浏览器试玩;不得继续委派、更新计划、读取、搜索、查询状态、修改项目或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_delegated_playtest_repair { - restrict_agent_runtime_autonomous_delegated_playtest_repair_tools( - &mut request, - )?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate。必须立即向 code-prototype 创建一个新的后续修复委派,把最近一次 preview.validate 的全部失败诊断写入 task 和 acceptanceCriteria,expectedArtifacts 必须包含 game/index.html;repairOfDelegationId 与 runId 都设为 null,由专业 Agent 产生新的 revision。该任务是对新发现试玩缺口的后续修复,不得对已返工 delivery 再返工。不得直接修改项目、更新计划、读取、搜索、重复验证、查询状态或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_failed_playtest { - restrict_agent_runtime_autonomous_failed_playtest_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留 file.write、file.patch、file.delete 与 project.patchset。必须直接修改 game/index.html,修复最近一次 preview.validate 已证明的交互、状态或可见控件故障;不得更新计划、读取、搜索、重复验证、查询状态、委派或 respond_to_user。源码仍须遵守单字段 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS} 字符和单轮总计 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS} 字符上限,优先使用小范围 file.patch。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_reverify_after_mutation { - restrict_agent_runtime_autonomous_reverification_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n最近一次失败验证后已经完成新的项目修改,旧诊断不再代表当前 revision。本次修复的原生工具目录只保留 project.verify 与 command.run_limited;必须立即验证当前 revision,不得继续更新计划、读取、搜索、查询状态、修改项目或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_pending_verification { - restrict_agent_runtime_autonomous_verification_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录已收窄。必须直接调用实际项目修改工具修复已知问题,或在项目已经满足要求时立即调用 project.verify / command.run_limited 取得当前 revision 的通过凭证。不得只更新计划、读取、搜索、查询状态或 respond_to_user。源码仍须遵守单字段 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS} 字符和单轮总计 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS} 字符上限;优先使用小范围 file.patch。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_verified_delivery { - restrict_agent_runtime_autonomous_verified_delivery_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n当前 revision 已通过验证。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它交付专业合同结论,Runtime 会使用当前验证凭证收束结构化计划。不得更新计划、读取、搜索、查询状态、修改项目或解释。不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_truncated_scaffold { - autonomous_scaffold_repair_active = true; - restrict_agent_runtime_autonomous_truncated_scaffold_repair_tools( - &mut request, - )?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n无效的完整写入已被拒绝,现有 game/index.html 仍保持闭合且未修改。本次修复的原生工具目录只保留 file.patch;必须使用之前 file.read / project.search observation 中的短小精确原文作为 oldText,将一个可独立运行的小功能补丁插入现有文档,保留已有 与 ,不能再次完整重写文件。newText 不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符,且本轮结束后的 HTML 和 JavaScript 必须完整闭合;其余功能留到后续 planning 继续小步 patch。不要解释,不要 markdown,不要代码围栏。" - ))); - } else if force_autonomous_read_only_delivery { - restrict_agent_runtime_autonomous_read_only_delivery_repair_tools( - &mut request, - )?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n当前专业合同明确要求只读交付,不允许修改项目。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它回传审查结论、具体缺口和证据。Runtime 会在交付终态收束当前结构化计划。不得更新计划、读取、搜索、查询状态、修改项目或解释。" - ))); - } else if force_autonomous_pre_mutation { - autonomous_scaffold_repair_active = true; - restrict_agent_runtime_autonomous_liveness_repair_tools(&mut request)?; - request.messages.push(LlmMessage::user(format!( - "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录已收窄;必须直接调用当前提供的一个实际项目修改工具,或调用 respond_to_user 交付只读合同结论。不得只更新计划、读取、搜索、查询状态或空验证。首次 scaffold 的任一源码字段不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符;先保证 HTML、 与 ,后续再用 patch 扩展", + source_payload.max_field_chars, + AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS + ), + )); + } + validate_agent_runtime_autonomous_response_plan_completion( + root, + agent_id, + session_id, + allow_runtime_plan_completion, + &parsed.plan, + ) + .map_err(|error| { + AgentRuntimeToolPlanProtocolError::new( + AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, + error, + ) + })?; + Some(source_payload) + } else { + None + }; + Ok((parsed, source_payload)) + }); + let parsed = match parsed { + Ok((parsed, source_payload)) + if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD => + { + let verification_gate = + read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + let project_revision = + read_game_creator_agent_runtime_project_revision(root)?.revision; + let supervisor_requires_delegated_repair = if agent_id + == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + { + let collaboration_policy = + resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)? + .policy; + let collaboration_state = + read_supervisor_collaboration_state_at(root, agent_id, run_id)?; + collaboration_policy.orchestrator_only_after_delegation + && collaboration_state.has_collaboration() + } else { + false + }; + match validate_agent_runtime_autonomous_plan_liveness_at( + root, + agent_id, + run_id, + loop_index, + project_revision, + &verification_gate, + observations, + &parsed.plan, + supervisor_requires_delegated_repair, + ) { + Ok(()) => Ok((parsed, source_payload)), + Err(error) => Err(AgentRuntimeToolPlanProtocolError::new( + AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, + error, + )), + } + } + parsed => parsed, + }; + let parsed = match parsed { + Ok((parsed, source_payload)) + if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID => + { + let collaboration_policy = + resolve_supervisor_collaboration_policy_for_run_at(root, agent_id, run_id)? + .policy; + let collaboration_state = + read_supervisor_collaboration_state_at(root, agent_id, run_id)?; + let preflight = preflight_supervisor_collaboration_plan( + agent_id, + &parsed.plan.actions, + &collaboration_policy, + &collaboration_state, + )?; + if let Some(violation) = preflight.violation { + Err(AgentRuntimeToolPlanProtocolError::new( + AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, + format!("{}:{}", violation.summary, violation.detail), + )) + } else if loop_index > AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT + && supervisor_collaboration_policy_has_initial_requirements( + &collaboration_policy, + ) + && !collaboration_state.has_collaboration() + && preflight.contract.is_none() + { + Err(AgentRuntimeToolPlanProtocolError::new( + AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics, + format!( + "{AGENT_RUNTIME_SUPERVISOR_INITIAL_COLLABORATION_LIVENESS_ERROR_PREFIX};当前已到第 {loop_index} 轮,父 run 仍无协作事实,本响应也未提交满足项目 policy 的完整 agent.delegate / agent.spawn_isolated 协作批次。请在本次修复一次性建立完整首批协作,不得继续只更新计划、读取、搜索、查询状态或返回最终回复" + ), + )) + } else { + Ok((parsed, source_payload)) + } + } + parsed => parsed, + }; + match parsed { + Ok((parsed, source_payload)) => { + if provider_retry::read_for_run_at(root, agent_id, run_id)?.is_some() { + return Err( + game_creator_agent_runtime_provider_handoff_reconciliation_error( + root, + &request_snapshot, + "有效 tool-plan 响应命中不应存在的后继 repair retry", + ), + ); + } + let ParsedAgentRuntimeToolPlan { + mut plan, + protocol, + call_id: _, + function_name: _, + call_ids, + function_names, + mut normalization_kinds, + mut normalization_count, + mut normalized_text_chars, + mut normalized_text_sha256, + } = parsed; + if let Some((count, source_chars, source_sha256)) = + response_handoff.thinking_normalization_metadata() + { + if !normalization_kinds.contains(&"complete-think-block") { + normalization_kinds.insert(0, "complete-think-block"); + } + normalization_count = normalization_count.saturating_add(count); + normalized_text_chars = source_chars; + normalized_text_sha256 = Some(source_sha256.to_string()); + } + enrich_game_creator_mcp_actions(&mut plan, &mcp_catalog)?; + let call_id_sha256s = call_ids + .iter() + .map(|value| format!("{:x}", Sha256::digest(value.as_bytes()))) + .collect::>(); + let response_id_sha256 = response + .response_id + .as_deref() + .map(|value| format!("{:x}", Sha256::digest(value.as_bytes()))); + let response_id_chars = response + .response_id + .as_deref() + .map(|value| value.chars().count()) + .unwrap_or(0); + append_game_creator_agent_tool_plan_audit_idempotent( + root, + serde_json::json!({ + "recordType": "agent.runtime.tool_plan.protocol", + "agentId": agent_id, + "taskId": provider_snapshot.task_id, + "sessionId": session_id, + "runId": run_id, + "source": provider_snapshot.source, + "loopIteration": loop_index, + "repairAttempt": repair_attempt, + "requestSlot": request_slot, + "responseFingerprint": response_fingerprint, + "providerRequestIdSha256": provider_request_id_sha256, + "protocol": protocol, + "functionCallCount": call_ids.len(), + "callIdSha256s": call_id_sha256s, + "functionNames": function_names, + "normalizationKinds": normalization_kinds, + "normalizationCount": normalization_count, + "normalizedTextChars": normalized_text_chars, + "normalizedTextSha256": normalized_text_sha256, + "responseIdSha256": response_id_sha256, + "responseIdChars": response_id_chars, + "autonomousSourcePayloadValidated": source_payload.is_some(), + "autonomousSourceMutationActionCount": source_payload.map(|value| value.mutation_action_count), + "autonomousSourceMaxFieldChars": source_payload.map(|value| value.max_field_chars), + "autonomousSourceTotalChars": source_payload.map(|value| value.total_chars), + }), + )?; + return Ok(RequestedAgentRuntimeToolPlanOutcome::Ready(Some( + RequestedAgentRuntimeToolPlan { + plan, + repository_context_fingerprint, + mcp_catalog_fingerprint: mcp_catalog.fingerprint.clone(), + estimated_input_tokens, + auto_compact_token_limit, + usage: response.usage, + compaction, + }, + ))); + } + Err(error) if error.kind() == AgentRuntimeToolPlanProtocolErrorKind::CatalogBinding => { + return Err(error.to_string()); + } + Err(error) if repair_attempt < format_repair_attempts => { + if game_creator_agent_runtime_cancel_requested_for(root, agent_id, run_id) { + return Err("Agent 后台任务已收到取消请求".to_string()); + } + let next_attempt = repair_attempt + 1; + let response_preview = + game_creator_agent_tool_plan_response_preview(&response, 2_400); + let protocol_error = sanitize_agent_runtime_text(&error.to_string(), 400); + let protocol = if response.tool_calls.is_empty() { + "text_json" + } else if response.tool_calls.len() == 1 + && response.tool_calls[0].name == AGENT_RUNTIME_TOOL_PLAN_FUNCTION_NAME + { + "native_function" + } else { + "native_runtime_tools" + }; + let call_id = response.tool_calls.first().map(|call| call.id.clone()); + let function_name = response.tool_calls.first().map(|call| call.name.clone()); + append_game_creator_agent_tool_plan_audit_idempotent( + root, + serde_json::json!({ + "recordType": "agent.runtime.tool_plan.repair", + "agentId": agent_id, + "taskId": provider_snapshot.task_id, + "sessionId": session_id, + "runId": run_id, + "source": provider_snapshot.source, + "loopIteration": loop_index, + "repairAttempt": repair_attempt, + "requestSlot": request_slot, + "responseFingerprint": response_fingerprint, + "providerRequestIdSha256": provider_request_id_sha256, + "attempt": next_attempt, + "maxAttempts": format_repair_attempts, + "protocolErrorKind": error.kind().as_str(), + "protocolErrorSha256": format!( + "{:x}", + Sha256::digest(protocol_error.as_bytes()) + ), + "protocolErrorChars": protocol_error.chars().count(), + "responsePreviewSha256": format!( + "{:x}", + Sha256::digest(response_preview.as_bytes()) + ), + "responsePreviewChars": response_preview.chars().count(), + "protocol": protocol, + "callIdSha256": call_id.as_deref().map(|value| format!( + "{:x}", + Sha256::digest(value.as_bytes()) + )), + "functionNameSha256": function_name.as_deref().map(|value| format!( + "{:x}", + Sha256::digest(value.as_bytes()) + )), + }), + )?; + request + .messages + .push(LlmMessage::assistant(response_preview)); + let force_autonomous_pre_mutation = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX) + && !request.function_tools.is_empty(); + let force_autonomous_read_only_delivery = + force_autonomous_pre_mutation && read_only_delivery; + let force_autonomous_pending_verification = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_PENDING_VERIFICATION_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_reverify_after_mutation = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_REVERIFY_AFTER_MUTATION_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_supervisor_delivery_convergence = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_SUPERVISOR_DELIVERY_CONVERGENCE_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_preview_after_static = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_PREVIEW_AFTER_STATIC_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_verified_delivery = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_VERIFIED_DELIVERY_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_failed_playtest = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_FAILED_PLAYTEST_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_delegated_playtest_repair = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX, + ) + && !request.function_tools.is_empty(); + let force_autonomous_response_plan_completion = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error + .starts_with(AGENT_RUNTIME_AUTONOMOUS_RESPONSE_PLAN_LIVENESS_ERROR_PREFIX) + && !request.function_tools.is_empty(); + let force_autonomous_truncated_scaffold = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && protocol_error + .starts_with(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX) + && !request.function_tools.is_empty(); + let force_supervisor_initial_collaboration = + agent_runtime_protocol_error_requires_supervisor_collaboration_repair( + &protocol_error, + ) && !request.function_tools.is_empty(); + if force_supervisor_initial_collaboration + || force_autonomous_response_plan_completion + || force_autonomous_delegated_playtest_repair + || force_autonomous_failed_playtest + || force_autonomous_pending_verification + || force_autonomous_reverify_after_mutation + || force_autonomous_supervisor_delivery_convergence + || force_autonomous_preview_after_static + || force_autonomous_verified_delivery + || force_autonomous_truncated_scaffold + || force_autonomous_pre_mutation + { + request.function_tools = + build_agent_runtime_native_function_tools(&mcp_catalog)?; + } + if force_supervisor_initial_collaboration { + restrict_agent_runtime_supervisor_collaboration_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留首批协作工具。必须根据当前 Project Supervisor 协作策略,在同一响应中一次性调用完整的 agent.delegate / agent.spawn_isolated 批次,使首批协作合同全部成立。不得更新计划、读取、搜索、查询状态、修改项目或返回最终回复。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_response_plan_completion { + restrict_agent_runtime_autonomous_response_plan_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留 update_agent_plan 与 respond_to_user。必须在同一响应先调用 update_agent_plan,保留原步骤标题并把已经真实完成的全部步骤标为 completed,再调用 respond_to_user 交付刚才已经形成的结论。不得只调用其中一个函数,不得新增步骤、继续读取、修改项目或解释。" + ))); + } else if force_autonomous_supervisor_delivery_convergence { + restrict_agent_runtime_autonomous_supervisor_delivery_convergence_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已有 ready 未认领回执或 3 个 active delivery,本次修复的原生工具目录只保留 agent.run_status。必须立即以 agentId=null、scope=all、delegationId=null 查询状态并原子认领 readyDelegateReceipts;不得创建第四次 agent.delegate、更新计划、读取、搜索、修改项目、重复验证或 respond_to_user。认领完成后再依据最新 project revision 重新规划验证。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_preview_after_static { + restrict_agent_runtime_autonomous_preview_after_static_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前 revision 已通过 game.static_smoke,本次修复的原生工具目录只保留 preview.validate。必须立即对 desktop 与 mobile 执行当前完成合同绑定的真实浏览器试玩;不得继续委派、更新计划、读取、搜索、查询状态、修改项目或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_delegated_playtest_repair { + restrict_agent_runtime_autonomous_delegated_playtest_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前父 run 已进入只编排模式,本次修复的原生工具目录只保留 agent.delegate。必须立即向 code-prototype 创建一个新的后续修复委派,把最近一次 preview.validate 的全部失败诊断写入 task 和 acceptanceCriteria,expectedArtifacts 必须包含 game/index.html;repairOfDelegationId 与 runId 都设为 null,由专业 Agent 产生新的 revision。该任务是对新发现试玩缺口的后续修复,不得对已返工 delivery 再返工。不得直接修改项目、更新计划、读取、搜索、重复验证、查询状态或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_failed_playtest { + restrict_agent_runtime_autonomous_failed_playtest_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录只保留 file.write、file.patch、file.delete 与 project.patchset。必须直接修改 game/index.html,修复最近一次 preview.validate 已证明的交互、状态或可见控件故障;不得更新计划、读取、搜索、重复验证、查询状态、委派或 respond_to_user。源码仍须遵守单字段 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS} 字符和单轮总计 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS} 字符上限,优先使用小范围 file.patch。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_reverify_after_mutation { + restrict_agent_runtime_autonomous_reverification_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n最近一次失败验证后已经完成新的项目修改,旧诊断不再代表当前 revision。本次修复的原生工具目录只保留 project.verify 与 command.run_limited;必须立即验证当前 revision,不得继续更新计划、读取、搜索、查询状态、修改项目或 respond_to_user。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_pending_verification { + restrict_agent_runtime_autonomous_verification_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录已收窄。必须直接调用实际项目修改工具修复已知问题,或在项目已经满足要求时立即调用 project.verify / command.run_limited 取得当前 revision 的通过凭证。不得只更新计划、读取、搜索、查询状态或 respond_to_user。源码仍须遵守单字段 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_FIELD_MAX_CHARS} 字符和单轮总计 {AGENT_RUNTIME_AUTONOMOUS_SOURCE_TOTAL_MAX_CHARS} 字符上限;优先使用小范围 file.patch。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_verified_delivery { + restrict_agent_runtime_autonomous_verified_delivery_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前 revision 已通过验证。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它交付专业合同结论,Runtime 会使用当前验证凭证收束结构化计划。不得更新计划、读取、搜索、查询状态、修改项目或解释。不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_truncated_scaffold { + autonomous_scaffold_repair_active = true; + restrict_agent_runtime_autonomous_truncated_scaffold_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n无效的完整写入已被拒绝,现有 game/index.html 仍保持闭合且未修改。本次修复的原生工具目录只保留 file.patch;必须使用之前 file.read / project.search observation 中的短小精确原文作为 oldText,将一个可独立运行的小功能补丁插入现有文档,保留已有 与 ,不能再次完整重写文件。newText 不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符,且本轮结束后的 HTML 和 JavaScript 必须完整闭合;其余功能留到后续 planning 继续小步 patch。不要解释,不要 markdown,不要代码围栏。" + ))); + } else if force_autonomous_read_only_delivery { + restrict_agent_runtime_autonomous_read_only_delivery_repair_tools( + &mut request, + )?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前专业合同明确要求只读交付,不允许修改项目。本次修复的原生工具目录只保留 respond_to_user;必须立即调用它回传审查结论、具体缺口和证据。Runtime 会在交付终态收束当前结构化计划。不得更新计划、读取、搜索、查询状态、修改项目或解释。" + ))); + } else if force_autonomous_pre_mutation { + autonomous_scaffold_repair_active = true; + restrict_agent_runtime_autonomous_liveness_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n本次修复的原生工具目录已收窄;必须直接调用当前提供的一个实际项目修改工具,或调用 respond_to_user 交付只读合同结论。不得只更新计划、读取、搜索、查询状态或空验证。首次 scaffold 的任一源码字段不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符;先保证 HTML、