@womp/spark-agent 8.0.0 → 9.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.
- package/CHANGELOG.md +58 -0
- package/README.md +3 -2
- package/dist/capabilities/materials.d.ts +10 -1
- package/dist/capabilities/materials.js +30 -10
- package/dist/generated/materials-declarations.d.ts +1 -1
- package/dist/generated/materials-declarations.js +1 -1
- package/dist/generated/materials-flowir-declarations.d.ts +1 -0
- package/dist/generated/materials-flowir-declarations.js +3 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +0 -1
- package/dist/kakapo.d.ts +1 -1
- package/dist/materials/flowir.d.ts +15 -0
- package/dist/materials/flowir.js +362 -0
- package/dist/materials/graph.d.ts +7 -0
- package/dist/materials/graph.js +13 -1
- package/dist/materials/lift.d.ts +3 -0
- package/dist/materials/lift.js +216 -0
- package/dist/materials/schemas/materials-dialect.json +690 -0
- package/dist/materials/types.d.ts +15 -0
- package/dist/scene-operations.d.ts +2 -17
- package/dist/scene-operations.js +4 -40
- package/dist/sdk-geometry-host.js +1 -2
- package/dist/sdk-materials-host.js +136 -10
- package/dist/sdk-openscad-host.js +1 -3
- package/dist/sdk-scene-host.d.ts +3 -5
- package/dist/sdk-scene-host.js +5 -15
- package/dist/server/sqlite-store.d.ts +1 -5
- package/dist/server/sqlite-store.js +3 -40
- package/dist/server/standalone.js +0 -1
- package/package.json +3 -1
|
@@ -101,9 +101,24 @@ export type MaterialWriteResult = {
|
|
|
101
101
|
color: MaterialVector;
|
|
102
102
|
exposed?: boolean;
|
|
103
103
|
}[];
|
|
104
|
+
/** Parameter overrides reset by a FlowIR replacement. */
|
|
105
|
+
diagnostics?: string[];
|
|
104
106
|
status: "applied";
|
|
105
107
|
rendering: "unverified";
|
|
106
108
|
};
|
|
109
|
+
export type MaterialProgramWrite = {
|
|
110
|
+
materialId?: number;
|
|
111
|
+
cloneFrom?: number;
|
|
112
|
+
layerIndex: number;
|
|
113
|
+
source: string;
|
|
114
|
+
objectIds?: number[];
|
|
115
|
+
};
|
|
116
|
+
export type MaterialProgramRead = {
|
|
117
|
+
materialId: number;
|
|
118
|
+
layerIndex: number;
|
|
119
|
+
source: string | null;
|
|
120
|
+
diagnostics: string[];
|
|
121
|
+
};
|
|
107
122
|
export type MaterialNodeOptions = {
|
|
108
123
|
op: string;
|
|
109
124
|
name?: string;
|
|
@@ -4,10 +4,10 @@ export type SceneEditResult = {
|
|
|
4
4
|
} | {
|
|
5
5
|
empty: true;
|
|
6
6
|
};
|
|
7
|
+
/** One scene edit's outcome, held for the invocation that made it. */
|
|
7
8
|
export type SceneOperation = {
|
|
8
9
|
id: string;
|
|
9
10
|
targetKey: string;
|
|
10
|
-
revision: number;
|
|
11
11
|
status: "started" | "prepared" | "committed" | "rolled_back" | "unknown";
|
|
12
12
|
result?: SceneEditResult;
|
|
13
13
|
changed?: boolean;
|
|
@@ -17,23 +17,9 @@ export type SceneOperation = {
|
|
|
17
17
|
};
|
|
18
18
|
completedAt?: string;
|
|
19
19
|
};
|
|
20
|
-
|
|
21
|
-
getSceneOperation(targetKey: string, id: string): Promise<SceneOperation | undefined>;
|
|
22
|
-
createSceneOperation(operation: SceneOperation): Promise<boolean>;
|
|
23
|
-
saveSceneOperation(operation: SceneOperation): Promise<SceneOperation>;
|
|
24
|
-
}
|
|
25
|
-
/** Recover acknowledged/no-op results without replaying or refetching an uncertain edit. */
|
|
26
|
-
export declare function recoverSceneOperation(options: {
|
|
27
|
-
api: KakapoAPI;
|
|
28
|
-
store: SceneOperationStore;
|
|
29
|
-
targetKey: string;
|
|
30
|
-
id: string;
|
|
31
|
-
assertActive(): void | Promise<void>;
|
|
32
|
-
}): Promise<SceneOperation | undefined>;
|
|
33
|
-
/** Hosts may omit persistence; journal-backed hosts retain duplicate-operation protection. */
|
|
20
|
+
/** Applies one edit directly to the scene and reports its outcome; nothing is journaled or replayed. */
|
|
34
21
|
export declare function executeSceneOperation<T>(options: {
|
|
35
22
|
api: KakapoAPI;
|
|
36
|
-
store?: SceneOperationStore;
|
|
37
23
|
onOutcome?(operation: SceneOperation): void;
|
|
38
24
|
operation: Pick<SceneOperation, "id" | "targetKey">;
|
|
39
25
|
assertEditable(): void | Promise<void>;
|
|
@@ -44,5 +30,4 @@ export declare function executeSceneOperation<T>(options: {
|
|
|
44
30
|
}): Promise<{
|
|
45
31
|
value: T;
|
|
46
32
|
operation: SceneOperation;
|
|
47
|
-
reused?: boolean;
|
|
48
33
|
}>;
|
package/dist/scene-operations.js
CHANGED
|
@@ -42,44 +42,15 @@ function resultValue(operation) {
|
|
|
42
42
|
function complete(operation, status) {
|
|
43
43
|
return { ...operation, status, completedAt: operation.completedAt ?? new Date().toISOString() };
|
|
44
44
|
}
|
|
45
|
-
/**
|
|
46
|
-
export async function recoverSceneOperation(options) {
|
|
47
|
-
await options.assertActive();
|
|
48
|
-
const previous = await options.store.getSceneOperation(options.targetKey, options.id);
|
|
49
|
-
if (!previous || previous.status === "committed" || previous.status === "rolled_back")
|
|
50
|
-
return previous;
|
|
51
|
-
await options.assertActive();
|
|
52
|
-
if (previous.changed === false && previous.result) {
|
|
53
|
-
return options.store.saveSceneOperation(complete(previous, "committed"));
|
|
54
|
-
}
|
|
55
|
-
return previous;
|
|
56
|
-
}
|
|
57
|
-
/** Hosts may omit persistence; journal-backed hosts retain duplicate-operation protection. */
|
|
45
|
+
/** Applies one edit directly to the scene and reports its outcome; nothing is journaled or replayed. */
|
|
58
46
|
export async function executeSceneOperation(options) {
|
|
59
|
-
const { api,
|
|
47
|
+
const { api, operation: identity } = options;
|
|
60
48
|
const savesScene = options.saveToRenderer === true;
|
|
61
49
|
await options.assertEditable();
|
|
62
|
-
const previous = await store?.getSceneOperation(identity.targetKey, identity.id);
|
|
63
|
-
if (previous && store) {
|
|
64
|
-
if ((previous.save !== undefined) !== savesScene)
|
|
65
|
-
throw new Error("Scene operation identity was reused with a different save policy");
|
|
66
|
-
if (previous.status === "committed")
|
|
67
|
-
return { value: resultValue(previous), operation: previous, reused: true };
|
|
68
|
-
if (previous.status === "rolled_back")
|
|
69
|
-
throw new Error(previous.error ?? "Scene operation was rolled back");
|
|
70
|
-
const recovered = await recoverSceneOperation({
|
|
71
|
-
api, store, targetKey: identity.targetKey, id: identity.id, assertActive: options.assertEditable,
|
|
72
|
-
});
|
|
73
|
-
if (recovered?.status === "committed")
|
|
74
|
-
return { value: resultValue(recovered), operation: recovered, reused: true };
|
|
75
|
-
throw new Error("Scene edit outcome is uncertain. Inspect the scene before making a new edit; this operation was not replayed.");
|
|
76
|
-
}
|
|
77
50
|
let operation = {
|
|
78
|
-
...identity,
|
|
51
|
+
...identity, status: "started",
|
|
79
52
|
...(savesScene ? { save: { status: "unknown" } } : {}),
|
|
80
53
|
};
|
|
81
|
-
if (store && !await store.createSceneOperation(operation))
|
|
82
|
-
throw new Error("Scene operation is already in progress");
|
|
83
54
|
try {
|
|
84
55
|
const edit = async (draft) => {
|
|
85
56
|
await options.assertEditable();
|
|
@@ -94,8 +65,6 @@ export async function executeSceneOperation(options) {
|
|
|
94
65
|
...operation, status: "prepared", changed,
|
|
95
66
|
...(savesScene && !changed ? { save: { status: "not_needed" } } : {}),
|
|
96
67
|
};
|
|
97
|
-
if (store)
|
|
98
|
-
operation = await store.saveSceneOperation(operation);
|
|
99
68
|
await options.assertEditable();
|
|
100
69
|
if (changed)
|
|
101
70
|
await options.prepareCommit?.(operation);
|
|
@@ -118,16 +87,11 @@ export async function executeSceneOperation(options) {
|
|
|
118
87
|
...(operation.save && status === "rolled_back" ? { save: { status: "not_needed" } } : {}),
|
|
119
88
|
};
|
|
120
89
|
options.onOutcome?.(operation);
|
|
121
|
-
if (store)
|
|
122
|
-
operation = await store.saveSceneOperation(operation);
|
|
123
90
|
}
|
|
124
91
|
catch {
|
|
125
|
-
// Keep the original error
|
|
92
|
+
// Keep the original error.
|
|
126
93
|
}
|
|
127
94
|
throw error;
|
|
128
95
|
}
|
|
129
|
-
// A prepared record alone never proves an uncertain commit succeeded.
|
|
130
|
-
if (store)
|
|
131
|
-
operation = await store.saveSceneOperation(complete(operation, "committed"));
|
|
132
96
|
return { value: resultValue(operation), operation };
|
|
133
97
|
}
|
|
@@ -246,8 +246,7 @@ export function createSdkGeometryHost(options) {
|
|
|
246
246
|
}
|
|
247
247
|
catch (error) {
|
|
248
248
|
if (operationId !== undefined && repairKey !== undefined) {
|
|
249
|
-
|
|
250
|
-
if (operation?.status === "rolled_back")
|
|
249
|
+
if (error instanceof SceneOperationError && error.operation.status === "rolled_back")
|
|
251
250
|
await repairs.refund(repairKey, operationId);
|
|
252
251
|
}
|
|
253
252
|
throw error;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { sparkErrorDiagnostics } from "./error-diagnostics.js";
|
|
2
|
-
import { graphSchema, materialId, parseMaterialCatalog, validateMaterialGraph, writeSchema, value3, } from "./materials/graph.js";
|
|
2
|
+
import { graphSchema, materialId, parseMaterialCatalog, validateMaterialGraph, writeSchema, programWriteSchema, value3, } from "./materials/graph.js";
|
|
3
|
+
import { comparableMaterialGraph, compileMaterialFlowir } from "./materials/flowir.js";
|
|
4
|
+
import { liftMaterialFlowir } from "./materials/lift.js";
|
|
5
|
+
import { boundedDiagnostics } from "./geometry/diagnostics.js";
|
|
3
6
|
function record(value, label) {
|
|
4
7
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
5
8
|
throw new Error(`Invalid ${label}`);
|
|
@@ -115,7 +118,7 @@ export function createSdkMaterialsHost(options) {
|
|
|
115
118
|
],
|
|
116
119
|
};
|
|
117
120
|
};
|
|
118
|
-
|
|
121
|
+
const host = {
|
|
119
122
|
catalog: (settings, context) => options.editor.read(context, async (api) => {
|
|
120
123
|
const nodes = await catalogue(api, context, settings.refreshTextures === true);
|
|
121
124
|
const selected = {};
|
|
@@ -200,7 +203,7 @@ export function createSdkMaterialsHost(options) {
|
|
|
200
203
|
explanation: "A zero queue does not prove compilation, texture loading or the new rendered appearance succeeded. Inspect a scene screenshot.",
|
|
201
204
|
};
|
|
202
205
|
}),
|
|
203
|
-
write: async (input, context) => {
|
|
206
|
+
write: async (input, context, program) => {
|
|
204
207
|
const request = writeSchema.parse(input);
|
|
205
208
|
await options.assertAvailable(context);
|
|
206
209
|
let resultId = request.materialId;
|
|
@@ -211,7 +214,8 @@ export function createSdkMaterialsHost(options) {
|
|
|
211
214
|
}, async (api, draft) => {
|
|
212
215
|
await options.assertAvailable(context);
|
|
213
216
|
context.signal.throwIfAborted();
|
|
214
|
-
if (
|
|
217
|
+
if (program === undefined &&
|
|
218
|
+
request.materialId !== undefined &&
|
|
215
219
|
request.layers === undefined &&
|
|
216
220
|
request.constants === undefined) {
|
|
217
221
|
draft.getMaterial(request.materialId);
|
|
@@ -226,12 +230,58 @@ export function createSdkMaterialsHost(options) {
|
|
|
226
230
|
const catalog = await catalogue(api, context, false);
|
|
227
231
|
const sourceId = request.materialId ?? request.cloneFrom;
|
|
228
232
|
const original = sourceId === undefined ? undefined : rawMaterial(draft, sourceId);
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
let
|
|
233
|
+
const sourceLayers = sourceId === undefined ? [] : layers(draft, sourceId);
|
|
234
|
+
const proposedLayers = program ? [...sourceLayers] : request.layers ?? sourceLayers;
|
|
235
|
+
let programSymbols;
|
|
236
|
+
const programDiagnostics = [];
|
|
237
|
+
if (program) {
|
|
238
|
+
while (proposedLayers.length <= program.layerIndex)
|
|
239
|
+
proposedLayers.push(null);
|
|
240
|
+
const compiled = compileMaterialFlowir(program.source, catalog);
|
|
241
|
+
const replacement = compiled.graph;
|
|
242
|
+
programSymbols = compiled.symbols;
|
|
243
|
+
const previous = sourceLayers[program.layerIndex];
|
|
244
|
+
for (const node of replacement.nodes) {
|
|
245
|
+
const matches = previous?.nodes.filter(candidate => candidate.op === node.op && candidate.name === node.name) ?? [];
|
|
246
|
+
const old = matches.length === 1 ? matches[0] : undefined;
|
|
247
|
+
if (!old || node.name === undefined)
|
|
248
|
+
continue;
|
|
249
|
+
if (old.location !== undefined)
|
|
250
|
+
node.location = old.location;
|
|
251
|
+
if (old.editor_flags !== undefined)
|
|
252
|
+
node.editor_flags = old.editor_flags;
|
|
253
|
+
if (node.op !== "Constant")
|
|
254
|
+
continue;
|
|
255
|
+
const exposure = node.in3.x;
|
|
256
|
+
const oldExposure = old.in3?.x ?? 0;
|
|
257
|
+
const nodeSlot = replacement.connections.find(edge => edge.input_node === replacement.nodes.indexOf(node))?.input_slot;
|
|
258
|
+
const oldSlot = previous?.connections.find(edge => edge.input_node === previous.nodes.indexOf(old))?.input_slot;
|
|
259
|
+
const vectorWidget = exposure === 1 && oldExposure === 2 && oldSlot === 1;
|
|
260
|
+
const compatible = (nodeSlot === oldSlot && exposure === oldExposure) || vectorWidget;
|
|
261
|
+
if (compatible) {
|
|
262
|
+
if (vectorWidget) {
|
|
263
|
+
node.in1 = node.in2;
|
|
264
|
+
delete node.in2;
|
|
265
|
+
for (const edge of replacement.connections)
|
|
266
|
+
if (edge.input_node === replacement.nodes.indexOf(node) && edge.input_slot === 2)
|
|
267
|
+
edge.input_slot = 1;
|
|
268
|
+
}
|
|
269
|
+
if (old.reference_id !== undefined)
|
|
270
|
+
node.reference_id = old.reference_id;
|
|
271
|
+
if (old.in3 !== undefined)
|
|
272
|
+
node.in3 = old.in3;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
for (const option of previous?.nodes.filter(node => node.op === "Options") ?? [])
|
|
276
|
+
replacement.nodes.push(option);
|
|
277
|
+
proposedLayers[program.layerIndex] = replacement;
|
|
278
|
+
}
|
|
279
|
+
const graphs = proposedLayers.map((graph, slot) => graph === null ? null : program && slot !== program.layerIndex ? graph : validateMaterialGraph(graph, catalog));
|
|
280
|
+
let rewriteGraphs = program !== undefined || request.layers !== undefined || request.materialId === undefined;
|
|
232
281
|
const used = allReferences(draft);
|
|
233
282
|
const refs = new Map();
|
|
234
|
-
|
|
283
|
+
const changedGraphs = program && request.materialId !== undefined ? [graphs[program.layerIndex]] : graphs;
|
|
284
|
+
for (const graph of changedGraphs)
|
|
235
285
|
for (const node of graph?.nodes ?? [])
|
|
236
286
|
if (node.op === "Constant") {
|
|
237
287
|
const id = typeof node.reference_id === "number" && node.reference_id > 0
|
|
@@ -249,6 +299,18 @@ export function createSdkMaterialsHost(options) {
|
|
|
249
299
|
? record(original.data, "material data")
|
|
250
300
|
: undefined;
|
|
251
301
|
const constants = record(sourceData?.constants ?? {}, "material constants");
|
|
302
|
+
if (program) {
|
|
303
|
+
const retained = new Set(graphs.flatMap(graph => graph?.nodes.filter(node => node.op === "Constant").map(node => String(node.reference_id)) ?? []));
|
|
304
|
+
for (const node of sourceLayers[program.layerIndex]?.nodes ?? []) {
|
|
305
|
+
if (node.op !== "Constant" || retained.has(String(node.reference_id)))
|
|
306
|
+
continue;
|
|
307
|
+
const key = String(node.reference_id);
|
|
308
|
+
if (constants[key] !== undefined) {
|
|
309
|
+
delete constants[key];
|
|
310
|
+
programDiagnostics.push(`Reset override for ${String(node.name ?? key)}: parameter removed or type changed.`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
252
314
|
const referenceIdChanges = {};
|
|
253
315
|
for (const [key, override] of Object.entries(request.constants ?? {})) {
|
|
254
316
|
const node = refs.get(Number(key));
|
|
@@ -264,7 +326,7 @@ export function createSdkMaterialsHost(options) {
|
|
|
264
326
|
// Replaced graphs can reuse IDs whose deleted overrides remain in the native cache.
|
|
265
327
|
if (request.constants?.[key] === null ||
|
|
266
328
|
(request.materialId !== undefined &&
|
|
267
|
-
request.layers !== undefined &&
|
|
329
|
+
(request.layers !== undefined || program !== undefined) &&
|
|
268
330
|
constants[key] === undefined)) {
|
|
269
331
|
const id = freshReference(used);
|
|
270
332
|
rewriteGraphs = true;
|
|
@@ -286,6 +348,8 @@ export function createSdkMaterialsHost(options) {
|
|
|
286
348
|
const slots = graphs.map((graph, slot) => {
|
|
287
349
|
if (graph === null)
|
|
288
350
|
return 0;
|
|
351
|
+
if (program && request.materialId !== undefined && slot !== program.layerIndex)
|
|
352
|
+
return materialId.parse(oldSlots[slot]);
|
|
289
353
|
if (!rewriteGraphs)
|
|
290
354
|
return materialId.parse(oldSlots[slot]);
|
|
291
355
|
const oldId = oldSlots[slot];
|
|
@@ -310,6 +374,30 @@ export function createSdkMaterialsHost(options) {
|
|
|
310
374
|
});
|
|
311
375
|
while (slots.length < 8)
|
|
312
376
|
slots.push(0);
|
|
377
|
+
if (program) {
|
|
378
|
+
const ext = draft.getSceneExt();
|
|
379
|
+
if (!ext.shadersExt)
|
|
380
|
+
draft.patchSceneExt([{ op: "add", path: "/shadersExt", value: {} }], { returnScene: false });
|
|
381
|
+
const previousMetadata = ext.shadersExt;
|
|
382
|
+
if (request.cloneFrom !== undefined)
|
|
383
|
+
for (let slot = 0; slot < slots.length; slot++) {
|
|
384
|
+
if (slot === program.layerIndex || !slots[slot])
|
|
385
|
+
continue;
|
|
386
|
+
const originalShader = oldSlots[slot];
|
|
387
|
+
const metadata = previousMetadata?.[String(originalShader)];
|
|
388
|
+
if (metadata !== undefined)
|
|
389
|
+
draft.patchSceneExt([{ op: "add", path: `/shadersExt/${slots[slot]}`, value: metadata }], { returnScene: false });
|
|
390
|
+
}
|
|
391
|
+
draft.patchSceneExt([{
|
|
392
|
+
op: "add",
|
|
393
|
+
path: `/shadersExt/${slots[program.layerIndex]}`,
|
|
394
|
+
value: {
|
|
395
|
+
...record(previousMetadata?.[String(oldSlots[program.layerIndex])] ?? {}, "shader metadata"),
|
|
396
|
+
flowirSource: program.source,
|
|
397
|
+
flowirSymbols: programSymbols,
|
|
398
|
+
},
|
|
399
|
+
}], { returnScene: false });
|
|
400
|
+
}
|
|
313
401
|
patchEntry(draft, "materials", resultId, {
|
|
314
402
|
...target,
|
|
315
403
|
...(original ?? {}),
|
|
@@ -339,7 +427,8 @@ export function createSdkMaterialsHost(options) {
|
|
|
339
427
|
materialId: resultId,
|
|
340
428
|
objectIds: directUsers(draft, resultId),
|
|
341
429
|
referenceIdChanges,
|
|
342
|
-
|
|
430
|
+
...(program ? { diagnostics: boundedDiagnostics(programDiagnostics) } : {}),
|
|
431
|
+
parameters: (program ? changedGraphs : graphs).flatMap(graph => graph?.nodes.filter(node => node.op === "Constant").map(node => {
|
|
343
432
|
const referenceId = node.reference_id;
|
|
344
433
|
const override = constants[String(referenceId)];
|
|
345
434
|
return { name: node.name ?? "", referenceId,
|
|
@@ -356,5 +445,42 @@ export function createSdkMaterialsHost(options) {
|
|
|
356
445
|
rendering: "unverified",
|
|
357
446
|
};
|
|
358
447
|
},
|
|
448
|
+
writeProgram: async (input, context) => {
|
|
449
|
+
const request = programWriteSchema.parse(input);
|
|
450
|
+
return host.write({
|
|
451
|
+
...(request.materialId === undefined ? {} : { materialId: request.materialId }),
|
|
452
|
+
...(request.cloneFrom === undefined ? {} : { cloneFrom: request.cloneFrom }),
|
|
453
|
+
...(request.objectIds === undefined ? {} : { objectIds: request.objectIds }),
|
|
454
|
+
}, context, request);
|
|
455
|
+
},
|
|
456
|
+
readProgram: async (material, layerIndex, context) => options.editor.read(context, async (api) => {
|
|
457
|
+
await options.assertAvailable(context);
|
|
458
|
+
context.signal.throwIfAborted();
|
|
459
|
+
const id = materialId.parse(material);
|
|
460
|
+
if (!Number.isInteger(layerIndex) || layerIndex < 0 || layerIndex > 7)
|
|
461
|
+
throw new Error("layerIndex must be within 0..7");
|
|
462
|
+
const materialData = rawMaterial(api, id);
|
|
463
|
+
const shaderId = Array.isArray(materialData.shaders) ? materialData.shaders[layerIndex] : undefined;
|
|
464
|
+
if (typeof shaderId !== "number" || shaderId <= 0)
|
|
465
|
+
return { materialId: id, layerIndex, source: null, diagnostics: ["This layer is empty."] };
|
|
466
|
+
const metadata = api.getSceneExt().shadersExt?.[String(shaderId)];
|
|
467
|
+
const source = metadata?.flowirSource;
|
|
468
|
+
const catalog = await catalogue(api, context, false);
|
|
469
|
+
const actual = layers(api, id)[layerIndex];
|
|
470
|
+
if (!actual)
|
|
471
|
+
return { materialId: id, layerIndex, source: null, diagnostics: ["This layer is empty."] };
|
|
472
|
+
try {
|
|
473
|
+
if (typeof source === "string") {
|
|
474
|
+
const rebuilt = compileMaterialFlowir(source, catalog).graph;
|
|
475
|
+
if (comparableMaterialGraph(rebuilt) === comparableMaterialGraph(actual))
|
|
476
|
+
return { materialId: id, layerIndex, source, diagnostics: [] };
|
|
477
|
+
}
|
|
478
|
+
return { materialId: id, layerIndex, source: liftMaterialFlowir(actual, catalog), diagnostics: [] };
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
return { materialId: id, layerIndex, source: null, diagnostics: boundedDiagnostics([sparkErrorDiagnostics(error).message]) };
|
|
482
|
+
}
|
|
483
|
+
}),
|
|
359
484
|
};
|
|
485
|
+
return host;
|
|
360
486
|
}
|
|
@@ -238,9 +238,7 @@ export function createSdkOpenScadHost(options) {
|
|
|
238
238
|
}
|
|
239
239
|
catch (error) {
|
|
240
240
|
if (creating === false && operationId !== "") {
|
|
241
|
-
|
|
242
|
-
? error.operation : await editor.getOperation(operationId);
|
|
243
|
-
if (operation?.status === "rolled_back") {
|
|
241
|
+
if (error instanceof SceneOperationError && error.operation.status === "rolled_back") {
|
|
244
242
|
await geometryRepairs.refund(resolved.repairKey, operationId);
|
|
245
243
|
await printabilityRepairs.refund(resolved.repairKey, operationId);
|
|
246
244
|
}
|
package/dist/sdk-scene-host.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FontFamily, KakapoAPI, SceneEdit, MaterialResource, NativeSceneSnapshot } from "@womp/kakapo-sdk";
|
|
2
2
|
import type { ArtifactRef, SparkInvocationContext } from "./capabilities.js";
|
|
3
3
|
import type { SceneHost, SceneHealthHost, SceneScreenshotOptions } from "./capabilities/scene.js";
|
|
4
|
-
import { type SceneOperation
|
|
4
|
+
import { type SceneOperation } from "./scene-operations.js";
|
|
5
5
|
import type { SceneHdr } from "./scene-environment.js";
|
|
6
6
|
export type SdkSceneServices = {
|
|
7
7
|
fonts: FontFamily[];
|
|
@@ -19,7 +19,6 @@ export type SdkSceneServices = {
|
|
|
19
19
|
export type SdkSceneEditorOptions = {
|
|
20
20
|
targetKey: string;
|
|
21
21
|
projectId?: number;
|
|
22
|
-
operations?: SceneOperationStore;
|
|
23
22
|
saveToRenderer?: boolean;
|
|
24
23
|
withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void,
|
|
25
24
|
/** Supply this when the host adds a stricter connection lifetime. */
|
|
@@ -27,7 +26,7 @@ export type SdkSceneEditorOptions = {
|
|
|
27
26
|
assertEditable(context: SparkInvocationContext): void | Promise<void>;
|
|
28
27
|
/** Suspend for the complete access, including reads and failed preparation. */
|
|
29
28
|
acquireAccess?(context: SparkInvocationContext): Promise<(synchronized: boolean) => void | Promise<void>>;
|
|
30
|
-
/** Must adopt each operation ID at most once
|
|
29
|
+
/** Must adopt each operation ID at most once. */
|
|
31
30
|
onCommitted?(operation: SceneOperation, api: KakapoAPI, context: SparkInvocationContext, identity: SdkSceneEditIdentity, scene: NativeSceneSnapshot): Promise<Record<string, unknown> | void>;
|
|
32
31
|
};
|
|
33
32
|
export type SdkSceneEditIdentity = {
|
|
@@ -35,13 +34,12 @@ export type SdkSceneEditIdentity = {
|
|
|
35
34
|
operation: string;
|
|
36
35
|
target?: Record<string, unknown> | (() => Record<string, unknown>);
|
|
37
36
|
};
|
|
38
|
-
/**
|
|
37
|
+
/** A failed edit's outcome, for callers that settle per-edit state such as repair budgets. */
|
|
39
38
|
export declare class SceneOperationError extends Error {
|
|
40
39
|
readonly operation: SceneOperation;
|
|
41
40
|
constructor(operation: SceneOperation, cause: unknown);
|
|
42
41
|
}
|
|
43
42
|
export type SdkSceneEditor = {
|
|
44
|
-
getOperation(operationId: string): Promise<SceneOperation | undefined>;
|
|
45
43
|
withConnection<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
|
|
46
44
|
read<T>(context: SparkInvocationContext, action: (api: KakapoAPI, beginSettlement: () => void) => T | Promise<T>): Promise<T>;
|
|
47
45
|
access<T>(identity: SdkSceneEditIdentity, callback: (api: KakapoAPI, draft: SceneEdit, operationId: string) => T | Promise<T>, context: SparkInvocationContext, afterCommitted?: (operation: SceneOperation, api: KakapoAPI) => Promise<Record<string, unknown> | void>): Promise<{
|
package/dist/sdk-scene-host.js
CHANGED
|
@@ -10,7 +10,7 @@ function assertSceneSaved(operation) {
|
|
|
10
10
|
if (operation.save?.status === "unknown" || operation.save?.status === "requested")
|
|
11
11
|
throw new Error("The scene edit was applied, but its save outcome is unknown. Inspect persistence before continuing; do not repeat the edit.");
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
13
|
+
/** A failed edit's outcome, for callers that settle per-edit state such as repair budgets. */
|
|
14
14
|
export class SceneOperationError extends Error {
|
|
15
15
|
operation;
|
|
16
16
|
constructor(operation, cause) {
|
|
@@ -74,7 +74,6 @@ export function createSdkSceneDraft(api, scene, services) {
|
|
|
74
74
|
export function createSdkSceneEditor(options) {
|
|
75
75
|
const ordinals = new WeakMap();
|
|
76
76
|
return {
|
|
77
|
-
getOperation: (operationId) => options.operations?.getSceneOperation(options.targetKey, operationId) ?? Promise.resolve(undefined),
|
|
78
77
|
withConnection: (context, action) => options.withConnection(context, async (api, settle, active = context) => {
|
|
79
78
|
active.signal.throwIfAborted();
|
|
80
79
|
return action(api, settle);
|
|
@@ -131,7 +130,6 @@ export function createSdkSceneEditor(options) {
|
|
|
131
130
|
result: operation.result,
|
|
132
131
|
save: operation.save,
|
|
133
132
|
error: operation.error,
|
|
134
|
-
...(options.operations ? { synchronized } : {}),
|
|
135
133
|
}),
|
|
136
134
|
});
|
|
137
135
|
};
|
|
@@ -142,7 +140,6 @@ export function createSdkSceneEditor(options) {
|
|
|
142
140
|
try {
|
|
143
141
|
result = await executeSceneOperation({
|
|
144
142
|
api,
|
|
145
|
-
store: options.operations,
|
|
146
143
|
onOutcome: operation => { outcome = operation; },
|
|
147
144
|
saveToRenderer: options.saveToRenderer,
|
|
148
145
|
operation: {
|
|
@@ -168,10 +165,9 @@ export function createSdkSceneEditor(options) {
|
|
|
168
165
|
catch (error) {
|
|
169
166
|
// Receipt persistence must not replace the original SDK/tool error.
|
|
170
167
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
await record(operation);
|
|
168
|
+
if (outcome) {
|
|
169
|
+
synchronized = outcome.status === "rolled_back";
|
|
170
|
+
await record(outcome);
|
|
175
171
|
}
|
|
176
172
|
}
|
|
177
173
|
catch {
|
|
@@ -180,12 +176,6 @@ export function createSdkSceneEditor(options) {
|
|
|
180
176
|
throw error;
|
|
181
177
|
}
|
|
182
178
|
saveSettled = !result.operation.changed || !["unknown", "requested"].includes(result.operation.save?.status ?? "");
|
|
183
|
-
// A duplicate returns its journaled result; retain its original receipt
|
|
184
|
-
// and let explicit recovery handle any earlier synchronization failure.
|
|
185
|
-
if (result.reused) {
|
|
186
|
-
assertSceneSaved(result.operation);
|
|
187
|
-
return result;
|
|
188
|
-
}
|
|
189
179
|
synchronized = !result.operation.changed || !options.onCommitted;
|
|
190
180
|
await record(result.operation);
|
|
191
181
|
if (!synchronized) {
|
|
@@ -216,7 +206,7 @@ export function createSdkSceneEditor(options) {
|
|
|
216
206
|
return result;
|
|
217
207
|
}
|
|
218
208
|
catch (error) {
|
|
219
|
-
if (
|
|
209
|
+
if (outcome)
|
|
220
210
|
throw new SceneOperationError(outcome, error);
|
|
221
211
|
throw error;
|
|
222
212
|
}
|
|
@@ -2,11 +2,10 @@ import type { SparkGoal } from "../goals.js";
|
|
|
2
2
|
import type { ExecutionScreenshot } from "../types.js";
|
|
3
3
|
import type { SparkArtifact, StoredSparkChunk } from "../ui.js";
|
|
4
4
|
import type { SparkArtifactStore, StoredSparkArtifact } from "./artifacts.js";
|
|
5
|
-
import type { SceneOperation, SceneOperationStore } from "../scene-operations.js";
|
|
6
5
|
import { type RepairStateStore } from "../repair-state.js";
|
|
7
6
|
import type { SparkAgentStore, SparkConversation, SparkConversationSummary, SparkRun, SparkRunSettings } from "./types.js";
|
|
8
7
|
/** Local development storage. Execution ownership belongs to the host service. */
|
|
9
|
-
export declare class SqliteSparkStore implements SparkAgentStore, SparkArtifactStore
|
|
8
|
+
export declare class SqliteSparkStore implements SparkAgentStore, SparkArtifactStore {
|
|
10
9
|
private readonly artifactBaseUrl;
|
|
11
10
|
private readonly database;
|
|
12
11
|
private ownershipToken;
|
|
@@ -15,9 +14,6 @@ export declare class SqliteSparkStore implements SparkAgentStore, SparkArtifactS
|
|
|
15
14
|
close(): void;
|
|
16
15
|
claimExecutionOwnership(): void;
|
|
17
16
|
listInterruptedRuns(): Promise<SparkRun[]>;
|
|
18
|
-
getSceneOperation(targetKey: string, id: string): Promise<SceneOperation | undefined>;
|
|
19
|
-
createSceneOperation(operation: SceneOperation): Promise<boolean>;
|
|
20
|
-
saveSceneOperation(operation: SceneOperation): Promise<SceneOperation>;
|
|
21
17
|
createConversation(value: SparkConversation): Promise<void>;
|
|
22
18
|
getConversation(id: string): Promise<SparkConversation | undefined>;
|
|
23
19
|
listConversations(ownerId: string): Promise<SparkConversationSummary[]>;
|
|
@@ -2,7 +2,6 @@ import { mkdirSync } from "node:fs";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
4
|
import { emptyRepairState, } from "../repair-state.js";
|
|
5
|
-
const completedSceneOperationLimit = 128;
|
|
6
5
|
/** Local development storage. Execution ownership belongs to the host service. */
|
|
7
6
|
export class SqliteSparkStore {
|
|
8
7
|
artifactBaseUrl;
|
|
@@ -13,7 +12,7 @@ export class SqliteSparkStore {
|
|
|
13
12
|
mkdirSync(dirname(path), { recursive: true });
|
|
14
13
|
this.database = new DatabaseSync(path);
|
|
15
14
|
const version = this.database.prepare("PRAGMA user_version").get()?.user_version;
|
|
16
|
-
if (version !== 0 && version !== 1 && version !== 2 && version !== 3 && version !== 4) {
|
|
15
|
+
if (version !== 0 && version !== 1 && version !== 2 && version !== 3 && version !== 4 && version !== 5) {
|
|
17
16
|
this.database.close();
|
|
18
17
|
throw new Error(`Unsupported Spark database version: ${version}`);
|
|
19
18
|
}
|
|
@@ -39,14 +38,11 @@ export class SqliteSparkStore {
|
|
|
39
38
|
CREATE TABLE IF NOT EXISTS execution_owner (
|
|
40
39
|
singleton INTEGER PRIMARY KEY CHECK (singleton = 1), pid INTEGER NOT NULL, token TEXT NOT NULL
|
|
41
40
|
);
|
|
42
|
-
|
|
43
|
-
target_key TEXT NOT NULL, id TEXT NOT NULL, revision INTEGER NOT NULL, data TEXT NOT NULL,
|
|
44
|
-
PRIMARY KEY (target_key, id)
|
|
45
|
-
);
|
|
41
|
+
DROP TABLE IF EXISTS scene_operations;
|
|
46
42
|
CREATE TABLE IF NOT EXISTS repair_states (
|
|
47
43
|
target_key TEXT PRIMARY KEY, revision INTEGER NOT NULL, data TEXT NOT NULL
|
|
48
44
|
);
|
|
49
|
-
PRAGMA user_version =
|
|
45
|
+
PRAGMA user_version = 5;
|
|
50
46
|
`);
|
|
51
47
|
}
|
|
52
48
|
repairStateStore(targetKey) {
|
|
@@ -111,39 +107,6 @@ export class SqliteSparkStore {
|
|
|
111
107
|
throw new Error("Claim execution ownership before recovering runs");
|
|
112
108
|
return this.database.prepare("SELECT data FROM runs WHERE json_extract(data, '$.status') IN ('queued', 'running')").all().map((row) => JSON.parse(String(row.data)));
|
|
113
109
|
}
|
|
114
|
-
async getSceneOperation(targetKey, id) {
|
|
115
|
-
const row = this.database.prepare("SELECT data FROM scene_operations WHERE target_key = ? AND id = ?").get(targetKey, id);
|
|
116
|
-
return row ? JSON.parse(String(row.data)) : undefined;
|
|
117
|
-
}
|
|
118
|
-
async createSceneOperation(operation) {
|
|
119
|
-
const result = this.database.prepare("INSERT OR IGNORE INTO scene_operations VALUES (?, ?, ?, ?)").run(operation.targetKey, operation.id, operation.revision, JSON.stringify(operation));
|
|
120
|
-
return result.changes === 1;
|
|
121
|
-
}
|
|
122
|
-
async saveSceneOperation(operation) {
|
|
123
|
-
const next = { ...operation, revision: operation.revision + 1 };
|
|
124
|
-
this.database.exec("BEGIN IMMEDIATE");
|
|
125
|
-
try {
|
|
126
|
-
const result = this.database.prepare("UPDATE scene_operations SET revision = ?, data = ? WHERE target_key = ? AND id = ? AND revision = ?").run(next.revision, JSON.stringify(next), operation.targetKey, operation.id, operation.revision);
|
|
127
|
-
if (result.changes !== 1)
|
|
128
|
-
throw new Error("Scene operation changed concurrently or is missing");
|
|
129
|
-
if (next.status === "committed" || next.status === "rolled_back") {
|
|
130
|
-
this.database.prepare(`
|
|
131
|
-
DELETE FROM scene_operations WHERE rowid IN (
|
|
132
|
-
SELECT rowid FROM scene_operations
|
|
133
|
-
WHERE target_key = ? AND json_extract(data, '$.status') IN ('committed', 'rolled_back')
|
|
134
|
-
ORDER BY COALESCE(json_extract(data, '$.completedAt'), '') DESC
|
|
135
|
-
LIMIT -1 OFFSET ?
|
|
136
|
-
)
|
|
137
|
-
`).run(next.targetKey, completedSceneOperationLimit);
|
|
138
|
-
}
|
|
139
|
-
this.database.exec("COMMIT");
|
|
140
|
-
return next;
|
|
141
|
-
}
|
|
142
|
-
catch (error) {
|
|
143
|
-
this.database.exec("ROLLBACK");
|
|
144
|
-
throw error;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
110
|
async createConversation(value) {
|
|
148
111
|
this.database.prepare("INSERT INTO conversations VALUES (?, ?, ?, ?)")
|
|
149
112
|
.run(value.id, value.ownerId, value.updatedAt, JSON.stringify(value));
|
|
@@ -102,7 +102,6 @@ export async function createStandaloneSparkAgentApp(options) {
|
|
|
102
102
|
const targetKey = `${context.ownerId}:${sceneIdentity}`;
|
|
103
103
|
const editorOptions = {
|
|
104
104
|
targetKey,
|
|
105
|
-
operations: store,
|
|
106
105
|
saveToRenderer: true,
|
|
107
106
|
withConnection: (invocation, action) => sceneAccess.run(invocation.signal, async (signal, beginSettlement) => {
|
|
108
107
|
if (!api.isConnected)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@womp/spark-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.1.0",
|
|
4
4
|
"description": "Portable skills-based AI agent with local and durable chat transports for Womp",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"dist/**/*.d.ts",
|
|
75
75
|
"dist/server/fixtures/**/*.json",
|
|
76
76
|
"dist/geometry/schemas/**/*.json",
|
|
77
|
+
"dist/materials/schemas/**/*.json",
|
|
77
78
|
"README.md",
|
|
78
79
|
"CHANGELOG.md"
|
|
79
80
|
],
|
|
@@ -105,6 +106,7 @@
|
|
|
105
106
|
"web:build": "yarn build && tsc --noEmit -p web/tsconfig.json && vite build --config web/vite.config.ts",
|
|
106
107
|
"test": "yarn build && tsx --tsconfig tsconfig.json --test test/**/*.test.ts && node --input-type=module -e \"await Promise.all([import('@womp/spark-agent'), import('@womp/spark-agent/ui'), import('@womp/spark-agent/client'), import('@womp/spark-agent/server'), import('@womp/spark-agent/express'), import('@womp/spark-agent/kakapo'), import('@womp/spark-agent/geometry'), import('@womp/spark-agent/openscad')])\"",
|
|
107
108
|
"test:live": "tsx --tsconfig tsconfig.json test/womp-scene-api.live.ts",
|
|
109
|
+
"test:live:materials": "tsx --tsconfig tsconfig.json test/materials-flowir.live.ts",
|
|
108
110
|
"prepack": "yarn build",
|
|
109
111
|
"release:pack": "node scripts/npm-release.mjs pack"
|
|
110
112
|
},
|