Design: Mini-App Harness in the App Builder Assistant
Status: Draft — for review
Related: mini-app-build-harness-prd.md ·
mini-app-build-harness-design.md ·
app-builder.md · nodetool app debug (CLAUDE.md)
The best way to give the in-editor App Builder assistant the harness’s feedback loop, and the ways that look right but aren’t.
1. The gap
The build-harness PRD closed the loop for batch builds: buildApp runs
spec → plan → author → check → run → judge, and nodetool app debug gives any
agent the oracle on demand. The interactive assistant — the Ask Agent
panel in the App Builder — got none of it. Today it:
- edits the document through the 19
ui_app_*tools (web/src/lib/tools/builtin/puck.ts→PuckAgentBinder→ the shared doc-ops in@nodetool-ai/app-runtime); - reads its own work back only via
ui_app_get_snapshotandui_app_get_binding_targets; - can verify the workflow (
validate_workflow/debug_workfloware resident tools) but not the app: no tool tells it whether a binding resolves, whether a click starts a run, or what a widget ends up showing.
The PRD named this in §2.1: “an agent in editor chat improvises this loop turn
by turn.” The oracle exists (simulateApp in
@nodetool-ai/execution/app-debug); the editor assistant just can’t reach it.
There is no /api/applications/:id/debug route and no app-level tool on
either the server or the frontend surface.
2. The fact that decides the design
The assistant edits an unsaved draft. The document lives in React state
(AppBuilderShell) plus Puck’s own state; the applications row updates only
when the user saves (tRPC applications.update). Any verification keyed on
application_id reads the row, so it would grade a stale document while the
assistant keeps editing the live one — a verdict that lies exactly when the
assistant most needs it (mid-edit).
So the primary integration must carry the live document inline to the
server, the way validate_workflow accepts an inline graph. Everything
below follows from that.
3. Design
Three pieces, smallest first. Each reuses a seam the harness already cut.
3.1 Server route: POST /api/applications/debug
A sibling of POST /api/applications/build, in a new
packages/websocket/src/lib/app-debug-service.ts:
POST /api/applications/debug {
application_id?: string, // saved app — read the row
document?: unknown, // OR the live draft, verbatim
params?: Record<string, unknown>,
interact?: InteractionStep[], // the app-debug script language
run?: boolean, // false = static wiring check only (ms, free)
timeout_ms?: number
} → compacted AppDebugReport (§3.4)
Implementation is assembly, not new machinery:
- Runner: extract
buildRunnerfromapp-build-service.ts(oneExecutionSessionper run over an in-memory graph, nothing persisted) into a shared helper both services use. It already satisfiesAppServerRunInput → AppServerRunOutcome, the only contractsimulateApphas with any kernel runner. - Target resolution: the
documentpath builds aResolvedAppTargetfrom the inline document with graphs loaded per operation viaWorkflow.find(userId, id).getGraph(). Theapplication_idpath needs the application-row resolution that today lives inpackages/cli/src/app-debug/app-target.ts(resolveApplication,hostGraphFor). Move that piece into@nodetool-ai/execution/app-debugwith its loaders injected — the same relocation the build-harness design did for the simulator itself, for the same reason: CLI and server surfaces must not drift. The CLI keeps file/DSL targets and re-exports. - Long runs: register with
debugSessionsthe way a build does, honoringpollfor multi-minute runs.interactive: true(agent-answered escalations) composes later for free — the session machinery is shared.
3.2 Server tool: debug_app
Next to debug_workflow in packages/agents/src/tools/mcp-tools.ts: a thin
apiPost to the route, permission class execute (run: false requests are
read-shaped, but the tool can execute workflows, so it takes the conservative
class). Make it resident in RESIDENT_TOOL_NAMES
(unified-websocket-runner.ts) and teach it in CHAT_AGENT_SYSTEM_PROMPT as
the app half of the existing plan → validate → create → debug loop. This
serves every chat surface — global chat debugging a saved app included — not
just the builder panel.
3.3 Frontend tool: ui_app_debug
The piece that closes the loop on the draft. One new tool in
web/src/lib/tools/builtin/puck.ts:
getPuckAgentHandler(application_id)— add adocument()accessor toPuckAgentHandlerthat assembles the current working document (Puck data + meta), mirroring the headless bridge’s existingdocument().- POST it as the route’s inline
document. - Return the compacted report.
Args: { application_id, run?, params?, interact? }. With run: false it is
the free static check (app debug --no-run); with run: true and no
interact the simulator’s defaultInteractions click the app’s natural
trigger. The assistant can script multi-step flows (set, click, run,
seedResource, …) in the same InteractionStep vocabulary the CLI and the
build harness use — no new language.
3.4 One compacted report shape
AppDebugReport is bundle-sized; a chat turn needs the verdict. Add a
summarizeAppReport(report) reducer in @nodetool-ai/execution/app-debug
returning: verdict (ok, headline, issues, warnings), per-widget end state
(id, type, binding, hasValue, visible, disabled, display
preview), invocation outcomes (status, decision, error), value previews, and
notSimulated. Both debug_app and ui_app_debug return this one shape —
the “summary reducer lives in execution so surfaces cannot drift” rule
debug_workflow already follows.
3.5 Prompt: teach the loop, retire the folklore
APP_BUILDER_SYSTEM_PROMPT (AppBuilderAgentPanel.tsx) predates operations,
variables, resources, and ui_app_get_binding_targets; its bare-name binding
advice survives only through the legacy fallback that resolves against
operations[0] — silently wrong on any multi-operation app. Rewrite it around
the harness loop:
- wire through
ui_app_get_binding_targets, never guessed names; - after any wiring change:
ui_app_debug { run: false }— free, instant; - before declaring the app done:
ui_app_debug { run: true }once, read the verdict, fix what it names. A run executes real workflows and spends real money — check often, run once. - for “build me an app” with nothing open:
build_app(the batch harness), then offer the bundle for import. The editor assistant refines; the batch harness drafts.
4. Rejected alternatives
- Run
buildApp’s repair loop inside the assistant. The conversation is the repair loop — the user plays judge and complaint-writer. What the assistant lacks is the oracle, not the orchestration. WiringbuildAppin would put two repair loops in one session, fighting over the document. - Save-then-debug as the primary path (tool reads the row, user must save
first). Simple, but grades a stale document mid-edit; verdicts that
sometimes describe an older draft are worse than none. The
application_idpath stays for saved apps; it just isn’t what the builder panel uses. - Client-side simulation. The simulator’s fold is shared code
(
@nodetool-ai/app-runtime), but its runs need the kernel; shipping kernel execution to the browser buys nothing over one HTTP round-trip to the same server that already runs builds. - A judge stage in the interactive loop. The judge exists because batch builds have no human; here the human is present. The structural verdict is the right ceiling for a tool call.
5. Milestones
- M1 — Route + service. Shared runner extraction, application-target
relocation into
execution/app-debug,POST /api/applications/debugwith inline-document support,summarizeAppReport. Tests mirrorapp-build-route.test.ts. - M2 —
debug_app. Tool + resident registration + chat system-prompt paragraph. Serves all chat surfaces immediately. - M3 —
ui_app_debug.PuckAgentHandler.document(), the frontend tool, headless-bridge mirror (so theapp-toolseval can score the check-before-done behavior),APP_BUILDER_SYSTEM_PROMPTrewrite. - M4 — Composition.
pollfor long runs,interactive: trueescalations, and an eval case that fails an assistant which declares an app done without a green run.
6. Failure modes
| Failure | Resolution |
|---|---|
| Inline document references a workflow the user can’t read | Per-operation unavailable, reported in the verdict — the simulator already models this |
Run exceeds timeout_ms |
Invocation reports timedOutMs; verdict issue; no parked session |
| Draft diverges mid-request (user edits while the tool runs) | The report names the document version it graded; the tool result says “as of this snapshot” |
Cost runaway from repeated run: true |
Prompt discipline plus the same per-run timeout; runs land in the prediction ledger like every kernel run |