Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 158148e0c9 | |||
| 45d7dc2a63 | |||
| 0a34f4e514 | |||
| 09a9fa7b10 | |||
| 896cfad81e | |||
| 9f40f044a5 | |||
| f7c8b9f217 | |||
| 13e137191a | |||
| fd88e516b8 | |||
| 1b7bc95914 | |||
| b4a9be4581 | |||
| e174b6dcf4 | |||
| e8f9929630 | |||
| deb327ce1f | |||
| 0635ddfdb1 | |||
| 6ab7047eff | |||
| 33336d6242 |
@@ -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
@@ -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
|
||||
|
||||
|
||||
-26
@@ -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 project’s 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 4–6 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 brief’s 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`.
|
||||
-4
@@ -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."
|
||||
-5
@@ -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.
|
||||
+1
-1
@@ -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",
|
||||
|
||||
+4
-12
@@ -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;
|
||||
|
||||
+10
@@ -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
Reference in New Issue
Block a user