@compilr-dev/sdk 0.33.1 → 0.35.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.
@@ -6,6 +6,7 @@ export * from './types.js';
6
6
  export * from './validate.js';
7
7
  export * from './scene.js';
8
8
  export * from './scene-ops.js';
9
+ export * from './scene-layers.js';
9
10
  export { parseColor, relativeLuminance, contrastRatio, oklabLightness, lightnessDeltaPct, isNeutral, hueAngle, AA_TEXT, SURFACE_DELTA_MIN, SURFACE_DELTA_TARGET, SURFACE_DELTA_MAX, HAIRLINE_MIN_PCT, HAIRLINE_MAX_PCT, } from './color.js';
10
11
  export type { Rgb } from './color.js';
11
12
  export { runQualityChecks, checkSurfaceTokens, checkTextContrast, checkHueCount, checkDeadTweaks, primaryVarNames, } from './quality-checks.js';
@@ -11,6 +11,7 @@ export * from './validate.js';
11
11
  */
12
12
  export * from './scene.js';
13
13
  export * from './scene-ops.js';
14
+ export * from './scene-layers.js';
14
15
  /*
15
16
  ⚠️ Renderer-safe on purpose. Both modules are pure arithmetic with no imports beyond
16
17
  each other, so Desktop's renderer can use them for the surface ramp without pulling the
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Scene layers → the Tweaks panel (3d-canvas-spec §18).
3
+ *
4
+ * A scene's control manifest is DERIVED from `scene.layers`, never stored on the canvas row:
5
+ * one source of truth, so a toggle cannot drift from the scene that declares it. What IS stored
6
+ * is the row's `values` — `layer_exterior: false` — because which layers are showing is the
7
+ * user's view state, not a change to the model. That split is the whole point of §18.2: hiding a
8
+ * wall to look inside must not bump the rev, enter the undo journal, or tell the agent the user
9
+ * edited the scene.
10
+ */
11
+ import type { ControlManifest, ParamValue } from './types.js';
12
+ import type { SceneFile, SceneObject } from './scene.js';
13
+ /** The panel group every layer toggle sits in. */
14
+ export declare const LAYER_GROUP = "Layers";
15
+ /**
16
+ * `exterior` → `layer_exterior`, `load-bearing` → `layer_load_bearing`.
17
+ *
18
+ * ⚠️ `PARAM_RE` (canvas/validate.ts) is `/^[a-zA-Z_][a-zA-Z0-9_]*$/`, so the obvious
19
+ * `layer:exterior` fails manifest validation — hence the prefix and the `-` → `_` mapping. Tags
20
+ * forbid underscores (`SCENE_TAG_PATTERN`) precisely so this stays reversible: without that,
21
+ * `load-bearing` and `load_bearing` would both land on `layer_load_bearing`.
22
+ */
23
+ export declare function layerParam(tag: string): string;
24
+ /** The inverse of `layerParam`; null for any other param. Unambiguous — see the note there. */
25
+ export declare function layerTagFromParam(param: string): string | null;
26
+ /** One toggle per declared layer, in declaration order. Empty when the scene declares none. */
27
+ export declare function sceneControlManifest(scene: SceneFile): ControlManifest;
28
+ /** Does this scene have anything to tweak? Drives whether the host shows the panel at all. */
29
+ export declare function sceneHasLayers(scene: SceneFile): boolean;
30
+ /** Every tag actually carried by an object, in first-seen order. */
31
+ export declare function sceneTags(scene: SceneFile): string[];
32
+ /**
33
+ * Tags objects carry that no layer declares — metadata, not controls.
34
+ *
35
+ * Reported by `scene_get` so the agent can see that a label it has been applying is not yet
36
+ * togglable, rather than wondering why the user has no switch for it.
37
+ */
38
+ export declare function undeclaredTags(scene: SceneFile): string[];
39
+ /** Layers declared with no object carrying the tag — a control that governs nothing, yet. */
40
+ export declare function emptyLayers(scene: SceneFile): string[];
41
+ /**
42
+ * The tags currently switched OFF, from the row's values.
43
+ *
44
+ * A layer with no entry in `values` falls back to its declared default, so a scene read before
45
+ * the user has touched anything hides exactly what the agent asked to be hidden.
46
+ */
47
+ export declare function hiddenTags(scene: SceneFile, values: Record<string, ParamValue> | undefined): Set<string>;
48
+ /**
49
+ * Is this object hidden in its own right?
50
+ *
51
+ * Only its OWN tags: a hidden ancestor hides its subtree through the renderer's node tree
52
+ * (three.js propagates `visible`), so walking the ancestry here would double-count and would
53
+ * disagree with what is drawn.
54
+ */
55
+ export declare function isTagHidden(object: SceneObject, hidden: ReadonlySet<string>): boolean;
56
+ /** One line for `scene_get`'s header: `exterior (on), glazing (off)`, or null when there are none. */
57
+ export declare function describeLayers(scene: SceneFile, values?: Record<string, ParamValue>): string | null;
@@ -0,0 +1,94 @@
1
+ /** The panel group every layer toggle sits in. */
2
+ export const LAYER_GROUP = 'Layers';
3
+ /**
4
+ * `exterior` → `layer_exterior`, `load-bearing` → `layer_load_bearing`.
5
+ *
6
+ * ⚠️ `PARAM_RE` (canvas/validate.ts) is `/^[a-zA-Z_][a-zA-Z0-9_]*$/`, so the obvious
7
+ * `layer:exterior` fails manifest validation — hence the prefix and the `-` → `_` mapping. Tags
8
+ * forbid underscores (`SCENE_TAG_PATTERN`) precisely so this stays reversible: without that,
9
+ * `load-bearing` and `load_bearing` would both land on `layer_load_bearing`.
10
+ */
11
+ export function layerParam(tag) {
12
+ return `layer_${tag.replace(/-/g, '_')}`;
13
+ }
14
+ /** The inverse of `layerParam`; null for any other param. Unambiguous — see the note there. */
15
+ export function layerTagFromParam(param) {
16
+ if (!param.startsWith('layer_'))
17
+ return null;
18
+ const tag = param.slice('layer_'.length).replace(/_/g, '-');
19
+ return tag.length > 0 ? tag : null;
20
+ }
21
+ /** One toggle per declared layer, in declaration order. Empty when the scene declares none. */
22
+ export function sceneControlManifest(scene) {
23
+ const controls = (scene.layers ?? []).map((l) => ({
24
+ type: 'toggle',
25
+ param: layerParam(l.tag),
26
+ label: l.label,
27
+ default: l.visible ?? true,
28
+ group: LAYER_GROUP,
29
+ }));
30
+ return { controls };
31
+ }
32
+ /** Does this scene have anything to tweak? Drives whether the host shows the panel at all. */
33
+ export function sceneHasLayers(scene) {
34
+ return (scene.layers ?? []).length > 0;
35
+ }
36
+ /** Every tag actually carried by an object, in first-seen order. */
37
+ export function sceneTags(scene) {
38
+ const out = [];
39
+ for (const o of scene.objects) {
40
+ for (const t of o.tags ?? [])
41
+ if (!out.includes(t))
42
+ out.push(t);
43
+ }
44
+ return out;
45
+ }
46
+ /**
47
+ * Tags objects carry that no layer declares — metadata, not controls.
48
+ *
49
+ * Reported by `scene_get` so the agent can see that a label it has been applying is not yet
50
+ * togglable, rather than wondering why the user has no switch for it.
51
+ */
52
+ export function undeclaredTags(scene) {
53
+ const declared = new Set((scene.layers ?? []).map((l) => l.tag));
54
+ return sceneTags(scene).filter((t) => !declared.has(t));
55
+ }
56
+ /** Layers declared with no object carrying the tag — a control that governs nothing, yet. */
57
+ export function emptyLayers(scene) {
58
+ const used = new Set(sceneTags(scene));
59
+ return (scene.layers ?? []).map((l) => l.tag).filter((t) => !used.has(t));
60
+ }
61
+ /**
62
+ * The tags currently switched OFF, from the row's values.
63
+ *
64
+ * A layer with no entry in `values` falls back to its declared default, so a scene read before
65
+ * the user has touched anything hides exactly what the agent asked to be hidden.
66
+ */
67
+ export function hiddenTags(scene, values) {
68
+ const hidden = new Set();
69
+ for (const layer of scene.layers ?? []) {
70
+ const raw = values?.[layerParam(layer.tag)];
71
+ const on = raw === undefined ? (layer.visible ?? true) : raw === true || raw === 'true' || raw === 1;
72
+ if (!on)
73
+ hidden.add(layer.tag);
74
+ }
75
+ return hidden;
76
+ }
77
+ /**
78
+ * Is this object hidden in its own right?
79
+ *
80
+ * Only its OWN tags: a hidden ancestor hides its subtree through the renderer's node tree
81
+ * (three.js propagates `visible`), so walking the ancestry here would double-count and would
82
+ * disagree with what is drawn.
83
+ */
84
+ export function isTagHidden(object, hidden) {
85
+ return (object.tags ?? []).some((t) => hidden.has(t));
86
+ }
87
+ /** One line for `scene_get`'s header: `exterior (on), glazing (off)`, or null when there are none. */
88
+ export function describeLayers(scene, values) {
89
+ const layers = scene.layers ?? [];
90
+ if (layers.length === 0)
91
+ return null;
92
+ const hidden = hiddenTags(scene, values);
93
+ return layers.map((l) => `${l.tag} (${hidden.has(l.tag) ? 'off' : 'on'})`).join(', ');
94
+ }
@@ -43,6 +43,11 @@ export type SceneOp =
43
43
  fit: true;
44
44
  };
45
45
  }
46
+ /** Replace the whole layer list (§18); `[]` clears it, so the scene has no Tweaks panel. */
47
+ | {
48
+ op: 'layers';
49
+ layers: unknown;
50
+ }
46
51
  /** Whole-scene replace (scene_create, JSON import). */
47
52
  | {
48
53
  op: 'replace';
@@ -54,7 +59,7 @@ export type SceneEditSource = {
54
59
  } | {
55
60
  kind: 'user';
56
61
  };
57
- export type SceneChangeKind = 'moved' | 'resized' | 'rotated' | 'recoloured' | 'renamed' | 'removed' | 'added' | 'edited' | 'camera' | 'replaced';
62
+ export type SceneChangeKind = 'moved' | 'resized' | 'rotated' | 'recoloured' | 'renamed' | 'removed' | 'added' | 'edited' | 'camera' | 'layers' | 'replaced';
58
63
  /** One journalled change (the writer stamps `rev`). */
59
64
  export interface SceneChange {
60
65
  rev: number;
@@ -259,6 +259,38 @@ function removeObject(scene, id, opts) {
259
259
  removed,
260
260
  };
261
261
  }
262
+ /**
263
+ * Replace `scene.layers` (§18). The shape is checked by the final `validateScene`, like every
264
+ * other op; this only rejects what would otherwise be stored as a non-list.
265
+ *
266
+ * ⚠️ This changes which toggles EXIST, not which are on — that is the canvas row's `values` and
267
+ * is not a scene edit at all.
268
+ */
269
+ function setLayers(scene, layers) {
270
+ if (!Array.isArray(layers)) {
271
+ return {
272
+ error: 'layers must be an array of { tag, label, visible? } — or [] to remove every toggle.',
273
+ };
274
+ }
275
+ const before = scene.layers ?? [];
276
+ const next = { ...scene };
277
+ if (layers.length === 0)
278
+ delete next.layers;
279
+ else
280
+ next.layers = layers;
281
+ const summary = layers.length === 0
282
+ ? `Layers cleared${before.length > 0 ? ` (was ${String(before.length)})` : ''}.`
283
+ : `Layers set: ${layers
284
+ .map((l) => (isRecord(l) && typeof l.tag === 'string' ? l.tag : '?'))
285
+ .join(', ')}.`;
286
+ return {
287
+ scene: next,
288
+ summary,
289
+ ids: [],
290
+ changes: [{ id: '', what: 'layers', from: before, to: layers }],
291
+ warnings: [],
292
+ };
293
+ }
262
294
  function setCamera(scene, camera) {
263
295
  if (isRecord(camera) && camera.fit === true) {
264
296
  const rest = { ...scene };
@@ -301,6 +333,8 @@ export function applySceneOp(scene, op, opts) {
301
333
  return removeObject(scene, op.id, opts);
302
334
  case 'camera':
303
335
  return setCamera(scene, op.camera);
336
+ case 'layers':
337
+ return setLayers(scene, op.layers);
304
338
  case 'replace': {
305
339
  const v = validateScene(op.scene);
306
340
  if (!v.ok)
@@ -395,6 +429,8 @@ function describeChange(c) {
395
429
  return `added ${typeof c.to === 'string' ? `${c.to} ` : ''}${q}`;
396
430
  case 'camera':
397
431
  return 'changed the camera';
432
+ case 'layers':
433
+ return 'changed which layers can be hidden';
398
434
  case 'replaced':
399
435
  return 'replaced the whole scene';
400
436
  case 'edited':
@@ -47,6 +47,14 @@ export interface SceneObjectBase {
47
47
  material?: SceneMaterial;
48
48
  /** User-only lock: the inspector cannot move/remove it; agents still can (and are warned). */
49
49
  locked?: boolean;
50
+ /**
51
+ * Cross-cutting labels (§18): `exterior`, `glazing`, `structure`. A tag on a GROUP applies to
52
+ * its whole subtree. Tags declared as `layers` become show/hide toggles for the user.
53
+ *
54
+ * ⚠️ Lowercase, hyphens, NO underscore — `layerParam` maps `-` to `_`, so allowing both would
55
+ * let `load-bearing` and `load_bearing` collide on one toggle.
56
+ */
57
+ tags?: string[];
50
58
  }
51
59
  export interface BoxObject extends SceneObjectBase {
52
60
  type: 'box';
@@ -139,6 +147,21 @@ export type SceneLight = {
139
147
  type: 'ambient';
140
148
  intensity?: number;
141
149
  };
150
+ /**
151
+ * A user-togglable layer (§18): one tag, one toggle in the Tweaks panel.
152
+ *
153
+ * Which layers are currently SHOWING is not here — it lives in the canvas record's `values`, so
154
+ * hiding a wall to look inside is not an edit to the scene (no rev, no undo entry, no user-edit
155
+ * notice to the agent). `visible` is only the default.
156
+ */
157
+ export interface SceneLayer {
158
+ /** The tag this layer governs. */
159
+ tag: string;
160
+ /** Panel label — "Exterior walls", not "exterior". */
161
+ label: string;
162
+ /** Showing by default. Default true. */
163
+ visible?: boolean;
164
+ }
142
165
  export interface SceneFile {
143
166
  version: 1;
144
167
  name: string;
@@ -149,12 +172,23 @@ export interface SceneFile {
149
172
  /** Missing → the default studio light. */
150
173
  lights?: SceneLight[];
151
174
  objects: SceneObject[];
175
+ /**
176
+ * Tags the user may show/hide (§18). A tag no layer declares is metadata, not a control.
177
+ * Missing or empty → the scene has no Tweaks panel.
178
+ */
179
+ layers?: SceneLayer[];
152
180
  }
153
181
  export interface SceneBounds {
154
182
  min: Vec3;
155
183
  max: Vec3;
156
184
  }
157
185
  export declare const SCENE_MAX_OBJECTS = 500;
186
+ /** Tag format (§18.3): no underscore, so `layerParam` cannot produce a collision. */
187
+ export declare const SCENE_TAG_PATTERN: RegExp;
188
+ export declare const SCENE_MAX_TAGS_PER_OBJECT = 8;
189
+ /** Distinct tags in one scene, and layers — beyond this a panel of toggles is a tree. */
190
+ export declare const SCENE_MAX_TAGS = 24;
191
+ export declare const SCENE_MAX_LAYERS = 24;
158
192
  export declare const SCENE_MAX_BYTES: number;
159
193
  /** Max objects in one scene_add_object batch (Q-1). */
160
194
  export declare const SCENE_MAX_BATCH = 50;
@@ -29,6 +29,12 @@ export const SCENE_EXTRUDE_AXES = ['x', 'y', 'z'];
29
29
  // Limits and defaults (§2.3, §2.4, Q-1, Q-14)
30
30
  // =============================================================================
31
31
  export const SCENE_MAX_OBJECTS = 500;
32
+ /** Tag format (§18.3): no underscore, so `layerParam` cannot produce a collision. */
33
+ export const SCENE_TAG_PATTERN = /^[a-z0-9][a-z0-9-]{0,23}$/;
34
+ export const SCENE_MAX_TAGS_PER_OBJECT = 8;
35
+ /** Distinct tags in one scene, and layers — beyond this a panel of toggles is a tree. */
36
+ export const SCENE_MAX_TAGS = 24;
37
+ export const SCENE_MAX_LAYERS = 24;
32
38
  export const SCENE_MAX_BYTES = 512 * 1024;
33
39
  /** Max objects in one scene_add_object batch (Q-1). */
34
40
  export const SCENE_MAX_BATCH = 50;
@@ -158,8 +164,8 @@ export function utf8Bytes(s) {
158
164
  }
159
165
  return n;
160
166
  }
161
- const TOP_KEYS = new Set(['version', 'name', 'units', 'camera', 'lights', 'objects']);
162
- const BASE_KEYS = ['id', 'name', 'type', 'position', 'rotation', 'group', 'locked'];
167
+ const TOP_KEYS = new Set(['version', 'name', 'units', 'camera', 'lights', 'objects', 'layers']);
168
+ const BASE_KEYS = ['id', 'name', 'type', 'position', 'rotation', 'group', 'locked', 'tags'];
163
169
  /** Surface fields: every type but `group` (a group has no surface of its own). */
164
170
  const SURFACE_KEYS = ['color', 'material'];
165
171
  const TYPE_KEYS = {
@@ -181,7 +187,7 @@ const TYPE_SHAPE = {
181
187
  torus: 'torus takes radius and tube',
182
188
  extrude: 'extrude takes points [[a, b], …], height and optional axis',
183
189
  wedge: 'wedge takes size [w, h, d] and optional ridge',
184
- group: 'a group is a container: id, name, position, rotation, group, locked only — colour its children',
190
+ group: 'a group is a container: id, name, position, rotation, group, locked, tags only — colour its children',
185
191
  };
186
192
  const MATERIAL_KEYS = new Set(['roughness', 'metalness', 'opacity']);
187
193
  const HEX_RE = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
@@ -362,6 +368,24 @@ function validateObject(raw, i, errors) {
362
368
  if (raw.locked !== undefined && typeof raw.locked !== 'boolean') {
363
369
  errors.push(`${label}.locked = ${show(raw.locked)} — true or false.`);
364
370
  }
371
+ if (raw.tags !== undefined) {
372
+ if (!Array.isArray(raw.tags)) {
373
+ errors.push(`${label}.tags = ${show(raw.tags)} — an array of labels, e.g. ["exterior"].`);
374
+ }
375
+ else if (raw.tags.length > SCENE_MAX_TAGS_PER_OBJECT) {
376
+ errors.push(`${label}.tags has ${String(raw.tags.length)} labels (max ${String(SCENE_MAX_TAGS_PER_OBJECT)}).`);
377
+ }
378
+ else {
379
+ for (const t of raw.tags) {
380
+ if (typeof t !== 'string' || !SCENE_TAG_PATTERN.test(t)) {
381
+ errors.push(`${label}.tags has ${show(t)} — a tag is 1–24 chars of lowercase letters, digits or "-" (no underscore: "load-bearing", not "load_bearing").`);
382
+ }
383
+ }
384
+ if (new Set(raw.tags).size !== raw.tags.length) {
385
+ errors.push(`${label}.tags repeats a label.`);
386
+ }
387
+ }
388
+ }
365
389
  if (type !== 'group' && raw.material !== undefined) {
366
390
  if (!isRecord(raw.material)) {
367
391
  errors.push(`${label}.material = ${show(raw.material)} — { roughness?, metalness?, opacity? }.`);
@@ -501,6 +525,44 @@ function validateGroups(objects, errors) {
501
525
  }
502
526
  });
503
527
  }
528
+ /** `layers` (§18): one toggle per tag, with a label the panel can show. */
529
+ function validateLayers(v, errors) {
530
+ if (!Array.isArray(v)) {
531
+ errors.push(`layers = ${show(v)} — an array of { tag, label, visible? }.`);
532
+ return;
533
+ }
534
+ if (v.length > SCENE_MAX_LAYERS) {
535
+ errors.push(`layers has ${String(v.length)} entries (max ${String(SCENE_MAX_LAYERS)}).`);
536
+ return;
537
+ }
538
+ const seen = new Set();
539
+ v.forEach((raw, i) => {
540
+ const at = `layers[${String(i)}]`;
541
+ if (!isRecord(raw)) {
542
+ errors.push(`${at} = ${show(raw)} — { tag, label, visible? }.`);
543
+ return;
544
+ }
545
+ for (const k of Object.keys(raw)) {
546
+ if (k !== 'tag' && k !== 'label' && k !== 'visible') {
547
+ errors.push(`${at}.${k} is not a layer field — tag, label, visible.`);
548
+ }
549
+ }
550
+ if (typeof raw.tag !== 'string' || !SCENE_TAG_PATTERN.test(raw.tag)) {
551
+ errors.push(`${at}.tag = ${show(raw.tag)} — 1–24 chars of lowercase letters, digits or "-" (no underscore).`);
552
+ }
553
+ else if (seen.has(raw.tag)) {
554
+ errors.push(`${at}.tag = "${raw.tag}" — already declared; one layer per tag.`);
555
+ }
556
+ else
557
+ seen.add(raw.tag);
558
+ if (typeof raw.label !== 'string' || raw.label.trim().length < 1 || raw.label.length > 48) {
559
+ errors.push(`${at}.label = ${show(raw.label)} — a string of 1–48 chars, e.g. "Exterior walls".`);
560
+ }
561
+ if (raw.visible !== undefined && typeof raw.visible !== 'boolean') {
562
+ errors.push(`${at}.visible = ${show(raw.visible)} — true or false.`);
563
+ }
564
+ });
565
+ }
504
566
  function validateLights(v, errors) {
505
567
  if (!Array.isArray(v)) {
506
568
  errors.push(`lights = ${show(v)} — an array of { type: "sun" | "ambient", … }.`);
@@ -561,7 +623,7 @@ export function validateScene(input) {
561
623
  }
562
624
  for (const k of Object.keys(input)) {
563
625
  if (!TOP_KEYS.has(k)) {
564
- errors.push(`${k} is not a scene field — version, name, units, camera, lights, objects.`);
626
+ errors.push(`${k} is not a scene field — version, name, units, camera, lights, objects, layers.`);
565
627
  }
566
628
  }
567
629
  if (input.version !== SCENE_VERSION) {
@@ -590,6 +652,8 @@ export function validateScene(input) {
590
652
  }
591
653
  if (input.lights !== undefined)
592
654
  validateLights(input.lights, errors);
655
+ if (input.layers !== undefined)
656
+ validateLayers(input.layers, errors);
593
657
  if (!Array.isArray(input.objects)) {
594
658
  errors.push(`objects = ${show(input.objects)} — must be an array.`);
595
659
  return { ok: false, errors };
@@ -612,6 +676,17 @@ export function validateScene(input) {
612
676
  }
613
677
  });
614
678
  validateGroups(objects.filter(isRecord), errors);
679
+ const distinct = new Set();
680
+ for (const o of objects) {
681
+ if (isRecord(o) && Array.isArray(o.tags)) {
682
+ for (const t of o.tags)
683
+ if (typeof t === 'string')
684
+ distinct.add(t);
685
+ }
686
+ }
687
+ if (distinct.size > SCENE_MAX_TAGS) {
688
+ errors.push(`The scene uses ${String(distinct.size)} different tags (max ${String(SCENE_MAX_TAGS)}). Reuse the labels you have — a tag is a layer the user toggles, not a note on one object.`);
689
+ }
615
690
  if (errors.length === 0) {
616
691
  const text = JSON.stringify(input, null, 2);
617
692
  const bytes = utf8Bytes(text);
@@ -279,6 +279,7 @@ export const CAPABILITY_PACKS = {
279
279
  'scene_update_object',
280
280
  'scene_remove_object',
281
281
  'scene_set_camera',
282
+ 'scene_set_layers',
282
283
  'scene_screenshot',
283
284
  ],
284
285
  readOnly: false,
@@ -291,10 +292,13 @@ export const CAPABILITY_PACKS = {
291
292
  '(objects: [...], ≤ 50), the group first, its parts with group: "<id>"; children are positioned in the ' +
292
293
  "group's frame. Roofs: wedge (gable; ridge along z, rotate 90° for x), cone with segments 4 (pyramid/hip), " +
293
294
  'or extrude with axis "x"/"z" (a side profile pushed horizontally). Read with scene_get before editing — ' +
294
- 'the user edits too. Keep neutral colours unless asked. After building, scene_screenshot to check.',
295
- // ≈ chars / 4: the snippet is ~910 chars; the seven schemas serialise to ~12 KB (§17 added
296
- // group/wedge/segments/axis/ridge and the axis mapping, ~3.7 KB).
297
- estimatedPromptTokens: 230,
295
+ "the user edits too (it returns an outline; pass ids: [...] for an object's numbers). TAG AS YOU BUILD: " +
296
+ 'tags: ["exterior"] etc. on each object, then scene_set_layers to give the user show/hide toggles — a tag ' +
297
+ 'spans groups, so "exterior" covers walls on every floor, and tagging afterwards means a pass over every ' +
298
+ 'object. Keep neutral colours unless asked. After building, scene_screenshot to check.',
299
+ // ≈ chars / 4: the snippet is ~1,250 chars; the eight schemas serialise to ~13 KB (§17 added
300
+ // group/wedge/segments/axis/ridge and the axis mapping, ~3.7 KB; §18 tags + scene_set_layers).
301
+ estimatedPromptTokens: 315,
298
302
  estimatedToolTokens: 3000,
299
303
  },
300
304
  plans: {
@@ -33,13 +33,50 @@ function statusFromText(text) {
33
33
  const m = text.match(/\b(400|401|403|404|408|409|413|422|429|500|502|503|504|529)\b/);
34
34
  return m ? Number(m[1]) : undefined;
35
35
  }
36
+ /**
37
+ * Textual markers for "the key is wrong", measured against the real wire text on 2026-10-03.
38
+ *
39
+ * ⚠️ EVERY MARKER BUT ONE USED TO BE AN ANTHROPIC STRING, so a bad key classified as `auth` for
40
+ * exactly one of three first-party providers. What the others actually send:
41
+ *
42
+ * Anthropic 401 {"error":{"type":"authentication_error","message":"API key is invalid."}}
43
+ * OpenAI 401 {"error":{"message":"Incorrect API key provided: sk-…","code":"invalid_api_key"}}
44
+ * Gemini 400 {"error":{"message":"API key not valid. Please pass a valid API key.",
45
+ * "details":[{"reason":"API_KEY_INVALID"}]}}
46
+ *
47
+ * Two independent reasons the old set missed them:
48
+ *
49
+ * 1. **Gemini answers 400, not 401**, so no status test can catch it — the text is the only
50
+ * signal, and "API key not valid" is not the substring "invalid api key".
51
+ * 2. **Our own providers rewrite the message and interpolate their name into the middle of the
52
+ * phrase**: `gemini-native` produces "Invalid Gemini API key. …" and `openai.ts` produces
53
+ * "Invalid OpenAI API key. …". Neither contains "invalid api key". In-process the thrown
54
+ * `ProviderError.statusCode` saved OpenAI; across an IPC boundary — Desktop, the path this
55
+ * module exists for — only the string crosses, and both classified as `unknown`.
56
+ *
57
+ * Hence `invalid(?: \w+)? api key`: it matches the bare phrase and the branded one, so a
58
+ * provider adding its name no longer silently breaks the classification.
59
+ */
60
+ const AUTH_MARKERS = [
61
+ /authentication_error/,
62
+ /permission_error/,
63
+ /unauthorized/,
64
+ /x-api-key/,
65
+ // Gemini: the message, and the machine-readable reason in `details`.
66
+ /api key not valid/,
67
+ /api[_ -]?key[_ -]?invalid/,
68
+ // OpenAI: the message, and its `error.code`.
69
+ /incorrect api key/,
70
+ /invalid_api_key/,
71
+ // Ours and anyone else's, with or without a provider name in the middle.
72
+ /invalid(?: \w+)? api key/,
73
+ /\b(?:no|missing) api key/,
74
+ ];
36
75
  function categorize(statusCode, text) {
37
76
  const lower = text.toLowerCase();
38
- // Auth: key issues. Match on status OR the provider's textual markers, since a
39
- // stringified error may not preserve the numeric code.
40
- if (statusCode === 401 ||
41
- statusCode === 403 ||
42
- /authentication_error|invalid x-api-key|invalid api key|no api key|x-api-key|permission_error|unauthorized/.test(lower)) {
77
+ // Auth: key issues. Match on status OR a textual marker, since a stringified error may not
78
+ // preserve the numeric code — and Gemini's bad-key status (400) is not an auth code at all.
79
+ if (statusCode === 401 || statusCode === 403 || AUTH_MARKERS.some((re) => re.test(lower))) {
43
80
  return 'auth';
44
81
  }
45
82
  if (statusCode === 429 || /rate.?limit|too many requests/.test(lower))
@@ -739,8 +739,24 @@ export function shouldClearHistoryOnModelChange(_fromModelId, _toModelId) {
739
739
  // Context Window Resolution
740
740
  // =============================================================================
741
741
  /**
742
- * Provider fallback context limits.
743
- * Used when a model is not in the registry or has no contextWindow set.
742
+ * What to assume when the endpoint is unknown.
743
+ *
744
+ * ⚠️ UNDER-REPORTING IS THE SAFE DIRECTION. Too small wastes context and compacts early; too
745
+ * large means compaction fires after the real limit, so the request 400s and reads as a provider
746
+ * fault. `custom` points at whatever the user is running — LM Studio, vLLM, a proxy — so it gets
747
+ * the same local-model assumption as Ollama rather than a number borrowed from a frontier model.
748
+ */
749
+ const UNKNOWN_ENDPOINT_CONTEXT = 32000;
750
+ /**
751
+ * Provider fallback context limits, used when a model is not in the registry or declares no
752
+ * `contextWindow` (every Groq / Together / Fireworks / Perplexity / OpenRouter / Ollama entry —
753
+ * those hosts define the window by deployment, so the registry does not guess per model).
754
+ *
755
+ * ⚠️ EXHAUSTIVE ON PURPOSE. This was a `default: return 200000`, which handed out ANTHROPIC's
756
+ * window to any provider not listed — so `getModelContextWindow('some-local-model', 'custom')`
757
+ * answered 200000 and a local 8k model would compact long after it had already overflowed. The
758
+ * `never` assignment below makes a new `ProviderType` a compile error here instead of a silent
759
+ * inheritance of someone else's limit.
744
760
  */
745
761
  function getProviderContextLimitFallback(provider) {
746
762
  switch (provider) {
@@ -763,8 +779,16 @@ function getProviderContextLimitFallback(provider) {
763
779
  return 131072;
764
780
  case 'openrouter':
765
781
  return 128000;
766
- default:
767
- return 200000;
782
+ case 'custom':
783
+ case undefined:
784
+ return UNKNOWN_ENDPOINT_CONTEXT;
785
+ default: {
786
+ // Unreachable while every ProviderType is handled above — and a COMPILE ERROR the moment
787
+ // one is added without a limit, which is the whole point of writing it this way.
788
+ const unhandled = provider;
789
+ void unhandled;
790
+ return UNKNOWN_ENDPOINT_CONTEXT;
791
+ }
768
792
  }
769
793
  }
770
794
  /**
package/dist/models.d.ts CHANGED
@@ -9,6 +9,20 @@ import type { ProviderType } from './config.js';
9
9
  * Default model for each provider.
10
10
  * Derived from MODEL_REGISTRY — uses the 'balanced' tier default for each provider.
11
11
  */
12
+ /**
13
+ * Default model for each provider — the 'balanced' tier entry from MODEL_REGISTRY.
14
+ *
15
+ * ⚠️ THE KEYS COME FROM `PROVIDER_METADATA`, NOT A HAND-WRITTEN LIST. The list used to be typed
16
+ * out here and was missing `ollama-anthropic`, so `DEFAULT_MODELS['ollama-anthropic']` was
17
+ * `undefined` behind a `Record<ProviderType, string>` — the `{} as Record<…>` cast is what let
18
+ * that compile. `PROVIDER_METADATA` is a true complete Record, so adding a provider to the union
19
+ * now forces an entry here too.
20
+ *
21
+ * ⚠️ `custom` IS DELIBERATELY EMPTY. It used to fall through to `'gpt-6-sol'`, so pointing
22
+ * `custom` at LM Studio or vLLM without naming a model requested one that endpoint has never
23
+ * heard of. A custom endpoint's model is the user's to name; there is no default to invent, and
24
+ * an empty string fails immediately and obviously instead of looking like an intent.
25
+ */
12
26
  export declare const DEFAULT_MODELS: Record<ProviderType, string>;
13
27
  /**
14
28
  * Default context window when model is unknown
package/dist/models.js CHANGED
@@ -4,36 +4,39 @@
4
4
  * This file maintains backward compatibility with the original SDK API.
5
5
  * Internally, it delegates to the full model registry in ./models/.
6
6
  */
7
- import { getDefaultModelForTier, getModelContextWindow, MODEL_REGISTRY } from './models/index.js';
7
+ import { getDefaultModelForTier, getModelContextWindow, MODEL_REGISTRY, PROVIDER_METADATA, } from './models/index.js';
8
8
  /**
9
9
  * Default model for each provider.
10
10
  * Derived from MODEL_REGISTRY — uses the 'balanced' tier default for each provider.
11
11
  */
12
+ /**
13
+ * Default model for each provider — the 'balanced' tier entry from MODEL_REGISTRY.
14
+ *
15
+ * ⚠️ THE KEYS COME FROM `PROVIDER_METADATA`, NOT A HAND-WRITTEN LIST. The list used to be typed
16
+ * out here and was missing `ollama-anthropic`, so `DEFAULT_MODELS['ollama-anthropic']` was
17
+ * `undefined` behind a `Record<ProviderType, string>` — the `{} as Record<…>` cast is what let
18
+ * that compile. `PROVIDER_METADATA` is a true complete Record, so adding a provider to the union
19
+ * now forces an entry here too.
20
+ *
21
+ * ⚠️ `custom` IS DELIBERATELY EMPTY. It used to fall through to `'gpt-6-sol'`, so pointing
22
+ * `custom` at LM Studio or vLLM without naming a model requested one that endpoint has never
23
+ * heard of. A custom endpoint's model is the user's to name; there is no default to invent, and
24
+ * an empty string fails immediately and obviously instead of looking like an intent.
25
+ */
12
26
  export const DEFAULT_MODELS = (() => {
13
- const providers = [
14
- 'claude',
15
- 'openai',
16
- 'gemini',
17
- 'ollama',
18
- 'together',
19
- 'groq',
20
- 'fireworks',
21
- 'perplexity',
22
- 'openrouter',
23
- 'custom',
24
- ];
25
27
  const result = {};
26
- for (const provider of providers) {
27
- const balanced = getDefaultModelForTier(provider, 'balanced');
28
- if (balanced) {
29
- result[provider] = balanced.id;
30
- }
31
- else {
32
- // Fallback: first supported model for this provider, or a sensible default
33
- const firstModel = MODEL_REGISTRY.find((m) => m.provider === provider && m.status === 'supported');
34
- // Fallback only when the registry has no model at all for the provider; gpt-4o is gone.
35
- result[provider] = firstModel?.id ?? 'gpt-6-sol';
28
+ for (const provider of Object.keys(PROVIDER_METADATA)) {
29
+ if (provider === 'custom') {
30
+ result[provider] = '';
31
+ continue;
36
32
  }
33
+ // `ollama-anthropic` is Ollama behind an Anthropic-shaped shim: same models.
34
+ const lookup = provider === 'ollama-anthropic' ? 'ollama' : provider;
35
+ const balanced = getDefaultModelForTier(lookup, 'balanced');
36
+ result[provider] =
37
+ balanced?.id ??
38
+ MODEL_REGISTRY.find((m) => m.provider === lookup && m.status === 'supported')?.id ??
39
+ '';
37
40
  }
38
41
  return result;
39
42
  })();
@@ -15,9 +15,16 @@
15
15
  import type { PlatformContext, PlatformToolsConfig } from '../context.js';
16
16
  import { type ISceneWriter } from '../scene-writer.js';
17
17
  import { type SceneFile } from '../../canvas/scene.js';
18
- export declare const SCENE_TOOL_NAMES: readonly ["scene_create", "scene_get", "scene_add_object", "scene_update_object", "scene_remove_object", "scene_set_camera", "scene_screenshot"];
19
- /** "sofa · box · Sofa" rows, indented under their group parent. */
20
- export declare function sceneOutline(scene: SceneFile): string;
18
+ export declare const SCENE_TOOL_NAMES: readonly ["scene_create", "scene_get", "scene_add_object", "scene_update_object", "scene_remove_object", "scene_set_camera", "scene_set_layers", "scene_screenshot"];
19
+ /**
20
+ * "sofa · box · Sofa" rows, indented under their group parent; `#tag` suffixes when `withTags`.
21
+ *
22
+ * ⚠️ `withTags` exists because tags are DECORATION and addressing is the job. 400 objects each
23
+ * carrying two tags pushes the outline past `SCENE_READ_MAX_CHARS` (measured: 20,723 chars), and
24
+ * refusing the cheap read — the one every edit depends on — to make room for labels would be the
25
+ * wrong trade. `sceneReadBody` drops them, with a count, rather than failing.
26
+ */
27
+ export declare function sceneOutline(scene: SceneFile, withTags?: boolean): string;
21
28
  /**
22
29
  * The most JSON one `scene_get` may return.
23
30
  *
@@ -93,6 +100,9 @@ export declare function createSceneTools(config: PlatformToolsConfig, options?:
93
100
  position?: unknown;
94
101
  target?: unknown;
95
102
  fit?: boolean;
103
+ }> | import("@compilr-dev/agents").Tool<{
104
+ canvas_id: number;
105
+ layers?: unknown;
96
106
  }> | import("@compilr-dev/agents").Tool<{
97
107
  canvas_id: number;
98
108
  width?: number;
@@ -14,7 +14,8 @@
14
14
  */
15
15
  import { defineTool, createErrorResult } from '@compilr-dev/agents';
16
16
  import { createSceneWriter } from '../scene-writer.js';
17
- import { SCENE_MAX_BATCH, SCENE_OBJECT_TYPES, describeScene, serializeScene, } from '../../canvas/scene.js';
17
+ import { SCENE_MAX_BATCH, SCENE_MAX_LAYERS, SCENE_OBJECT_TYPES, describeScene, serializeScene, } from '../../canvas/scene.js';
18
+ import { describeLayers, emptyLayers, undeclaredTags } from '../../canvas/scene-layers.js';
18
19
  import { formatUserEditNotice, } from '../../canvas/scene-ops.js';
19
20
  export const SCENE_TOOL_NAMES = [
20
21
  'scene_create',
@@ -23,13 +24,21 @@ export const SCENE_TOOL_NAMES = [
23
24
  'scene_update_object',
24
25
  'scene_remove_object',
25
26
  'scene_set_camera',
27
+ 'scene_set_layers',
26
28
  'scene_screenshot',
27
29
  ];
28
30
  const DEFAULT_SHOT = { width: 1280, height: 800 };
29
31
  const SHOT_MIN = 320;
30
32
  const SHOT_MAX = 2048;
31
- /** "sofa · box · Sofa" rows, indented under their group parent. */
32
- export function sceneOutline(scene) {
33
+ /**
34
+ * "sofa · box · Sofa" rows, indented under their group parent; `#tag` suffixes when `withTags`.
35
+ *
36
+ * ⚠️ `withTags` exists because tags are DECORATION and addressing is the job. 400 objects each
37
+ * carrying two tags pushes the outline past `SCENE_READ_MAX_CHARS` (measured: 20,723 chars), and
38
+ * refusing the cheap read — the one every edit depends on — to make room for labels would be the
39
+ * wrong trade. `sceneReadBody` drops them, with a count, rather than failing.
40
+ */
41
+ export function sceneOutline(scene, withTags = true) {
33
42
  if (scene.objects.length === 0)
34
43
  return '(no objects yet)';
35
44
  const byParent = new Map();
@@ -64,7 +73,9 @@ export function sceneOutline(scene) {
64
73
  const walk = (parent, depth) => {
65
74
  for (const o of byParent.get(parent) ?? []) {
66
75
  const lock = o.locked ? ' · locked' : '';
67
- rows.push(`${' '.repeat(depth)}${o.id} · ${typeOf(o)}${o.name ? ` · ${o.name}` : ''}${lock}`);
76
+ // §18: only tagged rows pay for this, and §6.4's budget still has to hold.
77
+ const tags = withTags && o.tags && o.tags.length > 0 ? ` · ${o.tags.map((t) => `#${t}`).join(' ')}` : '';
78
+ rows.push(`${' '.repeat(depth)}${o.id} · ${typeOf(o)}${o.name ? ` · ${o.name}` : ''}${lock}${tags}`);
68
79
  if (depth < 8)
69
80
  walk(o.id, depth + 1);
70
81
  }
@@ -81,6 +92,7 @@ export function sceneOutline(scene) {
81
92
  * for. So an oversized read must be refused with instructions, never silently shortened.
82
93
  */
83
94
  export const SCENE_READ_MAX_CHARS = 20_000;
95
+ const OUTLINE_HEAD = '--- objects (id · type · name) ---';
84
96
  /** "roof, walls" — the first few of a list, for an error that has to name what is available. */
85
97
  function someIds(ids, limit = 12) {
86
98
  return ids.length <= limit
@@ -136,10 +148,18 @@ export function sceneReadBody(scene, head, ids, full) {
136
148
  if (full === true) {
137
149
  return guard(serializeScene(scene), `This scene's JSON is over ${String(SCENE_READ_MAX_CHARS)} characters, which would be summarised before you saw it and you would lose the ids. Call scene_get without full for the outline, then with ids: [...] for the objects you need.`);
138
150
  }
139
- return {
140
- ok: true,
141
- text: `${head}\n\n--- objects (id · type · name) ---\n${sceneOutline(scene)}`,
142
- };
151
+ /*
152
+ The outline must always be readable: it is the cheap path every edit depends on, and the
153
+ object count is what bounds it. So when tags would push it over the ceiling they come OFF,
154
+ with a count — an explicit "tags omitted" is nothing like a silently shortened JSON dump,
155
+ which is what the guard exists to prevent.
156
+ */
157
+ const outlineWithTags = `${head}\n\n${OUTLINE_HEAD}\n${sceneOutline(scene)}`;
158
+ if (outlineWithTags.length <= SCENE_READ_MAX_CHARS)
159
+ return { ok: true, text: outlineWithTags };
160
+ const tagged = scene.objects.filter((o) => (o.tags ?? []).length > 0).length;
161
+ const note = `(tags omitted on ${String(tagged)} objects — this outline is near the read limit; the layers line above lists them, and scene_get with ids: [...] shows an object's own tags)`;
162
+ return guard(`${OUTLINE_HEAD}\n${sceneOutline(scene, false)}\n\n${note}`, `This scene's outline is over ${String(SCENE_READ_MAX_CHARS)} characters even without tags (${String(scene.objects.length)} objects). Read a part of it instead: scene_get with ids: [...] for the objects you are working on.`);
143
163
  }
144
164
  function clampShot(v, fallback) {
145
165
  if (typeof v !== 'number' || !Number.isFinite(v))
@@ -258,6 +278,14 @@ const OBJECT_PROPERTIES = {
258
278
  type: 'boolean',
259
279
  description: 'Lock against USER edits in the inspector (agents can still edit it).',
260
280
  },
281
+ tags: {
282
+ type: 'array',
283
+ items: { type: 'string' },
284
+ description: 'Cross-cutting labels, e.g. ["exterior"], ["glazing"]. Lowercase, hyphens, no underscore; ' +
285
+ 'up to 8 per object. Declare a tag with scene_set_layers and the user gets a show/hide ' +
286
+ 'toggle for it. A tag on a GROUP covers everything inside it. Tag as you build: labelling ' +
287
+ 'afterwards means a pass over every object.',
288
+ },
261
289
  };
262
290
  const OBJECT_FIELD_NAMES = Object.keys(OBJECT_PROPERTIES);
263
291
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -382,7 +410,17 @@ export function createSceneTools(config, options) {
382
410
  const r = await writer.read(input.canvas_id);
383
411
  if (!r.ok)
384
412
  return createErrorResult(r.error);
385
- const head = `Scene "${r.title}" (canvas ${String(r.canvasId)}): ${describeScene(r.scene)} · rev ${String(r.rev)}`;
413
+ /*
414
+ §18. The layer line goes in the HEADER, so it is there whichever read shape the agent
415
+ asked for — it needs to know a layer is hidden before it decides the scene is wrong.
416
+ `undefined` values: the defaults the scene declares, not the user's current toggles,
417
+ which the tools never see.
418
+ */
419
+ const layerLine = describeLayers(r.scene);
420
+ const untagged = undeclaredTags(r.scene);
421
+ const head = `Scene "${r.title}" (canvas ${String(r.canvasId)}): ${describeScene(r.scene)} · rev ${String(r.rev)}` +
422
+ (layerLine === null ? '' : `\nlayers: ${layerLine}`) +
423
+ (untagged.length > 0 ? `\ntags with no toggle: ${untagged.join(', ')}` : '');
386
424
  const body = sceneReadBody(r.scene, head, input.ids, input.full);
387
425
  if (!body.ok)
388
426
  return createErrorResult(body.error);
@@ -544,6 +582,67 @@ export function createSceneTools(config, options) {
544
582
  },
545
583
  });
546
584
  // ---------------------------------------------------------------------------
585
+ const sceneSetLayersTool = defineTool({
586
+ name: 'scene_set_layers',
587
+ description: 'Declare which tags the USER can show and hide, giving each a label for the Tweaks panel — ' +
588
+ 'e.g. [{ tag: "exterior", label: "Exterior walls" }]. Replaces the whole list; [] removes ' +
589
+ 'every toggle. Tags themselves go on objects (see tags on scene_add_object), and one tag can ' +
590
+ 'span groups, which is the point: "exterior" covers walls on every floor. Pass visible: false ' +
591
+ "to start a layer hidden (a cutaway). Toggling is the user's, not yours: it changes their " +
592
+ 'view, never the scene.',
593
+ inputSchema: {
594
+ type: 'object',
595
+ properties: {
596
+ canvas_id: { type: 'number', description: 'The scene canvas id.' },
597
+ layers: {
598
+ type: 'array',
599
+ maxItems: SCENE_MAX_LAYERS,
600
+ description: `Up to ${String(SCENE_MAX_LAYERS)} layers, in panel order. [] clears them.`,
601
+ items: {
602
+ type: 'object',
603
+ properties: {
604
+ tag: { type: 'string', description: 'The tag this toggle governs.' },
605
+ label: { type: 'string', description: 'Panel label, e.g. "Exterior walls".' },
606
+ visible: { type: 'boolean', description: 'Showing by default. Default true.' },
607
+ },
608
+ required: ['tag', 'label'],
609
+ },
610
+ },
611
+ },
612
+ required: ['canvas_id', 'layers'],
613
+ },
614
+ execute: async (input) => {
615
+ try {
616
+ if (!Array.isArray(input.layers)) {
617
+ return createErrorResult('layers must be an array of { tag, label, visible? } — or [] to remove every toggle.');
618
+ }
619
+ const r = await applyAsAgent(input.canvas_id, [{ op: 'layers', layers: input.layers }], () => null);
620
+ if (!r.success)
621
+ return r;
622
+ // A toggle that governs nothing is not an error — the agent may label the objects next —
623
+ // but it is invisible in the panel, so say so rather than let it look applied.
624
+ const read = await writer.read(input.canvas_id);
625
+ if (read.ok) {
626
+ const empty = emptyLayers(read.scene);
627
+ const undeclared = undeclaredTags(read.scene);
628
+ const notes = [];
629
+ if (empty.length > 0) {
630
+ notes.push(`No object carries ${empty.length === 1 ? 'the tag' : 'the tags'} ${empty.join(', ')} yet — add tags with scene_update_object and the toggle starts working.`);
631
+ }
632
+ if (undeclared.length > 0) {
633
+ notes.push(`Tags in use with no toggle: ${undeclared.join(', ')}.`);
634
+ }
635
+ if (notes.length > 0)
636
+ return { ...r, result: `${String(r.result)}\n\n${notes.join('\n')}` };
637
+ }
638
+ return r;
639
+ }
640
+ catch (error) {
641
+ return createErrorResult(errorMessage('set layers', error));
642
+ }
643
+ },
644
+ });
645
+ // ---------------------------------------------------------------------------
547
646
  const sceneScreenshotTool = defineTool({
548
647
  name: 'scene_screenshot',
549
648
  description: 'RENDER a 3D scene and SEE it — check proportions, overlaps, gaps and floating objects after ' +
@@ -596,6 +695,7 @@ export function createSceneTools(config, options) {
596
695
  sceneUpdateObjectTool,
597
696
  sceneRemoveObjectTool,
598
697
  sceneSetCameraTool,
698
+ sceneSetLayersTool,
599
699
  sceneScreenshotTool,
600
700
  ];
601
701
  }
@@ -314,6 +314,7 @@ export const TOOL_GROUPS = {
314
314
  'scene_update_object',
315
315
  'scene_remove_object',
316
316
  'scene_set_camera',
317
+ 'scene_set_layers',
317
318
  'scene_screenshot',
318
319
  ],
319
320
  readOnly: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.33.1",
3
+ "version": "0.35.0",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -81,7 +81,7 @@
81
81
  "node": ">=20.0.0"
82
82
  },
83
83
  "dependencies": {
84
- "@compilr-dev/agents": "^0.7.1",
84
+ "@compilr-dev/agents": "^0.8.0",
85
85
  "@compilr-dev/logger": "^0.1.0",
86
86
  "ajv": "^6.14.0",
87
87
  "yaml": "^2.8.4"