@womp/spark-agent
v11.0.0
Published
Portable skills-based AI agent with local and durable chat transports for Womp
Keywords
Readme
@womp/spark-agent
Portable AI agent for Womp applications and Kakapo scenes with model-authored JavaScript.
Spark Agent uses AI SDK 7 UIMessage and UIMessageChunk as its public chat protocol. Generated
JavaScript uses the host's executor and capabilities. Native browser execution and isolated QuickJS
execution use the same model loop. The host owns conversations, artifacts, authorization, and connections.
Release notes and migration guides are in CHANGELOG.md.
Requirements
- Node.js 22.13 or newer
- AI SDK 7
@ai-sdk/react4 for React clients
npm install @womp/spark-agentPackage entry points
@womp/spark-agent— browser-safe model loop, two tools, persistence hooks, and shared contracts@womp/spark-agent/browser— native JavaScript executor and local chat transport@womp/spark-agent/capabilities— capability composition and skill contracts@womp/spark-agent/kakapo— direct Kakapo runtime and scene capability@womp/spark-agent/quickjs— optional isolated JavaScript executor@womp/spark-agent/ui— browser-safe UI message types, schemas, and model catalog@womp/spark-agent/client—SparkChatTransportand HTTP client types@womp/spark-agent/server— run service, store interfaces, executor, and development stores@womp/spark-agent/express— mountable Express 5 router
The root, /browser, /ui, /capabilities, and /client entry points do not import QuickJS,
Kakapo, Express, or React at runtime.
Running in a host application
runSparkAgent accepts a configured model, the stored conversation (history, compacted once it
is long) or model messages, an executor, a capability composition, and an artifact resolver. It exposes exactly read_skill and execute_javascript to the model.
Skills carry instructions, declarations, and JavaScript examples. Kakapo skill declarations are
generated from its typed API at build time and packaged as JavaScript strings.
System prompt and editor selection
Every host sends sparkSystemPrompt(skills): the shared agent prompt and the skills directory,
with nothing host-specific, so it stays identical across a conversation. Hosts attach the editor
selection to the user message it was sent with as a data-selection part ({ nodeIds }). It is
not rendered as chat text; the model reads it as a short note on that message.
Capabilities
Compose only supported capabilities with composeCapabilities({ scene, account, ... }). Each
capability supplies its skills and createApi(context); the returned API is bound under womp.
Every invocation receives a cancellation signal and an awaited recordReceipt hook. Capabilities
own their transactions and must check cancellation at their commit boundary.
For a real user interaction, call context.waitForUser(label, async signal => ...) outside mutation
locks. The run stays active with requires_action progress and resumes the same invocation after
input; the wait is excluded from the execution budget. Abort settles the wait and late answers
cannot revive it. Custom executors must honor the optional host-owned userWait argument to execute.
Shared SDK scene adapters apply each edit directly to the scene and save it; nothing is journaled
or replayed. An edit that fails after it began throws SceneOperationError, whose operation
reports whether the edit was committed, rolled back or left uncertain.
Persistence and run lifecycle
Persist the user message before starting a run. Supply persistence.beforeToolCall,
persistence.recordReceipt, and persistence.afterToolCall for awaited storage barriers.
Supply persistence.saveMessage to save the canonical UI message at each model-step boundary and
at termination. Spark awaits each checkpoint before the next model request; a failed barrier aborts
the loop. Consume the run's stream while awaiting its result; a host run owner must keep consuming
when a view detaches.
The default model-step limit is 100; reaching it returns status: "incomplete", and hosts must not
restart automatically to bypass it. A generation that ends on any other non-stop reason fails the
run with the unified and provider finish reasons in its error and diagnostics.
When a run is given its stored history, Spark compacts it before any model step once the
conversation reaches the compaction threshold: earlier turns, and earlier steps of a long turn, are
replaced by a summary recorded on the run's message as a data-compaction part. Nothing else
shortens a request; one that is still over the model's context window fails with the provider's
error.
Artifacts
Screenshot and file APIs return serializable artifact references. emitImage and emitFile resolve
them through the host's artifact resolver. Keep references resolvable when reloading history, and
pass createSparkTools(...) to AI SDK's convertToModelMessages to reconstruct multimodal results.
emitImage(artifact, { inspect: false }) displays an image without adding its bytes to model input.
Local frontend
LocalSparkChatTransport accepts a host callback returning a SparkAgentRun from runSparkAgent.
The callback receives the standard AI SDK chat request, including its messages and abort signal.
It persists the initiating message, resolves the model and capabilities, and starts the shared loop.
Keep ownership of the active run outside the React renderer. Local browser runs end when the page
closes; reconnectToStream returns null.
Remote frontend
import { useChat } from "@ai-sdk/react";
import { SparkChatTransport } from "@womp/spark-agent/client";
import {
sparkDataPartSchemas,
sparkMessageMetadataSchema,
type SparkUIMessage,
} from "@womp/spark-agent/ui";
const transport = new SparkChatTransport({ api: "/agent" });
const chat = useChat<SparkUIMessage>({
id: conversationId,
messages: initialMessages,
transport,
resume: true,
messageMetadataSchema: sparkMessageMetadataSchema,
dataPartSchemas: sparkDataPartSchemas,
});Render message.parts. Standard AI SDK parts represent text, reasoning, files, tool inputs, tool
outputs, approvals, and step boundaries. Spark adds typed data-console, data-artifact,
data-run-status, data-step-metrics, data-execution-receipt, and data-tool-input-label parts.
An explicit Stop action must cancel the durable run before stopping local observation:
await transport.cancelRun(conversationId);
await chat.stop();Closing or reloading the page only detaches the SSE subscriber. It does not cancel the run.
HTTP and SSE
SparkChatTransport hides the durable run lifecycle from useChat:
POST /agent/conversations/:conversationId/messages
PATCH /agent/conversations/:conversationId/settings
POST /agent/conversations/:conversationId/goal
GET /agent/runs/:runId/stream?after=0
POST /agent/runs/:runId/cancelConversation settings contain modelId and reasoningEffort; save them with
transport.saveConversationSettings(id, settings). The goal endpoint accepts set, pause,
resume, clear, and update_budget commands.
A goal starts from an ordinary message, when the agent sets one, and changes when the user asks in
chat. A service with evaluateGoal gives the agent the goal tool. With set it starts a goal
before building, when the request sets constraints a result can be measured against, from a title
and the conditions, each marked measurable or left to the user's review. The goal starts at once,
with that run as its first try and with 5 tries and 2,000 credits unless the call gives others; the
service evaluates each measurable condition after every try and records the result on
goal.conditions. change gives the goal new conditions or limits, continue has a stopped goal
go on, and remove ends it; after a change the goal goes on in that turn. A set over an existing
goal ends that one, and the call's result keeps its summary as ended. onGoalChange tells a host
that counts a goal's spending when a run starts or stops working for one. A host leaves
handleGoal out of the run it executes to withhold the tool.
The SSE response contains ordered persistence envelopes around standard AI SDK chunks:
type StoredSparkChunk = {
sequence: number;
conversationId: string;
runId: string;
createdAt: string;
chunk: SparkUIMessageChunk;
};The transport unwraps each envelope and returns the contained UIMessageChunk to useChat.
Active assistant messages are replayed from sequence zero when reconnecting.
Embedding in an Express backend
Spark does not call listen() when imported. The host owns its HTTP server and mounts the router:
import { createSparkAgentRouter } from "@womp/spark-agent/express";
import { SparkAgentService, createSparkRunExecutor } from "@womp/spark-agent/server";
const service = new SparkAgentService({
store,
executeRun: createSparkRunExecutor({
resolveModel,
createHost,
}),
});
app.use(
"/agent",
userAccess,
createSparkAgentRouter({
service,
artifactStore,
resolveOwnerId: (request) => request.user.id,
}),
);MemorySparkAgentStore and MemorySparkArtifactStore are development implementations. Production
hosts must supply durable implementations backed by their database/object storage and must preserve
the ordering contract of appendRunChunk(). Custom stores must also:
- implement
saveConversationSettingsas a settings-only update, and preserve stored settings duringsaveConversation; - implement
saveConversationGoalto support goals; - optionally implement
releaseActiveRun(conversationId, runId, updatedAt)to clear the active run only if its ID still matches.
The built-in memory and SQLite stores implement all of these.
SparkAgentServiceOptions.endOrphanedRuns defaults to false. Enable it only when one process
owns the store: a message or Stop then ends unfinished runs not held by that process.
Mesh Wompify
Mesh Wompify reconstructs a mesh that is already in the scene as editable primitives, through the
shared mesh-wompify and scene-editing skills. The host imports the mesh itself and sends
/wompify-mesh <node id> <tier> as the user's message, with tier simple, high or ultra. The
skill sets the goal the reconstruction is refined toward, with the tier's credit and tries limits.
Standalone development server
The package includes a local server bound to 127.0.0.1:8787:
spark-agentConfigure the process with:
OPENAI_API_KEYANTHROPIC_API_KEYGEMINI_API_KEYKAKAPO_URL(defaults tows://127.0.0.1:5502)PORT(defaults to8787)SPARK_STORAGE_PATH(defaults to.spark/spark.sqlite)SPARK_SCENE_IDENTITY(optional; set only when the value names the same durable scene across renderer reconnects)
The CLI loads .env from its working directory when present; copy .env.example to .env and add
the provider key you intend to use. Process environment variables take precedence.
For repository development, yarn dev starts the Express server and Vite demo together. Open
http://127.0.0.1:5173. When using a sibling kakapo-sdk dependency, run yarn build in
../kakapo-sdk before installing Spark dependencies.
The standalone server persists conversations, run streams, and screenshots in SQLite; only one
local process can execute against a database. createStandaloneSparkAgentApp(options) is
asynchronous: await it before listening, and call await app.dispose() on shutdown.
yarn dev:demo-account (or spark-agent --profile demo-account) adds a fictional account skill
and two development material resources beside the real local Kakapo scene API. Its account reads
identify themselves as fixtures; other account services report unavailable.
Run a prompt from the command line
With the Spark server already running:
yarn agent-run --prompt "Make a cube" --model gpt-5.6-luna --reasoning max
yarn agent-run --prompt "Model this object using both views" --image front.png --image side.jpg --output .spark/agent-runs/reference-model--conversation <id>continues a thread.--image <path>attaches a local PNG, JPEG, WebP or GIF reference; repeat it for more images.--output <new-directory>savesresult.json,transcript.json, inputs, and run artifacts. For local runs, use a fresh subdirectory of the git-ignored.spark/directory.
Progress goes to stderr; one JSON result goes to stdout. Use yarn --silent agent-run when parsing
stdout. Exit codes: 0 completed, 1 run/transport/export failure, 2 invalid arguments, 3 requires
user action, 130 interrupted.
Measure intent routing
yarn intent-bench checks which route Spark takes for the requests in
src/intent-bench-cases.ts: editable scene, OpenSCAD, Geometry Nodes, image, image edit, mesh,
image-to-mesh, Flow, import, or a reply without acting. It needs no server or renderer. Each run
gets Swan's skills over fixtures: an empty scene, the case's attachments and earlier turns, and
Flow open or closed. Photo cases download their image from Womp's media host. The first call that would change the scene, generate, write a graph or
import is recorded, and the run stops before it takes effect. Material edits do not decide a
route, since Geometry Nodes prepares its materials in the scene first.
yarn intent-bench # every case 3 times on gpt-6-luna, 24 at once
yarn intent-bench --only photo-only,F1 --repeat 10 --model gpt-6-astra
yarn intent-bench --listIt prints each case's routes and pass rate and writes report.json, with every run's skills,
calls, deciding call and reply, under .spark/intent-bench/. It exits 1 when any run took a route
the case does not expect. Model calls are paid; the full set costs about $0.10 on gpt-6-luna.
Run cost estimates
The chat displays estimated API token cost in USD after every completed model step, from the
provider-reported usage and the rates in src/pricing.ts. Each data-step-metrics part stores
its cost snapshot, so recorded runs are not repriced when rates change. Missing usage or unknown
models are shown as unavailable, never priced as zero. Estimates exclude taxes, discounts, and
scene hosting; the provider bill is authoritative.
Prompt caching is enabled for every listed model. It depends on an identical request prefix, which is why per-turn state travels in messages rather than the system prompt.
Execution security
The standalone host uses a fresh QuickJS runtime for each JavaScript execution. It has no Node.js
or browser globals and receives only the composed capability APIs. Imports, filesystem access,
network APIs, and host environment variables are not exposed. QuickJS limits memory, stack size,
execution time, source size, host calls, and data crossing the host bridge. Both executors allow
100,000 host calls per execution by default (maxHostCalls overrides this in QuickJS).
Browser hosts can instead use native page execution. It runs in the page realm: it is not a sandbox and cannot interrupt a synchronous infinite loop.
QuickJS isolates generated code from the JavaScript host API; it is not an operating-system security boundary. Multi-tenant deployments must additionally isolate workers, authorize every Kakapo session and tool call, enforce quotas, and avoid passing privileged functions through the runtime bridge.
Validation
yarn build
yarn typecheck
yarn test
yarn web:buildRelease procedure
Pushes to main run .github/workflows/publish.yml. Publication is driven by package.json, not
Git tags: already-published versions are skipped; an unpublished stable version newer than npm's
latest runs the release checks and is published automatically.
Versions follow MAJOR.MINOR.PATCH: major for breaking changes, minor for backward-compatible features, patch for backward-compatible fixes.
- Configure npm's trusted publisher once for
@womp/spark-agent: organizationwompxyz, repositoryspark-agent, workflow filenamepublish.yml, no environment, and allow directnpm publish. GitHub-hosted runners use OIDC; no publishing token is required. - Bump
package.jsonto an unused stable version, add its entry toCHANGELOG.md, and push the release commit tomain. Prereleases are not published by this workflow. - CI installs the frozen lockfile, runs typechecks, tests and the UI build, verifies the packed
tarball's entry points and imports, then publishes and verifies registry integrity and
latest. - If publication reports an uncertain result, inspect npm before rerunning the workflow. Published versions cannot be replaced; corrections need a new version.
- In the consuming application, run
yarn add @womp/spark-agent@<version> --exactand commit its manifest and lockfile together.
Layout
src/agent.ts— AI SDK stream and Spark tool orchestrationsrc/prompt.ts— shared system promptsrc/compaction.ts— conversation compaction before each model stepsrc/context-window.ts— model context windows and request size estimatessrc/ui.ts— browser-safe protocol types and validation schemassrc/client.ts— durableChatTransportimplementationsrc/server/— service, storage contracts, Express adapter, artifacts, and standalone compositionsrc/runtime.ts— Kakapo scene connections and edit transactionssrc/quickjs-executor.ts— isolated JavaScript execution and host bridgesrc/womp-scene-api.ts— Womp-compatible Kakapo scene APIweb/src/App.tsx— referenceuseChat<SparkUIMessage>application
