Compare commits

..

17 Commits

Author SHA1 Message Date
lhk229 158148e0c9 Merge branch 'master' into opt/design_agent
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 4m57s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m27s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m37s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m32s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 3m59s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m23s
Project CI / Frontend tests (pull_request) Successful in 4m12s
Project CI / Repository checks (pull_request) Successful in 3m20s
Project CI / Native shell tests (pull_request) Successful in 6m40s
Project CI / Backend tests (pull_request) Successful in 7m27s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m50s
2026-09-14 17:38:31 +08:00
lhk229 45d7dc2a63 修复策划 Agent 历史思考位置
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m8s
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust tests (pull_request) Has been cancelled
按用户回合与响应顺序绑定持久化 reasoning,跨越 tool-only 响应。

合并同一正文对应的多段 reasoning,避免无归属内容堆积在消息列表底部。

补充策划 Runtime 顺序回归测试并更新里程碑文档。
2026-09-14 17:28:00 +08:00
lhk229 0a34f4e514 Merge remote-tracking branch 'origin/master' into opt/design_agent 2026-09-14 17:25:57 +08:00
lhk229 09a9fa7b10 Merge remote-tracking branch 'origin/master' into opt/design_agent
Project CI / Repository checks (pull_request) Successful in 3m13s
Project CI / Frontend tests (pull_request) Successful in 3m41s
Project CI / Backend tests (pull_request) Successful in 6m19s
Project CI / Native shell tests (pull_request) Failing after 17m39s
2026-09-14 15:17:11 +08:00
lhk229 896cfad81e Merge remote-tracking branch 'origin/master' into opt/design_agent 2026-09-14 14:47:57 +08:00
lhk229 9f40f044a5 修复策划 Agent 推理展示生命周期
按项目与回合绑定 reasoning 事件

保留回合结束后的思考过程并在新回合清理

补充默认折叠的展开收起交互与定向测试

更新推理与正文分离里程碑进度
2026-09-14 14:47:33 +08:00
lhk229 f7c8b9f217 修复策划 Agent 正文流式收尾
让最终 DesignView 等待临时正文播放完成后再提交正式消息

为非流式返回复用策划 Agent 的前端伪流式显示

保持 reasoning、GameAgent 和其他 Agent 路径不变
2026-09-14 14:27:22 +08:00
lhk229 13e137191a 修复策划 Agent 事件监听时序
让策划 Agent 回合在调用 Runtime 前等待 design-agent-update 监听注册完成

覆盖普通消息、澄清、重试和阶段批准入口

保持 GameAgent、Direct/Codex 与退役入口行为不变
2026-09-14 14:11:43 +08:00
lhk229 fd88e516b8 Merge branch 'master' into opt/design_agent
Project CI / Repository checks (pull_request) Successful in 2m51s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / Backend tests (pull_request) Successful in 5m26s
Project CI / Native shell tests (pull_request) Successful in 21m10s
2026-09-14 13:24:15 +08:00
lhk229 1b7bc95914 Merge remote-tracking branch 'origin/master' into opt/design_agent
Project CI / Repository checks (pull_request) Successful in 2m35s
Project CI / Frontend tests (pull_request) Successful in 3m20s
Project CI / Backend tests (pull_request) Successful in 8m8s
Project CI / Native shell tests (pull_request) Successful in 20m14s
2026-09-14 13:08:33 +08:00
lhk229 b4a9be4581 清理 reasoning 改造临时计划
删除已完成的五份实施计划

保留里程碑规范与最终验收证据
2026-09-14 13:03:21 +08:00
lhk229 e174b6dcf4 完成 reasoning 最终回归与验收
补充 Responses reasoning 与工具调用共存回归测试

补充策划 Runtime 重试清理 reasoning 测试

同步 platform-llm README 与策划 Agent 技术方案

记录第五轮验收证据与未验证边界
2026-09-14 13:01:19 +08:00
lhk229 e8f9929630 接通策划 Agent reasoning 展示
仅为策划 Provider 请求开启 reasoning 捕获

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

补齐回合重试、错误和项目切换的状态清理测试
2026-09-14 12:46:30 +08:00
lhk229 deb327ce1f 锁定 Provider 适配层兼容行为
验证 neutral adapter 默认关闭 reasoning 捕获

补充带 reasoning 上游响应的正文流回归覆盖

确认 GameAgent 与 AGC Runtime 继续只消费正文字段
2026-09-14 12:31:29 +08:00
lhk229 0635ddfdb1 实现 Provider reasoning 协议解析
解析 Chat 与 Responses 的 reasoning 字段和流式事件

保持正文、工具调用与 GameAgent 默认行为不变

补充 reasoning 分流、终态快照与兼容开关测试
2026-09-14 12:10:59 +08:00
lhk229 6ab7047eff 建立 Provider 推理旁路契约
为 LlmStreamDelta 和 LlmRunResponse 增加独立 reasoning 字段

新增默认关闭的 reasoning 捕获开关并保持请求体不变

补齐 AGC、api-server 和 handoff 构造点及第一轮验收计划
2026-09-14 11:42:19 +08:00
lhk229 33336d6242 制定 Provider 推理与正文分离改造方案
新增 Issue #331 的分步里程碑方案

明确策划 Agent 与 GameAgent 的兼容边界

补充验收标准、风险与回滚点
2026-09-14 11:07:51 +08:00
158 changed files with 2685 additions and 7059 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@genarrative/ai-game-creator-shell",
"private": true,
"version": "0.1.29",
"version": "0.1.27",
"type": "module",
"scripts": {
"dev": "node scripts/start-tauri-dev.mjs",
@@ -108,8 +108,6 @@ const rustSharedContractSource = fs.readFileSync(
);
const allowedUncalledTauriCommands = [
'append_direct_project_conversation_message',
// TODO: Remove the retired binding command after the legacy runtime path is removed.
'bind_components',
'chat_with_game_creator_agent',
'check_ui_editor_font_glyph_coverage',
'create_ui_design_resource',
@@ -8,7 +8,6 @@ export const SKILL_PACK_SCHEMA_VERSION = 'agc-skill-pack.v1';
export const EXPECTED_SKILL_NAMES = Object.freeze([
'agc-browser-playtest',
'agc-client-projection',
'agc-game-production-workflow',
'agc-project-structure',
'agc-web-game-development',
'taonier-art-assets',
+1 -1
View File
@@ -1725,7 +1725,7 @@ dependencies = [
[[package]]
name = "genarrative-ai-game-creator-shell"
version = "0.1.29"
version = "0.1.27"
dependencies = [
"agent-runtime-core",
"axum",
@@ -1,6 +1,6 @@
[package]
name = "genarrative-ai-game-creator-shell"
version = "0.1.29"
version = "0.1.27"
edition = "2021"
publish = false
@@ -1,26 +0,0 @@
---
name: agc-game-production-workflow
description: Orchestrate a complete AGC game delivery from an approved brief to a playable, art-integrated, browser-validated product. Use when creating a new game, implementing a substantial game brief, or turning a planning document into a finished game.
---
# AGC Game Production Workflow
Use this Skill as the top-level SOP for a new game or a substantial game brief. The tools are stages in one delivery chain, not independent suggestions. Do not stop after producing a plan, after writing code, or after generating an image.
## Stage flow
1. **Brief and scope** — Read the current planning output and project instructions. Extract the game loop, player actions, entities, visual requirements, target viewports, and the completion evidence. If the brief is incomplete, ask focused questions before side effects.
2. **Project and asset inventory** — Inspect the existing project structure and call `agc_list_registered_assets` (and `agc_list_project_files` when needed). Record which requested visuals already have usable registered identities and which are missing. Do not invent asset identities from filenames.
3. **Visual production** — For missing or unsuitable visuals, call the reviewed `agc_tools` workflow: use `taonier_prepare_game_art` for a complete package, or `agc_generate_image` / `agc_edit_image` for focused assets. Read returned paths, identities, and warnings. A warning or partial package requires a narrower retry or independent assets before continuing.
4. **Game implementation** — Implement the complete playable loop and wire the returned project-relative asset paths into the actual runtime. Every required character, object, background, effect, and UI visual must have a real source or an explicit brief-level decision to remain code-native. Generated assets that are unused, documentation-only, or replaced by emoji/CSS placeholders do not satisfy this stage.
5. **Build and local verification** — Run the projects bootstrap/install and verify/build commands. Confirm the actual playable entry under `dist` (or the editor runtime for a supported editor project) and fix build or asset-loading failures before preview.
6. **Browser playtest** — Call `agc_browser_playtest` for desktop and mobile evidence after meaningful changes. Check the game loop, input, layout, asset loading, and visible use of the generated art. Fix findings and repeat stages 46 until the evidence is clean.
7. **Delivery** — Report the implemented behavior, real asset paths and identities used, build result, playtest evidence, warnings, and any explicit remaining gap. Do not claim complete while a required stage is failed, skipped without the briefs justification, or missing evidence.
## Stage transitions
Advance only when the current stage has its output: brief → inventory; inventory → art decision; art decision → usable registered assets or an explicit no-art decision; implementation → source references to those assets; build → playable entry; playtest → evidence; delivery → truthful report. If a tool fails, preserve its error and stop or repair at that stage instead of silently substituting a later-stage placeholder.
For a small edit to an existing game where the brief and suitable assets are unchanged, use the focused edit path and do not regenerate art. This exception does not apply to a new game or a substantial planning brief.
Read the referenced specialist Skills for their detailed contracts: `agc-project-structure`, `taonier-art-assets`, `agc-web-game-development`, `agc-client-projection`, and `agc-browser-playtest`.
@@ -1,4 +0,0 @@
interface:
display_name: "完整游戏生产流程"
short_description: "从策划案到真实美术接入和试玩验收的连续交付"
default_prompt: "Use $agc-game-production-workflow to take the current game brief through inventory, art, implementation, build, playtest, and delivery."
@@ -1,5 +0,0 @@
# Workflow contract
The production Skill owns sequencing and transition evidence. Specialist Skills own the detailed safety and data rules for each tool family. A specialist tool result is never a delivery result by itself: image generation must be followed by registered identity inspection and runtime integration; code writing must be followed by build verification; a successful preview launch must be followed by desktop and mobile playtest evidence when the brief targets both.
The no-art exception is valid only when the brief explicitly requests a code-native visual treatment or the inventory proves that all required visuals are already registered and suitable. Emoji, CSS primitives, random local files, and generated files that are not referenced by the runtime are not evidence of an integrated art package.
@@ -14,7 +14,7 @@ Implement the user's actual game request in the current project as an npm-manage
3. Build with the project's npm script before previewing. The playable entry is the package directory's `dist/index.html`; never report an unbuilt bare-module page as playable. Import assets or configure public assets so all runtime media is included in dist; preview and exports cannot read outside it.
4. Build a complete playable loop: visible objective, responsive input, meaningful state changes, success or failure feedback, and a reliable restart path where the game needs one.
5. Fit the active game scene to desktop and mobile viewports without accidental page scrollbars. Reserve deliberate safe space for HUD elements instead of covering interactive content.
6. Invoke `taonier-art-assets` for every new game brief that needs visual assets. First reuse suitable registered Taonier art; when the brief's required visual elements are missing or unsuitable, call the reviewed `agc_tools` generation/edit workflow in the same task. After the tool returns, wire its relative paths into the game and verify the rendered result. A game with unused generated assets or placeholder emoji/CSS where requested art should appear is not complete. Load media defensively only for genuinely optional effects, and never relabel a local placeholder as platform art.
6. Reuse registered Taonier art when available through `agc_tools`. Load media defensively and keep gameplay usable when an optional derivative is absent; never relabel a local placeholder as platform art.
7. Let Phaser own the render loop and input dispatch. Avoid duplicate scenes, stale event listeners, and state that survives restart unintentionally.
8. After a meaningful game change, use the browser playtest Skill and fix issues shown by real evidence before reporting completion.
@@ -1,29 +1,7 @@
{
"schemaVersion": "agc-skill-pack.v1",
"version": "2026-08-26.13",
"version": "2026-08-26.12",
"skills": [
{
"name": "agc-game-production-workflow",
"purpose": "把完整游戏从策划案按阶段推进到真实素材接入、构建、试玩和交付",
"triggers": [
"从策划案创建完整游戏",
"实现完整游戏交付",
"需要衔接策划、素材、代码、构建和试玩"
],
"requiredTools": [
"agc_tools.agc_list_registered_assets",
"agc_tools.agc_generate_image",
"agc_tools.agc_edit_image",
"agc_tools.taonier_prepare_game_art",
"agc_tools.agc_browser_playtest"
],
"files": [
"SKILL.md",
"agents/openai.yaml",
"references/workflow-contract.md"
],
"sha256": "91082fdff4123f1e1fcf930af433cbea51a8c9d26991678b19028b344ea49f39"
},
{
"name": "agc-project-structure",
"purpose": "约束当前项目根、游戏代码、美术素材与客户端状态的职责边界",
@@ -53,7 +31,6 @@
"已有陶泥儿素材需要接入玩法"
],
"requiredTools": [
"agc_tools.agc_list_registered_assets",
"agc_tools.agc_generate_image",
"agc_tools.agc_edit_image",
"agc_tools.taonier_prepare_game_art"
@@ -63,7 +40,7 @@
"agents/openai.yaml",
"references/platform-art-contract.md"
],
"sha256": "bd1e415aac0cd0f97090296f34c67898dd731d1e177ec91a56027f9b68a88b37"
"sha256": "82e4b2ee8ca8147b51ca206b0565b3cc244dc5d3cddb8343875001c0beb4711f"
},
{
"name": "agc-web-game-development",
@@ -80,7 +57,7 @@
"agents/openai.yaml",
"references/game-quality-checklist.md"
],
"sha256": "05b5cfbf7a40fd303717491f5cea84ff339a73359c9678b283fd54d2b5c45efd"
"sha256": "0649c72dd53e05ad7c87b28def1397c2badf61b0c308091196c40f7c48a8b36a"
},
{
"name": "agc-browser-playtest",
@@ -5,15 +5,7 @@ description: Prepare, recover, inspect, and integrate real Taonier platform game
# Taonier Art Assets
Use real platform assets only through the reviewed `agc_tools` MCP server. When
building a new game from a brief that names characters, objects, backgrounds,
effects, or other visual elements, this Skill is an execution step: inspect
existing assets, generate or reuse suitable art, process it when needed, and
integrate the returned paths into the playable game before reporting the game
complete. Do not treat the art step as optional merely because the user did
not repeat “生图” in the latest message.
Use
Use real platform assets only through the reviewed `agc_tools` MCP server. Use
`agc_generate_image` for a single ordinary image, character image, visual-spec
image, UI design image, or publication material; use `agc_edit_image` for an
edit of an existing registered image; use `taonier_prepare_game_art` only for
@@ -25,11 +17,11 @@ the complete game-art package and its canonical slices.
## Workflow
1. Inspect existing `assets/` and registered project evidence before requesting new art. Reuse suitable assets when they satisfy the current brief. If the brief requires visual elements that are absent or unsuitable, call the appropriate generation tool during the same game implementation task; do not continue with placeholder art and silently defer generation.
1. Inspect existing `assets/` and registered project evidence before requesting new art. Reuse suitable assets when the user did not ask to regenerate them.
2. For one new image, call `agc_generate_image` with `kind="image"` (or `character`, `icon-spec`, `ui-prototype`, or `publication-material` when that is the explicit intent). For changes to an existing registered image, call `agc_edit_image` with its `sourceLocalAssetId`; do not fake an edit with a new-image request. For a complete game-art package, call `taonier_prepare_game_art` only when the current intent requires new or recoverable platform art. Use `mode="regenerate"` only after the latest User message is a standalone reviewed immediate-confirmation command such as `请重新生成美术`; punctuation may end it, but no brief, condition, negation, alternative, cost qualifier, deferral, or other text may accompany it. Describe the desired style and gameplay constraints in an earlier non-billable turn, then obtain the standalone confirmation turn; otherwise use `mode="reuse-or-create"`. Quoted UI copy or examples, explanations, questions, historical wording, model/MCP arguments do not authorize regeneration. Pass a concise game-specific visual brief that names the required gameplay entities, background exclusions, tiling needs, and viewport constraints. Do not call either generation tool for greetings, date questions, or text-only code fixes.
3. Treat the tool result as authoritative. Read `mode`, `assetPaths`, `slicePaths`, `resources`, and every entry in both `warnings` and `sliceWarnings`. `resources` is the client's safe projection of registered Canvas identities; use only its returned relative paths and identities. Never invent a resource, slice, platform identity, warning-free result, or successful regeneration.
4. A newly created or explicitly regenerated standard package is complete only when `slicePaths` contains the four canonical independent slices. An empty or partial `slicePaths` result never satisfies an independent-asset requirement: if a `sliceWarning` reports too many or unusable elements, narrow the edit/generation brief or generate the needed independent images and continue the integration; do not guess atlas coordinates, fabricate derivatives, or silently fall back to placeholders. A trusted legacy complete sheet may still be used without slices only when the current request does not require independent assets.
5. Inspect the returned background, complete sheet, and available slice previews before integrating them. Then use suitable returned runtime assets in the game's actual visible experience and confirm their visible use in desktop and mobile playtest evidence. The implementation is incomplete while generated assets remain unused, are referenced only by documentation, or are replaced by emoji, CSS shapes, or other placeholders where the brief requires the generated art. `art-spec.png` is a reference specification, not a runtime background, character, prop, or effect. Background exclusions, seamless tiling, entity semantics, and final draw dimensions are visual/runtime acceptance checks; a prompt alone does not prove them. A hidden or side-panel preview does not count as gameplay use.
4. A newly created or explicitly regenerated standard package is complete only when `slicePaths` contains the four canonical independent slices. An empty or partial `slicePaths` result never satisfies an independent-asset requirement; stop and report the warning instead of guessing atlas coordinates or fabricating derivatives. A trusted legacy complete sheet may still be used without slices only when the current request does not require independent assets.
5. Inspect the returned background, complete sheet, and available slice previews before integrating them. Then use suitable returned runtime assets in the game's actual visible experience and confirm their visible use in desktop and mobile playtest evidence. `art-spec.png` is a reference specification, not a runtime background, character, prop, or effect. Background exclusions, seamless tiling, entity semantics, and final draw dimensions are visual/runtime acceptance checks; a prompt alone does not prove them. A hidden or side-panel preview does not count as gameplay use.
6. Preserve warning details in the final report. If the tool reports missing credentials, uncertain operation state, invalid provenance, download failure, or decode failure, stop and report the actionable reason; do not substitute generated CSS shapes and call the platform step complete.
Before interpreting async recovery, source-preserved warnings, or slice warnings, read `references/platform-art-contract.md`.
@@ -2829,6 +2829,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,
});
}
@@ -3133,6 +3135,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,
@@ -4999,7 +5002,7 @@ case "$extra_roots" in *'"method":"skills/extraRoots/set"'*) ;; *) exit 87 ;; es
printf '%s\n' '{"id":2,"result":{}}'
IFS= read -r skills_list
case "$skills_list" in *'"method":"skills/list"'*) ;; *) exit 88 ;; esac
printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-game-production-workflow"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}'
printf '%s\n' '{"id":3,"result":{"data":[{"skills":[{"name":"agc-browser-playtest"},{"name":"agc-client-projection"},{"name":"agc-project-structure"},{"name":"agc-web-game-development"},{"name":"taonier-art-assets"}],"errors":[]}]}}'
while IFS= read -r line; do :; done
"#,
)
@@ -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,
@@ -49,6 +49,18 @@ pub(crate) struct DesignView {
messages: Vec<DesignMessage>,
running: bool,
can_retry: bool,
#[serde(skip_serializing_if = "Option::is_none")]
reasoning_text: Option<String>,
reasoning_entries: Vec<DesignReasoningEntry>,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct DesignReasoningEntry {
id: String,
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
}
#[derive(Clone, Debug, Serialize)]
@@ -65,6 +77,7 @@ pub(crate) struct DesignEvent {
}
fn design_view(session: &DesignSession, running: bool) -> DesignView {
let reasoning_entries = persisted_design_reasoning_entries(session);
DesignView {
session: DesignSessionSummary {
session_id: session.session_id.clone(),
@@ -82,9 +95,124 @@ fn design_view(session: &DesignSession, running: bool) -> DesignView {
&& session.turn.as_ref().is_some_and(|turn| turn.pending)
&& session.pending_approval.is_none()
&& session.pending_clarification.is_none(),
reasoning_text: reasoning_entries.last().map(|entry| entry.text.clone()),
reasoning_entries,
}
}
fn reasoning_text_from_history_item(item: &Value) -> Option<String> {
if item.get("type").and_then(Value::as_str) != Some("reasoning") {
return None;
}
let mut text = String::new();
if let Some(summary) = item.get("summary").and_then(Value::as_array) {
for part in summary {
if let Some(value) = part.get("text").and_then(Value::as_str) {
text.push_str(value.trim());
}
}
}
if let Some(content) = item.get("content").and_then(Value::as_array) {
for part in content {
let part_type = part.get("type").and_then(Value::as_str).unwrap_or_default();
if matches!(
part_type,
"reasoning" | "reasoning_content" | "reasoning_text" | "analysis" | "thinking"
) {
if let Some(value) = part.get("text").and_then(Value::as_str) {
text.push_str(value.trim());
}
}
}
}
(!text.trim().is_empty()).then_some(text)
}
fn persisted_design_reasoning_entries(session: &DesignSession) -> Vec<DesignReasoningEntry> {
// Responses history contains tool-only provider responses. Their reasoning is
// followed by function calls and only the next provider response may contain
// visible assistant text, so pairing on the next `message` item makes the
// earlier reasoning look like an orphan and moves it to the bottom of the UI.
// Both persisted streams retain user-turn boundaries; pair reasoning and
// visible assistant messages by their response order within each turn.
let mut assistant_groups: Vec<Vec<String>> = vec![Vec::new()];
for message in &session.messages {
if message.role == "user" {
assistant_groups.push(Vec::new());
} else if message.role == "assistant" {
assistant_groups
.last_mut()
.expect("assistant group always exists")
.push(message.id.clone());
}
}
let mut entries = Vec::new();
let mut group_index = 0;
let mut assistant_index = 0;
let mut sequence = 0_u64;
let mut current_reasoning = Vec::new();
let mut pending_reasoning = Vec::new();
let mut saw_response_output = false;
for item in &session.history {
if item.get("role").and_then(Value::as_str) == Some("user") {
if !pending_reasoning.is_empty() || !current_reasoning.is_empty() {
pending_reasoning.append(&mut current_reasoning);
}
group_index += 1;
assistant_index = 0;
saw_response_output = false;
continue;
}
if item.get("type").and_then(Value::as_str) == Some("reasoning") {
if saw_response_output {
pending_reasoning.append(&mut current_reasoning);
saw_response_output = false;
}
if let Some(text) = reasoning_text_from_history_item(item) {
sequence += 1;
current_reasoning.push(DesignReasoningEntry {
id: item
.get("id")
.and_then(Value::as_str)
.map(str::to_string)
.unwrap_or_else(|| format!("reasoning-{sequence}")),
text,
message_id: None,
});
}
continue;
}
if item.get("role").and_then(Value::as_str) == Some("assistant")
|| item.get("type").and_then(Value::as_str) == Some("message")
{
pending_reasoning.extend(current_reasoning.drain(..));
let assistant_id = assistant_groups
.get(group_index)
.and_then(|ids| ids.get(assistant_index))
.cloned();
assistant_index += 1;
for mut entry in pending_reasoning.drain(..) {
entry.message_id = assistant_id.clone();
entries.push(entry);
}
saw_response_output = false;
} else if item.get("type").is_some() {
saw_response_output = true;
}
}
pending_reasoning.append(&mut current_reasoning);
let fallback_id = assistant_groups
.get(group_index)
.and_then(|ids| ids.last())
.cloned();
for mut entry in pending_reasoning {
entry.message_id = fallback_id.clone();
entries.push(entry);
}
entries
}
fn design_event(
root: &Path,
turn_id: &str,
@@ -104,6 +232,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 +601,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 +688,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 +711,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 +742,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 +772,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 +826,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 +854,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!()
@@ -1285,6 +1487,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 {
@@ -1345,6 +1548,110 @@ 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);
}
#[test]
fn persisted_reasoning_follows_response_order_across_tool_only_responses() {
let mut session = new_design_session("project", "quality");
session.messages = vec![
DesignMessage {
id: "turn:user".into(),
role: "user".into(),
text: "需求".into(),
},
DesignMessage {
id: "call-1:tool".into(),
role: "tool".into(),
text: "读取资源".into(),
},
DesignMessage {
id: "turn:response:0".into(),
role: "assistant".into(),
text: "给出方案".into(),
},
];
session.history = vec![
json!({"role":"user", "content":"需求"}),
json!({"type":"reasoning", "id":"r1", "content":[{"type":"reasoning_text", "text":"第一段思考"}]}),
json!({"type":"function_call", "call_id":"call-1", "name":"read_resource", "arguments":"{}"}),
json!({"type":"reasoning", "id":"r2", "content":[{"type":"reasoning_text", "text":"第二段思考"}]}),
json!({"type":"message", "role":"assistant", "content":[{"type":"output_text", "text":"给出方案"}]}),
];
let entries = persisted_design_reasoning_entries(&session);
assert_eq!(
entries
.iter()
.map(|entry| (entry.id.as_str(), entry.message_id.as_deref()))
.collect::<Vec<_>>(),
vec![
("r1", Some("turn:response:0")),
("r2", Some("turn:response:0")),
]
);
}
#[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 scripted_design_provider_retry_clears_previous_reasoning_attempt() {
let (_temp, root, _resources) = init_design_project();
let mut session = new_design_session("design-fake", "quality");
begin_design_turn(&mut session, "turn-reasoning-retry");
let mut response = fake_response("reasoning-retry", "重试后的正文", Vec::new());
response.reasoning = "重试后的推理".into();
let _fake = fake_provider::install(
vec![
Err(platform_llm::LlmError::Upstream {
status_code: 503,
message: "busy".into(),
}),
Ok(response),
],
1,
);
let mut events = Vec::new();
let response =
request_scripted_design_provider(&root, &mut session, &mut |event| events.push(event))
.await
.expect("scripted retry 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();
File diff suppressed because one or more lines are too long
@@ -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,
@@ -1156,10 +1156,6 @@ mod tests {
assert!(with_canvas.contains("根据当前玩法需求编写规格和界面建议"));
assert!(with_canvas.contains("用途、数量、输出路径、尺寸、参考资源和是否需要 spritesheet"));
assert!(with_canvas.contains("再调用 canvas.asset_generate"));
assert!(with_canvas.contains("调用 canvas.asset_generate"));
assert!(with_canvas.contains("不要使用固定图片合同"));
assert!(with_canvas.contains("不修改 game/index.html"));
assert!(!with_canvas.contains("不调用 canvas.asset_generate"));
}
#[test]
@@ -801,7 +801,7 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
let initial_step = route.get("initialStep")?.as_str()?;
let render_mode = route.get("renderMode")?.as_str()?;
if resource_id.is_empty()
|| initial_step != "asset-separation"
|| initial_step != "visual-binding"
|| render_mode != "final-preview"
{
return None;
@@ -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,
@@ -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()),
@@ -6,16 +6,15 @@ use std::path::{Component, Path};
const AGC_SKILL_PACK_MANIFEST: &[u8] = include_bytes!("../../resources/agc-skills/manifest.json");
const AGC_SKILL_PACK_SCHEMA_VERSION: &str = "agc-skill-pack.v1";
pub(crate) const AGC_SKILL_PACK_EXPECTED_NAMES: [&str; 6] = [
pub(crate) const AGC_SKILL_PACK_EXPECTED_NAMES: [&str; 5] = [
"agc-browser-playtest",
"agc-client-projection",
"agc-game-production-workflow",
"agc-project-structure",
"agc-web-game-development",
"taonier-art-assets",
];
const AGC_SKILL_PACK_FILES: [(&str, &[u8]); 18] = [
const AGC_SKILL_PACK_FILES: [(&str, &[u8]); 15] = [
(
"agc-browser-playtest/SKILL.md",
include_bytes!("../../resources/agc-skills/agc-browser-playtest/SKILL.md"),
@@ -44,20 +43,6 @@ const AGC_SKILL_PACK_FILES: [(&str, &[u8]); 18] = [
"../../resources/agc-skills/agc-client-projection/references/projection-contract.md"
),
),
(
"agc-game-production-workflow/SKILL.md",
include_bytes!("../../resources/agc-skills/agc-game-production-workflow/SKILL.md"),
),
(
"agc-game-production-workflow/agents/openai.yaml",
include_bytes!("../../resources/agc-skills/agc-game-production-workflow/agents/openai.yaml"),
),
(
"agc-game-production-workflow/references/workflow-contract.md",
include_bytes!(
"../../resources/agc-skills/agc-game-production-workflow/references/workflow-contract.md"
),
),
(
"agc-project-structure/SKILL.md",
include_bytes!("../../resources/agc-skills/agc-project-structure/SKILL.md"),
@@ -306,10 +291,10 @@ mod tests {
use super::*;
#[test]
fn bundled_skill_pack_is_exactly_the_six_reviewed_skills() {
fn bundled_skill_pack_is_exactly_the_five_reviewed_skills() {
let manifest = validated_skill_pack_manifest().expect("validated manifest");
assert_eq!(manifest.schema_version, "agc-skill-pack.v1");
assert_eq!(manifest.skills.len(), 6);
assert_eq!(manifest.skills.len(), 5);
assert!(manifest.skills.iter().all(|entry| entry.sha256.len() == 64));
let serialized = serde_json::to_string(
&manifest
@@ -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 {
@@ -339,41 +339,6 @@ async fn recognize_ui(
ui_editor::commands::recognize_ui_impl(project_path, state).await
}
#[tauri::command]
async fn separate_ui(
project_path: String,
asset_id: String,
state: ui_editor::state::State,
) -> Result<ui_editor::commands::SeparationDTO, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
ui_editor::commands::separate_ui_impl(project_path, asset_id, state).await
}
#[tauri::command]
fn inspect_separation_recovery(
project_path: String,
asset_id: String,
) -> Result<ui_editor::commands::SeparationRecoveryDTO, String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.list")?;
ui_editor::commands::separation::inspect_separation_recovery(root, &asset_id)
}
#[tauri::command]
fn finalize_separation(project_path: String, asset_id: String) -> Result<(), String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
ui_editor::commands::separation::finalize_separation(root, &asset_id)
}
#[tauri::command]
fn discard_separation_recovery(project_path: String, asset_id: String) -> Result<(), String> {
let root = Path::new(project_path.trim());
enforce_project_permission_policy(root, "asset.register")?;
ui_editor::commands::separation::discard_separation_recovery(root, &asset_id)
}
#[tauri::command]
async fn merge_ui(state: ui_editor::state::State) -> Result<ui_editor::commands::MergeDTO, String> {
ui_editor::commands::merge_ui_impl(state).await
@@ -2724,10 +2689,6 @@ fn main() {
check_ui_editor_font_glyph_coverage,
suggest_ui_design_semantic,
recognize_ui,
separate_ui,
inspect_separation_recovery,
finalize_separation,
discard_separation_recovery,
merge_ui,
bind_components,
load_ui_design_state,
@@ -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 {
@@ -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,

Some files were not shown because too many files have changed in this diff Show More