livepilot 1.27.1 → 1.27.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +86 -0
- package/README.md +10 -7
- package/bin/livepilot.js +156 -37
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +33 -1
- package/livepilot/skills/livepilot-core/references/atlas-tool-notes.md +69 -0
- package/livepilot/skills/livepilot-core/references/overview.md +18 -11
- package/livepilot/skills/livepilot-core/references/perception.md +125 -0
- package/livepilot/skills/livepilot-devices/references/device-parameter-units.md +66 -0
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +8 -0
- package/livepilot/skills/livepilot-release/SKILL.md +1 -1
- package/livepilot/skills/livepilot-sample-engine/references/splice-tools-notes.md +56 -0
- package/livepilot/skills/livepilot-wonder/SKILL.md +6 -0
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +48 -11
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/atlas/__init__.py +181 -40
- package/mcp_server/atlas/overlays.py +46 -3
- package/mcp_server/atlas/tools.py +226 -86
- package/mcp_server/audit/checks.py +50 -22
- package/mcp_server/audit/state.py +10 -2
- package/mcp_server/audit/tools.py +27 -6
- package/mcp_server/composer/develop/apply.py +7 -7
- package/mcp_server/composer/fast/apply.py +29 -23
- package/mcp_server/composer/fast/brief_builder.py +6 -2
- package/mcp_server/composer/framework/atlas_resolver.py +16 -1
- package/mcp_server/composer/full/apply.py +40 -34
- package/mcp_server/composer/full/engine.py +66 -32
- package/mcp_server/composer/tools.py +16 -9
- package/mcp_server/connection.py +69 -10
- package/mcp_server/creative_constraints/engine.py +10 -2
- package/mcp_server/creative_constraints/tools.py +24 -7
- package/mcp_server/curves.py +30 -7
- package/mcp_server/device_forge/builder.py +40 -11
- package/mcp_server/device_forge/models.py +9 -0
- package/mcp_server/device_forge/tools.py +30 -11
- package/mcp_server/experiment/engine.py +29 -22
- package/mcp_server/experiment/tools.py +15 -5
- package/mcp_server/m4l_bridge.py +217 -62
- package/mcp_server/memory/taste_graph.py +43 -0
- package/mcp_server/memory/technique_store.py +26 -3
- package/mcp_server/memory/tools.py +62 -4
- package/mcp_server/mix_engine/critics.py +187 -30
- package/mcp_server/mix_engine/models.py +21 -1
- package/mcp_server/mix_engine/state_builder.py +87 -8
- package/mcp_server/mix_engine/tools.py +16 -4
- package/mcp_server/performance_engine/tools.py +56 -8
- package/mcp_server/persistence/base_store.py +11 -0
- package/mcp_server/persistence/project_store.py +132 -8
- package/mcp_server/persistence/taste_store.py +90 -7
- package/mcp_server/preview_studio/engine.py +40 -10
- package/mcp_server/preview_studio/models.py +40 -0
- package/mcp_server/preview_studio/tools.py +56 -12
- package/mcp_server/project_brain/arrangement_graph.py +4 -0
- package/mcp_server/project_brain/models.py +2 -0
- package/mcp_server/project_brain/role_graph.py +13 -7
- package/mcp_server/reference_engine/gap_analyzer.py +58 -3
- package/mcp_server/reference_engine/profile_builder.py +47 -4
- package/mcp_server/reference_engine/tools.py +6 -0
- package/mcp_server/runtime/execution_router.py +57 -1
- package/mcp_server/runtime/mcp_dispatch.py +22 -0
- package/mcp_server/runtime/remote_commands.py +9 -4
- package/mcp_server/runtime/tools.py +0 -1
- package/mcp_server/sample_engine/sources.py +0 -2
- package/mcp_server/sample_engine/tools.py +19 -38
- package/mcp_server/semantic_moves/mix_compilers.py +276 -51
- package/mcp_server/semantic_moves/performance_compilers.py +51 -17
- package/mcp_server/semantic_moves/resolvers.py +45 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +14 -6
- package/mcp_server/semantic_moves/tools.py +80 -2
- package/mcp_server/semantic_moves/transition_compilers.py +26 -9
- package/mcp_server/server.py +27 -25
- package/mcp_server/session_continuity/tracker.py +51 -3
- package/mcp_server/song_brain/builder.py +47 -5
- package/mcp_server/song_brain/tools.py +21 -7
- package/mcp_server/sound_design/critics.py +1 -0
- package/mcp_server/splice_client/client.py +117 -33
- package/mcp_server/splice_client/http_bridge.py +15 -3
- package/mcp_server/splice_client/quota.py +28 -0
- package/mcp_server/stuckness_detector/detector.py +8 -5
- package/mcp_server/synthesis_brain/adapters/drift.py +13 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +13 -0
- package/mcp_server/tools/_analyzer_engine/sample.py +36 -10
- package/mcp_server/tools/_perception_engine.py +6 -0
- package/mcp_server/tools/agent_os.py +4 -1
- package/mcp_server/tools/analyzer.py +198 -209
- package/mcp_server/tools/arrangement.py +7 -4
- package/mcp_server/tools/automation.py +24 -4
- package/mcp_server/tools/browser.py +25 -11
- package/mcp_server/tools/clips.py +6 -0
- package/mcp_server/tools/composition.py +33 -2
- package/mcp_server/tools/devices.py +53 -53
- package/mcp_server/tools/generative.py +14 -14
- package/mcp_server/tools/harmony.py +7 -7
- package/mcp_server/tools/mixing.py +4 -4
- package/mcp_server/tools/planner.py +68 -6
- package/mcp_server/tools/research.py +20 -2
- package/mcp_server/tools/theory.py +10 -10
- package/mcp_server/tools/transport.py +7 -2
- package/mcp_server/transition_engine/critics.py +13 -1
- package/mcp_server/user_corpus/tools.py +30 -1
- package/mcp_server/wonder_mode/engine.py +82 -9
- package/mcp_server/wonder_mode/session.py +32 -10
- package/mcp_server/wonder_mode/tools.py +14 -1
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +21 -8
- package/remote_script/LivePilot/arrangement.py +93 -33
- package/remote_script/LivePilot/browser.py +60 -4
- package/remote_script/LivePilot/devices.py +132 -62
- package/remote_script/LivePilot/mixing.py +31 -5
- package/remote_script/LivePilot/server.py +94 -22
- package/remote_script/LivePilot/tracks.py +11 -5
- package/remote_script/LivePilot/transport.py +11 -0
- package/requirements.txt +5 -5
- package/server.json +2 -2
|
@@ -73,7 +73,7 @@ async def _back_to_arranger(ctx: Any) -> dict:
|
|
|
73
73
|
if ableton is None:
|
|
74
74
|
return {"ok": False}
|
|
75
75
|
try:
|
|
76
|
-
return ableton.
|
|
76
|
+
return await ableton.send_command_async("back_to_arranger", {})
|
|
77
77
|
except Exception as exc:
|
|
78
78
|
logger.warning("apply_develop: back_to_arranger failed: %s", exc)
|
|
79
79
|
return {"ok": False}
|
|
@@ -148,10 +148,10 @@ async def apply_develop_plan(ctx: Context, plan: dict) -> dict:
|
|
|
148
148
|
plan_tempo = plan.get("tempo")
|
|
149
149
|
if plan_tempo is not None:
|
|
150
150
|
try:
|
|
151
|
-
session = ableton.
|
|
151
|
+
session = await ableton.send_command_async("get_session_info", {})
|
|
152
152
|
current_tempo = float(session.get("tempo", 0.0))
|
|
153
153
|
if abs(current_tempo - float(plan_tempo)) > 0.01:
|
|
154
|
-
ableton.
|
|
154
|
+
await ableton.send_command_async("set_tempo", {"tempo": float(plan_tempo)})
|
|
155
155
|
except Exception as exc:
|
|
156
156
|
logger.warning("apply_develop: tempo set failed: %s", exc)
|
|
157
157
|
|
|
@@ -171,27 +171,27 @@ async def apply_develop_plan(ctx: Context, plan: dict) -> dict:
|
|
|
171
171
|
try:
|
|
172
172
|
# Optional sample swap (sample-trigger layers only)
|
|
173
173
|
if sample_uri:
|
|
174
|
-
ableton.
|
|
174
|
+
await ableton.send_command_async(
|
|
175
175
|
"load_browser_item",
|
|
176
176
|
{"track_index": track_index, "uri": sample_uri},
|
|
177
177
|
)
|
|
178
178
|
sample_swaps += 1
|
|
179
179
|
|
|
180
180
|
# Create clip
|
|
181
|
-
ableton.
|
|
181
|
+
await ableton.send_command_async(
|
|
182
182
|
"create_clip",
|
|
183
183
|
{"track_index": track_index, "clip_index": scene_index, "length": clip_length},
|
|
184
184
|
)
|
|
185
185
|
|
|
186
186
|
# Add notes (skip if empty — empty clip is a valid drum-dropout pattern)
|
|
187
187
|
if notes:
|
|
188
|
-
ableton.
|
|
188
|
+
await ableton.send_command_async(
|
|
189
189
|
"add_notes",
|
|
190
190
|
{"track_index": track_index, "clip_index": scene_index, "notes": notes},
|
|
191
191
|
)
|
|
192
192
|
|
|
193
193
|
# Name the clip
|
|
194
|
-
ableton.
|
|
194
|
+
await ableton.send_command_async(
|
|
195
195
|
"set_clip_name",
|
|
196
196
|
{"track_index": track_index, "clip_index": scene_index, "name": name},
|
|
197
197
|
)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import asyncio
|
|
5
6
|
import logging
|
|
6
7
|
import time
|
|
7
8
|
|
|
@@ -22,13 +23,13 @@ def _is_drum_role(role: str) -> bool:
|
|
|
22
23
|
return role.lower() in DRUM_ROLES
|
|
23
24
|
|
|
24
25
|
|
|
25
|
-
def _detect_silent_load(ableton, track_index: int, device_index: int = 0) -> tuple:
|
|
26
|
+
async def _detect_silent_load(ableton, track_index: int, device_index: int = 0) -> tuple:
|
|
26
27
|
"""Detect if the loaded device is silently misconfigured (empty container).
|
|
27
28
|
|
|
28
29
|
Returns (is_silent: bool, reason: str).
|
|
29
30
|
"""
|
|
30
31
|
try:
|
|
31
|
-
device_info = ableton.
|
|
32
|
+
device_info = await ableton.send_command_async("get_device_info", {
|
|
32
33
|
"track_index": track_index, "device_index": device_index,
|
|
33
34
|
})
|
|
34
35
|
except Exception:
|
|
@@ -44,7 +45,7 @@ def _detect_silent_load(ableton, track_index: int, device_index: int = 0) -> tup
|
|
|
44
45
|
# Simpler with Sample Length near zero
|
|
45
46
|
if class_name == "OriginalSimpler":
|
|
46
47
|
try:
|
|
47
|
-
params_resp = ableton.
|
|
48
|
+
params_resp = await ableton.send_command_async("get_device_parameters", {
|
|
48
49
|
"track_index": track_index, "device_index": device_index,
|
|
49
50
|
})
|
|
50
51
|
for p in params_resp.get("parameters", []):
|
|
@@ -60,7 +61,7 @@ def _detect_silent_load(ableton, track_index: int, device_index: int = 0) -> tup
|
|
|
60
61
|
return False, ""
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def _apply_drum_role_repair(ableton, track_index: int, device_index: int = 0) -> dict:
|
|
64
|
+
async def _apply_drum_role_repair(ableton, track_index: int, device_index: int = 0) -> dict:
|
|
64
65
|
"""Apply role-default repair to a drum-role layer's instrument.
|
|
65
66
|
|
|
66
67
|
Defense in depth: load_browser_item's role='drum' silently fails to
|
|
@@ -83,7 +84,7 @@ def _apply_drum_role_repair(ableton, track_index: int, device_index: int = 0) ->
|
|
|
83
84
|
"""
|
|
84
85
|
# Detect device class so we apply class-appropriate params
|
|
85
86
|
try:
|
|
86
|
-
device_info = ableton.
|
|
87
|
+
device_info = await ableton.send_command_async("get_device_info", {
|
|
87
88
|
"track_index": track_index, "device_index": device_index,
|
|
88
89
|
})
|
|
89
90
|
class_name = device_info.get("class_name", "")
|
|
@@ -99,14 +100,19 @@ def _apply_drum_role_repair(ableton, track_index: int, device_index: int = 0) ->
|
|
|
99
100
|
])
|
|
100
101
|
|
|
101
102
|
try:
|
|
102
|
-
result = ableton.
|
|
103
|
+
result = await ableton.send_command_async("batch_set_parameters", {
|
|
103
104
|
"track_index": track_index,
|
|
104
105
|
"device_index": device_index,
|
|
105
106
|
"parameters": params_to_set,
|
|
106
107
|
})
|
|
108
|
+
# P2-50: batch_set_parameters no longer raises on a per-entry failure —
|
|
109
|
+
# it returns {"ok": bool, "failed": M, ...}. Reflect the actual outcome
|
|
110
|
+
# instead of hardcoding applied=True (default True keeps pre-P2-50
|
|
111
|
+
# Remote Scripts that omit "ok" working).
|
|
107
112
|
return {
|
|
108
|
-
"applied": True,
|
|
113
|
+
"applied": bool(result.get("ok", True)),
|
|
109
114
|
"device_class": class_name,
|
|
115
|
+
"failed": result.get("failed", 0),
|
|
110
116
|
"params": result.get("parameters", []),
|
|
111
117
|
}
|
|
112
118
|
except Exception as exc:
|
|
@@ -189,14 +195,14 @@ async def apply_fast_plan(
|
|
|
189
195
|
logger.debug("fast apply: preflight failed (bridge unavailable): %s", exc)
|
|
190
196
|
|
|
191
197
|
# Pre-flight: where do new tracks go, and which scene?
|
|
192
|
-
session = ableton.
|
|
198
|
+
session = await ableton.send_command_async("get_session_info", {})
|
|
193
199
|
starting_track_count = int(session.get("track_count", 0))
|
|
194
200
|
scene_count = int(session.get("scene_count", 0))
|
|
195
201
|
|
|
196
202
|
# Optional tempo override
|
|
197
203
|
if plan.get("tempo"):
|
|
198
204
|
try:
|
|
199
|
-
ableton.
|
|
205
|
+
await ableton.send_command_async("set_tempo", {"tempo": float(plan["tempo"])})
|
|
200
206
|
except Exception as exc:
|
|
201
207
|
logger.debug("apply: set_tempo failed: %s", exc)
|
|
202
208
|
|
|
@@ -216,7 +222,7 @@ async def apply_fast_plan(
|
|
|
216
222
|
# returns ("A-Reverb") instead of remembering integer send indices.
|
|
217
223
|
return_name_to_send_index: dict[str, int] = {}
|
|
218
224
|
try:
|
|
219
|
-
returns_resp = ableton.
|
|
225
|
+
returns_resp = await ableton.send_command_async("get_return_tracks", {}) or {}
|
|
220
226
|
for i, rt in enumerate(returns_resp.get("return_tracks", []) or []):
|
|
221
227
|
name = (rt.get("name") or "").strip()
|
|
222
228
|
if name:
|
|
@@ -243,7 +249,7 @@ async def apply_fast_plan(
|
|
|
243
249
|
|
|
244
250
|
new_track_idx = starting_track_count + len(new_track_indices)
|
|
245
251
|
try:
|
|
246
|
-
ableton.
|
|
252
|
+
await ableton.send_command_async("create_midi_track", {"index": -1, "name": track_name})
|
|
247
253
|
except Exception as exc:
|
|
248
254
|
logger.warning("apply: create_midi_track(%s) failed: %s", track_name, exc)
|
|
249
255
|
layer_results.append({
|
|
@@ -263,14 +269,14 @@ async def apply_fast_plan(
|
|
|
263
269
|
load_params: dict = {"track_index": new_track_idx, "uri": uri}
|
|
264
270
|
if simpler_role:
|
|
265
271
|
load_params["role"] = simpler_role
|
|
266
|
-
ableton.
|
|
272
|
+
await ableton.send_command_async("load_browser_item", load_params)
|
|
267
273
|
loaded = True
|
|
268
274
|
except Exception as exc:
|
|
269
275
|
logger.debug("apply: load_browser_item(%s, %s) failed: %s", new_track_idx, uri, exc)
|
|
270
276
|
|
|
271
277
|
if loaded:
|
|
272
278
|
# v1.24 Phase 4 Task 18b: detect empty containers post-load
|
|
273
|
-
is_silent, silent_reason = _detect_silent_load(ableton, new_track_idx, device_index=0)
|
|
279
|
+
is_silent, silent_reason = await _detect_silent_load(ableton, new_track_idx, device_index=0)
|
|
274
280
|
if is_silent:
|
|
275
281
|
silent_load_warning = silent_reason
|
|
276
282
|
logger.warning(
|
|
@@ -282,7 +288,7 @@ async def apply_fast_plan(
|
|
|
282
288
|
# load_browser_item role='drum' silently skips Vol/Snap/root fixes
|
|
283
289
|
# when the track already has FX. Re-apply deterministically.
|
|
284
290
|
if _is_drum_role(role):
|
|
285
|
-
role_repair = _apply_drum_role_repair(ableton, new_track_idx, device_index=0)
|
|
291
|
+
role_repair = await _apply_drum_role_repair(ableton, new_track_idx, device_index=0)
|
|
286
292
|
if not role_repair.get("applied"):
|
|
287
293
|
logger.debug(
|
|
288
294
|
"apply: drum role repair failed for track %s: %s",
|
|
@@ -301,7 +307,7 @@ async def apply_fast_plan(
|
|
|
301
307
|
clip_length_beats = max(bars * 4, int(max_end) + 1) if notes else bars * 4
|
|
302
308
|
|
|
303
309
|
try:
|
|
304
|
-
ableton.
|
|
310
|
+
await ableton.send_command_async("create_clip", {
|
|
305
311
|
"track_index": new_track_idx,
|
|
306
312
|
"clip_index": target_scene,
|
|
307
313
|
"length": float(clip_length_beats),
|
|
@@ -317,7 +323,7 @@ async def apply_fast_plan(
|
|
|
317
323
|
notes_added = 0
|
|
318
324
|
if notes:
|
|
319
325
|
try:
|
|
320
|
-
ableton.
|
|
326
|
+
await ableton.send_command_async("add_notes", {
|
|
321
327
|
"track_index": new_track_idx,
|
|
322
328
|
"clip_index": target_scene,
|
|
323
329
|
"notes": notes,
|
|
@@ -336,7 +342,7 @@ async def apply_fast_plan(
|
|
|
336
342
|
if not device_name:
|
|
337
343
|
continue
|
|
338
344
|
try:
|
|
339
|
-
ins_resp = ableton.
|
|
345
|
+
ins_resp = await ableton.send_command_async("insert_device", {
|
|
340
346
|
"track_index": new_track_idx,
|
|
341
347
|
"device_name": device_name,
|
|
342
348
|
}) or {}
|
|
@@ -346,7 +352,7 @@ async def apply_fast_plan(
|
|
|
346
352
|
if device_index is not None:
|
|
347
353
|
for pname, pvalue in (fx.get("params") or {}).items():
|
|
348
354
|
try:
|
|
349
|
-
ableton.
|
|
355
|
+
await ableton.send_command_async("set_device_parameter", {
|
|
350
356
|
"track_index": new_track_idx,
|
|
351
357
|
"device_index": int(device_index),
|
|
352
358
|
"parameter_name": str(pname),
|
|
@@ -396,7 +402,7 @@ async def apply_fast_plan(
|
|
|
396
402
|
})
|
|
397
403
|
continue
|
|
398
404
|
try:
|
|
399
|
-
ableton.
|
|
405
|
+
await ableton.send_command_async("set_track_send", {
|
|
400
406
|
"track_index": new_track_idx,
|
|
401
407
|
"send_index": int(send_index),
|
|
402
408
|
"value": value,
|
|
@@ -446,7 +452,7 @@ async def apply_fast_plan(
|
|
|
446
452
|
# Fire the scene
|
|
447
453
|
fired = False
|
|
448
454
|
try:
|
|
449
|
-
ableton.
|
|
455
|
+
await ableton.send_command_async("fire_scene", {"scene_index": target_scene})
|
|
450
456
|
fired = True
|
|
451
457
|
except Exception as exc:
|
|
452
458
|
logger.warning("apply: fire_scene(%s) failed: %s", target_scene, exc)
|
|
@@ -454,7 +460,7 @@ async def apply_fast_plan(
|
|
|
454
460
|
# Final fresh-project cleanup: delete the leftover default track if
|
|
455
461
|
# the brief left one in place to satisfy Ableton's "≥1 track" guard.
|
|
456
462
|
final_cleanup_actions: list[str] = []
|
|
457
|
-
new_session = ableton.
|
|
463
|
+
new_session = await ableton.send_command_async("get_session_info", {})
|
|
458
464
|
final_tracks = new_session.get("tracks", []) or []
|
|
459
465
|
# If track 0 is still default-named and empty, AND we just added new
|
|
460
466
|
# tracks, prune it now (we have ≥2 tracks total, safe to delete).
|
|
@@ -462,9 +468,9 @@ async def apply_fast_plan(
|
|
|
462
468
|
first = final_tracks[0]
|
|
463
469
|
if fast_compose.is_default_track_name(first.get("name", "")):
|
|
464
470
|
try:
|
|
465
|
-
ti0 = ableton.
|
|
471
|
+
ti0 = await ableton.send_command_async("get_track_info", {"track_index": 0})
|
|
466
472
|
if fast_compose.track_is_empty(ti0):
|
|
467
|
-
ableton.
|
|
473
|
+
await ableton.send_command_async("delete_track", {"track_index": 0})
|
|
468
474
|
final_cleanup_actions.append("deleted_leftover_default_track")
|
|
469
475
|
# All track indices shift down by 1
|
|
470
476
|
new_track_indices = [i - 1 for i in new_track_indices]
|
|
@@ -1402,8 +1402,12 @@ def build_creative_brief(
|
|
|
1402
1402
|
" — those are filtered out before the brief reaches you.\n"
|
|
1403
1403
|
"5. **TIER-1: Fire each search in `recommended_searches` BEFORE designing\n"
|
|
1404
1404
|
" that role.** Each entry gives you a (tool, query) pair — call the named\n"
|
|
1405
|
-
" Ableton Knowledge MCP tool
|
|
1406
|
-
"
|
|
1405
|
+
" Ableton Knowledge MCP tool with the `mcp__Ableton_Knowledge__` prefix\n"
|
|
1406
|
+
" (e.g. search_live_manual → mcp__Ableton_Knowledge__search_live_manual).\n"
|
|
1407
|
+
" Most queries hit the Live manual or video tutorials; capture 1 useful\n"
|
|
1408
|
+
" snippet per role and apply it. The Ableton Knowledge MCP is OPTIONAL —\n"
|
|
1409
|
+
" if any mcp__Ableton_Knowledge__* tool is unavailable or returns nothing,\n"
|
|
1410
|
+
" skip it and proceed using the creative guidance alone.\n"
|
|
1407
1411
|
"6. **TIER-2: If `reference_artist` is set**, also fire each search in\n"
|
|
1408
1412
|
" `reference_searches` and design USING that artist's signature techniques.\n"
|
|
1409
1413
|
"7. For each layer: pick ONE uri from instruments_by_role[role], design\n"
|
|
@@ -541,8 +541,23 @@ class AtlasResolver:
|
|
|
541
541
|
@staticmethod
|
|
542
542
|
def _to_candidate(dev: dict, score: float, reasoning: str, *, source: str) -> AtlasCandidate:
|
|
543
543
|
char_tags = list(dev.get("character_tags") or dev.get("tags") or [])
|
|
544
|
+
uri = dev.get("uri") or ""
|
|
545
|
+
# LIVE#3: M4L pack instruments carry a bogus `query:Synths#<Name>` atlas
|
|
546
|
+
# URI that load_browser_item can't resolve. Clear it for every resolver
|
|
547
|
+
# consumer (compose-full design / atlas_explore included) and note the
|
|
548
|
+
# preset fallback. Lazy import of the canonical ID set keeps a single
|
|
549
|
+
# source of truth without a module-load circular import.
|
|
550
|
+
try:
|
|
551
|
+
from mcp_server.atlas.tools import _M4L_PACK_SYNTH_IDS
|
|
552
|
+
if (dev.get("id") or "").lower() in _M4L_PACK_SYNTH_IDS and uri.startswith("query:Synths#"):
|
|
553
|
+
uri = ""
|
|
554
|
+
hint = ("M4L pack instrument — not directly loadable via "
|
|
555
|
+
"query:Synths#; resolve via search_browser(path='sounds').")
|
|
556
|
+
reasoning = (reasoning + " | " + hint) if reasoning else hint
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
544
559
|
return AtlasCandidate(
|
|
545
|
-
uri=
|
|
560
|
+
uri=uri,
|
|
546
561
|
name=dev.get("name") or "",
|
|
547
562
|
source=source,
|
|
548
563
|
score=score,
|
|
@@ -329,7 +329,7 @@ async def apply_full_plan(
|
|
|
329
329
|
)
|
|
330
330
|
|
|
331
331
|
# ── Detect + clean default tracks ──────────────────────────────────
|
|
332
|
-
session = ableton.
|
|
332
|
+
session = await ableton.send_command_async("get_session_info", {})
|
|
333
333
|
starting_track_count = int(session.get("track_count", 0))
|
|
334
334
|
|
|
335
335
|
fresh_project = fast_compose.detect_fresh_project(session)
|
|
@@ -337,7 +337,7 @@ async def apply_full_plan(
|
|
|
337
337
|
candidates: list[int] = []
|
|
338
338
|
for i in range(starting_track_count):
|
|
339
339
|
try:
|
|
340
|
-
ti = ableton.
|
|
340
|
+
ti = await ableton.send_command_async("get_track_info", {"track_index": i})
|
|
341
341
|
if fast_compose.track_is_empty(ti):
|
|
342
342
|
candidates.append(i)
|
|
343
343
|
except Exception as exc:
|
|
@@ -350,7 +350,7 @@ async def apply_full_plan(
|
|
|
350
350
|
deleted = 0
|
|
351
351
|
for idx in deletable:
|
|
352
352
|
try:
|
|
353
|
-
ableton.
|
|
353
|
+
await ableton.send_command_async("delete_track", {"track_index": idx})
|
|
354
354
|
deleted += 1
|
|
355
355
|
except Exception as exc:
|
|
356
356
|
logger.debug("full apply: delete_track(%s) failed: %s", idx, exc)
|
|
@@ -414,18 +414,22 @@ async def apply_full_plan(
|
|
|
414
414
|
result = await _load_sample(ctx, **resolved_params)
|
|
415
415
|
elif tool_name in ("create_midi_track", "create_audio_track"):
|
|
416
416
|
# Track the index so postflight can set monitoring on them
|
|
417
|
-
result = ableton.
|
|
417
|
+
result = await ableton.send_command_async(tool_name, resolved_params) or {}
|
|
418
418
|
if ok and isinstance(result, dict):
|
|
419
|
-
|
|
419
|
+
# create_midi_track/create_audio_track return {"index": N}
|
|
420
|
+
# from the real Remote Script, not {"track_index": N} —
|
|
421
|
+
# fall back to the legacy key for any mock that still
|
|
422
|
+
# uses it.
|
|
423
|
+
track_idx = result.get("index", result.get("track_index"))
|
|
420
424
|
if track_idx is not None:
|
|
421
425
|
created_track_indices.append(int(track_idx))
|
|
422
426
|
elif tool_name in _FULL_PLAN_TCP_TOOLS:
|
|
423
427
|
# Direct Remote-Script TCP command
|
|
424
|
-
result = ableton.
|
|
428
|
+
result = await ableton.send_command_async(tool_name, resolved_params) or {}
|
|
425
429
|
else:
|
|
426
430
|
# Unknown tool — try generic TCP send (most LivePilot tools
|
|
427
431
|
# have a 1:1 Remote-Script handler with the same name).
|
|
428
|
-
result = ableton.
|
|
432
|
+
result = await ableton.send_command_async(tool_name, resolved_params) or {}
|
|
429
433
|
except Exception as exc:
|
|
430
434
|
ok = False
|
|
431
435
|
err_msg = str(exc)
|
|
@@ -456,14 +460,14 @@ async def apply_full_plan(
|
|
|
456
460
|
# invalidate the indices below.
|
|
457
461
|
final_cleanup_actions: list[str] = []
|
|
458
462
|
try:
|
|
459
|
-
post_session = ableton.
|
|
463
|
+
post_session = await ableton.send_command_async("get_session_info", {})
|
|
460
464
|
tracks = post_session.get("tracks", []) or []
|
|
461
465
|
if tracks and len(tracks) > 1:
|
|
462
466
|
default_indices: list[int] = []
|
|
463
467
|
for i, t in enumerate(tracks):
|
|
464
468
|
if fast_compose.is_default_track_name(t.get("name", "")):
|
|
465
469
|
try:
|
|
466
|
-
ti = ableton.
|
|
470
|
+
ti = await ableton.send_command_async("get_track_info", {"track_index": i})
|
|
467
471
|
if fast_compose.track_is_empty(ti):
|
|
468
472
|
default_indices.append(i)
|
|
469
473
|
except Exception as exc:
|
|
@@ -475,7 +479,7 @@ async def apply_full_plan(
|
|
|
475
479
|
if len(tracks) - len(final_cleanup_actions) <= 1:
|
|
476
480
|
break
|
|
477
481
|
try:
|
|
478
|
-
ableton.
|
|
482
|
+
await ableton.send_command_async("delete_track", {"track_index": idx})
|
|
479
483
|
final_cleanup_actions.append(f"deleted_leftover_default_track_at_{idx}")
|
|
480
484
|
except Exception as exc:
|
|
481
485
|
logger.debug("full apply: final cleanup delete_track(%s) failed: %s", idx, exc)
|
|
@@ -620,7 +624,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
620
624
|
if ab is None:
|
|
621
625
|
return {"ok": False}
|
|
622
626
|
try:
|
|
623
|
-
return ab.
|
|
627
|
+
return await ab.send_command_async(
|
|
624
628
|
"set_track_input_monitoring",
|
|
625
629
|
{"track_index": track_index, "state": state},
|
|
626
630
|
)
|
|
@@ -646,7 +650,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
646
650
|
fresh_cleanup_actions: list[str] = []
|
|
647
651
|
try:
|
|
648
652
|
from ..fast.brief_builder import detect_fresh_project, is_default_track_name
|
|
649
|
-
session_preflight = ableton.
|
|
653
|
+
session_preflight = await ableton.send_command_async("get_session_info", {})
|
|
650
654
|
if detect_fresh_project(session_preflight):
|
|
651
655
|
# Delete default tracks in REVERSE order to keep indices stable.
|
|
652
656
|
# Ableton requires at least 1 track — keep the lowest-indexed default.
|
|
@@ -662,7 +666,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
662
666
|
if len(default_indices) > 1:
|
|
663
667
|
for idx in default_indices[:-1]:
|
|
664
668
|
try:
|
|
665
|
-
ableton.
|
|
669
|
+
await ableton.send_command_async("delete_track", {"track_index": idx})
|
|
666
670
|
fresh_cleanup_actions.append(f"deleted_default_track_{idx}")
|
|
667
671
|
except Exception as exc:
|
|
668
672
|
logger.debug("apply_full_v2: delete_track(%d) failed: %s", idx, exc)
|
|
@@ -674,15 +678,15 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
674
678
|
plan_key = plan.get("key")
|
|
675
679
|
if plan_tempo is not None:
|
|
676
680
|
try:
|
|
677
|
-
session = ableton.
|
|
681
|
+
session = await ableton.send_command_async("get_session_info", {})
|
|
678
682
|
current_tempo = float(session.get("tempo", 0.0))
|
|
679
683
|
if abs(current_tempo - float(plan_tempo)) > 0.01:
|
|
680
|
-
ableton.
|
|
684
|
+
await ableton.send_command_async("set_tempo", {"tempo": float(plan_tempo)})
|
|
681
685
|
except Exception as exc:
|
|
682
686
|
logger.warning("apply_full_v2: tempo set failed: %s", exc)
|
|
683
687
|
if plan_key:
|
|
684
688
|
try:
|
|
685
|
-
ableton.
|
|
689
|
+
await ableton.send_command_async("set_song_scale", {"root_note": plan_key})
|
|
686
690
|
except Exception as exc:
|
|
687
691
|
logger.debug("apply_full_v2: set_song_scale skipped: %s", exc)
|
|
688
692
|
|
|
@@ -702,7 +706,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
702
706
|
track_index = track_spec.get("track_index")
|
|
703
707
|
if track_index is None:
|
|
704
708
|
try:
|
|
705
|
-
result = ableton.
|
|
709
|
+
result = await ableton.send_command_async(
|
|
706
710
|
"create_midi_track",
|
|
707
711
|
{"index": -1, "name": track_spec.get("role", "")},
|
|
708
712
|
)
|
|
@@ -733,7 +737,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
733
737
|
instrument = track_spec.get("instrument") or {}
|
|
734
738
|
if instrument.get("uri"):
|
|
735
739
|
try:
|
|
736
|
-
ableton.
|
|
740
|
+
await ableton.send_command_async(
|
|
737
741
|
"load_browser_item",
|
|
738
742
|
{"track_index": track_index, "uri": instrument["uri"]},
|
|
739
743
|
)
|
|
@@ -754,7 +758,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
754
758
|
track_role = track_spec.get("role", "")
|
|
755
759
|
if _is_drum_role(track_role):
|
|
756
760
|
try:
|
|
757
|
-
_apply_drum_role_repair(ableton, track_index, device_index=0)
|
|
761
|
+
await _apply_drum_role_repair(ableton, track_index, device_index=0)
|
|
758
762
|
except Exception as exc:
|
|
759
763
|
logger.debug(
|
|
760
764
|
"apply_full_v2: drum_role_repair failed for track %s: %s",
|
|
@@ -769,7 +773,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
769
773
|
if not device_name:
|
|
770
774
|
continue
|
|
771
775
|
try:
|
|
772
|
-
ins_resp = ableton.
|
|
776
|
+
ins_resp = await ableton.send_command_async("insert_device", {
|
|
773
777
|
"track_index": track_index,
|
|
774
778
|
"device_name": device_name,
|
|
775
779
|
}) or {}
|
|
@@ -778,7 +782,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
778
782
|
if device_index is None:
|
|
779
783
|
# Fallback: query track and take the last device
|
|
780
784
|
try:
|
|
781
|
-
track_info = ableton.
|
|
785
|
+
track_info = await ableton.send_command_async(
|
|
782
786
|
"get_track_info", {"track_index": track_index}
|
|
783
787
|
)
|
|
784
788
|
device_index = len(track_info.get("devices", [])) - 1
|
|
@@ -786,7 +790,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
786
790
|
pass
|
|
787
791
|
for param_name, param_value in (effect_spec.get("params") or {}).items():
|
|
788
792
|
try:
|
|
789
|
-
ableton.
|
|
793
|
+
await ableton.send_command_async("set_device_parameter", {
|
|
790
794
|
"track_index": track_index,
|
|
791
795
|
"device_index": int(device_index),
|
|
792
796
|
"parameter_name": str(param_name),
|
|
@@ -811,7 +815,9 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
811
815
|
return_name = (send_spec.get("return_name") or "").strip()
|
|
812
816
|
value = send_spec.get("value")
|
|
813
817
|
send_index = send_spec.get("send_index")
|
|
814
|
-
|
|
818
|
+
# return_name is always a str (post-.strip()), never None — the
|
|
819
|
+
# real "no destination given" case is an empty string.
|
|
820
|
+
if not return_name and send_index is None:
|
|
815
821
|
continue
|
|
816
822
|
try:
|
|
817
823
|
value = float(value or 0.0)
|
|
@@ -819,7 +825,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
819
825
|
continue
|
|
820
826
|
if send_index is None and return_name:
|
|
821
827
|
try:
|
|
822
|
-
session_info = ableton.
|
|
828
|
+
session_info = await ableton.send_command_async("get_session_info", {})
|
|
823
829
|
return_tracks = session_info.get("return_tracks", []) or []
|
|
824
830
|
for i, rt in enumerate(return_tracks):
|
|
825
831
|
if (rt.get("name") or "").lower() == return_name.lower():
|
|
@@ -840,7 +846,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
840
846
|
})
|
|
841
847
|
continue
|
|
842
848
|
try:
|
|
843
|
-
ableton.
|
|
849
|
+
await ableton.send_command_async("set_track_send", {
|
|
844
850
|
"track_index": track_index,
|
|
845
851
|
"send_index": int(send_index),
|
|
846
852
|
"value": value,
|
|
@@ -922,18 +928,18 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
922
928
|
slot = vi
|
|
923
929
|
variant_id_to_slot[variant["id"]] = slot
|
|
924
930
|
try:
|
|
925
|
-
ableton.
|
|
931
|
+
await ableton.send_command_async("create_clip", {
|
|
926
932
|
"track_index": track_index,
|
|
927
933
|
"clip_index": slot,
|
|
928
934
|
"length": 4.0,
|
|
929
935
|
})
|
|
930
936
|
if variant.get("notes"):
|
|
931
|
-
ableton.
|
|
937
|
+
await ableton.send_command_async("add_notes", {
|
|
932
938
|
"track_index": track_index,
|
|
933
939
|
"clip_index": slot,
|
|
934
940
|
"notes": variant["notes"],
|
|
935
941
|
})
|
|
936
|
-
ableton.
|
|
942
|
+
await ableton.send_command_async("set_clip_name", {
|
|
937
943
|
"track_index": track_index,
|
|
938
944
|
"clip_index": slot,
|
|
939
945
|
"name": variant["id"],
|
|
@@ -996,7 +1002,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
996
1002
|
# NEW FLOW: create ONE native arrangement clip spanning the full
|
|
997
1003
|
# section. Replaces create_arrangement_clip (session-clip duplication).
|
|
998
1004
|
try:
|
|
999
|
-
native_resp = ableton.
|
|
1005
|
+
native_resp = await ableton.send_command_async("create_native_arrangement_clip", {
|
|
1000
1006
|
"track_index": track_index,
|
|
1001
1007
|
"start_time": section_start_beats,
|
|
1002
1008
|
"length": section_length_beats,
|
|
@@ -1022,7 +1028,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
1022
1028
|
# clip start — same coordinate space the agent used).
|
|
1023
1029
|
if variant_notes:
|
|
1024
1030
|
try:
|
|
1025
|
-
ableton.
|
|
1031
|
+
await ableton.send_command_async("add_arrangement_notes", {
|
|
1026
1032
|
"track_index": track_index,
|
|
1027
1033
|
"clip_index": new_clip_index,
|
|
1028
1034
|
"notes": variant_notes,
|
|
@@ -1038,7 +1044,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
1038
1044
|
# full section length (e.g. a 4-beat kick loops 16× in a 64-beat
|
|
1039
1045
|
# verse section without requiring 64 beats of notes).
|
|
1040
1046
|
try:
|
|
1041
|
-
ableton.
|
|
1047
|
+
await ableton.send_command_async("set_clip_loop", {
|
|
1042
1048
|
"track_index": track_index,
|
|
1043
1049
|
"clip_index": new_clip_index,
|
|
1044
1050
|
"enabled": True,
|
|
@@ -1101,7 +1107,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
1101
1107
|
# would survive cleanup.
|
|
1102
1108
|
try:
|
|
1103
1109
|
from ..fast.brief_builder import is_default_track_name as _is_default_track_name
|
|
1104
|
-
session_post = ableton.
|
|
1110
|
+
session_post = await ableton.send_command_async("get_session_info", {})
|
|
1105
1111
|
all_tracks = session_post.get("tracks", [])
|
|
1106
1112
|
# Skip tracks we just created — only target leftover/zombie tracks
|
|
1107
1113
|
compose_track_indices = set(applied_track_indices)
|
|
@@ -1120,7 +1126,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
1120
1126
|
# Indicates a leftover from a previous compose run that the
|
|
1121
1127
|
# default-name detector missed.
|
|
1122
1128
|
try:
|
|
1123
|
-
track_info = ableton.
|
|
1129
|
+
track_info = await ableton.send_command_async("get_track_info", {"track_index": idx})
|
|
1124
1130
|
devices = track_info.get("devices", []) or []
|
|
1125
1131
|
clip_slots = track_info.get("clip_slots", []) or []
|
|
1126
1132
|
has_instrument = any(
|
|
@@ -1140,7 +1146,7 @@ async def apply_full_plan_v2(ctx: Context, plan: dict) -> dict:
|
|
|
1140
1146
|
# Delete in reverse-index order so indices stay stable
|
|
1141
1147
|
for idx in sorted(candidate_indices, reverse=True):
|
|
1142
1148
|
try:
|
|
1143
|
-
ableton.
|
|
1149
|
+
await ableton.send_command_async("delete_track", {"track_index": idx})
|
|
1144
1150
|
fresh_cleanup_actions.append(f"postflight_deleted_track_{idx}")
|
|
1145
1151
|
except Exception as exc:
|
|
1146
1152
|
logger.debug("apply_full_v2: postflight delete_track(%s) failed: %s", idx, exc)
|