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
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import asyncio
|
|
8
9
|
import tempfile
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
@@ -48,11 +49,13 @@ async def generate_m4l_effect(
|
|
|
48
49
|
}
|
|
49
50
|
dt = type_map.get(device_type)
|
|
50
51
|
if dt is None:
|
|
51
|
-
return {"error": f"Invalid device_type: {device_type}. Use:
|
|
52
|
+
return {"error": f"INVALID_PARAM: Invalid device_type: {device_type}. Use one of: {', '.join(type_map)}"}
|
|
52
53
|
|
|
53
54
|
parsed_params = []
|
|
54
55
|
if params:
|
|
55
56
|
for p in params:
|
|
57
|
+
if not isinstance(p, dict) or "name" not in p:
|
|
58
|
+
return {"error": f"INVALID_PARAM: each param requires a 'name' key, got: {p!r}"}
|
|
56
59
|
parsed_params.append(GenExprParam(
|
|
57
60
|
name=p["name"],
|
|
58
61
|
default=p.get("default", 0.5),
|
|
@@ -72,9 +75,16 @@ async def generate_m4l_effect(
|
|
|
72
75
|
if install:
|
|
73
76
|
subdir = _SUBDIR_MAP[dt]
|
|
74
77
|
output_dir = ABLETON_USER_LIBRARY / subdir
|
|
75
|
-
path = build_device(spec, output_dir=output_dir)
|
|
76
78
|
else:
|
|
77
|
-
|
|
79
|
+
# Reuse ONE forge directory under the system temp instead of spawning a
|
|
80
|
+
# fresh mkdtemp() per call (those accumulated, never cleaned). The .amxd
|
|
81
|
+
# is the returned deliverable so it must persist for the caller.
|
|
82
|
+
output_dir = Path(tempfile.gettempdir()) / "livepilot_forge"
|
|
83
|
+
|
|
84
|
+
# build_device does blocking filesystem I/O (mkdir + write_bytes). This is
|
|
85
|
+
# an async tool running on the event loop, so offload to a worker thread to
|
|
86
|
+
# avoid freezing all other handlers + the bridge UDP endpoint.
|
|
87
|
+
path = await asyncio.to_thread(build_device, spec, output_dir)
|
|
78
88
|
|
|
79
89
|
return {
|
|
80
90
|
"status": "created",
|
|
@@ -113,15 +123,11 @@ async def list_genexpr_templates(
|
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
ctx: Context,
|
|
119
|
-
source_path: str,
|
|
120
|
-
) -> dict:
|
|
121
|
-
"""Copy a .amxd file to Ableton's User Library.
|
|
126
|
+
def _install_m4l_device_sync(source_path: str) -> dict:
|
|
127
|
+
"""Blocking filesystem work for install_m4l_device (read/mkdir/write).
|
|
122
128
|
|
|
123
|
-
|
|
124
|
-
|
|
129
|
+
Run via asyncio.to_thread from the async tool below so the event loop
|
|
130
|
+
(and the bridge UDP endpoint) never stalls on disk I/O.
|
|
125
131
|
"""
|
|
126
132
|
src = Path(source_path)
|
|
127
133
|
if not src.exists():
|
|
@@ -160,3 +166,16 @@ async def install_m4l_device(
|
|
|
160
166
|
"path": str(dest),
|
|
161
167
|
"hint": "Device now available in Ableton's browser.",
|
|
162
168
|
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@mcp.tool()
|
|
172
|
+
async def install_m4l_device(
|
|
173
|
+
ctx: Context,
|
|
174
|
+
source_path: str,
|
|
175
|
+
) -> dict:
|
|
176
|
+
"""Copy a .amxd file to Ableton's User Library.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
source_path: Path to the .amxd file to install
|
|
180
|
+
"""
|
|
181
|
+
return await asyncio.to_thread(_install_m4l_device_sync, source_path)
|
|
@@ -17,7 +17,9 @@ The engine itself is pure orchestration logic.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
import asyncio
|
|
20
21
|
import hashlib
|
|
22
|
+
import threading
|
|
21
23
|
import time
|
|
22
24
|
from typing import Optional
|
|
23
25
|
|
|
@@ -28,8 +30,15 @@ import logging
|
|
|
28
30
|
logger = logging.getLogger(__name__)
|
|
29
31
|
|
|
30
32
|
# ── In-memory experiment store ───────────────────────────────────────────────
|
|
33
|
+
#
|
|
34
|
+
# _EXPERIMENTS is mutated from both threadpooled sync tools (create_experiment)
|
|
35
|
+
# and event-loop async tools (run_experiment / commit_branch_async) — a
|
|
36
|
+
# module-level lock around the dict ops keeps inserts/reads race-free under
|
|
37
|
+
# mixed sync/async concurrent callers, mirroring preview_studio.engine and
|
|
38
|
+
# wonder_mode.session.
|
|
31
39
|
|
|
32
40
|
_EXPERIMENTS: dict[str, ExperimentSet] = {}
|
|
41
|
+
_EXPERIMENTS_LOCK = threading.Lock()
|
|
33
42
|
|
|
34
43
|
|
|
35
44
|
def _gen_id(prefix: str, seed: str) -> str:
|
|
@@ -89,7 +98,8 @@ def create_experiment_from_seeds(
|
|
|
89
98
|
created_at_ms=now,
|
|
90
99
|
)
|
|
91
100
|
|
|
92
|
-
|
|
101
|
+
with _EXPERIMENTS_LOCK:
|
|
102
|
+
_EXPERIMENTS[exp_id] = experiment
|
|
93
103
|
return experiment
|
|
94
104
|
|
|
95
105
|
|
|
@@ -114,12 +124,15 @@ def create_experiment(
|
|
|
114
124
|
|
|
115
125
|
def get_experiment(experiment_id: str) -> Optional[ExperimentSet]:
|
|
116
126
|
"""Get an experiment by ID."""
|
|
117
|
-
|
|
127
|
+
with _EXPERIMENTS_LOCK:
|
|
128
|
+
return _EXPERIMENTS.get(experiment_id)
|
|
118
129
|
|
|
119
130
|
|
|
120
131
|
def list_experiments() -> list[dict]:
|
|
121
132
|
"""List all experiment sets."""
|
|
122
|
-
|
|
133
|
+
with _EXPERIMENTS_LOCK:
|
|
134
|
+
exps = list(_EXPERIMENTS.values())
|
|
135
|
+
return [exp.to_dict() for exp in exps]
|
|
123
136
|
|
|
124
137
|
# ── Run experiments (requires Ableton connection) ────────────────────────────
|
|
125
138
|
|
|
@@ -211,12 +224,16 @@ async def run_branch_async(
|
|
|
211
224
|
analyze_mix) are skipped in the apply pass — they're used for snapshot
|
|
212
225
|
capture separately.
|
|
213
226
|
"""
|
|
214
|
-
from ..runtime.execution_router import
|
|
227
|
+
from ..runtime.execution_router import (
|
|
228
|
+
execute_plan_steps_async,
|
|
229
|
+
filter_apply_steps,
|
|
230
|
+
undo_remote_steps,
|
|
231
|
+
)
|
|
215
232
|
|
|
216
233
|
branch.status = "running"
|
|
217
234
|
branch.compiled_plan = compiled_plan
|
|
218
235
|
|
|
219
|
-
branch.before_snapshot = capture_fn
|
|
236
|
+
branch.before_snapshot = await asyncio.to_thread(capture_fn)
|
|
220
237
|
|
|
221
238
|
# Filter out read-only verification steps from the apply pass (canonical
|
|
222
239
|
# list lives in execution_router.READ_ONLY_TOOLS).
|
|
@@ -244,24 +261,13 @@ async def run_branch_async(
|
|
|
244
261
|
]
|
|
245
262
|
|
|
246
263
|
steps_executed = sum(1 for r in exec_results if r.ok)
|
|
247
|
-
# Only remote_command steps land on Ableton's linear undo stack. Bridge
|
|
248
|
-
# (M4L/OSC) and mcp_tool mutations do NOT, so issuing one `undo` per
|
|
249
|
-
# successful step over-undoes and walks back unrelated prior user edits.
|
|
250
|
-
# Count undos from remote_command successes alone.
|
|
251
|
-
undo_count = sum(
|
|
252
|
-
1 for r in exec_results if r.ok and r.backend == "remote_command"
|
|
253
|
-
)
|
|
254
264
|
branch.executed_at_ms = int(time.time() * 1000)
|
|
255
|
-
branch.after_snapshot = capture_fn
|
|
265
|
+
branch.after_snapshot = await asyncio.to_thread(capture_fn)
|
|
256
266
|
|
|
257
|
-
# Undo only the remote_command steps back to checkpoint
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
ableton.send_command("undo", {})
|
|
262
|
-
except Exception as exc:
|
|
263
|
-
logger.debug("run_branch_async failed: %s", exc)
|
|
264
|
-
break
|
|
267
|
+
# Undo only the remote_command steps back to checkpoint (bridge/mcp_tool
|
|
268
|
+
# mutations don't land on Ableton's linear undo stack — see
|
|
269
|
+
# undo_remote_steps' docstring).
|
|
270
|
+
await undo_remote_steps(ableton, exec_results)
|
|
265
271
|
|
|
266
272
|
# A branch is "evaluated" only if it actually applied at least one step.
|
|
267
273
|
# If every step failed, mark it "failed" — this is the truth-release
|
|
@@ -442,7 +448,8 @@ def commit_branch(
|
|
|
442
448
|
|
|
443
449
|
def discard_experiment(experiment_id: str) -> dict:
|
|
444
450
|
"""Discard an entire experiment set."""
|
|
445
|
-
|
|
451
|
+
with _EXPERIMENTS_LOCK:
|
|
452
|
+
exp = _EXPERIMENTS.get(experiment_id)
|
|
446
453
|
if not exp:
|
|
447
454
|
return {"error": f"Experiment {experiment_id} not found"}
|
|
448
455
|
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import asyncio
|
|
14
|
+
import functools
|
|
13
15
|
import time
|
|
14
16
|
from typing import Optional
|
|
15
17
|
|
|
@@ -351,7 +353,7 @@ async def run_experiment(
|
|
|
351
353
|
if experiment.baseline_transport is None:
|
|
352
354
|
from .baseline import capture_baseline
|
|
353
355
|
try:
|
|
354
|
-
experiment.baseline_transport = capture_baseline
|
|
356
|
+
experiment.baseline_transport = await asyncio.to_thread(capture_baseline, ableton)
|
|
355
357
|
except Exception as exc:
|
|
356
358
|
logger.debug("baseline capture failed: %s", exc)
|
|
357
359
|
experiment.baseline_transport = None
|
|
@@ -365,7 +367,8 @@ async def run_experiment(
|
|
|
365
367
|
# Between branches (not before the first), restore the baseline so
|
|
366
368
|
# the next before_snapshot reads from the same reference state.
|
|
367
369
|
if pending_seen > 0:
|
|
368
|
-
|
|
370
|
+
await asyncio.to_thread(
|
|
371
|
+
engine.prepare_for_next_branch,
|
|
369
372
|
ableton, experiment.baseline_transport, stabilize_ms=300,
|
|
370
373
|
)
|
|
371
374
|
pending_seen += 1
|
|
@@ -414,7 +417,7 @@ async def run_experiment(
|
|
|
414
417
|
results.append(branch.to_dict())
|
|
415
418
|
continue
|
|
416
419
|
|
|
417
|
-
session_info = ableton.
|
|
420
|
+
session_info = await ableton.send_command_async("get_session_info")
|
|
418
421
|
kernel = {"session_info": session_info, "mode": "explore"}
|
|
419
422
|
plan = compiler.compile(move, kernel)
|
|
420
423
|
compiled_dict = plan.to_dict()
|
|
@@ -422,12 +425,19 @@ async def run_experiment(
|
|
|
422
425
|
# Pick the capture function — render-verify mode captures audio
|
|
423
426
|
# and extracts a TimbralFingerprint, adding latency but giving
|
|
424
427
|
# classify_branch_outcome real measurable evidence.
|
|
428
|
+
#
|
|
429
|
+
# NOTE: bound via functools.partial (not a lambda) so the blocking
|
|
430
|
+
# `_capture_snapshot*` call is never a "bare" call site in this
|
|
431
|
+
# function's body — it is only ever invoked downstream inside
|
|
432
|
+
# `engine.run_branch_async` via `await asyncio.to_thread(capture_fn)`,
|
|
433
|
+
# which is where the actual off-loop offload happens.
|
|
425
434
|
if render_verify:
|
|
426
|
-
capture_fn =
|
|
435
|
+
capture_fn = functools.partial(
|
|
436
|
+
_capture_snapshot_with_render_verify,
|
|
427
437
|
ctx, duration_seconds=render_duration_seconds,
|
|
428
438
|
)
|
|
429
439
|
else:
|
|
430
|
-
capture_fn =
|
|
440
|
+
capture_fn = functools.partial(_capture_snapshot, ctx)
|
|
431
441
|
|
|
432
442
|
# Run the branch through the async router
|
|
433
443
|
await engine.run_branch_async(
|