livepilot 1.27.0 → 1.27.2
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 +68 -0
- package/README.md +26 -7
- package/bin/livepilot.js +86 -23
- package/installer/install.js +21 -12
- package/livepilot/.Codex-plugin/plugin.json +1 -1
- package/livepilot/.claude-plugin/plugin.json +1 -1
- package/livepilot/skills/livepilot-core/SKILL.md +8 -8
- package/livepilot/skills/livepilot-core/references/overview.md +2 -2
- package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
- 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 +20 -1
- package/mcp_server/atlas/tools.py +83 -22
- package/mcp_server/composer/fast/brief_builder.py +6 -2
- package/mcp_server/composer/full/apply.py +9 -0
- package/mcp_server/composer/full/layer_planner.py +16 -6
- package/mcp_server/composer/tools.py +12 -6
- package/mcp_server/connection.py +2 -1
- package/mcp_server/creative_constraints/engine.py +46 -0
- package/mcp_server/experiment/engine.py +10 -3
- package/mcp_server/grader/client.py +30 -2
- package/mcp_server/grader/tools.py +8 -2
- package/mcp_server/hook_hunter/analyzer.py +15 -2
- package/mcp_server/m4l_bridge.py +145 -54
- package/mcp_server/memory/taste_graph.py +16 -0
- package/mcp_server/memory/technique_store.py +23 -3
- package/mcp_server/mix_engine/state_builder.py +4 -2
- package/mcp_server/musical_intelligence/detectors.py +11 -2
- package/mcp_server/musical_intelligence/tools.py +15 -2
- package/mcp_server/preview_studio/engine.py +30 -1
- package/mcp_server/project_brain/tools.py +56 -52
- package/mcp_server/reference_engine/tools.py +22 -2
- package/mcp_server/runtime/execution_router.py +6 -0
- package/mcp_server/runtime/live_version.py +27 -8
- package/mcp_server/runtime/mcp_dispatch.py +22 -0
- package/mcp_server/runtime/remote_commands.py +9 -4
- package/mcp_server/runtime/safety_kernel.py +11 -0
- package/mcp_server/sample_engine/critics.py +7 -3
- package/mcp_server/sample_engine/slice_workflow.py +3 -2
- package/mcp_server/sample_engine/tools.py +53 -21
- package/mcp_server/server.py +1 -1
- package/mcp_server/song_brain/builder.py +17 -1
- package/mcp_server/sound_design/tools.py +1 -1
- package/mcp_server/splice_client/http_bridge.py +43 -1
- package/mcp_server/splice_client/models.py +7 -3
- package/mcp_server/synthesis_brain/adapters/analog.py +13 -1
- package/mcp_server/synthesis_brain/adapters/operator.py +5 -2
- package/mcp_server/synthesis_brain/adapters/wavetable.py +4 -4
- package/mcp_server/tools/_composition_engine/models.py +6 -0
- package/mcp_server/tools/_composition_engine/sections.py +4 -0
- package/mcp_server/tools/clips.py +5 -4
- package/mcp_server/tools/composition.py +5 -1
- package/mcp_server/tools/midi_io.py +40 -1
- package/mcp_server/tools/transport.py +1 -1
- package/mcp_server/user_corpus/plugin_engine/detector.py +66 -11
- package/mcp_server/user_corpus/runner.py +7 -1
- package/mcp_server/user_corpus/scanners/amxd.py +24 -13
- package/mcp_server/user_corpus/tools.py +45 -9
- package/mcp_server/wonder_mode/tools.py +66 -27
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +21 -8
- package/remote_script/LivePilot/server.py +23 -3
- package/remote_script/LivePilot/tracks.py +11 -5
- package/requirements.txt +1 -1
- package/server.json +2 -2
|
@@ -17,13 +17,20 @@ from . import engine
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
def _build_synth_profiles_for_wonder(
|
|
20
|
+
def _build_synth_profiles_for_wonder(
|
|
21
|
+
ctx, request_text: str, diagnosis: dict, session_info: dict | None = None
|
|
22
|
+
) -> list:
|
|
21
23
|
"""Build SynthProfile objects for every track holding a native synth.
|
|
22
24
|
|
|
23
25
|
Wires the synthesis_brain producer into enter_wonder_mode's runtime
|
|
24
26
|
flow — without this helper, ``propose_synth_branches`` is registered
|
|
25
27
|
but unreachable from the MCP surface.
|
|
26
28
|
|
|
29
|
+
``session_info``: optional pre-fetched get_session_info payload. When
|
|
30
|
+
a non-empty dict is supplied the helper reuses it instead of issuing
|
|
31
|
+
its own round-trip on the single-client TCP socket. Callers should
|
|
32
|
+
always thread the shared payload to avoid redundant fetches.
|
|
33
|
+
|
|
27
34
|
Returns a list of SynthProfile objects (possibly empty). All errors
|
|
28
35
|
are swallowed and logged — missing devices, disconnected Ableton,
|
|
29
36
|
unsupported devices all land as "no synth branches" rather than
|
|
@@ -39,11 +46,16 @@ def _build_synth_profiles_for_wonder(ctx, request_text: str, diagnosis: dict) ->
|
|
|
39
46
|
if ableton is None:
|
|
40
47
|
return []
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
# Reuse the pre-fetched session payload when available; only fall back
|
|
50
|
+
# to a fresh round-trip when called without one.
|
|
51
|
+
if isinstance(session_info, dict) and session_info and "error" not in session_info:
|
|
52
|
+
session = session_info
|
|
53
|
+
else:
|
|
54
|
+
try:
|
|
55
|
+
session = ableton.send_command("get_session_info", {})
|
|
56
|
+
except Exception as exc:
|
|
57
|
+
logger.debug("session fetch for synth profiles failed: %s", exc)
|
|
58
|
+
return []
|
|
47
59
|
if not isinstance(session, dict) or "error" in session:
|
|
48
60
|
return []
|
|
49
61
|
|
|
@@ -164,21 +176,35 @@ def _get_ledger_entries(ctx: Context) -> list[dict]:
|
|
|
164
176
|
return []
|
|
165
177
|
|
|
166
178
|
|
|
167
|
-
def _get_stuckness_report(
|
|
168
|
-
|
|
179
|
+
def _get_stuckness_report(
|
|
180
|
+
ctx: Context,
|
|
181
|
+
song_brain: dict,
|
|
182
|
+
action_ledger: list[dict] | None = None,
|
|
183
|
+
session_info: dict | None = None,
|
|
184
|
+
) -> dict | None:
|
|
185
|
+
"""Run stuckness detection on recent actions if available.
|
|
186
|
+
|
|
187
|
+
``action_ledger`` and ``session_info`` may be supplied pre-fetched by
|
|
188
|
+
the caller. enter_wonder_mode already fetches both once at the top of
|
|
189
|
+
the flow, so threading them here avoids a duplicate ledger read and a
|
|
190
|
+
redundant get_session_info round-trip on the single-client TCP socket.
|
|
191
|
+
"""
|
|
169
192
|
try:
|
|
170
193
|
from ..stuckness_detector.detector import detect_stuckness
|
|
171
|
-
action_ledger
|
|
194
|
+
if action_ledger is None:
|
|
195
|
+
action_ledger = _get_ledger_entries(ctx)
|
|
172
196
|
if not action_ledger:
|
|
173
197
|
return None
|
|
174
|
-
#
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
198
|
+
# Reuse the pre-fetched session payload; only round-trip if the
|
|
199
|
+
# caller did not supply one.
|
|
200
|
+
if session_info is None:
|
|
201
|
+
session_info = {}
|
|
202
|
+
try:
|
|
203
|
+
ableton = ctx.lifespan_context.get("ableton")
|
|
204
|
+
if ableton:
|
|
205
|
+
session_info = ableton.send_command("get_session_info", {})
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
logger.warning("session_info fetch for stuckness failed: %s", exc)
|
|
182
208
|
report = detect_stuckness(
|
|
183
209
|
action_history=action_ledger,
|
|
184
210
|
session_info=session_info,
|
|
@@ -218,7 +244,24 @@ def enter_wonder_mode(
|
|
|
218
244
|
taste_graph = _get_taste_graph(ctx)
|
|
219
245
|
active_constraints = _get_active_constraints()
|
|
220
246
|
action_ledger = _get_ledger_entries(ctx)
|
|
221
|
-
|
|
247
|
+
|
|
248
|
+
# Single get_session_info round-trip for the whole flow. The Remote
|
|
249
|
+
# Script is single-client on TCP 9878, so each redundant fetch is a
|
|
250
|
+
# serialized round-trip — fetch once and thread the payload into the
|
|
251
|
+
# stuckness report, the kernel dict, and the synth-profile builder.
|
|
252
|
+
session_info: dict = {}
|
|
253
|
+
try:
|
|
254
|
+
ableton = ctx.lifespan_context.get("ableton")
|
|
255
|
+
if ableton:
|
|
256
|
+
session_info = ableton.send_command("get_session_info", {})
|
|
257
|
+
except Exception as exc:
|
|
258
|
+
logger.warning("session_info fetch failed: %s", exc)
|
|
259
|
+
if not isinstance(session_info, dict) or "error" in session_info:
|
|
260
|
+
session_info = {}
|
|
261
|
+
|
|
262
|
+
stuckness_report = _get_stuckness_report(
|
|
263
|
+
ctx, song_brain, action_ledger=action_ledger, session_info=session_info
|
|
264
|
+
)
|
|
222
265
|
|
|
223
266
|
# 1. Build diagnosis
|
|
224
267
|
diagnosis = build_diagnosis(
|
|
@@ -249,14 +292,8 @@ def enter_wonder_mode(
|
|
|
249
292
|
# Graceful degradation — analytical variants still work
|
|
250
293
|
logger.warning("sample opportunity search failed: %s", exc)
|
|
251
294
|
|
|
252
|
-
# 1c.
|
|
253
|
-
|
|
254
|
-
try:
|
|
255
|
-
ableton = ctx.lifespan_context.get("ableton")
|
|
256
|
-
if ableton:
|
|
257
|
-
session_info = ableton.send_command("get_session_info", {})
|
|
258
|
-
except Exception as exc:
|
|
259
|
-
logger.warning("session_info fetch for kernel failed: %s", exc)
|
|
295
|
+
# 1c. session_info for the kernel was already fetched once above and
|
|
296
|
+
# threaded into the stuckness report — reuse it here, no extra round-trip.
|
|
260
297
|
|
|
261
298
|
# 2. Generate variants (legacy path)
|
|
262
299
|
result = engine.generate_wonder_variants(
|
|
@@ -291,7 +328,9 @@ def enter_wonder_mode(
|
|
|
291
328
|
# track that holds a native synth. Lets propose_synth_branches reach
|
|
292
329
|
# the runtime — without this the producer is dark despite being
|
|
293
330
|
# registered in the conductor.
|
|
294
|
-
synth_profiles = _build_synth_profiles_for_wonder(
|
|
331
|
+
synth_profiles = _build_synth_profiles_for_wonder(
|
|
332
|
+
ctx, request_text, diag_dict, session_info=session_info
|
|
333
|
+
)
|
|
295
334
|
|
|
296
335
|
# Composer branches fire when the base conductor routes to
|
|
297
336
|
# composition — otherwise we'd be emitting composition scaffolding
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.27.
|
|
3
|
+
"version": "1.27.2",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
5
|
"description": "Agentic production system for Ableton Live 12 — 467 tools, 56 domains, 44 semantic moves. Device atlas (5264 devices, 120 enriched, 7 indexes), Splice intelligence (gRPC + GraphQL describe-a-sound + preview + collections + presets), 9-band spectral perception auto-loaded via ensure_analyzer_on_master, Creative Director skill, technique memory, 12 creative intelligence engines",
|
|
6
6
|
"author": "Pilot Studio",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.27.
|
|
8
|
+
__version__ = "1.27.2"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
|
@@ -98,22 +98,28 @@ def _force_reload_handlers(cs=None):
|
|
|
98
98
|
except Exception:
|
|
99
99
|
pass
|
|
100
100
|
|
|
101
|
+
failures = []
|
|
102
|
+
|
|
101
103
|
try:
|
|
102
104
|
importlib.reload(router)
|
|
103
105
|
except Exception as exc:
|
|
104
|
-
|
|
106
|
+
msg = "reload(router) FAILED — %s: %s" % (type(exc).__name__, exc)
|
|
107
|
+
_log(msg)
|
|
108
|
+
failures.append(("LivePilot.router", "%s: %s" % (type(exc).__name__, exc)))
|
|
105
109
|
|
|
106
110
|
try:
|
|
107
111
|
importlib.reload(utils)
|
|
108
112
|
except Exception as exc:
|
|
109
|
-
|
|
113
|
+
msg = "reload(utils) FAILED — %s: %s" % (type(exc).__name__, exc)
|
|
114
|
+
_log(msg)
|
|
115
|
+
failures.append(("LivePilot.utils", "%s: %s" % (type(exc).__name__, exc)))
|
|
110
116
|
|
|
111
117
|
# Invalidate caches so iter_modules sees newly-added files even if
|
|
112
118
|
# an importer cached the previous directory listing.
|
|
113
119
|
importlib.invalidate_caches()
|
|
114
120
|
pkg = _sys.modules.get("LivePilot")
|
|
115
121
|
if pkg is None or getattr(pkg, "__path__", None) is None:
|
|
116
|
-
return
|
|
122
|
+
return {"discovered": 0, "reloaded": 0, "first_imported": 0, "failures": failures}
|
|
117
123
|
|
|
118
124
|
discovered = reloaded = first_imported = 0
|
|
119
125
|
for _finder, modname, _is_pkg in pkgutil.iter_modules(pkg.__path__):
|
|
@@ -130,8 +136,9 @@ def _force_reload_handlers(cs=None):
|
|
|
130
136
|
importlib.import_module(full_name)
|
|
131
137
|
first_imported += 1
|
|
132
138
|
except Exception as exc:
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
msg = "reload(%s) FAILED — %s: %s" % (full_name, type(exc).__name__, exc)
|
|
140
|
+
_log(msg)
|
|
141
|
+
failures.append((full_name, "%s: %s" % (type(exc).__name__, exc)))
|
|
135
142
|
|
|
136
143
|
# reload_handlers_cmd lives in __init__.py (not a handler module),
|
|
137
144
|
# so the step-3 loop does not cover it. Re-register manually.
|
|
@@ -140,15 +147,21 @@ def _force_reload_handlers(cs=None):
|
|
|
140
147
|
_log("reload complete — %d discovered (%d reloaded, %d first-imported)" % (
|
|
141
148
|
discovered, reloaded, first_imported))
|
|
142
149
|
|
|
150
|
+
return {"discovered": discovered, "reloaded": reloaded,
|
|
151
|
+
"first_imported": first_imported, "failures": failures}
|
|
152
|
+
|
|
143
153
|
|
|
144
154
|
def reload_handlers_cmd(song, params):
|
|
145
155
|
"""TCP-accessible reload trigger. Lets automation refresh handlers
|
|
146
156
|
without a UI Control Surface toggle — the core dev-loop improvement.
|
|
147
|
-
Returns the handler count so the caller can assert before/after.
|
|
148
|
-
|
|
157
|
+
Returns the handler count so the caller can assert before/after.
|
|
158
|
+
Returns any reload failures so the caller can detect silent errors."""
|
|
159
|
+
result = _force_reload_handlers(cs=None)
|
|
160
|
+
failures = result.get("failures", []) if isinstance(result, dict) else []
|
|
149
161
|
return {
|
|
150
162
|
"reloaded": True,
|
|
151
163
|
"handler_count": len(router._handlers),
|
|
164
|
+
"errors": [{"module": m, "error": e} for m, e in failures],
|
|
152
165
|
}
|
|
153
166
|
|
|
154
167
|
|
|
@@ -346,9 +346,14 @@ class LivePilotServer(object):
|
|
|
346
346
|
else:
|
|
347
347
|
timeout = 10
|
|
348
348
|
|
|
349
|
-
# Per-command response queue
|
|
349
|
+
# Per-command response queue + cancellation flag. The flag is shared
|
|
350
|
+
# between this TCP thread and the main thread that dequeues the item:
|
|
351
|
+
# if we time out below, we set it so _process_next_command skips the
|
|
352
|
+
# (now-abandoned) dispatch instead of mutating Live after the client
|
|
353
|
+
# has already been told the command timed out (phantom write).
|
|
350
354
|
response_queue = queue.Queue()
|
|
351
|
-
|
|
355
|
+
cancelled = threading.Event()
|
|
356
|
+
self._command_queue.put((command, response_queue, cancelled))
|
|
352
357
|
|
|
353
358
|
# Schedule processing on Ableton's main thread
|
|
354
359
|
try:
|
|
@@ -385,6 +390,13 @@ class LivePilotServer(object):
|
|
|
385
390
|
try:
|
|
386
391
|
resp = response_queue.get(timeout=timeout)
|
|
387
392
|
except queue.Empty:
|
|
393
|
+
# Mark the queued command as abandoned. If it hasn't been dequeued
|
|
394
|
+
# yet (or is awaiting its settle hop), _process_next_command sees
|
|
395
|
+
# the flag and skips router.dispatch — preventing a write that the
|
|
396
|
+
# client was just told timed out from executing on Live's main
|
|
397
|
+
# thread later. If dispatch already happened, setting the flag is
|
|
398
|
+
# a harmless no-op.
|
|
399
|
+
cancelled.set()
|
|
388
400
|
resp = {
|
|
389
401
|
"id": request_id,
|
|
390
402
|
"ok": False,
|
|
@@ -399,10 +411,18 @@ class LivePilotServer(object):
|
|
|
399
411
|
"""Called on Ableton's main thread via schedule_message.
|
|
400
412
|
Processes one command from the queue."""
|
|
401
413
|
try:
|
|
402
|
-
command, response_queue = self._command_queue.get_nowait()
|
|
414
|
+
command, response_queue, cancelled = self._command_queue.get_nowait()
|
|
403
415
|
except queue.Empty:
|
|
404
416
|
return
|
|
405
417
|
|
|
418
|
+
# The TCP thread already gave up on this command (timed out) and told
|
|
419
|
+
# the client so. Do NOT dispatch it — running the write now would be a
|
|
420
|
+
# phantom mutation. Nobody is waiting on response_queue, so just keep
|
|
421
|
+
# the main-thread pump alive by draining whatever is left.
|
|
422
|
+
if cancelled.is_set():
|
|
423
|
+
self._drain_queue()
|
|
424
|
+
return
|
|
425
|
+
|
|
406
426
|
cmd_type = command.get("type", "")
|
|
407
427
|
is_write = is_write_command(cmd_type)
|
|
408
428
|
|
|
@@ -91,12 +91,18 @@ def get_track_info(song, params):
|
|
|
91
91
|
if track.is_foldable:
|
|
92
92
|
result["fold_state"] = bool(track.fold_state)
|
|
93
93
|
|
|
94
|
-
# Regular tracks
|
|
94
|
+
# Regular tracks expose arm + input type; Group/Return/Master tracks raise
|
|
95
|
+
# RuntimeError on these LOM properties. hasattr() does NOT catch it (Live
|
|
96
|
+
# raises rather than omitting the attribute), so a Group track at a positive
|
|
97
|
+
# index used to crash get_track_info. Guard each access — mirrors the
|
|
98
|
+
# transport.py get_session_info fix (commit 4aec8e6, closing PR #35).
|
|
95
99
|
if track_index >= 0:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
for _prop in ("arm", "has_midi_input", "has_audio_input",
|
|
101
|
+
"current_monitoring_state"):
|
|
102
|
+
try:
|
|
103
|
+
result[_prop] = getattr(track, _prop)
|
|
104
|
+
except Exception:
|
|
105
|
+
result[_prop] = None
|
|
100
106
|
else:
|
|
101
107
|
result["arm"] = None
|
|
102
108
|
result["has_midi_input"] = None
|
package/requirements.txt
CHANGED
|
@@ -13,7 +13,7 @@ mutagen>=1.47.0
|
|
|
13
13
|
# falls back to the SQLite sounds.db which only returns locally downloaded
|
|
14
14
|
# samples (see docs/2026-04-14-bugs-discovered.md — P0-2).
|
|
15
15
|
grpcio>=1.81.1
|
|
16
|
-
protobuf>=7.35.
|
|
16
|
+
protobuf>=7.35.1
|
|
17
17
|
|
|
18
18
|
# Known benign warning during install:
|
|
19
19
|
# ERROR: pip's dependency resolver does not currently take into account
|
package/server.json
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.27.
|
|
9
|
+
"version": "1.27.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.27.
|
|
14
|
+
"version": "1.27.1",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|