livepilot 1.9.21 → 1.9.23

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 (110) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.mcpbignore +40 -0
  3. package/AGENTS.md +2 -2
  4. package/CHANGELOG.md +47 -0
  5. package/CONTRIBUTING.md +1 -1
  6. package/README.md +47 -72
  7. package/bin/livepilot.js +135 -0
  8. package/livepilot/.Codex-plugin/plugin.json +2 -2
  9. package/livepilot/.claude-plugin/plugin.json +2 -2
  10. package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
  11. package/livepilot/commands/arrange.md +42 -14
  12. package/livepilot/commands/beat.md +68 -21
  13. package/livepilot/commands/evaluate.md +23 -13
  14. package/livepilot/commands/mix.md +35 -11
  15. package/livepilot/commands/perform.md +31 -19
  16. package/livepilot/commands/sounddesign.md +38 -17
  17. package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
  18. package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
  19. package/livepilot/skills/livepilot-core/SKILL.md +60 -4
  20. package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
  21. package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
  22. package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
  23. package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
  24. package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
  25. package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
  26. package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
  27. package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
  28. package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
  29. package/livepilot/skills/livepilot-core/references/overview.md +4 -4
  30. package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
  31. package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
  32. package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
  33. package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
  34. package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
  35. package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
  36. package/livepilot/skills/livepilot-release/SKILL.md +15 -15
  37. package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
  38. package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
  39. package/livepilot.mcpb +0 -0
  40. package/m4l_device/livepilot_bridge.js +1 -1
  41. package/manifest.json +91 -0
  42. package/mcp_server/__init__.py +1 -1
  43. package/mcp_server/creative_constraints/__init__.py +6 -0
  44. package/mcp_server/creative_constraints/engine.py +277 -0
  45. package/mcp_server/creative_constraints/models.py +75 -0
  46. package/mcp_server/creative_constraints/tools.py +341 -0
  47. package/mcp_server/experiment/__init__.py +6 -0
  48. package/mcp_server/experiment/engine.py +213 -0
  49. package/mcp_server/experiment/models.py +120 -0
  50. package/mcp_server/experiment/tools.py +263 -0
  51. package/mcp_server/hook_hunter/__init__.py +5 -0
  52. package/mcp_server/hook_hunter/analyzer.py +342 -0
  53. package/mcp_server/hook_hunter/models.py +57 -0
  54. package/mcp_server/hook_hunter/tools.py +586 -0
  55. package/mcp_server/memory/taste_graph.py +261 -0
  56. package/mcp_server/memory/tools.py +88 -0
  57. package/mcp_server/mix_engine/critics.py +2 -2
  58. package/mcp_server/mix_engine/models.py +1 -1
  59. package/mcp_server/mix_engine/state_builder.py +2 -2
  60. package/mcp_server/musical_intelligence/__init__.py +8 -0
  61. package/mcp_server/musical_intelligence/detectors.py +421 -0
  62. package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
  63. package/mcp_server/musical_intelligence/tools.py +221 -0
  64. package/mcp_server/preview_studio/__init__.py +5 -0
  65. package/mcp_server/preview_studio/engine.py +280 -0
  66. package/mcp_server/preview_studio/models.py +73 -0
  67. package/mcp_server/preview_studio/tools.py +423 -0
  68. package/mcp_server/runtime/session_kernel.py +96 -0
  69. package/mcp_server/runtime/tools.py +90 -1
  70. package/mcp_server/semantic_moves/__init__.py +13 -0
  71. package/mcp_server/semantic_moves/compiler.py +116 -0
  72. package/mcp_server/semantic_moves/mix_compilers.py +291 -0
  73. package/mcp_server/semantic_moves/mix_moves.py +157 -0
  74. package/mcp_server/semantic_moves/models.py +46 -0
  75. package/mcp_server/semantic_moves/performance_compilers.py +208 -0
  76. package/mcp_server/semantic_moves/performance_moves.py +81 -0
  77. package/mcp_server/semantic_moves/registry.py +32 -0
  78. package/mcp_server/semantic_moves/resolvers.py +126 -0
  79. package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
  80. package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
  81. package/mcp_server/semantic_moves/tools.py +204 -0
  82. package/mcp_server/semantic_moves/transition_compilers.py +222 -0
  83. package/mcp_server/semantic_moves/transition_moves.py +76 -0
  84. package/mcp_server/server.py +10 -0
  85. package/mcp_server/session_continuity/__init__.py +6 -0
  86. package/mcp_server/session_continuity/models.py +86 -0
  87. package/mcp_server/session_continuity/tools.py +230 -0
  88. package/mcp_server/session_continuity/tracker.py +235 -0
  89. package/mcp_server/song_brain/__init__.py +6 -0
  90. package/mcp_server/song_brain/builder.py +477 -0
  91. package/mcp_server/song_brain/models.py +132 -0
  92. package/mcp_server/song_brain/tools.py +294 -0
  93. package/mcp_server/stuckness_detector/__init__.py +5 -0
  94. package/mcp_server/stuckness_detector/detector.py +400 -0
  95. package/mcp_server/stuckness_detector/models.py +66 -0
  96. package/mcp_server/stuckness_detector/tools.py +195 -0
  97. package/mcp_server/tools/_conductor.py +104 -6
  98. package/mcp_server/tools/analyzer.py +1 -1
  99. package/mcp_server/tools/devices.py +34 -0
  100. package/mcp_server/wonder_mode/__init__.py +6 -0
  101. package/mcp_server/wonder_mode/diagnosis.py +84 -0
  102. package/mcp_server/wonder_mode/engine.py +493 -0
  103. package/mcp_server/wonder_mode/session.py +114 -0
  104. package/mcp_server/wonder_mode/tools.py +285 -0
  105. package/package.json +2 -2
  106. package/remote_script/LivePilot/__init__.py +1 -1
  107. package/remote_script/LivePilot/browser.py +4 -1
  108. package/remote_script/LivePilot/devices.py +29 -0
  109. package/remote_script/LivePilot/tracks.py +11 -4
  110. package/scripts/generate_tool_catalog.py +131 -0
@@ -0,0 +1,423 @@
1
+ """Preview Studio MCP tools — 5 tools for creative comparison.
2
+
3
+ create_preview_set — generate safe/strong/unexpected variants
4
+ compare_preview_variants — rank variants by taste + identity + impact
5
+ commit_preview_variant — apply the chosen variant
6
+ discard_preview_set — throw away all variants
7
+ render_preview_variant — render a short preview via undo system
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Optional
13
+
14
+ from fastmcp import Context
15
+
16
+ from ..server import mcp
17
+ from . import engine
18
+
19
+
20
+ def _get_ableton(ctx: Context):
21
+ return ctx.lifespan_context["ableton"]
22
+
23
+
24
+ def _should_refuse_analytical(compiled_plan, wonder_linked: bool) -> bool:
25
+ """Check if an analytical variant should be refused in Wonder context."""
26
+ return compiled_plan is None and wonder_linked
27
+
28
+
29
+ def _find_wonder_session_by_preview(set_id: str):
30
+ """Find a WonderSession linked to this preview set."""
31
+ try:
32
+ from ..wonder_mode.session import find_session_by_preview_set
33
+ return find_session_by_preview_set(set_id)
34
+ except Exception:
35
+ return None
36
+
37
+
38
+ @mcp.tool()
39
+ def create_preview_set(
40
+ ctx: Context,
41
+ request_text: str,
42
+ kernel_id: str = "",
43
+ strategy: str = "creative_triptych",
44
+ wonder_session_id: str = "",
45
+ ) -> dict:
46
+ """Create a preview set with multiple creative options.
47
+
48
+ Generates safe / strong / unexpected variants for comparison.
49
+ Each variant includes what it changes, why it matters, and what
50
+ it preserves from the song's identity.
51
+
52
+ request_text: what the user wants (e.g., "make this more magical")
53
+ kernel_id: optional session kernel reference
54
+ strategy: "creative_triptych" (default) or "binary"
55
+ wonder_session_id: optional — links to a WonderSession for lifecycle tracking
56
+
57
+ Returns: preview set with variant summaries.
58
+ """
59
+ if not request_text.strip():
60
+ return {"error": "request_text cannot be empty"}
61
+
62
+ # Wonder-aware path: use variants from WonderSession
63
+ if wonder_session_id:
64
+ from ..wonder_mode.session import get_wonder_session
65
+ ws = get_wonder_session(wonder_session_id)
66
+ if not ws:
67
+ return {"error": f"Wonder session {wonder_session_id} not found"}
68
+ if not ws.variants:
69
+ return {"error": f"Wonder session {wonder_session_id} has no variants"}
70
+
71
+ from .models import PreviewVariant, PreviewSet
72
+ import time
73
+
74
+ # Filter to executable variants only
75
+ exec_variants = [v for v in ws.variants if not v.get("analytical_only")]
76
+ if not exec_variants:
77
+ return {"error": "No executable variants in Wonder session — all are analytical-only"}
78
+
79
+ now = int(time.time() * 1000)
80
+ preview_variants = []
81
+ for v in exec_variants:
82
+ preview_variants.append(PreviewVariant(
83
+ variant_id=v.get("variant_id", ""),
84
+ label=v.get("label", ""),
85
+ intent=v.get("intent", ""),
86
+ novelty_level=v.get("novelty_level", 0.5),
87
+ identity_effect=v.get("identity_effect", "preserves"),
88
+ what_changed=v.get("what_changed", ""),
89
+ what_preserved=v.get("what_preserved", ""),
90
+ why_it_matters=v.get("why_it_matters", ""),
91
+ move_id=v.get("move_id", ""),
92
+ compiled_plan=v.get("compiled_plan"),
93
+ taste_fit=v.get("taste_fit", 0.5),
94
+ score=v.get("score", 0.0),
95
+ summary=v.get("distinctness_reason", ""),
96
+ created_at_ms=now,
97
+ ))
98
+
99
+ set_id = f"ps_wonder_{wonder_session_id[:12]}"
100
+ ps = PreviewSet(
101
+ set_id=set_id,
102
+ request_text=request_text,
103
+ strategy="wonder",
104
+ source_kernel_id=kernel_id,
105
+ variants=preview_variants,
106
+ created_at_ms=now,
107
+ )
108
+ engine.store_preview_set(ps)
109
+
110
+ # Update WonderSession
111
+ ws.preview_set_id = set_id
112
+ ws.transition_to("previewing")
113
+
114
+ return ps.to_dict()
115
+
116
+ # Get request-aware moves via propose_next_best_move logic
117
+ # instead of arbitrary registry order
118
+ available_moves = []
119
+ try:
120
+ from ..semantic_moves import registry
121
+ from ..semantic_moves.tools import propose_next_best_move as _propose
122
+ # Use the proposer's keyword+taste scoring to find relevant moves
123
+ request_lower = request_text.lower()
124
+ all_moves = list(registry._REGISTRY.values())
125
+ scored = []
126
+ for move in all_moves:
127
+ score = 0.0
128
+ move_words = set(move.move_id.replace("_", " ").split())
129
+ intent_words = set(move.intent.lower().split())
130
+ request_words = set(request_lower.split())
131
+ overlap = request_words & (move_words | intent_words)
132
+ score += len(overlap) * 0.3
133
+ for dim in move.targets:
134
+ if dim in request_lower:
135
+ score += 0.2
136
+ if score > 0:
137
+ scored.append((move.to_dict(), score))
138
+ scored.sort(key=lambda x: -x[1])
139
+ available_moves = [m for m, _ in scored[:3]]
140
+ # Fallback: if no keyword match, take top 3 from full registry
141
+ if not available_moves:
142
+ available_moves = registry.list_moves()[:3]
143
+ except Exception:
144
+ pass
145
+
146
+ # Get song brain if available
147
+ song_brain: dict = {}
148
+ try:
149
+ from ..song_brain.tools import _current_brain
150
+ if _current_brain is not None:
151
+ song_brain = _current_brain.to_dict()
152
+ except Exception as _e:
153
+ if __debug__:
154
+ import sys
155
+ print(f"LivePilot: SongBrain unavailable in preview_studio: {_e}", file=sys.stderr)
156
+
157
+ # Get taste graph — use session-scoped stores, extract numeric weights
158
+ taste_graph: dict = {}
159
+ try:
160
+ from ..memory.taste_graph import build_taste_graph
161
+ from ..memory.taste_memory import TasteMemoryStore
162
+ from ..memory.anti_memory import AntiMemoryStore
163
+ taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
164
+ anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
165
+ graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store)
166
+ taste_graph = graph.to_dict()
167
+ except Exception:
168
+ pass
169
+
170
+ ps = engine.create_preview_set(
171
+ request_text=request_text,
172
+ kernel_id=kernel_id,
173
+ strategy=strategy,
174
+ available_moves=available_moves,
175
+ song_brain=song_brain,
176
+ taste_graph=taste_graph,
177
+ )
178
+
179
+ return ps.to_dict()
180
+
181
+
182
+ @mcp.tool()
183
+ def compare_preview_variants(
184
+ ctx: Context,
185
+ set_id: str,
186
+ taste_weight: float = 0.3,
187
+ novelty_weight: float = 0.2,
188
+ identity_weight: float = 0.5,
189
+ ) -> dict:
190
+ """Compare and rank variants in a preview set.
191
+
192
+ Rankings combine taste fit, novelty balance, and identity preservation.
193
+ Returns ranked list with scores and a recommended pick.
194
+
195
+ set_id: the preview set to compare
196
+ taste_weight: how much to weight user taste fit (0-1)
197
+ novelty_weight: how much to weight novelty balance (0-1)
198
+ identity_weight: how much to weight identity preservation (0-1)
199
+ """
200
+ ps = engine.get_preview_set(set_id)
201
+ if not ps:
202
+ return {"error": f"Preview set {set_id} not found"}
203
+
204
+ criteria = {
205
+ "taste_weight": taste_weight,
206
+ "novelty_weight": novelty_weight,
207
+ "identity_weight": identity_weight,
208
+ }
209
+
210
+ comparison = engine.compare_variants(ps, criteria)
211
+ return comparison
212
+
213
+
214
+ @mcp.tool()
215
+ def commit_preview_variant(
216
+ ctx: Context,
217
+ set_id: str,
218
+ variant_id: str,
219
+ ) -> dict:
220
+ """Commit the chosen variant from a preview set.
221
+
222
+ Marks the variant as committed and discards the others.
223
+ The caller should then apply the variant's compiled plan.
224
+
225
+ set_id: the preview set
226
+ variant_id: the chosen variant to commit
227
+ """
228
+ ps = engine.get_preview_set(set_id)
229
+ if not ps:
230
+ return {"error": f"Preview set {set_id} not found"}
231
+
232
+ chosen = engine.commit_variant(ps, variant_id)
233
+ if not chosen:
234
+ available = [v.variant_id for v in ps.variants]
235
+ return {
236
+ "error": f"Variant {variant_id} not found in set {set_id}",
237
+ "available_variants": available,
238
+ }
239
+
240
+ result = {
241
+ "committed": True,
242
+ "variant_id": chosen.variant_id,
243
+ "label": chosen.label,
244
+ "intent": chosen.intent,
245
+ "move_id": chosen.move_id,
246
+ "identity_effect": chosen.identity_effect,
247
+ "what_preserved": chosen.what_preserved,
248
+ }
249
+
250
+ # Wonder lifecycle hooks
251
+ ws = _find_wonder_session_by_preview(set_id)
252
+ if ws:
253
+ ws.selected_variant_id = variant_id
254
+ ws.outcome = "committed"
255
+ ws.transition_to("resolved")
256
+
257
+ # Record accepted turn resolution
258
+ try:
259
+ from ..session_continuity.tracker import record_turn_resolution, resolve_thread
260
+ record_turn_resolution(
261
+ request_text=ws.request_text,
262
+ outcome="accepted",
263
+ move_applied=chosen.move_id,
264
+ identity_effect=chosen.identity_effect,
265
+ user_sentiment="liked",
266
+ )
267
+ if ws.creative_thread_id:
268
+ resolve_thread(ws.creative_thread_id)
269
+ except Exception:
270
+ pass
271
+
272
+ # Update taste graph
273
+ try:
274
+ from ..memory.taste_graph import build_taste_graph
275
+ from ..memory.taste_memory import TasteMemoryStore
276
+ from ..memory.anti_memory import AntiMemoryStore
277
+ taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
278
+ anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
279
+ graph = build_taste_graph(taste_store=taste_store, anti_store=anti_store)
280
+ # Look up family from WonderSession's variant list
281
+ family = ""
282
+ for v in ws.variants:
283
+ if v.get("variant_id") == variant_id:
284
+ family = v.get("family", "")
285
+ break
286
+ if chosen.move_id and family:
287
+ graph.record_move_outcome(
288
+ move_id=chosen.move_id,
289
+ family=family,
290
+ kept=True,
291
+ )
292
+ except Exception:
293
+ pass
294
+
295
+ result["wonder_session_id"] = ws.session_id
296
+
297
+ return result
298
+
299
+
300
+ @mcp.tool()
301
+ def render_preview_variant(
302
+ ctx: Context,
303
+ set_id: str = "",
304
+ variant_id: str = "",
305
+ bars: int = 8,
306
+ ) -> dict:
307
+ """Render a short preview of a specific variant for evaluation.
308
+
309
+ Captures a snapshot of what the variant would sound like if applied,
310
+ without permanently changing the session. Uses Ableton's undo system
311
+ to revert after capture.
312
+
313
+ set_id: the preview set containing the variant
314
+ variant_id: which variant to render
315
+ bars: how many bars to capture (default 8)
316
+
317
+ Returns the variant's snapshot data and summary.
318
+ """
319
+ ps = engine.get_preview_set(set_id)
320
+ if not ps:
321
+ return {"error": f"Preview set {set_id} not found"}
322
+
323
+ variant = None
324
+ for v in ps.variants:
325
+ if v.variant_id == variant_id:
326
+ variant = v
327
+ break
328
+
329
+ if not variant:
330
+ available = [v.variant_id for v in ps.variants]
331
+ return {
332
+ "error": f"Variant {variant_id} not found in set {set_id}",
333
+ "available_variants": available,
334
+ }
335
+
336
+ # Wonder-linked context: refuse analytical variants
337
+ wonder_linked = _find_wonder_session_by_preview(set_id) is not None
338
+ if _should_refuse_analytical(variant.compiled_plan, wonder_linked):
339
+ return {
340
+ "error": "This variant is analytical-only and cannot be previewed",
341
+ "variant_id": variant_id,
342
+ "analytical_only": True,
343
+ }
344
+
345
+ # If the variant has a compiled plan, we could apply-capture-undo.
346
+ # Without a compiled plan, return the variant's analytical preview.
347
+ if variant.compiled_plan:
348
+ ableton = _get_ableton(ctx)
349
+ # compiled_plan may be a list (from semantic moves) or a dict with "steps" key
350
+ plan = variant.compiled_plan
351
+ steps = plan if isinstance(plan, list) else plan.get("steps", [])
352
+ applied_count = 0
353
+ try:
354
+ # Capture before state
355
+ before_info = ableton.send_command("get_session_info", {})
356
+
357
+ # Apply the plan steps, tracking how many succeed
358
+ for step in steps:
359
+ cmd = step.get("tool") or step.get("command")
360
+ args = step.get("params") or step.get("args", {})
361
+ if cmd:
362
+ ableton.send_command(cmd, args)
363
+ applied_count += 1
364
+
365
+ # Capture after state
366
+ after_info = ableton.send_command("get_session_info", {})
367
+ except Exception as e:
368
+ return {"error": f"Render failed: {e}", "variant_id": variant_id}
369
+ finally:
370
+ # Undo all applied changes regardless of success/failure
371
+ for _ in range(applied_count):
372
+ try:
373
+ ableton.send_command("undo")
374
+ except Exception:
375
+ break
376
+
377
+ variant.status = "rendered"
378
+ variant.render_ref = f"render_{variant_id}_{bars}bars"
379
+
380
+ return {
381
+ "rendered": True,
382
+ "variant_id": variant_id,
383
+ "label": variant.label,
384
+ "bars": bars,
385
+ "before_summary": {"tempo": before_info.get("tempo"), "tracks": before_info.get("track_count")},
386
+ "after_summary": {"tempo": after_info.get("tempo"), "tracks": after_info.get("track_count")},
387
+ "identity_effect": variant.identity_effect,
388
+ "what_changed": variant.what_changed,
389
+ "what_preserved": variant.what_preserved,
390
+ }
391
+ else:
392
+ # Analytical preview — no live render
393
+ variant.status = "rendered"
394
+ return {
395
+ "rendered": True,
396
+ "variant_id": variant_id,
397
+ "label": variant.label,
398
+ "bars": bars,
399
+ "mode": "analytical",
400
+ "intent": variant.intent,
401
+ "novelty_level": variant.novelty_level,
402
+ "identity_effect": variant.identity_effect,
403
+ "what_changed": variant.what_changed,
404
+ "what_preserved": variant.what_preserved,
405
+ "why_it_matters": variant.why_it_matters,
406
+ "note": "Analytical preview — no compiled plan available for live render",
407
+ }
408
+
409
+
410
+ @mcp.tool()
411
+ def discard_preview_set(
412
+ ctx: Context,
413
+ set_id: str,
414
+ ) -> dict:
415
+ """Discard an entire preview set and all its variants.
416
+
417
+ Use when the user doesn't want any of the options.
418
+ """
419
+ success = engine.discard_set(set_id)
420
+ if not success:
421
+ return {"error": f"Preview set {set_id} not found"}
422
+
423
+ return {"discarded": True, "set_id": set_id}
@@ -0,0 +1,96 @@
1
+ """SessionKernel — the canonical turn snapshot for V2 orchestration.
2
+
3
+ Assembles project brain, capability state, action ledger, taste profile,
4
+ anti-preferences, and session memory into one unified object. This is the
5
+ single source of truth for any complex agentic workflow.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import json
12
+ from dataclasses import dataclass, field, asdict
13
+ from typing import Any, Optional
14
+
15
+
16
+ @dataclass
17
+ class SessionKernel:
18
+ """Immutable turn snapshot. Built once per complex request."""
19
+
20
+ kernel_id: str
21
+ request_text: str = ""
22
+ mode: str = "improve" # observe | improve | explore | finish | diagnose
23
+ aggression: float = 0.5
24
+
25
+ # Session topology
26
+ tempo: float = 120.0
27
+ track_count: int = 0
28
+ session_info: dict = field(default_factory=dict)
29
+
30
+ # Capability state
31
+ capability_state: dict = field(default_factory=dict)
32
+
33
+ # Action ledger
34
+ ledger_summary: dict = field(default_factory=dict)
35
+
36
+ # Memory
37
+ session_memory: list = field(default_factory=list)
38
+ taste_graph: dict = field(default_factory=dict)
39
+ anti_preferences: list = field(default_factory=list)
40
+
41
+ # Protection
42
+ protected_dimensions: dict = field(default_factory=dict)
43
+
44
+ # Routing hints (filled by conductor)
45
+ recommended_engines: list = field(default_factory=list)
46
+ recommended_workflow: str = ""
47
+
48
+ def to_dict(self) -> dict:
49
+ return asdict(self)
50
+
51
+
52
+ def build_session_kernel(
53
+ session_info: dict,
54
+ capability_state: dict,
55
+ request_text: str = "",
56
+ mode: str = "improve",
57
+ aggression: float = 0.5,
58
+ ledger_summary: Optional[dict] = None,
59
+ session_memory: Optional[list] = None,
60
+ taste_graph: Optional[dict] = None,
61
+ anti_preferences: Optional[list] = None,
62
+ protected_dimensions: Optional[dict] = None,
63
+ ) -> SessionKernel:
64
+ """Build a SessionKernel from raw data.
65
+
66
+ All optional fields degrade gracefully to empty defaults.
67
+ The kernel_id is deterministic from the core inputs so it's stable
68
+ within the same turn context.
69
+ """
70
+ # Deterministic kernel_id from inputs
71
+ id_seed = json.dumps(
72
+ {
73
+ "tempo": session_info.get("tempo"),
74
+ "track_count": session_info.get("track_count"),
75
+ "request": request_text,
76
+ "mode": mode,
77
+ },
78
+ sort_keys=True,
79
+ )
80
+ kernel_id = hashlib.sha256(id_seed.encode()).hexdigest()[:12]
81
+
82
+ return SessionKernel(
83
+ kernel_id=kernel_id,
84
+ request_text=request_text,
85
+ mode=mode,
86
+ aggression=aggression,
87
+ tempo=session_info.get("tempo", 120.0),
88
+ track_count=session_info.get("track_count", 0),
89
+ session_info=session_info,
90
+ capability_state=capability_state,
91
+ ledger_summary=ledger_summary or {},
92
+ session_memory=session_memory or [],
93
+ taste_graph=taste_graph or {},
94
+ anti_preferences=anti_preferences or [],
95
+ protected_dimensions=protected_dimensions or {},
96
+ )
@@ -1,7 +1,8 @@
1
- """MCP tool wrappers for runtime capability state.
1
+ """MCP tool wrappers for runtime capability state and session kernel.
2
2
 
3
3
  Tools:
4
4
  get_capability_state — probe session + analyzer + memory, return snapshot
5
+ get_session_kernel — build the unified V2 turn snapshot for orchestration
5
6
  """
6
7
 
7
8
  from __future__ import annotations
@@ -65,3 +66,91 @@ def get_capability_state(ctx: Context) -> dict:
65
66
  )
66
67
 
67
68
  return state.to_dict()
69
+
70
+
71
+ @mcp.tool()
72
+ def get_session_kernel(
73
+ ctx: Context,
74
+ request_text: str = "",
75
+ mode: str = "improve",
76
+ aggression: float = 0.5,
77
+ ) -> dict:
78
+ """Build the unified turn snapshot for V2 orchestration.
79
+
80
+ This is the preferred entrypoint for any complex agentic workflow.
81
+ Assembles: session info, capability state, action ledger, taste profile,
82
+ anti-preferences, and session memory into one canonical snapshot.
83
+
84
+ mode: observe | improve | explore | finish | diagnose
85
+ aggression: 0.0 (subtle) to 1.0 (bold)
86
+
87
+ Returns: SessionKernel dict with kernel_id, session topology, capabilities,
88
+ memory context, and routing hints.
89
+ """
90
+ from .session_kernel import build_session_kernel
91
+
92
+ ableton = ctx.lifespan_context["ableton"]
93
+ spectral = ctx.lifespan_context.get("spectral")
94
+
95
+ # Core: session info + capability state
96
+ session_info = ableton.send_command("get_session_info")
97
+
98
+ analyzer_ok = False
99
+ if spectral is not None:
100
+ analyzer_ok = spectral.is_connected
101
+
102
+ state = build_capability_state(
103
+ session_ok=True,
104
+ analyzer_ok=analyzer_ok,
105
+ memory_ok=True,
106
+ )
107
+
108
+ # Optional subcomponents — degrade gracefully
109
+ ledger_summary = {}
110
+ taste_graph = {}
111
+ anti_prefs = []
112
+ session_mem = []
113
+
114
+ try:
115
+ from .action_ledger import ActionLedger
116
+ ledger = ActionLedger.instance()
117
+ if ledger:
118
+ ledger_summary = ledger.summary()
119
+ except Exception:
120
+ pass
121
+
122
+ try:
123
+ from ..memory.taste_memory import TasteMemoryStore
124
+ taste_store = TasteMemoryStore()
125
+ taste_graph = {d.name: d.to_dict() for d in taste_store._dims.values()
126
+ if d.evidence_count > 0}
127
+ except Exception:
128
+ pass
129
+
130
+ try:
131
+ from ..memory.anti_memory import AntiMemoryStore
132
+ anti_store = AntiMemoryStore()
133
+ anti_prefs = anti_store.list_all()
134
+ except Exception:
135
+ pass
136
+
137
+ try:
138
+ from ..memory.session_memory import SessionMemoryStore
139
+ mem_store = SessionMemoryStore()
140
+ session_mem = mem_store.recent(limit=10)
141
+ except Exception:
142
+ pass
143
+
144
+ kernel = build_session_kernel(
145
+ session_info=session_info,
146
+ capability_state=state.to_dict(),
147
+ request_text=request_text,
148
+ mode=mode,
149
+ aggression=aggression,
150
+ ledger_summary=ledger_summary,
151
+ session_memory=session_mem,
152
+ taste_graph=taste_graph,
153
+ anti_preferences=anti_prefs,
154
+ )
155
+
156
+ return kernel.to_dict()
@@ -0,0 +1,13 @@
1
+ """Semantic moves — musical intents that compile to deterministic tool sequences."""
2
+
3
+ # Import move families to auto-register them
4
+ from . import mix_moves # noqa: F401
5
+ from . import transition_moves # noqa: F401
6
+ from . import sound_design_moves # noqa: F401
7
+ from . import performance_moves # noqa: F401
8
+
9
+ # Import compilers to auto-register them
10
+ from . import mix_compilers # noqa: F401
11
+ from . import transition_compilers # noqa: F401
12
+ from . import sound_design_compilers # noqa: F401
13
+ from . import performance_compilers # noqa: F401