/** * The client for `POST /api/cli/v1/forge/stream` — steps 1-2 of the world forger (LLM scene * design, then geometry forge + GLB upload), which run on api-server because they are the paid, * private half. The terminal frame carries the `ForgeArtifact` steps 3-5 need; the `jobId` that * makes the work resumable is on EVERY frame, terminal or not, and is tracked as such — see * `lastJobId` in {@link requestForge}. * * Mirrors `generate/stream.ts` in shape (same SSE plumbing, same pre-stream status mapping), but * the terminal payload is different — `{ success, jobId, artifact }` rather than * `{ success, message, patches }` — and so is what a failure means to the creator, which is what * {@link classifyForgeFailure} is for. */ import type { ForgeReusePlan, ForgeArtifact, ForgeProgressEvent } from '@bitmagic/world-forger/pipeline/index.js'; import type { Environment } from '../config/environments.js'; export interface ForgeSuccess { success: true; jobId: string; artifact: ForgeArtifact; /** * Step 1's `platformer-movement` artifact, when the design produced one (only a * `--platformer` forge does). It lives in api-server's store, and steps 3-5 run against the * CLI's own local store — so it has to travel on this frame or step 5 cannot read it. See * `seedForgeStore`, which writes it into the local store, and * `warnIfPlatformerMovementMissing`, which stays as the older-server fallback. */ platformerMovement?: Record; /** EDIT jobs: the forge that built the level being edited. */ parentJobId?: string; /** EDIT jobs: what the server says this run may reuse from the parent (see seed-edit-store.ts). */ reuse?: ForgeReusePlan; } export interface ForgeFailure { success: false; jobId: string | null; message: string; /** Present when the failure carried a forged GLB the creator can still voxelize by hand. */ glbUrl?: string; /** * The server's own request deadline fired — the run was cut off mid-flight rather than failing. * See {@link isDeadlineFailure} for why this cannot be inferred from {@link progress}. */ timedOut: boolean; /** Every progress line the server sent, in order — the input to {@link classifyForgeFailure}. */ progress: string[]; /** * The same frames, structured, for the servers that send them. Preferred by * {@link classifyForgeFailure}; empty against a server deployed before the fields existed, which * is exactly when {@link progress} is still the only answer. */ events: ForgeProgressEvent[]; } export type ForgeOutcome = ForgeSuccess | ForgeFailure; /** * Which of steps 1-2 failed, which decides the ONLY thing a creator actually wants to know: * whether resuming this job can possibly help. * * - `design` — step 1 writes its `spec` artifact only on success, so nothing was saved. A * resume re-runs the design and is billed for it again. It CAN succeed (the designer is * non-deterministic and retries internally), but it is not free. * - `forge` — step 2 is pure geometry over an already-cached `spec`. It is deterministic: a * resume feeds the identical spec to the identical code and fails identically. The web lane * says the same thing structurally, with `DETERMINISTIC_STEPS = new Set(['forge'])` in * `game-play-agent/src/mastra/tools/run-world-forger.ts`. The fix is a different prompt, not * a retry. * - `unknown` — we could not tell; the message states both cases rather than guessing. */ export type ForgeFailureStage = 'design' | 'forge' | 'unknown'; export declare function isDeadlineFailure(failure: Pick): boolean; /** * What to tell a creator whose run hit the server deadline: resume, unconditionally. No stage * advice, because no stage failed. */ export declare function forgeDeadlineAdvice(jobId: string | null): string; /** * What to tell a creator whose stream ended with NO terminal frame at all. * * This is the deadline's twin and needs the same answer — the run was cut off mid-flight, api-server * deliberately does not cancel it (`cli-forge.ts`'s run promise is left to finish), and whatever it * completes is written to the job's store — except that here nothing said so, because no frame * arrived to say it. Without a job id the creator's only move is a fresh `--prompt` run, which mints * a NEW job id and re-runs (and re-bills) a design the abandoned job may already have finished and * saved. That is why {@link requestForge} tracks the job id off EVERY frame rather than only the * terminal one: api-server puts it on every progress frame precisely so this case has an answer. */ export declare function forgeTruncationMessage(jobId: string | null): string; export declare function classifyForgeFailure(progress: readonly string[], events?: readonly ForgeProgressEvent[]): ForgeFailureStage; /** * The resume advice for a failure, as a paragraph appended to the server's own message. Every * branch says explicitly that world.json is unchanged, because a creator who does not know has to * go and look — and a forge costs enough that "did I just pay for nothing, and is my project * still intact?" is the first question. */ export declare function forgeFailureAdvice(stage: ForgeFailureStage, jobId: string | null): string; /** * The complete message for a failed forge: the server's own text, then the ONE piece of advice * that fits. The deadline check comes first and short-circuits the stage classification — see * {@link isDeadlineFailure} for why the two must never both speak. * * The single entry point the command uses, so "which advice wins" is decided in one place and is * testable without a network. */ export declare function describeForgeFailure(failure: ForgeFailure): string; export interface ForgeStreamDeps { fetch: typeof globalThis.fetch; /** * Called for every progress frame. `event` is the structured form when the server sent one and * null otherwise — the caller decides what to do with an unstructured frame (`commands/forge.ts` * prints it as a plain line, which is what the CLI did for every frame before this existed). */ onProgress: (message: string, event: ForgeProgressEvent | null) => void; } /** * POSTs to `/api/cli/v1/forge/stream` and consumes the SSE response. * * A non-2xx response fails before any stream opens (see `toCliError`). On 200, each `progress` * event is forwarded AND recorded, and the terminal `result`/`error` event resolves this call. A * stream that closes with no terminal event is a truncated connection, not a success — and it fails * with {@link forgeTruncationMessage}, naming the job id the frames already reported so the * expensive half can be resumed rather than paid for twice. */ export declare function requestForge(environment: Environment, accessToken: string, body: Record, deps: ForgeStreamDeps, options?: { /** Endpoint path — the side-on lane posts to its sibling route. Default: the 3D forge. */ path?: string; /** Replaces the generic error for a 404 — the sibling route missing means an * api-server deployed before that lane existed, which deserves its own words. */ notFoundMessage?: string; }): Promise;