diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs index 1d0ecf233..8859b133f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_app_server.rs @@ -2842,6 +2842,8 @@ impl CodexAppServerConnection { callback(&platform_llm::LlmStreamDelta { accumulated_text: streamed_text.clone(), delta_text: delta, + accumulated_reasoning: String::new(), + reasoning_delta: String::new(), finish_reason: None, }); } @@ -3146,6 +3148,7 @@ fn parse_game_creator_codex_app_server_text( } else { String::new() }, + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: Some(thread_id.to_string()), usage: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs index 8af1c9d2d..23fd00fe9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/codex_cli.rs @@ -589,6 +589,7 @@ fn parse_game_creator_codex_cli_response( } else { String::new() }, + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id, usage, 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 3cd44b4bd..ac64025ac 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 @@ -1285,6 +1285,7 @@ mod tests { provider: platform_llm::LlmProvider::OpenAiCompatible, model: "fake-design".into(), text: text.into(), + reasoning: String::new(), finish_reason: Some(if calls.is_empty() { "stop".into() } else { 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 index d53f2f58e..d0cb7c0bf 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/interaction.rs @@ -409,6 +409,8 @@ where (self.on_delta)(&platform_llm::LlmStreamDelta { accumulated_text, delta_text, + accumulated_reasoning: String::new(), + reasoning_delta: String::new(), finish_reason, }); } @@ -499,6 +501,7 @@ mod tests { 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, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs index 8cd93a2c5..d0e3a92a0 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/response_stream_tests.rs @@ -115,6 +115,7 @@ fn persist_tool_plan_handoff_repair_chain( provider: platform_llm::LlmProvider::OpenAiCompatible, model: llm.model.clone(), text: text.to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -146,6 +147,8 @@ fn stream_delta(delta_text: &str, accumulated_text: &str) -> platform_llm::LlmSt platform_llm::LlmStreamDelta { accumulated_text: accumulated_text.to_string(), delta_text: delta_text.to_string(), + accumulated_reasoning: String::new(), + reasoning_delta: String::new(), finish_reason: None, } } @@ -1003,6 +1006,7 @@ async fn provider_handoff_identity_drift_closes_lifecycle_without_leaking_respon provider: platform_llm::LlmProvider::OpenAiCompatible, model: old_llm.model.clone(), text: private_response.to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -1116,6 +1120,7 @@ async fn tool_plan_handoff_identity_drift_closes_entire_repair_chain_before_remo provider: platform_llm::LlmProvider::OpenAiCompatible, model: old_llm.model.clone(), text: text.to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -1302,6 +1307,7 @@ async fn tool_plan_capacity_gate_runs_before_provider_lifecycle_and_network() { provider: platform_llm::LlmProvider::OpenAiCompatible, model: llm.model.clone(), text: format!("capacity response {loop_iteration}"), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -1436,6 +1442,7 @@ async fn tool_plan_handoff_durable_control_closes_entire_repair_chain_before_rem provider: platform_llm::LlmProvider::OpenAiCompatible, model: llm.model.clone(), text: text.to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -1552,6 +1559,7 @@ fn provider_recovery_cleanup_closes_tool_plan_lifecycle_before_removing_handoff( provider: platform_llm::LlmProvider::OpenAiCompatible, model: llm.model.clone(), text: "cleanup handoff".to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -1623,6 +1631,7 @@ fn runtime_resume_scans_and_cleans_terminal_tool_plan_handoff() { provider: platform_llm::LlmProvider::OpenAiCompatible, model: llm.model.clone(), text: "terminal handoff".to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, @@ -1718,6 +1727,7 @@ async fn provider_handoff_retry_conflict_preserves_both_sidecars_for_reconciliat provider: platform_llm::LlmProvider::OpenAiCompatible, model: llm.model.clone(), text: "已成功但尚未消费的回复".to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: None, usage: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_control.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_control.rs index dc5ad1e60..bd6f8b6b7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_control.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/provider_control.rs @@ -798,6 +798,7 @@ mod provider_reconciliation_diagnostic_tests { let response = platform_llm::LlmRunResponse { provider: platform_llm::LlmProvider::OpenAiCompatible, model: "test-model".to_string(), + reasoning: String::new(), text: "C:\\private\\response".to_string(), finish_reason: Some("completed".to_string()), response_id: Some("response-1".to_string()), diff --git a/apps/ai-game-creator-shell/src-tauri/src/context_compaction.rs b/apps/ai-game-creator-shell/src-tauri/src/context_compaction.rs index d605b6e2c..419e00d23 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/context_compaction.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/context_compaction.rs @@ -905,6 +905,7 @@ mod tests { provider: platform_llm::LlmProvider::OpenAiCompatible, model: "context-compaction-test".to_string(), text: summary.into(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: Some("context-compaction-response".to_string()), usage: Some(platform_llm::LlmTokenUsage { diff --git a/apps/ai-game-creator-shell/src-tauri/src/provider_handoff.rs b/apps/ai-game-creator-shell/src-tauri/src/provider_handoff.rs index bae190561..e4cf78d4c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/provider_handoff.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/provider_handoff.rs @@ -51,6 +51,7 @@ impl AgentRuntimeProviderHandoffRecord { provider: self.response.provider, model: self.response.model.clone(), text: self.response.text.clone(), + reasoning: String::new(), finish_reason: self.response.finish_reason.clone(), response_id: self.response.response_id.clone(), usage: self.response.usage.clone(), @@ -340,6 +341,7 @@ mod tests { provider: LlmProvider::OpenAiCompatible, model: "handoff-model".to_string(), text: text.to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: Some("response-handoff".to_string()), usage: Some(LlmTokenUsage { 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 b0ce174bc..20a6dfc0d 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 @@ -2775,6 +2775,7 @@ fn durable_provider_handoff_prevents_shutdown_even_when_corrupt() { provider: platform_llm::LlmProvider::OpenAiCompatible, model: "provider-handoff-runner-test".to_string(), text: "durable final reply".to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: Some("provider-handoff-response".to_string()), usage: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs index f66a45540..28b4dcbc1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/mod.rs @@ -4475,6 +4475,7 @@ fn real_e2e_tool_plan_checkpoint_response() -> platform_llm::LlmRunResponse { provider: platform_llm::LlmProvider::OpenAiCompatible, model: "real-e2e-checkpoint-model".to_string(), text: REAL_E2E_TOOL_PLAN_CHECKPOINT_PRIVATE_RESPONSE.to_string(), + reasoning: String::new(), finish_reason: Some("tool_calls".to_string()), response_id: Some("real-e2e-checkpoint-private-response-id".to_string()), usage: None, @@ -4720,6 +4721,7 @@ fn agent_tool_plan_llm_response( provider: platform_llm::LlmProvider::OpenAiCompatible, model: "mock-game-model".to_string(), text: text.into(), + reasoning: String::new(), finish_reason: Some("tool_calls".to_string()), response_id: Some("response-tool-plan-test".to_string()), usage: None, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/model.rs b/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/model.rs index c7f5451eb..f2a14571f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/model.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/model.rs @@ -127,6 +127,7 @@ impl AgentRuntimeToolPlanHandoffEntry { provider: self.response.provider, model: self.response.model.clone(), text, + reasoning: String::new(), finish_reason: self.response.finish_reason.clone(), response_id: self.response.response_id.clone(), usage: self.response.usage.as_ref().map(LlmTokenUsage::from), diff --git a/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/tests.rs index e9f048dbd..2e7f6fe79 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tool_plan_handoff/tests.rs @@ -84,6 +84,7 @@ fn response(text: &str, tool_calls: Vec) -> LlmRunResponse { provider: LlmProvider::OpenAiCompatible, model: "tool-plan-handoff-model".to_string(), text: text.to_string(), + reasoning: String::new(), finish_reason: Some("tool_calls".to_string()), response_id: Some("tool-plan-handoff-response".to_string()), usage: Some(LlmTokenUsage { diff --git a/docs/project-memory/plans/【实施计划】Provider推理契约与兼容开关-2026-09-14.md b/docs/project-memory/plans/【实施计划】Provider推理契约与兼容开关-2026-09-14.md new file mode 100644 index 000000000..17d0068e5 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】Provider推理契约与兼容开关-2026-09-14.md @@ -0,0 +1,67 @@ +# 【实施计划】Provider 推理契约与兼容开关 + +| 字段 | 值 | +| --- | --- | +| Milestone | `docs/project-memory/plans/【里程碑】Provider推理与正文分离及策划Agent展示-2026-09-14.md` | +| Status | awaiting-review | +| Owner | Codex | + +## 修改边界 + +允许修改: + +- `server-rs/crates/platform-llm/src/lib.rs` 中的 Provider 请求/响应共享类型; +- 受新增字段影响的 Rust 构造点和测试辅助函数; +- 与共享字段语义直接相关的单元测试; +- 本实施计划文档。 + +明确不修改: + +- Responses / Chat reasoning 解析逻辑; +- `design_runtime.rs`、Tauri 事件和前端 UI; +- GameAgent 的正文过滤、工具调用和消息展示逻辑; +- HTTP、OpenAPI、SpacetimeDB 和持久化 schema。 + +## 实现顺序 + +1. 在 `LlmStreamDelta` 和 `LlmRunResponse` 中增加独立 reasoning 字段,并明确空值语义。 +2. 在 `LlmRunRequest` 增加默认关闭的 reasoning 捕获开关,确保既有请求默认行为不变。 +3. 补齐共享层及 AGC Rust 侧全部结构体构造点,新增字段统一使用空值。 +4. 增加契约级测试,证明默认关闭时正文、工具调用和 finish reason 语义不受影响。 +5. 检查 GameAgent 及其它调用方仍只读取正文字段,形成第一轮验收证据。 + +## 验证命令 + +```text +cargo test -p platform-llm +cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml +npm run check:encoding +git diff --check +``` + +## 风险与回滚点 + +- 共享结构体字段增加会触及多个构造点;以编译错误和全量搜索确保没有遗漏。 +- reasoning 字段只能作为旁路数据,任何正文拼接都不属于本轮范围。 +- 若发现现有调用方依赖结构体字段数量或序列化形状,停止扩展并回滚共享契约改动。 +- 本轮可整体回退共享类型与构造点提交,不影响既有策划 UI 预留代码。 + +## 第一轮执行记录 + +- 已完成:`LlmRunRequest.capture_reasoning` 默认关闭,并提供显式 builder;该字段不会进入 Provider 请求体。 +- 已完成:`LlmStreamDelta` 增加独立 reasoning 增量/累计字段;本轮所有现有调用点填空值。 +- 已完成:`LlmRunResponse` 增加独立 reasoning 字段;本轮所有现有解析和恢复构造点填空值。 +- 已确认:未修改 reasoning 解析逻辑、策划事件、前端 UI、GameAgent 正文过滤和工具调用规则。 + +### 第一轮验收证据 + +| 证据 | 结果 | +| --- | --- | +| `cargo test --manifest-path server-rs/Cargo.toml -p platform-llm` | PASS,146 个测试通过 | +| `cargo check --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --tests` | PASS | +| `cargo fmt --manifest-path server-rs/Cargo.toml --all -- --check` | PASS | +| `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` | PASS | +| `npm run check:encoding` | PASS | +| `git diff --check` | PASS | + +第一轮完成后暂停,等待评审通过再进入 Provider reasoning 解析和策划 Runtime 接通。 diff --git a/server-rs/crates/api-server/src/llm/mod.rs b/server-rs/crates/api-server/src/llm/mod.rs index a82a1a021..9d53dd690 100644 --- a/server-rs/crates/api-server/src/llm/mod.rs +++ b/server-rs/crates/api-server/src/llm/mod.rs @@ -120,6 +120,7 @@ pub async fn proxy_llm_chat_completions( request_timeout_ms: None, response_reasoning_effort: None, response_text_verbosity: None, + capture_reasoning: false, function_tools: Vec::new(), tool_choice: None, }; diff --git a/server-rs/crates/platform-agent/src/apimart_gpt5_adapter.rs b/server-rs/crates/platform-agent/src/apimart_gpt5_adapter.rs index 713a4e938..9fbe66c67 100644 --- a/server-rs/crates/platform-agent/src/apimart_gpt5_adapter.rs +++ b/server-rs/crates/platform-agent/src/apimart_gpt5_adapter.rs @@ -55,6 +55,7 @@ pub fn build_gpt5_multimodal_request( api_kind: LlmApiKind::OpenAiChat, response_reasoning_effort: None, response_text_verbosity: None, + capture_reasoning: false, function_tools: Vec::new(), tool_choice: None, } diff --git a/server-rs/crates/platform-llm/src/lib.rs b/server-rs/crates/platform-llm/src/lib.rs index d21c888d5..5fce9cb5d 100644 --- a/server-rs/crates/platform-llm/src/lib.rs +++ b/server-rs/crates/platform-llm/src/lib.rs @@ -180,6 +180,8 @@ pub struct LlmRunRequest { pub request_timeout_ms: Option, pub response_reasoning_effort: Option, pub response_text_verbosity: Option, + /// 是否把 Provider 返回的内部 reasoning 作为独立旁路字段暴露给调用方;默认关闭。 + pub capture_reasoning: bool, pub function_tools: Vec, pub tool_choice: Option, } @@ -237,6 +239,10 @@ impl LlmResponseTextVerbosity { pub struct LlmStreamDelta { pub accumulated_text: String, pub delta_text: String, + /// 与正文分离的 Provider 推理文本;未捕获或没有数据时为空。 + pub accumulated_reasoning: String, + /// 当前回调的推理增量;不得追加到正文。 + pub reasoning_delta: String, pub finish_reason: Option, } @@ -254,6 +260,8 @@ pub struct LlmRunResponse { pub provider: LlmProvider, pub model: String, pub text: String, + /// 与正文分离的 Provider 推理文本;未捕获或没有数据时为空。 + pub reasoning: String, pub finish_reason: Option, pub response_id: Option, pub usage: Option, @@ -1253,6 +1261,7 @@ impl LlmRunRequest { request_timeout_ms: None, response_reasoning_effort: None, response_text_verbosity: None, + capture_reasoning: false, function_tools: Vec::new(), tool_choice: None, } @@ -1318,6 +1327,12 @@ impl LlmRunRequest { self } + /// 只设置本地捕获意图,不改变请求的模型、推理档位和协议请求体。 + pub fn with_reasoning_capture(mut self, enabled: bool) -> Self { + self.capture_reasoning = enabled; + self + } + pub fn with_function_tools(mut self, function_tools: Vec) -> Self { self.function_tools = function_tools; self @@ -1914,6 +1929,7 @@ impl LlmClient { provider: self.config.provider(), model: resolved_model, text: content, + reasoning: String::new(), finish_reason: accumulation.finish_reason, response_id, usage: accumulation.usage, @@ -2360,6 +2376,8 @@ where let update = LlmStreamDelta { accumulated_text: accumulation.text.clone(), delta_text, + accumulated_reasoning: String::new(), + reasoning_delta: String::new(), finish_reason: Some(event_finish_reason), }; on_delta(&update); @@ -2368,6 +2386,8 @@ where let update = LlmStreamDelta { accumulated_text: accumulation.text.clone(), delta_text, + accumulated_reasoning: String::new(), + reasoning_delta: String::new(), finish_reason: None, }; on_delta(&update); @@ -3234,6 +3254,7 @@ fn parse_chat_completions_response( provider, model: parsed.model.unwrap_or_else(|| fallback_model.to_string()), text: content, + reasoning: String::new(), finish_reason: first_choice.finish_reason.clone(), response_id: parsed.id, usage: parsed.usage, @@ -3278,6 +3299,7 @@ fn parse_responses_response( provider, model: parsed.model.unwrap_or_else(|| fallback_model.to_string()), text: content, + reasoning: String::new(), finish_reason: parsed.status, response_id: parsed.id, usage: parsed.usage.map(|usage| LlmTokenUsage { @@ -3319,6 +3341,7 @@ fn parse_anthropic_response( provider, model: parsed.model.unwrap_or_else(|| fallback_model.to_string()), text: content, + reasoning: String::new(), finish_reason: parsed.stop_reason, response_id: parsed.id, usage: parsed.usage.map(map_anthropic_usage), @@ -4305,9 +4328,40 @@ mod tests { let request = LlmRunRequest::single_turn("系统", "用户"); assert_eq!(request.api_kind, LlmApiKind::OpenAiResponses); + assert!(!request.capture_reasoning); + assert!( + request + .clone() + .with_reasoning_capture(true) + .capture_reasoning + ); assert_eq!(request.with_openai_chat().api_kind, LlmApiKind::OpenAiChat); } + #[test] + fn reasoning_capture_switch_does_not_change_provider_request_body() { + let config = LlmConfig::new( + LlmProvider::OpenAiCompatible, + "https://example.com/v1".to_string(), + "secret".to_string(), + "model-a".to_string(), + DEFAULT_REQUEST_TIMEOUT_MS, + DEFAULT_MAX_RETRIES, + DEFAULT_RETRY_BACKOFF_MS, + ) + .expect("config should be valid"); + let request = LlmRunRequest::single_turn("系统", "用户"); + let normal = serde_json::to_value(build_request_body(&request, &config, false)) + .expect("normal body should serialize"); + let capture = serde_json::to_value(build_request_body( + &request.clone().with_reasoning_capture(true), + &config, + false, + )) + .expect("capture body should serialize"); + assert_eq!(normal, capture); + } + fn native_responses_output_fixture() -> Vec { vec![ serde_json::json!({ diff --git a/server-rs/crates/platform-llm/src/provider_adapter.rs b/server-rs/crates/platform-llm/src/provider_adapter.rs index 13346ebb6..2426bcc46 100644 --- a/server-rs/crates/platform-llm/src/provider_adapter.rs +++ b/server-rs/crates/platform-llm/src/provider_adapter.rs @@ -419,6 +419,7 @@ pub fn llm_response_from_provider_response( provider, model: response.model().to_string(), text: text_parts.join(""), + reasoning: String::new(), finish_reason: response.finish_reason().map(str::to_string), response_id: response.response_id().map(str::to_string), usage: response.usage().map(|usage| crate::LlmTokenUsage { @@ -882,6 +883,7 @@ mod tests { provider: LlmProvider::OpenAiCompatible, model: "model-1".to_string(), text: "完成".to_string(), + reasoning: String::new(), finish_reason: Some("stop".to_string()), response_id: Some("upstream-response".to_string()), usage: Some(LlmTokenUsage {