livepilot 1.9.23 → 1.9.24

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.
Files changed (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +2 -2
  3. package/CHANGELOG.md +46 -0
  4. package/README.md +94 -0
  5. package/livepilot/.Codex-plugin/plugin.json +1 -1
  6. package/livepilot/.claude-plugin/plugin.json +1 -1
  7. package/livepilot/skills/livepilot-core/references/overview.md +1 -1
  8. package/livepilot/skills/livepilot-release/SKILL.md +14 -0
  9. package/livepilot.mcpb +0 -0
  10. package/manifest.json +1 -1
  11. package/mcp_server/__init__.py +1 -1
  12. package/mcp_server/hook_hunter/analyzer.py +23 -0
  13. package/mcp_server/hook_hunter/models.py +1 -0
  14. package/mcp_server/hook_hunter/tools.py +4 -2
  15. package/mcp_server/memory/taste_graph.py +68 -1
  16. package/mcp_server/memory/tools.py +15 -4
  17. package/mcp_server/musical_intelligence/detectors.py +14 -1
  18. package/mcp_server/musical_intelligence/tools.py +11 -8
  19. package/mcp_server/persistence/__init__.py +1 -0
  20. package/mcp_server/persistence/base_store.py +82 -0
  21. package/mcp_server/persistence/project_store.py +106 -0
  22. package/mcp_server/persistence/taste_store.py +122 -0
  23. package/mcp_server/preview_studio/models.py +1 -0
  24. package/mcp_server/preview_studio/tools.py +56 -13
  25. package/mcp_server/runtime/capability.py +66 -0
  26. package/mcp_server/runtime/capability_probe.py +118 -0
  27. package/mcp_server/runtime/execution_router.py +139 -0
  28. package/mcp_server/runtime/remote_commands.py +82 -0
  29. package/mcp_server/semantic_moves/mix_moves.py +41 -41
  30. package/mcp_server/semantic_moves/performance_moves.py +13 -13
  31. package/mcp_server/semantic_moves/sound_design_moves.py +15 -15
  32. package/mcp_server/semantic_moves/tools.py +18 -17
  33. package/mcp_server/semantic_moves/transition_moves.py +16 -16
  34. package/mcp_server/services/__init__.py +1 -0
  35. package/mcp_server/services/motif_service.py +67 -0
  36. package/mcp_server/session_continuity/tracker.py +29 -1
  37. package/mcp_server/song_brain/builder.py +28 -1
  38. package/mcp_server/song_brain/models.py +4 -0
  39. package/mcp_server/song_brain/tools.py +20 -2
  40. package/mcp_server/wonder_mode/tools.py +6 -1
  41. package/package.json +1 -1
  42. package/remote_script/LivePilot/__init__.py +1 -1
  43. package/scripts/sync_metadata.py +132 -0
@@ -0,0 +1,139 @@
1
+ """Unified execution router for compiled plan steps.
2
+
3
+ Classifies each step by backend (remote_command, mcp_tool, bridge_command)
4
+ and dispatches to the correct execution path. Replaces the pattern of
5
+ sending everything through ableton.send_command() blindly.
6
+
7
+ Step backends:
8
+ remote_command — valid Remote Script handler, goes through TCP
9
+ bridge_command — M4L bridge handler, goes through TCP (requires bridge)
10
+ mcp_tool — MCP-layer Python function, called directly
11
+ unknown — not a valid target anywhere
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import Any, Optional
18
+
19
+ from .remote_commands import BRIDGE_COMMANDS, REMOTE_COMMANDS
20
+
21
+
22
+ # MCP-only tools that exist as Python functions but NOT as TCP handlers.
23
+ # These must be called through direct import, not ableton.send_command().
24
+ MCP_TOOLS: frozenset[str] = frozenset({
25
+ "apply_automation_shape",
26
+ "apply_gesture_template",
27
+ "analyze_mix",
28
+ "get_master_spectrum",
29
+ "get_emotional_arc",
30
+ "capture_audio",
31
+ "get_motif_graph",
32
+ })
33
+
34
+
35
+ @dataclass
36
+ class ExecutionResult:
37
+ """Result of executing a single plan step."""
38
+
39
+ ok: bool = False
40
+ backend: str = ""
41
+ tool: str = ""
42
+ result: Any = None
43
+ error: str = ""
44
+
45
+ def to_dict(self) -> dict:
46
+ d = {"ok": self.ok, "backend": self.backend, "tool": self.tool}
47
+ if self.ok:
48
+ d["result"] = self.result
49
+ else:
50
+ d["error"] = self.error
51
+ return d
52
+
53
+
54
+ def classify_step(tool: str) -> str:
55
+ """Classify a step's execution backend."""
56
+ if tool in REMOTE_COMMANDS:
57
+ return "remote_command"
58
+ if tool in BRIDGE_COMMANDS:
59
+ return "bridge_command"
60
+ if tool in MCP_TOOLS:
61
+ return "mcp_tool"
62
+ return "unknown"
63
+
64
+
65
+ def execute_step(
66
+ tool: str,
67
+ params: dict,
68
+ ableton: Any = None,
69
+ ctx: Any = None,
70
+ declared_backend: str | None = None,
71
+ ) -> ExecutionResult:
72
+ """Execute a single plan step through the correct backend."""
73
+ backend = declared_backend if declared_backend in ("remote_command", "bridge_command", "mcp_tool") else classify_step(tool)
74
+
75
+ if backend in ("remote_command", "bridge_command"):
76
+ if ableton is None:
77
+ return ExecutionResult(
78
+ ok=False, backend=backend, tool=tool,
79
+ error="Ableton connection unavailable",
80
+ )
81
+ try:
82
+ result = ableton.send_command(tool, params)
83
+ return ExecutionResult(ok=True, backend=backend, tool=tool, result=result)
84
+ except Exception as e:
85
+ return ExecutionResult(ok=False, backend=backend, tool=tool, error=str(e))
86
+
87
+ elif backend == "mcp_tool":
88
+ # MCP tools require direct Python dispatch.
89
+ # For now, return a clear error — full MCP dispatch is wired per-tool
90
+ # in the callers (apply_semantic_move, render_preview_variant).
91
+ return ExecutionResult(
92
+ ok=False, backend=backend, tool=tool,
93
+ error=f"MCP tool '{tool}' requires direct Python dispatch — "
94
+ f"not executable through TCP. Use the MCP layer directly.",
95
+ )
96
+
97
+ else:
98
+ return ExecutionResult(
99
+ ok=False, backend="unknown", tool=tool,
100
+ error=f"Unknown tool '{tool}' — not a Remote Script command, "
101
+ f"bridge command, or registered MCP tool",
102
+ )
103
+
104
+
105
+ def execute_plan_steps(
106
+ steps: list[dict],
107
+ ableton: Any = None,
108
+ ctx: Any = None,
109
+ stop_on_failure: bool = True,
110
+ ) -> list[ExecutionResult]:
111
+ """Execute a list of plan steps, returning results for each.
112
+
113
+ Stops on first failure by default. Set stop_on_failure=False
114
+ to continue past errors (useful for best-effort execution).
115
+ """
116
+ results: list[ExecutionResult] = []
117
+
118
+ for step in steps:
119
+ tool = step.get("tool") or step.get("command", "")
120
+ params = step.get("params") or step.get("args", {})
121
+ # Honor declared backend from step annotations (PR5) if present
122
+ declared_backend = step.get("backend")
123
+
124
+ if not tool:
125
+ results.append(ExecutionResult(
126
+ ok=False, backend="unknown", tool="",
127
+ error="Step has no tool/command field",
128
+ ))
129
+ if stop_on_failure:
130
+ break
131
+ continue
132
+
133
+ result = execute_step(tool, params, ableton=ableton, ctx=ctx, declared_backend=declared_backend)
134
+ results.append(result)
135
+
136
+ if not result.ok and stop_on_failure:
137
+ break
138
+
139
+ return results
@@ -0,0 +1,82 @@
1
+ """Canonical set of all valid Remote Script commands.
2
+
3
+ Every command here has a @register handler in remote_script/LivePilot/.
4
+ This is the source of truth for what can be called via
5
+ ableton.send_command(). If a command is not in this set, sending it
6
+ through TCP will return NOT_FOUND from the Remote Script.
7
+
8
+ Maintained manually — the Remote Script uses Ableton's Python and
9
+ cannot be imported in CI. Update this when adding new handlers.
10
+ """
11
+
12
+ REMOTE_COMMANDS: frozenset[str] = frozenset({
13
+ # transport (10)
14
+ "get_session_info", "set_tempo", "set_time_signature",
15
+ "start_playback", "stop_playback", "continue_playback",
16
+ "toggle_metronome", "set_session_loop", "undo", "redo",
17
+ # tracks (17)
18
+ "get_track_info", "create_midi_track", "create_audio_track",
19
+ "create_return_track", "delete_track", "duplicate_track",
20
+ "set_track_name", "set_track_color", "set_track_mute",
21
+ "set_track_solo", "set_track_arm", "stop_track_clips",
22
+ "set_group_fold", "set_track_input_monitoring",
23
+ "get_freeze_status", "freeze_track", "flatten_track",
24
+ # clips (11)
25
+ "get_clip_info", "create_clip", "delete_clip", "duplicate_clip",
26
+ "fire_clip", "stop_clip", "set_clip_name", "set_clip_color",
27
+ "set_clip_loop", "set_clip_launch", "set_clip_warp_mode",
28
+ # notes (8)
29
+ "add_notes", "get_notes", "remove_notes", "remove_notes_by_id",
30
+ "modify_notes", "duplicate_notes", "transpose_notes", "quantize_clip",
31
+ # mixing (11)
32
+ "set_track_volume", "set_track_pan", "set_track_send",
33
+ "get_return_tracks", "get_master_track", "set_master_volume",
34
+ "get_track_routing", "get_track_meters", "get_master_meters",
35
+ "get_mix_snapshot", "set_track_routing",
36
+ # scenes (12)
37
+ "get_scenes_info", "create_scene", "delete_scene", "duplicate_scene",
38
+ "fire_scene", "set_scene_name", "set_scene_color", "set_scene_tempo",
39
+ "get_scene_matrix", "fire_scene_clips", "stop_all_clips",
40
+ "get_playing_clips",
41
+ # devices (12)
42
+ "get_device_info", "get_device_parameters", "set_device_parameter",
43
+ "batch_set_parameters", "toggle_device", "delete_device",
44
+ "move_device", "load_device_by_uri", "find_and_load_device",
45
+ "set_simpler_playback_mode", "get_rack_chains", "set_chain_volume",
46
+ # clip_automation (3)
47
+ "get_clip_automation", "set_clip_automation", "clear_clip_automation",
48
+ # browser (5)
49
+ "get_browser_tree", "get_browser_items", "search_browser",
50
+ "load_browser_item", "get_device_presets",
51
+ # arrangement (19)
52
+ "get_arrangement_clips", "create_arrangement_clip",
53
+ "add_arrangement_notes", "get_arrangement_notes",
54
+ "remove_arrangement_notes", "remove_arrangement_notes_by_id",
55
+ "modify_arrangement_notes", "duplicate_arrangement_notes",
56
+ "set_arrangement_automation", "transpose_arrangement_notes",
57
+ "set_arrangement_clip_name", "jump_to_time",
58
+ "capture_midi", "start_recording", "stop_recording",
59
+ "get_cue_points", "jump_to_cue", "toggle_cue_point",
60
+ "back_to_arranger",
61
+ # diagnostics (1)
62
+ "get_session_diagnostics",
63
+ # ping (built-in)
64
+ "ping",
65
+ })
66
+
67
+ # M4L bridge commands — routed through TCP but handled by livepilot_bridge.js
68
+ # These require the M4L Analyzer device on the master track.
69
+ BRIDGE_COMMANDS: frozenset[str] = frozenset({
70
+ "get_params", "get_hidden_params", "get_auto_state", "walk_rack",
71
+ "get_chains_deep", "get_track_cpu", "get_selected", "get_key",
72
+ "get_clip_file_path", "replace_simpler_sample", "get_simpler_slices",
73
+ "crop_simpler", "reverse_simpler", "warp_simpler",
74
+ "get_warp_markers", "add_warp_marker", "move_warp_marker",
75
+ "remove_warp_marker", "capture_audio", "capture_stop",
76
+ "check_flucoma", "scrub_clip", "stop_scrub", "get_display_values",
77
+ "get_plugin_params", "map_plugin_param", "get_plugin_presets",
78
+ "load_sample_to_simpler",
79
+ })
80
+
81
+ # Combined: all valid send_command targets
82
+ ALL_VALID_COMMANDS: frozenset[str] = REMOTE_COMMANDS | BRIDGE_COMMANDS
@@ -11,14 +11,14 @@ TIGHTEN_LOW_END = SemanticMove(
11
11
  protect={"warmth": 0.6},
12
12
  risk_level="low",
13
13
  compile_plan=[
14
- {"tool": "get_master_spectrum", "params": {}, "description": "Check current sub/low balance"},
15
- {"tool": "set_device_parameter", "params": {"description": "High-pass sub bass around 30-40 Hz"}, "description": "HP filter sub rumble"},
16
- {"tool": "set_track_volume", "params": {"description": "Reduce sub bass volume 5-10% if sub > 50%"}, "description": "Reduce sub volume"},
17
- {"tool": "set_device_parameter", "params": {"description": "Gentle saturation drive +2-4dB for harmonic definition"}, "description": "Add bass harmonics"},
14
+ {"tool": "get_master_spectrum", "params": {}, "description": "Check current sub/low balance", "backend": "mcp_tool"},
15
+ {"tool": "set_device_parameter", "params": {"description": "High-pass sub bass around 30-40 Hz"}, "description": "HP filter sub rumble", "backend": "remote_command"},
16
+ {"tool": "set_track_volume", "params": {"description": "Reduce sub bass volume 5-10% if sub > 50%"}, "description": "Reduce sub volume", "backend": "remote_command"},
17
+ {"tool": "set_device_parameter", "params": {"description": "Gentle saturation drive +2-4dB for harmonic definition"}, "description": "Add bass harmonics", "backend": "remote_command"},
18
18
  ],
19
19
  verification_plan=[
20
- {"tool": "get_master_spectrum", "check": "sub band should decrease, low-mid stable"},
21
- {"tool": "get_track_meters", "check": "bass track still producing audio"},
20
+ {"tool": "get_master_spectrum", "check": "sub band should decrease, low-mid stable", "backend": "mcp_tool"},
21
+ {"tool": "get_track_meters", "check": "bass track still producing audio", "backend": "remote_command"},
22
22
  ],
23
23
  )
24
24
 
@@ -30,14 +30,14 @@ WIDEN_STEREO = SemanticMove(
30
30
  protect={"cohesion": 0.7},
31
31
  risk_level="low",
32
32
  compile_plan=[
33
- {"tool": "analyze_mix", "params": {}, "description": "Check current stereo state"},
34
- {"tool": "set_track_pan", "params": {"description": "Pan harmonic elements wider: +/-25-40%"}, "description": "Pan harmonics wider"},
35
- {"tool": "set_track_pan", "params": {"description": "Pan percussion subtly: +/-10-20%"}, "description": "Pan perc subtly"},
36
- {"tool": "set_track_send", "params": {"description": "Increase reverb send on wide elements +10-15%"}, "description": "Add depth to wide elements"},
33
+ {"tool": "analyze_mix", "params": {}, "description": "Check current stereo state", "backend": "mcp_tool"},
34
+ {"tool": "set_track_pan", "params": {"description": "Pan harmonic elements wider: +/-25-40%"}, "description": "Pan harmonics wider", "backend": "remote_command"},
35
+ {"tool": "set_track_pan", "params": {"description": "Pan percussion subtly: +/-10-20%"}, "description": "Pan perc subtly", "backend": "remote_command"},
36
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send on wide elements +10-15%"}, "description": "Add depth to wide elements", "backend": "remote_command"},
37
37
  ],
38
38
  verification_plan=[
39
- {"tool": "get_track_meters", "check": "all tracks producing stereo output"},
40
- {"tool": "analyze_mix", "check": "stereo.mono_risk is false, side_activity > 0.1"},
39
+ {"tool": "get_track_meters", "check": "all tracks producing stereo output", "backend": "remote_command"},
40
+ {"tool": "analyze_mix", "check": "stereo.mono_risk is false, side_activity > 0.1", "backend": "mcp_tool"},
41
41
  ],
42
42
  )
43
43
 
@@ -49,14 +49,14 @@ MAKE_PUNCHIER = SemanticMove(
49
49
  protect={"clarity": 0.7, "warmth": 0.5},
50
50
  risk_level="low",
51
51
  compile_plan=[
52
- {"tool": "get_track_meters", "params": {"include_stereo": True}, "description": "Read current levels"},
53
- {"tool": "set_track_volume", "params": {"description": "Push drum track +5-8%"}, "description": "Push drum level"},
54
- {"tool": "set_track_volume", "params": {"description": "Pull pad/texture -5-10%"}, "description": "Pull back pads"},
55
- {"tool": "set_device_parameter", "params": {"description": "Lower Glue Compressor threshold 2-3dB if on master"}, "description": "Tighten master bus"},
52
+ {"tool": "get_track_meters", "params": {"include_stereo": True}, "description": "Read current levels", "backend": "remote_command"},
53
+ {"tool": "set_track_volume", "params": {"description": "Push drum track +5-8%"}, "description": "Push drum level", "backend": "remote_command"},
54
+ {"tool": "set_track_volume", "params": {"description": "Pull pad/texture -5-10%"}, "description": "Pull back pads", "backend": "remote_command"},
55
+ {"tool": "set_device_parameter", "params": {"description": "Lower Glue Compressor threshold 2-3dB if on master"}, "description": "Tighten master bus", "backend": "remote_command"},
56
56
  ],
57
57
  verification_plan=[
58
- {"tool": "get_master_spectrum", "check": "mid and high-mid energy increased relative to before"},
59
- {"tool": "get_track_meters", "check": "all tracks still producing audio"},
58
+ {"tool": "get_master_spectrum", "check": "mid and high-mid energy increased relative to before", "backend": "mcp_tool"},
59
+ {"tool": "get_track_meters", "check": "all tracks still producing audio", "backend": "remote_command"},
60
60
  ],
61
61
  )
62
62
 
@@ -68,13 +68,13 @@ DARKEN_MIX = SemanticMove(
68
68
  protect={"width": 0.7, "clarity": 0.5},
69
69
  risk_level="low",
70
70
  compile_plan=[
71
- {"tool": "get_master_spectrum", "params": {}, "description": "Check current tonal balance"},
72
- {"tool": "set_device_parameter", "params": {"description": "Lower EQ/Auto Filter high shelf -2-4dB on bright tracks"}, "description": "Roll off highs"},
73
- {"tool": "set_track_send", "params": {"description": "Increase reverb send on darkened elements for depth compensation"}, "description": "Compensate depth"},
71
+ {"tool": "get_master_spectrum", "params": {}, "description": "Check current tonal balance", "backend": "mcp_tool"},
72
+ {"tool": "set_device_parameter", "params": {"description": "Lower EQ/Auto Filter high shelf -2-4dB on bright tracks"}, "description": "Roll off highs", "backend": "remote_command"},
73
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send on darkened elements for depth compensation"}, "description": "Compensate depth", "backend": "remote_command"},
74
74
  ],
75
75
  verification_plan=[
76
- {"tool": "get_master_spectrum", "check": "high and air bands decreased, low-mid stable or increased"},
77
- {"tool": "get_track_meters", "check": "all tracks producing audio (filter didn't kill signal)"},
76
+ {"tool": "get_master_spectrum", "check": "high and air bands decreased, low-mid stable or increased", "backend": "mcp_tool"},
77
+ {"tool": "get_track_meters", "check": "all tracks producing audio (filter didn't kill signal)", "backend": "remote_command"},
78
78
  ],
79
79
  )
80
80
 
@@ -86,13 +86,13 @@ REDUCE_REPETITION = SemanticMove(
86
86
  protect={"cohesion": 0.6},
87
87
  risk_level="medium",
88
88
  compile_plan=[
89
- {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff"}, "description": "Add organic filter drift"},
90
- {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on send levels"}, "description": "Add depth movement"},
91
- {"tool": "set_device_parameter", "params": {"description": "Increase Beat Repeat variation or chance"}, "description": "Add rhythmic variation"},
89
+ {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff"}, "description": "Add organic filter drift", "backend": "mcp_tool"},
90
+ {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on send levels"}, "description": "Add depth movement", "backend": "mcp_tool"},
91
+ {"tool": "set_device_parameter", "params": {"description": "Increase Beat Repeat variation or chance"}, "description": "Add rhythmic variation", "backend": "remote_command"},
92
92
  ],
93
93
  verification_plan=[
94
- {"tool": "get_track_meters", "check": "all tracks still producing audio"},
95
- {"tool": "capture_audio", "check": "LRA > 2 LU (dynamic range should increase)"},
94
+ {"tool": "get_track_meters", "check": "all tracks still producing audio", "backend": "remote_command"},
95
+ {"tool": "capture_audio", "check": "LRA > 2 LU (dynamic range should increase)", "backend": "mcp_tool"},
96
96
  ],
97
97
  )
98
98
 
@@ -104,13 +104,13 @@ MAKE_KICK_BASS_LOCK = SemanticMove(
104
104
  protect={"warmth": 0.6, "cohesion": 0.7},
105
105
  risk_level="low",
106
106
  compile_plan=[
107
- {"tool": "get_device_parameters", "params": {"description": "Read bass EQ/filter state"}, "description": "Inspect bass chain"},
108
- {"tool": "set_device_parameter", "params": {"description": "High-pass bass at 40-60 Hz to clear space for kick sub"}, "description": "HP bass for kick clearance"},
109
- {"tool": "set_device_parameter", "params": {"description": "Sidechain compressor or volume duck on bass from kick"}, "description": "Duck bass on kick hits"},
107
+ {"tool": "get_device_parameters", "params": {"description": "Read bass EQ/filter state"}, "description": "Inspect bass chain", "backend": "remote_command"},
108
+ {"tool": "set_device_parameter", "params": {"description": "High-pass bass at 40-60 Hz to clear space for kick sub"}, "description": "HP bass for kick clearance", "backend": "remote_command"},
109
+ {"tool": "set_device_parameter", "params": {"description": "Sidechain compressor or volume duck on bass from kick"}, "description": "Duck bass on kick hits", "backend": "remote_command"},
110
110
  ],
111
111
  verification_plan=[
112
- {"tool": "get_master_spectrum", "check": "sub and low bands balanced, no masking"},
113
- {"tool": "get_track_meters", "check": "both kick and bass tracks producing audio"},
112
+ {"tool": "get_master_spectrum", "check": "sub and low bands balanced, no masking", "backend": "mcp_tool"},
113
+ {"tool": "get_track_meters", "check": "both kick and bass tracks producing audio", "backend": "remote_command"},
114
114
  ],
115
115
  )
116
116
 
@@ -122,13 +122,13 @@ CREATE_BUILDUP_TENSION = SemanticMove(
122
122
  protect={"clarity": 0.6},
123
123
  risk_level="medium",
124
124
  compile_plan=[
125
- {"tool": "apply_gesture_template", "params": {"template_name": "tension_ratchet"}, "description": "Apply staged tension ratchet"},
126
- {"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising HP filter over 4-8 bars"}, "description": "HP filter rise"},
127
- {"tool": "set_track_send", "params": {"description": "Increase reverb send for wash effect"}, "description": "Add reverb wash"},
125
+ {"tool": "apply_gesture_template", "params": {"template_name": "tension_ratchet"}, "description": "Apply staged tension ratchet", "backend": "mcp_tool"},
126
+ {"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising HP filter over 4-8 bars"}, "description": "HP filter rise", "backend": "mcp_tool"},
127
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send for wash effect"}, "description": "Add reverb wash", "backend": "remote_command"},
128
128
  ],
129
129
  verification_plan=[
130
- {"tool": "get_emotional_arc", "check": "tension should increase before target section"},
131
- {"tool": "get_track_meters", "check": "all tracks still producing audio"},
130
+ {"tool": "get_emotional_arc", "check": "tension should increase before target section", "backend": "mcp_tool"},
131
+ {"tool": "get_track_meters", "check": "all tracks still producing audio", "backend": "remote_command"},
132
132
  ],
133
133
  )
134
134
 
@@ -140,11 +140,11 @@ SMOOTH_SCENE_HANDOFF = SemanticMove(
140
140
  protect={"clarity": 0.7},
141
141
  risk_level="low",
142
142
  compile_plan=[
143
- {"tool": "apply_gesture_template", "params": {"template_name": "pre_arrival_vacuum"}, "description": "Pull energy back before transition"},
144
- {"tool": "apply_gesture_template", "params": {"template_name": "re_entry_spotlight"}, "description": "Spotlight returning elements"},
143
+ {"tool": "apply_gesture_template", "params": {"template_name": "pre_arrival_vacuum"}, "description": "Pull energy back before transition", "backend": "mcp_tool"},
144
+ {"tool": "apply_gesture_template", "params": {"template_name": "re_entry_spotlight"}, "description": "Spotlight returning elements", "backend": "mcp_tool"},
145
145
  ],
146
146
  verification_plan=[
147
- {"tool": "get_emotional_arc", "check": "transition point should show energy dip then recovery"},
147
+ {"tool": "get_emotional_arc", "check": "transition point should show energy dip then recovery", "backend": "mcp_tool"},
148
148
  ],
149
149
  )
150
150
 
@@ -16,12 +16,12 @@ RECOVER_ENERGY = SemanticMove(
16
16
  risk_level="low",
17
17
  required_capabilities=["session"],
18
18
  compile_plan=[
19
- {"tool": "set_track_volume", "params": {"description": "Gradually restore drum volume"}, "description": "Bring drums back"},
20
- {"tool": "set_track_volume", "params": {"description": "Restore bass volume"}, "description": "Bring bass back"},
21
- {"tool": "set_track_send", "params": {"description": "Reduce reverb send to tighten mix"}, "description": "Tighten reverb"},
19
+ {"tool": "set_track_volume", "params": {"description": "Gradually restore drum volume"}, "description": "Bring drums back", "backend": "remote_command"},
20
+ {"tool": "set_track_volume", "params": {"description": "Restore bass volume"}, "description": "Bring bass back", "backend": "remote_command"},
21
+ {"tool": "set_track_send", "params": {"description": "Reduce reverb send to tighten mix"}, "description": "Tighten reverb", "backend": "remote_command"},
22
22
  ],
23
23
  verification_plan=[
24
- {"tool": "get_track_meters", "check": "drum and bass tracks producing audio"},
24
+ {"tool": "get_track_meters", "check": "drum and bass tracks producing audio", "backend": "remote_command"},
25
25
  ],
26
26
  )
27
27
 
@@ -34,11 +34,11 @@ DECOMPRESS_TENSION = SemanticMove(
34
34
  risk_level="low",
35
35
  required_capabilities=["session"],
36
36
  compile_plan=[
37
- {"tool": "set_track_volume", "params": {"description": "Pull back high-energy elements 15-20%"}, "description": "Pull energy down"},
38
- {"tool": "set_track_send", "params": {"description": "Increase reverb for spaciousness"}, "description": "Open space"},
37
+ {"tool": "set_track_volume", "params": {"description": "Pull back high-energy elements 15-20%"}, "description": "Pull energy down", "backend": "remote_command"},
38
+ {"tool": "set_track_send", "params": {"description": "Increase reverb for spaciousness"}, "description": "Open space", "backend": "remote_command"},
39
39
  ],
40
40
  verification_plan=[
41
- {"tool": "get_track_meters", "check": "all tracks still alive, overall energy decreased"},
41
+ {"tool": "get_track_meters", "check": "all tracks still alive, overall energy decreased", "backend": "remote_command"},
42
42
  ],
43
43
  )
44
44
 
@@ -51,11 +51,11 @@ SAFE_SPOTLIGHT = SemanticMove(
51
51
  risk_level="low",
52
52
  required_capabilities=["session"],
53
53
  compile_plan=[
54
- {"tool": "set_track_volume", "params": {"description": "Pull non-spotlight tracks to 30-40%"}, "description": "Pull background"},
55
- {"tool": "set_track_volume", "params": {"description": "Push spotlight track to 80-85%"}, "description": "Push spotlight"},
54
+ {"tool": "set_track_volume", "params": {"description": "Pull non-spotlight tracks to 30-40%"}, "description": "Pull background", "backend": "remote_command"},
55
+ {"tool": "set_track_volume", "params": {"description": "Push spotlight track to 80-85%"}, "description": "Push spotlight", "backend": "remote_command"},
56
56
  ],
57
57
  verification_plan=[
58
- {"tool": "get_track_meters", "check": "spotlight track clearly dominant, others still audible"},
58
+ {"tool": "get_track_meters", "check": "spotlight track clearly dominant, others still audible", "backend": "remote_command"},
59
59
  ],
60
60
  )
61
61
 
@@ -68,11 +68,11 @@ EMERGENCY_SIMPLIFY = SemanticMove(
68
68
  risk_level="low",
69
69
  required_capabilities=["session"],
70
70
  compile_plan=[
71
- {"tool": "set_track_volume", "params": {"description": "Pull all non-rhythm tracks to 10-15%"}, "description": "Strip to essentials"},
72
- {"tool": "set_track_volume", "params": {"description": "Keep drums at current level"}, "description": "Maintain rhythm"},
71
+ {"tool": "set_track_volume", "params": {"description": "Pull all non-rhythm tracks to 10-15%"}, "description": "Strip to essentials", "backend": "remote_command"},
72
+ {"tool": "set_track_volume", "params": {"description": "Keep drums at current level"}, "description": "Maintain rhythm", "backend": "remote_command"},
73
73
  ],
74
74
  verification_plan=[
75
- {"tool": "get_track_meters", "check": "drums producing audio, other tracks at low but nonzero level"},
75
+ {"tool": "get_track_meters", "check": "drums producing audio, other tracks at low but nonzero level", "backend": "remote_command"},
76
76
  ],
77
77
  )
78
78
 
@@ -14,12 +14,12 @@ ADD_WARMTH = SemanticMove(
14
14
  protect={"clarity": 0.6, "punch": 0.5},
15
15
  risk_level="low",
16
16
  compile_plan=[
17
- {"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation"},
18
- {"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth"},
17
+ {"tool": "set_device_parameter", "params": {"description": "Add Saturator drive +2-4dB for harmonic warmth"}, "description": "Add saturation", "backend": "remote_command"},
18
+ {"tool": "set_device_parameter", "params": {"description": "Boost EQ low-mid shelf +1-2dB"}, "description": "Low-mid warmth", "backend": "remote_command"},
19
19
  ],
20
20
  verification_plan=[
21
- {"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable"},
22
- {"tool": "get_track_meters", "check": "target track producing audio"},
21
+ {"tool": "get_master_spectrum", "check": "low-mid energy increased, high-mid stable", "backend": "mcp_tool"},
22
+ {"tool": "get_track_meters", "check": "target track producing audio", "backend": "remote_command"},
23
23
  ],
24
24
  )
25
25
 
@@ -31,11 +31,11 @@ ADD_TEXTURE = SemanticMove(
31
31
  protect={"clarity": 0.6},
32
32
  risk_level="medium",
33
33
  compile_plan=[
34
- {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion"},
35
- {"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay"},
34
+ {"tool": "apply_automation_shape", "params": {"curve_type": "perlin", "description": "Perlin noise on filter cutoff for organic texture"}, "description": "Organic filter motion", "backend": "mcp_tool"},
35
+ {"tool": "set_track_send", "params": {"description": "Increase delay send for spatial texture"}, "description": "Spatial texture via delay", "backend": "remote_command"},
36
36
  ],
37
37
  verification_plan=[
38
- {"tool": "get_track_meters", "check": "track producing audio with variation"},
38
+ {"tool": "get_track_meters", "check": "track producing audio with variation", "backend": "remote_command"},
39
39
  ],
40
40
  )
41
41
 
@@ -47,11 +47,11 @@ SHAPE_TRANSIENTS = SemanticMove(
47
47
  protect={"warmth": 0.5},
48
48
  risk_level="low",
49
49
  compile_plan=[
50
- {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack"},
51
- {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release"},
50
+ {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor attack time (faster = sharper transients, slower = rounder)"}, "description": "Shape attack", "backend": "remote_command"},
51
+ {"tool": "set_device_parameter", "params": {"description": "Adjust Compressor release for rhythmic pumping"}, "description": "Shape release", "backend": "remote_command"},
52
52
  ],
53
53
  verification_plan=[
54
- {"tool": "get_track_meters", "check": "track producing audio with expected transient character"},
54
+ {"tool": "get_track_meters", "check": "track producing audio with expected transient character", "backend": "remote_command"},
55
55
  ],
56
56
  )
57
57
 
@@ -63,13 +63,13 @@ ADD_SPACE = SemanticMove(
63
63
  protect={"punch": 0.6, "clarity": 0.5},
64
64
  risk_level="low",
65
65
  compile_plan=[
66
- {"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth"},
67
- {"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture"},
68
- {"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field"},
66
+ {"tool": "set_track_send", "params": {"description": "Increase reverb send to 25-35%"}, "description": "Add reverb depth", "backend": "remote_command"},
67
+ {"tool": "set_track_send", "params": {"description": "Add subtle delay send 10-15%"}, "description": "Add delay texture", "backend": "remote_command"},
68
+ {"tool": "set_track_pan", "params": {"description": "Widen pan slightly for spatial presence"}, "description": "Widen spatial field", "backend": "remote_command"},
69
69
  ],
70
70
  verification_plan=[
71
- {"tool": "get_track_meters", "check": "stereo output present, no phase cancellation"},
72
- {"tool": "analyze_mix", "check": "stereo.mono_risk is false"},
71
+ {"tool": "get_track_meters", "check": "stereo output present, no phase cancellation", "backend": "remote_command"},
72
+ {"tool": "analyze_mix", "check": "stereo.mono_risk is false", "backend": "mcp_tool"},
73
73
  ],
74
74
  )
75
75
 
@@ -177,24 +177,25 @@ def apply_semantic_move(
177
177
  result["note"] = "Awaiting approval — present the plan to the user, then execute steps individually"
178
178
  return result
179
179
 
180
- # explore mode — execute immediately
180
+ # explore mode — execute through unified router
181
+ from ..runtime.execution_router import execute_plan_steps
182
+
183
+ step_dicts = [
184
+ {"tool": step.tool, "params": step.params, "description": step.description}
185
+ for step in plan.steps
186
+ ]
187
+ exec_results = execute_plan_steps(step_dicts, ableton=ableton, ctx=ctx)
188
+
181
189
  executed_steps = []
182
- for step in plan.steps:
183
- try:
184
- tool_result = ableton.send_command(step.tool, step.params)
185
- executed_steps.append({
186
- "tool": step.tool,
187
- "description": step.description,
188
- "result": tool_result,
189
- "ok": True,
190
- })
191
- except Exception as exc:
192
- executed_steps.append({
193
- "tool": step.tool,
194
- "description": step.description,
195
- "error": str(exc),
196
- "ok": False,
197
- })
190
+ for i, er in enumerate(exec_results):
191
+ executed_steps.append({
192
+ "tool": er.tool,
193
+ "backend": er.backend,
194
+ "description": step_dicts[i].get("description", ""),
195
+ "result": er.result if er.ok else None,
196
+ "error": er.error if not er.ok else None,
197
+ "ok": er.ok,
198
+ })
198
199
 
199
200
  result = plan.to_dict()
200
201
  result["executed"] = True