@womp/spark-agent 4.1.1 → 4.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.
Files changed (40) hide show
  1. package/.env.example +6 -0
  2. package/README.md +436 -406
  3. package/dist/agent-tools.js +7 -1
  4. package/dist/agent.js +11 -2
  5. package/dist/bounded-output.d.ts +37 -0
  6. package/dist/bounded-output.js +110 -0
  7. package/dist/capabilities/geometry.js +14 -14
  8. package/dist/capabilities/operations.js +38 -33
  9. package/dist/capabilities/scene.js +87 -66
  10. package/dist/color.d.ts +8 -0
  11. package/dist/color.js +29 -0
  12. package/dist/error-diagnostics.d.ts +32 -0
  13. package/dist/error-diagnostics.js +80 -0
  14. package/dist/generated/scene-declarations.d.ts +1 -1
  15. package/dist/generated/scene-declarations.js +1 -1
  16. package/dist/geometry/compiler.js +6 -4
  17. package/dist/geometry/diagnostics.d.ts +18 -0
  18. package/dist/geometry/diagnostics.js +70 -0
  19. package/dist/geometry/schemas/backend-schema.json +3393 -3393
  20. package/dist/geometry/schemas/flowir-dialect.json +1023 -1023
  21. package/dist/geometry.js +6 -6
  22. package/dist/prompt.js +70 -70
  23. package/dist/scene-environment.d.ts +1 -1
  24. package/dist/scene-environment.js +8 -4
  25. package/dist/scene-health.d.ts +23 -0
  26. package/dist/scene-health.js +71 -0
  27. package/dist/sdk-geometry-host.js +5 -3
  28. package/dist/sdk-scene-health-host.js +27 -5
  29. package/dist/sdk-scene-host.d.ts +5 -5
  30. package/dist/sdk-scene-host.js +7 -2
  31. package/dist/server/fixtures/materials/matte-blue.json +14 -14
  32. package/dist/server/fixtures/materials/polished-copper.json +14 -14
  33. package/dist/server/service.js +35 -10
  34. package/dist/server/sqlite-store.js +37 -37
  35. package/dist/server/types.d.ts +2 -0
  36. package/dist/ui.d.ts +46 -0
  37. package/dist/ui.js +9 -1
  38. package/dist/womp-scene-api.d.ts +11 -10
  39. package/dist/womp-scene-api.js +11 -9
  40. package/package.json +139 -139
package/dist/geometry.js CHANGED
@@ -7,12 +7,12 @@ export { migrateParameterOverrides, readParameterOverrides, } from "./geometry/p
7
7
  let reference;
8
8
  export function geometryReference() {
9
9
  reference ??= {
10
- rules: `Write one statement per line:
11
- param Float width = 20.0 min=1 max=100 step=1
12
- node box Primitive Type=Cube Position=Vec3(0.0,0.0,0.0) Rotation=Vec3(0.0,0.0,0.0) Size=width Roundness=0.0
13
- out Geometry=box.Geometry
14
- Use decimal components in Vec3 parameter/constant literals, e.g. Vec3(20.0,20.0,20.0). Output name is case-sensitive: Geometry.
15
- Exactly one cast applies per port and casts do not chain: Float[] -> Vec3 is unavailable even though Float[] -> Float and Float -> Vec3 are both listed. Build the vector with CombineXYZ instead.
10
+ rules: `Write one statement per line:
11
+ param Float width = 20.0 min=1 max=100 step=1
12
+ node box Primitive Type=Cube Position=Vec3(0.0,0.0,0.0) Rotation=Vec3(0.0,0.0,0.0) Size=width Roundness=0.0
13
+ out Geometry=box.Geometry
14
+ Use decimal components in Vec3 parameter/constant literals, e.g. Vec3(20.0,20.0,20.0). Output name is case-sensitive: Geometry.
15
+ Exactly one cast applies per port and casts do not chain: Float[] -> Vec3 is unavailable even though Float[] -> Float and Float -> Vec3 are both listed. Build the vector with CombineXYZ instead.
16
16
  ${flowir.renderDslRulesBlock()}`,
17
17
  catalog: flowir.renderSchemaBlock(flowirRegistry.getSchema()),
18
18
  };
package/dist/prompt.js CHANGED
@@ -1,55 +1,55 @@
1
- export const SPARK_AGENT_PROMPT = `You are Spark, Womp's unified assistant.
2
- Use read_skill to discover the APIs needed for the user's request. Skills provide TypeScript
3
- declarations for documentation; execute_javascript runs JavaScript, with top-level await.
4
- Only the listed capabilities are available. Reading a skill does not grant additional permissions.
5
- Use only the supplied womp APIs and output helpers to access application services and state.
6
- If a capability is unavailable, explain what is missing and ask the user to enable it when applicable.
7
- Do not work around missing capabilities by inspecting page globals, DOM or framework internals,
8
- or making direct network requests. Native execution is not a security sandbox; it does not expand
9
- the supported API or the user's granted capabilities.
10
- Give each execute_javascript call a concise present-progressive purpose, such as "Generating a car image", "Inspecting the scene", or "Importing the model". Write purpose before code.
11
- Each execution has fresh local variables. Keep stable object, job, and artifact references.
12
- Use print for concise, filtered observations; await emitImage or emitFile with a host-issued artifact
13
- reference to show actual output. emitImage inspects by default; use { inspect: false } to display without adding image bytes to model context.
14
- 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.
15
- Do not claim visual verification without inspecting the image. Missing artifact references must be recovered or reported; omitted image bytes alone are not a missing result.
16
- Inspect the scene before editing and verify the committed result.
17
- Preserve existing content unless the user asks to replace or delete it. Use the latest target revision
18
- when a write requires one. Capabilities own transactions: an entire script is not atomic.
19
- If an operation fails after earlier effects, use its receipts to inspect and retry only unresolved work.
20
- Never claim an uncertain commit rolled back. Never resubmit a job just because waiting timed out.
21
- Complete related work in this conversation without delegating to another agent. Report limits,
22
- unsupported actions, cancellation, and incomplete work truthfully. Finish with a concise explanation
23
- of the result in the user's language; keep implementation details out of that explanation.
24
-
25
- Creation intent
26
-
27
- Choose the workflow from the user's intended result and the conversation.
28
- Describe choices by their outcome, not internal tools.
29
-
30
- - Use editable CAD for printable, measured, mechanical, or real-world-fit
31
- objects. Ask about missing dimensions only when a sensible default would
32
- likely make the result wrong. CAD is the preferred printing workflow;
33
- do not claim print readiness without supporting validation.
34
-
35
- - Build directly in the scene for ordinary 3D requests, including “3D X”
36
- and “make X in 3D,” and when the user wants individually editable shapes
37
- or geometry that blends with existing objects. Printable, measured,
38
- or mechanical intent takes precedence and routes to CAD.
39
-
40
- - Use mesh generation when the user explicitly requests a mesh, asks to
41
- turn an existing image into 3D, or chooses a sculpted result rather than
42
- individually editable shapes.
43
-
44
- - Use image generation for pictures, logos, concept art, wallpapers,
45
- and other clearly visual output.
46
-
47
- - For an ambiguous creative request without explicit 3D or printing intent,
48
- generate one image preview first. Then offer one short, outcome-based
49
- choice: refine the image, turn it into a sculpted 3D piece, or build it
50
- from individually editable shapes. Offer adjustable CAD instead of
51
- editable shapes when the subject is measured or must fit something real.
52
-
1
+ export const SPARK_AGENT_PROMPT = `You are Spark, Womp's unified assistant.
2
+ Use read_skill to discover the APIs needed for the user's request. Skills provide TypeScript
3
+ declarations for documentation; execute_javascript runs JavaScript, with top-level await.
4
+ Only the listed capabilities are available. Reading a skill does not grant additional permissions.
5
+ Use only the supplied womp APIs and output helpers to access application services and state.
6
+ If a capability is unavailable, explain what is missing and ask the user to enable it when applicable.
7
+ Do not work around missing capabilities by inspecting page globals, DOM or framework internals,
8
+ or making direct network requests. Native execution is not a security sandbox; it does not expand
9
+ the supported API or the user's granted capabilities.
10
+ Give each execute_javascript call a concise present-progressive purpose, such as "Generating a car image", "Inspecting the scene", or "Importing the model". Write purpose before code.
11
+ Each execution has fresh local variables. Keep stable object, job, and artifact references.
12
+ Use print for concise, filtered observations; await emitImage or emitFile with a host-issued artifact
13
+ reference to show actual output. emitImage inspects by default; use { inspect: false } to display without adding image bytes to model context.
14
+ 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.
15
+ Do not claim visual verification without inspecting the image. Missing artifact references must be recovered or reported; omitted image bytes alone are not a missing result.
16
+ Inspect the scene before editing and verify the committed result.
17
+ Preserve existing content unless the user asks to replace or delete it. Use the latest target revision
18
+ when a write requires one. Capabilities own transactions: an entire script is not atomic.
19
+ If an operation fails after earlier effects, use its receipts to inspect and retry only unresolved work.
20
+ Never claim an uncertain commit rolled back. Never resubmit a job just because waiting timed out.
21
+ Complete related work in this conversation without delegating to another agent. Report limits,
22
+ unsupported actions, cancellation, and incomplete work truthfully. Finish with a concise explanation
23
+ of the result in the user's language; keep implementation details out of that explanation.
24
+
25
+ Creation intent
26
+
27
+ Choose the workflow from the user's intended result and the conversation.
28
+ Describe choices by their outcome, not internal tools.
29
+
30
+ - Use editable CAD for printable, measured, mechanical, or real-world-fit
31
+ objects. Ask about missing dimensions only when a sensible default would
32
+ likely make the result wrong. CAD is the preferred printing workflow;
33
+ do not claim print readiness without supporting validation.
34
+
35
+ - Build directly in the scene for ordinary 3D requests, including “3D X”
36
+ and “make X in 3D,” and when the user wants individually editable shapes
37
+ or geometry that blends with existing objects. Printable, measured,
38
+ or mechanical intent takes precedence and routes to CAD.
39
+
40
+ - Use mesh generation when the user explicitly requests a mesh, asks to
41
+ turn an existing image into 3D, or chooses a sculpted result rather than
42
+ individually editable shapes.
43
+
44
+ - Use image generation for pictures, logos, concept art, wallpapers,
45
+ and other clearly visual output.
46
+
47
+ - For an ambiguous creative request without explicit 3D or printing intent,
48
+ generate one image preview first. Then offer one short, outcome-based
49
+ choice: refine the image, turn it into a sculpted 3D piece, or build it
50
+ from individually editable shapes. Offer adjustable CAD instead of
51
+ editable shapes when the subject is measured or must fit something real.
52
+
53
53
  - Choose construction methods by the user's requested workflow and the
54
54
  available capabilities, not by assuming editable shapes require lower
55
55
  fidelity. A sculpted appearance does not by itself require mesh generation.
@@ -57,22 +57,22 @@ Describe choices by their outcome, not internal tools.
57
57
  and detail with that method. Offer alternatives only for a specific
58
58
  demonstrated limitation; ask before switching or simplifying the requested
59
59
  result. No method guarantees exact reconstruction from a single image.
60
-
61
- - When the user chooses editable shapes or CAD after a preview, preserve
62
- its palette, proportions, and defining features. When they choose a
63
- sculpted result, use that preview as the image-to-mesh input rather
64
- than starting again from a text prompt.
65
-
66
- - If the user wants repeated generation variations and Flow is available,
67
- offer a reusable Flow workflow.
68
-
69
- - Ask at most one focused question at a time, only when a sensible default
70
- would likely produce the wrong outcome. Do not repeat resolved questions.
71
- Interpret replies using the preceding offer; if several options were
72
- offered and “yes” does not identify one, clarify briefly.
73
-
74
- - Treat a clear answer to your question as an instruction to proceed.
75
- Continue from existing results rather than restarting the workflow.
76
-
77
- Offer only workflows available in this host. Follow their authorization
60
+
61
+ - When the user chooses editable shapes or CAD after a preview, preserve
62
+ its palette, proportions, and defining features. When they choose a
63
+ sculpted result, use that preview as the image-to-mesh input rather
64
+ than starting again from a text prompt.
65
+
66
+ - If the user wants repeated generation variations and Flow is available,
67
+ offer a reusable Flow workflow.
68
+
69
+ - Ask at most one focused question at a time, only when a sensible default
70
+ would likely produce the wrong outcome. Do not repeat resolved questions.
71
+ Interpret replies using the preceding offer; if several options were
72
+ offered and “yes” does not identify one, clarify briefly.
73
+
74
+ - Treat a clear answer to your question as an instruction to proceed.
75
+ Continue from existing results rather than restarting the workflow.
76
+
77
+ Offer only workflows available in this host. Follow their authorization
78
78
  and confirmation requirements before generation or scene changes.`;
@@ -17,7 +17,7 @@ export declare function createSceneEnvironment(scene: SceneEdit, hdrs: readonly
17
17
  setHdrName: (name: string) => void;
18
18
  setHdrMultiplier: (multiplier: number) => void;
19
19
  setHdrOrientation: (orientation: Vec2) => void;
20
- setBackgroundColor: (color: {
20
+ setBackgroundColor: (rgb255: {
21
21
  r: number;
22
22
  g: number;
23
23
  b: number;
@@ -1,4 +1,5 @@
1
1
  import { metadataObject } from "./scene-metadata.js";
2
+ import { assertColor255 } from "./color.js";
2
3
  function finite(value, label) {
3
4
  if (!Number.isFinite(value))
4
5
  throw new Error(`${label} must be a finite number`);
@@ -65,10 +66,13 @@ export function createSceneEnvironment(scene, hdrs, print) {
65
66
  write({ hdr_orientation: { x, y } });
66
67
  print(`Set HDR orientation to: (${x}, ${y})`);
67
68
  },
68
- setBackgroundColor: (color) => {
69
- const r = finite(color?.r, "Background red");
70
- const g = finite(color?.g, "Background green");
71
- const b = finite(color?.b, "Background blue");
69
+ setBackgroundColor: (rgb255) => {
70
+ const r = finite(rgb255?.r, "Background red");
71
+ const g = finite(rgb255?.g, "Background green");
72
+ const b = finite(rgb255?.b, "Background blue");
73
+ // The same 0-255 domain as every other color here, so the same refusal
74
+ // rather than a silent near-black (./color.ts).
75
+ assertColor255([r, g, b], "Background color");
72
76
  write({ backdrop_color1: { x: r / 255, y: g / 255, z: b / 255 } });
73
77
  print(`Set background color to: (${r}, ${g}, ${b})`);
74
78
  },
@@ -40,6 +40,29 @@ export type SceneHealth = {
40
40
  reason?: string;
41
41
  };
42
42
  export declare function unverifiedSceneHealth(affectedAreaIds: string[], reason: string): SceneHealth;
43
+ export declare const MAX_RECEIPT_THERMOMETER_OBJECTS = 8;
44
+ /**
45
+ * The same health reading, with the thermometer's per-Area table bounded for a
46
+ * receipt.
47
+ *
48
+ * WHY. Every scene, geometry and OpenSCAD receipt embeds the whole thermometer,
49
+ * one entry per Area, and a receipt is both rendered in the panel and read by
50
+ * the model. A 20-Area scene therefore repeats a table the model did not ask
51
+ * about on every edit. The totals, the limits and the overflow flags are what a
52
+ * decision is made on, so they are kept whole: what is bounded is the number of
53
+ * per-Area rows, affected and overflowing Areas first, with the omission stated
54
+ * in `reason` rather than left silent. Health used for a repair decision --
55
+ * `hasGeometryOverflow`, `hasSceneResourcePressure`, the host's `onHealth` --
56
+ * reads the untrimmed value, so no decision changes.
57
+ */
58
+ export declare function receiptSceneHealth(health: SceneHealth, limit?: number): SceneHealth;
59
+ /**
60
+ * Receipt details with an embedded scene health reading bounded by
61
+ * `receiptSceneHealth`. Anything else, including details with no health, is
62
+ * returned untouched: a receipt is read back by hosts for recovery, so this
63
+ * changes the one payload that is unbounded by construction and nothing else.
64
+ */
65
+ export declare function boundedReceiptDetails(details: Record<string, unknown>): Record<string, unknown>;
43
66
  export declare function getSdkSceneAreaId(api: KakapoAPI, objectId: number): string;
44
67
  export declare function getSdkSceneAreaIds(api: KakapoAPI): string[];
45
68
  export declare function hasGeometryOverflow(health: SceneHealth): boolean;
@@ -34,6 +34,77 @@ export function unverifiedSceneHealth(affectedAreaIds, reason) {
34
34
  reason,
35
35
  };
36
36
  }
37
+ export const MAX_RECEIPT_THERMOMETER_OBJECTS = 8;
38
+ /**
39
+ * The same health reading, with the thermometer's per-Area table bounded for a
40
+ * receipt.
41
+ *
42
+ * WHY. Every scene, geometry and OpenSCAD receipt embeds the whole thermometer,
43
+ * one entry per Area, and a receipt is both rendered in the panel and read by
44
+ * the model. A 20-Area scene therefore repeats a table the model did not ask
45
+ * about on every edit. The totals, the limits and the overflow flags are what a
46
+ * decision is made on, so they are kept whole: what is bounded is the number of
47
+ * per-Area rows, affected and overflowing Areas first, with the omission stated
48
+ * in `reason` rather than left silent. Health used for a repair decision --
49
+ * `hasGeometryOverflow`, `hasSceneResourcePressure`, the host's `onHealth` --
50
+ * reads the untrimmed value, so no decision changes.
51
+ */
52
+ export function receiptSceneHealth(health, limit = MAX_RECEIPT_THERMOMETER_OBJECTS) {
53
+ const thermometer = health.thermometer;
54
+ if (thermometer === undefined)
55
+ return health;
56
+ const ids = Object.keys(thermometer.objects);
57
+ if (ids.length <= limit)
58
+ return health;
59
+ const overflowing = (id) => {
60
+ const object = thermometer.objects[id];
61
+ return (object !== undefined &&
62
+ (object.flag_prim_overflow > 0 ||
63
+ object.flag_depth_overflow > 0 ||
64
+ object.flag_vox_overflow > 0 ||
65
+ object.num_prims >= thermometer.max_num_prims ||
66
+ object.voxels >= thermometer.max_per_object));
67
+ };
68
+ const ordered = [
69
+ ...health.affectedAreaIds.filter((id) => thermometer.objects[id] !== undefined),
70
+ ...ids.filter((id) => !health.affectedAreaIds.includes(id) && overflowing(id)),
71
+ ...ids.filter((id) => !health.affectedAreaIds.includes(id) && !overflowing(id)),
72
+ ];
73
+ const kept = ordered.slice(0, limit);
74
+ const objects = {};
75
+ for (const id of kept)
76
+ objects[id] = thermometer.objects[id];
77
+ const note = `Thermometer rows bounded for this receipt: ${kept.length} of ${ids.length} Areas shown, affected and overflowing first; ${ids.length - kept.length} omitted. Totals, limits and overflow flags are complete. Use scene.health(objectIds) for a specific Area.`;
78
+ return {
79
+ ...health,
80
+ thermometer: { ...thermometer, objects },
81
+ reason: health.reason === undefined ? note : `${health.reason} ${note}`,
82
+ };
83
+ }
84
+ /**
85
+ * Receipt details with an embedded scene health reading bounded by
86
+ * `receiptSceneHealth`. Anything else, including details with no health, is
87
+ * returned untouched: a receipt is read back by hosts for recovery, so this
88
+ * changes the one payload that is unbounded by construction and nothing else.
89
+ */
90
+ export function boundedReceiptDetails(details) {
91
+ const health = details.sceneHealth;
92
+ if (typeof health !== "object" || health === null || Array.isArray(health))
93
+ return details;
94
+ const candidate = health;
95
+ if (Array.isArray(candidate.affectedAreaIds) === false)
96
+ return details;
97
+ const parsed = thermometerSchema.safeParse(candidate.thermometer);
98
+ if (parsed.success === false)
99
+ return details;
100
+ return {
101
+ ...details,
102
+ sceneHealth: receiptSceneHealth({
103
+ ...candidate,
104
+ thermometer: parsed.data,
105
+ }),
106
+ };
107
+ }
37
108
  export function getSdkSceneAreaId(api, objectId) {
38
109
  let node = api.getNode(objectId);
39
110
  let areaId = node.kind === "union" ? node.id : objectId;
@@ -1,5 +1,6 @@
1
1
  import { compileGeometry, migrateGraphExt, readEditableGeometry, } from "./geometry/compiler.js";
2
2
  import { graphExtSchema } from "./geometry/graph.js";
3
+ import { boundedDiagnostics } from "./geometry/diagnostics.js";
3
4
  import { migrateParameterOverrides, readParameterOverrides, } from "./geometry/parameters.js";
4
5
  import { getSdkSceneAreaId, readStableSdkSceneHealth } from "./scene-health.js";
5
6
  import { createGeometryRepairTracker, } from "./repair-state.js";
@@ -78,7 +79,8 @@ async function readTarget(api, objectId, target = resolveTarget(api, objectId))
78
79
  }
79
80
  catch (error) {
80
81
  result.source = null;
81
- result.diagnostics.push(errorMessage(error));
82
+ // Bounded: a compiler diagnostic is unbounded text (./geometry/diagnostics.ts).
83
+ result.diagnostics.push(...boundedDiagnostics([errorMessage(error)]));
82
84
  }
83
85
  return structuredClone(result);
84
86
  }
@@ -163,7 +165,7 @@ export function createSdkGeometryHost(options) {
163
165
  instanceId: target.instanceId,
164
166
  affectedObjectIds: [],
165
167
  revision: target.revision,
166
- diagnostics,
168
+ diagnostics: boundedDiagnostics(diagnostics),
167
169
  });
168
170
  let compiled;
169
171
  let nextExt;
@@ -295,7 +297,7 @@ export function createSdkGeometryHost(options) {
295
297
  status: "applied",
296
298
  ...applied.value,
297
299
  revision: applied.operation.resultRevision ?? input.expectedRevision,
298
- diagnostics,
300
+ diagnostics: boundedDiagnostics(diagnostics),
299
301
  };
300
302
  }
301
303
  finally {
@@ -1,13 +1,35 @@
1
1
  import { getSdkSceneAreaId, getSdkSceneAreaIds, readStableSdkSceneHealth, } from "./scene-health.js";
2
+ /**
3
+ * The Area that owns `objectId`, or undefined when the node is in no Area --
4
+ * the scene root 0, a root-level Group, a Light. The engine's thermometer has a
5
+ * row per Area only, so such an id has no row of its own and asking for one
6
+ * would report the whole scene as unverified.
7
+ */
8
+ function owningAreaId(api, objectId) {
9
+ const areaId = getSdkSceneAreaId(api, objectId);
10
+ return api.getNode(Number(areaId)).kind === "union" ? areaId : undefined;
11
+ }
2
12
  export function createSdkSceneHealthHost(editor, onHealth) {
3
13
  return {
4
14
  read: (objectIds, context) => editor.read(context, async (api) => {
5
- const areaIds = objectIds.length > 0
6
- ? objectIds.map((id) => getSdkSceneAreaId(api, id))
7
- : getSdkSceneAreaIds(api);
15
+ const owned = objectIds
16
+ .map((id) => ({ id, areaId: owningAreaId(api, id) }))
17
+ .filter((entry) => entry.areaId !== undefined);
18
+ // `scene.read()` publishes node 0, and ids outside any Area are legal
19
+ // arguments. They narrow nothing, so they are reported rather than
20
+ // allowed to make the whole reading unverified; with nothing left to
21
+ // narrow by, this is the whole-scene read `health()` already means.
22
+ const outside = objectIds.filter((id) => owned.some((entry) => entry.id === id) === false);
23
+ const areaIds = owned.length > 0 ? owned.map((entry) => entry.areaId) : getSdkSceneAreaIds(api);
8
24
  const health = await readStableSdkSceneHealth(api, [...new Set(areaIds)], context.signal);
9
- onHealth?.(health);
10
- return health;
25
+ const note = outside.length === 0
26
+ ? undefined
27
+ : `${outside.join(", ")} ${outside.length === 1 ? "is" : "are"} in no Area, and the engine reports health per Area, so ${owned.length > 0 ? "health is reported for the remaining ids" : "this is the whole scene's health"}.`;
28
+ const reported = note === undefined
29
+ ? health
30
+ : { ...health, reason: health.reason === undefined ? note : `${health.reason} ${note}` };
31
+ onHealth?.(reported);
32
+ return reported;
11
33
  }),
12
34
  };
13
35
  }
@@ -115,7 +115,7 @@ export declare function createSdkSceneDraft(api: KakapoAPI, scene: SceneEdit, se
115
115
  g: number;
116
116
  b: number;
117
117
  };
118
- nodeSetColor: (id: number, color: {
118
+ nodeSetColor: (id: number, rgb255: {
119
119
  r: number;
120
120
  g: number;
121
121
  b: number;
@@ -206,7 +206,7 @@ export declare function createSdkSceneDraft(api: KakapoAPI, scene: SceneEdit, se
206
206
  g: number;
207
207
  b: number;
208
208
  };
209
- materialSetColor: (id: number, color: {
209
+ materialSetColor: (id: number, rgb255: {
210
210
  r: number;
211
211
  g: number;
212
212
  b: number;
@@ -216,7 +216,7 @@ export declare function createSdkSceneDraft(api: KakapoAPI, scene: SceneEdit, se
216
216
  g: number;
217
217
  b: number;
218
218
  };
219
- materialSetSecondaryColor: (id: number, color: {
219
+ materialSetSecondaryColor: (id: number, rgb255: {
220
220
  r: number;
221
221
  g: number;
222
222
  b: number;
@@ -226,7 +226,7 @@ export declare function createSdkSceneDraft(api: KakapoAPI, scene: SceneEdit, se
226
226
  g: number;
227
227
  b: number;
228
228
  };
229
- materialSetSubsurfaceColor: (id: number, color: {
229
+ materialSetSubsurfaceColor: (id: number, rgb255: {
230
230
  r: number;
231
231
  g: number;
232
232
  b: number;
@@ -294,7 +294,7 @@ export declare function createSdkSceneDraft(api: KakapoAPI, scene: SceneEdit, se
294
294
  setHdrName: (name: string) => void;
295
295
  setHdrMultiplier: (multiplier: number) => void;
296
296
  setHdrOrientation: (orientation: import("@womp/kakapo-sdk").Vec2) => void;
297
- setBackgroundColor: (color: {
297
+ setBackgroundColor: (rgb255: {
298
298
  r: number;
299
299
  g: number;
300
300
  b: number;
@@ -2,6 +2,7 @@ import { createWompSceneApi, sceneNodeType } from "./womp-scene-api.js";
2
2
  import { sceneUnitMultiplier } from "./coordinates.js";
3
3
  import { executeSceneOperation, } from "./scene-operations.js";
4
4
  import { readMaterialMetadata, writeMaterialMetadata, } from "./scene-metadata.js";
5
+ import { boundedReceiptDetails } from "./scene-health.js";
5
6
  export function createSdkSceneDraft(api, scene, services) {
6
7
  let defaultMaterialId = scene
7
8
  .listMaterials()
@@ -100,13 +101,17 @@ export function createSdkSceneEditor(options) {
100
101
  : operation.status === "rolled_back"
101
102
  ? "rolled_back"
102
103
  : "unknown",
103
- details: {
104
+ // An embedded scene health reading is bounded here, where every
105
+ // receipt is recorded -- the package's geometry and OpenSCAD
106
+ // hosts and a host's own `onCommitted` all put one in details
107
+ // (./scene-health.ts `boundedReceiptDetails`).
108
+ details: boundedReceiptDetails({
104
109
  ...hostDetails,
105
110
  result: operation.result,
106
111
  save: operation.save,
107
112
  error: operation.error,
108
113
  synchronized,
109
- },
114
+ }),
110
115
  });
111
116
  };
112
117
  try {
@@ -1,14 +1,14 @@
1
- {
2
- "name": "Development: Matte Blue",
3
- "resource": {
4
- "data": {
5
- "color.x": 0.08,
6
- "color.y": 0.3,
7
- "color.z": 0.8,
8
- "roughness": 0.85,
9
- "metalness": 0,
10
- "index_of_refraction": 1.5
11
- },
12
- "shaders": []
13
- }
14
- }
1
+ {
2
+ "name": "Development: Matte Blue",
3
+ "resource": {
4
+ "data": {
5
+ "color.x": 0.08,
6
+ "color.y": 0.3,
7
+ "color.z": 0.8,
8
+ "roughness": 0.85,
9
+ "metalness": 0,
10
+ "index_of_refraction": 1.5
11
+ },
12
+ "shaders": []
13
+ }
14
+ }
@@ -1,14 +1,14 @@
1
- {
2
- "name": "Development: Polished Copper",
3
- "resource": {
4
- "data": {
5
- "color.x": 0.955,
6
- "color.y": 0.638,
7
- "color.z": 0.538,
8
- "roughness": 0.18,
9
- "metalness": 1,
10
- "index_of_refraction": 1.5
11
- },
12
- "shaders": []
13
- }
14
- }
1
+ {
2
+ "name": "Development: Polished Copper",
3
+ "resource": {
4
+ "data": {
5
+ "color.x": 0.955,
6
+ "color.y": 0.638,
7
+ "color.z": 0.538,
8
+ "roughness": 0.18,
9
+ "metalness": 1,
10
+ "index_of_refraction": 1.5
11
+ },
12
+ "shaders": []
13
+ }
14
+ }
@@ -1,3 +1,4 @@
1
+ import { sparkErrorDiagnostics } from "../error-diagnostics.js";
1
2
  import { readUIMessageStream, validateUIMessages } from "ai";
2
3
  import { sparkDataPartSchemas, sparkMessageMetadataSchema, } from "../ui.js";
3
4
  const TERMINAL_STATUSES = new Set([
@@ -102,7 +103,7 @@ export class SparkAgentService {
102
103
  }
103
104
  async deleteConversation(ownerId, id) {
104
105
  await this.getConversation(ownerId, id);
105
- if (await this.store.deleteConversation(id) === false) {
106
+ if ((await this.store.deleteConversation(id)) === false) {
106
107
  throw new SparkServiceError(409, "Stop the active run before deleting this conversation.");
107
108
  }
108
109
  }
@@ -209,7 +210,9 @@ export class SparkAgentService {
209
210
  this.controllers.set(run.id, controller);
210
211
  const execution = this.runInBackground(run, conversation, controller);
211
212
  this.executions.set(run.id, execution);
212
- void execution.finally(() => this.executions.delete(run.id)).catch((error) => {
213
+ void execution
214
+ .finally(() => this.executions.delete(run.id))
215
+ .catch((error) => {
213
216
  console.error("Spark run persistence failed", error);
214
217
  });
215
218
  return run;
@@ -314,7 +317,7 @@ export class SparkAgentService {
314
317
  continue;
315
318
  await this.appendChunk(run, chunk);
316
319
  }
317
- })();
320
+ })().then(() => undefined, (error) => error);
318
321
  let executionError;
319
322
  let result;
320
323
  try {
@@ -323,9 +326,11 @@ export class SparkAgentService {
323
326
  catch (error) {
324
327
  executionError = error;
325
328
  }
326
- await consume;
329
+ const streamError = await consume;
327
330
  if (executionError !== undefined)
328
331
  throw executionError;
332
+ if (streamError !== undefined)
333
+ throw streamError;
329
334
  if (controller.signal.aborted) {
330
335
  await this.appendStatus(run, "cancelled");
331
336
  await this.finishRun(run, conversation, "cancelled");
@@ -341,25 +346,26 @@ export class SparkAgentService {
341
346
  await this.finishRun(run, conversation, "cancelled");
342
347
  return;
343
348
  }
344
- const message = error instanceof Error ? error.message : String(error);
349
+ const diagnostics = sparkErrorDiagnostics(error);
350
+ const message = diagnostics.message;
345
351
  const failedChunk = {
346
352
  type: "data-run-status",
347
353
  id: `run-status-${run.id}`,
348
- data: { runId: run.id, status: "failed", error: message },
354
+ data: { runId: run.id, status: "failed", error: message, errorDiagnostics: diagnostics },
349
355
  };
350
356
  await this.appendChunk(run, { type: "error", errorText: message });
351
357
  await this.appendChunk(run, failedChunk);
352
- await this.finishRun(run, conversation, "failed", message);
358
+ await this.finishRun(run, conversation, "failed", message, diagnostics);
353
359
  }
354
360
  finally {
355
361
  this.controllers.delete(run.id);
356
362
  }
357
363
  }
358
- async finishRun(run, conversation, status, error) {
364
+ async finishRun(run, conversation, status, error, errorDiagnostics) {
359
365
  const completedAt = now();
360
366
  const storedChunks = await this.store.listRunChunks(run.id, 0);
361
367
  const chunks = storedChunks.map(({ chunk }) => chunk);
362
- const assistant = await this.materializeMessage(run, chunks, status, completedAt, error);
368
+ const assistant = await this.materializeMessage(run, chunks, status, completedAt, error, errorDiagnostics);
363
369
  if (assistant !== undefined) {
364
370
  await this.appendChunk(run, {
365
371
  type: "message-metadata",
@@ -379,12 +385,13 @@ export class SparkAgentService {
379
385
  ...run,
380
386
  status,
381
387
  ...(error === undefined ? {} : { error }),
388
+ ...(errorDiagnostics === undefined ? {} : { errorDiagnostics }),
382
389
  updatedAt: completedAt,
383
390
  completedAt,
384
391
  });
385
392
  this.wakeRun(run.id);
386
393
  }
387
- async materializeMessage(run, chunks, status, completedAt, error) {
394
+ async materializeMessage(run, chunks, status, completedAt, error, errorDiagnostics) {
388
395
  let message;
389
396
  try {
390
397
  for await (const current of readUIMessageStream({
@@ -412,6 +419,7 @@ export class SparkAgentService {
412
419
  modelId: run.settings.modelId,
413
420
  ...(message.metadata?.usage === undefined ? {} : { usage: message.metadata.usage }),
414
421
  ...(error === undefined ? {} : { error }),
422
+ ...(errorDiagnostics === undefined ? {} : { errorDiagnostics }),
415
423
  },
416
424
  };
417
425
  }
@@ -423,6 +431,23 @@ export class SparkAgentService {
423
431
  });
424
432
  }
425
433
  async appendChunk(run, chunk) {
434
+ if (chunk.type === "error")
435
+ chunk = { ...chunk, errorText: sparkErrorDiagnostics(chunk.errorText).message };
436
+ if (chunk.type === "message-metadata" || chunk.type === "data-run-status") {
437
+ const data = chunk.type === "message-metadata" ? chunk.messageMetadata : chunk.data;
438
+ const safe = {
439
+ ...(data.error === undefined ? {} : { error: sparkErrorDiagnostics(data.error).message }),
440
+ ...(data.errorDiagnostics === undefined
441
+ ? {}
442
+ : {
443
+ errorDiagnostics: sparkErrorDiagnostics(data.errorDiagnostics),
444
+ }),
445
+ };
446
+ chunk =
447
+ chunk.type === "message-metadata"
448
+ ? { ...chunk, messageMetadata: { ...chunk.messageMetadata, ...safe } }
449
+ : { ...chunk, data: { ...chunk.data, ...safe } };
450
+ }
426
451
  const stored = await this.store.appendRunChunk(run.id, run.conversationId, chunk);
427
452
  for (const listener of this.listeners.get(run.id) ?? [])
428
453
  listener(stored);