livepilot 1.10.9 → 1.13.0
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 +327 -0
- package/README.md +7 -7
- 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/branches/__init__.py +32 -0
- package/mcp_server/branches/types.py +230 -0
- package/mcp_server/composer/__init__.py +10 -1
- package/mcp_server/composer/branch_producer.py +229 -0
- package/mcp_server/evaluation/policy.py +129 -2
- package/mcp_server/experiment/engine.py +47 -11
- package/mcp_server/experiment/models.py +72 -7
- package/mcp_server/experiment/tools.py +231 -35
- package/mcp_server/m4l_bridge.py +488 -13
- package/mcp_server/memory/taste_graph.py +84 -11
- package/mcp_server/persistence/taste_store.py +21 -5
- package/mcp_server/runtime/execution_router.py +7 -0
- package/mcp_server/runtime/mcp_dispatch.py +32 -0
- package/mcp_server/runtime/remote_commands.py +54 -0
- package/mcp_server/runtime/session_kernel.py +46 -0
- package/mcp_server/runtime/tools.py +29 -3
- package/mcp_server/sample_engine/slice_classifier.py +169 -0
- package/mcp_server/server.py +11 -3
- package/mcp_server/synthesis_brain/__init__.py +53 -0
- package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
- package/mcp_server/synthesis_brain/adapters/analog.py +167 -0
- package/mcp_server/synthesis_brain/adapters/base.py +86 -0
- package/mcp_server/synthesis_brain/adapters/drift.py +166 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +151 -0
- package/mcp_server/synthesis_brain/adapters/operator.py +169 -0
- package/mcp_server/synthesis_brain/adapters/wavetable.py +228 -0
- package/mcp_server/synthesis_brain/engine.py +91 -0
- package/mcp_server/synthesis_brain/models.py +121 -0
- package/mcp_server/synthesis_brain/timbre.py +194 -0
- package/mcp_server/tools/_conductor.py +144 -0
- package/mcp_server/tools/analyzer.py +187 -7
- package/mcp_server/tools/clips.py +65 -0
- package/mcp_server/tools/devices.py +517 -5
- package/mcp_server/tools/diagnostics.py +42 -0
- package/mcp_server/tools/follow_actions.py +202 -0
- package/mcp_server/tools/grooves.py +142 -0
- package/mcp_server/tools/miditool.py +280 -0
- package/mcp_server/tools/scales.py +126 -0
- package/mcp_server/tools/take_lanes.py +135 -0
- package/mcp_server/tools/tracks.py +46 -3
- package/mcp_server/tools/transport.py +62 -1
- package/mcp_server/wonder_mode/engine.py +324 -0
- package/mcp_server/wonder_mode/tools.py +153 -1
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +8 -4
- package/remote_script/LivePilot/clips.py +62 -0
- package/remote_script/LivePilot/devices.py +444 -0
- package/remote_script/LivePilot/diagnostics.py +52 -1
- package/remote_script/LivePilot/follow_actions.py +235 -0
- package/remote_script/LivePilot/grooves.py +185 -0
- package/remote_script/LivePilot/scales.py +138 -0
- package/remote_script/LivePilot/take_lanes.py +175 -0
- package/remote_script/LivePilot/tracks.py +59 -1
- package/remote_script/LivePilot/transport.py +90 -1
- package/remote_script/LivePilot/version_detect.py +9 -0
- package/server.json +3 -3
|
@@ -16,6 +16,7 @@ from typing import Optional
|
|
|
16
16
|
from fastmcp import Context
|
|
17
17
|
|
|
18
18
|
from ..server import mcp
|
|
19
|
+
from ..branches import BranchSeed
|
|
19
20
|
from . import engine
|
|
20
21
|
from .models import BranchSnapshot
|
|
21
22
|
import logging
|
|
@@ -63,18 +64,71 @@ def create_experiment(
|
|
|
63
64
|
request_text: str,
|
|
64
65
|
move_ids: Optional[list] = None,
|
|
65
66
|
limit: int = 3,
|
|
67
|
+
seeds: Optional[list] = None,
|
|
68
|
+
compiled_plans: Optional[list] = None,
|
|
66
69
|
) -> dict:
|
|
67
70
|
"""Create an experiment set to compare multiple approaches.
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
Otherwise, uses propose_next_best_move to find candidates.
|
|
72
|
+
Three input modes (in priority order):
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
74
|
+
1. seeds (PR3+): a list of BranchSeed dicts. Each seed becomes one branch.
|
|
75
|
+
compiled_plans (optional parallel list) attaches pre-compiled plans
|
|
76
|
+
for freeform / synthesis / composer producers. Seed dict shape:
|
|
77
|
+
{seed_id, source, move_id, hypothesis, protected_qualities,
|
|
78
|
+
affected_scope, distinctness_reason, risk_label, novelty_label,
|
|
79
|
+
analytical_only}
|
|
80
|
+
Missing fields default per BranchSeed. This is the canonical path
|
|
81
|
+
for producers that have already done their own selection work.
|
|
82
|
+
|
|
83
|
+
2. move_ids: legacy path — one semantic_move seed per move_id.
|
|
84
|
+
Unchanged behavior; internally delegates to the seeds path.
|
|
85
|
+
|
|
86
|
+
3. Auto-proposal: neither seeds nor move_ids provided. Scans the semantic
|
|
87
|
+
move registry by keyword overlap with request_text and takes the top
|
|
88
|
+
``limit`` moves (default 3).
|
|
75
89
|
|
|
76
90
|
Returns: experiment set with branch IDs ready for run_experiment.
|
|
77
91
|
"""
|
|
92
|
+
# ── Mode 1: seeds provided ──────────────────────────────────────────
|
|
93
|
+
if seeds:
|
|
94
|
+
rehydrated: list[BranchSeed] = []
|
|
95
|
+
for i, s in enumerate(seeds):
|
|
96
|
+
if isinstance(s, BranchSeed):
|
|
97
|
+
rehydrated.append(s)
|
|
98
|
+
elif isinstance(s, dict):
|
|
99
|
+
try:
|
|
100
|
+
rehydrated.append(BranchSeed(**s))
|
|
101
|
+
except TypeError as exc:
|
|
102
|
+
return {"error": f"seeds[{i}] invalid: {exc}"}
|
|
103
|
+
else:
|
|
104
|
+
return {
|
|
105
|
+
"error": (
|
|
106
|
+
f"seeds[{i}] must be dict or BranchSeed, "
|
|
107
|
+
f"got {type(s).__name__}"
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if compiled_plans is not None and len(compiled_plans) != len(rehydrated):
|
|
112
|
+
return {
|
|
113
|
+
"error": (
|
|
114
|
+
f"compiled_plans length ({len(compiled_plans)}) must match "
|
|
115
|
+
f"seeds length ({len(rehydrated)})"
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
ableton = _get_ableton(ctx)
|
|
120
|
+
ableton.send_command("get_session_info")
|
|
121
|
+
kernel_id = f"kern_{int(time.time())}"
|
|
122
|
+
|
|
123
|
+
experiment = engine.create_experiment_from_seeds(
|
|
124
|
+
request_text=request_text,
|
|
125
|
+
seeds=rehydrated,
|
|
126
|
+
kernel_id=kernel_id,
|
|
127
|
+
compiled_plans=compiled_plans,
|
|
128
|
+
)
|
|
129
|
+
return experiment.to_dict()
|
|
130
|
+
|
|
131
|
+
# ── Mode 2/3: legacy move_ids path ──────────────────────────────────
|
|
78
132
|
if not move_ids:
|
|
79
133
|
# Auto-propose moves from the registry
|
|
80
134
|
from ..semantic_moves import registry
|
|
@@ -119,19 +173,30 @@ def create_experiment(
|
|
|
119
173
|
async def run_experiment(
|
|
120
174
|
ctx: Context,
|
|
121
175
|
experiment_id: str,
|
|
176
|
+
exploration_rules: bool = False,
|
|
122
177
|
) -> dict:
|
|
123
178
|
"""Run all pending branches in an experiment.
|
|
124
179
|
|
|
125
180
|
For each branch:
|
|
126
181
|
1. Compile the semantic move against current session
|
|
182
|
+
(skipped when branch.compiled_plan is already set — PR3+)
|
|
127
183
|
2. Capture before state
|
|
128
|
-
3. Execute the compiled plan (through the async router
|
|
184
|
+
3. Execute the compiled plan (through the async router)
|
|
129
185
|
4. Capture after state
|
|
130
186
|
5. Undo all successful steps (revert to checkpoint)
|
|
131
|
-
6. Evaluate the branch
|
|
187
|
+
6. Evaluate the branch and classify its outcome via evaluation.policy
|
|
132
188
|
7. Record per-step results on branch.execution_log
|
|
133
189
|
|
|
134
190
|
Branches run sequentially (Ableton has linear undo).
|
|
191
|
+
|
|
192
|
+
exploration_rules (PR7): when True, branches that fail technical gates
|
|
193
|
+
(score < 0.40, non-positive measurable delta) are classified as
|
|
194
|
+
"interesting_but_failed" instead of "failed" — they stay in the
|
|
195
|
+
experiment for audit but don't appear in the ranking. Protection
|
|
196
|
+
violations STILL force undo regardless of this flag — that's a safety
|
|
197
|
+
invariant, not a taste judgment.
|
|
198
|
+
|
|
199
|
+
Default False preserves pre-PR7 behavior exactly.
|
|
135
200
|
"""
|
|
136
201
|
experiment = engine.get_experiment(experiment_id)
|
|
137
202
|
if not experiment:
|
|
@@ -149,19 +214,54 @@ async def run_experiment(
|
|
|
149
214
|
if branch.status != "pending":
|
|
150
215
|
continue
|
|
151
216
|
|
|
152
|
-
#
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
217
|
+
# PR3: respect a pre-existing compiled_plan on the branch (freeform /
|
|
218
|
+
# synthesis / composer producers bring their own). Only compile from
|
|
219
|
+
# move_id when the branch arrived without a plan — which requires a
|
|
220
|
+
# semantic_move seed (or a legacy move-only branch).
|
|
221
|
+
compiled_dict = branch.compiled_plan
|
|
222
|
+
|
|
223
|
+
if compiled_dict is None:
|
|
224
|
+
# Analytical-only branches short-circuit — no plan to run.
|
|
225
|
+
# Marked with status="analytical" so ranked_branches()
|
|
226
|
+
# (which only surfaces "evaluated") excludes them, and
|
|
227
|
+
# commit_experiment refuses to re-apply them.
|
|
228
|
+
if branch.seed is not None and branch.seed.analytical_only:
|
|
229
|
+
branch.status = "analytical"
|
|
230
|
+
branch.score = 0.0
|
|
231
|
+
branch.evaluation = {
|
|
232
|
+
"score": 0.0,
|
|
233
|
+
"keep_change": False,
|
|
234
|
+
"status": "analytical",
|
|
235
|
+
"note": "analytical_only branch — no execution path",
|
|
236
|
+
}
|
|
237
|
+
results.append(branch.to_dict())
|
|
238
|
+
continue
|
|
239
|
+
|
|
240
|
+
if not branch.move_id:
|
|
241
|
+
branch.status = "failed"
|
|
242
|
+
branch.score = 0.0
|
|
243
|
+
branch.evaluation = {
|
|
244
|
+
"error": (
|
|
245
|
+
"Branch has no compiled_plan and no move_id — "
|
|
246
|
+
"freeform producers must pre-populate compiled_plan"
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
results.append(branch.to_dict())
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Compile from semantic move
|
|
253
|
+
move = registry.get_move(branch.move_id)
|
|
254
|
+
if not move:
|
|
255
|
+
branch.status = "failed"
|
|
256
|
+
branch.score = 0.0
|
|
257
|
+
branch.evaluation = {"error": f"Move {branch.move_id} not found"}
|
|
258
|
+
results.append(branch.to_dict())
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
session_info = ableton.send_command("get_session_info")
|
|
262
|
+
kernel = {"session_info": session_info, "mode": "explore"}
|
|
263
|
+
plan = compiler.compile(move, kernel)
|
|
264
|
+
compiled_dict = plan.to_dict()
|
|
165
265
|
|
|
166
266
|
# Run the branch through the async router
|
|
167
267
|
await engine.run_branch_async(
|
|
@@ -174,26 +274,78 @@ async def run_experiment(
|
|
|
174
274
|
ctx=ctx,
|
|
175
275
|
)
|
|
176
276
|
|
|
177
|
-
# Evaluate
|
|
277
|
+
# Evaluate — score via the inline heuristic, then classify via
|
|
278
|
+
# evaluation.policy for a unified keep/undo/interesting_but_failed
|
|
279
|
+
# decision (PR7).
|
|
280
|
+
from ..evaluation.policy import classify_branch_outcome
|
|
281
|
+
|
|
178
282
|
def eval_fn(before, after):
|
|
179
|
-
# Simple heuristic evaluation when spectral data isn't available
|
|
283
|
+
# Simple heuristic evaluation when spectral data isn't available.
|
|
284
|
+
# protection_violated is rough — derived from whether any track
|
|
285
|
+
# went silent (signal lost on a track = protection violation).
|
|
180
286
|
score = 0.5 # Neutral
|
|
287
|
+
protection_violated = False
|
|
288
|
+
lost_tracks = 0
|
|
289
|
+
|
|
181
290
|
if before.get("track_meters") and after.get("track_meters"):
|
|
182
|
-
# Check all tracks still alive
|
|
183
291
|
before_alive = sum(1 for t in before["track_meters"] if t.get("level", 0) > 0)
|
|
184
292
|
after_alive = sum(1 for t in after["track_meters"] if t.get("level", 0) > 0)
|
|
185
|
-
|
|
293
|
+
lost_tracks = max(0, before_alive - after_alive)
|
|
294
|
+
if lost_tracks == 0:
|
|
186
295
|
score += 0.1
|
|
187
296
|
else:
|
|
188
|
-
score -= 0.2
|
|
297
|
+
score -= 0.2
|
|
298
|
+
# A track going silent is a protection violation — always
|
|
299
|
+
# undo regardless of exploration mode.
|
|
300
|
+
protection_violated = True
|
|
189
301
|
|
|
190
302
|
if before.get("spectrum") and after.get("spectrum"):
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
303
|
+
score += 0.1 # presence-of-data bonus
|
|
304
|
+
|
|
305
|
+
score = round(score, 3)
|
|
306
|
+
outcome = classify_branch_outcome(
|
|
307
|
+
score=score,
|
|
308
|
+
protection_violated=protection_violated,
|
|
309
|
+
# Minimal hard-rule inputs — the heuristic doesn't compute
|
|
310
|
+
# measurable_count / goal_progress deltas. target_count=0 and
|
|
311
|
+
# measurable_count=0 lets rule 1 defer to score-only judgment.
|
|
312
|
+
measurable_count=0,
|
|
313
|
+
target_count=0,
|
|
314
|
+
goal_progress=0.0,
|
|
315
|
+
exploration_rules=exploration_rules,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
"score": outcome.score,
|
|
320
|
+
"keep_change": outcome.keep_change,
|
|
321
|
+
"status": outcome.status,
|
|
322
|
+
"failure_reasons": outcome.failure_reasons,
|
|
323
|
+
"note": outcome.note,
|
|
324
|
+
"lost_tracks": lost_tracks,
|
|
325
|
+
}
|
|
195
326
|
|
|
196
327
|
engine.evaluate_branch(branch, eval_fn)
|
|
328
|
+
|
|
329
|
+
# Promote the classified status onto the branch. ranked_branches()
|
|
330
|
+
# only surfaces status="evaluated", so branches the classifier
|
|
331
|
+
# rejected ("undo") or retained for audit ("interesting_but_failed")
|
|
332
|
+
# are both correctly excluded from winner recommendations.
|
|
333
|
+
# Without this mapping, a branch the hard-rule classifier explicitly
|
|
334
|
+
# rejected could still win a ranking and be re-applied by commit.
|
|
335
|
+
if branch.evaluation and branch.evaluation.get("status"):
|
|
336
|
+
status = branch.evaluation["status"]
|
|
337
|
+
if status == "keep":
|
|
338
|
+
branch.status = "evaluated"
|
|
339
|
+
elif status == "interesting_but_failed":
|
|
340
|
+
branch.status = "interesting_but_failed"
|
|
341
|
+
elif status == "undo":
|
|
342
|
+
# Undo-classified branches had their steps rolled back by
|
|
343
|
+
# run_branch_async's undo pass; they must NOT be eligible
|
|
344
|
+
# winners. "rejected" is a terminal branch status distinct
|
|
345
|
+
# from "failed" (execution failed) and distinct from
|
|
346
|
+
# "interesting_but_failed" (exploration-mode retention).
|
|
347
|
+
branch.status = "rejected"
|
|
348
|
+
|
|
197
349
|
results.append(branch.to_dict())
|
|
198
350
|
|
|
199
351
|
return {
|
|
@@ -221,6 +373,30 @@ def compare_experiments(
|
|
|
221
373
|
return {"error": f"Experiment {experiment_id} not found"}
|
|
222
374
|
|
|
223
375
|
ranked = experiment.ranked_branches()
|
|
376
|
+
|
|
377
|
+
# Surface non-winning branch categories separately. None of these are
|
|
378
|
+
# candidates for commit — ranked_branches() filters them out — but the
|
|
379
|
+
# user sees what was tried.
|
|
380
|
+
interesting_failed = [
|
|
381
|
+
b for b in experiment.branches if b.status == "interesting_but_failed"
|
|
382
|
+
]
|
|
383
|
+
rejected = [
|
|
384
|
+
b for b in experiment.branches if b.status == "rejected"
|
|
385
|
+
]
|
|
386
|
+
analytical = [
|
|
387
|
+
b for b in experiment.branches if b.status == "analytical"
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
def _audit_row(b):
|
|
391
|
+
return {
|
|
392
|
+
"branch_id": b.branch_id,
|
|
393
|
+
"name": b.name,
|
|
394
|
+
"move_id": b.move_id,
|
|
395
|
+
"score": b.score,
|
|
396
|
+
"summary": b.compiled_plan.get("summary", "") if b.compiled_plan else "",
|
|
397
|
+
"evaluation": b.evaluation,
|
|
398
|
+
}
|
|
399
|
+
|
|
224
400
|
return {
|
|
225
401
|
"experiment_id": experiment_id,
|
|
226
402
|
"request": experiment.request_text,
|
|
@@ -228,16 +404,14 @@ def compare_experiments(
|
|
|
228
404
|
"ranking": [
|
|
229
405
|
{
|
|
230
406
|
"rank": i + 1,
|
|
231
|
-
|
|
232
|
-
"name": b.name,
|
|
233
|
-
"move_id": b.move_id,
|
|
234
|
-
"score": b.score,
|
|
235
|
-
"summary": b.compiled_plan.get("summary", "") if b.compiled_plan else "",
|
|
236
|
-
"evaluation": b.evaluation,
|
|
407
|
+
**_audit_row(b),
|
|
237
408
|
}
|
|
238
409
|
for i, b in enumerate(ranked)
|
|
239
410
|
],
|
|
240
411
|
"winner": ranked[0].to_dict() if ranked else None,
|
|
412
|
+
"interesting_but_failed": [_audit_row(b) for b in interesting_failed],
|
|
413
|
+
"rejected": [_audit_row(b) for b in rejected],
|
|
414
|
+
"analytical": [_audit_row(b) for b in analytical],
|
|
241
415
|
}
|
|
242
416
|
|
|
243
417
|
|
|
@@ -258,6 +432,28 @@ async def commit_experiment(
|
|
|
258
432
|
if not experiment:
|
|
259
433
|
return {"error": f"Experiment {experiment_id} not found"}
|
|
260
434
|
|
|
435
|
+
# Refuse to commit branches the classifier rejected or that were
|
|
436
|
+
# analytical-only. Those statuses exist specifically so callers
|
|
437
|
+
# can't route them into re-application, and ranked_branches()
|
|
438
|
+
# already excludes them — so reaching commit with such a branch
|
|
439
|
+
# means the caller is bypassing the ranking layer.
|
|
440
|
+
target = experiment.get_branch(branch_id)
|
|
441
|
+
if target is None:
|
|
442
|
+
return {"error": f"Branch {branch_id} not found"}
|
|
443
|
+
if target.status in ("rejected", "analytical", "failed"):
|
|
444
|
+
return {
|
|
445
|
+
"error": (
|
|
446
|
+
f"Cannot commit branch with status '{target.status}'. "
|
|
447
|
+
f"'rejected' = hard-rule classifier rolled back; "
|
|
448
|
+
f"'analytical' = no executable plan; "
|
|
449
|
+
f"'failed' = zero steps applied successfully. "
|
|
450
|
+
f"Use compare_experiments to see eligible winners "
|
|
451
|
+
f"(only status='evaluated' branches are ranking candidates)."
|
|
452
|
+
),
|
|
453
|
+
"branch_id": branch_id,
|
|
454
|
+
"branch_status": target.status,
|
|
455
|
+
}
|
|
456
|
+
|
|
261
457
|
ableton = _get_ableton(ctx)
|
|
262
458
|
bridge = ctx.lifespan_context.get("m4l")
|
|
263
459
|
mcp_registry = ctx.lifespan_context.get("mcp_dispatch", {})
|