livepilot 1.27.1 → 1.27.3

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 (117) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/README.md +10 -7
  3. package/bin/livepilot.js +156 -37
  4. package/livepilot/.Codex-plugin/plugin.json +1 -1
  5. package/livepilot/.claude-plugin/plugin.json +1 -1
  6. package/livepilot/skills/livepilot-core/SKILL.md +33 -1
  7. package/livepilot/skills/livepilot-core/references/atlas-tool-notes.md +69 -0
  8. package/livepilot/skills/livepilot-core/references/overview.md +18 -11
  9. package/livepilot/skills/livepilot-core/references/perception.md +125 -0
  10. package/livepilot/skills/livepilot-devices/references/device-parameter-units.md +66 -0
  11. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  12. package/livepilot/skills/livepilot-mix-engine/SKILL.md +8 -0
  13. package/livepilot/skills/livepilot-release/SKILL.md +1 -1
  14. package/livepilot/skills/livepilot-sample-engine/references/splice-tools-notes.md +56 -0
  15. package/livepilot/skills/livepilot-wonder/SKILL.md +6 -0
  16. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  17. package/m4l_device/livepilot_bridge.js +48 -11
  18. package/mcp_server/__init__.py +1 -1
  19. package/mcp_server/atlas/__init__.py +181 -40
  20. package/mcp_server/atlas/overlays.py +46 -3
  21. package/mcp_server/atlas/tools.py +226 -86
  22. package/mcp_server/audit/checks.py +50 -22
  23. package/mcp_server/audit/state.py +10 -2
  24. package/mcp_server/audit/tools.py +27 -6
  25. package/mcp_server/composer/develop/apply.py +7 -7
  26. package/mcp_server/composer/fast/apply.py +29 -23
  27. package/mcp_server/composer/fast/brief_builder.py +6 -2
  28. package/mcp_server/composer/framework/atlas_resolver.py +16 -1
  29. package/mcp_server/composer/full/apply.py +40 -34
  30. package/mcp_server/composer/full/engine.py +66 -32
  31. package/mcp_server/composer/tools.py +16 -9
  32. package/mcp_server/connection.py +69 -10
  33. package/mcp_server/creative_constraints/engine.py +10 -2
  34. package/mcp_server/creative_constraints/tools.py +24 -7
  35. package/mcp_server/curves.py +30 -7
  36. package/mcp_server/device_forge/builder.py +40 -11
  37. package/mcp_server/device_forge/models.py +9 -0
  38. package/mcp_server/device_forge/tools.py +30 -11
  39. package/mcp_server/experiment/engine.py +29 -22
  40. package/mcp_server/experiment/tools.py +15 -5
  41. package/mcp_server/m4l_bridge.py +217 -62
  42. package/mcp_server/memory/taste_graph.py +43 -0
  43. package/mcp_server/memory/technique_store.py +26 -3
  44. package/mcp_server/memory/tools.py +62 -4
  45. package/mcp_server/mix_engine/critics.py +187 -30
  46. package/mcp_server/mix_engine/models.py +21 -1
  47. package/mcp_server/mix_engine/state_builder.py +87 -8
  48. package/mcp_server/mix_engine/tools.py +16 -4
  49. package/mcp_server/performance_engine/tools.py +56 -8
  50. package/mcp_server/persistence/base_store.py +11 -0
  51. package/mcp_server/persistence/project_store.py +132 -8
  52. package/mcp_server/persistence/taste_store.py +90 -7
  53. package/mcp_server/preview_studio/engine.py +40 -10
  54. package/mcp_server/preview_studio/models.py +40 -0
  55. package/mcp_server/preview_studio/tools.py +56 -12
  56. package/mcp_server/project_brain/arrangement_graph.py +4 -0
  57. package/mcp_server/project_brain/models.py +2 -0
  58. package/mcp_server/project_brain/role_graph.py +13 -7
  59. package/mcp_server/reference_engine/gap_analyzer.py +58 -3
  60. package/mcp_server/reference_engine/profile_builder.py +47 -4
  61. package/mcp_server/reference_engine/tools.py +6 -0
  62. package/mcp_server/runtime/execution_router.py +57 -1
  63. package/mcp_server/runtime/mcp_dispatch.py +22 -0
  64. package/mcp_server/runtime/remote_commands.py +9 -4
  65. package/mcp_server/runtime/tools.py +0 -1
  66. package/mcp_server/sample_engine/sources.py +0 -2
  67. package/mcp_server/sample_engine/tools.py +19 -38
  68. package/mcp_server/semantic_moves/mix_compilers.py +276 -51
  69. package/mcp_server/semantic_moves/performance_compilers.py +51 -17
  70. package/mcp_server/semantic_moves/resolvers.py +45 -0
  71. package/mcp_server/semantic_moves/sound_design_compilers.py +14 -6
  72. package/mcp_server/semantic_moves/tools.py +80 -2
  73. package/mcp_server/semantic_moves/transition_compilers.py +26 -9
  74. package/mcp_server/server.py +27 -25
  75. package/mcp_server/session_continuity/tracker.py +51 -3
  76. package/mcp_server/song_brain/builder.py +47 -5
  77. package/mcp_server/song_brain/tools.py +21 -7
  78. package/mcp_server/sound_design/critics.py +1 -0
  79. package/mcp_server/splice_client/client.py +117 -33
  80. package/mcp_server/splice_client/http_bridge.py +15 -3
  81. package/mcp_server/splice_client/quota.py +28 -0
  82. package/mcp_server/stuckness_detector/detector.py +8 -5
  83. package/mcp_server/synthesis_brain/adapters/drift.py +13 -0
  84. package/mcp_server/synthesis_brain/adapters/meld.py +13 -0
  85. package/mcp_server/tools/_analyzer_engine/sample.py +36 -10
  86. package/mcp_server/tools/_perception_engine.py +6 -0
  87. package/mcp_server/tools/agent_os.py +4 -1
  88. package/mcp_server/tools/analyzer.py +198 -209
  89. package/mcp_server/tools/arrangement.py +7 -4
  90. package/mcp_server/tools/automation.py +24 -4
  91. package/mcp_server/tools/browser.py +25 -11
  92. package/mcp_server/tools/clips.py +6 -0
  93. package/mcp_server/tools/composition.py +33 -2
  94. package/mcp_server/tools/devices.py +53 -53
  95. package/mcp_server/tools/generative.py +14 -14
  96. package/mcp_server/tools/harmony.py +7 -7
  97. package/mcp_server/tools/mixing.py +4 -4
  98. package/mcp_server/tools/planner.py +68 -6
  99. package/mcp_server/tools/research.py +20 -2
  100. package/mcp_server/tools/theory.py +10 -10
  101. package/mcp_server/tools/transport.py +7 -2
  102. package/mcp_server/transition_engine/critics.py +13 -1
  103. package/mcp_server/user_corpus/tools.py +30 -1
  104. package/mcp_server/wonder_mode/engine.py +82 -9
  105. package/mcp_server/wonder_mode/session.py +32 -10
  106. package/mcp_server/wonder_mode/tools.py +14 -1
  107. package/package.json +1 -1
  108. package/remote_script/LivePilot/__init__.py +21 -8
  109. package/remote_script/LivePilot/arrangement.py +93 -33
  110. package/remote_script/LivePilot/browser.py +60 -4
  111. package/remote_script/LivePilot/devices.py +132 -62
  112. package/remote_script/LivePilot/mixing.py +31 -5
  113. package/remote_script/LivePilot/server.py +94 -22
  114. package/remote_script/LivePilot/tracks.py +11 -5
  115. package/remote_script/LivePilot/transport.py +11 -0
  116. package/requirements.txt +5 -5
  117. package/server.json +2 -2
@@ -5,6 +5,7 @@
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import asyncio
8
9
  import tempfile
9
10
  from pathlib import Path
10
11
 
@@ -48,11 +49,13 @@ async def generate_m4l_effect(
48
49
  }
49
50
  dt = type_map.get(device_type)
50
51
  if dt is None:
51
- return {"error": f"Invalid device_type: {device_type}. Use: audio_effect, midi_effect, instrument"}
52
+ return {"error": f"INVALID_PARAM: Invalid device_type: {device_type}. Use one of: {', '.join(type_map)}"}
52
53
 
53
54
  parsed_params = []
54
55
  if params:
55
56
  for p in params:
57
+ if not isinstance(p, dict) or "name" not in p:
58
+ return {"error": f"INVALID_PARAM: each param requires a 'name' key, got: {p!r}"}
56
59
  parsed_params.append(GenExprParam(
57
60
  name=p["name"],
58
61
  default=p.get("default", 0.5),
@@ -72,9 +75,16 @@ async def generate_m4l_effect(
72
75
  if install:
73
76
  subdir = _SUBDIR_MAP[dt]
74
77
  output_dir = ABLETON_USER_LIBRARY / subdir
75
- path = build_device(spec, output_dir=output_dir)
76
78
  else:
77
- path = build_device(spec, output_dir=tempfile.mkdtemp(prefix="livepilot_forge_"))
79
+ # Reuse ONE forge directory under the system temp instead of spawning a
80
+ # fresh mkdtemp() per call (those accumulated, never cleaned). The .amxd
81
+ # is the returned deliverable so it must persist for the caller.
82
+ output_dir = Path(tempfile.gettempdir()) / "livepilot_forge"
83
+
84
+ # build_device does blocking filesystem I/O (mkdir + write_bytes). This is
85
+ # an async tool running on the event loop, so offload to a worker thread to
86
+ # avoid freezing all other handlers + the bridge UDP endpoint.
87
+ path = await asyncio.to_thread(build_device, spec, output_dir)
78
88
 
79
89
  return {
80
90
  "status": "created",
@@ -113,15 +123,11 @@ async def list_genexpr_templates(
113
123
  }
114
124
 
115
125
 
116
- @mcp.tool()
117
- async def install_m4l_device(
118
- ctx: Context,
119
- source_path: str,
120
- ) -> dict:
121
- """Copy a .amxd file to Ableton's User Library.
126
+ def _install_m4l_device_sync(source_path: str) -> dict:
127
+ """Blocking filesystem work for install_m4l_device (read/mkdir/write).
122
128
 
123
- Args:
124
- source_path: Path to the .amxd file to install
129
+ Run via asyncio.to_thread from the async tool below so the event loop
130
+ (and the bridge UDP endpoint) never stalls on disk I/O.
125
131
  """
126
132
  src = Path(source_path)
127
133
  if not src.exists():
@@ -160,3 +166,16 @@ async def install_m4l_device(
160
166
  "path": str(dest),
161
167
  "hint": "Device now available in Ableton's browser.",
162
168
  }
169
+
170
+
171
+ @mcp.tool()
172
+ async def install_m4l_device(
173
+ ctx: Context,
174
+ source_path: str,
175
+ ) -> dict:
176
+ """Copy a .amxd file to Ableton's User Library.
177
+
178
+ Args:
179
+ source_path: Path to the .amxd file to install
180
+ """
181
+ return await asyncio.to_thread(_install_m4l_device_sync, source_path)
@@ -17,7 +17,9 @@ The engine itself is pure orchestration logic.
17
17
 
18
18
  from __future__ import annotations
19
19
 
20
+ import asyncio
20
21
  import hashlib
22
+ import threading
21
23
  import time
22
24
  from typing import Optional
23
25
 
@@ -28,8 +30,15 @@ import logging
28
30
  logger = logging.getLogger(__name__)
29
31
 
30
32
  # ── In-memory experiment store ───────────────────────────────────────────────
33
+ #
34
+ # _EXPERIMENTS is mutated from both threadpooled sync tools (create_experiment)
35
+ # and event-loop async tools (run_experiment / commit_branch_async) — a
36
+ # module-level lock around the dict ops keeps inserts/reads race-free under
37
+ # mixed sync/async concurrent callers, mirroring preview_studio.engine and
38
+ # wonder_mode.session.
31
39
 
32
40
  _EXPERIMENTS: dict[str, ExperimentSet] = {}
41
+ _EXPERIMENTS_LOCK = threading.Lock()
33
42
 
34
43
 
35
44
  def _gen_id(prefix: str, seed: str) -> str:
@@ -89,7 +98,8 @@ def create_experiment_from_seeds(
89
98
  created_at_ms=now,
90
99
  )
91
100
 
92
- _EXPERIMENTS[exp_id] = experiment
101
+ with _EXPERIMENTS_LOCK:
102
+ _EXPERIMENTS[exp_id] = experiment
93
103
  return experiment
94
104
 
95
105
 
@@ -114,12 +124,15 @@ def create_experiment(
114
124
 
115
125
  def get_experiment(experiment_id: str) -> Optional[ExperimentSet]:
116
126
  """Get an experiment by ID."""
117
- return _EXPERIMENTS.get(experiment_id)
127
+ with _EXPERIMENTS_LOCK:
128
+ return _EXPERIMENTS.get(experiment_id)
118
129
 
119
130
 
120
131
  def list_experiments() -> list[dict]:
121
132
  """List all experiment sets."""
122
- return [exp.to_dict() for exp in _EXPERIMENTS.values()]
133
+ with _EXPERIMENTS_LOCK:
134
+ exps = list(_EXPERIMENTS.values())
135
+ return [exp.to_dict() for exp in exps]
123
136
 
124
137
  # ── Run experiments (requires Ableton connection) ────────────────────────────
125
138
 
@@ -211,12 +224,16 @@ async def run_branch_async(
211
224
  analyze_mix) are skipped in the apply pass — they're used for snapshot
212
225
  capture separately.
213
226
  """
214
- from ..runtime.execution_router import execute_plan_steps_async, filter_apply_steps
227
+ from ..runtime.execution_router import (
228
+ execute_plan_steps_async,
229
+ filter_apply_steps,
230
+ undo_remote_steps,
231
+ )
215
232
 
216
233
  branch.status = "running"
217
234
  branch.compiled_plan = compiled_plan
218
235
 
219
- branch.before_snapshot = capture_fn()
236
+ branch.before_snapshot = await asyncio.to_thread(capture_fn)
220
237
 
221
238
  # Filter out read-only verification steps from the apply pass (canonical
222
239
  # list lives in execution_router.READ_ONLY_TOOLS).
@@ -244,24 +261,13 @@ async def run_branch_async(
244
261
  ]
245
262
 
246
263
  steps_executed = sum(1 for r in exec_results if r.ok)
247
- # Only remote_command steps land on Ableton's linear undo stack. Bridge
248
- # (M4L/OSC) and mcp_tool mutations do NOT, so issuing one `undo` per
249
- # successful step over-undoes and walks back unrelated prior user edits.
250
- # Count undos from remote_command successes alone.
251
- undo_count = sum(
252
- 1 for r in exec_results if r.ok and r.backend == "remote_command"
253
- )
254
264
  branch.executed_at_ms = int(time.time() * 1000)
255
- branch.after_snapshot = capture_fn()
265
+ branch.after_snapshot = await asyncio.to_thread(capture_fn)
256
266
 
257
- # Undo only the remote_command steps back to checkpoint. Undo is itself a
258
- # remote_command, routed through the normal ableton.send_command path.
259
- for _ in range(undo_count):
260
- try:
261
- ableton.send_command("undo", {})
262
- except Exception as exc:
263
- logger.debug("run_branch_async failed: %s", exc)
264
- break
267
+ # Undo only the remote_command steps back to checkpoint (bridge/mcp_tool
268
+ # mutations don't land on Ableton's linear undo stack — see
269
+ # undo_remote_steps' docstring).
270
+ await undo_remote_steps(ableton, exec_results)
265
271
 
266
272
  # A branch is "evaluated" only if it actually applied at least one step.
267
273
  # If every step failed, mark it "failed" — this is the truth-release
@@ -442,7 +448,8 @@ def commit_branch(
442
448
 
443
449
  def discard_experiment(experiment_id: str) -> dict:
444
450
  """Discard an entire experiment set."""
445
- exp = _EXPERIMENTS.get(experiment_id)
451
+ with _EXPERIMENTS_LOCK:
452
+ exp = _EXPERIMENTS.get(experiment_id)
446
453
  if not exp:
447
454
  return {"error": f"Experiment {experiment_id} not found"}
448
455
 
@@ -10,6 +10,8 @@
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import asyncio
14
+ import functools
13
15
  import time
14
16
  from typing import Optional
15
17
 
@@ -351,7 +353,7 @@ async def run_experiment(
351
353
  if experiment.baseline_transport is None:
352
354
  from .baseline import capture_baseline
353
355
  try:
354
- experiment.baseline_transport = capture_baseline(ableton)
356
+ experiment.baseline_transport = await asyncio.to_thread(capture_baseline, ableton)
355
357
  except Exception as exc:
356
358
  logger.debug("baseline capture failed: %s", exc)
357
359
  experiment.baseline_transport = None
@@ -365,7 +367,8 @@ async def run_experiment(
365
367
  # Between branches (not before the first), restore the baseline so
366
368
  # the next before_snapshot reads from the same reference state.
367
369
  if pending_seen > 0:
368
- engine.prepare_for_next_branch(
370
+ await asyncio.to_thread(
371
+ engine.prepare_for_next_branch,
369
372
  ableton, experiment.baseline_transport, stabilize_ms=300,
370
373
  )
371
374
  pending_seen += 1
@@ -414,7 +417,7 @@ async def run_experiment(
414
417
  results.append(branch.to_dict())
415
418
  continue
416
419
 
417
- session_info = ableton.send_command("get_session_info")
420
+ session_info = await ableton.send_command_async("get_session_info")
418
421
  kernel = {"session_info": session_info, "mode": "explore"}
419
422
  plan = compiler.compile(move, kernel)
420
423
  compiled_dict = plan.to_dict()
@@ -422,12 +425,19 @@ async def run_experiment(
422
425
  # Pick the capture function — render-verify mode captures audio
423
426
  # and extracts a TimbralFingerprint, adding latency but giving
424
427
  # classify_branch_outcome real measurable evidence.
428
+ #
429
+ # NOTE: bound via functools.partial (not a lambda) so the blocking
430
+ # `_capture_snapshot*` call is never a "bare" call site in this
431
+ # function's body — it is only ever invoked downstream inside
432
+ # `engine.run_branch_async` via `await asyncio.to_thread(capture_fn)`,
433
+ # which is where the actual off-loop offload happens.
425
434
  if render_verify:
426
- capture_fn = lambda: _capture_snapshot_with_render_verify(
435
+ capture_fn = functools.partial(
436
+ _capture_snapshot_with_render_verify,
427
437
  ctx, duration_seconds=render_duration_seconds,
428
438
  )
429
439
  else:
430
- capture_fn = lambda: _capture_snapshot(ctx)
440
+ capture_fn = functools.partial(_capture_snapshot, ctx)
431
441
 
432
442
  # Run the branch through the async router
433
443
  await engine.run_branch_async(