接通策划 Agent reasoning 展示

仅为策划 Provider 请求开启 reasoning 捕获

将流式与终态 reasoning 映射到已有折叠事件

补齐回合重试、错误和项目切换的状态清理测试
This commit is contained in:
2026-09-14 12:46:30 +08:00
parent deb327ce1f
commit e8f9929630
3 changed files with 174 additions and 10 deletions
@@ -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<String, String> {
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::<Vec<_>>();
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();
+4
View File
@@ -1041,6 +1041,7 @@ export function App({
setChatAgentBusy(true);
setProjectSupervisorRuntimeError('');
setPlanningV2TransientReplyTarget('');
setPlanningV2Reasoning('');
try {
const view = await invoke<DesignView>('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<PlanningSessionCommandResultV2>(
@@ -11825,6 +11828,7 @@ export function App({
clientTurnId,
};
setPlanningV2TransientReplyTarget('');
setPlanningV2Reasoning('');
setChatAgentBusy(true);
setPlanGddDecisionBusy(true);
void invoke<DesignView>('decide_design_phase', {
@@ -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` | PASS9 个策划 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` | PASS4562 个文件 |
| `npm run check:doc-index` | PASS102 份 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 事件契约。