livepilot 1.19.0 → 1.20.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.
@@ -0,0 +1,229 @@
1
+ """Compilers for routing-domain semantic moves (v1.20).
2
+
3
+ Each compiler is pure — reads ``kernel["seed_args"]`` for user targets and
4
+ ``kernel["session_info"]`` for topology, emits a CompiledPlan of concrete
5
+ tool calls the execution router can dispatch. No I/O, no MCP calls.
6
+
7
+ Rejection policy: when seed_args are missing/invalid, emit a plan with
8
+ ``executable=False`` (empty steps) and a clear warning. This is the v1.20
9
+ contract — see docs/plans/v1.20-structural-plan.md §7.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from .compiler import CompiledPlan, CompiledStep, register_compiler
15
+ from .models import SemanticMove
16
+
17
+
18
+ def _return_track_index_to_abs(return_track_index: int) -> int:
19
+ """Map a 0-based return index to Ableton's negative-track convention.
20
+
21
+ Return A (return_track_index=0) → track_index=-1
22
+ Return B (return_track_index=1) → track_index=-2
23
+ (See mcp_server/tools/mixing.py:227 — "-1=A, -2=B".)
24
+ """
25
+ return -(return_track_index + 1)
26
+
27
+
28
+ def _empty_plan(move: SemanticMove, warnings: list[str]) -> CompiledPlan:
29
+ return CompiledPlan(
30
+ move_id=move.move_id,
31
+ intent=move.intent,
32
+ steps=[],
33
+ risk_level=move.risk_level,
34
+ summary="; ".join(warnings) if warnings else "No plan compiled",
35
+ requires_approval=True,
36
+ warnings=warnings,
37
+ )
38
+
39
+
40
+ # ── build_send_chain ──────────────────────────────────────────────────────────
41
+
42
+
43
+ def _compile_build_send_chain(move: SemanticMove, kernel: dict) -> CompiledPlan:
44
+ args = kernel.get("seed_args") or {}
45
+ return_idx = args.get("return_track_index")
46
+ device_chain = args.get("device_chain")
47
+ warnings: list[str] = []
48
+
49
+ if return_idx is None or device_chain is None:
50
+ return _empty_plan(move, [
51
+ "build_send_chain requires seed_args.return_track_index + device_chain"
52
+ ])
53
+ if not isinstance(return_idx, int) or return_idx < 0:
54
+ return _empty_plan(move, [
55
+ f"return_track_index must be a non-negative int, got {return_idx!r}"
56
+ ])
57
+ if not isinstance(device_chain, (list, tuple)) or not device_chain:
58
+ return _empty_plan(move, [
59
+ "device_chain is empty — nothing to load onto the return"
60
+ ])
61
+ if not all(isinstance(d, str) and d.strip() for d in device_chain):
62
+ return _empty_plan(move, [
63
+ "device_chain entries must be non-empty strings (device names)"
64
+ ])
65
+
66
+ abs_track_index = _return_track_index_to_abs(return_idx)
67
+
68
+ steps: list[CompiledStep] = []
69
+ for device_name in device_chain:
70
+ steps.append(CompiledStep(
71
+ tool="find_and_load_device",
72
+ params={
73
+ "track_index": abs_track_index,
74
+ "device_name": device_name,
75
+ # Return chains legitimately may hold two of the same device
76
+ # (Echo feedback stacking, parallel reverbs). Don't block it.
77
+ "allow_duplicate": True,
78
+ },
79
+ description=f"Load {device_name} onto return {chr(ord('A') + return_idx)}",
80
+ verify_after=True,
81
+ backend="remote_command",
82
+ ))
83
+
84
+ # Verify-only read at the end (caller uses this to confirm ordering).
85
+ steps.append(CompiledStep(
86
+ tool="get_track_info",
87
+ params={"track_index": abs_track_index},
88
+ description="Verify return-track device order after load",
89
+ verify_after=False,
90
+ backend="remote_command",
91
+ ))
92
+
93
+ return CompiledPlan(
94
+ move_id=move.move_id,
95
+ intent=move.intent,
96
+ steps=steps,
97
+ risk_level=move.risk_level,
98
+ summary=(
99
+ f"Load {len(device_chain)} device(s) onto return "
100
+ f"{chr(ord('A') + return_idx)}: {', '.join(device_chain)}"
101
+ ),
102
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
103
+ warnings=warnings,
104
+ )
105
+
106
+
107
+ # ── configure_send_architecture ───────────────────────────────────────────────
108
+
109
+
110
+ def _compile_configure_send_architecture(move: SemanticMove, kernel: dict) -> CompiledPlan:
111
+ args = kernel.get("seed_args") or {}
112
+ track_indices = args.get("source_track_indices")
113
+ send_index = args.get("send_index")
114
+ levels = args.get("levels")
115
+
116
+ if track_indices is None or send_index is None or levels is None:
117
+ return _empty_plan(move, [
118
+ "configure_send_architecture requires seed_args.source_track_indices + send_index + levels"
119
+ ])
120
+ if not isinstance(track_indices, (list, tuple)) or not track_indices:
121
+ return _empty_plan(move, ["source_track_indices must be a non-empty list"])
122
+ if not isinstance(levels, (list, tuple)):
123
+ return _empty_plan(move, ["levels must be a list"])
124
+ if len(track_indices) != len(levels):
125
+ return _empty_plan(move, [
126
+ f"source_track_indices ({len(track_indices)}) and levels "
127
+ f"({len(levels)}) must have the same length"
128
+ ])
129
+ if not isinstance(send_index, int) or send_index < 0:
130
+ return _empty_plan(move, [f"send_index must be a non-negative int, got {send_index!r}"])
131
+
132
+ warnings: list[str] = []
133
+ steps: list[CompiledStep] = []
134
+ for track_i, level in zip(track_indices, levels):
135
+ if not isinstance(track_i, int):
136
+ return _empty_plan(move, [f"track_index must be int, got {track_i!r}"])
137
+ try:
138
+ level_f = float(level)
139
+ except (TypeError, ValueError):
140
+ return _empty_plan(move, [f"level must be numeric, got {level!r}"])
141
+ clamped = max(0.0, min(1.0, level_f))
142
+ if clamped != level_f:
143
+ warnings.append(
144
+ f"Clamped level {level_f} → {clamped} for track {track_i} "
145
+ "(send values must be in [0.0, 1.0])"
146
+ )
147
+ steps.append(CompiledStep(
148
+ tool="set_track_send",
149
+ params={
150
+ "track_index": track_i,
151
+ "send_index": send_index,
152
+ "value": clamped,
153
+ },
154
+ description=(
155
+ f"Set track {track_i} send {send_index} to {clamped:.2f}"
156
+ ),
157
+ verify_after=True,
158
+ backend="remote_command",
159
+ ))
160
+
161
+ return CompiledPlan(
162
+ move_id=move.move_id,
163
+ intent=move.intent,
164
+ steps=steps,
165
+ risk_level=move.risk_level,
166
+ summary=f"Set {len(steps)} send levels on send {send_index}",
167
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
168
+ warnings=warnings,
169
+ )
170
+
171
+
172
+ # ── set_track_routing ─────────────────────────────────────────────────────────
173
+
174
+
175
+ def _compile_set_track_routing(move: SemanticMove, kernel: dict) -> CompiledPlan:
176
+ args = kernel.get("seed_args") or {}
177
+ track_index = args.get("track_index")
178
+ # Seed_args keeps the ergonomic ``output_routing_type`` /
179
+ # ``output_routing_channel`` names (matching the MCP tool's public
180
+ # surface, so the director can type them naturally). But compiled
181
+ # steps must use the wire-format names — the remote_command backend
182
+ # bypasses the MCP tool's rename at mcp_server/tools/mixing.py:230
183
+ # (output_routing_type → output_type). Ableton's Remote Script at
184
+ # remote_script/LivePilot/mixing.py:227 keys on the wire format.
185
+ out_type = args.get("output_routing_type")
186
+ out_channel = args.get("output_routing_channel")
187
+
188
+ if track_index is None:
189
+ return _empty_plan(move, ["set_track_routing requires seed_args.track_index"])
190
+ if out_type is None and out_channel is None:
191
+ return _empty_plan(move, [
192
+ "set_track_routing requires at least output_routing_type or output_routing_channel"
193
+ ])
194
+ if not isinstance(track_index, int):
195
+ return _empty_plan(move, [f"track_index must be int, got {track_index!r}"])
196
+
197
+ params: dict = {"track_index": track_index}
198
+ if out_type is not None:
199
+ params["output_type"] = str(out_type)
200
+ if out_channel is not None:
201
+ params["output_channel"] = str(out_channel)
202
+
203
+ step = CompiledStep(
204
+ tool="set_track_routing",
205
+ params=params,
206
+ description=(
207
+ f"Set track {track_index} output routing → "
208
+ f"{out_type or '(unchanged)'} / {out_channel or '(unchanged)'}"
209
+ ),
210
+ verify_after=True,
211
+ backend="remote_command",
212
+ )
213
+
214
+ return CompiledPlan(
215
+ move_id=move.move_id,
216
+ intent=move.intent,
217
+ steps=[step],
218
+ risk_level=move.risk_level,
219
+ summary=step.description,
220
+ requires_approval=(kernel.get("mode", "improve") != "explore"),
221
+ warnings=[],
222
+ )
223
+
224
+
225
+ # ── Register compilers ────────────────────────────────────────────────────────
226
+
227
+ register_compiler("build_send_chain", _compile_build_send_chain)
228
+ register_compiler("configure_send_architecture", _compile_configure_send_architecture)
229
+ register_compiler("set_track_routing", _compile_set_track_routing)
@@ -0,0 +1,109 @@
1
+ """Routing-domain semantic moves (v1.20) — build send chains, configure
2
+ send architectures, rewire track outputs.
3
+
4
+ These moves take user-supplied targets via ``kernel["seed_args"]`` (threaded
5
+ through by ``apply_semantic_move(args=...)`` / ``preview_semantic_move(args=...)``).
6
+ Compilers are pure and live in :mod:`routing_compilers`.
7
+
8
+ The routing family exists to give the Creative Director Phase 6 a
9
+ semantic_move path for three raw-tool patterns that previously required
10
+ the escape hatch:
11
+
12
+ * Loading a chain of devices onto a return track (dub/ambient sends)
13
+ * Setting send levels across a bunch of source tracks in one move
14
+ * Rewiring a track's output routing (e.g., "Sends Only")
15
+ """
16
+
17
+ from .models import SemanticMove
18
+ from .registry import register
19
+
20
+
21
+ BUILD_SEND_CHAIN = SemanticMove(
22
+ move_id="build_send_chain",
23
+ family="device_creation",
24
+ intent=(
25
+ "Build a device chain on a return track — e.g., a dub Echo → "
26
+ "Auto Filter → Convolution Reverb send architecture. Takes the "
27
+ "return_track_index and an ordered device_chain list via seed_args."
28
+ ),
29
+ targets={"space": 0.4, "depth": 0.3, "cohesion": 0.3},
30
+ protect={"low_end": 0.6},
31
+ risk_level="medium",
32
+ plan_template=[
33
+ {
34
+ "tool": "find_and_load_device",
35
+ "params": {"description": "Load each device in device_chain onto the return track, in order"},
36
+ "description": "Load device chain onto return",
37
+ "backend": "remote_command",
38
+ },
39
+ ],
40
+ verification_plan=[
41
+ {
42
+ "tool": "get_track_info",
43
+ "check": "return track now shows the loaded devices in the expected order",
44
+ "backend": "remote_command",
45
+ },
46
+ ],
47
+ )
48
+
49
+ CONFIGURE_SEND_ARCHITECTURE = SemanticMove(
50
+ move_id="configure_send_architecture",
51
+ family="mix",
52
+ intent=(
53
+ "Set send levels across a set of source tracks in a single move — "
54
+ "e.g., route three source tracks to the Reverb return at balanced "
55
+ "levels. Takes source_track_indices, send_index, levels via seed_args."
56
+ ),
57
+ targets={"space": 0.5, "depth": 0.3, "cohesion": 0.2},
58
+ protect={"clarity": 0.5},
59
+ risk_level="low",
60
+ plan_template=[
61
+ {
62
+ "tool": "set_track_send",
63
+ "params": {"description": "One set_track_send call per (track, level) pair"},
64
+ "description": "Apply send levels",
65
+ "backend": "remote_command",
66
+ },
67
+ ],
68
+ verification_plan=[
69
+ {
70
+ "tool": "get_track_info",
71
+ "check": "each source track's send value matches the requested level",
72
+ "backend": "remote_command",
73
+ },
74
+ ],
75
+ )
76
+
77
+ SET_TRACK_ROUTING_MOVE = SemanticMove(
78
+ move_id="set_track_routing",
79
+ family="mix",
80
+ intent=(
81
+ "Rewire a track's output routing — e.g., switch an intermediate "
82
+ "track to 'Sends Only' for a bus architecture. Takes track_index "
83
+ "plus output_routing_type/channel via seed_args."
84
+ ),
85
+ # Routing is topology, not a dimension claim; protect clarity because a
86
+ # bad routing move can silence the track entirely.
87
+ targets={},
88
+ protect={"clarity": 0.5},
89
+ risk_level="medium",
90
+ plan_template=[
91
+ {
92
+ "tool": "set_track_routing",
93
+ "params": {"description": "Single set_track_routing call with the provided output fields"},
94
+ "description": "Rewire track output routing",
95
+ "backend": "remote_command",
96
+ },
97
+ ],
98
+ verification_plan=[
99
+ {
100
+ "tool": "get_track_routing",
101
+ "check": "output_type and output_channel match the requested values",
102
+ "backend": "remote_command",
103
+ },
104
+ ],
105
+ )
106
+
107
+
108
+ for _move in (BUILD_SEND_CHAIN, CONFIGURE_SEND_ARCHITECTURE, SET_TRACK_ROUTING_MOVE):
109
+ register(_move)
@@ -45,6 +45,7 @@ def list_semantic_moves(
45
45
  def preview_semantic_move(
46
46
  ctx: Context,
47
47
  move_id: str,
48
+ args: Optional[dict] = None,
48
49
  ) -> dict:
49
50
  """Preview what a semantic move will do before applying it.
50
51
 
@@ -54,6 +55,11 @@ def preview_semantic_move(
54
55
  tool calls the move would emit right now; use plan_template to understand
55
56
  the move's shape independent of session state.
56
57
 
58
+ args (v1.20+): user-supplied seed parameters threaded into the kernel as
59
+ ``kernel["seed_args"]``. Routing / content / metadata moves require these
60
+ (e.g., ``{"return_track_index": 0, "device_chain": ["Echo", ...]}``).
61
+ Pre-v1.20 moves read only from ``session_info`` and ignore seed_args.
62
+
57
63
  Existing callers reading plan_template are unaffected by the addition.
58
64
  """
59
65
  move = registry.get_move(move_id)
@@ -96,7 +102,10 @@ def preview_semantic_move(
96
102
  session_info=session_info,
97
103
  capability_state=state.to_dict(),
98
104
  )
99
- plan = move_compiler.compile(move, kernel.to_dict())
105
+ kernel_dict = kernel.to_dict()
106
+ # v1.20: thread user seed_args through to the compiler.
107
+ kernel_dict["seed_args"] = dict(args) if args else {}
108
+ plan = move_compiler.compile(move, kernel_dict)
100
109
  result["compiled_plan"] = plan.to_dict()
101
110
  result["compiled_plan_executable"] = bool(plan.executable)
102
111
  except Exception as e:
@@ -285,6 +294,7 @@ async def apply_semantic_move(
285
294
  ctx: Context,
286
295
  move_id: str,
287
296
  mode: str = "improve",
297
+ args: Optional[dict] = None,
288
298
  ) -> dict:
289
299
  """Compile and optionally execute a semantic move against the current session.
290
300
 
@@ -297,6 +307,12 @@ async def apply_semantic_move(
297
307
  - "explore": compile and EXECUTE immediately, capturing before/after.
298
308
  - "observe" / "diagnose": compile only, never execute. Return the plan.
299
309
 
310
+ args (v1.20+): user-supplied seed parameters threaded into the kernel as
311
+ ``kernel["seed_args"]``. Required by routing / content / metadata moves —
312
+ e.g., ``apply_semantic_move("build_send_chain", mode="explore",
313
+ args={"return_track_index": 0, "device_chain": ["Echo", "Auto Filter"]})``.
314
+ Pre-v1.20 moves read only from ``session_info`` and ignore seed_args.
315
+
300
316
  Returns: CompiledPlan with concrete steps, summary, and execution status.
301
317
  """
302
318
  from . import compiler
@@ -312,6 +328,7 @@ async def apply_semantic_move(
312
328
  "session_info": session_info,
313
329
  "mode": mode,
314
330
  "capability_state": {},
331
+ "seed_args": dict(args) if args else {},
315
332
  }
316
333
 
317
334
  # Compile the move
@@ -373,9 +390,46 @@ async def apply_semantic_move(
373
390
  "ok": er.ok,
374
391
  })
375
392
 
393
+ success_count = sum(1 for s in executed_steps if s["ok"])
394
+ failure_count = sum(1 for s in executed_steps if not s["ok"])
395
+
396
+ # v1.20: write the executed move to the SessionLedger so
397
+ # get_last_move / memory_list / anti-repetition-rules can see it
398
+ # WITHOUT requiring the director to call add_session_memory
399
+ # manually. Best-effort — a ledger write failure must not fail
400
+ # the overall move.
401
+ ledger_entry_id: Optional[str] = None
402
+ try:
403
+ from ..runtime.action_ledger import SessionLedger
404
+ ledger = ctx.lifespan_context.setdefault("action_ledger", SessionLedger())
405
+ ledger_entry_id = ledger.start_move(
406
+ engine="semantic_moves",
407
+ move_class=move.family,
408
+ intent=f"{move.move_id}: {move.intent}",
409
+ undo_scope="micro",
410
+ )
411
+ for es in executed_steps:
412
+ if es["ok"]:
413
+ ledger.append_action(
414
+ ledger_entry_id,
415
+ tool_name=es["tool"],
416
+ summary=es.get("description", "") or es["tool"],
417
+ )
418
+ # Provisional keep — evaluate_move / user undo flip this later.
419
+ ledger.finalize_move(
420
+ ledger_entry_id,
421
+ kept=(failure_count == 0),
422
+ score=(float(success_count) / len(executed_steps)) if executed_steps else 0.0,
423
+ memory_candidate=False,
424
+ )
425
+ except Exception as exc: # pragma: no cover — ledger is best-effort
426
+ logger.warning("apply_semantic_move ledger write failed: %s", exc)
427
+
376
428
  result = plan.to_dict()
377
429
  result["executed"] = True
378
430
  result["execution_results"] = executed_steps
379
- result["success_count"] = sum(1 for s in executed_steps if s["ok"])
380
- result["failure_count"] = sum(1 for s in executed_steps if not s["ok"])
431
+ result["success_count"] = success_count
432
+ result["failure_count"] = failure_count
433
+ if ledger_entry_id is not None:
434
+ result["ledger_entry_id"] = ledger_entry_id
381
435
  return result
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "livepilot",
3
- "version": "1.19.0",
3
+ "version": "1.20.0",
4
4
  "mcpName": "io.github.dreamrec/livepilot",
5
5
  "description": "Agentic production system for Ableton Live 12 — 429 tools, 53 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",
@@ -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.19.0"
8
+ __version__ = "1.20.0"
9
9
 
10
10
  from _Framework.ControlSurface import ControlSurface
11
11
  from . import router
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.19.0",
9
+ "version": "1.20.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "livepilot",
14
- "version": "1.19.0",
14
+ "version": "1.20.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }