livepilot 1.9.21 → 1.9.23
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/.claude-plugin/marketplace.json +3 -3
- package/.mcpbignore +40 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -14
- package/livepilot/commands/beat.md +68 -21
- package/livepilot/commands/evaluate.md +23 -13
- package/livepilot/commands/mix.md +35 -11
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -17
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +15 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +342 -0
- package/mcp_server/hook_hunter/models.py +57 -0
- package/mcp_server/hook_hunter/tools.py +586 -0
- package/mcp_server/memory/taste_graph.py +261 -0
- package/mcp_server/memory/tools.py +88 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +421 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +221 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +73 -0
- package/mcp_server/preview_studio/tools.py +423 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +204 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +235 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +477 -0
- package/mcp_server/song_brain/models.py +132 -0
- package/mcp_server/song_brain/tools.py +294 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +285 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- package/scripts/generate_tool_catalog.py +131 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Experiment branch data models.
|
|
2
|
+
|
|
3
|
+
An ExperimentBranch represents one trial of a semantic move against the
|
|
4
|
+
current session state. Multiple branches form an experiment set that can
|
|
5
|
+
be compared and ranked.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class BranchSnapshot:
|
|
17
|
+
"""Captured state before or after a branch experiment."""
|
|
18
|
+
spectrum: Optional[dict] = None
|
|
19
|
+
rms: Optional[float] = None
|
|
20
|
+
peak: Optional[float] = None
|
|
21
|
+
track_meters: Optional[list] = None
|
|
22
|
+
timestamp_ms: int = 0
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> dict:
|
|
25
|
+
d = {}
|
|
26
|
+
if self.spectrum is not None:
|
|
27
|
+
d["spectrum"] = self.spectrum
|
|
28
|
+
if self.rms is not None:
|
|
29
|
+
d["rms"] = self.rms
|
|
30
|
+
if self.peak is not None:
|
|
31
|
+
d["peak"] = self.peak
|
|
32
|
+
if self.track_meters is not None:
|
|
33
|
+
d["track_meters"] = self.track_meters
|
|
34
|
+
d["timestamp_ms"] = self.timestamp_ms
|
|
35
|
+
return d
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ExperimentBranch:
|
|
40
|
+
"""One trial branch in an experiment set."""
|
|
41
|
+
branch_id: str
|
|
42
|
+
name: str
|
|
43
|
+
move_id: str
|
|
44
|
+
source_kernel_id: str = ""
|
|
45
|
+
status: str = "pending" # pending | running | evaluated | committed | discarded
|
|
46
|
+
|
|
47
|
+
# Compiled plan for this branch
|
|
48
|
+
compiled_plan: Optional[dict] = None
|
|
49
|
+
|
|
50
|
+
# Captured snapshots
|
|
51
|
+
before_snapshot: Optional[BranchSnapshot] = None
|
|
52
|
+
after_snapshot: Optional[BranchSnapshot] = None
|
|
53
|
+
|
|
54
|
+
# Evaluation results
|
|
55
|
+
evaluation: Optional[dict] = None
|
|
56
|
+
score: float = 0.0
|
|
57
|
+
|
|
58
|
+
# Metadata
|
|
59
|
+
created_at_ms: int = 0
|
|
60
|
+
executed_at_ms: int = 0
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
d = {
|
|
64
|
+
"branch_id": self.branch_id,
|
|
65
|
+
"name": self.name,
|
|
66
|
+
"move_id": self.move_id,
|
|
67
|
+
"status": self.status,
|
|
68
|
+
"score": self.score,
|
|
69
|
+
"created_at_ms": self.created_at_ms,
|
|
70
|
+
}
|
|
71
|
+
if self.compiled_plan:
|
|
72
|
+
d["step_count"] = self.compiled_plan.get("step_count", 0)
|
|
73
|
+
d["summary"] = self.compiled_plan.get("summary", "")
|
|
74
|
+
if self.before_snapshot:
|
|
75
|
+
d["before_snapshot"] = self.before_snapshot.to_dict()
|
|
76
|
+
if self.after_snapshot:
|
|
77
|
+
d["after_snapshot"] = self.after_snapshot.to_dict()
|
|
78
|
+
if self.evaluation:
|
|
79
|
+
d["evaluation"] = self.evaluation
|
|
80
|
+
return d
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class ExperimentSet:
|
|
85
|
+
"""A collection of branches being compared for one request."""
|
|
86
|
+
experiment_id: str
|
|
87
|
+
request_text: str
|
|
88
|
+
branches: list[ExperimentBranch] = field(default_factory=list)
|
|
89
|
+
status: str = "open" # open | evaluated | committed | discarded
|
|
90
|
+
winner_branch_id: Optional[str] = None
|
|
91
|
+
created_at_ms: int = 0
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def branch_count(self) -> int:
|
|
95
|
+
return len(self.branches)
|
|
96
|
+
|
|
97
|
+
def get_branch(self, branch_id: str) -> Optional[ExperimentBranch]:
|
|
98
|
+
for b in self.branches:
|
|
99
|
+
if b.branch_id == branch_id:
|
|
100
|
+
return b
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
def ranked_branches(self) -> list[ExperimentBranch]:
|
|
104
|
+
"""Return branches sorted by score descending."""
|
|
105
|
+
evaluated = [b for b in self.branches if b.status == "evaluated"]
|
|
106
|
+
return sorted(evaluated, key=lambda b: -b.score)
|
|
107
|
+
|
|
108
|
+
def to_dict(self) -> dict:
|
|
109
|
+
return {
|
|
110
|
+
"experiment_id": self.experiment_id,
|
|
111
|
+
"request_text": self.request_text,
|
|
112
|
+
"status": self.status,
|
|
113
|
+
"branch_count": self.branch_count,
|
|
114
|
+
"branches": [b.to_dict() for b in self.branches],
|
|
115
|
+
"winner_branch_id": self.winner_branch_id,
|
|
116
|
+
"ranking": [
|
|
117
|
+
{"branch_id": b.branch_id, "name": b.name, "score": b.score}
|
|
118
|
+
for b in self.ranked_branches()
|
|
119
|
+
],
|
|
120
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Experiment MCP tools — create, run, compare, commit, discard experiments.
|
|
2
|
+
|
|
3
|
+
5 tools for branch-based creative search:
|
|
4
|
+
create_experiment — set up branches from semantic moves
|
|
5
|
+
run_experiment — trial each branch (apply → capture → undo)
|
|
6
|
+
compare_experiments — rank branches by evaluation score
|
|
7
|
+
commit_experiment — re-apply the winner permanently
|
|
8
|
+
discard_experiment — throw away all branches
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from fastmcp import Context
|
|
17
|
+
|
|
18
|
+
from ..server import mcp
|
|
19
|
+
from . import engine
|
|
20
|
+
from .models import BranchSnapshot
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _get_ableton(ctx: Context):
|
|
24
|
+
return ctx.lifespan_context["ableton"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _capture_snapshot(ctx: Context) -> BranchSnapshot:
|
|
28
|
+
"""Capture current session state as a BranchSnapshot."""
|
|
29
|
+
ableton = _get_ableton(ctx)
|
|
30
|
+
spectral = ctx.lifespan_context.get("spectral")
|
|
31
|
+
|
|
32
|
+
snapshot = BranchSnapshot(timestamp_ms=int(time.time() * 1000))
|
|
33
|
+
|
|
34
|
+
# Track meters (always available)
|
|
35
|
+
try:
|
|
36
|
+
meters = ableton.send_command("get_track_meters", {"include_stereo": True})
|
|
37
|
+
snapshot.track_meters = meters.get("tracks", [])
|
|
38
|
+
except Exception:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
# Spectral data (requires M4L analyzer)
|
|
42
|
+
if spectral and spectral.is_connected:
|
|
43
|
+
try:
|
|
44
|
+
spec = spectral.get("spectrum")
|
|
45
|
+
if spec:
|
|
46
|
+
snapshot.spectrum = spec.get("value", {})
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
rms_data = spectral.get("rms")
|
|
52
|
+
if rms_data:
|
|
53
|
+
snapshot.rms = rms_data.get("value")
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
return snapshot
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp.tool()
|
|
61
|
+
def create_experiment(
|
|
62
|
+
ctx: Context,
|
|
63
|
+
request_text: str,
|
|
64
|
+
move_ids: Optional[list] = None,
|
|
65
|
+
limit: int = 3,
|
|
66
|
+
) -> dict:
|
|
67
|
+
"""Create an experiment set to compare multiple approaches.
|
|
68
|
+
|
|
69
|
+
If move_ids is provided, creates one branch per move.
|
|
70
|
+
Otherwise, uses propose_next_best_move to find candidates.
|
|
71
|
+
|
|
72
|
+
request_text: what the user wants (e.g., "make this punchier")
|
|
73
|
+
move_ids: specific moves to try (e.g., ["make_punchier", "tighten_low_end"])
|
|
74
|
+
limit: max branches when auto-proposing (default 3)
|
|
75
|
+
|
|
76
|
+
Returns: experiment set with branch IDs ready for run_experiment.
|
|
77
|
+
"""
|
|
78
|
+
if not move_ids:
|
|
79
|
+
# Auto-propose moves from the registry
|
|
80
|
+
from ..semantic_moves import registry
|
|
81
|
+
from ..semantic_moves.tools import propose_next_best_move
|
|
82
|
+
# Use the propose function's logic directly
|
|
83
|
+
all_moves = list(registry._REGISTRY.values())
|
|
84
|
+
request_lower = request_text.lower()
|
|
85
|
+
scored = []
|
|
86
|
+
for move in all_moves:
|
|
87
|
+
score = 0.0
|
|
88
|
+
move_words = set(move.move_id.replace("_", " ").split())
|
|
89
|
+
intent_words = set(move.intent.lower().split())
|
|
90
|
+
request_words = set(request_lower.split())
|
|
91
|
+
overlap = request_words & (move_words | intent_words)
|
|
92
|
+
score += len(overlap) * 0.3
|
|
93
|
+
for dim in move.targets:
|
|
94
|
+
if dim in request_lower:
|
|
95
|
+
score += 0.2
|
|
96
|
+
if score > 0.1:
|
|
97
|
+
scored.append((move.move_id, score))
|
|
98
|
+
scored.sort(key=lambda x: -x[1])
|
|
99
|
+
move_ids = [m[0] for m, _ in scored[:limit]] if scored else []
|
|
100
|
+
|
|
101
|
+
if not move_ids:
|
|
102
|
+
return {"error": "No matching semantic moves found for this request"}
|
|
103
|
+
|
|
104
|
+
# Build kernel_id from session
|
|
105
|
+
ableton = _get_ableton(ctx)
|
|
106
|
+
session = ableton.send_command("get_session_info")
|
|
107
|
+
kernel_id = f"kern_{int(time.time())}"
|
|
108
|
+
|
|
109
|
+
experiment = engine.create_experiment(
|
|
110
|
+
request_text=request_text,
|
|
111
|
+
move_ids=move_ids,
|
|
112
|
+
kernel_id=kernel_id,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return experiment.to_dict()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@mcp.tool()
|
|
119
|
+
def run_experiment(
|
|
120
|
+
ctx: Context,
|
|
121
|
+
experiment_id: str,
|
|
122
|
+
) -> dict:
|
|
123
|
+
"""Run all pending branches in an experiment.
|
|
124
|
+
|
|
125
|
+
For each branch:
|
|
126
|
+
1. Compile the semantic move against current session
|
|
127
|
+
2. Capture before state
|
|
128
|
+
3. Execute the compiled plan
|
|
129
|
+
4. Capture after state
|
|
130
|
+
5. Undo all changes (revert to checkpoint)
|
|
131
|
+
6. Evaluate the branch
|
|
132
|
+
|
|
133
|
+
Branches run sequentially (Ableton has linear undo).
|
|
134
|
+
"""
|
|
135
|
+
experiment = engine.get_experiment(experiment_id)
|
|
136
|
+
if not experiment:
|
|
137
|
+
return {"error": f"Experiment {experiment_id} not found"}
|
|
138
|
+
|
|
139
|
+
ableton = _get_ableton(ctx)
|
|
140
|
+
|
|
141
|
+
# Import compiler
|
|
142
|
+
from ..semantic_moves import registry, compiler
|
|
143
|
+
|
|
144
|
+
results = []
|
|
145
|
+
for branch in experiment.branches:
|
|
146
|
+
if branch.status != "pending":
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Compile the move
|
|
150
|
+
move = registry.get_move(branch.move_id)
|
|
151
|
+
if not move:
|
|
152
|
+
branch.status = "evaluated"
|
|
153
|
+
branch.score = 0.0
|
|
154
|
+
branch.evaluation = {"error": f"Move {branch.move_id} not found"}
|
|
155
|
+
results.append(branch.to_dict())
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
session_info = ableton.send_command("get_session_info")
|
|
159
|
+
kernel = {"session_info": session_info, "mode": "explore"}
|
|
160
|
+
plan = compiler.compile(move, kernel)
|
|
161
|
+
compiled_dict = plan.to_dict()
|
|
162
|
+
|
|
163
|
+
# Run the branch (apply → capture → undo)
|
|
164
|
+
engine.run_branch(
|
|
165
|
+
branch=branch,
|
|
166
|
+
ableton=ableton,
|
|
167
|
+
compiled_plan=compiled_dict,
|
|
168
|
+
capture_fn=lambda: _capture_snapshot(ctx),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Evaluate
|
|
172
|
+
def eval_fn(before, after):
|
|
173
|
+
# Simple heuristic evaluation when spectral data isn't available
|
|
174
|
+
score = 0.5 # Neutral
|
|
175
|
+
if before.get("track_meters") and after.get("track_meters"):
|
|
176
|
+
# Check all tracks still alive
|
|
177
|
+
before_alive = sum(1 for t in before["track_meters"] if t.get("level", 0) > 0)
|
|
178
|
+
after_alive = sum(1 for t in after["track_meters"] if t.get("level", 0) > 0)
|
|
179
|
+
if after_alive >= before_alive:
|
|
180
|
+
score += 0.1
|
|
181
|
+
else:
|
|
182
|
+
score -= 0.2 # Lost a track
|
|
183
|
+
|
|
184
|
+
if before.get("spectrum") and after.get("spectrum"):
|
|
185
|
+
# Spectral balance improvement heuristic
|
|
186
|
+
score += 0.1 # Bonus for having spectral data
|
|
187
|
+
|
|
188
|
+
return {"score": round(score, 3), "keep_change": score > 0.45}
|
|
189
|
+
|
|
190
|
+
engine.evaluate_branch(branch, eval_fn)
|
|
191
|
+
results.append(branch.to_dict())
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
"experiment_id": experiment_id,
|
|
195
|
+
"branches_run": len(results),
|
|
196
|
+
"results": results,
|
|
197
|
+
"ranking": [
|
|
198
|
+
{"branch_id": b.branch_id, "name": b.name, "score": b.score, "move_id": b.move_id}
|
|
199
|
+
for b in experiment.ranked_branches()
|
|
200
|
+
],
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@mcp.tool()
|
|
205
|
+
def compare_experiments(
|
|
206
|
+
ctx: Context,
|
|
207
|
+
experiment_id: str,
|
|
208
|
+
) -> dict:
|
|
209
|
+
"""Compare and rank all evaluated branches in an experiment.
|
|
210
|
+
|
|
211
|
+
Returns branches sorted by score with their evaluations and summaries.
|
|
212
|
+
"""
|
|
213
|
+
experiment = engine.get_experiment(experiment_id)
|
|
214
|
+
if not experiment:
|
|
215
|
+
return {"error": f"Experiment {experiment_id} not found"}
|
|
216
|
+
|
|
217
|
+
ranked = experiment.ranked_branches()
|
|
218
|
+
return {
|
|
219
|
+
"experiment_id": experiment_id,
|
|
220
|
+
"request": experiment.request_text,
|
|
221
|
+
"branch_count": experiment.branch_count,
|
|
222
|
+
"ranking": [
|
|
223
|
+
{
|
|
224
|
+
"rank": i + 1,
|
|
225
|
+
"branch_id": b.branch_id,
|
|
226
|
+
"name": b.name,
|
|
227
|
+
"move_id": b.move_id,
|
|
228
|
+
"score": b.score,
|
|
229
|
+
"summary": b.compiled_plan.get("summary", "") if b.compiled_plan else "",
|
|
230
|
+
"evaluation": b.evaluation,
|
|
231
|
+
}
|
|
232
|
+
for i, b in enumerate(ranked)
|
|
233
|
+
],
|
|
234
|
+
"winner": ranked[0].to_dict() if ranked else None,
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@mcp.tool()
|
|
239
|
+
def commit_experiment(
|
|
240
|
+
ctx: Context,
|
|
241
|
+
experiment_id: str,
|
|
242
|
+
branch_id: str,
|
|
243
|
+
) -> dict:
|
|
244
|
+
"""Commit the winning branch — re-apply its moves permanently.
|
|
245
|
+
|
|
246
|
+
This executes the branch's compiled plan again, this time without undoing.
|
|
247
|
+
The experiment is marked as committed.
|
|
248
|
+
"""
|
|
249
|
+
experiment = engine.get_experiment(experiment_id)
|
|
250
|
+
if not experiment:
|
|
251
|
+
return {"error": f"Experiment {experiment_id} not found"}
|
|
252
|
+
|
|
253
|
+
ableton = _get_ableton(ctx)
|
|
254
|
+
return engine.commit_branch(experiment, branch_id, ableton)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@mcp.tool()
|
|
258
|
+
def discard_experiment(
|
|
259
|
+
ctx: Context,
|
|
260
|
+
experiment_id: str,
|
|
261
|
+
) -> dict:
|
|
262
|
+
"""Discard an entire experiment — no changes are kept."""
|
|
263
|
+
return engine.discard_experiment(experiment_id)
|