livepilot 1.17.1 → 1.17.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 +124 -0
- package/README.md +8 -7
- package/m4l_device/BUILD_GUIDE.md +24 -20
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/m4l_bridge.py +2 -1
- package/mcp_server/preview_studio/engine.py +85 -11
- package/mcp_server/preview_studio/models.py +8 -0
- package/mcp_server/preview_studio/tools.py +98 -48
- package/mcp_server/runtime/capability_state.py +18 -0
- package/mcp_server/runtime/degradation.py +62 -0
- package/mcp_server/runtime/tools.py +53 -4
- package/mcp_server/song_brain/tools.py +23 -0
- package/mcp_server/synthesis_brain/timbre.py +14 -8
- package/mcp_server/tools/_agent_os_engine/__init__.py +10 -0
- package/mcp_server/tools/_agent_os_engine/iteration.py +344 -0
- package/mcp_server/tools/agent_os.py +194 -3
- package/mcp_server/tools/analyzer.py +19 -6
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/requirements.txt +5 -5
- package/server.json +3 -3
|
@@ -240,9 +240,9 @@ def evaluate_move(
|
|
|
240
240
|
Takes before/after sonic snapshots and the active GoalVector.
|
|
241
241
|
Returns a score and keep/undo recommendation.
|
|
242
242
|
|
|
243
|
-
Snapshots should contain: spectrum (
|
|
244
|
-
|
|
245
|
-
making changes.
|
|
243
|
+
Snapshots should contain: spectrum (9-band dict sub_low → air, or
|
|
244
|
+
8-band from pre-v1.16 .amxd builds), rms, peak. Get these from
|
|
245
|
+
get_master_spectrum + get_master_rms before and after making changes.
|
|
246
246
|
|
|
247
247
|
Hard rules enforce undo when:
|
|
248
248
|
- No measurable improvement (delta <= 0)
|
|
@@ -471,3 +471,194 @@ def route_request(
|
|
|
471
471
|
|
|
472
472
|
plan = conductor.classify_request(request)
|
|
473
473
|
return plan.to_dict()
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
# ── iterate_toward_goal (closed evaluation loop) ──────────────────────
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@mcp.tool()
|
|
480
|
+
async def iterate_toward_goal(
|
|
481
|
+
ctx: Context,
|
|
482
|
+
goal_vector: dict | str,
|
|
483
|
+
candidate_move_sets: list,
|
|
484
|
+
threshold: float = 0.70,
|
|
485
|
+
max_iterations: int = 3,
|
|
486
|
+
on_timeout: str = "commit_best",
|
|
487
|
+
render_verify: bool = False,
|
|
488
|
+
) -> dict:
|
|
489
|
+
"""Close the evaluation loop: run experiments until threshold or timeout.
|
|
490
|
+
|
|
491
|
+
Each iteration creates an experiment from one candidate_move_sets entry,
|
|
492
|
+
runs all branches (which auto-undo per-branch via the experiment engine),
|
|
493
|
+
and checks the top-ranked branch's score against the GoalVector. If score
|
|
494
|
+
>= threshold, commit that branch permanently and stop. Otherwise discard
|
|
495
|
+
the experiment and try the next candidate set. On timeout, commit the
|
|
496
|
+
best-so-far (on_timeout='commit_best') or commit nothing
|
|
497
|
+
(on_timeout='discard_on_timeout').
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
goal_vector: Compiled GoalVector dict (from compile_goal_vector) or
|
|
501
|
+
JSON string. Provides the scoring target passed through to the
|
|
502
|
+
evaluation scorer inside each run_experiment call.
|
|
503
|
+
candidate_move_sets: List of move_id lists — one per iteration.
|
|
504
|
+
Example: [["make_punchier", "widen_stereo"], ["tighten_low_end"]].
|
|
505
|
+
Iteration 0 tries the first list, iteration 1 the second, etc.
|
|
506
|
+
If shorter than max_iterations, iteration stops when exhausted.
|
|
507
|
+
threshold: Winner score required to commit early. 0.0–1.0. Default 0.70.
|
|
508
|
+
max_iterations: Hard cap on outer-loop iterations. Default 3.
|
|
509
|
+
on_timeout: "commit_best" (commit highest-scoring experiment at end)
|
|
510
|
+
or "discard_on_timeout" (no commit if threshold never met).
|
|
511
|
+
render_verify: When True each branch captures + analyzes audio
|
|
512
|
+
(~6s extra per branch). Default False.
|
|
513
|
+
|
|
514
|
+
Returns: IterationResult dict with status, iterations_run,
|
|
515
|
+
committed_experiment_id, committed_branch_id, final_score, steps,
|
|
516
|
+
reason.
|
|
517
|
+
|
|
518
|
+
Safety: Only commits when threshold_met OR (on_timeout='commit_best' AND
|
|
519
|
+
best-so-far exists). Never double-undoes — per-branch undo is handled
|
|
520
|
+
inside run_experiment; this tool only issues commit or discard.
|
|
521
|
+
"""
|
|
522
|
+
import time as _time
|
|
523
|
+
from ..branches import seed_from_move_id
|
|
524
|
+
from ..experiment import engine as exp_engine
|
|
525
|
+
from ..experiment.tools import (
|
|
526
|
+
_capture_snapshot,
|
|
527
|
+
_capture_snapshot_with_render_verify,
|
|
528
|
+
)
|
|
529
|
+
from ..semantic_moves import registry, compiler
|
|
530
|
+
from ..evaluation.policy import classify_branch_outcome
|
|
531
|
+
from ._agent_os_engine import iterate_toward_goal_engine_async
|
|
532
|
+
|
|
533
|
+
gv_dict = _parse_json_param(goal_vector, "goal_vector")
|
|
534
|
+
|
|
535
|
+
if not isinstance(candidate_move_sets, list) or not all(
|
|
536
|
+
isinstance(s, list) and all(isinstance(m, str) for m in s)
|
|
537
|
+
for s in candidate_move_sets
|
|
538
|
+
):
|
|
539
|
+
return {
|
|
540
|
+
"error": (
|
|
541
|
+
"candidate_move_sets must be a list of lists of move_id strings"
|
|
542
|
+
)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
ableton = _get_ableton(ctx)
|
|
546
|
+
bridge = ctx.lifespan_context.get("m4l")
|
|
547
|
+
mcp_registry = ctx.lifespan_context.get("mcp_dispatch", {})
|
|
548
|
+
|
|
549
|
+
# Pre-validate the GoalVector once — the eval_fn closure reuses this.
|
|
550
|
+
goal = engine.validate_goal_vector(
|
|
551
|
+
request_text=gv_dict.get("request_text", "iterate_toward_goal"),
|
|
552
|
+
targets=gv_dict.get("targets", {}),
|
|
553
|
+
protect=gv_dict.get("protect", {}),
|
|
554
|
+
mode=gv_dict.get("mode", "improve"),
|
|
555
|
+
aggression=float(gv_dict.get("aggression", 0.5)),
|
|
556
|
+
research_mode=gv_dict.get("research_mode", "none"),
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
# ── Callbacks wire the pure-logic engine to real experiment I/O ──
|
|
560
|
+
|
|
561
|
+
async def _create(move_ids: list[str]) -> str:
|
|
562
|
+
seeds = [seed_from_move_id(mid) for mid in move_ids]
|
|
563
|
+
kernel_id = f"iter_kern_{int(_time.time())}"
|
|
564
|
+
exp = exp_engine.create_experiment_from_seeds(
|
|
565
|
+
request_text=gv_dict.get("request_text", "iterate_toward_goal"),
|
|
566
|
+
seeds=seeds,
|
|
567
|
+
kernel_id=kernel_id,
|
|
568
|
+
)
|
|
569
|
+
return exp.experiment_id
|
|
570
|
+
|
|
571
|
+
async def _run(experiment_id: str):
|
|
572
|
+
experiment = exp_engine.get_experiment(experiment_id)
|
|
573
|
+
if experiment is None:
|
|
574
|
+
return None, 0.0
|
|
575
|
+
|
|
576
|
+
if render_verify:
|
|
577
|
+
capture_fn = lambda: _capture_snapshot_with_render_verify(ctx, 2.0)
|
|
578
|
+
else:
|
|
579
|
+
capture_fn = lambda: _capture_snapshot(ctx)
|
|
580
|
+
|
|
581
|
+
for branch in experiment.branches:
|
|
582
|
+
if branch.status != "pending":
|
|
583
|
+
continue
|
|
584
|
+
|
|
585
|
+
# Compile plan from semantic move when branch doesn't carry one
|
|
586
|
+
if branch.compiled_plan is None and branch.move_id:
|
|
587
|
+
move = registry.get_move(branch.move_id)
|
|
588
|
+
if move is None:
|
|
589
|
+
branch.status = "failed"
|
|
590
|
+
continue
|
|
591
|
+
session_info = ableton.send_command("get_session_info")
|
|
592
|
+
kernel = {"session_info": session_info, "mode": "explore"}
|
|
593
|
+
plan = compiler.compile(move, kernel)
|
|
594
|
+
branch.compiled_plan = plan.to_dict()
|
|
595
|
+
|
|
596
|
+
if branch.compiled_plan is None:
|
|
597
|
+
branch.status = "failed"
|
|
598
|
+
continue
|
|
599
|
+
|
|
600
|
+
await exp_engine.run_branch_async(
|
|
601
|
+
branch=branch,
|
|
602
|
+
ableton=ableton,
|
|
603
|
+
compiled_plan=branch.compiled_plan,
|
|
604
|
+
capture_fn=capture_fn,
|
|
605
|
+
bridge=bridge,
|
|
606
|
+
mcp_registry=mcp_registry,
|
|
607
|
+
ctx=ctx,
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
def eval_fn(before, after):
|
|
611
|
+
score_result = engine.compute_evaluation_score(goal, before, after)
|
|
612
|
+
outcome = classify_branch_outcome(
|
|
613
|
+
score=score_result.get("score", 0.0),
|
|
614
|
+
protection_violated=not score_result.get("keep_change", True)
|
|
615
|
+
and "protected" in " ".join(score_result.get("notes", [])).lower(),
|
|
616
|
+
measurable_count=0,
|
|
617
|
+
target_count=0,
|
|
618
|
+
goal_progress=score_result.get("goal_progress", 0.0),
|
|
619
|
+
exploration_rules=False,
|
|
620
|
+
)
|
|
621
|
+
return {
|
|
622
|
+
"score": outcome.score,
|
|
623
|
+
"keep_change": outcome.keep_change,
|
|
624
|
+
"status": outcome.status,
|
|
625
|
+
"note": outcome.note,
|
|
626
|
+
"dimension_changes": score_result.get("dimension_changes", {}),
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
exp_engine.evaluate_branch(branch, eval_fn)
|
|
630
|
+
if branch.evaluation and branch.evaluation.get("status") == "keep":
|
|
631
|
+
branch.status = "evaluated"
|
|
632
|
+
elif branch.evaluation and branch.evaluation.get("status") == "undo":
|
|
633
|
+
branch.status = "rejected"
|
|
634
|
+
|
|
635
|
+
ranked = experiment.ranked_branches()
|
|
636
|
+
if not ranked:
|
|
637
|
+
return None, 0.0
|
|
638
|
+
top = ranked[0]
|
|
639
|
+
return top.branch_id, float(top.score or 0.0)
|
|
640
|
+
|
|
641
|
+
async def _commit(experiment_id: str, branch_id: str) -> dict:
|
|
642
|
+
return await exp_engine.commit_branch_async(
|
|
643
|
+
exp_engine.get_experiment(experiment_id),
|
|
644
|
+
branch_id,
|
|
645
|
+
ableton,
|
|
646
|
+
bridge=bridge,
|
|
647
|
+
mcp_registry=mcp_registry,
|
|
648
|
+
ctx=ctx,
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
async def _discard(experiment_id: str) -> dict:
|
|
652
|
+
return exp_engine.discard_experiment(experiment_id)
|
|
653
|
+
|
|
654
|
+
result = await iterate_toward_goal_engine_async(
|
|
655
|
+
candidate_move_sets=candidate_move_sets,
|
|
656
|
+
threshold=float(threshold),
|
|
657
|
+
max_iterations=int(max_iterations),
|
|
658
|
+
create_experiment_fn=_create,
|
|
659
|
+
run_experiment_fn=_run,
|
|
660
|
+
commit_fn=_commit,
|
|
661
|
+
discard_fn=_discard,
|
|
662
|
+
on_timeout=on_timeout,
|
|
663
|
+
)
|
|
664
|
+
return result.to_dict()
|
|
@@ -214,11 +214,24 @@ async def get_master_spectrum(
|
|
|
214
214
|
samples: int = 0,
|
|
215
215
|
sub_detail: bool = False,
|
|
216
216
|
) -> dict:
|
|
217
|
-
"""Get
|
|
218
|
-
|
|
219
|
-
Returns band energies
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
"""Get 9-band frequency analysis of the master bus.
|
|
218
|
+
|
|
219
|
+
Returns band energies (fffb~ center frequencies shown in parens):
|
|
220
|
+
sub_low 20-60 Hz (~35 Hz center) — kick fundamentals, Villalobos subs
|
|
221
|
+
sub 60-120 Hz (~85 Hz) — 808s, sub-bass body
|
|
222
|
+
low 120-250 Hz (~175 Hz) — bass body, warmth
|
|
223
|
+
low_mid 250-500 Hz (~350 Hz) — mud zone, male vocal lows
|
|
224
|
+
mid 500-1 kHz (~700 Hz) — vocal presence, snare body
|
|
225
|
+
high_mid 1-2 kHz (~1.4 kHz) — consonants, pick attack
|
|
226
|
+
high 2-4 kHz (~2.8 kHz) — presence, vocal intelligibility
|
|
227
|
+
presence 4-8 kHz (~5.6 kHz) — cymbal definition, air of breath
|
|
228
|
+
air 8-20 kHz (~12 kHz) — shimmer, sparkle
|
|
229
|
+
Values 0.0-1.0.
|
|
230
|
+
|
|
231
|
+
Older .amxd builds (pre-v1.16) emit the legacy 8-band layout without the
|
|
232
|
+
explicit `sub_low` split — the server auto-detects band count from the OSC
|
|
233
|
+
payload and picks the right name set. Re-freeze the Max device to get the
|
|
234
|
+
9-band resolution.
|
|
222
235
|
|
|
223
236
|
Also returns detected key/scale if enough audio has been analyzed.
|
|
224
237
|
Requires LivePilot Analyzer on master track.
|
|
@@ -242,7 +255,7 @@ async def get_master_spectrum(
|
|
|
242
255
|
Pass `sub_detail=True` to attach a `sub_detail` dict with three
|
|
243
256
|
finer buckets: `sub_deep` (20-45 Hz), `sub_mid` (45-60 Hz),
|
|
244
257
|
`sub_high` (60-80 Hz). Derived from the FluCoMa mel spectrum
|
|
245
|
-
(40 bands) rather than the
|
|
258
|
+
(40 bands) rather than the 9-band cache, so it requires FluCoMa
|
|
246
259
|
to be active. When FluCoMa is unavailable, sub_detail is omitted
|
|
247
260
|
with a `sub_detail_warning` field explaining why.
|
|
248
261
|
"""
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.17.
|
|
3
|
+
"version": "1.17.2",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 427 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -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.17.
|
|
8
|
+
__version__ = "1.17.2"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/requirements.txt
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
numpy>=1.24.0
|
|
3
3
|
fastmcp>=3.0.0,<3.3.0 # pinned upper bound — _get_all_tools() accesses private internals
|
|
4
4
|
midiutil>=1.2.1
|
|
5
|
-
pretty_midi>=0.2.
|
|
5
|
+
pretty_midi>=0.2.11
|
|
6
6
|
# v1.8 Perception Layer (offline analysis)
|
|
7
|
-
pyloudnorm>=0.
|
|
8
|
-
soundfile>=0.
|
|
9
|
-
scipy>=1.
|
|
7
|
+
pyloudnorm>=0.2.0
|
|
8
|
+
soundfile>=0.13.1
|
|
9
|
+
scipy>=1.17.1
|
|
10
10
|
mutagen>=1.47.0
|
|
11
11
|
# v1.10.5 Splice online catalog integration — required, not optional.
|
|
12
12
|
# Without these, SpliceGRPCClient silently disables itself and search_samples
|
|
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.60.0
|
|
16
|
-
protobuf>=
|
|
16
|
+
protobuf>=7.34.1
|
|
17
17
|
|
|
18
18
|
# Development / testing (not required for runtime)
|
|
19
19
|
# pip install pytest pytest-asyncio
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "427-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.17.
|
|
9
|
+
"version": "1.17.2",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.17.
|
|
14
|
+
"version": "1.17.2",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|