From ba9832a28773b851d211329f91acb1495e9c2338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 23 Sep 2026 12:35:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=A0=E9=99=A4AGC=E6=97=A0=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=96=B9=E7=9A=84=20steer=20Tauri=20=E5=91=BD?= =?UTF-8?q?=E4=BB=A4=E4=B8=8E=20LLM=20steer=20=E5=88=A4=E5=AE=9A=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 删除 Tauri 命令 steer_game_creator_agent_runtime_task,并同步 main.rs invoke_handler 注册与 AgentRuntimeSteerResult 的 assistantReply / interruptDecision / decisionReason 字段 - 删除只服务该命令的 agent/interaction.rs 整层(runtime_steer_decision 工具、判定请求构建与响应解析、decide_game_creator_agent_runtime_steer_at)及其 agent.rs 模块挂载 - 删除 runtime.interrupt_for_steer_decision RPC 与 Runner 客户端/派发分支、持久 decision 记录 agent.runtime.steer_decision、AgentRuntimeProviderInterrupt::applied_steer_cursor 及 register 的 cursor 参数 - 删除只测该链条的 provider / runner / runtime_state 用例 --- .../src-tauri/src/agent.rs | 2 - .../src-tauri/src/agent/interaction.rs | 234 ------------ .../src-tauri/src/agent/runtime_protocol.rs | 7 +- .../src/agent/runtime_protocol/models.rs | 1 - .../runtime_protocol/real_e2e_checkpoint.rs | 1 - .../src/agent/runtime_protocol/steering.rs | 248 +----------- .../src-tauri/src/commands.rs | 133 ------- .../src-tauri/src/main.rs | 7 - .../src-tauri/src/runner.rs | 3 +- .../src-tauri/src/runner/client.rs | 23 -- .../src-tauri/src/runner/dispatch.rs | 20 - .../src-tauri/src/runner/tests.rs | 288 -------------- .../src-tauri/src/tests/provider.rs | 356 ------------------ .../src-tauri/src/tests/runtime_state.rs | 101 ----- 14 files changed, 3 insertions(+), 1421 deletions(-) delete mode 100644 apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 05b25f127..a828e4928 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -38,7 +38,6 @@ mod direct_turn_metrics; mod direct_turn_stream; mod direct_validation; mod generation; -mod interaction; mod prompt; mod runtime_actions; mod runtime_adapter; @@ -76,7 +75,6 @@ pub(crate) use direct_turn_metrics::*; pub(crate) use direct_turn_stream::*; pub(crate) use direct_validation::DirectValidationConfig; pub(crate) use generation::*; -pub(crate) use interaction::*; pub(crate) use prompt::*; pub(crate) use runtime_actions::*; pub(crate) use runtime_adapter::*; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs deleted file mode 100644 index 397c754f1..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs +++ /dev/null @@ -1,234 +0,0 @@ -use super::*; - -const AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS: u32 = 1_200; -pub(crate) const AGENT_RUNTIME_STEER_DECISION_TOOL: &str = "runtime_steer_decision"; - -#[derive(Debug, Deserialize)] -#[serde(deny_unknown_fields, rename_all = "camelCase")] -struct AgentRuntimeSteerDecisionArguments { - reply: String, - interrupt_current_provider: bool, - reason: String, -} - -fn agent_runtime_steer_decision_function_tool() -> platform_llm::LlmFunctionTool { - platform_llm::LlmFunctionTool::new( - AGENT_RUNTIME_STEER_DECISION_TOOL, - prompt_text!("interaction.steer_decision_description"), - serde_json::json!({ - "type": "object", - "required": ["reply", "interruptCurrentProvider", "reason"], - "additionalProperties": false, - "properties": { - "reply": { - "type": "string", - "minLength": 1, - "maxLength": 2000 - }, - "interruptCurrentProvider": { "type": "boolean" }, - "reason": { - "type": "string", - "minLength": 1, - "maxLength": 500 - } - } - }), - ) - .with_strict(true) -} - -fn build_agent_runtime_steer_decision_request( - root: &Path, - state: &AgentRuntimeState, - instruction: &str, -) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> { - let (llm, config_path, context) = build_game_creator_role_agent_context_for_session( - root, - &state.agent_id, - Some(&state.session_id), - )?; - let api_kind = parse_game_creator_llm_api_kind(&llm.api_kind)?; - let runtime_summary = serde_json::json!({ - "status": state.status, - "phase": state.phase, - "currentAction": state.current_action, - "waitingOn": state.waiting_on, - "nextStep": state.next_step, - "plan": state.plan, - "appliedSteerCursor": state.applied_steer_cursor, - }); - let system = format!( - prompt_text!("interaction.steer_decision_system"), - game_creator_project_supervisor_chat_system_prompt() - ); - let user = format!( - prompt_text!("interaction.steer_decision_user"), - serde_json::to_string_pretty(&runtime_summary) - .map_err(|error| format!("序列化 steer Runtime 摘要失败:{error}"))?, - instruction.trim(), - context = context, - ); - let request = LlmRunRequest::single_turn(system, user) - .with_api_kind(api_kind) - .with_max_output_tokens(AGENT_RUNTIME_STEER_DECISION_MAX_OUTPUT_TOKENS) - .with_function_tools(vec![agent_runtime_steer_decision_function_tool()]) - .with_tool_choice(platform_llm::LlmToolChoice::Required); - let request = apply_game_creator_llm_reasoning_effort(request, &llm)?; - Ok((llm, config_path, request)) -} - -fn parse_agent_runtime_steer_decision_response( - response: &platform_llm::LlmRunResponse, -) -> Result { - if response.tool_calls.len() != 1 { - return Err("Supervisor steer decision 必须返回一个 runtime_steer_decision".to_string()); - } - let call = &response.tool_calls[0]; - if call.name != AGENT_RUNTIME_STEER_DECISION_TOOL { - return Err(format!( - "Supervisor steer decision 返回未知工具:{}", - call.name - )); - } - let arguments = serde_json::from_str::(&call.arguments) - .map_err(|error| format!("runtime_steer_decision 参数无效:{error}"))?; - let reply = arguments.reply.trim(); - let reason = arguments.reason.trim(); - if reply.is_empty() || reason.is_empty() { - return Err("runtime_steer_decision reply/reason 不能为空".to_string()); - } - Ok(AgentRuntimeSteerDecision { - reply: reply.to_string(), - interrupt_current_provider: arguments.interrupt_current_provider, - reason: reason.to_string(), - }) -} - -pub(crate) async fn decide_game_creator_agent_runtime_steer_at( - root: &Path, - state: &AgentRuntimeState, - steer_id: &str, - sequence: u64, - instruction: &str, -) -> Result { - if let Some(decision) = read_game_creator_agent_runtime_steer_decision_at( - root, - &state.agent_id, - &state.run_id, - steer_id, - )? { - return Ok(decision); - } - if state.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - || state.parent_agent_id.is_some() - || state.parent_run_id.is_some() - { - return Err("只有根 Project Supervisor 支持运行中非终态回复".to_string()); - } - let (llm, config_path, request) = - build_agent_runtime_steer_decision_request(root, state, instruction)?; - let decision_snapshot = { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.steer_decision.snapshot", - )?; - let mut snapshot = capture_game_creator_agent_runtime_provider_request_snapshot_at_locked( - root, - &state.agent_id, - &state.session_id, - &state.run_id, - "steer-decision", - &format!("steer-{sequence}"), - state.applied_steer_cursor, - )?; - // The main Runtime Provider request must keep running while this - // independent LLM turn decides whether it is stale. A distinct node - // identity gives Codex app-server a separate process/thread gate. - snapshot.agent_id = format!("{}-steer-decision", state.agent_id); - snapshot.task_id = snapshot.agent_id.clone(); - snapshot - }; - let agent_mode = - normalize_game_creator_agent_mode(&load_game_creator_app_config()?.agent_mode)?; - let response = match agent_mode.as_str() { - GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER => { - request_game_creator_agent_codex_app_server(&decision_snapshot, &llm, request).await - } - GAME_CREATOR_AGENT_MODE_CODEX_CLI => request_game_creator_agent_codex_cli(request).await, - GAME_CREATOR_AGENT_MODE_PROVIDER => { - build_game_creator_agent_runtime_llm_client(&llm, &config_path)? - .run(request) - .await - } - _ => unreachable!("agent mode is normalized"), - } - .map_err(|error| format!("Supervisor steer decision 调用 LLM 失败:{error}"))?; - let decision = parse_agent_runtime_steer_decision_response(&response)?; - persist_game_creator_agent_runtime_steer_decision_and_reply_at( - root, state, steer_id, sequence, decision, - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn response( - text: &str, - tool_calls: Vec, - ) -> platform_llm::LlmRunResponse { - platform_llm::LlmRunResponse { - provider: LlmProvider::OpenAiCompatible, - model: "interaction-test".to_string(), - text: text.to_string(), - reasoning: String::new(), - finish_reason: Some("stop".to_string()), - response_id: Some("interaction-response".to_string()), - usage: None, - tool_calls, - responses_output: Vec::new(), - } - } - - fn tool_call(name: &str, arguments: &str) -> platform_llm::LlmToolCall { - platform_llm::LlmToolCall { - id: "call-interaction".to_string(), - name: name.to_string(), - arguments: arguments.to_string(), - } - } - - #[test] - fn steer_decision_replies_without_interrupting_status_questions() { - let decision = parse_agent_runtime_steer_decision_response(&response( - "", - vec![tool_call( - AGENT_RUNTIME_STEER_DECISION_TOOL, - r#"{"reply":"我正在完成玩法实现,当前任务会继续。","interruptCurrentProvider":false,"reason":"这是状态询问,不会使当前方案过期。"}"#, - )], - )) - .expect("parse non-interrupting steer decision"); - assert_eq!( - decision, - AgentRuntimeSteerDecision { - reply: "我正在完成玩法实现,当前任务会继续。".to_string(), - interrupt_current_provider: false, - reason: "这是状态询问,不会使当前方案过期。".to_string(), - } - ); - } - - #[test] - fn steer_decision_can_request_safe_interrupt_for_conflicting_change() { - let decision = parse_agent_runtime_steer_decision_response(&response( - "", - vec![tool_call( - AGENT_RUNTIME_STEER_DECISION_TOOL, - r#"{"reply":"明白,我会停止旧方向并改成回合制。","interruptCurrentProvider":true,"reason":"用户明确改向,旧 Provider 仍在生成过期方案。"}"#, - )], - )) - .expect("parse interrupting steer decision"); - assert!(decision.interrupt_current_provider); - assert!(decision.reply.contains("改成回合制")); - } -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs index e73fbaada..b40f680ec 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol.rs @@ -84,20 +84,15 @@ pub(crate) use run_configuration::{ }; pub(crate) use steering::{ acquire_game_creator_agent_runtime_steer_project_write_lock_with_wait, - append_game_creator_agent_runtime_steer_decision_failure_reply_at, consume_game_creator_agent_runtime_steers, game_creator_agent_runtime_provider_request_count_for_roots, game_creator_agent_runtime_steer_ledger_path, - interrupt_game_creator_agent_runtime_provider_for_decided_steer_at, interrupt_game_creator_agent_runtime_provider_request_at, interrupt_game_creator_agent_runtime_provider_requests_for_roots, - persist_game_creator_agent_runtime_steer_decision_and_reply_at, - read_game_creator_agent_runtime_steer_decision_at, register_game_creator_agent_runtime_provider_request, render_game_creator_agent_runtime_steers_for_prompt, steer_game_creator_agent_runtime_task_at, - steer_game_creator_agent_runtime_task_for_profile_at, unregister_game_creator_agent_runtime_provider_request, - validate_game_creator_agent_runtime_steer_notification_at, AgentRuntimeSteerDecision, + validate_game_creator_agent_runtime_steer_notification_at, }; pub(crate) use verification::{ game_creator_agent_runtime_verification_gate_path, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs index d2698b814..559470694 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/models.rs @@ -489,7 +489,6 @@ pub(in crate::agent) struct AgentRuntimeSteerLedgerSnapshot { #[derive(Default)] pub(crate) struct AgentRuntimeProviderInterrupt { - pub(crate) applied_steer_cursor: u64, pub(crate) interrupted: AtomicBool, pub(crate) notify: tokio::sync::Notify, } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/real_e2e_checkpoint.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/real_e2e_checkpoint.rs index 3e7e052dd..e1a54896e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/real_e2e_checkpoint.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/real_e2e_checkpoint.rs @@ -1075,7 +1075,6 @@ where root, &snapshot.agent_id, &snapshot.run_id, - snapshot.applied_steer_cursor, ) .map_err(|error| redact_agent_runtime_error(root, &error, 500))?; before_control_recheck(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs index 9037da1e1..93c0bc144 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/steering.rs @@ -431,202 +431,6 @@ pub(in crate::agent) fn ensure_game_creator_agent_runtime_steer_audit( ) } -const AGENT_RUNTIME_STEER_DECISION_RECORD_TYPE: &str = "agent.runtime.steer_decision"; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct AgentRuntimeSteerDecision { - pub(crate) reply: String, - pub(crate) interrupt_current_provider: bool, - pub(crate) reason: String, -} - -fn parse_game_creator_agent_runtime_steer_decision_record( - record: &serde_json::Value, -) -> Result { - let reply = record - .get("reply") - .and_then(serde_json::Value::as_str) - .filter(|value| !value.trim().is_empty()) - .ok_or_else(|| "Supervisor steer decision 缺少 reply".to_string())?; - let reply_sha256 = format!("{:x}", Sha256::digest(reply.as_bytes())); - if record - .get("replySha256") - .and_then(serde_json::Value::as_str) - != Some(reply_sha256.as_str()) - { - return Err("Supervisor steer decision reply 指纹冲突".to_string()); - } - let interrupt_current_provider = record - .get("interruptCurrentProvider") - .and_then(serde_json::Value::as_bool) - .ok_or_else(|| "Supervisor steer decision 缺少 interruptCurrentProvider".to_string())?; - let reason = record - .get("reason") - .and_then(serde_json::Value::as_str) - .unwrap_or_default(); - Ok(AgentRuntimeSteerDecision { - reply: reply.to_string(), - interrupt_current_provider, - reason: reason.to_string(), - }) -} - -pub(crate) fn read_game_creator_agent_runtime_steer_decision_at( - root: &Path, - agent_id: &str, - run_id: &str, - steer_id: &str, -) -> Result, String> { - let (records, _) = read_agent_db_records_bounded(root, 16 * 1024 * 1024)?; - let mut decision = None; - for record in records.iter().filter(|record| { - record.get("recordType").and_then(serde_json::Value::as_str) - == Some(AGENT_RUNTIME_STEER_DECISION_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("steerId").and_then(serde_json::Value::as_str) == Some(steer_id) - }) { - let parsed = parse_game_creator_agent_runtime_steer_decision_record(record)?; - if decision - .as_ref() - .is_some_and(|existing| existing != &parsed) - { - return Err("Supervisor steer decision 出现冲突终态".to_string()); - } - decision = Some(parsed); - } - Ok(decision) -} - -pub(crate) fn persist_game_creator_agent_runtime_steer_decision_and_reply_at( - root: &Path, - state: &AgentRuntimeState, - steer_id: &str, - sequence: u64, - decision: AgentRuntimeSteerDecision, -) -> Result { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.steer_decision.persist", - )?; - let snapshot = - read_game_creator_agent_runtime_steer_ledger(root, &state.agent_id, &state.run_id)?; - let entry = snapshot - .entries - .get(steer_id) - .ok_or_else(|| "Supervisor steer decision 未命中持久 steer".to_string())?; - if entry.identity.sequence != sequence - || entry.identity.session_id != state.session_id - || entry.identity.source != state.source - { - return Err("Supervisor steer decision 与持久 steer 身份冲突".to_string()); - } - let reply = sanitize_agent_runtime_text(decision.reply.trim(), 2_000); - if reply.is_empty() { - return Err("Supervisor steer decision reply 不能为空".to_string()); - } - let reason = redact_agent_runtime_project_paths( - root, - &sanitize_agent_runtime_text(decision.reason.trim(), 500), - 500, - ); - let decision = AgentRuntimeSteerDecision { - reply, - interrupt_current_provider: decision.interrupt_current_provider, - reason, - }; - let persisted = if let Some(existing) = read_game_creator_agent_runtime_steer_decision_at( - root, - &state.agent_id, - &state.run_id, - steer_id, - )? { - existing - } else { - append_agent_db_record( - root, - serde_json::json!({ - "recordType": AGENT_RUNTIME_STEER_DECISION_RECORD_TYPE, - "agentId": state.agent_id, - "taskId": state.task_id, - "sessionId": state.session_id, - "runId": state.run_id, - "source": state.source, - "steerId": steer_id, - "sequence": sequence, - "messageId": entry.identity.message_id, - "instructionSha256": entry.identity.instruction_sha256, - "reply": decision.reply, - "replySha256": format!("{:x}", Sha256::digest(decision.reply.as_bytes())), - "interruptCurrentProvider": decision.interrupt_current_provider, - "reason": decision.reason, - "decidedAt": unix_timestamp(), - }), - )?; - decision - }; - let message_id = entry.identity.message_id.as_deref().unwrap_or_default(); - let correlation_id = message_id - .strip_prefix(AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX) - .ok_or_else(|| "Supervisor steer decision 缺少消息关联身份".to_string())?; - append_game_creator_agent_runtime_public_status_message_for_correlation_at( - root, - correlation_id, - "steer-reply", - &persisted.reply, - )?; - Ok(persisted) -} - -pub(crate) fn append_game_creator_agent_runtime_steer_decision_failure_reply_at( - root: &Path, - state: &AgentRuntimeState, - steer_id: &str, - error: &str, -) -> Result { - let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( - root, - "runtime.steer_decision.failure", - )?; - let snapshot = - read_game_creator_agent_runtime_steer_ledger(root, &state.agent_id, &state.run_id)?; - let entry = snapshot - .entries - .get(steer_id) - .ok_or_else(|| "Supervisor steer decision 失败记录未命中持久 steer".to_string())?; - let correlation_id = entry - .identity - .message_id - .as_deref() - .and_then(|message_id| message_id.strip_prefix(AGENT_RUNTIME_STEER_MESSAGE_ID_PREFIX)) - .ok_or_else(|| "Supervisor steer decision 失败记录缺少消息关联身份".to_string())?; - let reply = "我会继续当前任务。这次没有完成中断判断,你的消息仍会在下一个安全边界进入规划。"; - append_game_creator_agent_runtime_public_status_message_for_correlation_at( - root, - correlation_id, - "steer-decision-failed", - reply, - )?; - let safe_error = redact_agent_runtime_error(root, error, 500); - append_agent_db_record( - root, - serde_json::json!({ - "recordType": "agent.runtime.steer_decision.failed", - "agentId": state.agent_id, - "taskId": state.task_id, - "sessionId": state.session_id, - "runId": state.run_id, - "source": state.source, - "steerId": steer_id, - "sequence": entry.identity.sequence, - "errorSha256": format!("{:x}", Sha256::digest(safe_error.as_bytes())), - "errorChars": safe_error.chars().count(), - "failedAt": unix_timestamp(), - }), - )?; - Ok(reply.to_string()) -} - pub(crate) fn game_creator_agent_runtime_accepts_steer(state: &AgentRuntimeState) -> bool { !state.agent_id.starts_with("child-") && state.source != AGENT_RUNTIME_ISOLATED_CHILD_SOURCE @@ -1125,9 +929,6 @@ fn transition_goal_contract_root_steer_at( // Returning true prevents CLI/Tauri callers from forwarding a stale // runtime.steer notification to the old external-runner Run. provider_interrupted: true, - assistant_reply: None, - interrupt_decision: None, - decision_reason: None, }) } @@ -1367,9 +1168,6 @@ pub(crate) fn steer_game_creator_agent_runtime_task_for_profile_at( sequence: entry.identity.sequence, status: latest_status, provider_interrupted, - assistant_reply: None, - interrupt_decision: None, - decision_reason: None, }) } @@ -1420,44 +1218,6 @@ pub(crate) fn interrupt_game_creator_agent_runtime_provider_request_at( Ok(first) } -pub(crate) fn interrupt_game_creator_agent_runtime_provider_for_decided_steer_at( - root: &Path, - agent_id: &str, - run_id: &str, - steer_id: &str, -) -> Result { - let decision = - read_game_creator_agent_runtime_steer_decision_at(root, agent_id, run_id, steer_id)? - .ok_or_else(|| "Supervisor steer 尚无 LLM 中断判定".to_string())?; - if !decision.interrupt_current_provider { - return Ok(false); - } - let snapshot = read_game_creator_agent_runtime_steer_ledger(root, agent_id, run_id)?; - let entry = snapshot - .entries - .get(steer_id) - .ok_or_else(|| "LLM 中断判定未命中持久 steer".to_string())?; - let key = agent_runtime_provider_interrupt_key(root, agent_id, run_id)?; - let active = { - let registry = game_creator_agent_provider_interrupts() - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - registry.get(&key).cloned() - }; - let Some(active) = active else { - return Ok(false); - }; - if active.applied_steer_cursor >= entry.identity.sequence { - // The provider request was created after this steer was already - // applied. Interrupting it would cancel the new plan, not the stale - // request the LLM evaluated. - return Ok(false); - } - let first = !active.interrupted.swap(true, Ordering::AcqRel); - active.notify.notify_one(); - Ok(first) -} - pub(crate) fn interrupt_game_creator_agent_runtime_provider_requests_for_roots( roots: &[PathBuf], ) -> usize { @@ -1523,13 +1283,9 @@ pub(crate) fn register_game_creator_agent_runtime_provider_request( root: &Path, agent_id: &str, run_id: &str, - applied_steer_cursor: u64, ) -> Result<(String, Arc), String> { let key = agent_runtime_provider_interrupt_key(root, agent_id, run_id)?; - let active = Arc::new(AgentRuntimeProviderInterrupt { - applied_steer_cursor, - ..AgentRuntimeProviderInterrupt::default() - }); + let active = Arc::new(AgentRuntimeProviderInterrupt::default()); let mut registry = game_creator_agent_provider_interrupts() .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); @@ -2183,14 +1939,12 @@ mod shutdown_tests { &known_root, "code-prototype", "known-run", - 0, ) .expect("register known Provider request"); let (other_key, other_request) = register_game_creator_agent_runtime_provider_request( &other_root, "code-prototype", "other-run", - 0, ) .expect("register other Provider request"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index cb7345c01..328bf441a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -1525,139 +1525,6 @@ pub(crate) fn clear_game_creator_agent_goal( ) } -#[tauri::command] -#[allow(dead_code)] -// Tauri IPC 入口:前端暂无调用方,内部实现 `*_at` 仍被 `--agent-steer`、goal 与测试使用。 -// 保留注册以维持既有 App IPC 表面;重接前端入口或删除属于单独的 native 能力取舍。 -pub(crate) async fn steer_game_creator_agent_runtime_task( - project_path: String, - agent_id: String, - session_id: String, - run_id: String, - steer_id: String, - instruction: String, - run_profile: Option, - source: Option, -) -> Result { - let root = Path::new(project_path.trim()); - enforce_project_permission_policy(root, "conversation.read")?; - enforce_project_permission_policy(root, "conversation.write")?; - enforce_project_permission_policy(root, "agent.run_status")?; - if let Some(source) = source - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - { - if !agent_runtime_supervisor_source_is_trusted(source) { - return Err("Project Supervisor steer source 不受信任".to_string()); - } - let task = read_latest_game_creator_agent_runtime_task_by_run_id( - root, - agent_id.trim(), - run_id.trim(), - )? - .ok_or_else(|| "Agent Runtime steer 的 Run 不存在".to_string())?; - if task.source != source { - return Err("Agent Runtime steer source 与当前 Run 不一致".to_string()); - } - } - let mut result = steer_game_creator_agent_runtime_task_for_profile_at( - root, - agent_id.trim(), - session_id.trim(), - run_id.trim(), - steer_id.trim(), - instruction.trim(), - run_profile.as_deref(), - "tauri", - )?; - let external_runner = - external_agent_runner_enabled() && !external_agent_runner_is_server_process(); - let wake_external_runner_without_interrupt = || -> Result<(), String> { - let provider_interrupted = - steer_external_agent_runner(root, agent_id.trim(), run_id.trim(), steer_id.trim())?; - if provider_interrupted { - return Err("Agent Runner 的 runtime.steer 非法中断了 Provider".to_string()); - } - Ok(()) - }; - let state = result.runtime.state.clone(); - let requires_supervisor_decision = state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - && state.parent_agent_id.is_none() - && state.parent_run_id.is_none(); - if requires_supervisor_decision { - match decide_game_creator_agent_runtime_steer_at( - root, - &state, - steer_id.trim(), - result.sequence, - instruction.trim(), - ) - .await - { - Ok(decision) => { - result.assistant_reply = Some(decision.reply.clone()); - result.interrupt_decision = Some(decision.interrupt_current_provider); - result.decision_reason = Some(decision.reason.clone()); - if decision.interrupt_current_provider { - result.provider_interrupted = if external_runner { - interrupt_external_agent_runner_provider_for_steer_decision( - root, - agent_id.trim(), - run_id.trim(), - steer_id.trim(), - )? - } else { - interrupt_game_creator_agent_runtime_provider_for_decided_steer_at( - root, - agent_id.trim(), - run_id.trim(), - steer_id.trim(), - )? - }; - if !external_runner { - wake_pending_game_creator_agent_background_tasks_at(root) - .map_err(|error| error.to_string())?; - } - } else if external_runner { - wake_external_runner_without_interrupt()?; - } else { - wake_pending_game_creator_agent_background_tasks_at(root) - .map_err(|error| error.to_string())?; - } - } - Err(error) => { - result.assistant_reply = Some( - append_game_creator_agent_runtime_steer_decision_failure_reply_at( - root, - &state, - steer_id.trim(), - &error, - )?, - ); - result.decision_reason = Some("decision-failed".to_string()); - if external_runner { - wake_external_runner_without_interrupt()?; - } else { - wake_pending_game_creator_agent_background_tasks_at(root) - .map_err(|error| error.to_string())?; - } - } - } - } else if external_runner { - wake_external_runner_without_interrupt()?; - } else { - wake_pending_game_creator_agent_background_tasks_at(root) - .map_err(|error| error.to_string())?; - } - result.runtime = read_game_creator_agent_runtime_for_session_at( - root, - agent_id.trim(), - Some(session_id.trim()), - )?; - Ok(result) -} - #[tauri::command] pub(crate) fn cancel_game_creator_agent_runtime_task( project_path: String, diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 2dc5cf29c..47ffa4e5d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -823,12 +823,6 @@ struct AgentRuntimeSteerResult { sequence: u64, status: String, provider_interrupted: bool, - #[serde(skip_serializing_if = "Option::is_none")] - assistant_reply: Option, - #[serde(skip_serializing_if = "Option::is_none")] - interrupt_decision: Option, - #[serde(skip_serializing_if = "Option::is_none")] - decision_reason: Option, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] @@ -2621,7 +2615,6 @@ fn main() { pause_game_creator_agent_goal, resume_game_creator_agent_goal, clear_game_creator_agent_goal, - steer_game_creator_agent_runtime_task, cancel_game_creator_agent_runtime_task, retry_game_creator_agent_runtime_task, confirm_retry_game_creator_agent_runtime_task, diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner.rs b/apps/ai-game-creator-shell/src-tauri/src/runner.rs index bdc25e741..92deb5c11 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner.rs @@ -13,8 +13,7 @@ pub(crate) use client::{ configure_external_agent_runner, configure_external_agent_runner_read_only, continue_external_agent_runner_action, ensure_external_agent_runner_started, ensure_external_agent_runner_started_for_gui, hold_external_agent_runner_gui_participant_lock, - install_external_agent_runner_platform_session, - interrupt_external_agent_runner_provider_for_steer_decision, notify_external_agent_runner, + install_external_agent_runner_platform_session, notify_external_agent_runner, pause_external_agent_runner, read_external_agent_runner_status, require_external_agent_runner_configured_for_cli_runtime_write, require_external_agent_runner_for_cli_runtime_write, resume_external_agent_runner, diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner/client.rs b/apps/ai-game-creator-shell/src-tauri/src/runner/client.rs index 72ee2dc83..a171ec157 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner/client.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner/client.rs @@ -2071,29 +2071,6 @@ pub(crate) fn steer_external_agent_runner( parse_external_agent_runner_steer_result(&result) } -pub(crate) fn interrupt_external_agent_runner_provider_for_steer_decision( - root: &Path, - agent: &str, - run_id: &str, - steer_id: &str, -) -> Result { - if [agent, run_id, steer_id] - .into_iter() - .any(|value| value.trim().is_empty()) - { - return Err("LLM steer 中断判定必须同时提供 agent/runId/steerId".to_string()); - } - let result = send_external_agent_runner_runtime_request( - root, - "runtime.interrupt_for_steer_decision", - Some(agent.trim()), - Some(run_id.trim()), - None, - Some(steer_id.trim()), - )?; - parse_external_agent_runner_steer_result(&result) -} - pub(crate) fn pause_external_agent_runner( root: &Path, agent: &str, diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs b/apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs index 0d71971be..831fa14af 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner/dispatch.rs @@ -576,7 +576,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim( | "runtime.resume" | "runtime.continue_action" | "runtime.steer" - | "runtime.interrupt_for_steer_decision" | "runtime.pause" | "runtime.cancel" | "runtime.compact" @@ -634,7 +633,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim( | "runtime.resume" | "runtime.continue_action" | "runtime.steer" - | "runtime.interrupt_for_steer_decision" | "runtime.pause" | "runtime.cancel" | "runtime.compact" => { @@ -684,23 +682,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim( "providerInterrupted": false, })) })(), - "runtime.interrupt_for_steer_decision" => (|| { - let agent = external_agent_runner_request_agent(request)?; - let run_id = external_agent_runner_request_run_id(request)?; - let steer_id = external_agent_runner_request_steer_id(request)?; - let provider_interrupted = crate::interrupt_game_creator_agent_runtime_provider_for_decided_steer_at( - &root, - &agent, - &run_id, - &steer_id, - )?; - crate::wake_pending_game_creator_agent_background_tasks_at(&root) - .map_err(|error| error.to_string())?; - Ok(json!({ - "accepted": true, - "providerInterrupted": provider_interrupted, - })) - })(), "runtime.pause" => (|| { let agent = external_agent_runner_request_agent(request)?; let run_id = external_agent_runner_request_run_id(request)?; @@ -1157,7 +1138,6 @@ pub(super) fn handle_external_agent_runner_request( | "runtime.resume" | "runtime.continue_action" | "runtime.steer" - | "runtime.interrupt_for_steer_decision" | "runtime.pause" | "runtime.cancel" | "runtime.compact" diff --git a/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs index e70d8172e..08c03c365 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/runner/tests.rs @@ -2008,7 +2008,6 @@ fn runtime_steer_queues_without_interrupt_and_deduplicates_request_id() { &root, "code-prototype", "run-steer-rpc", - 0, ) .expect("register active Provider before runtime.steer"); let request = ExternalAgentRunnerRequest { @@ -2054,293 +2053,6 @@ fn runtime_steer_queues_without_interrupt_and_deduplicates_request_id() { crate::unregister_game_creator_agent_runtime_provider_request(&provider_key, &active_provider); } -#[test] -fn runtime_interrupt_for_steer_decision_rejects_missing_durable_decision() { - let directory = unique_test_directory(); - let root = directory.0.join("project"); - crate::init_local_game_project_at( - &root, - "project-steer-missing-decision-rpc", - "Runner steer 缺少判定测试", - ) - .expect("initialize missing-decision project"); - let runtime = crate::start_game_creator_agent_runtime_task_for_session_at( - &root, - "code-prototype", - None, - "保持当前 Provider 请求运行", - "run-steer-missing-decision-rpc", - "agent-background-task", - "等待 Supervisor 判定", - vec!["完成当前任务".to_string()], - ) - .expect("start missing-decision runtime"); - let steer = crate::steer_game_creator_agent_runtime_task_at( - &root, - &runtime.agent_id, - &runtime.session_id, - &runtime.run_id, - "steer-missing-decision-rpc-1", - "先告诉我进度。", - "runner-test", - ) - .expect("persist steer without decision"); - assert_eq!(steer.sequence, 1); - let (provider_key, active_provider) = - crate::register_game_creator_agent_runtime_provider_request( - &root, - &runtime.agent_id, - &runtime.run_id, - 0, - ) - .expect("register active Provider before missing decision request"); - let token = "steer-missing-decision-token-steer-missing-decision-token"; - let state = ExternalAgentRunnerServerState::new( - directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), - test_endpoint(token, "steer-missing-decision-boot", 30304), - ); - let response = dispatch_external_agent_runner_runtime_request( - &ExternalAgentRunnerRequest { - protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, - request_id: "steer-missing-decision-request-1".to_string(), - token: token.to_string(), - method: "runtime.interrupt_for_steer_decision".to_string(), - params: ExternalAgentRunnerRequestParams { - root: Some(root.to_string_lossy().into_owned()), - agent: Some(runtime.agent_id.clone()), - run_id: Some(runtime.run_id.clone()), - steer_id: Some(steer.steer_id), - ..ExternalAgentRunnerRequestParams::default() - }, - }, - &state, - ); - - assert!(!response.ok); - assert_eq!( - response.error.as_ref().map(|error| error.code.as_str()), - Some("runtime-error") - ); - assert!(response - .error - .as_ref() - .is_some_and(|error| error.message.contains("尚无 LLM 中断判定"))); - assert!( - !active_provider.interrupted.load(Ordering::Acquire), - "a missing durable LLM decision must leave the active Provider running" - ); - crate::unregister_game_creator_agent_runtime_provider_request(&provider_key, &active_provider); -} - -#[test] -fn runtime_interrupt_for_false_steer_decision_keeps_provider_running() { - let directory = unique_test_directory(); - let root = directory.0.join("project"); - crate::init_local_game_project_at( - &root, - "project-steer-false-decision-rpc", - "Runner steer 不中断判定测试", - ) - .expect("initialize false-decision project"); - let runtime = crate::start_game_creator_agent_runtime_task_for_session_at( - &root, - "code-prototype", - None, - "保持当前 Provider 请求运行", - "run-steer-false-decision-rpc", - "agent-background-task", - "处理状态询问", - vec!["完成当前任务".to_string()], - ) - .expect("start false-decision runtime"); - let steer = crate::steer_game_creator_agent_runtime_task_at( - &root, - &runtime.agent_id, - &runtime.session_id, - &runtime.run_id, - "steer-false-decision-rpc-1", - "现在做到哪一步了?", - "runner-test", - ) - .expect("persist status-question steer"); - crate::persist_game_creator_agent_runtime_steer_decision_and_reply_at( - &root, - &runtime, - &steer.steer_id, - steer.sequence, - crate::AgentRuntimeSteerDecision { - reply: "当前任务仍在继续,我会按现有方向完成。".to_string(), - interrupt_current_provider: false, - reason: "状态询问不会让当前方案过期。".to_string(), - }, - ) - .expect("persist non-interrupting LLM decision"); - let (provider_key, active_provider) = - crate::register_game_creator_agent_runtime_provider_request( - &root, - &runtime.agent_id, - &runtime.run_id, - 0, - ) - .expect("register active Provider for false decision"); - let token = "steer-false-decision-token-steer-false-decision-token"; - let state = ExternalAgentRunnerServerState::new( - directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), - test_endpoint(token, "steer-false-decision-boot", 30305), - ); - let response = dispatch_external_agent_runner_runtime_request( - &ExternalAgentRunnerRequest { - protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, - request_id: "steer-false-decision-request-1".to_string(), - token: token.to_string(), - method: "runtime.interrupt_for_steer_decision".to_string(), - params: ExternalAgentRunnerRequestParams { - root: Some(root.to_string_lossy().into_owned()), - agent: Some(runtime.agent_id.clone()), - run_id: Some(runtime.run_id.clone()), - steer_id: Some(steer.steer_id), - ..ExternalAgentRunnerRequestParams::default() - }, - }, - &state, - ); - - assert!( - response.ok, - "false decision RPC failed: {:?}", - response.error - ); - assert_eq!( - response - .result - .as_ref() - .and_then(|value| value["providerInterrupted"].as_bool()), - Some(false) - ); - assert!(!active_provider.interrupted.load(Ordering::Acquire)); - crate::unregister_game_creator_agent_runtime_provider_request(&provider_key, &active_provider); -} - -#[test] -fn runtime_interrupt_for_true_steer_decision_only_interrupts_older_provider_cursor() { - let directory = unique_test_directory(); - let root = directory.0.join("project"); - crate::init_local_game_project_at( - &root, - "project-steer-true-decision-rpc", - "Runner steer 条件中断测试", - ) - .expect("initialize true-decision project"); - let runtime = crate::start_game_creator_agent_runtime_task_for_session_at( - &root, - "code-prototype", - None, - "按当前方向实现玩法", - "run-steer-true-decision-rpc", - "agent-background-task", - "生成当前方案", - vec!["完成当前任务".to_string()], - ) - .expect("start true-decision runtime"); - let steer = crate::steer_game_creator_agent_runtime_task_at( - &root, - &runtime.agent_id, - &runtime.session_id, - &runtime.run_id, - "steer-true-decision-rpc-1", - "停止旧方向,改成全新的玩法。", - "runner-test", - ) - .expect("persist conflicting steer"); - crate::persist_game_creator_agent_runtime_steer_decision_and_reply_at( - &root, - &runtime, - &steer.steer_id, - steer.sequence, - crate::AgentRuntimeSteerDecision { - reply: "这个改动会让旧方案过期,我会安全切换方向。".to_string(), - interrupt_current_provider: true, - reason: "当前 Provider 正在生成已经冲突的旧方案。".to_string(), - }, - ) - .expect("persist interrupting LLM decision"); - let token = "steer-true-decision-token-steer-true-decision-token"; - let state = ExternalAgentRunnerServerState::new( - directory.0.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME), - test_endpoint(token, "steer-true-decision-boot", 30306), - ); - let request = |request_id: &str| ExternalAgentRunnerRequest { - protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, - request_id: request_id.to_string(), - token: token.to_string(), - method: "runtime.interrupt_for_steer_decision".to_string(), - params: ExternalAgentRunnerRequestParams { - root: Some(root.to_string_lossy().into_owned()), - agent: Some(runtime.agent_id.clone()), - run_id: Some(runtime.run_id.clone()), - steer_id: Some(steer.steer_id.clone()), - ..ExternalAgentRunnerRequestParams::default() - }, - }; - - let (old_provider_key, old_provider) = - crate::register_game_creator_agent_runtime_provider_request( - &root, - &runtime.agent_id, - &runtime.run_id, - steer.sequence.saturating_sub(1), - ) - .expect("register Provider created before steer"); - let old_response = dispatch_external_agent_runner_runtime_request( - &request("steer-true-decision-old-provider-request"), - &state, - ); - assert!( - old_response.ok, - "old Provider interrupt RPC failed: {:?}", - old_response.error - ); - assert_eq!( - old_response - .result - .as_ref() - .and_then(|value| value["providerInterrupted"].as_bool()), - Some(true) - ); - assert!(old_provider.interrupted.load(Ordering::Acquire)); - crate::unregister_game_creator_agent_runtime_provider_request(&old_provider_key, &old_provider); - - let (new_provider_key, new_provider) = - crate::register_game_creator_agent_runtime_provider_request( - &root, - &runtime.agent_id, - &runtime.run_id, - steer.sequence, - ) - .expect("register Provider created after steer was applied"); - let new_response = dispatch_external_agent_runner_runtime_request( - &request("steer-true-decision-new-provider-request"), - &state, - ); - assert!( - new_response.ok, - "new Provider keep-running RPC failed: {:?}", - new_response.error - ); - assert_eq!( - new_response - .result - .as_ref() - .and_then(|value| value["providerInterrupted"].as_bool()), - Some(false) - ); - assert!( - !new_provider.interrupted.load(Ordering::Acquire), - "a Provider created at the decided steer cursor must keep running" - ); - crate::unregister_game_creator_agent_runtime_provider_request(&new_provider_key, &new_provider); -} - #[test] fn typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run() { let pause_directory = unique_test_directory(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs index aeb59c559..caada9a36 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/provider.rs @@ -7394,362 +7394,6 @@ fn prompt_context_tail_truncation_keeps_latest_blackboard_and_agent_message() { fs::remove_dir_all(root).ok(); } -#[tokio::test] -async fn agent_runtime_steer_waits_for_llm_decision_before_interrupting_old_provider() { - let root = unique_project_path(); - let state = start_agent_runtime_steer_fixture(&root, "steer-provider-run"); - let (provider_started_tx, provider_started_rx) = tokio::sync::oneshot::channel(); - let wait_root = root.clone(); - let wait_agent = state.agent_id.clone(); - let wait_session = state.session_id.clone(); - let wait_run = state.run_id.clone(); - let mut provider_wait = tokio::spawn(async move { - await_game_creator_agent_runtime_provider_request( - &wait_root, - &wait_agent, - &wait_session, - &wait_run, - "tool-plan", - "test-interrupt", - 0, - async move { - let _ = provider_started_tx.send(()); - std::future::pending::>().await - }, - ) - .await - }); - provider_started_rx.await.expect("provider future started"); - let steer = steer_game_creator_agent_runtime_task_at( - &root, - &state.agent_id, - &state.session_id, - &state.run_id, - "steer-provider-1", - "停止等待旧方案,按新要求重新规划。", - "test", - ) - .expect("queue steer during provider wait"); - assert!(!steer.provider_interrupted); - assert!( - tokio::time::timeout(Duration::from_millis(100), &mut provider_wait) - .await - .is_err(), - "queueing steer alone must leave the active Provider request running" - ); - persist_game_creator_agent_runtime_steer_decision_and_reply_at( - &root, - &state, - "steer-provider-1", - steer.sequence, - AgentRuntimeSteerDecision { - reply: "这个要求会使旧方案过期,我会改向。".to_string(), - interrupt_current_provider: true, - reason: "旧 Provider 正在生成与新要求冲突的方案。".to_string(), - }, - ) - .expect("persist LLM steer decision"); - assert!( - interrupt_game_creator_agent_runtime_provider_for_decided_steer_at( - &root, - &state.agent_id, - &state.run_id, - "steer-provider-1", - ) - .expect("interrupt stale Provider after LLM decision") - ); - let outcome = tokio::time::timeout(Duration::from_secs(2), &mut provider_wait) - .await - .expect("provider wait interrupted after decision") - .expect("provider wait task joined") - .expect("provider wait result"); - assert!(outcome.is_none()); - let lifecycle = read_agent_db_records_for_test(&root) - .into_iter() - .filter(|record| { - record["recordType"] == "agent.runtime.provider_request.lifecycle" - && record["runId"] == state.run_id - }) - .collect::>(); - assert_eq!(lifecycle.len(), 2); - assert_eq!(lifecycle[0]["status"], "started"); - assert_eq!(lifecycle[1]["status"], "interrupted"); - assert_eq!(lifecycle[0]["requestId"], lifecycle[1]["requestId"]); - assert!(lifecycle.iter().all(|record| { - record["auditSchemaVersion"] == "game-creator-provider-request-lifecycle.v2" - && record["webSearchEnabled"] == false - && record["requestKind"] == "tool-plan" - && record["requestSlot"] == "test-interrupt" - && record.get("prompt").is_none() - && record.get("response").is_none() - && record.get("error").is_none() - && record.get("baseUrl").is_none() - })); -} - -#[tokio::test] -async fn root_supervisor_steer_decision_uses_llm_and_persists_non_terminal_reply() { - let root = unique_project_path(); - init_local_game_project_at(&root, "supervisor-steer-llm", "总控边做边聊测试") - .expect("initialize Supervisor steer decision project"); - let state = start_game_creator_agent_runtime_task_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "完成当前可玩版本", - "supervisor-steer-llm-run", - AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, - "正在实现玩法", - vec!["实现并验证玩法".to_string()], - ) - .expect("start Supervisor runtime"); - let steer = steer_game_creator_agent_runtime_task_at( - &root, - &state.agent_id, - &state.session_id, - &state.run_id, - "supervisor-steer-llm-1", - "你现在在做什么?", - "test", - ) - .expect("queue Supervisor status question"); - let arguments = serde_json::json!({ - "reply": "我正在实现核心玩法,当前任务会继续。", - "interruptCurrentProvider": false, - "reason": "状态询问不会让当前方案过期。" - }) - .to_string(); - let (sender, receiver) = mpsc::channel(); - let base_url = spawn_mock_llm_raw_responses_with_capture( - vec![native_agent_tool_plan_chat_response( - "steer-decision-call", - AGENT_RUNTIME_STEER_DECISION_TOOL, - arguments, - )], - Some(sender), - ); - let _config_guard = write_test_local_config(format!( - r#"{{ - "agentMode": "provider", - "agentLlm": {{ - "project-supervisor": {{ - "apiKey": "steer-decision-key", - "baseUrl": {base_url:?}, - "model": "steer-decision-model", - "apiKind": "openai_chat", - "stream": false, - "maxRetries": 0 - }} - }} -}}"# - )); - - let decision = decide_game_creator_agent_runtime_steer_at( - &root, - &state, - &steer.steer_id, - steer.sequence, - "你现在在做什么?", - ) - .await - .expect("run Supervisor steer LLM decision"); - - assert_eq!(decision.reply, "我正在实现核心玩法,当前任务会继续。"); - assert!(!decision.interrupt_current_provider); - let request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("capture Supervisor steer decision request"); - assert!(request.contains(AGENT_RUNTIME_STEER_DECISION_TOOL)); - assert!(request.contains("你现在在做什么?")); - let project_conversation = - read_local_conversation_at(&root, None).expect("read public Supervisor steer reply"); - assert_eq!( - project_conversation - .messages - .iter() - .filter(|message| message.content == decision.reply) - .count(), - 1 - ); - fs::remove_dir_all(root).ok(); -} - -#[tokio::test] -async fn root_supervisor_steer_decision_failure_keeps_provider_and_persists_fallback_reply() { - let root = unique_project_path(); - init_local_game_project_at( - &root, - "supervisor-steer-decision-failure", - "总控判定失败继续任务测试", - ) - .expect("initialize Supervisor steer decision failure project"); - let state = start_game_creator_agent_runtime_task_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "完成当前可玩版本", - "supervisor-steer-decision-failure-run", - AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, - "正在实现玩法", - vec!["实现并验证玩法".to_string()], - ) - .expect("start Supervisor runtime"); - let (provider_started_tx, provider_started_rx) = tokio::sync::oneshot::channel(); - let wait_root = root.clone(); - let wait_agent = state.agent_id.clone(); - let wait_session = state.session_id.clone(); - let wait_run = state.run_id.clone(); - let mut provider_wait = tokio::spawn(async move { - await_game_creator_agent_runtime_provider_request( - &wait_root, - &wait_agent, - &wait_session, - &wait_run, - "tool-plan", - "steer-decision-failure-active-provider", - 0, - async move { - let _ = provider_started_tx.send(()); - std::future::pending::>().await - }, - ) - .await - }); - provider_started_rx.await.expect("provider future started"); - let steer = steer_game_creator_agent_runtime_task_at( - &root, - &state.agent_id, - &state.session_id, - &state.run_id, - "supervisor-steer-decision-failure-1", - "现在做到哪里了?", - "test", - ) - .expect("queue Supervisor steer before failed decision"); - let base_url = spawn_mock_llm_raw_responses_with_capture( - vec![native_agent_tool_plan_chat_response( - "unexpected-steer-decision-call", - "runtime_execute", - "{}".to_string(), - )], - None, - ); - let _config_guard = write_test_local_config(format!( - r#"{{ - "agentMode": "provider", - "agentLlm": {{ - "project-supervisor": {{ - "apiKey": "steer-decision-failure-key", - "baseUrl": {base_url:?}, - "model": "steer-decision-failure-model", - "apiKind": "openai_chat", - "stream": false, - "maxRetries": 0 - }} - }} -}}"# - )); - - let error = decide_game_creator_agent_runtime_steer_at( - &root, - &state, - &steer.steer_id, - steer.sequence, - "现在做到哪里了?", - ) - .await - .expect_err("invalid decision tool must fail closed"); - let fallback = append_game_creator_agent_runtime_steer_decision_failure_reply_at( - &root, - &state, - &steer.steer_id, - &error, - ) - .expect("persist non-terminal decision failure reply"); - - assert!( - tokio::time::timeout(Duration::from_millis(100), &mut provider_wait) - .await - .is_err(), - "failed steer decision must not interrupt the active Provider" - ); - let runtime = read_game_creator_agent_runtime_at(&root, &state.agent_id) - .expect("read continuing Supervisor runtime"); - assert_eq!(runtime.state.run_id, state.run_id); - assert!(!matches!( - runtime.state.status.as_str(), - "completed" | "failed" | "cancelled" - )); - let project_conversation = - read_local_conversation_at(&root, None).expect("read public fallback reply"); - assert_eq!( - project_conversation - .messages - .iter() - .filter(|message| message.content == fallback) - .count(), - 1 - ); - assert!(read_game_creator_agent_runtime_steer_decision_at( - &root, - &state.agent_id, - &state.run_id, - &steer.steer_id, - ) - .expect("read absent failed decision") - .is_none()); - - provider_wait.abort(); - let _ = provider_wait.await; - fs::remove_dir_all(root).ok(); -} - -#[test] -fn steer_decision_never_interrupts_provider_started_after_steer_was_applied() { - let root = unique_project_path(); - let state = start_agent_runtime_steer_fixture(&root, "steer-new-provider-run"); - let steer = steer_game_creator_agent_runtime_task_at( - &root, - &state.agent_id, - &state.session_id, - &state.run_id, - "steer-new-provider-1", - "改成新的实现方向。", - "test", - ) - .expect("queue steer before new Provider"); - persist_game_creator_agent_runtime_steer_decision_and_reply_at( - &root, - &state, - "steer-new-provider-1", - steer.sequence, - AgentRuntimeSteerDecision { - reply: "我会按新方向继续。".to_string(), - interrupt_current_provider: true, - reason: "旧方向已经过期。".to_string(), - }, - ) - .expect("persist interrupt decision"); - let (key, active) = register_game_creator_agent_runtime_provider_request( - &root, - &state.agent_id, - &state.run_id, - steer.sequence, - ) - .expect("register Provider created after steer application"); - - assert!( - !interrupt_game_creator_agent_runtime_provider_for_decided_steer_at( - &root, - &state.agent_id, - &state.run_id, - "steer-new-provider-1", - ) - .expect("keep new Provider running") - ); - assert!(!active.interrupted.load(Ordering::Acquire)); - unregister_game_creator_agent_runtime_provider_request(&key, &active); - fs::remove_dir_all(root).ok(); -} - #[tokio::test] async fn agent_runtime_durable_cancel_after_provider_registration_starts_no_request() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs index 9515a6e4a..ef7d00ac7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_state.rs @@ -5061,107 +5061,6 @@ fn agent_runtime_steer_is_idempotent_rejects_conflicts_and_enforces_capacity() { .is_err()); } -#[test] -fn root_supervisor_steer_persists_one_correlated_public_acknowledgement() { - let root = unique_project_path(); - init_local_game_project_at(&root, "project-steer-public-ack", "总控追加消息确认测试") - .expect("initialize Supervisor steer fixture"); - let state = start_game_creator_agent_runtime_task_at( - &root, - GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, - "制作一个可玩的游戏", - "supervisor-steer-public-ack-run", - "project-supervisor-gui", - "正在制作游戏", - vec!["完成可玩版本".to_string()], - ) - .expect("start root Supervisor runtime"); - - for _ in 0..2 { - steer_game_creator_agent_runtime_task_at( - &root, - &state.agent_id, - &state.session_id, - &state.run_id, - "supervisor-steer-public-ack-1", - "你在做什么?", - "test", - ) - .expect("queue idempotent Supervisor steer"); - } - for _ in 0..2 { - persist_game_creator_agent_runtime_steer_decision_and_reply_at( - &root, - &state, - "supervisor-steer-public-ack-1", - 1, - AgentRuntimeSteerDecision { - reply: "我正在完成玩法实现;这是状态询问,当前任务会继续。".to_string(), - interrupt_current_provider: false, - reason: "状态询问不需要中断当前 Provider。".to_string(), - }, - ) - .expect("persist idempotent Supervisor steer reply"); - } - - let supervisor_conversation = read_local_conversation_for_session_at( - &root, - Some(&state.agent_id), - Some(&state.session_id), - ) - .expect("read Supervisor steer conversation"); - let steer_message = supervisor_conversation - .messages - .iter() - .find(|message| message.role == "user" && message.content == "你在做什么?") - .expect("find persisted steer user message"); - let correlation_id = steer_message - .message_id - .as_deref() - .and_then(|message_id| message_id.strip_prefix("agent-steer-")) - .expect("read steer correlation id"); - - let project_conversation = - read_local_conversation_at(&root, None).expect("read public steer acknowledgement"); - let acknowledgements = project_conversation - .messages - .iter() - .filter(|message| { - message.content == "收到。我正在判断这条消息是否需要调整当前任务;现有任务会继续运行。" - }) - .collect::>(); - assert_eq!(acknowledgements.len(), 1); - assert!(acknowledgements[0] - .message_id - .as_deref() - .is_some_and(|message_id| { - message_id.starts_with(&format!( - "{AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX}{correlation_id}-" - )) - })); - assert_eq!( - project_conversation - .messages - .iter() - .filter(|message| { - message.content == "我正在完成玩法实现;这是状态询问,当前任务会继续。" - }) - .count(), - 1 - ); - let decision = read_game_creator_agent_runtime_steer_decision_at( - &root, - &state.agent_id, - &state.run_id, - "supervisor-steer-public-ack-1", - ) - .expect("read persisted Supervisor steer decision") - .expect("Supervisor steer decision exists"); - assert!(!decision.interrupt_current_provider); - - fs::remove_dir_all(root).ok(); -} - #[test] fn agent_runtime_steer_sequences_are_unique_under_concurrent_acceptance() { let root = unique_project_path();