use super::*; static AGENT_RUNTIME_EVENT_ID_SEQUENCE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); pub(crate) const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX: &str = "runtime-public-status-"; fn game_creator_agent_runtime_public_status_message_id( agent_id: &str, session_id: &str, run_id: &str, status: &str, ) -> String { let correlation_id = game_creator_agent_runtime_message_correlation_id(agent_id, session_id, run_id); game_creator_agent_runtime_public_status_message_id_for_correlation(&correlation_id, status) } fn game_creator_agent_runtime_public_status_message_id_for_correlation( correlation_id: &str, status: &str, ) -> String { let status_fingerprint = format!("{:x}", Sha256::digest(status.as_bytes())); format!( "{AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX}{correlation_id}-{}", status_fingerprint.chars().take(16).collect::() ) } pub(crate) fn game_creator_agent_runtime_message_correlation_id( agent_id: &str, session_id: &str, run_id: &str, ) -> String { let identity = format!("{agent_id}\n{session_id}\n{run_id}"); let fingerprint = format!("{:x}", Sha256::digest(identity.as_bytes())); fingerprint.chars().take(32).collect::() } pub(crate) fn game_creator_agent_runtime_accepted_public_message(agent_id: &str) -> String { if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { "任务已接收,项目总控 Agent 正在启动处理。".to_string() } else { "任务已接收,专业 Agent 正在启动处理。".to_string() } } pub(crate) fn append_game_creator_agent_runtime_public_status_message_at( root: &Path, agent_id: &str, session_id: &str, run_id: &str, status: &str, content: &str, ) -> Result<(), String> { let message_id = game_creator_agent_runtime_public_status_message_id(agent_id, session_id, run_id, status); append_local_conversation_message_for_session_idempotent_at( root, None, None, LocalConversationMessage { role: "assistant".to_string(), content: content.to_string(), agent_id: None, }, &message_id, ) .map(|_| ()) } pub(crate) fn append_game_creator_agent_runtime_public_status_message_for_correlation_at( root: &Path, correlation_id: &str, status: &str, content: &str, ) -> Result<(), String> { let message_id = game_creator_agent_runtime_public_status_message_id_for_correlation(correlation_id, status); append_local_conversation_message_for_session_idempotent_at( root, None, None, LocalConversationMessage { role: "assistant".to_string(), content: content.to_string(), agent_id: None, }, &message_id, ) .map(|_| ()) } pub(crate) fn append_game_creator_agent_runtime_terminal_public_message_at( root: &Path, state: &AgentRuntimeState, error: &str, ) -> Result<(), String> { let content = game_creator_agent_runtime_failure_conversation_message(&state.agent_id, error); let status = if error.contains(GAME_CHAT_FIRST_PLAYABLE_HARD_BUDGET_ERROR_PREFIX) { "game-chat-hard-deadline" } else if state.phase == "budget-exhausted" { "budget-exhausted" } else if state.phase == "needs-reconciliation" { "needs-reconciliation" } else { "failed" }; if state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID && (state.parent_agent_id.is_some() || state.parent_run_id.is_some()) { // Receipt and isolated-join continuations are internal Supervisor // work. Their backend-owned Runtime event may remain visible, but a // second Session message would duplicate that same terminal outcome // in the formal project chat. return Ok(()); } if state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID && state.parent_agent_id.is_none() && state.parent_run_id.is_none() { return append_game_creator_agent_runtime_public_status_message_at( root, &state.agent_id, &state.session_id, &state.run_id, status, &content, ); } let message_id = game_creator_agent_runtime_public_status_message_id( &state.agent_id, &state.session_id, &state.run_id, status, ); append_local_conversation_message_for_session_idempotent_at( root, Some(&state.agent_id), Some(&state.session_id), LocalConversationMessage { role: "assistant".to_string(), content, agent_id: None, }, &message_id, ) .map(|_| ()) } fn new_game_creator_agent_runtime_event_id( state: &AgentRuntimeState, event_type: &str, phase: &str, action_id: Option<&str>, ) -> String { if let Some(action_id) = action_id { return format!( "runtime-event-action-{}-{}-{}-{}", state.run_id, event_type, phase, action_id ); } let sequence = AGENT_RUNTIME_EVENT_ID_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed); format!( "runtime-event-{}-{}-{}-{}", std::process::id(), unix_millis(), sequence, event_type ) } fn game_creator_agent_runtime_event_type_is_public(event_type: &str) -> bool { matches!( event_type, "thinking_summary" | "plan" | "plan_update" | "action" | "observation" | "turn.started" | "turn.progress" | "turn.completed" | "turn.failed" | "turn.budget_exhausted" | "turn.cancelled" | "response" | "response.stale" | "goal.paused" | "goal.resumed" | "agent.delegate.result" | "agent.delegate.result_failed" ) || event_type.starts_with("tool_confirmation.") || event_type.starts_with("user_input.") } fn game_creator_agent_runtime_public_event_text( root: &Path, event_type: &str, summary: &str, ) -> Option { let event_type = event_type.trim(); if !game_creator_agent_runtime_event_type_is_public(event_type) { return None; } let summary = redact_agent_runtime_error(root, summary.trim(), 240); if summary.is_empty() { return None; } let lower = summary.to_ascii_lowercase(); if [ "runtime.", "agent.runtime.", "provider.", "provider_request.", "parallel_read_batch.", "provider_action_batch.", "finalization.", "context.", "process_session.", "steer.", "autonomous_manifest.parent_wake", "agent.delegate.parent_wake", "command.exec:", "command.exec:", "command.output_read:", "command.output_read:", "agent.action_history:", "agent.action_history:", ] .iter() .any(|prefix| lower.starts_with(prefix)) { return None; } if [ "sha256", "fingerprint", "authorization", "bearer", "api key", "api_key", "password", "secret", "cookie", "token=", "private process output", " Result { start_game_creator_agent_runtime_turn_for_session_at(root, agent_id, None, prompt, run_id) } pub(crate) fn start_game_creator_agent_runtime_turn_for_session_at( root: &Path, agent_id: &str, session_id: Option<&str>, prompt: &str, run_id: &str, ) -> Result { start_game_creator_agent_runtime_task_for_session_at( root, agent_id, session_id, prompt, run_id, "agent-chat", "读取项目上下文", vec![ "读取项目记忆、黑板、本 Agent 私有记忆和最近对话".to_string(), "按当前 Agent 职责独立推理本轮建议".to_string(), "流式回复开发者并把本轮 runtime 状态落盘".to_string(), ], ) } pub(crate) fn start_game_creator_agent_runtime_task_at( root: &Path, agent_id: &str, task: &str, run_id: &str, source: &str, current_action: &str, plan: Vec, ) -> Result { start_game_creator_agent_runtime_task_for_session_at( root, agent_id, None, task, run_id, source, current_action, plan, ) } pub(crate) fn start_game_creator_agent_runtime_task_for_session_at( root: &Path, agent_id: &str, session_id: Option<&str>, task: &str, run_id: &str, source: &str, current_action: &str, plan: Vec, ) -> Result { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; validate_project_root(root)?; with_agent_conversation_session_lane_at(root, &agent_id, "Agent Session Runtime 启动", || { start_game_creator_agent_runtime_task_for_session_in_session_lane_at( root, &agent_id, session_id, task, run_id, source, current_action, plan, ) }) } pub(super) fn start_game_creator_agent_runtime_task_for_session_in_session_lane_at( root: &Path, agent_id: &str, session_id: Option<&str>, task: &str, run_id: &str, source: &str, current_action: &str, plan: Vec, ) -> Result { let isolated_instance = agent_id .starts_with("child-") .then(|| resolve_isolated_agent_instance_at(root, &agent_id)) .transpose()?; let session_id = resolve_agent_conversation_session_id_at(root, &agent_id, session_id, true)?; let run_id = normalize_game_creator_agent_runtime_run_id(&agent_id, run_id); if isolated_instance .as_ref() .is_some_and(|instance| instance.session_id != session_id || instance.run_id != run_id) { return Err("动态隔离子 Agent Session/Run 与实例契约不一致".to_string()); } let task = task.trim(); if task.is_empty() { return Err("Agent Runtime 任务不能为空".to_string()); } let runtime_task = sanitize_agent_runtime_text(task, AGENT_RUNTIME_TASK_MAX_CHARS); if source.trim() == AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE { ensure_game_creator_agent_delegate_receipt_conversation_at( root, &agent_id, &session_id, task, )?; } let queued_task_record = read_latest_game_creator_agent_runtime_task_by_run_id( root, &agent_id, &run_id, )? .filter(|record| { record.session_id == session_id && record.source == source.trim() && matches!( record.status.as_str(), "pending" | "running" | "waiting-for-confirmation" | "waiting-for-user-input" ) }); let task_link = queued_task_record .as_ref() .map(|record| AgentRuntimeTaskLink { parent_agent_id: record.parent_agent_id.clone(), parent_run_id: record.parent_run_id.clone(), delegation_id: record.delegation_id.clone(), }) .unwrap_or_default(); let previous_state = read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id)) .ok() .map(|result| result.state); let mut state = default_game_creator_agent_runtime_state(&agent_id, &run_id); state.started_at = queued_task_record .as_ref() .map(|record| record.updated_at) .filter(|updated_at| *updated_at > 0) .unwrap_or_else(unix_timestamp); state.session_id = session_id; state.source = source.trim().to_string(); let (run_profile, run_profile_binding_fingerprint) = agent_runtime_run_profile_identity_at( root, &agent_id, &run_id, queued_task_record .as_ref() .map(|record| record.run_profile.as_str()), queued_task_record .as_ref() .map(|record| record.run_profile_binding_fingerprint.as_str()), )?; state.run_profile = run_profile; state.run_profile_binding_fingerprint = run_profile_binding_fingerprint; state.parent_agent_id = task_link.parent_agent_id; state.parent_run_id = task_link.parent_run_id; state.delegation_id = task_link.delegation_id; state.status = "running".to_string(); state.phase = "planning".to_string(); state.current_task = runtime_task.clone(); state.current_goal = runtime_task.clone(); state.current_action = current_action.trim().to_string(); state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string(); state.next_step = "等待 Agent 输出计划或回复".to_string(); update_agent_runtime_plan_steps(&mut state, plan); state.observations = vec!["已创建本轮 Agent Runtime run。".to_string()]; if let Some(previous_state) = previous_state { let same_runtime_run = previous_state.agent_id == state.agent_id && previous_state.task_id == state.task_id && previous_state.session_id == state.session_id && previous_state.run_id == state.run_id && previous_state.source == state.source && previous_state.current_task == state.current_task; if same_runtime_run { if previous_state.started_at > 0 { state.started_at = previous_state.started_at; } state.loop_iteration = previous_state.loop_iteration; state.max_loop_iterations = previous_state.max_loop_iterations; state.tool_action_budget = previous_state.tool_action_budget; validate_agent_runtime_structured_plan_snapshot( previous_state.plan_revision, &previous_state.plan_explanation, &previous_state.plan, &previous_state.plan_steps, previous_state.active_plan_step_index, )?; state.plan_revision = previous_state.plan_revision; state.plan_explanation = previous_state.plan_explanation; state.plan = previous_state.plan; state.plan_steps = previous_state.plan_steps; state.active_plan_step_index = previous_state.active_plan_step_index; } state.recent_tool_calls = previous_state.recent_tool_calls; state.context_usage = previous_state.context_usage; state.last_response = previous_state.last_response; } hydrate_game_creator_agent_goal_state_at(root, &mut state)?; refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?; state.updated_at = unix_timestamp(); append_game_creator_agent_runtime_task(root, &state)?; refresh_game_creator_agent_runtime_task_queue(root, &mut state)?; write_game_creator_agent_runtime_state(root, &state)?; let runtime_task_sha256 = format!("{:x}", Sha256::digest(runtime_task.as_bytes())); let runtime_task_chars = runtime_task.chars().count(); let private_runtime_task = agent_runtime_task_requires_private_audit( state.goal_id.as_deref(), state.parent_agent_id.as_deref(), ); let public_runtime_task = (!private_runtime_task).then(|| runtime_task.clone()); let runtime_event_detail = if private_runtime_task { format!( "goalRevision={} · taskChars={runtime_task_chars} · taskSha256={runtime_task_sha256} · goalBound={} · delegated={}", state.goal_revision, state.goal_id.is_some(), state.parent_agent_id.is_some(), ) } else { runtime_task.clone() }; append_game_creator_agent_runtime_event( root, &state, "turn.started", "running", "planning", "Agent Runtime 开始处理本轮输入。", Some(&runtime_event_detail), )?; append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.turn", "agentId": state.agent_id, "taskId": state.task_id, "sessionId": state.session_id, "runId": state.run_id, "source": state.source, "status": state.status, "phase": state.phase, "task": public_runtime_task, "taskSha256": runtime_task_sha256, "taskChars": runtime_task_chars, "goalBound": state.goal_id.is_some(), "delegated": state.parent_agent_id.is_some(), }), )?; Ok(state) } pub(crate) fn advance_game_creator_agent_runtime_turn_at( root: &Path, mut state: AgentRuntimeState, phase: &str, action: &str, observation: &str, ) -> Result { state.phase = phase.trim().to_string(); state.current_action = action.trim().to_string(); state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string(); state.next_step = agent_runtime_next_step_for_phase(&state.phase).to_string(); if !observation.trim().is_empty() { state.observations.push(observation.trim().to_string()); } refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?; state.updated_at = unix_timestamp(); append_game_creator_agent_runtime_task(root, &state)?; refresh_game_creator_agent_runtime_task_queue(root, &mut state)?; write_game_creator_agent_runtime_state(root, &state)?; append_game_creator_agent_runtime_event( root, &state, "turn.progress", state.status.as_str(), state.phase.as_str(), state.current_action.as_str(), Some(observation), )?; Ok(state) } pub(super) fn prepare_game_creator_agent_runtime_completed_state( root: &Path, mut state: AgentRuntimeState, response: &str, ) -> Result { state.pending_tool_action = None; state.status = "idle".to_string(); state.phase = "completed".to_string(); state.current_action = "等待下一轮输入".to_string(); state.waiting_on = "开发者下一轮输入".to_string(); state.next_step = "等待下一轮输入".to_string(); state.last_response = Some(sanitize_agent_runtime_text(response, 500)); state.error = None; complete_agent_runtime_remaining_plan_steps(&mut state, "本轮 Agent 已生成最终回复。"); state .observations .push("Agent 已完成回复,assistant 消息等待或已经由前端落盘。".to_string()); refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?; state.updated_at = unix_timestamp(); Ok(state) } pub(crate) fn redact_agent_runtime_private_process_output_from_response( response: &str, observations: &[AgentRuntimeToolObservation], ) -> String { if agent_runtime_observations_contain_private_process_output(observations) { "持久进程交互已完成,私有进程输出已省略。".to_string() } else { response.trim().to_string() } } pub(super) fn agent_runtime_observations_contain_private_process_output( observations: &[AgentRuntimeToolObservation], ) -> bool { observations.iter().any(|observation| { observation.tool == "command.poll" && observation.status == "ok" && observation .detail .as_deref() .and_then(|detail| serde_json::from_str::(detail).ok()) .and_then(|detail| { detail .get("output") .and_then(serde_json::Value::as_str) .map(|output| !output.is_empty()) }) .unwrap_or(false) }) } pub(crate) fn finish_game_creator_agent_runtime_turn_at( root: &Path, state: AgentRuntimeState, response: &str, ) -> Result { let mut state = prepare_game_creator_agent_runtime_completed_state(root, state, response)?; append_game_creator_agent_runtime_task(root, &state)?; refresh_game_creator_agent_runtime_task_queue(root, &mut state)?; write_game_creator_agent_runtime_state(root, &state)?; append_game_creator_agent_runtime_event( root, &state, "turn.completed", "idle", "completed", "Agent Runtime 完成本轮处理。", state.last_response.as_deref(), )?; append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.completed", "agentId": state.agent_id, "taskId": state.task_id, "sessionId": state.session_id, "runId": state.run_id, "source": state.source, "responsePreview": state.last_response, }), )?; remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?; publish_game_creator_agent_delegate_result_for_state( root, &state, state.last_response.as_deref(), ); Ok(state) } pub(super) fn game_creator_agent_runtime_event_exists( root: &Path, agent_id: &str, run_id: &str, event_type: &str, ) -> Result { let path = game_creator_agent_runtime_event_path(root, agent_id); let file = match File::open(&path) { Ok(file) => file, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), Err(error) => { return Err(format!( "读取 Agent Runtime 事件失败:{}: {error}", path.display() )) } }; for line in BufReader::new(file).lines() { let line = line .map_err(|error| format!("读取 Agent Runtime 事件失败:{}: {error}", path.display()))?; if line.trim().is_empty() { continue; } let event = serde_json::from_str::(&line) .map_err(|error| format!("解析 Agent Runtime 事件失败:{}: {error}", path.display()))?; if event.run_id == run_id && event.event_type == event_type { return Ok(true); } } Ok(false) } pub(crate) fn agent_db_record_exists_for_finalization( root: &Path, expected_record: &serde_json::Value, ) -> Result { let record_type = expected_record .get("recordType") .and_then(serde_json::Value::as_str) .ok_or_else(|| "finalization 审计缺少 recordType".to_string())?; let finalization_id = expected_record .get("finalizationId") .and_then(serde_json::Value::as_str) .ok_or_else(|| "finalization 审计缺少 finalizationId".to_string())?; let message_id = expected_record .get("messageId") .and_then(serde_json::Value::as_str) .ok_or_else(|| "finalization 审计缺少 messageId".to_string())?; let path = root.join(".agent/agent.db"); let file = match File::open(&path) { Ok(file) => file, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), Err(error) => { return Err(format!( "读取 Agent 本地索引失败:{}: {error}", path.display() )); } }; let mut exact_matches = 0usize; for line in BufReader::new(file).lines() { let line = line.map_err(|error| format!("读取 Agent 本地索引失败:{}: {error}", path.display()))?; if line.trim().is_empty() { continue; } let record = serde_json::from_str::(&line) .map_err(|error| format!("解析 Agent 本地索引失败:{}: {error}", path.display()))?; if record.get("recordType").and_then(serde_json::Value::as_str) != Some(record_type) || record .get("finalizationId") .and_then(serde_json::Value::as_str) != Some(finalization_id) || record.get("messageId").and_then(serde_json::Value::as_str) != Some(message_id) { continue; } if !agent_db_stored_record_matches_expected_payload(&record, expected_record) { return Err(format!( "Agent finalization 审计内容冲突:recordType={record_type} finalizationId={finalization_id}" )); } exact_matches = exact_matches.saturating_add(1); if exact_matches > 1 { return Err(format!( "Agent finalization 审计重复:recordType={record_type} finalizationId={finalization_id}" )); } } Ok(exact_matches == 1) } pub(super) fn agent_db_record_exists_for_action( root: &Path, record_type: &str, agent_id: &str, run_id: &str, action_id: &str, ) -> Result { let path = root.join(".agent/agent.db"); let file = match File::open(&path) { Ok(file) => file, Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false), Err(error) => { return Err(format!( "读取 Agent 本地索引失败:{}: {error}", path.display() )); } }; for line in BufReader::new(file).lines() { let line = line.map_err(|error| format!("读取 Agent 本地索引失败:{}: {error}", path.display()))?; if line.trim().is_empty() { continue; } let record = serde_json::from_str::(&line) .map_err(|error| format!("解析 Agent 本地索引失败:{}: {error}", path.display()))?; if record.get("recordType").and_then(serde_json::Value::as_str) == Some(record_type) && record.get("agentId").and_then(serde_json::Value::as_str) == Some(agent_id) && record.get("runId").and_then(serde_json::Value::as_str) == Some(run_id) && record.get("actionId").and_then(serde_json::Value::as_str) == Some(action_id) { return Ok(true); } } Ok(false) } pub(super) fn finish_game_creator_agent_background_runtime_turn_idempotently_at( root: &Path, state: AgentRuntimeState, journal: &AgentRuntimeFinalizationJournal, response: &str, ) -> Result { let mut completed = prepare_game_creator_agent_runtime_completed_state(root, state, response)?; let expected_terminal_detail = completed.last_response.clone().unwrap_or_default(); let public_response_detail = format!( "responseSha256={} responseChars={}", journal.response_fingerprint, journal.response.chars().count() ); match read_latest_game_creator_agent_runtime_task_by_run_id( root, &completed.agent_id, &completed.run_id, )? { Some(task) if task.status == "completed" => { if task.terminal_detail.as_deref() != Some(expected_terminal_detail.as_str()) { return Err("Agent Runtime completed task 与 finalization 回复不匹配".to_string()); } } Some(task) if matches!( task.status.as_str(), "failed" | "cancelled" | "budget-exhausted" ) => { return Err(format!( "Agent Runtime finalization 与既有终态冲突:{}", task.status )); } _ => append_game_creator_agent_runtime_task(root, &completed)?, } refresh_game_creator_agent_runtime_task_queue(root, &mut completed)?; write_game_creator_agent_runtime_state(root, &completed)?; append_game_creator_agent_runtime_finalization_lifecycle_stage( root, journal, "runtime-completed", unix_timestamp(), )?; complete_game_creator_agent_goal_for_runtime_at_locked(root, &mut completed, response)?; let goal_projection_matches = read_latest_game_creator_agent_runtime_task_by_run_id( root, &completed.agent_id, &completed.run_id, )? .is_some_and(|task| { task.status == "completed" && task.goal_id == completed.goal_id && task.goal_revision == completed.goal_revision && task.goal_status == completed.goal_status }); if !goal_projection_matches { append_game_creator_agent_runtime_task(root, &completed)?; } refresh_game_creator_agent_runtime_task_queue(root, &mut completed)?; write_game_creator_agent_runtime_state(root, &completed)?; append_game_creator_agent_runtime_finalization_lifecycle_stage( root, journal, "goal-completed", unix_timestamp(), )?; if !game_creator_agent_runtime_event_exists( root, &completed.agent_id, &completed.run_id, "turn.completed", )? { append_game_creator_agent_runtime_event( root, &completed, "turn.completed", "idle", "completed", "Agent Runtime 完成本轮处理。", Some(&public_response_detail), )?; } let completed_audit = serde_json::json!({ "recordType": "agent.runtime.completed", "agentId": completed.agent_id, "taskId": completed.task_id, "sessionId": completed.session_id, "runId": completed.run_id, "source": completed.source, "finalizationId": journal.finalization_id, "messageId": journal.message_id, "responseFingerprint": journal.response_fingerprint, "responseChars": journal.response.chars().count(), }); if !agent_db_record_exists_for_finalization(root, &completed_audit)? { append_agent_db_record(root, completed_audit)?; } remove_game_creator_agent_runtime_pending_tool_action( root, &completed.agent_id, &completed.run_id, )?; remove_game_creator_agent_runtime_provider_action_batch( root, &completed.agent_id, &completed.run_id, )?; remove_game_creator_agent_runtime_confirmations(root, &completed.agent_id, &completed.run_id)?; provider_retry::remove_at(root, &completed.agent_id, &completed.run_id)?; publish_game_creator_agent_delegate_result_for_state_at_locked( root, &completed, completed.last_response.as_deref(), ); if !game_creator_agent_runtime_event_exists( root, &completed.agent_id, &completed.run_id, "response", )? { append_game_creator_agent_runtime_event( root, &completed, "response", completed.status.as_str(), completed.phase.as_str(), "Agent 已生成最终回复。", Some(&public_response_detail), )?; } let background_completed_audit = serde_json::json!({ "recordType": "agent.runtime.background_task.completed", "agentId": completed.agent_id, "taskId": completed.task_id, "sessionId": completed.session_id, "runId": completed.run_id, "source": completed.source, "finalizationId": journal.finalization_id, "messageId": journal.message_id, "responseFingerprint": journal.response_fingerprint, "responseChars": journal.response.chars().count(), }); if !agent_db_record_exists_for_finalization(root, &background_completed_audit)? { append_agent_db_record(root, background_completed_audit)?; } Ok(completed) } pub(crate) fn prepare_game_creator_agent_background_stale_continuation_at( root: &Path, state: &mut AgentRuntimeState, task: &str, plan: &mut AgentRuntimeToolPlan, observations: &mut Vec, context_tracker: &mut AgentRuntimeContextWindowTracker, blocker: AgentRuntimeToolObservation, ) -> Result { let blocker_summary = blocker.summary(); let blocker_detail = blocker.detail.clone(); let runtime_blocker_summary = blocker_detail .as_deref() .filter(|detail| !detail.trim().is_empty()) .map(|detail| format!("{blocker_summary}:{detail}")) .unwrap_or_else(|| blocker_summary.clone()); retry_agent_runtime_active_plan_step( state, "最终回复生成期间项目 revision 已变化,旧回复已丢弃并等待重新规划。", ); state.status = "running".to_string(); state.phase = "observation".to_string(); state.current_action = "丢弃过期最终回复并回到 planning".to_string(); state.waiting_on = "Agent 重新规划并验证当前项目 revision".to_string(); state.next_step = "根据 runtime.verification blocker 重新规划、验证并生成当前回复".to_string(); state.error = None; state.observations.push(runtime_blocker_summary); state.updated_at = unix_timestamp(); append_game_creator_agent_runtime_task(root, state)?; refresh_game_creator_agent_runtime_task_queue(root, state)?; write_game_creator_agent_runtime_state(root, state)?; append_game_creator_agent_runtime_event( root, state, "response.stale", state.status.as_str(), state.phase.as_str(), "最终回复生成期间项目 revision 已变化,已丢弃旧回复并在同一 run 重新规划。", blocker_detail.as_deref(), )?; append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.background_task.response_stale", "agentId": state.agent_id, "taskId": state.task_id, "sessionId": state.session_id, "runId": state.run_id, "source": state.source, "summary": blocker_summary, "detail": blocker_detail, }), )?; plan.actions.clear(); plan.response.clear(); context_tracker.record(&blocker); observations.push(blocker); *observations = sanitize_game_creator_agent_runtime_context_observations_for_storage(root, observations); let next_loop_index = usize::try_from(state.loop_iteration).unwrap_or(usize::MAX); persist_game_creator_agent_runtime_context( root, state, task, plan, observations, next_loop_index, context_tracker, )?; let mut continuation = AgentRuntimeContinuationContext { plan: plan.clone(), observations: observations.clone(), next_loop_index, context_stalled: false, ..AgentRuntimeContinuationContext::default() }; context_tracker.apply_to_continuation(&mut continuation); Ok(continuation) } pub(crate) fn game_creator_agent_runtime_finalization_assistant_exists( root: &Path, journal: &AgentRuntimeFinalizationJournal, ) -> Result { let Some(message) = read_local_conversation_message_by_id_for_session_at( root, Some(&journal.agent_id), Some(&journal.session_id), &journal.message_id, )? else { return Ok(false); }; if message.role != "assistant" || message.content != journal.response || message.agent_id.as_deref() != Some(journal.agent_id.as_str()) { return Err("Agent Runtime finalization messageId 与既有会话消息冲突".to_string()); } Ok(true) } pub(super) fn advance_game_creator_agent_runtime_finalization_at( root: &Path, state: AgentRuntimeState, journal: &mut AgentRuntimeFinalizationJournal, checkpoint: &mut F, ) -> Result where F: FnMut(AgentRuntimeFinalizationCheckpoint) -> Result<(), String>, { let assistant_exists = game_creator_agent_runtime_finalization_assistant_exists(root, journal)?; append_game_creator_agent_runtime_finalization_lifecycle_stage( root, journal, "prepared", journal.prepared_at, )?; match journal.status.as_str() { AGENT_RUNTIME_FINALIZATION_STATUS_PREPARED => { append_local_conversation_message_for_session_idempotent_with_finalization_at( root, Some(&journal.agent_id), Some(&journal.session_id), LocalConversationMessage { role: "assistant".to_string(), content: journal.response.clone(), agent_id: None, }, &journal.message_id, &journal.finalization_id, )?; checkpoint(AgentRuntimeFinalizationCheckpoint::AssistantAppended)?; let now = unix_timestamp(); journal.status = AGENT_RUNTIME_FINALIZATION_STATUS_ASSISTANT_PERSISTED.to_string(); journal.assistant_persisted_at = Some(now); journal.updated_at = now; write_game_creator_agent_runtime_finalization_journal(root, journal)?; } AGENT_RUNTIME_FINALIZATION_STATUS_ASSISTANT_PERSISTED | AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED => { if !assistant_exists { return Err( "Agent Runtime finalization 已记录 assistant,但会话消息不存在".to_string(), ); } } _ => return Err("Agent Runtime finalization 状态无效".to_string()), } append_game_creator_agent_runtime_finalization_lifecycle_stage( root, journal, "assistant-persisted", journal.assistant_persisted_at.unwrap_or(journal.updated_at), )?; let completed = finish_game_creator_agent_background_runtime_turn_idempotently_at( root, state, journal, &journal.response, )?; if journal.status != AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED { checkpoint(AgentRuntimeFinalizationCheckpoint::RuntimeCompleted)?; let now = unix_timestamp(); journal.status = AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED.to_string(); journal.runtime_completed_at = Some(now); journal.updated_at = now; write_game_creator_agent_runtime_finalization_journal(root, journal)?; } append_game_creator_agent_runtime_finalization_lifecycle_stage( root, journal, "goal-completed", journal.runtime_completed_at.unwrap_or(journal.updated_at), )?; mark_game_creator_agent_runtime_response_stream_committed_at(root, &completed, journal)?; checkpoint(AgentRuntimeFinalizationCheckpoint::ResponseStreamCommitted)?; remove_game_creator_agent_runtime_finalization_recovery_sidecars( root, &journal.agent_id, &journal.run_id, )?; Ok(completed) } pub(super) fn record_game_creator_agent_runtime_finalization_pending( root: &Path, state: &AgentRuntimeState, error: &str, ) { let error = redact_agent_runtime_error(root, error, 500); let mut visible_state = read_game_creator_agent_runtime_at(root, &state.agent_id) .map(|result| result.state) .unwrap_or_else(|_| state.clone()); let same_live_run = visible_state.run_id == state.run_id && !matches!( visible_state.phase.as_str(), "completed" | "cancelled" | "failed" ); if same_live_run { visible_state.status = "running".to_string(); visible_state.phase = "finalizing".to_string(); visible_state.current_action = "正在恢复最终回复持久化".to_string(); visible_state.waiting_on = "Agent Runtime finalization 恢复".to_string(); visible_state.next_step = "修复持久化错误后自动完成当前 run".to_string(); visible_state.error = Some(error.clone()); visible_state.updated_at = unix_timestamp(); let _ = append_game_creator_agent_runtime_task(root, &visible_state); let _ = refresh_game_creator_agent_runtime_task_queue(root, &mut visible_state); let _ = write_game_creator_agent_runtime_state(root, &visible_state); } let _ = append_game_creator_agent_runtime_event( root, &visible_state, "finalization.pending", visible_state.status.as_str(), visible_state.phase.as_str(), "最终回复已进入可恢复持久化,等待恢复完成。", Some(&error), ); let _ = append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.background_task.finalization_pending", "agentId": visible_state.agent_id.clone(), "taskId": visible_state.task_id.clone(), "sessionId": visible_state.session_id.clone(), "runId": visible_state.run_id.clone(), "source": visible_state.source.clone(), "error": error, }), ); emit_game_creator_agent_runtime_update(root, &visible_state.agent_id); } pub(crate) fn finish_game_creator_agent_background_runtime_turn_with_checkpoint_at( root: &Path, mut state: AgentRuntimeState, response: &str, response_revision: u64, observations: &[AgentRuntimeToolObservation], mut checkpoint: F, ) -> Result where F: FnMut(AgentRuntimeFinalizationCheckpoint) -> Result<(), String>, { let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( root, "runtime.background.complete", )?; if game_creator_agent_runtime_cancel_requested(root, &state) { mark_game_creator_agent_runtime_cancelled_at_locked( root, &mut state, "Agent 后台任务已按开发者请求取消", Some("取消请求在最终回复持久化前生效。"), )?; return Ok(AgentBackgroundFinalizationOutcome::Cancelled(state)); } let steer_snapshot = read_game_creator_agent_runtime_steer_ledger(root, &state.agent_id, &state.run_id)?; if steer_snapshot.entries.values().any(|entry| { entry.identity.sequence > state.applied_steer_cursor || entry.status != "applied" }) { return Ok(AgentBackgroundFinalizationOutcome::Stale( AgentRuntimeToolObservation { tool: "runtime.steer".to_string(), status: "stale".to_string(), summary: "最终回复生成期间收到运行中追加指令,旧回复已作废".to_string(), detail: Some(format!( "responseSteerCursor={} · latestSteerSequence={}", state.applied_steer_cursor, steer_snapshot .entries .values() .map(|entry| entry.identity.sequence) .max() .unwrap_or(0) )), }, )); } let current_revision = read_game_creator_agent_runtime_project_revision(root)?; let blocker = if let Some(blocker) = structured_plan_completion_blocker(&state) { Some(blocker) } else if let Some(blocker) = game_creator_agent_goal_completion_blocker_at_locked(root, &state) { Some(blocker) } else if let Some(blocker) = agent_runtime_non_verification_completion_blocker_at_locked( root, &state.agent_id, &state.run_id, ) { Some(blocker) } else if let Some(blocker) = autonomous_game_build_completion_blocker_at_locked(root, &state) { Some(blocker) } else if current_revision.revision != response_revision { Some(agent_runtime_verification_blocker( "最终回复基于的项目 revision 已过期,不能把任务标记为完成", format!( "responseRevision={response_revision}, currentRevision={};请根据最新项目状态重新规划后再生成最终回复。", current_revision.revision ), )) } else { evaluate_project_verification_completion_at_locked( root, &state.agent_id, &state.run_id, observations, )? }; if let Some(blocker) = blocker { let detail = blocker.detail.as_deref().unwrap_or_default(); let _ = append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.background_task.completion_blocked", "agentId": state.agent_id, "taskId": state.task_id, "sessionId": state.session_id, "runId": state.run_id, "source": state.source, "summary": blocker.summary, "detail": detail, }), ); return Ok(AgentBackgroundFinalizationOutcome::Stale(blocker)); } let response = redact_agent_runtime_private_process_output_from_response(response, observations); let mut journal = build_game_creator_agent_runtime_finalization_journal( root, &state, &response, response_revision, ) .map_err(|error| redact_agent_runtime_error(root, &error, 500))?; write_game_creator_agent_runtime_finalization_journal(root, &journal) .map_err(|error| redact_agent_runtime_error(root, &error, 500))?; if let Err(error) = append_game_creator_agent_runtime_finalization_lifecycle_stage( root, &journal, "prepared", journal.prepared_at, ) { let error = redact_agent_runtime_error(root, &error, 500); record_game_creator_agent_runtime_finalization_pending(root, &state, &error); return Ok(AgentBackgroundFinalizationOutcome::Pending(error)); } if let Err(error) = checkpoint(AgentRuntimeFinalizationCheckpoint::Prepared) { let error = redact_agent_runtime_error(root, &error, 500); record_game_creator_agent_runtime_finalization_pending(root, &state, &error); return Ok(AgentBackgroundFinalizationOutcome::Pending(error)); } match advance_game_creator_agent_runtime_finalization_at( root, state.clone(), &mut journal, &mut checkpoint, ) { Ok(completed) => { if let Err(error) = close_game_creator_agent_runtime_steer_ledger_at_locked(root, &completed) { let _ = append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.steer.close_failed", "agentId": completed.agent_id, "taskId": completed.task_id, "sessionId": completed.session_id, "runId": completed.run_id, "source": completed.source, "appliedSteerCursor": completed.applied_steer_cursor, "error": sanitize_agent_runtime_text(&error, 240), }), ); } Ok(AgentBackgroundFinalizationOutcome::Completed(completed)) } Err(error) => { let error = redact_agent_runtime_error(root, &error, 500); record_game_creator_agent_runtime_finalization_pending(root, &state, &error); Ok(AgentBackgroundFinalizationOutcome::Pending(error)) } } } pub(crate) fn finish_game_creator_agent_background_runtime_turn_at( root: &Path, state: AgentRuntimeState, response: &str, response_revision: u64, observations: &[AgentRuntimeToolObservation], ) -> Result { finish_game_creator_agent_background_runtime_turn_with_checkpoint_at( root, state, response, response_revision, observations, |_| Ok(()), ) } fn game_creator_agent_runtime_failure_metadata(error: &str) -> (String, usize) { ( format!("{:x}", Sha256::digest(error.as_bytes())), error.chars().count(), ) } fn game_creator_agent_runtime_public_failure_detail(agent_id: &str, error: &str) -> String { game_creator_agent_runtime_failure_conversation_message(agent_id, error) } pub(crate) fn append_game_creator_agent_background_task_failed_audit( root: &Path, state: &AgentRuntimeState, failure_kind: &'static str, ) -> Result<(), String> { let error = state .error .as_deref() .ok_or_else(|| "后台任务失败审计缺少私有错误诊断".to_string())?; let (error_sha256, error_chars) = game_creator_agent_runtime_failure_metadata(error); append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.background_task.failed", "agentId": state.agent_id, "taskId": state.task_id, "sessionId": state.session_id, "runId": state.run_id, "source": state.source, "failureKind": failure_kind, "errorSha256": error_sha256, "errorChars": error_chars, }), ) } pub(crate) fn fail_game_creator_agent_runtime_turn_at( root: &Path, mut state: AgentRuntimeState, error: &str, ) -> Result { state.pending_tool_action = None; state.status = "failed".to_string(); state.phase = "failed".to_string(); state.current_action = "等待开发者处理失败".to_string(); state.waiting_on = "开发者处理失败".to_string(); state.next_step = "等待开发者处理失败".to_string(); let public_error = redact_agent_runtime_error(root, error, 500); state.error = Some(public_error.clone()); // The public terminal message is deliberately committed before the // remaining Runtime projections. Even if a task/event/state write is the // failing subsystem, the user still receives one stable failure outcome. let public_status_result = append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error); write_non_terminal_isolated_child_cancel_tombstones_for_parent_at( root, &state.agent_id, &state.run_id, "父 Agent 任务失败,取消动态隔离子任务", )?; fail_agent_runtime_remaining_plan_steps(&mut state, &public_error); let _ = refresh_game_creator_agent_runtime_tool_policy(root, &mut state); state.updated_at = unix_timestamp(); append_game_creator_agent_runtime_task(root, &state)?; refresh_game_creator_agent_runtime_task_queue(root, &mut state)?; write_game_creator_agent_runtime_state(root, &state)?; append_game_creator_agent_runtime_event( root, &state, "error", "failed", "failed", "Agent Runtime 本轮处理失败。", state.error.as_deref(), )?; append_game_creator_agent_runtime_event( root, &state, "turn.failed", "failed", "failed", "Agent Runtime 本轮处理失败。", state.error.as_deref(), )?; remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?; publish_game_creator_agent_delegate_result_for_state(root, &state, state.error.as_deref()); if public_status_result.is_err() { append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error)?; } Ok(state) } pub(crate) fn fail_game_creator_agent_runtime_budget_at( root: &Path, mut state: AgentRuntimeState, error: &str, ) -> Result { state.pending_tool_action = None; state.status = "failed".to_string(); state.phase = "budget-exhausted".to_string(); state.current_action = "Agent loop 预算耗尽".to_string(); state.waiting_on = "开发者调整目标或重试".to_string(); state.next_step = "调整任务范围后重试".to_string(); let public_error = redact_agent_runtime_error(root, error, 500); state.error = Some(public_error.clone()); let public_status_result = append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error); write_non_terminal_isolated_child_cancel_tombstones_for_parent_at( root, &state.agent_id, &state.run_id, "父 Agent 任务预算耗尽,取消动态隔离子任务", )?; fail_agent_runtime_remaining_plan_steps(&mut state, &public_error); let _ = refresh_game_creator_agent_runtime_tool_policy(root, &mut state); state.updated_at = unix_timestamp(); append_game_creator_agent_runtime_task(root, &state)?; refresh_game_creator_agent_runtime_task_queue(root, &mut state)?; write_game_creator_agent_runtime_state(root, &state)?; append_game_creator_agent_runtime_event( root, &state, "error", "failed", "budget-exhausted", "Agent Runtime 达到 loop 预算但任务仍未收束。", state.error.as_deref(), )?; append_game_creator_agent_runtime_event( root, &state, "turn.budget_exhausted", "failed", "budget-exhausted", "Agent Runtime 达到 loop 预算但任务仍未收束。", state.error.as_deref(), )?; remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?; publish_game_creator_agent_delegate_result_for_state(root, &state, state.error.as_deref()); if public_status_result.is_err() { append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error)?; } Ok(state) } pub(crate) fn normalize_game_creator_runtime_agent_id(agent_id: &str) -> Result { let agent_id = agent_id.trim(); if agent_id.is_empty() { return Err("Agent ID 不能为空".to_string()); } if game_creator_runtime_agent_catalog()? .get(agent_id) .is_some() { return Ok(agent_id.to_string()); } for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { for role in group.roles { if game_creator_agent_role_alias_id(group.id, role.role) == agent_id { return Ok(role.task_id.to_string()); } } } if agent_id.starts_with("child-") && agent_id.len() <= 96 && normalize_conversation_agent_id(agent_id).is_ok() { return Ok(agent_id.to_string()); } Err(format!("未知 Agent:{agent_id}")) } pub(crate) fn game_creator_runtime_template_agent_id_at( root: &Path, agent_id: &str, ) -> Result { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; if agent_id.starts_with("child-") { return resolve_isolated_agent_instance_at(root, &agent_id) .map(|instance| instance.template_agent_id); } Ok(agent_id) } pub(super) fn game_creator_agent_role_alias_id(group: &str, role: &str) -> String { format!("{group}-{role}") .to_lowercase() .chars() .map(|character| { if character.is_ascii_alphanumeric() || character == '_' || character == '-' { character } else { '-' } }) .collect() } pub(crate) fn normalize_game_creator_agent_runtime_run_id(agent_id: &str, run_id: &str) -> String { let run_id = run_id.trim(); if run_id.is_empty() { format!("agent-runtime-{agent_id}-{}", unix_timestamp()) } else { truncate_agent_runtime_text(run_id, 160) } } pub(super) fn unique_game_creator_agent_runtime_run_id( root: &Path, agent_id: &str, run_id: &str, ) -> Result { let base_run_id = normalize_game_creator_agent_runtime_run_id(agent_id, run_id); let path = game_creator_agent_runtime_task_path(root, agent_id); let records = read_all_game_creator_agent_runtime_tasks(&path)?; let used_run_ids = records .into_iter() .map(|record| record.run_id) .collect::>(); if !used_run_ids.contains(&base_run_id) { return Ok(base_run_id); } let prefix = truncate_agent_runtime_text(&base_run_id, 118); for attempt in 0..100 { let candidate = format!("{prefix}-dup-{}-{}", unix_timestamp_nanos(), attempt); if !used_run_ids.contains(&candidate) { return Ok(candidate); } } Err(format!("无法生成唯一 Agent Runtime runId:{base_run_id}")) } pub(crate) fn default_game_creator_agent_runtime_state( agent_id: &str, run_id: &str, ) -> AgentRuntimeState { AgentRuntimeState { schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: agent_id.to_string(), task_id: agent_id.to_string(), session_id: format!("agent-session-{agent_id}"), run_id: run_id.to_string(), source: "agent-chat".to_string(), run_profile: default_agent_runtime_run_profile(), run_profile_binding_fingerprint: String::new(), parent_agent_id: None, parent_run_id: None, delegation_id: None, status: "idle".to_string(), phase: "idle".to_string(), current_task: String::new(), current_goal: String::new(), goal_id: None, goal_revision: 0, goal_status: None, goal_outcome: None, goal_constraints: Vec::new(), goal_verification: Vec::new(), current_action: "等待输入".to_string(), waiting_on: "开发者输入".to_string(), next_step: "等待输入".to_string(), loop_iteration: 0, max_loop_iterations: AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT as u32, tool_action_budget: AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT as u32, plan_revision: 0, plan_explanation: String::new(), plan: vec![ "读取项目上下文".to_string(), "按角色职责推理".to_string(), "回复并记录 runtime 事件".to_string(), ], plan_steps: Vec::new(), active_plan_step_index: None, observations: Vec::new(), recent_tool_calls: Vec::new(), pending_tool_action: None, task_queue: AgentRuntimeTaskQueueSummary::default(), allowed_tools: default_game_creator_agent_runtime_allowed_tools(), tool_policy: AgentRuntimeToolPolicySnapshot::default(), applied_steer_cursor: 0, applied_steer_refs: Vec::new(), queued_steer_count: 0, context_usage: AgentRuntimeContextUsage::default(), last_response: None, error: None, started_at: 0, updated_at: unix_timestamp(), } } pub(crate) fn agent_runtime_state_from_task_record( record: &AgentRuntimeTaskRecord, ) -> AgentRuntimeState { let mut state = default_game_creator_agent_runtime_state(&record.agent_id, &record.run_id); state.task_id = record.task_id.clone(); state.session_id = record.session_id.clone(); state.source = record.source.clone(); state.run_profile = record.run_profile.clone(); state.run_profile_binding_fingerprint = record.run_profile_binding_fingerprint.clone(); state.parent_agent_id = record.parent_agent_id.clone(); state.parent_run_id = record.parent_run_id.clone(); state.delegation_id = record.delegation_id.clone(); state.goal_id = record.goal_id.clone(); state.goal_revision = record.goal_revision; state.goal_status = record.goal_status.clone(); state.status = record.status.clone(); state.phase = record.phase.clone(); state.current_task = record.task.clone(); state.current_goal = record.task.clone(); state.current_action = record.current_action.clone(); state.waiting_on = agent_runtime_waiting_on_for_phase(&record.phase).to_string(); state.next_step = agent_runtime_next_step_for_phase(&record.phase).to_string(); state.error = record.error.clone(); state.updated_at = record.updated_at; state } pub(crate) fn default_game_creator_agent_runtime_allowed_tools() -> Vec { agent_runtime_executable_tools() .into_iter() .map(str::to_string) .collect() } pub(super) fn normalize_game_creator_agent_runtime_state( state: &mut AgentRuntimeState, agent_id: &str, ) { if state.schema_version.trim().is_empty() { state.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string(); } if state.agent_id.trim().is_empty() { state.agent_id = agent_id.to_string(); } if state.task_id.trim().is_empty() { state.task_id = agent_id.to_string(); } if state.session_id.trim().is_empty() { state.session_id = format!("agent-session-{agent_id}"); } if state.source.trim().is_empty() { state.source = "agent-chat".to_string(); } if state.status.trim().is_empty() { state.status = "idle".to_string(); } if state.phase.trim().is_empty() { state.phase = "idle".to_string(); } if state.current_goal.trim().is_empty() && !state.current_task.trim().is_empty() { state.current_goal = state.current_task.clone(); } if state.current_action.trim().is_empty() { state.current_action = "等待输入".to_string(); } if state.waiting_on.trim().is_empty() { state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string(); } if state.next_step.trim().is_empty() { state.next_step = agent_runtime_next_step_for_phase(&state.phase).to_string(); } if state.max_loop_iterations == 0 { state.max_loop_iterations = AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT as u32; } if state.tool_action_budget == 0 { state.tool_action_budget = AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT as u32; } state.plan_explanation = if agent_runtime_has_structured_plan(state) { sanitize_agent_runtime_text(&state.plan_explanation, 240) } else { String::new() }; if state.plan.is_empty() && !agent_runtime_has_structured_plan(state) { state.plan = vec![ "读取项目上下文".to_string(), "按角色职责推理".to_string(), "回复并记录 runtime 事件".to_string(), ]; } if state.plan_steps.is_empty() && !state.plan.is_empty() && !agent_runtime_has_structured_plan(state) { state.plan_steps = state .plan .iter() .filter(|item| !item.trim().is_empty()) .take(AGENT_RUNTIME_PLAN_STEP_LIMIT) .enumerate() .map(|(index, title)| AgentRuntimePlanStep { index: index as u32, title: sanitize_agent_runtime_text(title, 180), status: if state.phase == "completed" { "completed" } else if index == 0 && state.phase != "idle" { "active" } else { "pending" } .to_string(), detail: None, updated_at: state.updated_at, }) .collect(); } if !agent_runtime_has_structured_plan(state) && state.plan_steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT { state.plan_steps.truncate(AGENT_RUNTIME_PLAN_STEP_LIMIT); } if !agent_runtime_has_structured_plan(state) { if let Some(active_index) = state.active_plan_step_index { if !state .plan_steps .iter() .any(|step| step.index == active_index) { state.active_plan_step_index = None; } } } else if let Err(error) = validate_agent_runtime_structured_plan_snapshot( state.plan_revision, &state.plan_explanation, &state.plan, &state.plan_steps, state.active_plan_step_index, ) { state.status = "needs-reconciliation".to_string(); state.phase = "needs-reconciliation".to_string(); state.current_action = "结构化计划状态损坏".to_string(); state.waiting_on = "人工核对当前 run 的计划快照".to_string(); state.next_step = "修复计划快照后恢复当前 run".to_string(); state.error = Some(sanitize_agent_runtime_text(&error, 500)); } if state.allowed_tools.is_empty() { state.allowed_tools = default_game_creator_agent_runtime_allowed_tools(); } else { for tool in default_game_creator_agent_runtime_allowed_tools() { if !state.allowed_tools.iter().any(|existing| existing == &tool) { state.allowed_tools.push(tool); } } } if state.updated_at == 0 { state.updated_at = unix_timestamp(); } if state.tool_policy.allowed_tools.is_empty() { state.tool_policy.allowed_tools = agent_runtime_executable_tools() .into_iter() .map(str::to_string) .collect(); } if state.recent_tool_calls.len() > AGENT_RUNTIME_RECENT_TOOL_CALL_LIMIT { let keep_from = state .recent_tool_calls .len() .saturating_sub(AGENT_RUNTIME_RECENT_TOOL_CALL_LIMIT); state.recent_tool_calls = state.recent_tool_calls.split_off(keep_from); } } pub(super) fn agent_runtime_next_step_for_phase(phase: &str) -> &'static str { match phase.trim() { "planning" => "等待 Agent 输出计划或回复", "action" => "等待工具观察结果", "waiting-for-confirmation" => "等待开发者确认工具动作", "waiting-for-user-input" => "等待用户提交全部澄清回答", "waiting-for-provider-retry" => "等待持久退避到期后恢复同一 Provider 请求", "response" => "等待 Agent 整理最终回复", "completed" | "idle" => "等待下一轮输入", "cancelled" => "可重试该后台任务或提交新任务", "paused" | "pausing" => "等待开发者恢复持久 Goal", "failed" => "等待开发者处理失败", _ => "继续推进当前任务", } } pub(super) fn agent_runtime_waiting_on_for_phase(phase: &str) -> &'static str { match phase.trim() { "planning" => "Agent 输出计划或回复", "llm" => "Agent LLM 回复", "action" => "工具观察结果", "waiting-for-confirmation" => "开发者确认 Agent 工具动作", "waiting-for-user-input" => "用户回答 Agent 的澄清问题", "waiting-for-provider-retry" => "Provider 瞬态重试退避到期", "response" => "Agent 整理最终回复", "completed" | "idle" => "开发者下一轮输入", "cancelled" => "开发者下一轮输入", "paused" | "pausing" => "开发者恢复持久 Goal", "failed" => "开发者处理失败", _ => "当前任务推进", } } pub(super) fn normalize_game_creator_agent_runtime_event(event: &mut AgentRuntimeEvent) { if event.schema_version.trim().is_empty() { event.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string(); } if event.agent_id.trim().is_empty() { event.agent_id = event.task_id.clone(); } if event.task_id.trim().is_empty() { event.task_id = event.agent_id.clone(); } if event.session_id.trim().is_empty() { event.session_id = format!("agent-session-{}", event.agent_id); } if event.source.trim().is_empty() { event.source = "agent-chat".to_string(); } if event.event_type.trim().is_empty() { event.event_type = "turn.event".to_string(); } if event.status.trim().is_empty() { event.status = "idle".to_string(); } if event.phase.trim().is_empty() { event.phase = "idle".to_string(); } if event.updated_at == 0 { event.updated_at = unix_timestamp(); } } pub(super) fn normalize_game_creator_agent_runtime_task(record: &mut AgentRuntimeTaskRecord) { if record.schema_version.trim().is_empty() { record.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string(); } if record.agent_id.trim().is_empty() { record.agent_id = record.task_id.clone(); } if record.task_id.trim().is_empty() { record.task_id = record.agent_id.clone(); } if record.session_id.trim().is_empty() { record.session_id = format!("agent-session-{}", record.agent_id); } if record.run_id.trim().is_empty() { record.run_id = format!("agent-runtime-{}-{}", record.agent_id, record.updated_at); } if record.source.trim().is_empty() { record.source = "agent-chat".to_string(); } if record.run_profile.trim().is_empty() { record.run_profile = default_agent_runtime_run_profile(); } if record.status.trim().is_empty() { record.status = "idle".to_string(); } if record.phase == "completed" && record.status == "idle" { record.status = "completed".to_string(); } if record.phase == "failed" && record.status != "failed" { record.status = "failed".to_string(); } if record.phase.trim().is_empty() { record.phase = "idle".to_string(); } if record.current_action.trim().is_empty() { record.current_action = "等待输入".to_string(); } if record.updated_at == 0 { record.updated_at = unix_timestamp(); } } pub(super) fn validate_game_creator_agent_runtime_task_goal_binding( record: &AgentRuntimeTaskRecord, ) -> Result<(), String> { match record.goal_id.as_deref() { None if record.goal_revision == 0 && record.goal_status.is_none() => Ok(()), Some(goal_id) if !goal_id.trim().is_empty() && record.goal_revision > 0 && record .goal_status .as_deref() .is_some_and(agent_goal_status_is_valid) => { Ok(()) } _ => Err(format!( "Agent Runtime task Goal 绑定无效:runId={}", record.run_id )), } } pub(super) fn game_creator_agent_runtime_session_path(root: &Path, agent_id: &str) -> PathBuf { root.join(".agent") .join("runtime") .join("agents") .join(format!("{agent_id}.json")) } pub(crate) fn game_creator_agent_runtime_event_path(root: &Path, agent_id: &str) -> PathBuf { root.join(".agent") .join("runtime") .join("events") .join(format!("{agent_id}.jsonl")) } pub(crate) fn game_creator_agent_runtime_task_path(root: &Path, agent_id: &str) -> PathBuf { root.join(".agent") .join("runtime") .join("tasks") .join(format!("{agent_id}.jsonl")) } pub(super) fn game_creator_agent_runtime_cancel_path( root: &Path, agent_id: &str, run_id: &str, ) -> PathBuf { root.join(".agent") .join("runtime") .join("cancel") .join(agent_id) .join(format!("{run_id}.json")) } pub(crate) fn write_game_creator_agent_runtime_cancel_request( root: &Path, agent_id: &str, run_id: &str, reason: &str, ) -> Result<(), String> { let path = game_creator_agent_runtime_cancel_path(root, agent_id, run_id); if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|error| { format!( "创建 Agent Runtime 取消目录失败:{}: {error}", parent.display() ) })?; } let payload = serde_json::json!({ "agentId": agent_id, "runId": run_id, "reason": sanitize_agent_runtime_text(reason, 180), "updatedAt": unix_timestamp(), }); let content = serde_json::to_string_pretty(&payload) .map_err(|error| format!("序列化 Agent Runtime 取消请求失败:{error}"))?; fs::write(&path, format!("{content}\n")).map_err(|error| { format!( "写入 Agent Runtime 取消请求失败:{}: {error}", path.display() ) }) } pub(super) fn write_non_terminal_isolated_child_cancel_tombstones_for_parent_at( root: &Path, parent_agent_id: &str, parent_run_id: &str, reason: &str, ) -> Result<(), String> { for child in list_non_terminal_isolated_children_for_parent_cancel_at( root, parent_agent_id, parent_run_id, )? { write_game_creator_agent_runtime_cancel_request( root, &child.instance_id, &child.run_id, reason, )?; } Ok(()) } pub(crate) fn remove_game_creator_agent_runtime_cancel_request( root: &Path, agent_id: &str, run_id: &str, ) { let _ = fs::remove_file(game_creator_agent_runtime_cancel_path( root, agent_id, run_id, )); } pub(crate) fn game_creator_agent_runtime_cancel_requested( root: &Path, state: &AgentRuntimeState, ) -> bool { game_creator_agent_runtime_cancel_requested_for(root, &state.agent_id, &state.run_id) } pub(super) fn game_creator_agent_runtime_cancel_requested_for( root: &Path, agent_id: &str, run_id: &str, ) -> bool { if run_id.trim().is_empty() { return false; } game_creator_agent_runtime_cancel_path(root, agent_id, run_id).exists() } #[derive(Debug)] pub(crate) struct AgentRuntimeTaskLock { pub(super) file: Option, } #[cfg(unix)] static AGENT_RUNTIME_LOCK_OPEN_GUARD: OnceLock> = OnceLock::new(); impl Drop for AgentRuntimeTaskLock { fn drop(&mut self) { self.file.take(); } } pub(crate) fn try_acquire_game_creator_agent_runtime_task_lock( root: &Path, agent_id: &str, ) -> Result, String> { let relative_path = format!(".agent/runtime/locks/{agent_id}.lock"); let path = root.join(&relative_path); let Some(mut file) = try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)? else { return Ok(None); }; let token = format!("{}-{}", std::process::id(), unix_timestamp_nanos()); let payload = serde_json::json!({ "agentId": agent_id, "pid": std::process::id(), "token": token.clone(), "createdAt": unix_timestamp(), }); let content = serde_json::to_string_pretty(&payload) .map_err(|error| format!("生成 Agent Runtime 锁失败:{error}"))?; file.set_len(0) .and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ())) .and_then(|_| file.write_all(content.as_bytes())) .and_then(|_| file.sync_data()) .map_err(|error| format!("写入 Agent Runtime 锁失败:{}: {error}", path.display()))?; Ok(Some(AgentRuntimeTaskLock { file: Some(file) })) } pub(super) fn try_acquire_game_creator_agent_delegation_lock( root: &Path, delegation_id: &str, purpose: &str, ) -> Result, String> { let lock_id = agent_runtime_confirmation_path_component(delegation_id, "delegation"); let purpose = agent_runtime_confirmation_path_component(purpose, "lock"); let relative_path = format!(".agent/runtime/locks/delegations/{purpose}/{lock_id}.lock"); let path = root.join(&relative_path); let Some(mut file) = try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)? else { return Ok(None); }; let payload = serde_json::json!({ "delegationId": delegation_id, "pid": std::process::id(), "createdAt": unix_timestamp(), }); let content = serde_json::to_string_pretty(&payload) .map_err(|error| format!("生成 Agent 委派回执锁失败:{error}"))?; file.set_len(0) .and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ())) .and_then(|_| file.write_all(content.as_bytes())) .and_then(|_| file.sync_data()) .map_err(|error| format!("写入 Agent 委派回执锁失败:{}: {error}", path.display()))?; Ok(Some(AgentRuntimeTaskLock { file: Some(file) })) } pub(crate) fn try_acquire_game_creator_agent_delegation_lock_with_wait( root: &Path, delegation_id: &str, purpose: &str, ) -> Result, String> { for attempt in 0..100 { if let Some(lock) = try_acquire_game_creator_agent_delegation_lock(root, delegation_id, purpose)? { return Ok(Some(lock)); } if attempt < 99 { std::thread::sleep(Duration::from_millis(10)); } } Ok(None) } pub(super) fn acquire_game_creator_agent_runtime_task_journal_lock( root: &Path, agent_id: &str, ) -> Result { for attempt in 0..100 { if let Some(lock) = try_acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)? { return Ok(lock); } if attempt < 99 { std::thread::sleep(Duration::from_millis(10)); } } Err(format!( "Agent Runtime 任务账本正被其他进程写入:{agent_id}" )) } pub(super) fn try_acquire_game_creator_agent_runtime_task_journal_lock( root: &Path, agent_id: &str, ) -> Result, String> { let agent_id = agent_runtime_confirmation_path_component(agent_id, "agent"); let relative_path = format!(".agent/runtime/locks/task-journals/{agent_id}.lock"); let path = root.join(&relative_path); let Some(mut file) = try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)? else { return Ok(None); }; let payload = serde_json::json!({ "agentId": agent_id, "pid": std::process::id(), "createdAt": unix_timestamp(), }); let content = serde_json::to_string_pretty(&payload) .map_err(|error| format!("生成 Agent Runtime 任务账本锁失败:{error}"))?; file.set_len(0) .and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ())) .and_then(|_| file.write_all(content.as_bytes())) .and_then(|_| file.sync_data()) .map_err(|error| { format!( "写入 Agent Runtime 任务账本锁失败:{}: {error}", path.display() ) })?; Ok(Some(AgentRuntimeTaskLock { file: Some(file) })) } pub(super) fn acquire_game_creator_agent_runtime_task_lock_with_wait( root: &Path, agent_id: &str, ) -> Result { try_acquire_game_creator_agent_runtime_task_lock_with_wait(root, agent_id)? .ok_or_else(|| format!("Agent Runtime 正在执行该 Agent 的其他任务:{agent_id}")) } pub(super) fn try_acquire_game_creator_agent_runtime_task_lock_with_wait( root: &Path, agent_id: &str, ) -> Result, String> { for attempt in 0..25 { if let Some(runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock(root, agent_id)? { return Ok(Some(runtime_lock)); } if attempt < 24 { std::thread::sleep(Duration::from_millis(10)); } } Ok(None) } #[cfg(unix)] pub(super) fn try_open_game_creator_agent_runtime_task_lock_file( root: &Path, relative_path: &str, ) -> Result, String> { use std::ffi::CString; use std::os::fd::{AsRawFd, FromRawFd}; use std::os::unix::fs::{MetadataExt, OpenOptionsExt}; // macOS 上两个线程首次并发创建同一套 mkdirat/openat 锁目录时,loser // 可能在最终 O_CREAT 前短暂观察到 ENOENT。进程内只串行化安全打开阶段; // 返回后的 flock 仍负责真实的跨线程、跨进程互斥。 let _open_guard = AGENT_RUNTIME_LOCK_OPEN_GUARD .get_or_init(|| Mutex::new(())) .lock() .map_err(|_| "Agent Runtime 锁安全打开门禁已损坏".to_string())?; validate_project_root(root)?; let relative_path = normalize_relative_path(relative_path)?; let path = root.join(&relative_path); let mut components = relative_path.split('/').collect::>(); let file_name = components .pop() .ok_or_else(|| "Agent Runtime 锁路径缺少文件名".to_string())?; let mut directory = fs::OpenOptions::new() .read(true) .custom_flags(libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW) .open(root) .map_err(|error| format!("安全打开项目目录失败:{}: {error}", root.display()))?; for component in components { let component = CString::new(component).map_err(|_| "Agent Runtime 锁目录包含 NUL".to_string())?; // SAFETY: `directory` is a live directory fd and `component` is NUL terminated. let created = unsafe { libc::mkdirat(directory.as_raw_fd(), component.as_ptr(), 0o700) }; if created != 0 { let error = std::io::Error::last_os_error(); if error.kind() != std::io::ErrorKind::AlreadyExists { return Err(format!( "创建 Agent Runtime 锁目录失败:{}: {error}", path.display() )); } } // SAFETY: `directory` and `component` remain valid for the duration of openat. let fd = unsafe { libc::openat( directory.as_raw_fd(), component.as_ptr(), libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW, ) }; if fd < 0 { return Err(format!( "安全打开 Agent Runtime 锁目录失败:{}: {}", path.display(), std::io::Error::last_os_error() )); } // SAFETY: openat returned a new owned fd. directory = unsafe { File::from_raw_fd(fd) }; } let file_name = CString::new(file_name).map_err(|_| "Agent Runtime 锁文件名包含 NUL".to_string())?; // SAFETY: `directory` is a live directory fd and `file_name` is NUL terminated. let fd = unsafe { libc::openat( directory.as_raw_fd(), file_name.as_ptr(), libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW, 0o600, ) }; if fd < 0 { return Err(format!( "安全打开 Agent Runtime 锁失败:{}: {}", path.display(), std::io::Error::last_os_error() )); } // SAFETY: openat returned a new owned fd. let file = unsafe { File::from_raw_fd(fd) }; let metadata = file.metadata().map_err(|error| { format!( "读取 Agent Runtime 锁元数据失败:{}: {error}", path.display() ) })?; if !metadata.is_file() || metadata.nlink() != 1 { return Err(format!( "Agent Runtime 锁必须是无硬链接的普通文件:{}", path.display() )); } // SAFETY: flock only observes the valid fd owned by `file`; `file` remains alive on success. let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) }; if result == 0 { return Ok(Some(file)); } let error = std::io::Error::last_os_error(); if error.kind() == std::io::ErrorKind::WouldBlock { Ok(None) } else { Err(format!( "获取 Agent Runtime 系统文件锁失败:{}: {error}", path.display() )) } } #[cfg(windows)] pub(super) fn try_open_game_creator_agent_runtime_task_lock_file( root: &Path, relative_path: &str, ) -> Result, String> { use std::os::windows::fs::{MetadataExt, OpenOptionsExt}; const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400; const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000; const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000; const FILE_SHARE_READ_WRITE: u32 = 0x0000_0003; const ERROR_SHARING_VIOLATION: i32 = 32; const ERROR_LOCK_VIOLATION: i32 = 33; validate_project_root(root)?; let relative_path = normalize_relative_path(relative_path)?; let path = root.join(&relative_path); let mut components = relative_path.split('/').collect::>(); components .pop() .ok_or_else(|| "Agent Runtime 锁路径缺少文件名".to_string())?; let mut guarded_directories = Vec::with_capacity(components.len() + 1); let mut current = root.to_path_buf(); for component in std::iter::once(None).chain(components.into_iter().map(Some)) { if let Some(component) = component { current.push(component); if !current.exists() { fs::create_dir(¤t).map_err(|error| { format!( "创建 Agent Runtime 锁目录失败:{}: {error}", current.display() ) })?; } } let metadata = fs::symlink_metadata(¤t).map_err(|error| { format!( "读取 Agent Runtime 锁目录元数据失败:{}: {error}", current.display() ) })?; if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { return Err(format!( "Agent Runtime 锁目录必须是普通目录且不能是 reparse point:{}", current.display() )); } let handle = fs::OpenOptions::new() .read(true) .share_mode(FILE_SHARE_READ_WRITE) .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT) .open(¤t) .map_err(|error| { format!( "安全打开 Agent Runtime 锁目录失败:{}: {error}", current.display() ) })?; let opened = handle.metadata().map_err(|error| { format!( "复核 Agent Runtime 锁目录失败:{}: {error}", current.display() ) })?; if !opened.is_dir() || opened.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { return Err(format!( "Agent Runtime 锁目录打开后身份无效:{}", current.display() )); } guarded_directories.push(handle); } if let Ok(metadata) = fs::symlink_metadata(&path) { if metadata.file_type().is_symlink() || !metadata.is_file() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 { return Err(format!( "Agent Runtime 锁必须是普通文件且不能是重解析点:{}", path.display() )); } } match fs::OpenOptions::new() .create(true) .read(true) .write(true) .share_mode(0) .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT) .open(&path) { Ok(file) => { let metadata = file.metadata().map_err(|error| { format!( "读取 Agent Runtime 锁元数据失败:{}: {error}", path.display() ) })?; if !metadata.is_file() || metadata.file_type().is_symlink() { return Err(format!( "Agent Runtime 锁必须是无硬链接的普通文件且不能是重解析点:{}", path.display() )); } validate_windows_regular_file_handle(&file, "Agent Runtime 锁")?; Ok(Some(file)) } Err(error) if matches!( error.kind(), std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::WouldBlock ) || matches!( error.raw_os_error(), Some(ERROR_SHARING_VIOLATION | ERROR_LOCK_VIOLATION) ) => { Ok(None) } Err(error) => Err(format!( "获取 Agent Runtime 系统文件锁失败:{}: {error}", path.display() )), } } #[cfg(not(any(unix, windows)))] pub(super) fn try_open_game_creator_agent_runtime_task_lock_file( root: &Path, relative_path: &str, ) -> Result, String> { Err(format!( "当前平台不支持 Agent Runtime 系统文件锁:{}", root.join(relative_path).display() )) } pub(crate) fn game_creator_agent_runtime_task_lock_is_available( root: &Path, agent_id: &str, ) -> Result { let relative_path = format!(".agent/runtime/locks/{agent_id}.lock"); Ok(try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?.is_some()) } #[derive(Debug)] pub(crate) struct AgentRuntimeTaskLockStatus { pub(crate) is_stale: bool, pub(crate) belongs_to_previous_process: bool, } pub(crate) fn read_game_creator_agent_runtime_lock_status( path: &Path, ) -> AgentRuntimeTaskLockStatus { let Ok(content) = fs::read_to_string(path) else { return AgentRuntimeTaskLockStatus { is_stale: true, belongs_to_previous_process: true, }; }; let Ok(value) = serde_json::from_str::(&content) else { return AgentRuntimeTaskLockStatus { is_stale: true, belongs_to_previous_process: true, }; }; let pid = value.get("pid").and_then(serde_json::Value::as_u64); let created_at = value .get("createdAt") .and_then(serde_json::Value::as_u64) .unwrap_or(0); let is_stale = created_at == 0 || unix_timestamp().saturating_sub(created_at) > AGENT_RUNTIME_LOCK_STALE_AFTER_SECONDS; let belongs_to_previous_process = pid .map(|pid| pid != u64::from(std::process::id())) .unwrap_or(true); AgentRuntimeTaskLockStatus { is_stale, belongs_to_previous_process, } } pub(crate) fn write_game_creator_agent_runtime_state( root: &Path, state: &AgentRuntimeState, ) -> Result<(), String> { if !state.run_id.trim().is_empty() { let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at( root, &state.agent_id, &state.run_id, Some(&state.run_profile), Some(&state.run_profile_binding_fingerprint), )?; if run_profile != state.run_profile || binding_fingerprint != state.run_profile_binding_fingerprint { return Err("Agent Runtime 状态 Run Profile 绑定不匹配".to_string()); } } let path = game_creator_agent_runtime_session_path(root, &state.agent_id); if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|error| { format!( "创建 Agent Runtime 状态目录失败:{}: {error}", parent.display() ) })?; } let content = serde_json::to_string_pretty(state) .map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?; let temp_path = path.with_file_name(format!( ".{}.tmp.{}.{}", path.file_name() .and_then(|value| value.to_str()) .unwrap_or("runtime.json"), std::process::id(), unix_timestamp_nanos() )); fs::write(&temp_path, format!("{content}\n")).map_err(|error| { format!( "写入 Agent Runtime 临时状态失败:{}: {error}", temp_path.display() ) })?; fs::rename(&temp_path, &path).map_err(|error| { let _ = fs::remove_file(&temp_path); format!( "替换 Agent Runtime 状态失败:{} -> {}: {error}", temp_path.display(), path.display() ) }) } pub(super) fn append_game_creator_agent_runtime_event( root: &Path, state: &AgentRuntimeState, event_type: &str, status: &str, phase: &str, summary: &str, detail: Option<&str>, ) -> Result<(), String> { append_game_creator_agent_runtime_event_with_action( root, state, event_type, status, phase, summary, detail, None, ) } pub(crate) fn append_game_creator_agent_runtime_action_event( root: &Path, state: &AgentRuntimeState, event_type: &str, status: &str, phase: &str, summary: &str, detail: Option<&str>, action_id: &str, ) -> Result<(), String> { append_game_creator_agent_runtime_event_with_action( root, state, event_type, status, phase, summary, detail, Some(action_id), ) } pub(super) fn append_game_creator_agent_runtime_event_with_action( root: &Path, state: &AgentRuntimeState, event_type: &str, status: &str, phase: &str, summary: &str, detail: Option<&str>, action_id: Option<&str>, ) -> Result<(), String> { let path = game_creator_agent_runtime_event_path(root, &state.agent_id); if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|error| { format!( "创建 Agent Runtime 事件目录失败:{}: {error}", parent.display() ) })?; } let event = AgentRuntimeEvent { schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: state.agent_id.clone(), task_id: state.task_id.clone(), session_id: state.session_id.clone(), run_id: state.run_id.clone(), source: state.source.clone(), event_type: event_type.to_string(), event_id: new_game_creator_agent_runtime_event_id(state, event_type, phase, action_id), action_id: action_id.map(ToString::to_string), status: status.to_string(), phase: phase.to_string(), summary: summary.to_string(), public_text: game_creator_agent_runtime_public_event_text(root, event_type, summary), detail: detail .filter(|_| { !(event_type == "observation" && (summary.starts_with("command.exec:") || summary.starts_with("command.output_read:"))) }) .map(|value| { if matches!( event_type, "error" | "turn.failed" | "turn.budget_exhausted" ) { return game_creator_agent_runtime_public_failure_detail( &state.agent_id, value, ); } let max_chars = if event_type == "observation" && summary.starts_with("agent.action_history:") { AGENT_RUNTIME_FILE_CONTEXT_MAX_CHARS } else { 500 }; sanitize_agent_runtime_text(value, max_chars) }), updated_at: unix_timestamp(), }; if let Some(action_id) = action_id { if let Some(existing) = read_recent_game_creator_agent_runtime_events(&path)? .into_iter() .rev() .find(|candidate| { candidate.run_id == event.run_id && candidate.event_type == event.event_type && candidate.action_id.as_deref() == Some(action_id) && candidate.phase == event.phase }) { let legacy_detail_is_stricter_projection_compatible = event.event_type == "observation" && event.detail.is_none() && existing.detail.is_some(); if existing.agent_id != event.agent_id || existing.task_id != event.task_id || existing.session_id != event.session_id || existing.run_id != event.run_id || existing.source != event.source || existing.status != event.status || existing.phase != event.phase || existing.summary != event.summary || (existing.detail != event.detail && !legacy_detail_is_stricter_projection_compatible) { return Err(format!( "Agent Runtime action event 幂等身份冲突:actionId={action_id}" )); } return Ok(()); } } let line = serde_json::to_string(&event) .map_err(|error| format!("序列化 Agent Runtime 事件失败:{error}"))?; append_jsonl_line(&path, &line, "Agent Runtime 事件")?; emit_game_creator_agent_runtime_update(root, &state.agent_id); Ok(()) } pub(crate) fn append_game_creator_agent_runtime_task( root: &Path, state: &AgentRuntimeState, ) -> Result<(), String> { append_game_creator_agent_runtime_task_record(root, &agent_runtime_task_record(state)) } pub(crate) fn append_game_creator_agent_runtime_task_projection_once( root: &Path, state: &AgentRuntimeState, action_id: &str, ) -> Result<(), String> { let record = agent_runtime_task_record(state); let mut projection = serde_json::to_value(&record) .map_err(|error| format!("序列化 Agent Runtime action task 失败:{error}"))?; projection .as_object_mut() .ok_or_else(|| "Agent Runtime action task 必须是 JSON object".to_string())? .insert( "actionId".to_string(), serde_json::Value::String(action_id.to_string()), ); let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, &record.agent_id)?; let path = game_creator_agent_runtime_task_path(root, &record.agent_id); match File::open(&path) { Ok(file) => { for line in BufReader::new(file).lines() { let line = line.map_err(|error| { format!( "读取 Agent Runtime action task 失败:{}: {error}", path.display() ) })?; let existing = match serde_json::from_str::(line.trim()) { Ok(existing) => existing, Err(_) if line.trim().is_empty() => continue, Err(error) => { return Err(format!( "解析 Agent Runtime action task 失败:{}: {error}", path.display() )); } }; if existing.get("runId") != projection.get("runId") || existing.get("actionId") != projection.get("actionId") || existing.get("phase") != projection.get("phase") { continue; } for field in [ "agentId", "taskId", "sessionId", "runId", "actionId", "source", "status", "phase", "currentAction", ] { if existing.get(field) != projection.get(field) { return Err(format!( "Agent Runtime action task 幂等身份冲突:actionId={action_id} field={field}" )); } } return Ok(()); } } Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} Err(error) => { return Err(format!( "读取 Agent Runtime action task 失败:{}: {error}", path.display() )); } } let line = serde_json::to_string(&projection) .map_err(|error| format!("序列化 Agent Runtime action task 失败:{error}"))?; append_jsonl_line(&path, &line, "Agent Runtime 任务") } pub(super) fn agent_runtime_task_record(state: &AgentRuntimeState) -> AgentRuntimeTaskRecord { AgentRuntimeTaskRecord { goal_id: state.goal_id.clone(), goal_revision: state.goal_revision, goal_status: state.goal_status.clone(), schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: state.agent_id.clone(), task_id: state.task_id.clone(), session_id: state.session_id.clone(), run_id: state.run_id.clone(), source: state.source.clone(), run_profile: state.run_profile.clone(), run_profile_binding_fingerprint: state.run_profile_binding_fingerprint.clone(), parent_agent_id: state.parent_agent_id.clone(), parent_run_id: state.parent_run_id.clone(), delegation_id: state.delegation_id.clone(), task: state.current_task.clone(), status: game_creator_agent_runtime_task_status(state), phase: state.phase.clone(), current_action: state.current_action.clone(), terminal_detail: agent_runtime_terminal_detail(state), error: state.error.clone(), updated_at: state.updated_at, } } pub(super) fn append_game_creator_agent_runtime_cancelled_task_record( root: &Path, record: &AgentRuntimeTaskRecord, current_action: &str, ) -> Result { let cancelled = AgentRuntimeTaskRecord { goal_id: record.goal_id.clone(), goal_revision: record.goal_revision, goal_status: record.goal_status.clone(), schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: record.agent_id.clone(), task_id: record.task_id.clone(), session_id: record.session_id.clone(), run_id: record.run_id.clone(), source: record.source.clone(), run_profile: record.run_profile.clone(), run_profile_binding_fingerprint: record.run_profile_binding_fingerprint.clone(), parent_agent_id: record.parent_agent_id.clone(), parent_run_id: record.parent_run_id.clone(), delegation_id: record.delegation_id.clone(), task: record.task.clone(), status: "cancelled".to_string(), phase: "cancelled".to_string(), current_action: current_action.to_string(), terminal_detail: Some(sanitize_agent_runtime_text(current_action, 500)), error: None, updated_at: unix_timestamp(), }; append_game_creator_agent_runtime_task_record(root, &cancelled)?; Ok(cancelled) } pub(crate) fn mark_game_creator_agent_runtime_cancelled_at( root: &Path, state: &mut AgentRuntimeState, summary: &str, detail: Option<&str>, ) -> Result<(), String> { let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( root, "runtime.cancel.finalize", )?; mark_game_creator_agent_runtime_cancelled_at_locked(root, state, summary, detail) } pub(super) fn mark_game_creator_agent_runtime_cancelled_at_locked( root: &Path, state: &mut AgentRuntimeState, summary: &str, detail: Option<&str>, ) -> Result<(), String> { terminate_process_sessions_for_run_at(root, &state.agent_id, &state.run_id) .map_err(|error| format!("取消 Agent Runtime 前收束进程会话失败:{error}"))?; write_non_terminal_isolated_child_cancel_tombstones_for_parent_at( root, &state.agent_id, &state.run_id, "父 Agent 任务已取消,取消动态隔离子任务", )?; state.pending_tool_action = None; state.status = "cancelled".to_string(); state.phase = "cancelled".to_string(); state.current_action = summary.to_string(); state.waiting_on = "开发者下一轮输入".to_string(); state.next_step = "可重试该后台任务或提交新任务".to_string(); state.error = None; state.updated_at = unix_timestamp(); append_game_creator_agent_runtime_task(root, state)?; refresh_game_creator_agent_runtime_task_queue(root, state)?; write_game_creator_agent_runtime_state(root, state)?; let task_sha256 = format!("{:x}", Sha256::digest(state.current_task.as_bytes())); let task_chars = state.current_task.chars().count(); let goal_bound = state.goal_id.is_some(); let delegated = state.parent_agent_id.is_some(); let private_task = agent_runtime_task_requires_private_audit( state.goal_id.as_deref(), state.parent_agent_id.as_deref(), ); let public_task = (!private_task).then(|| state.current_task.clone()); let public_detail = if private_task { Some(format!( "taskChars={task_chars} · taskSha256={task_sha256} · goalBound={goal_bound} · delegated={delegated}" )) } else { detail.map(ToString::to_string) }; append_game_creator_agent_runtime_event( root, state, "turn.cancelled", "cancelled", "cancelled", summary, public_detail.as_deref(), )?; append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.background_task.cancelled", "agentId": state.agent_id.clone(), "taskId": state.task_id.clone(), "sessionId": state.session_id.clone(), "runId": state.run_id.clone(), "source": state.source.clone(), "task": public_task, "taskSha256": task_sha256, "taskChars": task_chars, "goalBound": goal_bound, "delegated": delegated, "summary": summary, }), )?; remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?; remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?; if let Some(goal) = mark_game_creator_agent_goal_cleared_for_runtime_at_locked(root, state)? { state.goal_status = Some(goal.status); state.updated_at = unix_timestamp(); write_game_creator_agent_runtime_state(root, state)?; } publish_game_creator_agent_delegate_result_for_state_at_locked( root, state, detail.or(Some(summary)), ); Ok(()) } pub(super) fn stop_game_creator_agent_runtime_if_cancel_requested( root: &Path, state: &mut AgentRuntimeState, ) -> bool { if game_creator_agent_runtime_cancel_requested(root, state) { let cancellation = mark_game_creator_agent_runtime_cancelled_at( root, state, "Agent 后台任务已按开发者请求取消", Some("取消请求会在当前 LLM 或工具调用返回后生效。"), ); if let Err(error) = cancellation { let error = format!("Agent 后台任务收到取消请求,但取消状态落盘失败:{error}"); state.status = "running".to_string(); state.phase = "needs-reconciliation".to_string(); state.current_action = "取消前的进程会话或持久状态需要人工核对".to_string(); state.waiting_on = "开发者核对进程会话和取消状态".to_string(); state.next_step = "刷新状态,核对 process session 后重新取消该任务".to_string(); state.error = Some(sanitize_agent_runtime_text(&error, 500)); state.updated_at = unix_timestamp(); let _ = append_game_creator_agent_runtime_task(root, state); let _ = refresh_game_creator_agent_runtime_task_queue(root, state); let _ = write_game_creator_agent_runtime_state(root, state); let _ = append_game_creator_agent_runtime_event( root, state, "turn.cancel_reconciliation", "running", "needs-reconciliation", "取消请求未能收束进程会话,当前 run 保持待核对。", Some(&error), ); } return true; } let pause_requested = hydrate_game_creator_agent_goal_state_at(root, state) .ok() .flatten() .is_some_and(|goal| goal.status == AGENT_GOAL_STATUS_PAUSE_REQUESTED); if !pause_requested { return false; } if let Err(error) = mark_game_creator_agent_runtime_paused_at(root, state) { let error = format!("Agent 后台任务收到暂停请求,但暂停状态落盘失败:{error}"); state.status = "running".to_string(); state.phase = "needs-reconciliation".to_string(); state.current_action = "暂停前的进程会话或持久状态需要人工核对".to_string(); state.waiting_on = "开发者核对进程会话和 Goal 状态".to_string(); state.next_step = "修复 Goal/Runtime 一致性后重新暂停".to_string(); state.error = Some(sanitize_agent_runtime_text(&error, 500)); state.updated_at = unix_timestamp(); let _ = append_game_creator_agent_runtime_task(root, state); let _ = refresh_game_creator_agent_runtime_task_queue(root, state); let _ = write_game_creator_agent_runtime_state(root, state); let _ = append_game_creator_agent_runtime_event( root, state, "goal.pause_reconciliation", "running", "needs-reconciliation", "暂停请求未能收束进程会话或 Goal 状态,当前 run 保持待核对。", Some(&error), ); } true } pub(crate) fn append_unique_game_creator_agent_runtime_pending_task( root: &Path, agent_id: &str, session_id: &str, task: &str, requested_run_id: &str, source: &str, requested_run_profile: Option<&str>, task_link: Option<&AgentRuntimeTaskLink>, ) -> Result { append_unique_game_creator_agent_runtime_task_with_initial_state( root, agent_id, session_id, task, requested_run_id, source, requested_run_profile, task_link, "pending", "queued", "等待当前后台任务完成", ) } pub(crate) fn append_unique_game_creator_agent_runtime_public_status_preparing_task( root: &Path, agent_id: &str, session_id: &str, task: &str, requested_run_id: &str, source: &str, requested_run_profile: Option<&str>, task_link: Option<&AgentRuntimeTaskLink>, ) -> Result { append_unique_game_creator_agent_runtime_task_with_initial_state( root, agent_id, session_id, task, requested_run_id, source, requested_run_profile, task_link, "preparing", "public-status-pending", "等待持久化启动确认", ) } fn append_unique_game_creator_agent_runtime_task_with_initial_state( root: &Path, agent_id: &str, session_id: &str, task: &str, requested_run_id: &str, source: &str, requested_run_profile: Option<&str>, task_link: Option<&AgentRuntimeTaskLink>, initial_status: &str, initial_phase: &str, initial_current_action: &str, ) -> Result { let autonomous_root_project_lock = (agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID && task_link.is_none() && requested_run_profile .is_some_and(|profile| profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD)) .then(|| { acquire_game_creator_agent_runtime_project_write_lock_with_wait( root, "runtime.autonomous-root.create", ) }) .transpose()?; let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?; let run_id = unique_game_creator_agent_runtime_run_id(root, agent_id, requested_run_id)?; let run_profile_binding = bind_game_creator_agent_runtime_run_profile_at( root, agent_id, &run_id, source, requested_run_profile, task_link, )?; let (goal_id, goal_revision, goal_status) = game_creator_agent_goal_task_binding_at(root, agent_id, session_id, &run_id)?; let task_link = task_link.cloned().unwrap_or_default(); let task_max_chars = if source.trim() == AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE { AGENT_RUNTIME_DELEGATE_RECEIPT_TASK_MAX_CHARS } else { AGENT_RUNTIME_TASK_MAX_CHARS }; let record = AgentRuntimeTaskRecord { goal_id, goal_revision, goal_status, schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: agent_id.to_string(), task_id: agent_id.to_string(), session_id: session_id.to_string(), run_id, source: source.trim().to_string(), run_profile: run_profile_binding.profile, run_profile_binding_fingerprint: run_profile_binding.binding_fingerprint, parent_agent_id: task_link.parent_agent_id, parent_run_id: task_link.parent_run_id, delegation_id: task_link.delegation_id, task: sanitize_agent_runtime_text(task, task_max_chars), status: initial_status.to_string(), phase: initial_phase.to_string(), current_action: initial_current_action.to_string(), terminal_detail: None, error: None, updated_at: unix_timestamp(), }; append_game_creator_agent_runtime_task_record_unlocked(root, &record)?; drop(_journal_lock); drop(autonomous_root_project_lock); if let Err(error) = ensure_autonomous_completion_contract_for_task_at(root, &record) { let public_error = redact_agent_runtime_project_paths(root, &error, 500); let failed = AgentRuntimeTaskRecord { status: "failed".to_string(), phase: "completion-contract-failed".to_string(), current_action: "自主构建完成合同未能建立,任务未执行".to_string(), terminal_detail: Some(public_error.clone()), error: Some(public_error), updated_at: unix_timestamp(), ..record.clone() }; append_game_creator_agent_runtime_task_record_unlocked(root, &failed)?; return Err(format!("自主构建完成合同建立失败,任务未执行:{error}")); } Ok(record) } pub(crate) fn append_or_read_exact_game_creator_agent_runtime_pending_task( root: &Path, agent_id: &str, session_id: &str, task: &str, requested_run_id: &str, source: &str, requested_run_profile: Option<&str>, task_link: &AgentRuntimeTaskLink, ) -> Result<(AgentRuntimeTaskRecord, bool), String> { let parent_agent_id = task_link .parent_agent_id .as_deref() .filter(|value| !value.trim().is_empty()) .ok_or_else(|| "静态委派缺少 parentAgentId".to_string())?; let parent_run_id = task_link .parent_run_id .as_deref() .filter(|value| !value.trim().is_empty()) .ok_or_else(|| "静态委派缺少 parentRunId".to_string())?; let delegation_id = task_link .delegation_id .as_deref() .filter(|value| !value.trim().is_empty()) .ok_or_else(|| "静态委派缺少 delegationId".to_string())?; if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || source != "agent-delegate" { return Err("精确 runId 入队只允许项目总控静态委派".to_string()); } let run_id = normalize_game_creator_agent_runtime_run_id(agent_id, requested_run_id); let task = sanitize_agent_runtime_text(task, AGENT_RUNTIME_TASK_MAX_CHARS); let run_profile_binding = bind_game_creator_agent_runtime_run_profile_at( root, agent_id, &run_id, source, requested_run_profile, Some(task_link), )?; let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?; let path = game_creator_agent_runtime_task_path(root, agent_id); let records = read_all_game_creator_agent_runtime_tasks(&path)?; if let Some(existing) = records .iter() .rev() .find(|record| record.delegation_id.as_deref() == Some(delegation_id)) { if existing.agent_id != agent_id || existing.session_id != session_id || existing.run_id != run_id || existing.source != source || existing.run_profile != run_profile_binding.profile || existing.run_profile_binding_fingerprint != run_profile_binding.binding_fingerprint || existing.parent_agent_id.as_deref() != Some(parent_agent_id) || existing.parent_run_id.as_deref() != Some(parent_run_id) || existing.task != task { return Err(format!("静态委派任务身份冲突:{delegation_id}")); } return Ok((existing.clone(), false)); } if records.iter().any(|record| record.run_id == run_id) { return Err(format!("静态委派预留 runId 已被其他任务占用:{run_id}")); } let record = AgentRuntimeTaskRecord { goal_id: None, goal_revision: 0, goal_status: None, schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(), agent_id: agent_id.to_string(), task_id: agent_id.to_string(), session_id: session_id.to_string(), run_id, source: source.to_string(), run_profile: run_profile_binding.profile, run_profile_binding_fingerprint: run_profile_binding.binding_fingerprint, parent_agent_id: Some(parent_agent_id.to_string()), parent_run_id: Some(parent_run_id.to_string()), delegation_id: Some(delegation_id.to_string()), task, status: "pending".to_string(), phase: "queued".to_string(), current_action: "等待当前后台任务完成".to_string(), terminal_detail: None, error: None, updated_at: unix_timestamp(), }; append_game_creator_agent_runtime_task_record_unlocked(root, &record)?; Ok((record, true)) } pub(crate) fn append_game_creator_agent_runtime_task_record( root: &Path, record: &AgentRuntimeTaskRecord, ) -> Result<(), String> { { let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, &record.agent_id)?; append_game_creator_agent_runtime_task_record_unlocked(root, record)?; } if record.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID && game_creator_agent_runtime_terminal_status(record).is_some() { suppress_static_delegate_deliveries_for_parent_terminal_at( root, &record.agent_id, &record.run_id, )?; } Ok(()) } pub(super) fn append_game_creator_agent_runtime_task_record_unlocked( root: &Path, record: &AgentRuntimeTaskRecord, ) -> Result<(), String> { validate_game_creator_agent_runtime_task_goal_binding(record)?; let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at( root, &record.agent_id, &record.run_id, Some(&record.run_profile), Some(&record.run_profile_binding_fingerprint), )?; if run_profile != record.run_profile || binding_fingerprint != record.run_profile_binding_fingerprint { return Err("Agent Runtime 任务 Run Profile 绑定不匹配".to_string()); } let path = game_creator_agent_runtime_task_path(root, &record.agent_id); if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(|error| { format!( "创建 Agent Runtime 任务目录失败:{}: {error}", parent.display() ) })?; } let line = serde_json::to_string(&record) .map_err(|error| format!("序列化 Agent Runtime 任务失败:{error}"))?; append_jsonl_line(&path, &line, "Agent Runtime 任务") } pub(super) fn refresh_game_creator_agent_runtime_task_queue( root: &Path, state: &mut AgentRuntimeState, ) -> Result<(), String> { let task_path = game_creator_agent_runtime_task_path(root, &state.agent_id); state.task_queue = read_game_creator_agent_runtime_task_snapshot(&task_path)?.task_queue; Ok(()) } pub(super) fn game_creator_agent_runtime_task_status(state: &AgentRuntimeState) -> String { if state.phase == "finalizing" { "running".to_string() } else if state.error.is_some() || state.status == "failed" || state.phase == "failed" { "failed".to_string() } else if state.status == "cancelled" || state.phase == "cancelled" { "cancelled".to_string() } else if state.phase == "completed" { "completed".to_string() } else if state.status == "running" { "running".to_string() } else { state.status.clone() } } pub(super) fn agent_runtime_terminal_detail(state: &AgentRuntimeState) -> Option { let detail = match state.phase.as_str() { "completed" => state.last_response.as_deref(), "failed" | "budget-exhausted" => state.error.as_deref(), "cancelled" => Some(state.current_action.as_str()), _ => None, }?; let detail = sanitize_agent_runtime_text(detail, 500); (!detail.trim().is_empty()).then_some(detail) } pub(crate) fn read_recent_game_creator_agent_runtime_events( path: &Path, ) -> Result, String> { read_recent_game_creator_agent_runtime_events_for_session(path, None) } pub(super) fn read_recent_game_creator_agent_runtime_events_for_session( path: &Path, session_id: Option<&str>, ) -> Result, String> { let mut events = Vec::new(); match File::open(path) { Ok(file) => { for line in BufReader::new(file).lines() { let line = line.map_err(|error| { format!("读取 Agent Runtime 事件失败:{}: {error}", path.display()) })?; let line = line.trim(); if line.is_empty() { continue; } match serde_json::from_str::(line) { Ok(mut event) => { normalize_game_creator_agent_runtime_event(&mut event); if session_id.is_some_and(|session_id| event.session_id != session_id) { continue; } events.push(event); } Err(_) => continue, } } } Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} Err(error) => { return Err(format!( "读取 Agent Runtime 事件失败:{}: {error}", path.display() )); } } if events.len() > AGENT_RUNTIME_RECENT_EVENT_LIMIT { Ok(events[events.len() - AGENT_RUNTIME_RECENT_EVENT_LIMIT..].to_vec()) } else { Ok(events) } } pub(super) struct AgentRuntimeTaskSnapshot { pub(super) task_queue: AgentRuntimeTaskQueueSummary, pub(super) recent_tasks: Vec, pub(super) run_started_at: Option, } pub(super) fn read_game_creator_agent_runtime_task_snapshot( path: &Path, ) -> Result { read_game_creator_agent_runtime_task_snapshot_for_session(path, None, None) } pub(super) fn read_game_creator_agent_runtime_task_snapshot_for_session( path: &Path, session_id: Option<&str>, run_id: Option<&str>, ) -> Result { let records = read_all_game_creator_agent_runtime_tasks(path)?; let run_started_at = run_id.and_then(|run_id| { records .iter() .filter(|record| { record.run_id == run_id && session_id.map_or(true, |session_id| record.session_id == session_id) }) .map(|record| record.updated_at) .filter(|updated_at| *updated_at > 0) .min() }); let latest = latest_game_creator_agent_runtime_tasks(records) .into_iter() .filter(|record| session_id.map_or(true, |session_id| record.session_id == session_id)) .collect::>(); let task_queue = summarize_game_creator_agent_runtime_task_queue(&latest); let mut recent = latest; if recent.len() > AGENT_RUNTIME_RECENT_TASK_LIMIT { recent = recent[recent.len() - AGENT_RUNTIME_RECENT_TASK_LIMIT..].to_vec(); } Ok(AgentRuntimeTaskSnapshot { task_queue, recent_tasks: recent, run_started_at, }) } pub(super) fn read_next_pending_game_creator_agent_runtime_task( root: &Path, agent_id: &str, ) -> Result, String> { let path = game_creator_agent_runtime_task_path(root, agent_id); let records = latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?); if records.iter().any(|record| record.status == "running") { return Ok(None); } for record in records { if record.status == "pending" { return Ok(Some(record)); } if let Some(queued) = promote_accepted_game_creator_agent_runtime_preparing_task_at(root, &record)? { return Ok(Some(queued)); } } Ok(None) } fn promote_accepted_game_creator_agent_runtime_preparing_task_at( root: &Path, record: &AgentRuntimeTaskRecord, ) -> Result, String> { if record.status != "preparing" || record.phase != "public-status-pending" { return Ok(None); } if !game_creator_agent_runtime_preparing_task_has_accepted_status_at(root, record)? { if !game_creator_agent_runtime_task_has_user_message_at(root, record)? { return Ok(None); } if let Err(error) = ensure_game_creator_agent_runtime_accepted_public_status_at(root, record) { fail_game_creator_agent_runtime_public_start_status_at(root, record, &error)?; return Err(format!("恢复 Agent Runtime 启动确认失败:{error}")); } } let queued = AgentRuntimeTaskRecord { status: "pending".to_string(), phase: "queued".to_string(), current_action: "等待当前后台任务完成".to_string(), updated_at: unix_timestamp(), ..record.clone() }; append_game_creator_agent_runtime_task_record(root, &queued)?; Ok(Some(queued)) } fn game_creator_agent_runtime_preparing_task_has_accepted_status_at( root: &Path, record: &AgentRuntimeTaskRecord, ) -> Result { if record.status != "preparing" || record.phase != "public-status-pending" { return Ok(false); } let message_id = game_creator_agent_runtime_public_status_message_id( &record.agent_id, &record.session_id, &record.run_id, "accepted", ); let Some(message) = read_local_conversation_message_by_id_for_session_at(root, None, None, &message_id)? else { return Ok(false); }; if message.role != "assistant" || message.content != game_creator_agent_runtime_accepted_public_message(&record.agent_id) { return Err(format!( "Agent Runtime 启动确认恢复身份冲突:runId={}", record.run_id )); } Ok(true) } pub(crate) fn game_creator_agent_runtime_task_has_user_message_at( root: &Path, record: &AgentRuntimeTaskRecord, ) -> Result { let message_id = agent_runtime_background_task_message_id( &record.agent_id, &record.session_id, &record.run_id, &record.source, ); let Some(message) = read_local_conversation_message_by_id_for_session_without_touch_at( root, Some(&record.agent_id), Some(&record.session_id), &message_id, )? else { return Ok(false); }; if message.role != "user" || message.content != record.task { return Err(format!( "Agent Runtime 启动用户消息身份冲突:runId={}", record.run_id )); } Ok(true) } pub(crate) fn ensure_game_creator_agent_runtime_accepted_public_status_at( root: &Path, record: &AgentRuntimeTaskRecord, ) -> Result<(), String> { if game_creator_agent_runtime_preparing_task_has_accepted_status_at(root, record)? { return Ok(()); } let append_result = append_game_creator_agent_runtime_public_status_message_at( root, &record.agent_id, &record.session_id, &record.run_id, "accepted", &game_creator_agent_runtime_accepted_public_message(&record.agent_id), ); match append_result { Ok(()) => Ok(()), Err(_error) if game_creator_agent_runtime_preparing_task_has_accepted_status_at(root, record)? => { // The conversation append is authoritative. A later audit write // may fail after the message is already durable; do not strand a // visible accepted task solely because its auxiliary audit needs // repair. Ok(()) } Err(error) => Err(error), } } pub(crate) fn fail_game_creator_agent_runtime_public_start_status_at( root: &Path, record: &AgentRuntimeTaskRecord, error: &str, ) -> Result<(), String> { let error = redact_agent_runtime_project_paths(root, error, 500); let failed_task = AgentRuntimeTaskRecord { status: "failed".to_string(), phase: "public-status-write-failed".to_string(), current_action: "启动确认未落盘,后台任务未执行".to_string(), terminal_detail: Some(error.clone()), error: Some(error.clone()), updated_at: unix_timestamp(), ..record.clone() }; let mut failed_state = agent_runtime_state_from_task_record(&failed_task); failed_state.status = failed_task.status.clone(); failed_state.phase = failed_task.phase.clone(); let public_status_result = append_game_creator_agent_runtime_terminal_public_message_at(root, &failed_state, &error); append_game_creator_agent_runtime_task_record(root, &failed_task)?; publish_game_creator_agent_delegate_result(root, &failed_task, Some(&error)); public_status_result } pub(super) fn suppress_game_creator_agent_delegate_receipt_for_terminal_parent( root: &Path, receipt_task: &AgentRuntimeTaskRecord, ) -> Result { if receipt_task.source == AGENT_RUNTIME_ISOLATED_JOIN_SOURCE { let Some(parent_run_id) = receipt_task .parent_run_id .as_deref() .filter(|value| !value.trim().is_empty()) else { append_game_creator_agent_runtime_queued_cancellation( root, &receipt_task.agent_id, receipt_task, "动态隔离 join 缺少父 run 关联,已阻止自动续跑", )?; return Ok(true); }; let parent_task = read_latest_game_creator_agent_runtime_task_by_run_id( root, &receipt_task.agent_id, parent_run_id, )?; if parent_task .as_ref() .is_some_and(game_creator_agent_runtime_parent_blocks_delegate_receipt) { append_game_creator_agent_runtime_queued_cancellation( root, &receipt_task.agent_id, receipt_task, "父任务已取消或失败,动态隔离 join 不再自动续跑", )?; return Ok(true); } if parent_task.is_none() { append_game_creator_agent_runtime_queued_cancellation( root, &receipt_task.agent_id, receipt_task, "动态隔离 join 找不到父 run,已阻止自动续跑", )?; return Ok(true); } return Ok(false); } if receipt_task.source != AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE { return Ok(false); } let Some(parent_run_id) = receipt_task .parent_run_id .as_deref() .filter(|value| !value.trim().is_empty()) else { close_game_creator_agent_delegate_receipt_before_start( root, receipt_task, "failed", "parent-link-missing", "委派回执缺少父 run 关联,已阻止自动续跑", "missing-parent-run-id", None, )?; return Ok(true); }; let Some(parent_task) = read_latest_game_creator_agent_runtime_task_by_run_id( root, &receipt_task.agent_id, parent_run_id, )? else { close_game_creator_agent_delegate_receipt_before_start( root, receipt_task, "failed", "parent-link-missing", "委派回执找不到父 run,已阻止自动续跑", "parent-run-not-found", Some(parent_run_id), )?; return Ok(true); }; if !game_creator_agent_runtime_parent_blocks_delegate_receipt(&parent_task) { return Ok(false); } close_game_creator_agent_delegate_receipt_before_start( root, receipt_task, "cancelled", "parent-terminal", "父任务已取消或失败,排队回执不再自动续跑", "parent-became-terminal-before-receipt-start", Some(parent_run_id), )?; Ok(true) } #[allow(clippy::too_many_arguments)] pub(super) fn close_game_creator_agent_delegate_receipt_before_start( root: &Path, receipt_task: &AgentRuntimeTaskRecord, status: &str, phase: &str, current_action: &str, reason: &str, parent_run_id: Option<&str>, ) -> Result<(), String> { let closed = AgentRuntimeTaskRecord { status: status.to_string(), phase: phase.to_string(), current_action: current_action.to_string(), terminal_detail: Some(sanitize_agent_runtime_text(&receipt_task.task, 500)), error: (status == "failed").then(|| current_action.to_string()), updated_at: unix_timestamp(), ..receipt_task.clone() }; append_game_creator_agent_runtime_task_record(root, &closed)?; remove_game_creator_agent_runtime_cancel_request( root, &receipt_task.agent_id, &receipt_task.run_id, ); let event_state = agent_runtime_state_from_task_record(&closed); let _ = append_game_creator_agent_runtime_event( root, &event_state, "agent.delegate.result_suppressed", status, phase, current_action, parent_run_id, ); let _ = append_agent_db_record( root, serde_json::json!({ "recordType": "agent.runtime.agent.delegate.result_suppressed", "agentId": receipt_task.agent_id, "sessionId": receipt_task.session_id, "runId": receipt_task.run_id, "parentRunId": parent_run_id, "delegationId": receipt_task.delegation_id, "status": status, "phase": phase, "reason": reason, }), ); emit_game_creator_agent_runtime_update(root, &receipt_task.agent_id); Ok(()) } pub(crate) fn read_next_runnable_game_creator_agent_runtime_task( root: &Path, agent_id: &str, ) -> Result, String> { loop { let Some(task) = read_next_pending_game_creator_agent_runtime_task(root, agent_id)? else { return Ok(None); }; if suppress_game_creator_agent_delegate_receipt_for_terminal_parent(root, &task)? { continue; } return Ok(Some(task)); } } pub(super) fn game_creator_agent_runtime_has_reconciliation_barrier( root: &Path, agent_id: &str, ) -> Result { let runtime = read_game_creator_agent_runtime_at(root, agent_id)?; if runtime.state.phase == "needs-reconciliation" { return Ok(true); } let path = game_creator_agent_runtime_task_path(root, agent_id); let records = latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?); Ok(records .into_iter() .any(|record| record.phase == "needs-reconciliation")) } pub(crate) fn read_latest_game_creator_agent_runtime_task_by_run_id( root: &Path, agent_id: &str, run_id: &str, ) -> Result, String> { let path = game_creator_agent_runtime_task_path(root, agent_id); let run_id = run_id.trim(); let records = latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?); Ok(records.into_iter().find(|record| record.run_id == run_id)) } pub(crate) fn read_latest_game_creator_agent_runtime_task_by_delegation_id( root: &Path, agent_id: &str, delegation_id: &str, ) -> Result, String> { let path = game_creator_agent_runtime_task_path(root, agent_id); let records = latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?); Ok(records.into_iter().find(|record| { record.delegation_id.as_deref() == Some(delegation_id) && record.source != AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE })) } pub(super) fn read_recoverable_game_creator_agent_runtime_task( root: &Path, agent_id: &str, ) -> Result, String> { let path = game_creator_agent_runtime_task_path(root, agent_id); let records = latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?); let mut task_states = Vec::with_capacity(records.len()); for record in &records { let state = match record.status.as_str() { "pending" => agent_runtime_core::RecoverableTaskState::Pending, "preparing" if game_creator_agent_runtime_preparing_task_has_accepted_status_at( root, record, )? || game_creator_agent_runtime_task_has_user_message_at(root, record)? => { agent_runtime_core::RecoverableTaskState::Pending } "running" => agent_runtime_core::RecoverableTaskState::Running, "waiting-for-confirmation" => { agent_runtime_core::RecoverableTaskState::WaitingForConfirmation } "waiting-for-user-input" => { agent_runtime_core::RecoverableTaskState::WaitingForUserInput } _ => agent_runtime_core::RecoverableTaskState::Other, }; task_states.push(state); } match agent_runtime_core::next_recovery_step(&task_states) { agent_runtime_core::RecoveryStep::ResumeRunning { index } | agent_runtime_core::RecoveryStep::StartPending { index } => { Ok(records.get(index).cloned()) } agent_runtime_core::RecoveryStep::WaitForExternalInput | agent_runtime_core::RecoveryStep::Idle => Ok(None), } } pub(super) fn read_recoverable_runnable_game_creator_agent_runtime_task( root: &Path, agent_id: &str, ) -> Result, String> { loop { let Some(mut task) = read_recoverable_game_creator_agent_runtime_task(root, agent_id)? else { return Ok(None); }; if let Some(queued) = promote_accepted_game_creator_agent_runtime_preparing_task_at(root, &task)? { task = queued; } if suppress_game_creator_agent_delegate_receipt_for_terminal_parent(root, &task)? { continue; } return Ok(Some(task)); } } pub(crate) fn read_all_game_creator_agent_runtime_tasks( path: &Path, ) -> Result, String> { let mut records = Vec::new(); match File::open(path) { Ok(file) => { let mut reader = BufReader::new(file); let mut line_number = 0usize; loop { let mut line = Vec::new(); let bytes_read = reader.read_until(b'\n', &mut line).map_err(|error| { format!("读取 Agent Runtime 任务失败:{}: {error}", path.display()) })?; if bytes_read == 0 { break; } line_number += 1; let terminated = line.last() == Some(&b'\n'); if terminated { line.pop(); if line.last() == Some(&b'\r') { line.pop(); } } let line = match std::str::from_utf8(&line) { Ok(line) => line.trim(), Err(error) if !terminated && error.error_len().is_none() => break, Err(error) => { return Err(format!( "读取 Agent Runtime 任务失败:{}: 第 {line_number} 行不是有效 UTF-8:{error}", path.display() )); } }; if line.is_empty() { continue; } let mut record = match serde_json::from_str::(line) { Ok(record) => record, Err(error) if !terminated && error.is_eof() => break, Err(error) => { return Err(format!( "读取 Agent Runtime 任务失败:{}: 第 {line_number} 行 JSON 无效:{error}", path.display() )); } }; normalize_game_creator_agent_runtime_task(&mut record); validate_game_creator_agent_runtime_task_status_phase(&record).map_err( |error| { format!( "读取 Agent Runtime 任务失败:{}: 第 {line_number} 行:{error}", path.display() ) }, )?; validate_game_creator_agent_runtime_task_goal_binding(&record).map_err( |error| { format!( "读取 Agent Runtime 任务失败:{}: 第 {line_number} 行:{error}", path.display() ) }, )?; records.push(record); } } Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} Err(error) => { return Err(format!( "读取 Agent Runtime 任务失败:{}: {error}", path.display() )); } } Ok(records) } fn validate_game_creator_agent_runtime_task_status_phase( record: &AgentRuntimeTaskRecord, ) -> Result<(), String> { if !matches!( record.status.as_str(), "idle" | "preparing" | "pending" | "running" | "waiting-for-confirmation" | "waiting-for-user-input" | "paused" | "pausing" | "cancelling" | "cancelled" | "completed" | "failed" | "needs-reconciliation" ) { return Err(format!( "Agent Runtime task status 无效:runId={} status={}", record.run_id, record.status )); } if !matches!( record.phase.as_str(), "idle" | "public-status-pending" | "queued" | "planning" | "llm" | "action" | "provider-action-batch" | "observation" | "response" | "finalizing" | "waiting-for-confirmation" | "waiting-for-user-input" | "waiting-for-provider-retry" | "waiting-for-process-session" | "waiting-for-isolated-join" | "waiting-for-delegate-receipts" | "waiting-for-manifest-tasks" | "waiting-for-visual-asset" | "brief" | "paused" | "pausing" | "cancelling" | "cancelled" | "completed" | "failed" | "budget-exhausted" | "needs-reconciliation" | "completion-contract-failed" | "conversation-write-failed" | "public-status-write-failed" | "parent-terminal" | "parent-link-missing" ) { return Err(format!( "Agent Runtime task phase 无效:runId={} phase={}", record.run_id, record.phase )); } Ok(()) } pub(super) fn latest_game_creator_agent_runtime_tasks( records: Vec, ) -> Vec { let mut latest: Vec = Vec::new(); for record in records { if let Some(index) = latest .iter() .position(|existing| existing.run_id == record.run_id) { latest.remove(index); } latest.push(record); } latest } pub(super) fn summarize_game_creator_agent_runtime_task_queue( records: &[AgentRuntimeTaskRecord], ) -> AgentRuntimeTaskQueueSummary { let mut summary = AgentRuntimeTaskQueueSummary { total: records.len() as u32, ..AgentRuntimeTaskQueueSummary::default() }; for record in records { match record.status.as_str() { "pending" => summary.pending += 1, "running" => summary.running += 1, "waiting-for-confirmation" => summary.waiting_for_confirmation += 1, "waiting-for-user-input" => summary.waiting_for_user_input += 1, "paused" => summary.paused += 1, "cancelled" => summary.cancelled += 1, "completed" => summary.completed += 1, "failed" => summary.failed += 1, _ => {} } if record.updated_at >= summary.updated_at { summary.updated_at = record.updated_at; summary.latest_run_id = Some(record.run_id.clone()); } } summary } pub(super) fn truncate_agent_runtime_text(value: &str, max_chars: usize) -> String { let value = value.trim(); let mut output = String::new(); for character in value.chars().take(max_chars) { output.push(character); } if value.chars().count() > max_chars { output.push('…'); } output } pub(super) fn truncate_agent_runtime_text_preserving_tail(value: &str, max_chars: usize) -> String { let value = value.trim(); let count = value.chars().count(); if count <= max_chars { return value.to_string(); } let mut characters = value.chars().rev().take(max_chars).collect::>(); characters.reverse(); format!("…{}", characters.into_iter().collect::()) } pub(super) fn sanitize_agent_runtime_text(value: &str, max_chars: usize) -> String { truncate_agent_runtime_text(sanitize_prompt_context(value).trim(), max_chars) } pub(super) fn redact_agent_runtime_url_tokens(value: &str) -> String { let mut output = String::with_capacity(value.len()); let mut cursor = 0usize; while cursor < value.len() { let remaining = &value[cursor..]; let url_prefix_bytes = if remaining .get(..8) .is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://")) { Some(8) } else if remaining .get(..7) .is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://")) { Some(7) } else { None }; let Some(url_prefix_bytes) = url_prefix_bytes else { let character = remaining.chars().next().unwrap_or_default(); output.push(character); cursor += character.len_utf8(); continue; }; output.push_str(""); cursor += url_prefix_bytes; while cursor < value.len() { let character = value[cursor..].chars().next().unwrap_or_default(); if character.is_whitespace() || matches!( character, '\'' | '"' | '`' | '<' | '>' | '[' | ']' | '{' | '}' ) { break; } cursor += character.len_utf8(); } } output } pub(crate) fn redact_agent_runtime_error(root: &Path, value: &str, max_chars: usize) -> String { let redacted = redact_agent_runtime_url_tokens(value); let redacted = redact_agent_runtime_project_paths_raw(root, &redacted); let redacted = redact_absolute_path_tokens(&redacted); let redacted = redact_secret_tokens(&redacted); let mut sanitized = sanitize_prompt_context(&redacted); let preserved_markers = [ "", "$PROJECT_ROOT", "", "[redacted-secret]", ] .into_iter() .filter(|marker| redacted.contains(marker) && !sanitized.contains(marker)) .collect::>(); if !preserved_markers.is_empty() { sanitized = format!("{} {}", preserved_markers.join(" "), sanitized.trim()); } truncate_agent_runtime_text(sanitized.trim(), max_chars) } pub(crate) fn redact_agent_runtime_project_paths( root: &Path, value: &str, max_chars: usize, ) -> String { sanitize_agent_runtime_text( &redact_agent_runtime_project_paths_raw(root, value), max_chars, ) } pub(super) fn redact_agent_runtime_project_paths_raw(root: &Path, value: &str) -> String { let mut redacted = value.to_string(); let root_display = root.to_string_lossy(); if !root_display.is_empty() { redacted = redacted.replace(root_display.as_ref(), "$PROJECT_ROOT"); } if let Ok(canonical_root) = root.canonicalize() { let canonical_display = canonical_root.to_string_lossy(); if !canonical_display.is_empty() && canonical_display != root_display { redacted = redacted.replace(canonical_display.as_ref(), "$PROJECT_ROOT"); } } redacted } pub(super) fn redact_agent_runtime_project_paths_preserving_tail( root: &Path, value: &str, max_chars: usize, ) -> String { let mut redacted = value.to_string(); let root_display = root.to_string_lossy(); if !root_display.is_empty() { redacted = redacted.replace(root_display.as_ref(), "$PROJECT_ROOT"); } if let Ok(canonical_root) = root.canonicalize() { let canonical_display = canonical_root.to_string_lossy(); if !canonical_display.is_empty() && canonical_display != root_display { redacted = redacted.replace(canonical_display.as_ref(), "$PROJECT_ROOT"); } } truncate_agent_runtime_text_preserving_tail( sanitize_prompt_context(&redacted).trim(), max_chars, ) } pub(super) fn render_agent_runtime_tool_names(values: &[String], limit: usize) -> String { if values.is_empty() { return "无".to_string(); } let mut names = values .iter() .filter(|value| !value.trim().is_empty()) .take(limit) .cloned() .collect::>(); if values.len() > limit { names.push(format!("等{}项", values.len())); } names.join(", ") }