@vib3code/sdk 2.0.1 → 2.0.3-canary.0a63e71

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 (192) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/DOCS/AGENT_HARNESS_ARCHITECTURE.md +245 -0
  3. package/DOCS/ANDROID_DEPLOYMENT.md +59 -0
  4. package/DOCS/ARCHITECTURE.md +1 -0
  5. package/DOCS/CI_TESTING.md +2 -0
  6. package/DOCS/CLI_ONBOARDING.md +3 -1
  7. package/DOCS/CONTROL_REFERENCE.md +2 -0
  8. package/DOCS/CROSS_SITE_DESIGN_PATTERNS.md +119 -0
  9. package/DOCS/ENV_SETUP.md +2 -0
  10. package/DOCS/EPIC_SCROLL_EVENTS.md +775 -0
  11. package/DOCS/EXPANSION_DESIGN.md +979 -0
  12. package/DOCS/EXPANSION_DESIGN_ULTRA.md +389 -0
  13. package/DOCS/EXPORT_FORMATS.md +2 -0
  14. package/DOCS/GPU_DISPOSAL_GUIDE.md +2 -0
  15. package/DOCS/HANDOFF_LANDING_PAGE.md +156 -0
  16. package/DOCS/HANDOFF_SDK_DEVELOPMENT.md +495 -0
  17. package/DOCS/LICENSING_TIERS.md +2 -0
  18. package/DOCS/MASTER_PLAN_2026-01-31.md +4 -2
  19. package/DOCS/MULTIVIZ_CHOREOGRAPHY_PATTERNS.md +939 -0
  20. package/DOCS/OBS_SETUP_GUIDE.md +2 -0
  21. package/DOCS/OPTIMIZATION_PLAN_MATH.md +119 -0
  22. package/DOCS/PRODUCT_STRATEGY.md +65 -0
  23. package/DOCS/PROJECT_SETUP.md +2 -0
  24. package/DOCS/README.md +105 -0
  25. package/DOCS/REFERENCE_SCROLL_ANALYSIS.md +99 -0
  26. package/DOCS/RENDERER_LIFECYCLE.md +2 -0
  27. package/DOCS/REPO_MANIFEST.md +2 -0
  28. package/DOCS/ROADMAP.md +113 -0
  29. package/DOCS/SCROLL_TIMELINE_v3.md +271 -0
  30. package/DOCS/SITE_REFACTOR_PLAN.md +102 -0
  31. package/DOCS/STATUS.md +26 -0
  32. package/DOCS/SYSTEM_INVENTORY.md +37 -32
  33. package/DOCS/TELEMETRY_EXPORTS.md +2 -0
  34. package/DOCS/VISUAL_ANALYSIS_CLICKERSS.md +87 -0
  35. package/DOCS/VISUAL_ANALYSIS_FACETAD.md +135 -0
  36. package/DOCS/VISUAL_ANALYSIS_SIMONE.md +97 -0
  37. package/DOCS/VISUAL_ANALYSIS_TABLESIDE.md +88 -0
  38. package/DOCS/WEBGPU_STATUS.md +121 -38
  39. package/DOCS/XR_BENCHMARKS.md +2 -0
  40. package/DOCS/archive/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +1 -0
  41. package/DOCS/archive/DEV_TRACK_ANALYSIS.md +1 -0
  42. package/DOCS/archive/DEV_TRACK_PLAN_2026-01-07.md +1 -0
  43. package/DOCS/archive/SESSION_014_PLAN.md +1 -0
  44. package/DOCS/archive/SESSION_LOG_2026-01-07.md +1 -0
  45. package/DOCS/archive/STRATEGIC_BLUEPRINT_2026-01-07.md +1 -0
  46. package/DOCS/archive/SYSTEM_AUDIT_2026-01-30.md +1 -0
  47. package/DOCS/archive/WEBGPU_STATUS_2026-02-15_STALE.md +1 -0
  48. package/DOCS/{DEV_TRACK_SESSION_2026-01-31.md → dev-tracks/DEV_TRACK_SESSION_2026-01-31.md} +3 -1
  49. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-06.md +233 -0
  50. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-13.md +129 -0
  51. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-15.md +144 -0
  52. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-16.md +110 -0
  53. package/DOCS/dev-tracks/PERF_UPGRADE_2026-02-16.md +310 -0
  54. package/DOCS/dev-tracks/README.md +12 -0
  55. package/README.md +26 -13
  56. package/cpp/CMakeLists.txt +236 -0
  57. package/cpp/bindings/embind.cpp +269 -0
  58. package/cpp/build.sh +129 -0
  59. package/cpp/geometry/Crystal.cpp +103 -0
  60. package/cpp/geometry/Fractal.cpp +136 -0
  61. package/cpp/geometry/GeometryGenerator.cpp +262 -0
  62. package/cpp/geometry/KleinBottle.cpp +71 -0
  63. package/cpp/geometry/Sphere.cpp +134 -0
  64. package/cpp/geometry/Tesseract.cpp +94 -0
  65. package/cpp/geometry/Tetrahedron.cpp +83 -0
  66. package/cpp/geometry/Torus.cpp +65 -0
  67. package/cpp/geometry/WarpFunctions.cpp +238 -0
  68. package/cpp/geometry/Wave.cpp +85 -0
  69. package/cpp/include/vib3_ffi.h +238 -0
  70. package/cpp/math/Mat4x4.cpp +409 -0
  71. package/cpp/math/Mat4x4.hpp +209 -0
  72. package/cpp/math/Projection.cpp +142 -0
  73. package/cpp/math/Projection.hpp +148 -0
  74. package/cpp/math/Rotor4D.cpp +322 -0
  75. package/cpp/math/Rotor4D.hpp +204 -0
  76. package/cpp/math/Vec4.cpp +303 -0
  77. package/cpp/math/Vec4.hpp +225 -0
  78. package/cpp/src/vib3_ffi.cpp +607 -0
  79. package/cpp/tests/Geometry_test.cpp +213 -0
  80. package/cpp/tests/Mat4x4_test.cpp +494 -0
  81. package/cpp/tests/Projection_test.cpp +298 -0
  82. package/cpp/tests/Rotor4D_test.cpp +423 -0
  83. package/cpp/tests/Vec4_test.cpp +489 -0
  84. package/docs/webgpu-live.html +1 -1
  85. package/package.json +41 -30
  86. package/src/agent/index.js +1 -3
  87. package/src/agent/mcp/MCPServer.js +1220 -144
  88. package/src/agent/mcp/index.js +1 -1
  89. package/src/agent/mcp/stdio-server.js +264 -0
  90. package/src/agent/mcp/tools.js +498 -31
  91. package/src/cli/index.js +431 -47
  92. package/src/core/CanvasManager.js +97 -204
  93. package/src/core/ErrorReporter.js +1 -1
  94. package/src/core/Parameters.js +1 -1
  95. package/src/core/VIB3Engine.js +93 -4
  96. package/src/core/VitalitySystem.js +53 -0
  97. package/src/core/index.js +18 -0
  98. package/src/core/renderers/FacetedRendererAdapter.js +10 -9
  99. package/src/core/renderers/HolographicRendererAdapter.js +13 -9
  100. package/src/core/renderers/QuantumRendererAdapter.js +11 -7
  101. package/src/creative/AestheticMapper.js +628 -0
  102. package/src/creative/ChoreographyPlayer.js +481 -0
  103. package/src/creative/index.js +11 -0
  104. package/src/experimental/GameLoop.js +72 -0
  105. package/src/experimental/LatticePhysics.js +100 -0
  106. package/src/experimental/LiveDirector.js +143 -0
  107. package/src/experimental/PlayerController4D.js +154 -0
  108. package/src/experimental/VIB3Actor.js +138 -0
  109. package/src/experimental/VIB3Compositor.js +117 -0
  110. package/src/experimental/VIB3Link.js +122 -0
  111. package/src/experimental/VIB3Orchestrator.js +146 -0
  112. package/src/experimental/VIB3Universe.js +109 -0
  113. package/src/experimental/demos/CrystalLabyrinth.js +202 -0
  114. package/src/export/TradingCardManager.js +3 -4
  115. package/src/export/index.js +11 -1
  116. package/src/faceted/FacetedSystem.js +260 -394
  117. package/src/games/glyph-war/GlyphWarVisualizer.js +641 -0
  118. package/src/geometry/generators/Crystal.js +2 -2
  119. package/src/geometry/warp/HypersphereCore.js +53 -24
  120. package/src/holograms/HolographicVisualizer.js +84 -98
  121. package/src/holograms/RealHolographicSystem.js +194 -43
  122. package/src/math/Mat4x4.js +308 -105
  123. package/src/math/Rotor4D.js +124 -40
  124. package/src/math/Vec4.js +200 -103
  125. package/src/math/index.js +7 -7
  126. package/src/polychora/PolychoraSystem.js +77 -0
  127. package/src/quantum/QuantumEngine.js +103 -66
  128. package/src/quantum/QuantumVisualizer.js +31 -22
  129. package/src/reactivity/index.js +3 -5
  130. package/src/render/LayerPresetManager.js +372 -0
  131. package/src/render/LayerReactivityBridge.js +344 -0
  132. package/src/render/LayerRelationshipGraph.js +610 -0
  133. package/src/render/MultiCanvasBridge.js +148 -25
  134. package/src/render/ShaderLoader.js +38 -0
  135. package/src/render/ShaderProgram.js +4 -4
  136. package/src/render/UnifiedRenderBridge.js +4 -1
  137. package/src/render/backends/WebGPUBackend.js +8 -4
  138. package/src/render/index.js +27 -2
  139. package/src/scene/Node4D.js +74 -24
  140. package/src/scene/index.js +4 -4
  141. package/src/shaders/common/geometry24.glsl +65 -0
  142. package/src/shaders/common/geometry24.wgsl +54 -0
  143. package/src/shaders/common/rotation4d.glsl +4 -4
  144. package/src/shaders/common/rotation4d.wgsl +2 -2
  145. package/src/shaders/common/uniforms.wgsl +15 -8
  146. package/src/shaders/faceted/faceted.frag.glsl +220 -80
  147. package/src/shaders/faceted/faceted.frag.wgsl +144 -90
  148. package/src/shaders/holographic/holographic.frag.glsl +28 -9
  149. package/src/shaders/holographic/holographic.frag.wgsl +112 -41
  150. package/src/shaders/quantum/quantum.frag.glsl +1 -0
  151. package/src/shaders/quantum/quantum.frag.wgsl +6 -4
  152. package/src/testing/ParallelTestFramework.js +2 -2
  153. package/src/ui/adaptive/renderers/webgpu/WebGPURenderer.ts +2 -2
  154. package/src/viewer/GalleryUI.js +17 -0
  155. package/src/viewer/ViewerPortal.js +2 -2
  156. package/src/viewer/index.js +1 -1
  157. package/tools/headless-renderer.js +258 -0
  158. package/tools/shader-sync-verify.js +14 -8
  159. package/tools/site-analysis/all-reports.json +32 -0
  160. package/tools/site-analysis/combined-analysis.md +50 -0
  161. package/tools/site-analyzer.mjs +779 -0
  162. package/tools/visual-catalog/capture.js +276 -0
  163. package/tools/visual-catalog/composite.js +138 -0
  164. package/types/adaptive-sdk.d.ts +204 -5
  165. package/types/agent/cli.d.ts +78 -0
  166. package/types/agent/index.d.ts +18 -0
  167. package/types/agent/mcp.d.ts +87 -0
  168. package/types/agent/telemetry.d.ts +190 -0
  169. package/types/core/VIB3Engine.d.ts +26 -0
  170. package/types/core/index.d.ts +261 -0
  171. package/types/creative/AestheticMapper.d.ts +72 -0
  172. package/types/creative/ChoreographyPlayer.d.ts +96 -0
  173. package/types/creative/index.d.ts +17 -0
  174. package/types/export/index.d.ts +243 -0
  175. package/types/geometry/index.d.ts +164 -0
  176. package/types/math/index.d.ts +214 -0
  177. package/types/render/LayerPresetManager.d.ts +78 -0
  178. package/types/render/LayerReactivityBridge.d.ts +85 -0
  179. package/types/render/LayerRelationshipGraph.d.ts +174 -0
  180. package/types/render/index.d.ts +3 -0
  181. package/types/scene/index.d.ts +204 -0
  182. package/types/systems/index.d.ts +244 -0
  183. package/types/variations/index.d.ts +62 -0
  184. package/types/viewer/index.d.ts +225 -0
  185. package/DOCS/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md +0 -34
  186. package/DOCS/DEV_TRACK_ANALYSIS.md +0 -77
  187. package/DOCS/DEV_TRACK_PLAN_2026-01-07.md +0 -42
  188. package/DOCS/SESSION_014_PLAN.md +0 -195
  189. package/DOCS/SESSION_LOG_2026-01-07.md +0 -56
  190. package/DOCS/STRATEGIC_BLUEPRINT_2026-01-07.md +0 -72
  191. package/DOCS/SYSTEM_AUDIT_2026-01-30.md +0 -738
  192. /package/src/viewer/{ReactivityManager.js → ViewerInputHandler.js} +0 -0
@@ -6,6 +6,12 @@
6
6
  import { toolDefinitions, getToolList, validateToolInput } from './tools.js';
7
7
  import { schemaRegistry } from '../../schemas/index.js';
8
8
  import { telemetry, EventType, withTelemetry } from '../telemetry/index.js';
9
+ import { AestheticMapper } from '../../creative/AestheticMapper.js';
10
+ import { ChoreographyPlayer } from '../../creative/ChoreographyPlayer.js';
11
+ import { ParameterTimeline } from '../../creative/ParameterTimeline.js';
12
+ import { ColorPresetsSystem } from '../../creative/ColorPresetsSystem.js';
13
+ import { TransitionAnimator } from '../../creative/TransitionAnimator.js';
14
+ import { PRESET_REGISTRY } from '../../render/LayerRelationshipGraph.js';
9
15
 
10
16
  /**
11
17
  * Generate unique IDs
@@ -38,6 +44,36 @@ export class MCPServer {
38
44
  this.engine = engine;
39
45
  this.sceneId = null;
40
46
  this.initialized = false;
47
+ this._gallerySlots = new Map();
48
+ }
49
+
50
+ /**
51
+ * Get or lazily create ColorPresetsSystem instance.
52
+ * Requires engine for the parameter update callback.
53
+ * @returns {ColorPresetsSystem|null}
54
+ */
55
+ _getColorPresets() {
56
+ if (this._colorPresets) return this._colorPresets;
57
+ if (!this.engine) return null;
58
+ this._colorPresets = new ColorPresetsSystem(
59
+ (name, value) => this.engine.setParameter(name, value)
60
+ );
61
+ return this._colorPresets;
62
+ }
63
+
64
+ /**
65
+ * Get or lazily create TransitionAnimator instance.
66
+ * Requires engine for the parameter update/get callbacks.
67
+ * @returns {TransitionAnimator|null}
68
+ */
69
+ _getTransitionAnimator() {
70
+ if (this._transitionAnimator) return this._transitionAnimator;
71
+ if (!this.engine) return null;
72
+ this._transitionAnimator = new TransitionAnimator(
73
+ (name, value) => this.engine.setParameter(name, value),
74
+ (name) => this.engine.getParameter(name)
75
+ );
76
+ return this._transitionAnimator;
41
77
  }
42
78
 
43
79
  buildResponse(operation, data, options = {}) {
@@ -137,10 +173,13 @@ export class MCPServer {
137
173
  result = this.getParameterSchema();
138
174
  break;
139
175
  case 'get_sdk_context':
140
- result = this.getSDKContext();
176
+ result = this.getSDKContext(args);
177
+ break;
178
+ case 'inspect_layers':
179
+ result = this.inspectLayers(args);
141
180
  break;
142
- case 'verify_knowledge':
143
- result = this.verifyKnowledge(args);
181
+ case 'set_holographic_layer':
182
+ result = this.setHolographicLayer(args);
144
183
  break;
145
184
  // Reactivity tools (Phase 6.5)
146
185
  case 'set_reactivity_config':
@@ -163,6 +202,60 @@ export class MCPServer {
163
202
  case 'list_behavior_presets':
164
203
  result = this.listBehaviorPresets();
165
204
  break;
205
+ // Agent-power tools (Phase 7)
206
+ case 'describe_visual_state':
207
+ result = this.describeVisualState();
208
+ break;
209
+ case 'batch_set_parameters':
210
+ result = await this.batchSetParameters(args);
211
+ break;
212
+ case 'create_timeline':
213
+ result = this.createTimeline(args);
214
+ break;
215
+ case 'play_transition':
216
+ result = this.playTransition(args);
217
+ break;
218
+ case 'apply_color_preset':
219
+ result = this.applyColorPreset(args);
220
+ break;
221
+ case 'set_post_processing':
222
+ result = this.setPostProcessing(args);
223
+ break;
224
+ case 'create_choreography':
225
+ result = this.createChoreography(args);
226
+ break;
227
+ // Visual feedback tools (Phase 7.1)
228
+ case 'capture_screenshot':
229
+ result = await this.captureScreenshot(args);
230
+ break;
231
+ case 'design_from_description':
232
+ result = await this.designFromDescription(args);
233
+ break;
234
+ case 'get_aesthetic_vocabulary':
235
+ result = this.getAestheticVocabulary();
236
+ break;
237
+ case 'play_choreography':
238
+ result = await this.playChoreographyTool(args);
239
+ break;
240
+ case 'control_timeline':
241
+ result = this.controlTimeline(args);
242
+ break;
243
+ // Layer relationship tools (Phase 8)
244
+ case 'set_layer_profile':
245
+ result = this.setLayerProfile(args);
246
+ break;
247
+ case 'set_layer_relationship':
248
+ result = this.setLayerRelationship(args);
249
+ break;
250
+ case 'set_layer_keystone':
251
+ result = this.setLayerKeystone(args);
252
+ break;
253
+ case 'get_layer_config':
254
+ result = this.getLayerConfig();
255
+ break;
256
+ case 'tune_layer_relationship':
257
+ result = this.tuneLayerRelationship(args);
258
+ break;
166
259
  default:
167
260
  throw new Error(`Unknown tool: ${toolName}`);
168
261
  }
@@ -426,32 +519,68 @@ export class MCPServer {
426
519
 
427
520
  telemetry.recordEvent(EventType.GALLERY_SAVE, { slot });
428
521
 
522
+ // Persist actual engine state if available
523
+ if (this.engine) {
524
+ const state = this.engine.exportState();
525
+ this._gallerySlots.set(slot, {
526
+ name: name || `Variation ${slot}`,
527
+ saved_at: new Date().toISOString(),
528
+ state
529
+ });
530
+ }
531
+
429
532
  return {
430
533
  slot,
431
534
  name: name || `Variation ${slot}`,
432
535
  saved_at: new Date().toISOString(),
536
+ persisted: !!this.engine,
537
+ gallery_size: this._gallerySlots.size,
433
538
  suggested_next_actions: ['load_from_gallery', 'randomize_parameters']
434
539
  };
435
540
  }
436
541
 
437
542
  /**
438
- * Load from gallery
543
+ * Load from gallery — restores previously saved state
439
544
  */
440
545
  loadFromGallery(args) {
441
546
  const { slot } = args;
442
547
 
443
- if (this.engine) {
444
- // Apply variation
445
- const params = this.engine.parameters?.generateVariationParameters?.(slot) || {};
446
- this.engine.setParameters(params);
548
+ telemetry.recordEvent(EventType.GALLERY_LOAD, { slot });
549
+
550
+ const saved = this._gallerySlots.get(slot);
551
+ if (saved && this.engine) {
552
+ // Restore saved state
553
+ this.engine.importState(saved.state);
554
+ return {
555
+ slot,
556
+ name: saved.name,
557
+ saved_at: saved.saved_at,
558
+ loaded_at: new Date().toISOString(),
559
+ restored: true,
560
+ ...this.getState()
561
+ };
447
562
  }
448
563
 
449
- telemetry.recordEvent(EventType.GALLERY_LOAD, { slot });
564
+ if (!saved) {
565
+ // No saved state — fall back to random variation
566
+ if (this.engine) {
567
+ const params = this.engine.parameters?.generateVariationParameters?.(slot) || {};
568
+ this.engine.setParameters(params);
569
+ }
570
+ return {
571
+ slot,
572
+ loaded_at: new Date().toISOString(),
573
+ restored: false,
574
+ note: 'No saved state in this slot — generated random variation',
575
+ ...this.getState()
576
+ };
577
+ }
450
578
 
451
579
  return {
452
580
  slot,
453
581
  loaded_at: new Date().toISOString(),
454
- ...this.getState()
582
+ restored: false,
583
+ note: 'Engine not initialized — cannot apply state'
455
584
  };
456
585
  }
457
586
 
@@ -524,162 +653,218 @@ export class MCPServer {
524
653
  /**
525
654
  * Get SDK context for agent onboarding
526
655
  */
527
- getSDKContext() {
528
- return {
529
- sdk_name: 'VIB3+ SDK',
530
- version: '1.9.0',
531
- purpose: 'General-purpose 4D rotation visualization SDK for plugins, extensions, wearables, and agentic use',
532
-
533
- quick_reference: {
534
- active_visualization_systems: 3,
535
- placeholder_systems: 1,
536
- rotation_planes: 6,
537
- base_geometries: 8,
538
- core_warp_types: 3,
539
- total_geometries: 24,
540
- canvas_layers_per_system: 5
541
- },
542
-
543
- systems: {
544
- ACTIVE: [
545
- { name: 'quantum', description: 'Complex quantum lattice visualizations with 24 geometries' },
546
- { name: 'faceted', description: 'Clean 2D geometric patterns with 4D rotation' },
547
- { name: 'holographic', description: '5-layer audio-reactive holographic effects' }
548
- ],
549
- PLACEHOLDER_TBD: [
550
- { name: 'polychora', status: 'TBD', description: '4D polytopes - placeholder, not production ready' }
551
- ]
656
+ getSDKContext(args = {}) {
657
+ const { include_state = true, include_tools = false } = args;
658
+
659
+ const context = {
660
+ sdk: 'VIB3+ 4D Visualization Engine',
661
+ version: '2.0.3',
662
+
663
+ // Capability manifest — what this engine can do
664
+ capabilities: {
665
+ systems: ['quantum', 'faceted', 'holographic'],
666
+ geometries: { count: 24, formula: 'core_type * 8 + base_geometry', base: 8, warps: 3 },
667
+ rotation: { planes: 6, '3D': ['XY', 'XZ', 'YZ'], '4D': ['XW', 'YW', 'ZW'], range: '±6.28 rad' },
668
+ layers: { count: 5, roles: ['background', 'shadow', 'content', 'highlight', 'accent'], addressable: true },
669
+ audio: { bands: ['bass', 'mid', 'high'], modes: ['add', 'multiply', 'replace', 'max', 'min'] },
670
+ input: { sources: ['deviceTilt', 'mousePosition', 'gyroscope', 'gamepad', 'perspective', 'programmatic', 'audio', 'midi'] },
671
+ creative: {
672
+ color_presets: 22,
673
+ easing_functions: 14,
674
+ post_effects: 14,
675
+ aesthetic_keywords: '130+',
676
+ choreography: true,
677
+ timeline_bpm_sync: true
678
+ },
679
+ environment: {
680
+ browser: typeof document !== 'undefined',
681
+ screenshot: typeof document !== 'undefined',
682
+ webgpu: typeof navigator !== 'undefined' && !!navigator.gpu,
683
+ wasm: typeof WebAssembly !== 'undefined'
684
+ }
552
685
  },
553
686
 
554
- geometry_encoding: {
555
- formula: 'geometry_index = core_index * 8 + base_index',
556
- base_geometries: ['tetrahedron', 'hypercube', 'sphere', 'torus', 'klein_bottle', 'fractal', 'wave', 'crystal'],
557
- core_types: ['base (0)', 'hypersphere (1)', 'hypertetrahedron (2)'],
558
- example: 'geometry 10 = hypersphere(sphere) because 1*8+2=10'
687
+ // Parameter ranges — the agent needs these to generate valid values
688
+ parameter_ranges: {
689
+ geometry: { min: 0, max: 23, type: 'integer' },
690
+ hue: { min: 0, max: 360, type: 'integer' },
691
+ saturation: { min: 0, max: 1 },
692
+ intensity: { min: 0, max: 1 },
693
+ speed: { min: 0.1, max: 3 },
694
+ chaos: { min: 0, max: 1 },
695
+ morphFactor: { min: 0, max: 2 },
696
+ gridDensity: { min: 4, max: 100 },
697
+ dimension: { min: 3.0, max: 4.5 },
698
+ rot4dXY: { min: -6.28, max: 6.28 },
699
+ rot4dXW: { min: -6.28, max: 6.28 }
559
700
  },
560
701
 
561
- rotation_planes: {
562
- total: 6,
563
- '3D_space': ['XY', 'XZ', 'YZ'],
564
- '4D_hyperspace': ['XW', 'YW', 'ZW'],
565
- range: '-6.28 to 6.28 radians'
702
+ // Workflow hints — what tool sequences accomplish creative goals
703
+ workflows: {
704
+ quick_design: 'design_from_description describe_visual_state → batch_set_parameters',
705
+ choreography: 'create_choreography play_choreography → describe_visual_state',
706
+ evolve: 'batch_set_parameters describe_visual_state → batch_set_parameters (iterate)',
707
+ layer_control: 'inspect_layers → set_holographic_layer → inspect_layers',
708
+ audio_reactive: 'configure_audio_band → apply_behavior_preset → describe_visual_state'
566
709
  },
567
710
 
568
- canvas_layers: {
569
- count: 5,
570
- names: ['background', 'shadow', 'content', 'highlight', 'accent']
571
- },
711
+ // Geometry quick-reference
712
+ geometry_map: {
713
+ 'sphere': 2, 'hypersphere+sphere': 10, 'hypertetra+sphere': 18,
714
+ 'torus': 3, 'hypersphere+torus': 11, 'hypertetra+torus': 19,
715
+ 'fractal': 5, 'hypersphere+fractal': 13, 'hypertetra+fractal': 21,
716
+ 'crystal': 7, 'wave': 6, 'klein_bottle': 4, 'hypercube': 1, 'tetrahedron': 0
717
+ }
718
+ };
572
719
 
573
- knowledge_quiz: {
574
- IMPORTANT: 'Call verify_knowledge with multiple choice answers (a/b/c/d) to confirm understanding',
575
- questions: [
576
- 'Q1: How many rotation planes? a)3 b)4 c)6 d)8',
577
- 'Q2: Geometry encoding formula? a)base*3+core b)core*8+base c)base+core d)core*base',
578
- 'Q3: Canvas layers per system? a)3 b)4 c)5 d)6',
579
- 'Q4: Which are the 3 ACTIVE systems? a)quantum,faceted,holographic b)quantum,faceted,polychora c)all four d)none',
580
- 'Q5: How many base geometry types? a)6 b)8 c)10 d)24',
581
- 'Q6: Core warp types? a)base,sphere,cube b)base,hypersphere,hypertetrahedron c)2D,3D,4D d)none'
582
- ]
583
- },
720
+ if (include_state) {
721
+ context.current_state = this.getState();
722
+ }
584
723
 
585
- documentation: {
586
- primary: 'DOCS/SYSTEM_INVENTORY.md',
587
- geometry: '24-GEOMETRY-6D-ROTATION-SUMMARY.md',
588
- controls: 'DOCS/CONTROL_REFERENCE.md',
589
- cli: 'DOCS/CLI_ONBOARDING.md'
590
- },
724
+ if (include_tools) {
725
+ context.tool_summary = Object.entries(toolDefinitions).map(([name, def]) => ({
726
+ name,
727
+ description: def.description.split('.')[0] // First sentence only
728
+ }));
729
+ }
591
730
 
592
- suggested_next_actions: ['verify_knowledge', 'create_4d_visualization', 'search_geometries']
593
- };
731
+ return context;
594
732
  }
595
733
 
596
734
  /**
597
- * Verify agent knowledge of SDK (multiple choice)
735
+ * Inspect holographic layer state returns per-layer metadata in JSON format.
736
+ * This replaces the old verify_knowledge quiz with something actually useful.
598
737
  */
599
- verifyKnowledge(answers) {
600
- const correctAnswers = {
601
- q1_rotation_planes: 'c', // 6 rotation planes
602
- q2_geometry_formula: 'b', // core*8+base
603
- q3_canvas_layers: 'c', // 5 layers
604
- q4_active_systems: 'a', // quantum, faceted, holographic (polychora is TBD)
605
- q5_base_geometries: 'b', // 8 base geometries
606
- q6_core_types: 'b' // base, hypersphere, hypertetrahedron
738
+ inspectLayers(args = {}) {
739
+ const { layer = 'all' } = args;
740
+
741
+ const systemName = this.engine?.currentSystemName || 'unknown';
742
+ const isHolographic = systemName === 'holographic';
743
+
744
+ if (!isHolographic) {
745
+ return {
746
+ system: systemName,
747
+ note: 'Layer inspection is most detailed for the holographic system (5 independent canvas layers). Switch with switch_system("holographic").',
748
+ layers: [{
749
+ role: 'main',
750
+ system: systemName,
751
+ opacity: 1.0,
752
+ enabled: true,
753
+ description: `Single canvas for ${systemName} system`
754
+ }],
755
+ suggested_next_actions: ['switch_system', 'describe_visual_state']
756
+ };
757
+ }
758
+
759
+ // Build layer metadata from the holographic system
760
+ const system = this.engine?.currentSystem;
761
+ const layerRoles = ['background', 'shadow', 'content', 'highlight', 'accent'];
762
+ const defaultConfigs = {
763
+ background: { densityMult: 0.4, speedMult: 0.2, colorShift: 0, intensity: 0.2, reactivity: 0.5 },
764
+ shadow: { densityMult: 0.8, speedMult: 0.3, colorShift: 180, intensity: 0.4, reactivity: 0.7 },
765
+ content: { densityMult: 1.0, speedMult: 1.0, colorShift: 0, intensity: 1.0, reactivity: 0.9 },
766
+ highlight: { densityMult: 1.5, speedMult: 0.8, colorShift: 60, intensity: 0.6, reactivity: 1.1 },
767
+ accent: { densityMult: 2.5, speedMult: 0.4, colorShift: 300, intensity: 0.3, reactivity: 1.5 }
607
768
  };
608
769
 
609
- const docReferences = {
610
- q1_rotation_planes: {
611
- topic: '6D ROTATION SYSTEM',
612
- doc: 'DOCS/SYSTEM_INVENTORY.md#the-6d-rotation-system',
613
- reason: '6 planes: XY, XZ, YZ (3D) + XW, YW, ZW (4D hyperspace)'
614
- },
615
- q2_geometry_formula: {
616
- topic: 'GEOMETRY ENCODING',
617
- doc: '24-GEOMETRY-6D-ROTATION-SUMMARY.md',
618
- reason: 'geometry = coreIndex * 8 + baseIndex. Example: 10 = 1*8+2 = hypersphere+sphere'
619
- },
620
- q3_canvas_layers: {
621
- topic: 'CANVAS LAYER SYSTEM',
622
- doc: 'DOCS/SYSTEM_INVENTORY.md#the-4-visualization-systems',
623
- reason: '5 layers: background, shadow, content, highlight, accent'
624
- },
625
- q4_active_systems: {
626
- topic: 'ACTIVE VS PLACEHOLDER SYSTEMS',
627
- doc: 'DOCS/SYSTEM_INVENTORY.md',
628
- reason: 'Only 3 ACTIVE: quantum, faceted, holographic. Polychora is TBD/placeholder!'
629
- },
630
- q5_base_geometries: {
631
- topic: 'BASE GEOMETRY TYPES',
632
- doc: '24-GEOMETRY-6D-ROTATION-SUMMARY.md',
633
- reason: '8 base: tetrahedron, hypercube, sphere, torus, klein, fractal, wave, crystal'
634
- },
635
- q6_core_types: {
636
- topic: 'CORE WARP TYPES',
637
- doc: '24-GEOMETRY-6D-ROTATION-SUMMARY.md',
638
- reason: '3 cores: base (no warp), hypersphere, hypertetrahedron'
639
- }
770
+ const buildLayerInfo = (role) => {
771
+ const config = defaultConfigs[role] || {};
772
+ const visualizer = system?.visualizers?.find?.(v => v?.role === role);
773
+ const overrides = this._layerOverrides?.get(role) || {};
774
+
775
+ return {
776
+ role,
777
+ enabled: overrides.enabled !== undefined ? overrides.enabled : true,
778
+ opacity: overrides.opacity !== undefined ? overrides.opacity : 1.0,
779
+ blendMode: overrides.blendMode || 'normal',
780
+ densityMult: overrides.densityMult ?? config.densityMult,
781
+ speedMult: overrides.speedMult ?? config.speedMult,
782
+ colorShift: overrides.colorShift ?? config.colorShift,
783
+ intensity: overrides.intensity ?? config.intensity,
784
+ reactivity: overrides.reactivity ?? config.reactivity,
785
+ has_visualizer: !!visualizer
786
+ };
640
787
  };
641
788
 
642
- const results = {
643
- score: 0,
644
- max_score: 6,
645
- details: [],
646
- REVIEW_REQUIRED: []
647
- };
648
-
649
- // Check each answer
650
- for (const [question, correct] of Object.entries(correctAnswers)) {
651
- const given = answers[question]?.toLowerCase?.() || answers[question];
652
- if (given === correct) {
653
- results.score++;
654
- results.details.push({ question, status: '✓ CORRECT' });
655
- } else if (given !== undefined) {
656
- results.details.push({
657
- question,
658
- status: '✗ WRONG',
659
- your_answer: given,
660
- correct_answer: correct
661
- });
662
- results.REVIEW_REQUIRED.push(docReferences[question]);
663
- }
664
- }
789
+ const layers = layer === 'all'
790
+ ? layerRoles.map(buildLayerInfo)
791
+ : [buildLayerInfo(layer)];
665
792
 
666
- results.percentage = Math.round((results.score / results.max_score) * 100);
793
+ return {
794
+ system: 'holographic',
795
+ layer_count: layerRoles.length,
796
+ layers,
797
+ suggested_next_actions: ['set_holographic_layer', 'batch_set_parameters', 'describe_visual_state']
798
+ };
799
+ }
667
800
 
668
- // Build response
669
- if (results.REVIEW_REQUIRED.length > 0) {
670
- results.MESSAGE = `Score: ${results.score}/${results.max_score}. YOU MAY PROCEED but PLEASE review the topics below to avoid errors.`;
671
- results.URGENT = results.REVIEW_REQUIRED.map(ref => ({
672
- TOPIC: ref.topic,
673
- READ: ref.doc,
674
- WHY: ref.reason
675
- }));
676
- } else {
677
- results.MESSAGE = `PERFECT SCORE! You understand the VIB3+ SDK architecture.`;
801
+ /**
802
+ * Set properties on an individual holographic layer.
803
+ * Stores overrides in a layer override map and applies them to the visualizer.
804
+ */
805
+ setHolographicLayer(args) {
806
+ const { layer, opacity, blendMode, enabled, colorShift, densityMult, speedMult, reactivity } = args;
807
+
808
+ const systemName = this.engine?.currentSystemName || 'unknown';
809
+ if (systemName !== 'holographic') {
810
+ return {
811
+ error: {
812
+ type: 'SystemError',
813
+ code: 'NOT_HOLOGRAPHIC',
814
+ message: `set_holographic_layer requires holographic system (current: ${systemName})`,
815
+ suggestion: 'Call switch_system("holographic") first'
816
+ }
817
+ };
678
818
  }
679
819
 
680
- results.suggested_next_actions = ['create_4d_visualization', 'get_state', 'search_geometries'];
820
+ // Initialize layer override storage
821
+ if (!this._layerOverrides) this._layerOverrides = new Map();
822
+ const existing = this._layerOverrides.get(layer) || {};
823
+ const updates = {};
824
+
825
+ if (opacity !== undefined) { existing.opacity = opacity; updates.opacity = opacity; }
826
+ if (blendMode !== undefined) { existing.blendMode = blendMode; updates.blendMode = blendMode; }
827
+ if (enabled !== undefined) { existing.enabled = enabled; updates.enabled = enabled; }
828
+ if (colorShift !== undefined) { existing.colorShift = colorShift; updates.colorShift = colorShift; }
829
+ if (densityMult !== undefined) { existing.densityMult = densityMult; updates.densityMult = densityMult; }
830
+ if (speedMult !== undefined) { existing.speedMult = speedMult; updates.speedMult = speedMult; }
831
+ if (reactivity !== undefined) { existing.reactivity = reactivity; updates.reactivity = reactivity; }
832
+
833
+ this._layerOverrides.set(layer, existing);
834
+
835
+ // Apply to visualizer if available
836
+ const system = this.engine?.currentSystem;
837
+ const visualizer = system?.visualizers?.find?.(v => v?.role === layer);
838
+ if (visualizer) {
839
+ if (opacity !== undefined && visualizer.canvas) {
840
+ visualizer.canvas.style.opacity = String(opacity);
841
+ }
842
+ if (blendMode !== undefined && visualizer.canvas) {
843
+ visualizer.canvas.style.mixBlendMode = blendMode;
844
+ }
845
+ if (enabled !== undefined && visualizer.canvas) {
846
+ visualizer.canvas.style.display = enabled ? '' : 'none';
847
+ }
848
+ if (colorShift !== undefined && visualizer.roleParams) {
849
+ visualizer.roleParams.colorShift = colorShift;
850
+ }
851
+ if (densityMult !== undefined && visualizer.roleParams) {
852
+ visualizer.roleParams.densityMult = densityMult;
853
+ }
854
+ if (speedMult !== undefined && visualizer.roleParams) {
855
+ visualizer.roleParams.speedMult = speedMult;
856
+ }
857
+ if (reactivity !== undefined) {
858
+ visualizer.reactivity = reactivity;
859
+ }
860
+ }
681
861
 
682
- return results;
862
+ return {
863
+ layer,
864
+ applied: updates,
865
+ current_state: this.inspectLayers({ layer }).layers[0],
866
+ suggested_next_actions: ['inspect_layers', 'set_holographic_layer', 'describe_visual_state']
867
+ };
683
868
  }
684
869
 
685
870
  /**
@@ -943,6 +1128,897 @@ export class MCPServer {
943
1128
  suggested_next_actions: ['apply_behavior_preset']
944
1129
  };
945
1130
  }
1131
+
1132
+ // ===== AGENT-POWER TOOLS (Phase 7 — Agent Harness) =====
1133
+
1134
+ /**
1135
+ * Generate a natural-language description of the current visual state.
1136
+ * Enables text-only agents to "see" what the visualization looks like.
1137
+ */
1138
+ describeVisualState() {
1139
+ const state = this.getState();
1140
+ const params = state.visual || {};
1141
+ const rotation = state.rotation_state || {};
1142
+ const geometry = state.geometry || {};
1143
+
1144
+ // Color description from hue
1145
+ const hue = params.hue || 0;
1146
+ const colorName = hue < 15 ? 'red' : hue < 45 ? 'orange' : hue < 75 ? 'yellow' :
1147
+ hue < 150 ? 'green' : hue < 195 ? 'cyan' : hue < 255 ? 'blue' :
1148
+ hue < 285 ? 'purple' : hue < 330 ? 'magenta' : 'red';
1149
+ const satDesc = (params.saturation || 0.8) > 0.7 ? 'vivid' :
1150
+ (params.saturation || 0.8) > 0.4 ? 'moderate' : 'desaturated';
1151
+ const intensityDesc = (params.intensity || 0.5) > 0.7 ? 'bright' :
1152
+ (params.intensity || 0.5) > 0.3 ? 'medium brightness' : 'dim';
1153
+
1154
+ // Motion description
1155
+ const speed = params.speed || 1.0;
1156
+ const speedDesc = speed > 2.0 ? 'rapidly' : speed > 1.0 ? 'moderately' :
1157
+ speed > 0.4 ? 'slowly' : 'very slowly';
1158
+ const chaos = params.chaos || 0;
1159
+ const chaosDesc = chaos > 0.7 ? 'highly turbulent' : chaos > 0.3 ? 'somewhat organic' :
1160
+ chaos > 0.05 ? 'subtly alive' : 'perfectly still';
1161
+
1162
+ // 4D rotation activity
1163
+ const has4D = Math.abs(rotation.XW || 0) > 0.1 ||
1164
+ Math.abs(rotation.YW || 0) > 0.1 ||
1165
+ Math.abs(rotation.ZW || 0) > 0.1;
1166
+ const rotDesc = has4D ? 'with visible 4D hyperspace rotation (inside-out morphing)' :
1167
+ 'in standard 3D orientation';
1168
+
1169
+ // Density/complexity
1170
+ const density = params.gridDensity || 10;
1171
+ const densityDesc = density > 50 ? 'extremely intricate' : density > 25 ? 'detailed' :
1172
+ density > 12 ? 'moderate detail' : 'bold and sparse';
1173
+
1174
+ // Projection
1175
+ const dim = params.dimension || 3.8;
1176
+ const projDesc = dim < 3.3 ? 'dramatic fish-eye distortion' :
1177
+ dim < 3.8 ? 'moderate perspective depth' : 'subtle, flattened perspective';
1178
+
1179
+ const description = [
1180
+ `A ${satDesc} ${colorName} ${geometry.core_type || 'base'} ${geometry.base_type || 'tetrahedron'}`,
1181
+ `rendered in the ${state.system || 'quantum'} system.`,
1182
+ `The pattern is ${densityDesc} and ${chaosDesc},`,
1183
+ `animating ${speedDesc} ${rotDesc}.`,
1184
+ `Color is ${intensityDesc} with ${projDesc}.`,
1185
+ params.morphFactor > 0.5 ? `Shape is morphing between geometries (factor: ${params.morphFactor}).` : ''
1186
+ ].filter(Boolean).join(' ');
1187
+
1188
+ return {
1189
+ description,
1190
+ mood: this._assessMood(params),
1191
+ complexity: density > 40 ? 'high' : density > 15 ? 'medium' : 'low',
1192
+ motion_level: speed > 1.5 ? 'high' : speed > 0.5 ? 'medium' : 'low',
1193
+ has_4d_rotation: has4D,
1194
+ color_family: colorName,
1195
+ suggested_next_actions: ['set_visual_parameters', 'set_rotation', 'batch_set_parameters']
1196
+ };
1197
+ }
1198
+
1199
+ /**
1200
+ * Assess the emotional mood of the current visual state
1201
+ */
1202
+ _assessMood(params) {
1203
+ const hue = params.hue || 0;
1204
+ const speed = params.speed || 1.0;
1205
+ const chaos = params.chaos || 0;
1206
+ const intensity = params.intensity || 0.5;
1207
+
1208
+ if (speed < 0.3 && chaos < 0.1) return 'serene';
1209
+ if (speed > 2.0 && chaos > 0.6) return 'chaotic';
1210
+ if (hue > 180 && hue < 260 && intensity < 0.5) return 'mysterious';
1211
+ if (hue > 0 && hue < 60 && intensity > 0.6) return 'warm';
1212
+ if (hue > 150 && hue < 200 && speed < 0.8) return 'tranquil';
1213
+ if (chaos > 0.5 && speed > 1.5) return 'energetic';
1214
+ if (intensity > 0.8) return 'vibrant';
1215
+ return 'balanced';
1216
+ }
1217
+
1218
+ /**
1219
+ * Atomically set multiple parameter categories in one call
1220
+ */
1221
+ async batchSetParameters(args) {
1222
+ const { system, geometry, rotation, visual, preset } = args;
1223
+
1224
+ // Switch system first if requested
1225
+ if (system && this.engine) {
1226
+ await this.engine.switchSystem(system);
1227
+ }
1228
+
1229
+ // Set geometry
1230
+ if (geometry !== undefined && this.engine) {
1231
+ this.engine.setParameter('geometry', geometry);
1232
+ }
1233
+
1234
+ // Set rotation
1235
+ if (rotation) {
1236
+ const rotMap = { XY: 'rot4dXY', XZ: 'rot4dXZ', YZ: 'rot4dYZ',
1237
+ XW: 'rot4dXW', YW: 'rot4dYW', ZW: 'rot4dZW' };
1238
+ for (const [key, value] of Object.entries(rotation)) {
1239
+ if (value !== undefined && this.engine) {
1240
+ this.engine.setParameter(rotMap[key], value);
1241
+ }
1242
+ }
1243
+ }
1244
+
1245
+ // Set visual parameters
1246
+ if (visual && this.engine) {
1247
+ for (const [key, value] of Object.entries(visual)) {
1248
+ this.engine.setParameter(key, value);
1249
+ }
1250
+ }
1251
+
1252
+ // Apply preset last (overrides relevant params)
1253
+ if (preset) {
1254
+ this.applyBehaviorPreset({ preset });
1255
+ }
1256
+
1257
+ telemetry.recordEvent(EventType.PARAMETER_BATCH_CHANGE, {
1258
+ count: (rotation ? Object.keys(rotation).length : 0) +
1259
+ (visual ? Object.keys(visual).length : 0) +
1260
+ (system ? 1 : 0) + (geometry !== undefined ? 1 : 0)
1261
+ });
1262
+
1263
+ return {
1264
+ ...this.getState(),
1265
+ batch_applied: true,
1266
+ suggested_next_actions: ['describe_visual_state', 'save_to_gallery', 'create_timeline']
1267
+ };
1268
+ }
1269
+
1270
+ /**
1271
+ * Create a ParameterTimeline from agent specification
1272
+ */
1273
+ createTimeline(args) {
1274
+ const { name, duration_ms, bpm, loop_mode = 'once', tracks } = args;
1275
+
1276
+ const timelineId = generateId('timeline');
1277
+
1278
+ // Validate tracks have properly sorted keyframes
1279
+ const validatedTracks = {};
1280
+ for (const [param, keyframes] of Object.entries(tracks)) {
1281
+ validatedTracks[param] = keyframes
1282
+ .map(kf => ({
1283
+ time: Math.max(0, Math.min(kf.time, duration_ms)),
1284
+ value: kf.value,
1285
+ easing: kf.easing || 'easeInOut'
1286
+ }))
1287
+ .sort((a, b) => a.time - b.time);
1288
+ }
1289
+
1290
+ // Build timeline data for ParameterTimeline consumption
1291
+ const timelineData = {
1292
+ id: timelineId,
1293
+ name: name || `Timeline ${timelineId}`,
1294
+ duration: duration_ms,
1295
+ bpm: bpm || null,
1296
+ loopMode: loop_mode,
1297
+ tracks: validatedTracks
1298
+ };
1299
+
1300
+ // If engine is available, create and start the timeline
1301
+ if (this.engine) {
1302
+ // Store for later retrieval
1303
+ if (!this._timelines) this._timelines = new Map();
1304
+ this._timelines.set(timelineId, timelineData);
1305
+ }
1306
+
1307
+ return {
1308
+ timeline_id: timelineId,
1309
+ name: timelineData.name,
1310
+ duration_ms,
1311
+ bpm: bpm || null,
1312
+ loop_mode,
1313
+ track_count: Object.keys(validatedTracks).length,
1314
+ tracks_summary: Object.entries(validatedTracks).map(([param, kfs]) => ({
1315
+ parameter: param,
1316
+ keyframe_count: kfs.length,
1317
+ value_range: [
1318
+ Math.min(...kfs.map(k => k.value)),
1319
+ Math.max(...kfs.map(k => k.value))
1320
+ ]
1321
+ })),
1322
+ load_code: `const tl = new ParameterTimeline((n, v) => engine.setParameter(n, v));\ntl.importTimeline(${JSON.stringify(timelineData)});\ntl.play();`,
1323
+ suggested_next_actions: ['play_transition', 'describe_visual_state', 'save_to_gallery']
1324
+ };
1325
+ }
1326
+
1327
+ /**
1328
+ * Play a smooth transition sequence
1329
+ */
1330
+ playTransition(args) {
1331
+ const { sequence } = args;
1332
+
1333
+ const transitionId = generateId('transition');
1334
+
1335
+ // Validate and normalize the sequence
1336
+ const normalizedSequence = sequence.map((step, i) => ({
1337
+ params: step.params,
1338
+ duration: step.duration || 1000,
1339
+ easing: step.easing || 'easeInOut',
1340
+ delay: step.delay || 0
1341
+ }));
1342
+
1343
+ const totalDuration = normalizedSequence.reduce(
1344
+ (sum, step) => sum + step.duration + step.delay, 0
1345
+ );
1346
+
1347
+ // Execute live if engine available
1348
+ let executing = false;
1349
+ const animator = this._getTransitionAnimator();
1350
+ if (animator) {
1351
+ const seqId = animator.sequence(normalizedSequence);
1352
+ executing = !!seqId;
1353
+ }
1354
+
1355
+ return {
1356
+ transition_id: transitionId,
1357
+ executing,
1358
+ step_count: normalizedSequence.length,
1359
+ total_duration_ms: totalDuration,
1360
+ steps: normalizedSequence.map((step, i) => ({
1361
+ index: i,
1362
+ params: Object.keys(step.params),
1363
+ duration: step.duration,
1364
+ easing: step.easing,
1365
+ delay: step.delay
1366
+ })),
1367
+ load_code: executing ? null : `const animator = new TransitionAnimator(\n (n, v) => engine.setParameter(n, v),\n (n) => engine.getParameter(n)\n);\nanimator.sequence(${JSON.stringify(normalizedSequence)});`,
1368
+ suggested_next_actions: ['describe_visual_state', 'create_timeline', 'save_to_gallery']
1369
+ };
1370
+ }
1371
+
1372
+ /**
1373
+ * Apply a named color preset
1374
+ */
1375
+ applyColorPreset(args) {
1376
+ const { preset, transition = true, duration = 800 } = args;
1377
+
1378
+ const colorSystem = this._getColorPresets();
1379
+
1380
+ if (colorSystem) {
1381
+ // Use real ColorPresetsSystem — full preset library with transitions
1382
+ const config = colorSystem.getPreset(preset);
1383
+ if (!config) {
1384
+ const allPresets = colorSystem.getPresets().map(p => p.name);
1385
+ return {
1386
+ error: {
1387
+ type: 'ValidationError',
1388
+ code: 'INVALID_COLOR_PRESET',
1389
+ message: `Unknown color preset: ${preset}`,
1390
+ valid_options: allPresets
1391
+ }
1392
+ };
1393
+ }
1394
+
1395
+ colorSystem.applyPreset(preset, transition, duration);
1396
+
1397
+ return {
1398
+ preset,
1399
+ applied: { hue: config.hue, saturation: config.saturation, intensity: config.intensity },
1400
+ transition: transition ? { enabled: true, duration } : { enabled: false },
1401
+ full_config: config,
1402
+ suggested_next_actions: ['set_post_processing', 'describe_visual_state', 'set_visual_parameters']
1403
+ };
1404
+ }
1405
+
1406
+ // Fallback: no engine, return preset metadata for artifact mode
1407
+ return {
1408
+ preset,
1409
+ applied: null,
1410
+ load_code: `const colors = new ColorPresetsSystem((n, v) => engine.setParameter(n, v));\ncolors.applyPreset('${preset}', ${transition}, ${duration});`,
1411
+ suggested_next_actions: ['set_post_processing', 'describe_visual_state', 'set_visual_parameters']
1412
+ };
1413
+ }
1414
+
1415
+ /**
1416
+ * Configure post-processing effects pipeline
1417
+ */
1418
+ setPostProcessing(args) {
1419
+ const { effects, chain_preset, clear_first = true } = args;
1420
+
1421
+ // Try to execute live in browser context
1422
+ let executing = false;
1423
+ if (typeof document !== 'undefined') {
1424
+ try {
1425
+ const target = document.getElementById('viz-container')
1426
+ || document.querySelector('.vib3-container')
1427
+ || document.querySelector('canvas')?.parentElement;
1428
+
1429
+ if (target) {
1430
+ // Lazy-init pipeline, importing dynamically to avoid Node.js issues
1431
+ if (!this._postPipeline) {
1432
+ // PostProcessingPipeline imported statically would fail in Node;
1433
+ // it's already a known browser-only module, so guard at runtime
1434
+ const { PostProcessingPipeline: PPP } = { PostProcessingPipeline: globalThis.PostProcessingPipeline };
1435
+ if (PPP) {
1436
+ this._postPipeline = new PPP(target);
1437
+ }
1438
+ }
1439
+
1440
+ if (this._postPipeline) {
1441
+ if (clear_first) this._postPipeline.clearChain?.();
1442
+ if (chain_preset) {
1443
+ this._postPipeline.loadPresetChain(chain_preset);
1444
+ } else if (effects) {
1445
+ for (const e of effects) {
1446
+ this._postPipeline.addEffect(e.name, { intensity: e.intensity || 0.5, ...e });
1447
+ }
1448
+ }
1449
+ this._postPipeline.apply();
1450
+ executing = true;
1451
+ }
1452
+ }
1453
+ } catch { /* fall through to code generation */ }
1454
+ }
1455
+
1456
+ return {
1457
+ applied: true,
1458
+ executing,
1459
+ effects: effects || [],
1460
+ chain_preset: chain_preset || null,
1461
+ cleared_previous: clear_first,
1462
+ load_code: executing ? null : (effects ?
1463
+ `const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));\n${effects.map(e => `pipeline.addEffect('${e.name}', { intensity: ${e.intensity || 0.5} });`).join('\n')}\npipeline.apply();` :
1464
+ `const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));\npipeline.loadPresetChain('${chain_preset}');\npipeline.apply();`),
1465
+ suggested_next_actions: ['describe_visual_state', 'apply_color_preset', 'create_choreography']
1466
+ };
1467
+ }
1468
+
1469
+ /**
1470
+ * Create a multi-scene choreography — the most powerful agent composition tool
1471
+ */
1472
+ createChoreography(args) {
1473
+ const { name, duration_ms, bpm, scenes } = args;
1474
+
1475
+ const choreographyId = generateId('choreo');
1476
+
1477
+ // Validate scene time ranges don't exceed duration
1478
+ const validatedScenes = scenes.map((scene, i) => ({
1479
+ index: i,
1480
+ time_start: Math.max(0, scene.time_start),
1481
+ time_end: Math.min(scene.time_end, duration_ms),
1482
+ system: scene.system,
1483
+ geometry: scene.geometry ?? 0,
1484
+ transition_in: scene.transition_in || { type: 'cut', duration: 0 },
1485
+ tracks: scene.tracks || {},
1486
+ color_preset: scene.color_preset || null,
1487
+ post_processing: scene.post_processing || [],
1488
+ audio: scene.audio || null
1489
+ }));
1490
+
1491
+ const choreography = {
1492
+ id: choreographyId,
1493
+ name: name || `Choreography ${choreographyId}`,
1494
+ duration_ms,
1495
+ bpm: bpm || null,
1496
+ scene_count: validatedScenes.length,
1497
+ scenes: validatedScenes
1498
+ };
1499
+
1500
+ // Store for later retrieval
1501
+ if (!this._choreographies) this._choreographies = new Map();
1502
+ this._choreographies.set(choreographyId, choreography);
1503
+
1504
+ return {
1505
+ choreography_id: choreographyId,
1506
+ name: choreography.name,
1507
+ duration_ms,
1508
+ bpm: bpm || null,
1509
+ scene_count: validatedScenes.length,
1510
+ scenes_summary: validatedScenes.map(s => ({
1511
+ index: s.index,
1512
+ time: `${s.time_start}ms → ${s.time_end}ms`,
1513
+ system: s.system,
1514
+ geometry: s.geometry,
1515
+ transition: s.transition_in.type,
1516
+ track_count: Object.keys(s.tracks).length,
1517
+ color_preset: s.color_preset,
1518
+ effects: s.post_processing
1519
+ })),
1520
+ choreography_json: JSON.stringify(choreography, null, 2),
1521
+ suggested_next_actions: ['describe_visual_state', 'export_package']
1522
+ };
1523
+ }
1524
+
1525
+ // ===== VISUAL FEEDBACK TOOLS (Phase 7.1 — Agent Harness) =====
1526
+
1527
+ /**
1528
+ * Capture the current visualization as a base64 PNG by compositing all canvas layers.
1529
+ * Only works in browser context where canvases exist.
1530
+ */
1531
+ async captureScreenshot(args) {
1532
+ const { width = 512, height = 512, format = 'png', quality = 0.92 } = args;
1533
+
1534
+ const isBrowser = typeof document !== 'undefined';
1535
+ if (!isBrowser) {
1536
+ return {
1537
+ error: {
1538
+ type: 'EnvironmentError',
1539
+ code: 'NO_BROWSER_CONTEXT',
1540
+ message: 'capture_screenshot requires a browser context with canvas elements',
1541
+ suggestion: 'Use the headless renderer tool (tools/headless-renderer.js) for Node.js environments'
1542
+ }
1543
+ };
1544
+ }
1545
+
1546
+ try {
1547
+ // Create composite canvas
1548
+ const composite = document.createElement('canvas');
1549
+ composite.width = width;
1550
+ composite.height = height;
1551
+ const ctx = composite.getContext('2d');
1552
+
1553
+ if (!ctx) {
1554
+ return {
1555
+ error: { type: 'SystemError', code: 'CANVAS_CONTEXT_FAILED', message: 'Could not get 2D context for compositing' }
1556
+ };
1557
+ }
1558
+
1559
+ // Fill with black background
1560
+ ctx.fillStyle = '#000000';
1561
+ ctx.fillRect(0, 0, width, height);
1562
+
1563
+ // Find all visualization canvases and composite them in z-order
1564
+ const canvases = document.querySelectorAll('canvas.visualization-canvas');
1565
+ const sortedCanvases = Array.from(canvases).sort((a, b) => {
1566
+ const zA = parseInt(a.style.zIndex || '0', 10);
1567
+ const zB = parseInt(b.style.zIndex || '0', 10);
1568
+ return zA - zB;
1569
+ });
1570
+
1571
+ for (const canvas of sortedCanvases) {
1572
+ if (canvas.width > 0 && canvas.height > 0) {
1573
+ ctx.drawImage(canvas, 0, 0, width, height);
1574
+ }
1575
+ }
1576
+
1577
+ // If no canvases found, try to find any canvas at all
1578
+ if (sortedCanvases.length === 0) {
1579
+ const anyCanvas = document.querySelector('canvas');
1580
+ if (anyCanvas && anyCanvas.width > 0) {
1581
+ ctx.drawImage(anyCanvas, 0, 0, width, height);
1582
+ }
1583
+ }
1584
+
1585
+ // Convert to data URL
1586
+ const mimeType = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png';
1587
+ const dataUrl = composite.toDataURL(mimeType, quality);
1588
+ const base64 = dataUrl.split(',')[1];
1589
+
1590
+ // Clean up
1591
+ composite.remove();
1592
+
1593
+ return {
1594
+ format,
1595
+ width,
1596
+ height,
1597
+ mime_type: mimeType,
1598
+ data_url: dataUrl,
1599
+ base64_length: base64.length,
1600
+ canvas_count: sortedCanvases.length,
1601
+ suggested_next_actions: ['describe_visual_state', 'set_visual_parameters', 'batch_set_parameters']
1602
+ };
1603
+ } catch (err) {
1604
+ return {
1605
+ error: {
1606
+ type: 'SystemError',
1607
+ code: 'SCREENSHOT_FAILED',
1608
+ message: err.message,
1609
+ suggestion: 'Check that canvas elements exist and are rendered'
1610
+ }
1611
+ };
1612
+ }
1613
+ }
1614
+
1615
+ /**
1616
+ * Map a natural-language description to VIB3+ parameters using AestheticMapper.
1617
+ */
1618
+ async designFromDescription(args) {
1619
+ const { description, apply = false } = args;
1620
+
1621
+ if (!this._aestheticMapper) {
1622
+ this._aestheticMapper = new AestheticMapper();
1623
+ }
1624
+
1625
+ const mapped = this._aestheticMapper.mapDescription(description);
1626
+ const resolved = this._aestheticMapper.resolveToValues(description);
1627
+
1628
+ // Apply to engine if requested
1629
+ if (apply && this.engine) {
1630
+ if (resolved.system) {
1631
+ await this.engine.switchSystem(resolved.system);
1632
+ }
1633
+ if (resolved.geometry !== undefined) {
1634
+ this.engine.setParameter('geometry', resolved.geometry);
1635
+ }
1636
+ for (const [param, value] of Object.entries(resolved.params)) {
1637
+ this.engine.setParameter(param, value);
1638
+ }
1639
+ }
1640
+
1641
+ return {
1642
+ description,
1643
+ applied: apply,
1644
+ matched_words: mapped.matched_words,
1645
+ total_words: mapped.total_words,
1646
+ resolved: {
1647
+ system: resolved.system,
1648
+ geometry: resolved.geometry,
1649
+ params: resolved.params,
1650
+ color_preset: resolved.color_preset,
1651
+ post_processing: resolved.post_processing
1652
+ },
1653
+ parameter_ranges: mapped.params,
1654
+ suggested_next_actions: apply
1655
+ ? ['describe_visual_state', 'capture_screenshot', 'create_timeline']
1656
+ : ['design_from_description', 'batch_set_parameters']
1657
+ };
1658
+ }
1659
+
1660
+ /**
1661
+ * Return the full aesthetic vocabulary by category.
1662
+ */
1663
+ getAestheticVocabulary() {
1664
+ if (!this._aestheticMapper) {
1665
+ this._aestheticMapper = new AestheticMapper();
1666
+ }
1667
+
1668
+ return {
1669
+ vocabulary: this._aestheticMapper.getVocabularyByCategory(),
1670
+ all_words: this._aestheticMapper.getVocabulary(),
1671
+ word_count: this._aestheticMapper.getVocabulary().length,
1672
+ usage: 'Pass space-separated words to design_from_description. Example: "serene ocean deep minimal"',
1673
+ suggested_next_actions: ['design_from_description']
1674
+ };
1675
+ }
1676
+
1677
+ /**
1678
+ * Load and play a choreography.
1679
+ */
1680
+ async playChoreographyTool(args) {
1681
+ const { choreography, choreography_id, action = 'play', seek_percent, loop = false } = args;
1682
+
1683
+ // Resolve choreography spec
1684
+ let spec = choreography;
1685
+ if (!spec && choreography_id && this._choreographies) {
1686
+ spec = this._choreographies.get(choreography_id);
1687
+ }
1688
+
1689
+ if (!spec && action === 'play') {
1690
+ return {
1691
+ error: {
1692
+ type: 'ValidationError',
1693
+ code: 'NO_CHOREOGRAPHY',
1694
+ message: 'Provide a choreography spec or a valid choreography_id',
1695
+ suggestion: 'Use create_choreography first, then pass the result here'
1696
+ }
1697
+ };
1698
+ }
1699
+
1700
+ // Create or reuse player
1701
+ if (!this._choreographyPlayer && this.engine) {
1702
+ this._choreographyPlayer = new ChoreographyPlayer(this.engine, {
1703
+ onSceneChange: (index, scene) => {
1704
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, {
1705
+ type: 'choreography_scene',
1706
+ scene_index: index,
1707
+ system: scene.system
1708
+ });
1709
+ }
1710
+ });
1711
+ }
1712
+
1713
+ if (!this._choreographyPlayer) {
1714
+ return {
1715
+ error: {
1716
+ type: 'SystemError',
1717
+ code: 'NO_ENGINE',
1718
+ message: 'Engine not initialized — cannot play choreography',
1719
+ suggestion: 'Initialize the VIB3Engine first'
1720
+ }
1721
+ };
1722
+ }
1723
+
1724
+ const player = this._choreographyPlayer;
1725
+
1726
+ switch (action) {
1727
+ case 'play':
1728
+ if (spec) {
1729
+ player.load(spec);
1730
+ player.loopMode = loop ? 'loop' : 'once';
1731
+ }
1732
+ player.play();
1733
+ break;
1734
+ case 'pause':
1735
+ player.pause();
1736
+ break;
1737
+ case 'stop':
1738
+ player.stop();
1739
+ break;
1740
+ case 'seek':
1741
+ if (seek_percent !== undefined) {
1742
+ player.seekToPercent(seek_percent);
1743
+ }
1744
+ break;
1745
+ }
1746
+
1747
+ return {
1748
+ action,
1749
+ state: player.getState(),
1750
+ suggested_next_actions: action === 'play'
1751
+ ? ['play_choreography', 'capture_screenshot', 'describe_visual_state']
1752
+ : ['play_choreography']
1753
+ };
1754
+ }
1755
+
1756
+ /**
1757
+ * Control a previously created timeline.
1758
+ */
1759
+ controlTimeline(args) {
1760
+ const { timeline_id, action, seek_percent, speed } = args;
1761
+
1762
+ if (!this._liveTimelines) this._liveTimelines = new Map();
1763
+
1764
+ let tl = this._liveTimelines.get(timeline_id);
1765
+
1766
+ // If timeline not live yet, try to create it from stored data
1767
+ if (!tl && this._timelines && this._timelines.has(timeline_id) && this.engine) {
1768
+ const data = this._timelines.get(timeline_id);
1769
+
1770
+ tl = new ParameterTimeline(
1771
+ (name, value) => this.engine.setParameter(name, value)
1772
+ );
1773
+
1774
+ // Build import-compatible format
1775
+ const importData = {
1776
+ type: 'vib3-parameter-timeline',
1777
+ version: '1.0.0',
1778
+ duration: data.duration,
1779
+ loopMode: data.loopMode || 'once',
1780
+ bpm: data.bpm || 120,
1781
+ tracks: {}
1782
+ };
1783
+
1784
+ for (const [param, keyframes] of Object.entries(data.tracks)) {
1785
+ importData.tracks[param] = {
1786
+ enabled: true,
1787
+ keyframes: keyframes.map(kf => ({
1788
+ time: kf.time,
1789
+ value: kf.value,
1790
+ easing: kf.easing || 'easeInOut'
1791
+ }))
1792
+ };
1793
+ }
1794
+
1795
+ tl.importTimeline(importData);
1796
+ this._liveTimelines.set(timeline_id, tl);
1797
+ }
1798
+
1799
+ if (!tl) {
1800
+ return {
1801
+ error: {
1802
+ type: 'ValidationError',
1803
+ code: 'TIMELINE_NOT_FOUND',
1804
+ message: `Timeline '${timeline_id}' not found`,
1805
+ suggestion: 'Create a timeline first with create_timeline'
1806
+ }
1807
+ };
1808
+ }
1809
+
1810
+ switch (action) {
1811
+ case 'play':
1812
+ tl.play();
1813
+ break;
1814
+ case 'pause':
1815
+ tl.pause();
1816
+ break;
1817
+ case 'stop':
1818
+ tl.stop();
1819
+ break;
1820
+ case 'seek':
1821
+ if (seek_percent !== undefined) {
1822
+ tl.seekToPercent(seek_percent);
1823
+ }
1824
+ break;
1825
+ case 'set_speed':
1826
+ if (speed !== undefined) {
1827
+ tl.playbackSpeed = Math.max(0.1, Math.min(10, speed));
1828
+ }
1829
+ break;
1830
+ }
1831
+
1832
+ return {
1833
+ timeline_id,
1834
+ action,
1835
+ playing: tl.playing,
1836
+ currentTime: tl.currentTime,
1837
+ duration: tl.duration,
1838
+ progress: tl.duration > 0 ? tl.currentTime / tl.duration : 0,
1839
+ playbackSpeed: tl.playbackSpeed,
1840
+ suggested_next_actions: ['control_timeline', 'describe_visual_state', 'capture_screenshot']
1841
+ };
1842
+ }
1843
+
1844
+ // ====================================================================
1845
+ // Layer Relationship Tools (Phase 8)
1846
+ // ====================================================================
1847
+
1848
+ /**
1849
+ * Get the holographic system's layer graph (if available).
1850
+ * @private
1851
+ * @returns {import('../../render/LayerRelationshipGraph.js').LayerRelationshipGraph|null}
1852
+ */
1853
+ _getLayerGraph() {
1854
+ if (!this.engine) return null;
1855
+ // Try to access the current system's layer graph
1856
+ const system = this.engine.currentSystem || this.engine._activeSystem;
1857
+ if (system && system.layerGraph) {
1858
+ return system.layerGraph;
1859
+ }
1860
+ if (system && system._layerGraph) {
1861
+ return system._layerGraph;
1862
+ }
1863
+ return null;
1864
+ }
1865
+
1866
+ /**
1867
+ * Load a named layer relationship profile.
1868
+ */
1869
+ setLayerProfile(args) {
1870
+ const { profile } = args;
1871
+ const graph = this._getLayerGraph();
1872
+
1873
+ if (!graph) {
1874
+ return {
1875
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1876
+ suggested_next_actions: ['switch_system']
1877
+ };
1878
+ }
1879
+
1880
+ graph.loadProfile(profile);
1881
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, { type: 'layer_profile', profile });
1882
+
1883
+ return {
1884
+ profile,
1885
+ keystone: graph.keystone,
1886
+ active_profile: graph.activeProfile,
1887
+ available_profiles: ['holographic', 'symmetry', 'chord', 'storm', 'legacy'],
1888
+ suggested_next_actions: ['get_layer_config', 'set_layer_relationship', 'tune_layer_relationship']
1889
+ };
1890
+ }
1891
+
1892
+ /**
1893
+ * Set relationship for a specific layer.
1894
+ */
1895
+ setLayerRelationship(args) {
1896
+ const { layer, relationship, config } = args;
1897
+ const graph = this._getLayerGraph();
1898
+
1899
+ if (!graph) {
1900
+ return {
1901
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1902
+ suggested_next_actions: ['switch_system']
1903
+ };
1904
+ }
1905
+
1906
+ if (config) {
1907
+ graph.setRelationship(layer, { preset: relationship, config });
1908
+ } else {
1909
+ graph.setRelationship(layer, relationship);
1910
+ }
1911
+
1912
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, {
1913
+ type: 'layer_relationship', layer, relationship
1914
+ });
1915
+
1916
+ return {
1917
+ layer,
1918
+ relationship,
1919
+ config: config || {},
1920
+ keystone: graph.keystone,
1921
+ suggested_next_actions: ['get_layer_config', 'tune_layer_relationship', 'describe_visual_state']
1922
+ };
1923
+ }
1924
+
1925
+ /**
1926
+ * Change the keystone (driver) layer.
1927
+ */
1928
+ setLayerKeystone(args) {
1929
+ const { layer } = args;
1930
+ const graph = this._getLayerGraph();
1931
+
1932
+ if (!graph) {
1933
+ return {
1934
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1935
+ suggested_next_actions: ['switch_system']
1936
+ };
1937
+ }
1938
+
1939
+ graph.setKeystone(layer);
1940
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, { type: 'layer_keystone', layer });
1941
+
1942
+ return {
1943
+ keystone: layer,
1944
+ note: 'Other layers\' relationships are preserved. Set new relationships for the old keystone if needed.',
1945
+ suggested_next_actions: ['set_layer_relationship', 'get_layer_config']
1946
+ };
1947
+ }
1948
+
1949
+ /**
1950
+ * Get current layer configuration.
1951
+ */
1952
+ getLayerConfig() {
1953
+ const graph = this._getLayerGraph();
1954
+
1955
+ if (!graph) {
1956
+ return {
1957
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1958
+ suggested_next_actions: ['switch_system']
1959
+ };
1960
+ }
1961
+
1962
+ const config = graph.exportConfig();
1963
+
1964
+ return {
1965
+ keystone: config.keystone,
1966
+ active_profile: config.profile,
1967
+ relationships: config.relationships,
1968
+ shaders: config.shaders,
1969
+ available_profiles: ['holographic', 'symmetry', 'chord', 'storm', 'legacy'],
1970
+ available_presets: ['echo', 'mirror', 'complement', 'harmonic', 'reactive', 'chase'],
1971
+ suggested_next_actions: ['set_layer_profile', 'set_layer_relationship', 'tune_layer_relationship']
1972
+ };
1973
+ }
1974
+
1975
+ /**
1976
+ * Tune a layer's relationship config.
1977
+ */
1978
+ tuneLayerRelationship(args) {
1979
+ const { layer, config: configOverrides } = args;
1980
+ const graph = this._getLayerGraph();
1981
+
1982
+ if (!graph) {
1983
+ return {
1984
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1985
+ suggested_next_actions: ['switch_system']
1986
+ };
1987
+ }
1988
+
1989
+ const graphConfig = graph.exportConfig();
1990
+ const currentRel = graphConfig.relationships[layer];
1991
+
1992
+ if (!currentRel || !currentRel.preset) {
1993
+ return {
1994
+ error: `Layer "${layer}" has no tunable preset relationship. Set one first with set_layer_relationship.`,
1995
+ suggested_next_actions: ['set_layer_relationship']
1996
+ };
1997
+ }
1998
+
1999
+ const factory = PRESET_REGISTRY[currentRel.preset];
2000
+ if (!factory) {
2001
+ return {
2002
+ error: `Unknown preset "${currentRel.preset}" on layer "${layer}".`,
2003
+ suggested_next_actions: ['set_layer_relationship']
2004
+ };
2005
+ }
2006
+
2007
+ const newConfig = { ...(currentRel.config || {}), ...configOverrides };
2008
+ graph.setRelationship(layer, { preset: currentRel.preset, config: newConfig });
2009
+
2010
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, {
2011
+ type: 'layer_tune', layer, tuned_keys: Object.keys(configOverrides)
2012
+ });
2013
+
2014
+ return {
2015
+ layer,
2016
+ preset: currentRel.preset,
2017
+ previous_config: currentRel.config,
2018
+ new_config: newConfig,
2019
+ suggested_next_actions: ['get_layer_config', 'describe_visual_state', 'capture_screenshot']
2020
+ };
2021
+ }
946
2022
  }
947
2023
 
948
2024
  // Singleton instance