diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs index ac64025ac..c0bc27a75 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/design_runtime.rs @@ -104,6 +104,17 @@ fn design_event( } } +fn design_reasoning_event( + root: &Path, + turn_id: &str, + id: Option<&str>, + reasoning: String, +) -> DesignEvent { + let mut event = design_event(root, turn_id, "reasoning", id, None, None); + event.reasoning_text = Some(reasoning); + event +} + fn design_project_id(root: &Path) -> Result { validate_project_root(root)?; Ok(read_existing_manifest_for_project(root)?.project_id) @@ -462,6 +473,7 @@ fn build_design_request( .with_tool_choice(platform_llm::LlmToolChoice::Auto) .with_web_search(false); apply_game_creator_llm_reasoning_effort(request, llm) + .map(|request| request.with_reasoning_capture(true)) } // 调试队列只接收副本,写盘慢或失败时丢弃,不参与会话恢复。 @@ -548,6 +560,12 @@ async fn request_design_provider( Some(String::new()), None, )); + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + String::new(), + )); let result = if llm.stream { let mut stream_sequence = 0_u64; client @@ -565,18 +583,30 @@ async fn request_design_provider( "model": llm.model, "deltaChars": delta.delta_text.chars().count(), "accumulatedChars": delta.accumulated_text.chars().count(), + "reasoningDeltaChars": delta.reasoning_delta.chars().count(), + "reasoningAccumulatedChars": delta.accumulated_reasoning.chars().count(), "deltaText": delta.delta_text, "finishReason": delta.finish_reason, }), ); - emit(design_event( - root, - &turn_id, - "text", - Some(&message_id), - Some(delta.accumulated_text.clone()), - None, - )); + if !delta.delta_text.is_empty() || delta.finish_reason.is_some() { + emit(design_event( + root, + &turn_id, + "text", + Some(&message_id), + Some(delta.accumulated_text.clone()), + None, + )); + } + if !delta.reasoning_delta.is_empty() { + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + delta.accumulated_reasoning.clone(), + )); + } }) .await } else { @@ -584,6 +614,14 @@ async fn request_design_provider( }; match result { Ok(response) => { + if !response.reasoning.is_empty() { + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + response.reasoning.clone(), + )); + } design_debug( root, "response", @@ -606,6 +644,12 @@ async fn request_design_provider( || game_creator_agent_runtime_transient_provider_error_kind(&error, false) .is_none() { + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + String::new(), + )); return Err(detail); } tokio::time::sleep(Duration::from_millis( @@ -654,8 +698,24 @@ async fn request_scripted_design_provider( Some(String::new()), None, )); + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + String::new(), + )); match fake_provider::take() { - Some(Ok(response)) => return Ok(response), + Some(Ok(response)) => { + if !response.reasoning.is_empty() { + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + response.reasoning.clone(), + )); + } + return Ok(response); + } Some(Err(error)) => { let detail = redact_agent_runtime_error( root, @@ -666,10 +726,24 @@ async fn request_scripted_design_provider( || game_creator_agent_runtime_transient_provider_error_kind(&error, false) .is_none() { + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + String::new(), + )); return Err(detail); } } - None => return Err("假 Provider 脚本耗尽".into()), + None => { + emit(design_reasoning_event( + root, + &turn_id, + Some(&message_id), + String::new(), + )); + return Err("假 Provider 脚本耗尽".into()); + } } } unreachable!() @@ -1346,6 +1420,37 @@ mod tests { .clone() } + #[test] + fn design_request_enables_reasoning_capture_only_for_design_runtime() { + let session = new_design_session("project", "quality"); + let request = build_design_request(&session, &pack(), &GameCreatorLlmConfig::default()) + .expect("design request"); + assert!(request.capture_reasoning); + } + + #[tokio::test(flavor = "current_thread")] + async fn scripted_design_provider_emits_reasoning_without_persisting_it() { + let (_temp, root, _resources) = init_design_project(); + let mut session = new_design_session("design-fake", "quality"); + begin_design_turn(&mut session, "turn-reasoning"); + let mut response = fake_response("reasoning", "正文", Vec::new()); + response.reasoning = "先分析需求,再组织方案。".into(); + let _fake = fake_provider::install(vec![Ok(response)], 0); + let mut events = Vec::new(); + let response = + request_scripted_design_provider(&root, &mut session, &mut |event| events.push(event)) + .await + .expect("scripted provider"); + + let reasoning_events = events + .iter() + .filter_map(|event| event.reasoning_text.as_deref()) + .collect::>(); + assert_eq!(reasoning_events, vec!["", "先分析需求,再组织方案。"]); + assert_eq!(response.text, "正文"); + assert!(session.history.is_empty()); + } + #[tokio::test(flavor = "current_thread")] async fn fake_provider_walks_five_phases_and_enters_consultant() { let (_temp, root, resources) = init_design_project(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 3aece9802..7861eccad 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -1041,6 +1041,7 @@ export function App({ setChatAgentBusy(true); setProjectSupervisorRuntimeError(''); setPlanningV2TransientReplyTarget(''); + setPlanningV2Reasoning(''); try { const view = await invoke('continue_design_agent_session', { projectPath: nextProjectPath, @@ -1555,6 +1556,7 @@ export function App({ setProjectSupervisorRuntimeError(''); setPlanningV2Session(null); setPlanningV2TransientReplyTarget(''); + setPlanningV2Reasoning(''); setPlanningV2Active(planningStartMode); planningV2ActiveRef.current = planningStartMode; designAgentLaneRef.current = planningStartMode; @@ -6072,6 +6074,7 @@ export function App({ setChatAgentBusy(true); setProjectSupervisorRuntimeError(''); setPlanningV2TransientReplyTarget(''); + setPlanningV2Reasoning(''); try { const result = currentSessionId ? await invoke( @@ -11825,6 +11828,7 @@ export function App({ clientTurnId, }; setPlanningV2TransientReplyTarget(''); + setPlanningV2Reasoning(''); setChatAgentBusy(true); setPlanGddDecisionBusy(true); void invoke('decide_design_phase', { diff --git a/docs/project-memory/plans/【实施计划】策划Runtime接通reasoning展示-2026-09-14.md b/docs/project-memory/plans/【实施计划】策划Runtime接通reasoning展示-2026-09-14.md new file mode 100644 index 000000000..95390aade --- /dev/null +++ b/docs/project-memory/plans/【实施计划】策划Runtime接通reasoning展示-2026-09-14.md @@ -0,0 +1,55 @@ +# 【实施计划】策划 Runtime 接通 reasoning 展示 + +| 字段 | 值 | +| --- | --- | +| Milestone | `docs/project-memory/plans/【里程碑】Provider推理与正文分离及策划Agent展示-2026-09-14.md` | +| Status | awaiting-review | +| Owner | Codex | + +## 修改边界 + +本轮只修改策划 Agent 的 Rust Runtime 事件映射、已有 App 状态清理和对应测试;复用现有 `DesignEvent.reasoningText` 与 `ProjectSupervisorView` 折叠展示,不新建 UI 组件,不修改 GameAgent、Direct/Codex、公开 API、SpacetimeDB 或持久化 schema。 + +## 实现顺序 + +1. 在策划专用 `LlmRunRequest` 上显式开启 `capture_reasoning`,普通 Agent 请求保持默认关闭。 +2. 将流式 `accumulated_reasoning` 和终态 `response.reasoning` 映射到已有 `reasoningText` 事件;每次 attempt 开始先清空,避免重试残留。 +3. 在脚本 Provider、成功/失败和项目切换路径补齐 reasoning 生命周期,debug 记录只记长度与受控内容,不把 reasoning 写入会话 history。 +4. 在 App 现有规划状态重置与事件订阅中清理旧 reasoning,并补充 Runtime/UI 状态测试;复用既有默认折叠展示。 + +## 验收标准 + +- 策划 Agent 请求显式开启 reasoning 捕获;其它 Agent 请求不受影响。 +- 流式 reasoning 独立更新 `DesignEvent.reasoningText`,正文事件仍只携带正文累计值。 +- 重试、新回合、项目切换和错误路径不会残留上一回合 reasoning。 +- reasoning 不进入 `DesignMessage`、普通 history、工具调用参数或 GameAgent 消息流。 +- 现有 AGC Runtime 测试、前端相关测试和格式/编码检查通过。 + +## 本轮验收证据 + +| 证据 | 结果 | +| --- | --- | +| `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell design_runtime` | PASS,9 个策划 Runtime 测试通过 | +| `cargo check --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --tests` | PASS;仓库既有警告未新增为错误 | +| `tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` | PASS | +| `npm run check:encoding` | PASS,4562 个文件 | +| `npm run check:doc-index` | PASS,102 份 Markdown | +| `git diff --check` | PASS | + +`npm run ai-game-creator-shell:typecheck` 的 TypeScript 与 skill-pack 检查通过,但其中既有 `check-config.mjs` 在当前 Windows 沙箱设置私有 DACL 时被系统拒绝;已单独运行 `tsc` 完成类型验证。未连接真实 Provider,未做浏览器运行时 smoke。 + +## 验证命令 + +```text +cargo test --manifest-path server-rs/Cargo.toml -p platform-llm +cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --tests +npm run ai-game-creator-shell:typecheck +npm run check:encoding +git diff --check +``` + +## 风险与回滚点 + +- 事件字段已存在但当前为空,优先补事件映射,不改变消息/会话持久化结构。 +- 终态 reasoning 可能重复流式累计,使用累计值覆盖同一响应槽并以回合开始清空控制生命周期。 +- 若 UI 状态清理影响规划会话恢复,回退 App 清理补丁,保留 Runtime reasoning 事件契约。