接通策划 Agent reasoning 展示
仅为策划 Provider 请求开启 reasoning 捕获 将流式与终态 reasoning 映射到已有折叠事件 补齐回合重试、错误和项目切换的状态清理测试
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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` | 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 事件契约。
|
||||
Reference in New Issue
Block a user