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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.mcpbignore +40 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -14
- package/livepilot/commands/beat.md +68 -21
- package/livepilot/commands/evaluate.md +23 -13
- package/livepilot/commands/mix.md +35 -11
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -17
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +15 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +342 -0
- package/mcp_server/hook_hunter/models.py +57 -0
- package/mcp_server/hook_hunter/tools.py +586 -0
- package/mcp_server/memory/taste_graph.py +261 -0
- package/mcp_server/memory/tools.py +88 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +421 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +221 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +73 -0
- package/mcp_server/preview_studio/tools.py +423 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +204 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +235 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +477 -0
- package/mcp_server/song_brain/models.py +132 -0
- package/mcp_server/song_brain/tools.py +294 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +285 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- 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
|