@womp/spark-agent 4.0.0 → 4.1.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.
Files changed (40) hide show
  1. package/.env.example +6 -0
  2. package/README.md +67 -59
  3. package/dist/agent-tools.js +49 -7
  4. package/dist/capabilities/geometry.js +19 -16
  5. package/dist/capabilities/openscad-instructions.d.ts +1 -1
  6. package/dist/capabilities/openscad-instructions.js +1 -4
  7. package/dist/capabilities/openscad.js +2 -2
  8. package/dist/capabilities/operations.js +13 -9
  9. package/dist/capabilities/scene.js +32 -27
  10. package/dist/generated/account-declarations.d.ts +1 -1
  11. package/dist/generated/account-declarations.js +1 -1
  12. package/dist/generated/asset-import-declarations.d.ts +1 -1
  13. package/dist/generated/asset-import-declarations.js +1 -1
  14. package/dist/generated/assets-declarations.d.ts +1 -1
  15. package/dist/generated/assets-declarations.js +1 -1
  16. package/dist/generated/flow-declarations.d.ts +1 -1
  17. package/dist/generated/flow-declarations.js +1 -1
  18. package/dist/generated/geometry-declarations.d.ts +1 -1
  19. package/dist/generated/geometry-declarations.js +1 -1
  20. package/dist/generated/knowledge-declarations.d.ts +1 -1
  21. package/dist/generated/knowledge-declarations.js +1 -1
  22. package/dist/generated/openscad-declarations.d.ts +1 -1
  23. package/dist/generated/openscad-declarations.js +1 -1
  24. package/dist/generated/operations-declarations.d.ts +1 -1
  25. package/dist/generated/operations-declarations.js +1 -1
  26. package/dist/generated/scene-declarations.d.ts +1 -1
  27. package/dist/generated/scene-declarations.js +1 -1
  28. package/dist/geometry/lower.js +3 -1
  29. package/dist/geometry/schemas/flowir-dialect.json +53 -9
  30. package/dist/geometry.js +2 -1
  31. package/dist/model-history.js +1 -1
  32. package/dist/prompt.d.ts +1 -1
  33. package/dist/prompt.js +75 -19
  34. package/dist/server/service.js +14 -16
  35. package/dist/tool-input-label.d.ts +2 -0
  36. package/dist/tool-input-label.js +39 -0
  37. package/dist/ui.d.ts +14 -0
  38. package/dist/ui.js +1 -0
  39. package/dist/womp-scene-api.js +7 -3
  40. package/package.json +6 -6
package/.env.example ADDED
@@ -0,0 +1,6 @@
1
+ OPENAI_API_KEY=
2
+ ANTHROPIC_API_KEY=
3
+ GEMINI_API_KEY=
4
+ KAKAPO_URL=ws://127.0.0.1:5502
5
+ PORT=8787
6
+ SPARK_STORAGE_PATH=.spark/spark.sqlite
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @womp/spark-agent
2
2
 
3
- Portable AI agent for Womp applications and Kakapo scenes with model-authored JavaScript.
4
-
3
+ Portable AI agent for Womp applications and Kakapo scenes with model-authored JavaScript.
4
+
5
5
  Spark Agent uses AI SDK 7 `UIMessage` and `UIMessageChunk` as its public chat protocol. Generated
6
6
  JavaScript uses the host's executor and capabilities. Native browser execution and isolated QuickJS
7
7
  execution use the same model loop. The host owns conversations, artifacts, authorization, and connections.
@@ -16,47 +16,47 @@ execution use the same model loop. The host owns conversations, artifacts, autho
16
16
  npm install @womp/spark-agent
17
17
  ```
18
18
 
19
- ## Run a prompt from the command line
20
-
21
- With the Spark server already running:
22
-
23
- ```sh
24
- yarn agent-run --prompt "Make a cube" --model gpt-5.6-luna --reasoning max
25
- ```
26
-
27
- Use `--conversation <id>` to continue a thread and `--output <new-directory>` to save
28
- `result.json`, `transcript.json` and original run artifacts under `artifacts/`.
29
- For local runs, use the git-ignored `.spark/` directory, with a fresh subdirectory per run:
30
-
31
- ```sh
32
- yarn agent-run --prompt "Make rainbow-colored boxes" --output .spark/agent-runs/rainbow-boxes
33
- yarn agent-run --prompt "Model this object using both views" --image front.png --image side.jpg --output .spark/agent-runs/reference-model
34
- ```
35
-
36
- Repeat `--image <path>` for local PNG, JPEG, WebP or GIF references, in argument order.
37
- The required prompt and all images are sent in one user message. Files, format signatures,
38
- and the full JSON request size (including base64 overhead) are checked before contacting the server.
39
- The CLI uses the server's default 25 MiB request limit; a custom server or provider may impose
40
- stricter limits. Images are never resized, and remote URLs are not accepted.
41
- With `--output`, original bytes are saved under `inputs/`; `input.json` and the result's `input`
42
- field list ordered original filenames, MIME types and relative paths. The submitted message in
43
- `transcript.json` uses those paths instead of inline data. Generated artifacts stay in `artifacts/`.
44
-
45
- Without output, artifacts remain server references. The CLI never clears the scene or
46
- takes extra screenshots. Defaults are `gpt-6-astra`, `high`, and
47
- `--api http://127.0.0.1:8787/agent`; unsupported settings are rejected.
48
-
49
- Progress goes to stderr; one JSON result goes to stdout. Use `yarn --silent agent-run`
50
- or the installed `agent-run` executable when parsing stdout (Yarn otherwise adds banners).
51
- Usage comes from the provider, with per-step USD estimates; missing usage is not estimated.
52
- Exit codes: 0 completed, 1 run/transport/export failure, 2 invalid arguments,
53
- 3 requires user action (continue in the UI), 130 interrupted. Interrupts attempt to cancel
54
- only this invocation's run; a failed cancellation is reported and the run may still be active.
55
- Output directories must not already exist. Artifact manifests include IDs, media types,
56
- server URLs and relative paths; failed downloads retain their references and report errors.
57
- Legacy inline base64 uploads in transcripts are replaced with `[inline data omitted]`.
58
-
59
- ## Package entry points
19
+ ## Run a prompt from the command line
20
+
21
+ With the Spark server already running:
22
+
23
+ ```sh
24
+ yarn agent-run --prompt "Make a cube" --model gpt-5.6-luna --reasoning max
25
+ ```
26
+
27
+ Use `--conversation <id>` to continue a thread and `--output <new-directory>` to save
28
+ `result.json`, `transcript.json` and original run artifacts under `artifacts/`.
29
+ For local runs, use the git-ignored `.spark/` directory, with a fresh subdirectory per run:
30
+
31
+ ```sh
32
+ yarn agent-run --prompt "Make rainbow-colored boxes" --output .spark/agent-runs/rainbow-boxes
33
+ yarn agent-run --prompt "Model this object using both views" --image front.png --image side.jpg --output .spark/agent-runs/reference-model
34
+ ```
35
+
36
+ Repeat `--image <path>` for local PNG, JPEG, WebP or GIF references, in argument order.
37
+ The required prompt and all images are sent in one user message. Files, format signatures,
38
+ and the full JSON request size (including base64 overhead) are checked before contacting the server.
39
+ The CLI uses the server's default 25 MiB request limit; a custom server or provider may impose
40
+ stricter limits. Images are never resized, and remote URLs are not accepted.
41
+ With `--output`, original bytes are saved under `inputs/`; `input.json` and the result's `input`
42
+ field list ordered original filenames, MIME types and relative paths. The submitted message in
43
+ `transcript.json` uses those paths instead of inline data. Generated artifacts stay in `artifacts/`.
44
+
45
+ Without output, artifacts remain server references. The CLI never clears the scene or
46
+ takes extra screenshots. Defaults are `gpt-6-astra`, `high`, and
47
+ `--api http://127.0.0.1:8787/agent`; unsupported settings are rejected.
48
+
49
+ Progress goes to stderr; one JSON result goes to stdout. Use `yarn --silent agent-run`
50
+ or the installed `agent-run` executable when parsing stdout (Yarn otherwise adds banners).
51
+ Usage comes from the provider, with per-step USD estimates; missing usage is not estimated.
52
+ Exit codes: 0 completed, 1 run/transport/export failure, 2 invalid arguments,
53
+ 3 requires user action (continue in the UI), 130 interrupted. Interrupts attempt to cancel
54
+ only this invocation's run; a failed cancellation is reported and the run may still be active.
55
+ Output directories must not already exist. Artifact manifests include IDs, media types,
56
+ server URLs and relative paths; failed downloads retain their references and report errors.
57
+ Legacy inline base64 uploads in transcripts are replaced with `[inline data omitted]`.
58
+
59
+ ## Package entry points
60
60
 
61
61
  - `@womp/spark-agent` — browser-safe model loop, two tools, persistence hooks, and shared contracts
62
62
  - `@womp/spark-agent/browser` — native JavaScript executor and local chat transport
@@ -100,9 +100,9 @@ the terminal checkpoint. A host run owner must keep consuming when a view detach
100
100
  A failed barrier aborts the loop. The default model-step limit is 100; a limit returns
101
101
  `status: "incomplete"`. A host must not automatically restart to bypass that limit.
102
102
 
103
- Spark does not estimate or limit input tokens or automatically shorten conversation history.
104
- The provider enforces its context limit; rejections follow the normal provider-error path.
105
- Provider-reported usage and cost tracking are unchanged.
103
+ Spark does not estimate or limit input tokens or automatically shorten conversation history.
104
+ The provider enforces its context limit; rejections follow the normal provider-error path.
105
+ Provider-reported usage and cost tracking are unchanged.
106
106
 
107
107
  Screenshot and file APIs return serializable artifact references. `emitImage` and `emitFile` resolve
108
108
  them through the host's artifact resolver. Keep references resolvable when reloading history, and
@@ -160,23 +160,23 @@ Closing or reloading the page only detaches the SSE subscriber. It does not canc
160
160
  `SparkChatTransport` hides the durable run lifecycle from `useChat`:
161
161
 
162
162
  ```text
163
- POST /agent/conversations/:conversationId/messages
164
- PATCH /agent/conversations/:conversationId/settings
163
+ POST /agent/conversations/:conversationId/messages
164
+ PATCH /agent/conversations/:conversationId/settings
165
165
  GET /agent/runs/:runId/stream?after=0
166
166
  POST /agent/runs/:runId/cancel
167
167
  ```
168
168
 
169
- Conversation settings contain `modelId` and `reasoningEffort`. The standalone UI saves selector
170
- changes immediately through `transport.saveConversationSettings(id, settings)` and restores them
171
- per thread. Older threads fall back to their last recorded model (or the default) and medium effort;
172
- unsupported choices fall back to supported settings. Assistant footers show the model recorded on
173
- that turn, independently of the selector.
174
-
175
- Custom stores must implement `saveConversationSettings` as a settings-only update, and preserve
176
- stored settings during `saveConversation` so streaming history saves cannot overwrite preferences.
177
- The built-in memory and SQLite stores do this without a database migration.
178
-
179
- The SSE response contains ordered persistence envelopes around standard AI SDK chunks:
169
+ Conversation settings contain `modelId` and `reasoningEffort`. The standalone UI saves selector
170
+ changes immediately through `transport.saveConversationSettings(id, settings)` and restores them
171
+ per thread. Older threads fall back to their last recorded model (or the default) and medium effort;
172
+ unsupported choices fall back to supported settings. Assistant footers show the model recorded on
173
+ that turn, independently of the selector.
174
+
175
+ Custom stores must implement `saveConversationSettings` as a settings-only update, and preserve
176
+ stored settings during `saveConversation` so streaming history saves cannot overwrite preferences.
177
+ The built-in memory and SQLite stores do this without a database migration.
178
+
179
+ The SSE response contains ordered persistence envelopes around standard AI SDK chunks:
180
180
 
181
181
  ```ts
182
182
  type StoredSparkChunk = {
@@ -364,6 +364,14 @@ Releases use a manual package publication; no separate release service is requir
364
364
  - Custom JavaScript executors must honor cancellation and host-owned user waits. Callback-based
365
365
  scene edits and artifact references work with both the native and QuickJS executors.
366
366
 
367
+ ### 4.1.0
368
+
369
+ - `emitImage(artifact, { inspect: false })` displays an image without adding its bytes to model input, including restored history. Inspection remains the default.
370
+ - Generation instructions use completed outputs directly and avoid redundant listing or emission; explicit inspection and follow-up imports remain supported.
371
+ - Streamed tool labels are emitted only after their label field is complete. Hosts can consume `data-tool-input-label` while the rest of the input streams.
372
+ - Server cancellation waits for active execution to settle before reporting its terminal state.
373
+ - Includes the latest geometry material, scalar-port, and primitive improvements from main.
374
+
367
375
  ### Publishing
368
376
 
369
377
  1. Choose an unused package version after checking `npm view @womp/spark-agent versions --json`.
@@ -1,5 +1,6 @@
1
1
  import { tool } from "ai";
2
2
  import { z } from "zod";
3
+ import { completedInputString } from "./tool-input-label.js";
3
4
  import { UserWait } from "./execution-lifetime.js";
4
5
  function errorMessage(error) {
5
6
  return error instanceof Error ? error.message : String(error);
@@ -13,6 +14,7 @@ function jsonValue(value) {
13
14
  }
14
15
  return JSON.parse(json);
15
16
  }
17
+ const imageOptionsSchema = z.object({ inspect: z.boolean().optional() }).strict();
16
18
  const artifactRefSchema = z.object({
17
19
  id: z.string().min(1),
18
20
  mediaType: z.string().min(1),
@@ -42,8 +44,31 @@ export function createSparkTools(options) {
42
44
  controller.signal.throwIfAborted();
43
45
  return output;
44
46
  };
47
+ const inputLabels = (field) => {
48
+ const inputs = new Map();
49
+ return {
50
+ onInputStart: ({ toolCallId }) => { inputs.set(toolCallId, ""); },
51
+ onInputDelta: ({ toolCallId, inputTextDelta }) => {
52
+ const previous = inputs.get(toolCallId);
53
+ if (previous === undefined)
54
+ return;
55
+ const text = previous + inputTextDelta;
56
+ const value = completedInputString(text, field);
57
+ if (value === undefined) {
58
+ inputs.set(toolCallId, text);
59
+ return;
60
+ }
61
+ inputs.delete(toolCallId);
62
+ if (!value.trim() || value.length > (field === "name" ? 160 : 240))
63
+ return;
64
+ options.write({ type: "data-tool-input-label", id: `tool-label-${toolCallId}`, data: { toolCallId, field, value } });
65
+ },
66
+ onInputAvailable: ({ toolCallId }) => { inputs.delete(toolCallId); },
67
+ };
68
+ };
45
69
  return {
46
70
  read_skill: tool({
71
+ ...inputLabels("name"),
47
72
  description: "Read a skill from the available directory, including instructions, TypeScript API declarations, and JavaScript examples.",
48
73
  inputSchema: z.object({ name: z.string().min(1).max(160) }),
49
74
  execute: (input, { toolCallId }) => execute({ id: toolCallId, name: "read_skill", input }, async () => {
@@ -59,9 +84,10 @@ export function createSparkTools(options) {
59
84
  }),
60
85
  }),
61
86
  execute_javascript: tool({
87
+ ...inputLabels("purpose"),
62
88
  description: `Run an async JavaScript body against the APIs discovered through skills. Use print, emitImage, and emitFile for output. Each call has a fresh local scope; capabilities own their commit boundaries. Execution budget: ${executionTimeoutMs} ms per call, excluding registered user/service waits (not all awaits).`,
63
89
  inputSchema: z.object({
64
- purpose: z.string().min(1).max(240),
90
+ purpose: z.string().min(1).max(240).describe('A concise present-progressive description, e.g. "Generating a car image", "Inspecting the scene", or "Importing the model". Write purpose before code.'),
65
91
  code: z.string().min(1).max(60_000),
66
92
  }),
67
93
  execute: (input, { toolCallId }) => execute({ id: toolCallId, name: "execute_javascript", input }, async () => {
@@ -134,7 +160,7 @@ export function createSparkTools(options) {
134
160
  });
135
161
  },
136
162
  };
137
- const emit = (reference, kind) => track((async () => {
163
+ const emit = (reference, kind, inspect = true) => track((async () => {
138
164
  signal.throwIfAborted();
139
165
  const ref = artifactRefSchema.parse(reference);
140
166
  const { artifact } = await options.artifacts.resolve(ref);
@@ -142,8 +168,10 @@ export function createSparkTools(options) {
142
168
  if (kind === "image" && artifact.mediaType.startsWith("image/") === false) {
143
169
  throw new Error(`Artifact ${ref.id} is not an image.`);
144
170
  }
145
- artifacts.push({ kind, artifact });
146
- options.write({ type: "file", url: artifact.url, mediaType: artifact.mediaType });
171
+ artifacts.push({ kind, artifact, ...(kind === "image" && !inspect ? { inspect: false } : {}) });
172
+ options.write({ type: "file", url: artifact.url, mediaType: artifact.mediaType,
173
+ ...(kind === "image" && !inspect ? { providerMetadata: { spark: { inspect: false } } } : {}),
174
+ });
147
175
  })());
148
176
  try {
149
177
  const value = await options.executor.execute(input.code, {
@@ -159,7 +187,10 @@ export function createSparkTools(options) {
159
187
  logs.push(text);
160
188
  options.write({ type: "data-console", data: { stream: "stdout", text } });
161
189
  },
162
- emitImage: (ref) => emit(ref, "image"),
190
+ emitImage: (ref, settings) => {
191
+ const parsed = imageOptionsSchema.parse(settings ?? {});
192
+ return emit(ref, "image", parsed.inspect ?? true);
193
+ },
163
194
  emitFile: (ref) => emit(ref, "file"),
164
195
  }, executionTimeoutMs, signal, executionWait);
165
196
  await Promise.all(pending);
@@ -194,7 +225,7 @@ export function createSparkTools(options) {
194
225
  }),
195
226
  toModelOutput: async ({ output }) => {
196
227
  const images = await Promise.all(output.artifacts
197
- .filter(({ kind }) => kind === "image")
228
+ .filter(({ kind, inspect }) => kind === "image" && inspect !== false)
198
229
  .map(async ({ artifact }) => {
199
230
  const resolved = await options.artifacts.resolve(artifact);
200
231
  return {
@@ -204,9 +235,20 @@ export function createSparkTools(options) {
204
235
  filename: resolved.artifact.name,
205
236
  };
206
237
  }));
238
+ const modelOutput = {
239
+ ...output,
240
+ artifacts: output.artifacts.map((entry) => {
241
+ if (entry.kind !== "image" || entry.inspect !== false)
242
+ return entry;
243
+ const { id, mediaType, name } = entry.artifact;
244
+ // Display URLs can themselves contain bytes (data URLs). Keep only
245
+ // a reusable reference in model input; the UI retains the original.
246
+ return { ...entry, artifact: { id, mediaType, ...(name ? { name } : {}) } };
247
+ }),
248
+ };
207
249
  return {
208
250
  type: "content",
209
- value: [{ type: "text", text: JSON.stringify(output) }, ...images],
251
+ value: [{ type: "text", text: JSON.stringify(modelOutput) }, ...images],
210
252
  };
211
253
  },
212
254
  }),
@@ -13,30 +13,33 @@ export function createGeometryCapability(host, reference) {
13
13
  skills: [
14
14
  {
15
15
  name: "geometry-nodes",
16
- version: "3",
17
- description: "Build visual Geometry Nodes graphs, parametric organic forms, smooth blends, and parametric animation; edit existing graph objects.",
18
- instructions: `Use womp.geometry for procedural Geometry Nodes, not Workbench Flow graphs.
19
- Choose this skill for explicit geometry-node editing, parametric organic or smoothly blended forms, and parametric animation. Mechanical, measured, hard-surface parts belong to parametric-parts when available unless the user requests a node graph.
20
- Keep follow-up edits to an existing Geometry Nodes object in its graph, preserving its adjustable parameters and instances.
21
- Read the target first. read(0) reads the scene root as a creation container.
22
- write updates an existing shared graph, attaches a new graph to an Area without one, or creates a new Area beneath the root/Group target. Use the returned objectId for follow-up edits.
23
- Read geometry-nodes-reference before writing source. Supply the complete FlowIR text and the exact revision returned by the latest read. JavaScript calls execute JavaScript; source is a FlowIR string, not JavaScript or JSON.
24
- After a stale revision rejection, read the latest graph and adapt the edit while preserving intervening user changes. Ask only when the edits conflict or remain ambiguous; never retry the old replacement with a refreshed revision token.
25
- If source is null and diagnostics are present, the existing graph cannot be edited without loss. Explain the limitation; never treat it as empty or create a replacement to bypass the refusal.
26
- Preserve unrelated statements, parameter names, and FlowIR node IDs from the read. Shared graph edits affect all listed instances; report the affected objects. Valid overrides migrate by parameter name and type. Removed or type-changed parameters return to graph defaults and are reported.
27
- Use min/max/step for naturally bounded parameters and omit ranges for free-form positions/rotations. Meaningful parameter names become editor labels. Do not invent label fields.
28
- Positions and sizes are millimetres; Boolean Blend uses the conversion in geometry-nodes-reference. Coordinates are right-handed, +X right, +Y up, +Z forward. Rotations are degrees, intrinsic X-Z-Y.
29
- On compiler errors, repair the source in this same conversation and retry with a fresh read. A rejected write made no change. Committed/unknown effects remain in execution receipts even if a later call fails; inspect before retrying.
30
- After applying, read the returned object and inspect a screenshot through scene-verification when available.`,
16
+ version: "5",
17
+ description: "Create and edit visual Geometry Nodes graphs with adjustable parameters and animation.",
18
+ instructions: `Use womp.geometry for requested Geometry Nodes or parametric geometry workflows, not Workbench Flow graphs.
19
+ Keep follow-up edits to an existing Geometry Nodes object in its graph, preserving its adjustable parameters and instances.
20
+ Read the target after any scene edit this build needs. read(0) reads the scene root as a creation container.
21
+ write updates an existing shared graph, attaches a new graph to an Area without one, or creates a new Area beneath the root/Group target. Use the returned objectId for follow-up edits.
22
+ Read geometry-nodes-reference before writing source. Supply the complete FlowIR text and the exact revision returned by the latest read. JavaScript calls execute JavaScript; source is a FlowIR string, not JavaScript or JSON.
23
+ After a stale revision rejection, read the latest graph and adapt the edit while preserving intervening user changes. Ask only when the edits conflict or remain ambiguous; never retry the old replacement with a refreshed revision token.
24
+ If source is null and diagnostics are present, the existing graph cannot be edited without loss. Explain the limitation; never treat it as empty or create a replacement to bypass the refusal.
25
+ Preserve unrelated statements, parameter names, and FlowIR node IDs from the read. Shared graph edits affect all listed instances; report the affected objects. Valid overrides migrate by parameter name and type. Removed or type-changed parameters return to graph defaults and are reported.
26
+ Use min/max/step for naturally bounded parameters and omit ranges for free-form positions/rotations. Meaningful parameter names become editor labels. Do not invent label fields.
27
+ SetMaterial assigns one scene material to every primitive it receives. Its Material input is a scene material ID, not a color; 0 selects the scene default material.
28
+ Obtain that ID from the scene-editing skill inside one scene.edit, where those lookups live: materialList with materialGetName and materialGetColor to reuse an existing material, materialCreate with materialSetColor and materialSetName for a new one, or listSuperMaterials with materialImport for a library material. Prefer reusing a material that already matches the request over creating another, and do not recolor a shared material unless every object using it should change.
29
+ A scene edit changes the scene revision, so create or import the material before reading the geometry target, then write, all within one JavaScript call. Each call has a fresh scope and cannot read an earlier call's variables.
30
+ Positions and sizes are millimetres; Boolean Blend uses the conversion in geometry-nodes-reference. Coordinates are right-handed, +X right, +Y up, +Z forward. Rotations are degrees, intrinsic X-Z-Y.
31
+ On compiler errors, repair the source in this same conversation and retry with a fresh read. A rejected write made no change. Committed/unknown effects remain in execution receipts even if a later call fails; inspect before retrying.
32
+ After applying, read the returned object and use the scene screenshot API with emitImage to inspect the applied result.`,
31
33
  declarations: WOMP_GEOMETRY_DECLARATIONS,
32
34
  examples: [
33
35
  "const target = await womp.geometry.read(objectId); print(target);",
34
36
  "const target = await womp.geometry.read(objectId); print(await womp.geometry.write({ objectId: target.objectId, source: editedFlowIR, expectedRevision: target.revision }));",
37
+ "const materialId = await womp.scene.edit(scene => { const id = scene.materialCreate(); scene.materialSetName(id, 'Red'); scene.materialSetColor(id, { r: 255, g: 0, b: 0 }); return id; }, (await womp.scene.read()).revision); const target = await womp.geometry.read(0); const source = ['param Float size = 20.0 min=1 max=100 step=1', 'node box Primitive Type=Cube Position=Vec3(0.0,0.0,0.0) Rotation=Vec3(0.0,0.0,0.0) Size=size Roundness=0.0', `node painted SetMaterial Material=${materialId} Geometry=box.Geometry`, 'out Geometry=painted.Geometry'].join('\\n'); print(await womp.geometry.write({ objectId: target.objectId, source, expectedRevision: target.revision }));",
35
38
  ],
36
39
  },
37
40
  {
38
41
  name: "geometry-nodes-reference",
39
- version: "1",
42
+ version: "2",
40
43
  description: "FlowIR syntax, types, enums, and supported Geometry Nodes catalog; read before writing a graph.",
41
44
  instructions: `${reference.rules}\n${reference.catalog}`,
42
45
  declarations: "",
@@ -1 +1 @@
1
- export declare const OPENSCAD_INSTRUCTIONS = "Use womp.openscad to create or update one adjustable part. Read an existing part before editing it.\nChoose CAD for mechanical, measured, and hard-surface parametric parts even without explicit dimensions.\nFor an explicit visual node graph, parametric organic or smoothly blended forms, or parametric animation,\nuse geometry-nodes when available. Do not force organic sculpture into CAD or silently change the requested editing surface.\nread(0) returns the root container and a scene revision for creation. read(id) returns an existing\npart or container. Use the scene-editing skill when available to discover other scene objects.\ncreate requires a parentId (0 means the scene root); update requires the existing objectId.\nBoth methods compile the effective source and parameters, apply one undoable change, and check wall thickness.\nA stale revision requires reading again and adapting the edit while preserving intervening user changes.\nAsk the user only when the intended edits conflict or remain ambiguous; never reuse the old replacement\nwith a refreshed revision token. Existing overrides are preserved for parameters still present;\nparams supplies overrides to merge into the current values. Keep top-level parameter names stable.\nReturn complete source on every update and preserve unrelated geometry. Never create a replacement to repair a part.\nCompile errors return status rejected without applying. Creation requires validated placement bounds.\nAn update may apply with validation unavailable; that is a diagnostic, not proof that it compiles.\nAn applied part remains in the scene even if thickness fails or is unavailable. Use the returned objectId for repairs.\nWhen thickness fails, increase thin walls and fragile connections while preserving requested dimensions and openings.\nAt most four applied printability repairs are allowed per logical build/edit in this run. Respect the returned limit;\ndo not bypass it by recreating the part. Unavailable thickness is not a pass. Report unresolved issues honestly.\nBuild connected geometry and avoid features below 1.4 mm. OpenSCAD source dimensions use millimetres,\nindependently of the scene display unit. New parts are centered and grounded from compiled bounds.\nOpenSCAD is Z-up; the scene-editing API is Y-up. For an unrotated part at its creation-time scale and an identity parent transform,\na source point [x,y,z] mm maps to scene {x:y, y:z, z:-x} times the unitMultiplier returned by scene.read(), plus nodeGetPos(partId).\nAccount for the part and parent transforms when they are not identity; source coordinates are not scene coordinates.\nAfter a major change, use the scene screenshot API and emitImage to inspect the applied result.\nA committed effect can survive later failure. Inspect receipts and the existing part before retrying.\nDo not print source in the final answer unless requested; describe the result and any unresolved diagnostics.\n\nRUNTIME\n- Produce a single OpenSCAD file.\n- Do not use imports, includes, external libraries, text, hull, minkowski, polyhedron, or extrudes.\n- Prefer cube, sphere, cylinder, translate, rotate, color, union, difference, and intersection.\n- Do not use $fn, $fa, or $fs; tessellation controls are ignored.\n- Build faceted geometry by combining supported primitives with CSG operations and transforms.\n- Do not use scale(); size primitives directly instead.\n- Use color(\"#rrggbb\") around distinct visible parts.\n- Keep the model centered near the origin and use millimeter-like units consistently.\n- For organic blending, use goop(radius_mm) { ... }. The radius is in millimetres before parent scaling, matching scene-editing; do not multiply by project unitMultiplier. It is ordered: the first child seeds with blend 0, and each later child uses radius_mm.\n- Put core primitives first. Avoid wrapping goop children in union() when they should blend individually.\n\nSCREWS AND THREADS\n- Copy helical_ridge below for rounded screw threads, necks, caps, and coils. Like geometry-node curves, it joins spheres with cylinders; never use stacked rings or disconnected spheres.\n- Dimensions are mm: radius = ridge centerline radius; pitch = axial travel/turn; tube_radius = half ridge thickness; handedness = 1 (right) or -1 (left).\n- For a hand control use thread_hand = 1; // [-1:Left, 1:Right] and pass it directly. Ternary expressions (?:) are unsupported.\n- Embed the ridge in the shaft/neck (e.g. radius = shaft_radius). Hollow fittings: subtract the bore from body + thread. Internal threads: subtract the helix from the socket wall with mating clearance. Mating parts need matching pitch and handedness.\n- Expose diameter, pitch, thread length, and thickness in Customizer; derive sampling. Keep 2 * tube_radius < pitch and allow tube_radius beyond both path endpoints for rounded ends. This is a rounded profile, not ISO/metric V-thread; never claim standards compliance.\n- Build heads/grips separately using cylinders, tapered cylinders, and radial ribs/cutters, not thread samples.\n\nmodule helical_ridge(radius, pitch, turns, tube_radius, handedness) {\n if (radius > 0 && pitch > 0 && turns > 0 && tube_radius > 0) {\n // Limit chord error to 0.03 mm or 3% of the ridge radius.\n chord_error = min(0.03, tube_radius * 0.03);\n steps_per_turn = max(48, ceil(180 / acos(1 - min(1, chord_error / radius))));\n steps = ceil(turns * steps_per_turn);\n for (i = [0:steps]) {\n angle = handedness * 360 * turns * i / steps;\n p = [radius * cos(angle), radius * sin(angle), pitch * turns * i / steps];\n translate(p) sphere(r = tube_radius);\n if (i < steps) {\n next_angle = handedness * 360 * turns * (i + 1) / steps;\n dx = radius * cos(next_angle) - p[0];\n dy = radius * sin(next_angle) - p[1];\n dz = pitch * turns / steps;\n segment_length = sqrt(dx * dx + dy * dy + dz * dz);\n translate(p)\n rotate([0, acos(dz / segment_length), atan2(dy, dx)])\n cylinder(h = segment_length, r = tube_radius);\n }\n }\n }\n}\n\nPARAMETERS\n- Make important top-level constants user-adjustable with OpenSCAD Customizer comments:\n - Numbers with ranges: body_radius = 12; // [4:0.5:40]\n - Counts: leg_count = 4; // [2:8]\n - Booleans: show_base = true;\n - Options: style = \"round\"; // [round, sharp, chunky]\n- Prefer stable size/count/ratio/style controls; keep fragile dependent dimensions as local expressions.\n\nSUPPORTED SUBSET\n- Export is best-effort from a supported CSG subset: cube, sphere, cylinder, translate, rotate, color, union, difference, intersection, and goop.\n- difference() cutters become subtractive primitives, so use clean cutters that fully pass through the target solid.\n- Unsupported OpenSCAD may validate but can warn or skip during export; prefer the supported subset above.";
1
+ export declare const OPENSCAD_INSTRUCTIONS = "Use womp.openscad to create or update one adjustable CAD part. Read an existing part before editing it.\nread(0) returns the root container and a scene revision for creation. read(id) returns an existing\npart or container. Use the scene-editing skill when available to discover other scene objects.\ncreate requires a parentId (0 means the scene root); update requires the existing objectId.\nBoth methods compile the effective source and parameters, apply one undoable change, and check wall thickness.\nA stale revision requires reading again and adapting the edit while preserving intervening user changes.\nAsk the user only when the intended edits conflict or remain ambiguous; never reuse the old replacement\nwith a refreshed revision token. Existing overrides are preserved for parameters still present;\nparams supplies overrides to merge into the current values. Keep top-level parameter names stable.\nReturn complete source on every update and preserve unrelated geometry. Never create a replacement to repair a part.\nCompile errors return status rejected without applying. Creation requires validated placement bounds.\nAn update may apply with validation unavailable; that is a diagnostic, not proof that it compiles.\nAn applied part remains in the scene even if thickness fails or is unavailable. Use the returned objectId for repairs.\nWhen thickness fails, increase thin walls and fragile connections while preserving requested dimensions and openings.\nAt most four applied printability repairs are allowed per logical build/edit in this run. Respect the returned limit;\ndo not bypass it by recreating the part. Unavailable thickness is not a pass. Report unresolved issues honestly.\nBuild connected geometry and avoid features below 1.4 mm. OpenSCAD source dimensions use millimetres,\nindependently of the scene display unit. New parts are centered and grounded from compiled bounds.\nOpenSCAD is Z-up; the scene-editing API is Y-up. For an unrotated part at its creation-time scale and an identity parent transform,\na source point [x,y,z] mm maps to scene {x:y, y:z, z:-x} times the unitMultiplier returned by scene.read(), plus nodeGetPos(partId).\nAccount for the part and parent transforms when they are not identity; source coordinates are not scene coordinates.\nAfter a major change, use the scene screenshot API and emitImage to inspect the applied result.\nA committed effect can survive later failure. Inspect receipts and the existing part before retrying.\nDo not print source in the final answer unless requested; describe the result and any unresolved diagnostics.\n\nRUNTIME\n- Produce a single OpenSCAD file.\n- Do not use imports, includes, external libraries, text, hull, minkowski, polyhedron, or extrudes.\n- Prefer cube, sphere, cylinder, translate, rotate, color, union, difference, and intersection.\n- Do not use $fn, $fa, or $fs; tessellation controls are ignored.\n- Build faceted geometry by combining supported primitives with CSG operations and transforms.\n- Do not use scale(); size primitives directly instead.\n- Use color(\"#rrggbb\") around distinct visible parts.\n- Keep the model centered near the origin and use millimeter-like units consistently.\n- For organic blending, use goop(radius_mm) { ... }. The radius is in millimetres before parent scaling, matching scene-editing; do not multiply by project unitMultiplier. It is ordered: the first child seeds with blend 0, and each later child uses radius_mm.\n- Put core primitives first. Avoid wrapping goop children in union() when they should blend individually.\n\nSCREWS AND THREADS\n- Copy helical_ridge below for rounded screw threads, necks, caps, and coils. Like geometry-node curves, it joins spheres with cylinders; never use stacked rings or disconnected spheres.\n- Dimensions are mm: radius = ridge centerline radius; pitch = axial travel/turn; tube_radius = half ridge thickness; handedness = 1 (right) or -1 (left).\n- For a hand control use thread_hand = 1; // [-1:Left, 1:Right] and pass it directly. Ternary expressions (?:) are unsupported.\n- Embed the ridge in the shaft/neck (e.g. radius = shaft_radius). Hollow fittings: subtract the bore from body + thread. Internal threads: subtract the helix from the socket wall with mating clearance. Mating parts need matching pitch and handedness.\n- Expose diameter, pitch, thread length, and thickness in Customizer; derive sampling. Keep 2 * tube_radius < pitch and allow tube_radius beyond both path endpoints for rounded ends. This is a rounded profile, not ISO/metric V-thread; never claim standards compliance.\n- Build heads/grips separately using cylinders, tapered cylinders, and radial ribs/cutters, not thread samples.\n\nmodule helical_ridge(radius, pitch, turns, tube_radius, handedness) {\n if (radius > 0 && pitch > 0 && turns > 0 && tube_radius > 0) {\n // Limit chord error to 0.03 mm or 3% of the ridge radius.\n chord_error = min(0.03, tube_radius * 0.03);\n steps_per_turn = max(48, ceil(180 / acos(1 - min(1, chord_error / radius))));\n steps = ceil(turns * steps_per_turn);\n for (i = [0:steps]) {\n angle = handedness * 360 * turns * i / steps;\n p = [radius * cos(angle), radius * sin(angle), pitch * turns * i / steps];\n translate(p) sphere(r = tube_radius);\n if (i < steps) {\n next_angle = handedness * 360 * turns * (i + 1) / steps;\n dx = radius * cos(next_angle) - p[0];\n dy = radius * sin(next_angle) - p[1];\n dz = pitch * turns / steps;\n segment_length = sqrt(dx * dx + dy * dy + dz * dz);\n translate(p)\n rotate([0, acos(dz / segment_length), atan2(dy, dx)])\n cylinder(h = segment_length, r = tube_radius);\n }\n }\n }\n}\n\nPARAMETERS\n- Make important top-level constants user-adjustable with OpenSCAD Customizer comments:\n - Numbers with ranges: body_radius = 12; // [4:0.5:40]\n - Counts: leg_count = 4; // [2:8]\n - Booleans: show_base = true;\n - Options: style = \"round\"; // [round, sharp, chunky]\n- Prefer stable size/count/ratio/style controls; keep fragile dependent dimensions as local expressions.\n\nSUPPORTED SUBSET\n- Export is best-effort from a supported CSG subset: cube, sphere, cylinder, translate, rotate, color, union, difference, intersection, and goop.\n- difference() cutters become subtractive primitives, so use clean cutters that fully pass through the target solid.\n- Unsupported OpenSCAD may validate but can warn or skip during export; prefer the supported subset above.";
@@ -1,7 +1,4 @@
1
- export const OPENSCAD_INSTRUCTIONS = `Use womp.openscad to create or update one adjustable part. Read an existing part before editing it.
2
- Choose CAD for mechanical, measured, and hard-surface parametric parts even without explicit dimensions.
3
- For an explicit visual node graph, parametric organic or smoothly blended forms, or parametric animation,
4
- use geometry-nodes when available. Do not force organic sculpture into CAD or silently change the requested editing surface.
1
+ export const OPENSCAD_INSTRUCTIONS = `Use womp.openscad to create or update one adjustable CAD part. Read an existing part before editing it.
5
2
  read(0) returns the root container and a scene revision for creation. read(id) returns an existing
6
3
  part or container. Use the scene-editing skill when available to discover other scene objects.
7
4
  create requires a parentId (0 means the scene root); update requires the existing objectId.
@@ -15,8 +15,8 @@ export function createOpenScadCapability(host) {
15
15
  skills: [
16
16
  {
17
17
  name: "parametric-parts",
18
- version: "3",
19
- description: "Create and edit adjustable mechanical, measured, and hard-surface OpenSCAD parts; compile them and check wall thickness.",
18
+ version: "4",
19
+ description: "Create and edit adjustable OpenSCAD parts; compile them and check wall thickness.",
20
20
  instructions: OPENSCAD_INSTRUCTIONS,
21
21
  declarations: WOMP_OPENSCAD_DECLARATIONS,
22
22
  examples: [
@@ -77,7 +77,7 @@ export function createOperationsCapability(host) {
77
77
  skills: [
78
78
  {
79
79
  name: "generation",
80
- version: "5",
80
+ version: "7",
81
81
  description: "Generate or edit images and meshes, inspect jobs, and wait for their outputs.",
82
82
  instructions: `Read operations.catalog() for the currently available models and selected defaults.
83
83
  Use generation for inline chat results. If the user is working in their Flow graph and flow-graphs is available,
@@ -100,18 +100,22 @@ different requests. Host receipts and saved message IDs record submitted effects
100
100
  generate() waits for cost confirmation and then for completion, failure, or cancellation.
101
101
  Use exactly one generation per execute_javascript call and print its result. Put follow-up work in
102
102
  another call. Do not poll, start background work, use Promise.all, or report success before completion.
103
- The host waits without consuming model steps. Pause ends the local wait, not an accepted backend job.
103
+ The host waits without consuming model steps. Stop ends agent execution; the host handles cancellation of its accepted jobs.
104
104
  Refresh recovery reconnects the same job. It never resumes the interrupted JavaScript stack.
105
- Inspect a recovered result and continue remaining work in a new call; do not repeat generation.
106
- Use list() to discover existing jobs. Follow nextFrom even when a page has no jobs.
107
- An uncertain submission requires inspection, never a replacement request.
108
- Only returned outputs exist. Emit images/files to inspect or show them; generating a mesh does not
109
- import it into the scene. Use the assets skill for importing. Stopping the agent ends its wait and
110
- prevents further submissions; it does not claim to cancel a job already accepted by the service.`,
105
+ Read the recovered status and artifact references, then continue any requested remaining work in a new call; do not repeat generation.
106
+ generate() returns the terminal status and job.outputs artifact references directly, including after recovery.
107
+ Completed generations are shown automatically to the user; image bytes are intentionally omitted from your context. Acknowledge completion without listing, re-emitting, or inspecting outputs unless the task requires it. Use returned artifact references for follow-up work.
108
+ For a simple generation request, finish with a brief acknowledgement of the completed status.
109
+ Inspect images deliberately when needed for verification or reference comparisons; never claim visual verification without inspecting the image.
110
+ Omitted image bytes are expected; missing artifact references are not. If a completed job has no outputs, recover the references or report the missing result.
111
+ Reserve list() for older jobs or missing references. Follow nextFrom even when a page has no jobs.
112
+ Verify an uncertain submission's job status, never issue a replacement request.
113
+ Only returned outputs exist. Generating a mesh does not import it into the scene. Use the assets skill for requested imports.
114
+ Never resume stopped work or resubmit a job because cancellation or submission is uncertain.`,
111
115
  declarations: WOMP_OPERATIONS_DECLARATIONS,
112
116
  examples: [
113
117
  "print(await womp.operations.catalog());",
114
- 'const result = await womp.operations.generate({ key: "blue-chair-preview", type: "text-to-image", prompt: "A blue chair on a plain background" }); print(result);',
118
+ 'const result = await womp.operations.generate({ key: "blue-chair-preview", type: "text-to-image", prompt: "A blue chair on a plain background" }); print(result); // If completed with outputs, briefly acknowledge; no listing or emission is needed.',
115
119
  ],
116
120
  },
117
121
  ],
@@ -16,15 +16,15 @@ export function createSceneCapability(host) {
16
16
  skills: [
17
17
  {
18
18
  name: "scene-editing",
19
- version: "10",
19
+ version: "25",
20
20
  description: "Inspect the open scene and edit its objects, materials, curves, and environment.",
21
- instructions: `Read the scene first and keep its revision and object IDs. read() includes health.
22
- Use womp.scene.health() to refresh whole-scene health or womp.scene.health(objectIds) to check specific objects (mapped to their owning Areas).
23
- Health is a separate snapshot, not proof of consistency with read().revision or that the latest edit finished evaluating. Inspect post-edit health in receipts before claiming success; refresh only when needed.
24
- Unavailable, stale, or unverified feedback must not trigger blind regeneration or a claim that the model passed. Hosts without health services return unverified health with a reason.
25
- For confirmed primitive or depth overflow, repair the same target while preserving design, dimensions, active parameter overrides, and unrelated objects. At most two geometry repairs are allowed per target in one logical run.
26
- Empty Areas may be pending, intentionally empty, mesh-only, or failed. Overflow flags take precedence over possibly stale counts. max_total_depth is a scene hierarchy limit, not measured Boolean depth.
27
- Call scene.edit(callback, expectedRevision)
21
+ instructions: `Read the scene first and keep its revision and object IDs. read() includes health.
22
+ Use womp.scene.health() to refresh whole-scene health or womp.scene.health(objectIds) to check specific objects (mapped to their owning Areas).
23
+ Health is a separate snapshot, not proof of consistency with read().revision or that the latest edit finished evaluating. Inspect post-edit health in receipts before claiming success; refresh only when needed.
24
+ Unavailable, stale, or unverified feedback must not trigger blind regeneration or a claim that the model passed. Hosts without health services return unverified health with a reason.
25
+ For confirmed primitive or depth overflow, repair the same target while preserving design, dimensions, active parameter overrides, and unrelated objects. At most two geometry repairs are allowed per target in one logical run.
26
+ Empty Areas may be pending, intentionally empty, mesh-only, or failed. Overflow flags take precedence over possibly stale counts. max_total_depth is a scene hierarchy limit, not measured Boolean depth.
27
+ Call scene.edit(callback, expectedRevision)
28
28
  for one coherent undoable change. The callback receives a draft; all node/material/curve functions and enums
29
29
  are properties of that draft. The draft expires when the callback ends. Await asynchronous methods.
30
30
  Do not start unrelated operations from inside an edit. A stale revision requires reading again.
@@ -32,14 +32,16 @@ Preserve the existing scene unless asked to remove or replace content. Use seman
32
32
  known IDs when editing rather than creating duplicates. Distances follow the current project's units
33
33
  (read().unitMultiplier is 1 for millimetres, 0.1 for centimetres, approximately 0.0393701 for inches).
34
34
  Convert a dimension in millimetres to project units by multiplying it by read().unitMultiplier.
35
- Area size is a dimensionless scale; Group size stays fixed at one. Lights must be created at root (parentId 0); their size has x/y dimensions.
36
- Light power must be a finite number in [0, 100].
37
- nodeCreate(Curve, parentId) creates an empty curve with zero blend. curvePointAdd(curveId, point, index?) returns the new index; omit index to append or supply an integer from 0 through point count to insert before that position.
38
- Point defaults: position/rotation/handles zero, profile size {x:1,y:1,z:1} in project units, the curve's material, fixed false, round 0. Pass {} for defaults.
39
- Removing all points is supported. One point renders a single profile primitive, not a path.
40
- nodeSetUnionResolution(unionId, resolution) sets Area/Union resolution, not child-part resolution, and clamps to 0.01–0.65. Material emission ranges from 0 to 1.
41
- Blend and mirror goop are radii in millimetres before parent scaling, regardless of project units. nodeSetBlend clamps to 0–500 mm.
42
- V1/V2 curve transforms are read-only;
35
+ Area size is a dimensionless scale; Group size stays fixed at one. Lights must be created at root (parentId 0); their size has x/y dimensions.
36
+ Light power must be a finite number in [0, 100].
37
+ nodeCreate(Curve, parentId) creates an empty curve with zero blend. curvePointAdd(curveId, point, index?) returns the new index; omit index to append or supply an integer from 0 through point count to insert before that position.
38
+ Point defaults: position/rotation/handles zero, profile size {x:1,y:1,z:1} in project units, the curve's material, fixed false, round 0. Pass {} for defaults.
39
+ Removing all points is supported. One point renders a single profile primitive, not a path.
40
+ Curves render sampled profiles, not a continuous sweep. For smooth curves, start with nodeSetDensity(curveId, 250) and nodeSetBlend(curveId, 3).
41
+ Density increases sampling and primitive cost; the 3 mm blend softens joins, not sample positions. Adjust blend to preserve thin features and the intended silhouette; these settings do not guarantee continuity.
42
+ nodeSetUnionResolution(unionId, resolution) sets Area/Union resolution, not child-part resolution, and clamps to 0.01–0.65; higher values increase detail and voxel cost. Material emission ranges from 0 to 1.
43
+ nodeSetBlend and mirror goop take radii in millimetres before parent scaling, regardless of project units, and clamp to 0–500 mm. This does not apply to the FlowIR Boolean Blend port, which takes native units; geometry-nodes-reference gives its conversion.
44
+ V1/V2 curve transforms are read-only;
43
45
  edit their points instead. Handles and closure are available only on V3 Bezier curves.
44
46
  Typed rotations are right-handed intrinsic XYZ Euler degrees, relative to the parent (R = Rx Ry Rz), including curve points and mirrors.
45
47
  Points and curve mirrors use the owning curve frame; primitive mirrors use the primitive's parent frame.
@@ -49,26 +51,29 @@ Alignment chooses a deterministic roll using Y-up (Z-up fallback when parallel);
49
51
  The SDK handles native XZY conversion; do not swap Euler components or convert again. Geometry Nodes source remains native XZY.
50
52
  Colors use RGB values from 0 to 255. Use the exposed enums instead of guessing values.
51
53
  For visible geometry color, create or clone a material, call materialSetColor, and assign it with nodeSetMaterialId only to
52
- Primitive, Curve, Text, SVG, Mesh, Field, Decal or OpenSCAD nodes. Assigning a curve material recolors its existing points; use curvePointSetMaterialId for per-point overrides afterward.
54
+ Primitive, Curve, Text, SVG, Mesh, Field, Decal or OpenSCAD nodes. Assigning a curve material recolors its existing points; use curvePointSetMaterialId for per-point overrides afterward.
53
55
  Areas/Unions, Groups, Lights and Sockets do not accept direct materials. Color ordinary containers through their supported children.
54
- Geometry Nodes materials belong in the graph: the current FlowIR API does not expose native SetMaterial/SetMaterialIndex.
55
- Report that limitation without replacing a parametric graph with ordinary primitives. Ordinary Stain/CSG children do not modify graph output.
56
+ Geometry Nodes materials belong in the graph: create or import the material here, then assign it inside the graph with the FlowIR SetMaterial node, passing the numeric ID this skill returns.
57
+ Never replace a parametric graph with ordinary primitives to color it. Ordinary Stain/CSG children do not modify graph output.
56
58
  nodeSetColor writes the node color field (used by lights); it does not replace a geometry object's material color.
57
- Do not recolor a shared material unless every object using it should change. Create and assign the material in the same edit as the geometry.
59
+ Do not recolor a shared material unless every object using it should change. Create and assign the material in the same edit as the scene geometry that uses it; a Geometry Nodes graph instead consumes a material ID created in an earlier edit.
58
60
  Create primitives, curves, and text inside Areas/Unions. Respect tree order for subtraction and intersection.
59
61
  Groups are organizational only; move and rotate their child Areas individually, not the Group. Use unions for blended masses.
60
- Sphere uses size.x as uniform diameter (y/z do not stretch it). Cylinder/Cone/Pyramid use x as base diameter/width and y as height;
61
- Torus uses x as outer diameter and y as tube thickness. For these four primitives, nodeSetSize accepts {x, y}; z follows x even when supplied. Cube uses all three dimensions; round controls rounding, not size.
62
+ Sphere uses size.x as uniform diameter (y/z do not stretch it), including new or resized spherical curve points. Cylinder/Cone/Pyramid use x as base diameter/width and y as height;
63
+ Torus uses x as outer diameter and y as tube thickness. For these four primitives, nodeSetSize accepts {x, y}; z follows x even when supplied. Cube uses all three dimensions. Node and curve-point round are dimensionless fractions clamped to [0, 1], not radii in project units; rounding does not set size. Cube rounding reaches half its shortest dimension at 1, so a shallow Cube retains broad flat faces rather than becoming an ellipsoid.
62
64
  Build and verify coarse geometry and unfamiliar operations before replicating details. Check one representative result, then repeat it.
65
+ For reference images, match the camera before adjusting geometry. Compare renders with the source from a consistent view,
66
+ correcting the largest silhouette, proportion, and pose differences before adding details. Return to that view after close-ups
67
+ or depth checks; do not distort geometry to compensate for a camera mismatch.
63
68
  nodeClone is shallow (no children). await cloneSubtree(id, { parentId?, name?, index? }) copies the complete hierarchy with fresh IDs,
64
69
  preserving local transforms, order and metadata, remapping internal references and sharing external materials/assets.
65
70
  Cloning requires a host transport with atomic scene patches; an unsupported host rejects it without changing the draft.
66
- For primitives, nodeSetBlend joins against all accumulated preceding geometry in the Area, not only the nearest part. A Union's blend affects its outer join, not joins between its children; top-level Area blend does not smooth its contents.
67
- For organic joins, overlap primitives and tune blend on the joining children while preserving intended gaps; keep crisp accessories separate.
71
+ For primitives, nodeSetBlend joins against all accumulated preceding geometry in the Area, not only the nearest part. A Union's blend affects its outer join, not joins between its children; top-level Area blend does not smooth its contents.
72
+ For organic joins, overlap primitives and tune blend on the joining children while preserving intended gaps; keep crisp accessories separate.
68
73
  Check font, HDR, and material catalogs before selecting names. A host can explicitly report a catalog as
69
74
  unavailable. Selecting an HDR resets its intensity; apply a custom intensity after selecting it.
70
- For library materials, use listSuperMaterials(), then await materialImport(name) inside scene.edit and assign the returned numeric ID with nodeSetMaterialId or curvePointSetMaterialId. A failed import rolls back the draft.
71
- Filter large read results with JavaScript and print only useful details.
75
+ For library materials, use listSuperMaterials(), then await materialImport(name) inside scene.edit and assign the returned numeric ID the way that object requires: nodeSetMaterialId or curvePointSetMaterialId for scene nodes, the FlowIR SetMaterial node for a Geometry Nodes graph. A failed import rolls back the draft.
76
+ Filter large read results with JavaScript and print only useful details.
72
77
  After a major edit, capture a screenshot outside the transaction and emitImage its reference.
73
78
  screenshot({ camera: { position: {x,y,z}, target: {x,y,z}, projection: 'perspective', horizontalFovDegrees: 60 } })
74
79
  sets a world-space camera in the same Womp coordinates and project units as scene positions (Y-up).
@@ -77,7 +82,7 @@ Direction is derived from position/target; do not use coincident points or an ex
77
82
  Omitted projection/lens settings are preserved. The camera changes immediately and remains active; there is no auto-fit.
78
83
  screenshot() keeps the active camera. Do not change a locked camera without the user's instruction. Verify silhouette,
79
84
  connections, openings, proportions, and materials before claiming completion. Use the owning top-level Area for scene-space bounds;
80
- OpenSCAD part bounds are local to the part; individual primitive and curve bounds are unsupported. Renderer bounds and resize calls
85
+ OpenSCAD part bounds are local to the part; individual primitive and curve bounds are unsupported. Renderer bounds and resize calls
81
86
  must finish before subsequent writes. Inside scene.edit, complete renderer-backed bounds reads before the first
82
87
  draft mutation; the SDK rejects them once the draft is dirty. Inspect committed results when renderer state has not caught up.
83
88
  A failed script can retain earlier committed effects. Use receipts and inspect unknown outcomes before retrying.`,