@womp/spark-agent 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ Notable changes to `@womp/spark-agent`, newest first. Versions follow
4
4
  [Semantic Versioning](https://semver.org/). Add an entry with each version bump; describe what
5
5
  changed for hosts and users, and how to migrate across breaking changes.
6
6
 
7
+ ## 9.2.0 — 2026-10-12
8
+
9
+ ### Added
10
+
11
+ - `flowRuns` capability (`flow-runs` skill, `womp.flowRuns`): describe, run, follow (`status`,
12
+ `wait` up to 60 s) and cancel a run of the user's saved Flow pipeline, with no canvas open.
13
+ Hosts implement `FlowRunsHost`; one run starts per JavaScript call, and run ids are validated
14
+ before the host is called. Crow composes it under the run's `flowRun` gate.
15
+ - flowRuns.run({ copy: true }) runs the flow on a new copy of the project ("Flow runs" folder); start and status report projectId, sourceProjectId and projectUrl. A copy start waits through `waitForService` ("Copying the project") when the host has it, so the copy is outside the 60 s JavaScript budget.
16
+ - flowRuns.run({ copy: true, deleteCopy: true }) deletes the copy once the run ends, and flowRuns.deleteCopy(runId) deletes an ended copy run's copy (through `waitForService`, "Deleting the copy"). Start and status report deleteCopy and copyDeletedAt; projectUrl is null once the copy is gone. Output links keep working. Right after a cancel, deleteCopy(runId) may answer run_active for about a minute while the run stops; the skill says to retry later.
17
+
7
18
  ## 9.1.0 — 2026-10-12
8
19
 
9
20
  The agent writes procedural materials as FlowIR programs, the same way it writes Geometry Nodes,
@@ -0,0 +1,95 @@
1
+ import type { SparkCapability, SparkInvocationContext } from "../capabilities.js";
2
+ /** An input the flow can be run with, addressed by its input node id. */
3
+ export type FlowRunInput = {
4
+ nodeId: string;
5
+ typeId: string;
6
+ valueType: string;
7
+ /** The value saved in the flow now. */
8
+ value: unknown;
9
+ /** Wired into the graph: a run needs a value for it. */
10
+ required: boolean;
11
+ };
12
+ export type FlowRunOutputNode = {
13
+ nodeId: string;
14
+ typeId: string;
15
+ };
16
+ export type FlowRunLive = {
17
+ runId: string;
18
+ trigger: string;
19
+ };
20
+ export type FlowRunDescription = {
21
+ projectId: number;
22
+ snapshotId: string;
23
+ inputs: FlowRunInput[];
24
+ outputs: FlowRunOutputNode[];
25
+ /** The run live on this flow now, if any; only one can be. */
26
+ liveRun: FlowRunLive | null;
27
+ };
28
+ export type FlowRunStarted = {
29
+ runId: string;
30
+ status: string;
31
+ repeated: boolean;
32
+ /** The project the run runs on: the copy for a copy run. */
33
+ projectId: number;
34
+ /** The copied project for a copy run; null in place. */
35
+ sourceProjectId: number | null;
36
+ /** The URL of projectId; null once a copy run's copy was deleted. */
37
+ projectUrl: string | null;
38
+ /** The copy is deleted when the run ends (run({ copy: true, deleteCopy: true })). */
39
+ deleteCopy: boolean;
40
+ /** When the copy was deleted (ISO); null while it exists. */
41
+ copyDeletedAt: string | null;
42
+ };
43
+ export type FlowRunStatus = {
44
+ runId: string;
45
+ /** "running", "completed", "failed" or "cancelled". */
46
+ status: string;
47
+ /** The output nodes' values once completed. */
48
+ outputs: unknown;
49
+ error: string | null;
50
+ errorCode: string | null;
51
+ /** For an ended run: "now" when this call reports it, "earlier" when it was already reported. */
52
+ reported: "now" | "earlier" | null;
53
+ projectId: number;
54
+ sourceProjectId: number | null;
55
+ projectUrl: string | null;
56
+ deleteCopy: boolean;
57
+ copyDeletedAt: string | null;
58
+ };
59
+ export type FlowRunCancelled = {
60
+ runId: string;
61
+ status: string;
62
+ cancelled: boolean;
63
+ };
64
+ export type FlowRunCopyDeleted = {
65
+ runId: string;
66
+ copyDeletedAt: string;
67
+ };
68
+ export type WompFlowRunsApi = {
69
+ describe(): Promise<FlowRunDescription>;
70
+ run(options?: {
71
+ inputs?: Record<string, unknown>;
72
+ copy?: boolean;
73
+ deleteCopy?: boolean;
74
+ }): Promise<FlowRunStarted>;
75
+ status(runId: string): Promise<FlowRunStatus>;
76
+ wait(runId: string, seconds?: number): Promise<FlowRunStatus>;
77
+ cancel(runId: string): Promise<FlowRunCancelled>;
78
+ deleteCopy(runId: string): Promise<FlowRunCopyDeleted>;
79
+ };
80
+ export type FlowRunsHost = {
81
+ describe(context: SparkInvocationContext): Promise<FlowRunDescription>;
82
+ start(options: {
83
+ inputs: Record<string, unknown>;
84
+ copy: boolean;
85
+ deleteCopy: boolean;
86
+ }, context: SparkInvocationContext): Promise<FlowRunStarted>;
87
+ status(runId: string, context: SparkInvocationContext): Promise<FlowRunStatus>;
88
+ /** Waits at most `seconds` for the run to end and returns its status either way. */
89
+ wait(runId: string, seconds: number, context: SparkInvocationContext): Promise<FlowRunStatus>;
90
+ cancel(runId: string, context: SparkInvocationContext): Promise<FlowRunCancelled>;
91
+ deleteCopy(runId: string, context: SparkInvocationContext): Promise<FlowRunCopyDeleted>;
92
+ };
93
+ export declare const FLOW_RUN_WAIT_MAX_SECONDS = 60;
94
+ export declare const FLOW_RUN_WAIT_DEFAULT_SECONDS = 30;
95
+ export declare function createFlowRunsCapability(host: FlowRunsHost): SparkCapability<WompFlowRunsApi>;
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ import { WOMP_FLOW_RUNS_DECLARATIONS } from "../generated/flow-runs-declarations.js";
3
+ export const FLOW_RUN_WAIT_MAX_SECONDS = 60;
4
+ export const FLOW_RUN_WAIT_DEFAULT_SECONDS = 30;
5
+ const runId = z
6
+ .string()
7
+ .regex(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i, "Use a runId returned by run().");
8
+ const runOptions = z
9
+ .object({
10
+ inputs: z.record(z.string().min(1).max(200), z.json()).optional(),
11
+ copy: z.boolean().optional(),
12
+ deleteCopy: z.boolean().optional(),
13
+ })
14
+ .strict()
15
+ .refine((options) => !options.deleteCopy || options.copy === true, { message: "deleteCopy needs copy: true." });
16
+ const waitSeconds = z.number().int().min(1).max(FLOW_RUN_WAIT_MAX_SECONDS).default(FLOW_RUN_WAIT_DEFAULT_SECONDS);
17
+ export function createFlowRunsCapability(host) {
18
+ return {
19
+ skills: [
20
+ {
21
+ name: "flow-runs",
22
+ version: "1",
23
+ description: "Run the user's saved Flow (AI Nodes) pipeline on their project, follow the run, and report its result.",
24
+ instructions: `Use womp.flowRuns to run the user's saved Flow pipeline. No canvas needs to be open.
25
+ A run operates on the user's real flow: the input values you pass are written into the flow and become its saved values. describe() lists the inputs by node id with their current values, and the output nodes. Pass only input node ids it lists.
26
+ Only one run of a flow can be live. run() starts the run and returns at once. For a short flow, call wait(runId) and then report the result.
27
+ To leave the user's flow untouched, pass copy: true. The run then happens on a new copy of the project, placed in the user's "Flow runs" folder: the input values, outputs and any scene changes land on the copy, and the user's flow, its saved values and its scene are not changed. Use it when the user wants to try values without changing their flow, or when their flow is busy. A copy run does not lock the scene. run() returns the copy's projectUrl; give it to the user so they can open the result.
28
+ To keep only the result of a copy run, also pass deleteCopy: true: the copy is deleted when the run ends, whether it completed, failed or was cancelled. Use it when the user wants the outputs, not the project; do not give them that run's projectUrl, which stops working (status() then returns projectUrl null).
29
+ To delete a copy run's copy later, call deleteCopy(runId) once the run has ended; it is refused for a run that is still running (cancel it first) or one that did not run on a copy. Right after cancel(), deleteCopy() may answer run_active for about a minute while the run stops: retry later, or pass deleteCopy: true at start instead. Output links keep working after the copy is deleted, either way.
30
+ A run may outlive your turn. If it does, the system reports the result in a follow-up, so do not keep calling status() across many calls.
31
+ When status() or wait() returns an ended run with reported "now", the result is yours to tell the user. With reported "earlier", the result was already reported, earlier in the conversation or by the refusal run() returned to you; do not repeat it.
32
+ Errors start with a code. flow_busy and snapshot_changed mean the user is using the flow: tell them and do not retry. backend_deploying means try again later. not_authorized is final. no_project means the conversation needs a project first.
33
+ Codes starting with input_ mean fix the inputs using describe() and run again. Any other code means tell the user what happened, and do not start another run unless they ask.
34
+ While a run is live on this project (not a copy run), womp.scene.access is refused, for inspecting the scene as well as editing it; womp.scene.health and womp.scene.screenshot still work. To use the scene, wait for the run to end or cancel it.`,
35
+ declarations: WOMP_FLOW_RUNS_DECLARATIONS,
36
+ examples: [
37
+ "print(await womp.flowRuns.describe());",
38
+ 'const started = await womp.flowRuns.run({ inputs: { "input-node-id": 123 } }); print(await womp.flowRuns.wait(started.runId, 60));',
39
+ 'const started = await womp.flowRuns.run({ inputs: { "input-node-id": 123 }, copy: true }); print(started.projectUrl);',
40
+ 'const started = await womp.flowRuns.run({ inputs: { "input-node-id": 123 }, copy: true, deleteCopy: true }); print(await womp.flowRuns.wait(started.runId, 60));',
41
+ ],
42
+ },
43
+ ],
44
+ createApi: (context) => {
45
+ let started = false;
46
+ return {
47
+ describe: () => host.describe(context),
48
+ run: async (options) => {
49
+ context.signal.throwIfAborted();
50
+ const parsed = runOptions.parse(options ?? {});
51
+ if (started)
52
+ throw new Error("Start one flow run per JavaScript call. Continue in a new call.");
53
+ started = true;
54
+ const copy = parsed.copy ?? false;
55
+ const start = () => host.start({ inputs: parsed.inputs ?? {}, copy, deleteCopy: parsed.deleteCopy ?? false }, context);
56
+ // Copying can outlast the 60 s JavaScript budget; a service wait is
57
+ // outside it, so the host's own copy timeout applies instead.
58
+ if (copy && context.waitForService)
59
+ return context.waitForService("Copying the project", start);
60
+ return start();
61
+ },
62
+ status: async (id) => host.status(runId.parse(id), context),
63
+ wait: async (id, seconds) => host.wait(runId.parse(id), waitSeconds.parse(seconds), context),
64
+ cancel: async (id) => host.cancel(runId.parse(id), context),
65
+ deleteCopy: async (id) => {
66
+ context.signal.throwIfAborted();
67
+ const parsed = runId.parse(id);
68
+ const remove = () => host.deleteCopy(parsed, context);
69
+ // Deleting a project can outlast the 60 s JavaScript budget, as copying one can.
70
+ if (context.waitForService)
71
+ return context.waitForService("Deleting the copy", remove);
72
+ return remove();
73
+ },
74
+ };
75
+ },
76
+ };
77
+ }
@@ -17,6 +17,8 @@ export { createGeometryCapability } from "./capabilities/geometry.js";
17
17
  export type { SceneHealthHost } from "./capabilities/scene.js";
18
18
  export { createFlowCapability } from "./capabilities/flow.js";
19
19
  export type { FlowHost, FlowSnapshot, FlowWrite, FlowWriteResult, WompFlowApi, } from "./capabilities/flow.js";
20
+ export { createFlowRunsCapability, FLOW_RUN_WAIT_DEFAULT_SECONDS, FLOW_RUN_WAIT_MAX_SECONDS, } from "./capabilities/flow-runs.js";
21
+ export type { FlowRunCancelled, FlowRunCopyDeleted, FlowRunDescription, FlowRunInput, FlowRunLive, FlowRunOutputNode, FlowRunStarted, FlowRunStatus, FlowRunsHost, WompFlowRunsApi, } from "./capabilities/flow-runs.js";
20
22
  export type { GeometryHost, GeometryTarget, GeometryInstance, GeometryParamValue, GeometryWrite, GeometryWriteResult, WompGeometryApi, } from "./capabilities/geometry.js";
21
23
  export type { WompOpenScadApi, OpenScadHost, OpenScadTarget, OpenScadSource, OpenScadCreate, OpenScadUpdate, OpenScadValidation, OpenScadPrintability, OpenScadWriteResult, } from "./capabilities/openscad.js";
22
24
  export type ArtifactRef = {
@@ -6,6 +6,7 @@ export { createAssetsCapability } from "./capabilities/assets.js";
6
6
  export { createOpenScadCapability } from "./capabilities/openscad.js";
7
7
  export { createGeometryCapability } from "./capabilities/geometry.js";
8
8
  export { createFlowCapability } from "./capabilities/flow.js";
9
+ export { createFlowRunsCapability, FLOW_RUN_WAIT_DEFAULT_SECONDS, FLOW_RUN_WAIT_MAX_SECONDS, } from "./capabilities/flow-runs.js";
9
10
  /** The enabled bindings are also the only source of discoverable skills. */
10
11
  export function composeCapabilities(capabilities) {
11
12
  const skills = Object.values(capabilities).flatMap((capability) => capability.skills);
@@ -0,0 +1 @@
1
+ export declare const WOMP_FLOW_RUNS_DECLARATIONS = "declare const womp: { flowRuns: {\n\"describe\": (() => Promise<{\n\"projectId\": number;\n\"snapshotId\": string;\n\"inputs\": Array<{\n\"nodeId\": string;\n\"typeId\": string;\n\"valueType\": string;\n\"value\": unknown;\n\"required\": false | true;\n}>;\n\"outputs\": Array<{\n\"nodeId\": string;\n\"typeId\": string;\n}>;\n\"liveRun\": null | {\n\"runId\": string;\n\"trigger\": string;\n};\n}>);\n\"run\": ((options?: undefined | {\n\"inputs\"?: undefined | {\n[key: string]: unknown;\n};\n\"copy\"?: undefined | false | true;\n\"deleteCopy\"?: undefined | false | true;\n}) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"repeated\": false | true;\n\"projectId\": number;\n\"sourceProjectId\": null | number;\n\"projectUrl\": null | string;\n\"deleteCopy\": false | true;\n\"copyDeletedAt\": null | string;\n}>);\n\"status\": ((runId: string) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"outputs\": unknown;\n\"error\": null | string;\n\"errorCode\": null | string;\n\"reported\": null | \"now\" | \"earlier\";\n\"projectId\": number;\n\"sourceProjectId\": null | number;\n\"projectUrl\": null | string;\n\"deleteCopy\": false | true;\n\"copyDeletedAt\": null | string;\n}>);\n\"wait\": ((runId: string, seconds?: undefined | number) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"outputs\": unknown;\n\"error\": null | string;\n\"errorCode\": null | string;\n\"reported\": null | \"now\" | \"earlier\";\n\"projectId\": number;\n\"sourceProjectId\": null | number;\n\"projectUrl\": null | string;\n\"deleteCopy\": false | true;\n\"copyDeletedAt\": null | string;\n}>);\n\"cancel\": ((runId: string) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"cancelled\": false | true;\n}>);\n\"deleteCopy\": ((runId: string) => Promise<{\n\"runId\": string;\n\"copyDeletedAt\": string;\n}>);\n} };\ndeclare function print(...values: unknown[]): void;\ndeclare function emitImage(ref: { id: string; mediaType: string; name?: string }, options?: { inspect?: boolean }): Promise<void>;\ndeclare function emitFile(ref: { id: string; mediaType: string; name?: string }): Promise<void>;\n";
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/generate-skill-declarations.mjs.
2
+ // prettier-ignore
3
+ export const WOMP_FLOW_RUNS_DECLARATIONS = "declare const womp: { flowRuns: {\n\"describe\": (() => Promise<{\n\"projectId\": number;\n\"snapshotId\": string;\n\"inputs\": Array<{\n\"nodeId\": string;\n\"typeId\": string;\n\"valueType\": string;\n\"value\": unknown;\n\"required\": false | true;\n}>;\n\"outputs\": Array<{\n\"nodeId\": string;\n\"typeId\": string;\n}>;\n\"liveRun\": null | {\n\"runId\": string;\n\"trigger\": string;\n};\n}>);\n\"run\": ((options?: undefined | {\n\"inputs\"?: undefined | {\n[key: string]: unknown;\n};\n\"copy\"?: undefined | false | true;\n\"deleteCopy\"?: undefined | false | true;\n}) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"repeated\": false | true;\n\"projectId\": number;\n\"sourceProjectId\": null | number;\n\"projectUrl\": null | string;\n\"deleteCopy\": false | true;\n\"copyDeletedAt\": null | string;\n}>);\n\"status\": ((runId: string) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"outputs\": unknown;\n\"error\": null | string;\n\"errorCode\": null | string;\n\"reported\": null | \"now\" | \"earlier\";\n\"projectId\": number;\n\"sourceProjectId\": null | number;\n\"projectUrl\": null | string;\n\"deleteCopy\": false | true;\n\"copyDeletedAt\": null | string;\n}>);\n\"wait\": ((runId: string, seconds?: undefined | number) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"outputs\": unknown;\n\"error\": null | string;\n\"errorCode\": null | string;\n\"reported\": null | \"now\" | \"earlier\";\n\"projectId\": number;\n\"sourceProjectId\": null | number;\n\"projectUrl\": null | string;\n\"deleteCopy\": false | true;\n\"copyDeletedAt\": null | string;\n}>);\n\"cancel\": ((runId: string) => Promise<{\n\"runId\": string;\n\"status\": string;\n\"cancelled\": false | true;\n}>);\n\"deleteCopy\": ((runId: string) => Promise<{\n\"runId\": string;\n\"copyDeletedAt\": string;\n}>);\n} };\ndeclare function print(...values: unknown[]): void;\ndeclare function emitImage(ref: { id: string; mediaType: string; name?: string }, options?: { inspect?: boolean }): Promise<void>;\ndeclare function emitFile(ref: { id: string; mediaType: string; name?: string }): Promise<void>;\n";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@womp/spark-agent",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "description": "Portable skills-based AI agent with local and durable chat transports for Womp",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",