/** * The screen agent — UI-generation blueprint §4.2 and §4.5. * * It is `vendo()` with a CLOSED loadout and a tight step budget, not a harness of * its own: the assembly verbs and the host's read tools by name, two hands of its * own, and one door out. There is no second drive of `startTurn` here — the step * cap, the seat resolution, `wireErrorMessage`, the history knobs and the system * precedence are the default harness's, so a rail cannot be fixed in one loop and * stay broken in the other. What this file holds is the CONFIGURATION: the brief, * the loadout, the hands, and the outcome the front door reads. * * - **The write path is `turn.workspace`.** The `claudeCode()` harness already * builds apps this way: the model writes the app with its own hands and the * runtime's commit is what makes it real (`claude-code/index.ts:338`, * `skills/building-apps.ts:68`). This agent has no disk and no shell, so its two * writing hands over the same `WorkspaceFs` — the whole screen, or one exact * passage of it — write `app.tsx` and nothing else, through one commit path. * - **The run's closing words are the receipt.** The loop reports what it built in * its own voice, grounded in what its saves told it: whether the paint happened, * and what each query delivered. `vendo_make` relays those words verbatim * (`make-receipt.ts`), so nothing downstream describes a screen it never saw. * - **The paint path is the render seam.** `wrapWorkspaceForRender` intercepts * `commit()`, compiles, and emits `data-vendo-view`. This file never emits a * view and never compiles anything — that is exactly why a screen it assembles * passes the same floor a `claudeCode()` app does. * - **`vendo_make` is withheld, not merely unused.** The screen agent IS what * `vendo_make` calls, so leaving it callable is a loop. The closed loadout * excludes it by omission. * - **The job description is the shipped skill.** `buildingAppsSkill` plus its * `references/format.md` are the same text `claudeCode()` reads. This file adds * one short block that corrects the ENVIRONMENT (no disk, no delegation, one * file, one door out) rather than restating the job — a third prompt is the * thing §0 forbids. * * Screens run UNSANDBOXED, by §6.5: a description is data, its props are * schema-validated, and the kit treats them as inert. There is no box here. */ import { type AppId, type SeatModels, type RunContext, type ToolListing, type ToolRegistry, type TurnId, type TurnTools, type UIPayload, type WorkspaceFs } from "./core/index.js"; import { type BriefingPack, type ScreenAssembler, type ScreenOutcome } from "./core/apps/index.js"; import { type RenderSeamOptions } from "./apps/index.js"; import { type LanguageModel } from "ai"; /** * The whole budget for assembling one screen. * * Sized off the work, not off a round number: learn a shape or search the * catalog (1–2), save the app (1–3, because saving as you go is what makes it * grow on screen), fix whatever a save reports (1–2), and one step to * speak. `instant()`'s `ACT_STEPS = 2` is a specialist that must not think; * `DEFAULT_MAX_STEPS = 20` is a resident that may. A screen is neither, and the * cap is the definition of "cheap": an ask that needs more than this is an ask * for a BUILD rather than an ask for a bigger number. */ export declare const SCREEN_STEPS = 10; /** The repair round's whole budget. The findings name the exact thing to change, * so a fix lands in one to three moves — save, read what came back, save again — * or it does not land at all; a second full budget only buys a rewrite of a * screen the person is already looking at. */ export declare const REPAIR_STEPS = 3; /** The file hand. One document and no path argument — a screen agent has exactly * one app directory, and a tool that takes a path is a tool that can write * outside it. */ export declare const SAVE_APP_TOOL = "save_app"; /** The edit hand — the same document, any number of exact passages in ONE * landing. A sibling rather * than a second shape of `save_app`: "exactly one of `content` or `edit`" is a * rule a JSON schema cannot state, so it would be enforced in prose and paid for * at runtime, on the one hand this loop calls most. Two hands say it in the * shape. Both land through the same commit and hear the same checks. */ export declare const EDIT_APP_TOOL = "edit_app"; /** * The door out of assembly (§4.5) — and it opens onto a QUESTION, never onto a * machine. * * PR #1407 took this hand off the loadout so that the model could not spend a * box by reaching for it, and that principle is untouched: all this hand does * now is record one line, which the front door turns into a standing approval * card (`build.propose`). The person's yes, whenever it lands, is still the only * thing that starts a build. Without the hand the loop had no word for "this ask * is bigger than a screen", so a real build was unreachable from chat at all — * measured live 2026-08-24, an ask needing npm packages got a degraded screen * and an apology. * * Never `vendo_`-prefixed: the loadout's `isAlwaysActive` would make it * un-gateable, and this tool is the screen agent's own, not a product capability * anybody else may reach. */ export declare const ESCALATE_TOOL = "escalate"; /** * What the lean loop needs, and nothing else. * * A structural subset of `Turn`, so a caller already inside a turn passes its own * turn verbatim — no adapter, no wrapper — and the `vendo_make` door builds the * same fields out of the pieces composition already holds. The two identities are * optional because only a caller inside a turn already has them. */ export interface ScreenSurface { readonly models: SeatModels; readonly tools: TurnTools; /** Wrapped by the render seam before it gets here, so `commit()` paints. */ readonly workspace: WorkspaceFs; readonly signal: AbortSignal; readonly threadId?: string; readonly turnId?: TurnId; /** * What this app's LAST PAINT actually delivered, per declared query. * * The gauntlet runs a screen's queries while it paints it, so the painted view * is the only place that answer exists — and `emit` belongs to * whoever wrapped the workspace, never to this loop, exactly as `paintedIn`'s * verdict does. So the wrapper reads it off the part it emitted and answers * here. Absent, or `undefined`, claims NOTHING: an unwrapped workspace has no * paint to report and this loop never invents one. */ readonly queryOutcomes?: () => readonly QueryOutcome[] | undefined; /** * Why this app's last save did not PAINT — the checks floor's own repair * instructions for the screen it refused. * * Read off the floor for the same reason `queryOutcomes` is read off the paint: * the refusal happens inside the seam's commit, which belongs to whoever wrapped * the workspace, and the seam's only channel for it is a log. Absent, or empty, * claims NOTHING — an unwrapped workspace has no floor to have refused anything. */ readonly screenIssues?: () => ScreenRefusal; } /** Why the floor refused a save, as the loop reads it. */ export interface ScreenRefusal { /** The checks floor's own repair instructions for the screen it refused. */ readonly blocking: readonly string[]; /** The refusal was about the DEPLOYMENT — the checks could not RUN here — so * nothing this loop writes changes it (`ComponentPaintResult.environment`). */ readonly environment?: true; } /** One declared query's outcome at paint time. `rows` is absent when the answer * is not countable — a single object is an answer too. */ export interface QueryOutcome { name: string; /** Did the call come back with data? A query that FAILED — errored, blocked, * refused — contributes nothing, and every binding on it renders "—". */ delivered: boolean; rows?: number; } export interface ScreenInput { /** The app whose files this run writes. Minted by the caller so the file path, * the view's stream id and any receipt all name the same app. */ appId: AppId; /** The person's ask, verbatim. */ request: string; /** The surface this screen renders into, in CSS pixels, when the host knows it. * The one fact about the render target a writer cannot learn from anything * else it is given — which is how eight-column tables and four-across stat * rows keep landing on a narrow panel, where the person, not the loop, * discovers the clip. Absent claims NOTHING: no width is invented, and the * brief then says nothing about the surface at all. */ viewport?: { width: number; height: number; }; /** THE briefing pack, already rendered (`renderBriefingPack`) — the host's * theme, design rules, product brief, component catalog and tool shape card, * in the same bytes the box rung is handed. Knowledge, not instruction, so it * sits with the job description rather than with the deployment's voice. */ briefing?: string; /** The `app.tsx` this run starts from — a REMIX's ported source, and nothing * else's ({@link ScreenAssemblerDeps.storedScreen} answers only for a seeded * row, `replayFrom` only for a re-seed). Absent on every other edit, whose * first message stays the ask alone. See {@link startingSource}. */ source?: string; /** Is there a builder behind an escalation ({@link ScreenAssemblerDeps.canBuild})? * The door out is equipped only where the answer is yes: a deployment with no * sandbox cannot honour the offer, and `vendo_make` would answer the * escalation with a failed receipt naming the gap. Absent is no. */ canBuild?: boolean; } /** What one assembly run answers. `ScreenOutcome` plus the title an assembled * screen named itself, which the front door turns into a receipt. */ export type ScreenResult = ScreenOutcome & { title?: string; /** What the run chose to record for the next editor (`save_app`'s * `decisions`). Never a summary this file wrote — only the agent's own words, * or nothing. */ decisions?: string; }; /** * The host tools a screen may WIRE, as the model reads them before writing a * button — and ONLY those. * * A tool on the loadout is already mounted with its own description and its own * JSON Schema (`equipClosedLoadout`), so writing it out here again is the same * tool twice in one prompt. What is left over is the write side of the registry: * the tools this loop may never call, but which an `on*` attribute may name. That * is the only part of the registry the model's own tool list cannot tell it about. * * WHAT IT RETURNS IS NOT HERE. The briefing pack's shape card already carries * every tool's response, in this host's own units and annotated with them * (`AppsRuntime.toolShapeBrief`, TOOL RESPONSE SHAPES) — and it rides the same * prompt, so a raw `returns:` JSON beside it was the same shape twice, the second * time worse. The INPUT stays: nothing else in the prompt says what a handler * must send, and a button wired with guessed argument names is the one failure * this section exists to prevent. * * A slot nothing could read prints its unknown sentence rather than a bare `{}`: * `{}` reads as "takes no arguments", so a blind tool would be called with none. * A DECLARED empty input still prints its schema — that IS the host's contract. */ export declare function toolBrief(wireable: readonly ToolListing[]): string; /** * What a painted view DELIVERED, per query the document declared. * * Read off the paint itself, which is the whole point: a description says what to * fetch (`queries`) and the render seam spreads the resolved answers beside it * (`data`) on the settled paint, keyed by query name — and a query that failed is * simply ABSENT from that record. So the facts the loop hears are the facts the * person's screen was painted from, rather than a second run of the same calls * that could disagree with it. */ export declare const paintedQueries: (payload: UIPayload) => readonly QueryOutcome[]; /** How a rung's brief joins its SECTIONS. Exported because the box's brief * (`build-agent.ts`) joins its own the same way: the pack is one section on * either side, so a reader can find it without counting positions. */ export declare const BRIEF_SECTION = "\n\n---\n\n"; /** * ONE assembly run, over any surface a `Turn` satisfies. * * Every host effect goes through `surface.tools.call()` and every file write * through `surface.workspace`, so the guard, the audit row, the approval card and * the paint seam are not this function's business and cannot be forgotten. */ export declare function assembleScreen(surface: ScreenSurface, input: ScreenInput): Promise; export interface ScreenAssemblerDeps { /** The seats, as `Turn.models` carries them. */ models: SeatModels; /** The GUARD-BOUND registry (`VendoGuard.bind(hostTools)`) — the same choke * point every harness's calls pass through. */ tools: ToolRegistry; /** This principal's workspace, unwrapped. The assembler wraps it with the * render seam itself, so composition never has to know that it must. */ workspace: (ctx: RunContext) => Promise; /** * The app's stored `app.tsx` — what the checkout below projects into the * workspace when the workspace has none. * * Composition fills it from `AppsRuntime.get`, exactly as it fills `render` and * `remember`: this file depends on no store and must never reach for one. The * reader that cannot do without it is the ✦ remix — `seed.from` writes the * splitter's ported source to the ROW, and this loop can only edit code it can * SEE in the workspace. Unfilled — or answering `undefined`, which is what * composition does for every app that is NOT a remix — an edit starts from * whatever the workspace already holds, which is exactly today's behaviour. */ storedScreen?: (appId: AppId, ctx: RunContext) => Promise; /** * The source THIS run must start from, replacing whatever the workspace holds * — `AppsRuntime.takeReplaySource`, published by a re-seed for its own replay * and gone once read. * * Deliberately NOT folded into `storedScreen`: that one fills an EMPTY * workspace and must never overwrite a save, while this one exists precisely * to overwrite. Keeping them two slots is what makes the overwrite * unreachable from an ordinary edit — an ordinary edit publishes nothing, so * there is nothing for it to take. */ replayFrom?: (appId: AppId) => string | undefined; /** The seam's optional halves — the checks floor and source persistence. A * screen assembled here passes the same floor every other author's does, or it * does not paint. */ render?: (ctx: RunContext) => Omit; /** * THE briefing pack (`AppsConfig.briefing`, assembled in * `compose-surfaces.ts`) — everything this host's writers are told about the * product. Two slots collapsed into one on purpose: the theme and design * rules and the tool shape card were two seams with two owners and two * arrival routes, which is how the box rung ended up with neither. * * Per call and ctx-taking: `designRules` re-resolves per generation so a * console publish applies to the next screen, and the shape card is projected * for THIS caller's tools. */ briefing?: (ctx: RunContext) => Promise; /** * Where a run's `decisions` land: the runtime's one memory door * (`AppsRuntime.remember`), which this file deliberately does not reach for * itself — composition fills the slot exactly as it does `render` above. * * Called only for an `assembled` run — the only answer that carries decisions * at all. `assembled` means the BYTES landed, which is not the same thing as a * row: a paint is what creates one, so a run whose saves the floor refused has * nowhere to put its memory, and that `not-found` is an expected state rather * than a fault (`commitSource` reports its half the same way). Unfilled, or * throwing, and the run's decisions are simply not recorded: a lost memory * write is never worth failing a screen the person can already see. */ remember?: (appId: AppId, decisions: string, ctx: RunContext) => Promise; /** * Is there a builder behind an escalation — `AppBuilder.available`, handed * over by the composition that fills BOTH slots. * * Availability by construction, exactly as `servedProxyPath`'s presence used * to say it (compose-apps.ts): the loop is offered the door out only where a * box could really be claimed after the person's yes. Unfilled reads as no, * which is what a deployment with no sandbox is. */ canBuild?: () => boolean; } /** * The `ScreenAssembler` the front door routes into. * * The layering is why this door exists at all — and why this file lives in the * umbrella. `@vendoai/vendo/apps` depends on `core` alone, so the `vendo_make` handler * cannot reach a harness; and the harness runtime no longer reaches apps, so * the loop that needs `vendo()` AND the render seam can only live here. The two * meet on core's `ScreenAssembler` and composition — the one place that already * holds the store, the guard-bound registry, the seats and the seam — is what * fills the slot. Unfilled, `vendo_make` behaves exactly as it did. * * The tool surface here is projected off the registry rather than off a `Turn`, * for the same reason the conductor's `queryRunner` is: this call is INSIDE a * tool the resident already mirrored and audited, so re-mirroring the assembly * loop's own reads would double every call in the transcript. The guard is the * same guard either way — that is the part that cannot be skipped. */ export declare function screenAssembler(deps: ScreenAssemblerDeps): ScreenAssembler;