livepilot 1.19.1 → 1.20.1
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 +192 -0
- 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/memory/session_memory.py +21 -2
- package/mcp_server/memory/tools.py +7 -1
- package/mcp_server/runtime/execution_router.py +9 -0
- package/mcp_server/runtime/mcp_dispatch.py +21 -0
- package/mcp_server/semantic_moves/__init__.py +8 -0
- package/mcp_server/semantic_moves/content_compilers.py +174 -0
- package/mcp_server/semantic_moves/content_moves.py +87 -0
- package/mcp_server/semantic_moves/device_mutation_compilers.py +157 -0
- package/mcp_server/semantic_moves/device_mutation_moves.py +94 -0
- package/mcp_server/semantic_moves/metadata_compilers.py +230 -0
- package/mcp_server/semantic_moves/metadata_moves.py +126 -0
- package/mcp_server/semantic_moves/routing_compilers.py +229 -0
- package/mcp_server/semantic_moves/routing_moves.py +109 -0
- package/mcp_server/semantic_moves/tools.py +57 -3
- package/package.json +1 -1
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,197 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.1 — CI hardening: Windows UTF-8 encoding + .amxd ping drift (April 24 2026)
|
|
4
|
+
|
|
5
|
+
Patch release fixing CI regressions that v1.20.0 shipped with (caught
|
|
6
|
+
by the actual CI run post-tag). No runtime behavior changes — tests
|
|
7
|
+
pass on every platform, and the .amxd ping now matches the repo
|
|
8
|
+
version. Zero new tests (both fixes are mechanical), zero regressions.
|
|
9
|
+
|
|
10
|
+
### Fixes
|
|
11
|
+
|
|
12
|
+
- **Windows-only `UnicodeDecodeError` in `tests/test_creative_director.py`.**
|
|
13
|
+
27 bare `Path.read_text()` calls used the locale default encoding.
|
|
14
|
+
On Windows that's cp1252, which chokes on the em-dashes (—), arrows
|
|
15
|
+
(→), and smart quotes v1.20 added to `SKILL.md` and the new
|
|
16
|
+
`phase-6-execution.md` reference. All 27 calls now pass
|
|
17
|
+
`encoding="utf-8"` explicitly. Cross-platform hygiene, applies
|
|
18
|
+
beyond the trigger case — any future markdown additions with
|
|
19
|
+
non-ASCII chars are now safe on Windows.
|
|
20
|
+
|
|
21
|
+
- **`LivePilot_Analyzer.amxd` ping reported v1.17.5 for 9 releases.**
|
|
22
|
+
The CI `amxd-freeze-drift` job has been red since v1.18.0 because
|
|
23
|
+
nobody re-froze the .amxd in Max Editor after the JS source bumps.
|
|
24
|
+
Users of v1.18.0-v1.20.0 got `{ok: true, version: "1.17.5"}` from
|
|
25
|
+
the bridge ping regardless of the installed version. Per
|
|
26
|
+
`feedback_amxd_safe_binary_patch` memory, same-length version
|
|
27
|
+
strings can be patched in-place without Max re-export. Both
|
|
28
|
+
occurrences in the binary updated to 1.20.1 (file size unchanged);
|
|
29
|
+
the JS source `livepilot_bridge.js` VERSION constant also bumped
|
|
30
|
+
so future clean freezes stay consistent.
|
|
31
|
+
|
|
32
|
+
### CI status after this release
|
|
33
|
+
|
|
34
|
+
| Job | Pre-v1.20.1 | v1.20.1 |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| python-tests (ubuntu, macos) × {3.11, 3.12} | ✓ | ✓ |
|
|
37
|
+
| python-tests (windows) × {3.11, 3.12} | ✗ UnicodeDecodeError | ✓ |
|
|
38
|
+
| metadata-drift | ✓ | ✓ |
|
|
39
|
+
| amxd-freeze-drift | ✗ stuck at 1.17.5 | ✓ |
|
|
40
|
+
| js-entrypoint | ✓ | ✓ |
|
|
41
|
+
|
|
42
|
+
## 1.20.0 — Item C phased cutover: 10 new semantic moves + Director Phase 6 rewrite (April 24 2026)
|
|
43
|
+
|
|
44
|
+
Implements the plan in `docs/plans/v1.20-structural-plan.md`. Ships 10
|
|
45
|
+
new semantic moves across four family-themed commits, rewrites the
|
|
46
|
+
creative director's Phase 6 to make `apply_semantic_move` the default
|
|
47
|
+
execution surface with a documented + tracked escape hatch, and hardens
|
|
48
|
+
three systemic issues surfaced during live pressure testing. Registry:
|
|
49
|
+
33 → 43 moves across the same 7 canonical families. Full test suite:
|
|
50
|
+
2858 → 2985 pass (+127, zero regressions).
|
|
51
|
+
|
|
52
|
+
### New semantic moves (10 total)
|
|
53
|
+
|
|
54
|
+
**Routing family (`mcp_server/semantic_moves/routing_moves.py`):**
|
|
55
|
+
- `build_send_chain` (device_creation, medium risk) — load an ordered
|
|
56
|
+
chain of devices onto a return track; the Basic Channel / dub-techno
|
|
57
|
+
/ ambient send-architecture primitive. `protect: low_end=0.6`.
|
|
58
|
+
- `configure_send_architecture` (mix, low risk) — set send levels
|
|
59
|
+
across multiple source tracks in one move.
|
|
60
|
+
- `set_track_routing` (mix, medium risk) — rewire a track's output
|
|
61
|
+
routing, e.g. "Sends Only" for bus architectures.
|
|
62
|
+
|
|
63
|
+
**Device-mutation family:**
|
|
64
|
+
- `configure_device` (sound_design, low risk) — bulk-configure N
|
|
65
|
+
parameters on an existing device in a single undoable move. Takes
|
|
66
|
+
`param_overrides: dict` (preset library deferred to v1.21).
|
|
67
|
+
- `remove_device` (sound_design, medium risk, protects
|
|
68
|
+
`signal_integrity=0.9`) — destructive removal with a required
|
|
69
|
+
`reason` string auto-logged to session memory for audit.
|
|
70
|
+
|
|
71
|
+
**Content family:**
|
|
72
|
+
- `load_chord_source` (sound_design, low risk) — create+voice+name a
|
|
73
|
+
MIDI chord clip in one move; feeds `build_send_chain` return chains.
|
|
74
|
+
- `create_drum_rack_pad` (device_creation, low risk) — add one pad to
|
|
75
|
+
a Drum Rack, Dilla-style kit-at-a-time.
|
|
76
|
+
|
|
77
|
+
**Metadata family:**
|
|
78
|
+
- `configure_groove` (arrangement, low risk) — the Dilla-swing
|
|
79
|
+
primitive; assigns a groove + optionally tunes its timing_amount.
|
|
80
|
+
- `set_scene_metadata` (arrangement, low risk) — conditional
|
|
81
|
+
name/color/tempo in one move.
|
|
82
|
+
- `set_track_metadata` (mix, low risk) — bundled rename + color, since
|
|
83
|
+
the two are always paired in Phase 6 usage.
|
|
84
|
+
|
|
85
|
+
### Director SKILL — Phase 6 rewrite
|
|
86
|
+
|
|
87
|
+
- **Decision table (authoritative):** each uncovered-pattern row now
|
|
88
|
+
points at a specific v1.20 move (e.g. "Set multiple params on a
|
|
89
|
+
device" → `configure_device`). 10 NEW rows marked explicitly.
|
|
90
|
+
- **Default execution surface**: `apply_semantic_move` +
|
|
91
|
+
`commit_experiment`, replacing the pre-v1.20 "raw tools + manual
|
|
92
|
+
`add_session_memory(move_executed)` marker" pattern.
|
|
93
|
+
- **Escape hatch policy** (v1.20 transitional state): when no move
|
|
94
|
+
covers the pattern, raw-tool execution is permitted only with the
|
|
95
|
+
three-call discipline — the raw call, an `add_session_memory(
|
|
96
|
+
category="move_executed")` marker, AND an `add_session_memory(
|
|
97
|
+
category="tech_debt")` log naming the uncovered pattern. Both
|
|
98
|
+
categories are mandatory; they serve different consumers (ledger
|
|
99
|
+
vs release planning).
|
|
100
|
+
- **New reference doc** `phase-6-execution.md` (349 lines) — full
|
|
101
|
+
contract (seed_args, compiled steps, risk/protect, typical caller)
|
|
102
|
+
for each of the 10 moves, plus a worked escape-hatch example.
|
|
103
|
+
|
|
104
|
+
### Architectural extension (commit 1)
|
|
105
|
+
|
|
106
|
+
`apply_semantic_move(args: dict)` and `preview_semantic_move(args: dict)`
|
|
107
|
+
now accept user seed parameters that flow into the compiler's kernel as
|
|
108
|
+
`kernel["seed_args"]`. Pre-v1.20 moves are unaffected (they read only
|
|
109
|
+
from `session_info`); the new routing/content/metadata moves read from
|
|
110
|
+
`seed_args` for user targets like `return_track_index`, `device_chain`,
|
|
111
|
+
`notes`, `track_index`, etc.
|
|
112
|
+
|
|
113
|
+
### Live-test hardening (bugs caught during the 6 pressure-test gate)
|
|
114
|
+
|
|
115
|
+
**Wire-format compiler fix.** `configure_device` and `set_track_routing`
|
|
116
|
+
initially emitted MCP-tool-input keys (`parameter_name`,
|
|
117
|
+
`output_routing_type`) which the MCP tool layer would normalize — but
|
|
118
|
+
compiled plans use the `remote_command` backend that goes directly to
|
|
119
|
+
`ableton.send_command()`, bypassing the MCP tool entirely. Ableton's
|
|
120
|
+
Remote Script reads wire-format keys (`name_or_index`, `output_type`)
|
|
121
|
+
exclusively. Fix: both compilers emit wire format. New regression suite
|
|
122
|
+
`tests/test_compiler_wire_format_parity.py` — 10 parametrized cases,
|
|
123
|
+
one per v1.20 move, asserting every compiled step's params match the
|
|
124
|
+
Remote Script handler's actual key inventory.
|
|
125
|
+
|
|
126
|
+
**Automatic ledger write.** `apply_semantic_move` in explore mode now
|
|
127
|
+
writes a LedgerEntry to `SessionLedger` (family, intent, per-step
|
|
128
|
+
actions, provisional `kept=True`, `score = success_fraction`). Returns
|
|
129
|
+
a `ledger_entry_id` in the response so callers can correlate with
|
|
130
|
+
post-hoc `evaluate_move` evaluation. Pre-v1.20 docs pointed
|
|
131
|
+
anti-repetition at `memory_list` which actually reads the persistent
|
|
132
|
+
technique library — wrong store. Director SKILL now points at
|
|
133
|
+
`get_action_ledger_summary`. `commit_experiment` auto-ledger is v1.21
|
|
134
|
+
scope.
|
|
135
|
+
|
|
136
|
+
**Session memory categories.** `_VALID_CATEGORIES` in
|
|
137
|
+
`mcp_server/memory/session_memory.py` now includes the three v1.20
|
|
138
|
+
director Phase 6 categories: `move_executed`, `tech_debt`, and
|
|
139
|
+
`override`. Pre-v1.20 categories preserved (backward compat);
|
|
140
|
+
arbitrary strings still rejected. 7 new contract tests.
|
|
141
|
+
|
|
142
|
+
### New tests (+127 across the release)
|
|
143
|
+
|
|
144
|
+
- `tests/test_registry_uniqueness.py` (4) — guard against dict-insertion
|
|
145
|
+
collisions; baseline move count.
|
|
146
|
+
- `tests/test_apply_semantic_move_args.py` (13) — seed_args threading +
|
|
147
|
+
ledger-write contract for each mode.
|
|
148
|
+
- `tests/test_routing_moves.py` (21) — per-move + cross-family.
|
|
149
|
+
- `tests/test_device_mutation_moves.py` (15).
|
|
150
|
+
- `tests/test_content_moves.py` (14).
|
|
151
|
+
- `tests/test_metadata_moves.py` (15).
|
|
152
|
+
- `tests/test_director_move_coverage.py` (8) — SKILL ↔ registry drift
|
|
153
|
+
detection; phase-6-execution.md contract coverage.
|
|
154
|
+
- `tests/test_compiler_wire_format_parity.py` (10) — wire-format
|
|
155
|
+
invariant across all 10 v1.20 moves.
|
|
156
|
+
- `tests/test_v1_20_session_memory_categories.py` (7) — allowlist
|
|
157
|
+
contract.
|
|
158
|
+
- Various in-place additions (execution_router / mcp_dispatch
|
|
159
|
+
classifier entries for `add_session_memory` and `add_drum_rack_pad`;
|
|
160
|
+
test_device_creation_moves invariant generalized to admit
|
|
161
|
+
device-loading moves alongside Device Forge moves).
|
|
162
|
+
|
|
163
|
+
### Live pressure-test results (the 6 plan §5 scenarios, all passing)
|
|
164
|
+
|
|
165
|
+
1. `build_send_chain` on Return A with Echo + Auto Filter + Hybrid
|
|
166
|
+
Reverb — 4 steps, 4 successes.
|
|
167
|
+
2. `configure_device` on the Reverb with dub-cathedral overrides
|
|
168
|
+
(Decay 25.5s, Room Size 339.89, Dry/Wet 40%, Predelay 8.19ms,
|
|
169
|
+
Diffusion 77%) — 5 params set in one batch_set_parameters call.
|
|
170
|
+
3. `configure_send_architecture` on track 0 → Send A at 0.4 — single
|
|
171
|
+
step, success.
|
|
172
|
+
4. `load_chord_source` on track 0 slot 0 with a C minor 7 voicing —
|
|
173
|
+
create_clip + add_notes + set_clip_name, 3/3 success.
|
|
174
|
+
5. 4 moves in sequence → `get_action_ledger_summary` returns 4 entries
|
|
175
|
+
(engine=semantic_moves, mix+arrangement families), zero `tech_debt`
|
|
176
|
+
entries — automatic ledger write confirmed end-to-end.
|
|
177
|
+
6. Escape hatch: raw `set_track_arm(track=2, armed=true)` + both
|
|
178
|
+
mandatory `add_session_memory` markers; `get_session_memory(
|
|
179
|
+
category="tech_debt")` returns the log entry as expected.
|
|
180
|
+
|
|
181
|
+
### Scope / non-goals
|
|
182
|
+
|
|
183
|
+
Not in v1.20, explicitly deferred:
|
|
184
|
+
- Hard cutover (closing the escape hatch). v1.21 target, conditional
|
|
185
|
+
on zero `tech_debt` entries over one month of production use.
|
|
186
|
+
- Preset YAML library for `configure_device`. The move's
|
|
187
|
+
`param_overrides` dict already accepts a pre-resolved preset; the
|
|
188
|
+
library is the layer that produces those dicts.
|
|
189
|
+
- `commit_experiment` automatic ledger write. Tracked as tech-debt.
|
|
190
|
+
- Rewriting the existing 33 moves to a new shape. v1.22+.
|
|
191
|
+
- Director Phase 6 compiler that picks moves automatically from user
|
|
192
|
+
intent. Current Phase 6 is user/director-selected; auto-selection
|
|
193
|
+
is a separate feature.
|
|
194
|
+
|
|
3
195
|
## 1.19.1 — v1.19.0 polish (April 24 2026)
|
|
4
196
|
|
|
5
197
|
Patch release addressing the three "Known gaps" documented at the
|
|
Binary file
|
|
@@ -34,7 +34,7 @@ outlets = 2; // 0: to udpsend (responses), 1: to buffer~/status
|
|
|
34
34
|
// Single source of truth for the bridge version — bumped alongside the
|
|
35
35
|
// rest of the release manifest. Surfaced in the UI via messnamed("livepilot_version", ...)
|
|
36
36
|
// so the frozen .amxd visibly reports which build it was last exported from.
|
|
37
|
-
var VERSION = "1.
|
|
37
|
+
var VERSION = "1.20.1";
|
|
38
38
|
|
|
39
39
|
// ── State ──────────────────────────────────────────────────────────────────
|
|
40
40
|
|
package/mcp_server/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""LivePilot MCP Server — bridges MCP protocol to Ableton Live."""
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.20.1"
|
|
@@ -11,7 +11,23 @@ import uuid
|
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
from typing import Optional
|
|
13
13
|
|
|
14
|
-
_VALID_CATEGORIES = {
|
|
14
|
+
_VALID_CATEGORIES = {
|
|
15
|
+
# Pre-v1.20 categories — observations, working hypotheses, decisions, issues.
|
|
16
|
+
"observation",
|
|
17
|
+
"hypothesis",
|
|
18
|
+
"decision",
|
|
19
|
+
"issue",
|
|
20
|
+
# v1.20 categories — creative director Phase 6 escape-hatch discipline.
|
|
21
|
+
# The director must write BOTH of these when executing a raw-tool call
|
|
22
|
+
# that no semantic move covers. See
|
|
23
|
+
# livepilot/skills/livepilot-creative-director/references/phase-6-execution.md
|
|
24
|
+
# §escape-hatch policy for the three-call contract.
|
|
25
|
+
"move_executed", # ledger marker (consumed by anti-repetition)
|
|
26
|
+
"tech_debt", # "pattern should be a semantic move" (consumed by release planning)
|
|
27
|
+
# v1.20 director override log — explicit user decisions to proceed
|
|
28
|
+
# despite a check_brief_compliance violation.
|
|
29
|
+
"override",
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
|
|
17
33
|
@dataclass
|
|
@@ -20,7 +36,10 @@ class SessionMemoryEntry:
|
|
|
20
36
|
|
|
21
37
|
id: str
|
|
22
38
|
timestamp_ms: int
|
|
23
|
-
|
|
39
|
+
# Allowed categories are declared in _VALID_CATEGORIES (module level).
|
|
40
|
+
# Current set: observation / hypothesis / decision / issue (pre-v1.20)
|
|
41
|
+
# plus move_executed / tech_debt / override (v1.20 director Phase 6).
|
|
42
|
+
category: str
|
|
24
43
|
content: str
|
|
25
44
|
engine: str # which engine created this
|
|
26
45
|
confidence: float
|
|
@@ -93,7 +93,13 @@ def get_session_memory(
|
|
|
93
93
|
def add_session_memory(
|
|
94
94
|
ctx: Context, category: str, content: str, engine: str = "agent_os"
|
|
95
95
|
) -> dict:
|
|
96
|
-
"""Add an ephemeral session memory entry
|
|
96
|
+
"""Add an ephemeral session memory entry.
|
|
97
|
+
|
|
98
|
+
Categories:
|
|
99
|
+
- observation / hypothesis / decision / issue (pre-v1.20)
|
|
100
|
+
- move_executed, tech_debt, override (v1.20 director Phase 6 —
|
|
101
|
+
escape-hatch discipline + anti-pattern override logging)
|
|
102
|
+
"""
|
|
97
103
|
store = _get_session_memory(ctx)
|
|
98
104
|
try:
|
|
99
105
|
entry_id = store.add(category=category, content=content, engine=engine)
|
|
@@ -54,6 +54,15 @@ MCP_TOOLS: frozenset[str] = frozenset({
|
|
|
54
54
|
"set_miditool_target",
|
|
55
55
|
"get_miditool_context",
|
|
56
56
|
"list_miditool_generators",
|
|
57
|
+
# Session memory writes (v1.20) — MCP-side store in mcp_server/memory/tools.py.
|
|
58
|
+
# No TCP round-trip. Used by remove_device to audit destructive ops + by
|
|
59
|
+
# the director's escape-hatch tech_debt logging.
|
|
60
|
+
"add_session_memory",
|
|
61
|
+
# Drum-rack pad construction (v1.20) — async orchestrator in
|
|
62
|
+
# mcp_server/tools/analyzer.py:775 that composes insert_rack_chain +
|
|
63
|
+
# set_drum_chain_note + insert_device + replace_sample_native. Used by
|
|
64
|
+
# the create_drum_rack_pad semantic move.
|
|
65
|
+
"add_drum_rack_pad",
|
|
57
66
|
})
|
|
58
67
|
|
|
59
68
|
|
|
@@ -122,6 +122,22 @@ async def _list_miditool_generators(params: dict, ctx: Any = None) -> dict:
|
|
|
122
122
|
return await _call(list_miditool_generators, ctx, params)
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
# ── Session memory writes (v1.20) ─────────────────────────────────────────
|
|
126
|
+
#
|
|
127
|
+
# remove_device emits an add_session_memory step to log its audit reason.
|
|
128
|
+
# Director Phase 6's escape hatch also writes tech_debt entries through
|
|
129
|
+
# this path. In-process — no TCP, no bridge.
|
|
130
|
+
|
|
131
|
+
async def _add_session_memory(params: dict, ctx: Any = None) -> dict:
|
|
132
|
+
from ..memory.tools import add_session_memory
|
|
133
|
+
return await _call(add_session_memory, ctx, params)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
async def _add_drum_rack_pad(params: dict, ctx: Any = None) -> dict:
|
|
137
|
+
from ..tools.analyzer import add_drum_rack_pad
|
|
138
|
+
return await _call(add_drum_rack_pad, ctx, params)
|
|
139
|
+
|
|
140
|
+
|
|
125
141
|
def build_mcp_dispatch_registry() -> dict[str, Callable]:
|
|
126
142
|
"""Return the canonical registry of MCP-only tools for plan execution.
|
|
127
143
|
|
|
@@ -147,4 +163,9 @@ def build_mcp_dispatch_registry() -> dict[str, Callable]:
|
|
|
147
163
|
"set_miditool_target": _set_miditool_target,
|
|
148
164
|
"get_miditool_context": _get_miditool_context,
|
|
149
165
|
"list_miditool_generators": _list_miditool_generators,
|
|
166
|
+
# v1.20 — session memory writes for remove_device audit + director
|
|
167
|
+
# escape-hatch tech_debt logging.
|
|
168
|
+
"add_session_memory": _add_session_memory,
|
|
169
|
+
# v1.20 — drum rack pad construction (async orchestrator).
|
|
170
|
+
"add_drum_rack_pad": _add_drum_rack_pad,
|
|
150
171
|
}
|
|
@@ -6,6 +6,10 @@ from . import transition_moves # noqa: F401
|
|
|
6
6
|
from . import sound_design_moves # noqa: F401
|
|
7
7
|
from . import performance_moves # noqa: F401
|
|
8
8
|
from . import device_creation_moves # noqa: F401
|
|
9
|
+
from . import routing_moves # noqa: F401 (v1.20)
|
|
10
|
+
from . import device_mutation_moves # noqa: F401 (v1.20)
|
|
11
|
+
from . import content_moves # noqa: F401 (v1.20)
|
|
12
|
+
from . import metadata_moves # noqa: F401 (v1.20)
|
|
9
13
|
from ..sample_engine import moves as sample_moves # noqa: F401
|
|
10
14
|
|
|
11
15
|
# Import compilers to auto-register them
|
|
@@ -15,3 +19,7 @@ from . import sound_design_compilers # noqa: F401
|
|
|
15
19
|
from . import performance_compilers # noqa: F401
|
|
16
20
|
from . import sample_compilers # noqa: F401
|
|
17
21
|
from . import device_creation_compilers # noqa: F401
|
|
22
|
+
from . import routing_compilers # noqa: F401 (v1.20)
|
|
23
|
+
from . import device_mutation_compilers # noqa: F401 (v1.20)
|
|
24
|
+
from . import content_compilers # noqa: F401 (v1.20)
|
|
25
|
+
from . import metadata_compilers # noqa: F401 (v1.20)
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Compilers for content-family semantic moves (v1.20).
|
|
2
|
+
|
|
3
|
+
Pure functions. Validate seed_args rigorously and emit steps whose params
|
|
4
|
+
match the underlying tool signatures exactly (clip_index vs clip_slot
|
|
5
|
+
naming carefully preserved — mcp_server/tools/clips.py:117 uses
|
|
6
|
+
``clip_index``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .compiler import CompiledPlan, CompiledStep, register_compiler
|
|
12
|
+
from .models import SemanticMove
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_DEFAULT_CLIP_LENGTH_BEATS = 4.0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _empty_plan(move: SemanticMove, warnings: list[str]) -> CompiledPlan:
|
|
19
|
+
return CompiledPlan(
|
|
20
|
+
move_id=move.move_id,
|
|
21
|
+
intent=move.intent,
|
|
22
|
+
steps=[],
|
|
23
|
+
risk_level=move.risk_level,
|
|
24
|
+
summary="; ".join(warnings) if warnings else "No plan compiled",
|
|
25
|
+
requires_approval=True,
|
|
26
|
+
warnings=warnings,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── load_chord_source ─────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _compile_load_chord_source(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
34
|
+
args = kernel.get("seed_args") or {}
|
|
35
|
+
track_index = args.get("track_index")
|
|
36
|
+
clip_slot = args.get("clip_slot")
|
|
37
|
+
notes = args.get("notes")
|
|
38
|
+
name = args.get("name")
|
|
39
|
+
length_beats = args.get("length_beats", _DEFAULT_CLIP_LENGTH_BEATS)
|
|
40
|
+
|
|
41
|
+
if track_index is None or clip_slot is None or notes is None or name is None:
|
|
42
|
+
return _empty_plan(move, [
|
|
43
|
+
"load_chord_source requires seed_args.track_index + clip_slot + notes + name"
|
|
44
|
+
])
|
|
45
|
+
if not isinstance(track_index, int) or not isinstance(clip_slot, int):
|
|
46
|
+
return _empty_plan(move, [
|
|
47
|
+
"track_index and clip_slot must be ints"
|
|
48
|
+
])
|
|
49
|
+
if clip_slot < 0:
|
|
50
|
+
return _empty_plan(move, [f"clip_slot must be non-negative, got {clip_slot}"])
|
|
51
|
+
if not isinstance(notes, (list, tuple)) or not notes:
|
|
52
|
+
return _empty_plan(move, ["notes must be a non-empty list of note dicts"])
|
|
53
|
+
if not isinstance(name, str) or not name.strip():
|
|
54
|
+
return _empty_plan(move, ["name must be a non-empty string"])
|
|
55
|
+
try:
|
|
56
|
+
length_f = float(length_beats)
|
|
57
|
+
except (TypeError, ValueError):
|
|
58
|
+
return _empty_plan(move, [f"length_beats must be numeric, got {length_beats!r}"])
|
|
59
|
+
if length_f <= 0:
|
|
60
|
+
return _empty_plan(move, [f"length_beats must be > 0, got {length_f}"])
|
|
61
|
+
|
|
62
|
+
# Step order matters: create_clip must land before add_notes, and the
|
|
63
|
+
# name step runs last so mid-pass inspection sees the real name, not
|
|
64
|
+
# a default "Clip" Live would assign.
|
|
65
|
+
create_step = CompiledStep(
|
|
66
|
+
tool="create_clip",
|
|
67
|
+
params={
|
|
68
|
+
"track_index": track_index,
|
|
69
|
+
"clip_index": clip_slot,
|
|
70
|
+
"length": length_f,
|
|
71
|
+
},
|
|
72
|
+
description=f"Create MIDI clip at track {track_index} slot {clip_slot} ({length_f} beats)",
|
|
73
|
+
verify_after=True,
|
|
74
|
+
backend="remote_command",
|
|
75
|
+
)
|
|
76
|
+
add_step = CompiledStep(
|
|
77
|
+
tool="add_notes",
|
|
78
|
+
params={
|
|
79
|
+
"track_index": track_index,
|
|
80
|
+
"clip_index": clip_slot,
|
|
81
|
+
"notes": list(notes),
|
|
82
|
+
},
|
|
83
|
+
description=f"Add {len(notes)} note(s) to clip",
|
|
84
|
+
verify_after=True,
|
|
85
|
+
backend="remote_command",
|
|
86
|
+
)
|
|
87
|
+
name_step = CompiledStep(
|
|
88
|
+
tool="set_clip_name",
|
|
89
|
+
params={
|
|
90
|
+
"track_index": track_index,
|
|
91
|
+
"clip_index": clip_slot,
|
|
92
|
+
"name": name.strip(),
|
|
93
|
+
},
|
|
94
|
+
description=f"Name clip '{name.strip()}'",
|
|
95
|
+
verify_after=True,
|
|
96
|
+
backend="remote_command",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return CompiledPlan(
|
|
100
|
+
move_id=move.move_id,
|
|
101
|
+
intent=move.intent,
|
|
102
|
+
steps=[create_step, add_step, name_step],
|
|
103
|
+
risk_level=move.risk_level,
|
|
104
|
+
summary=(
|
|
105
|
+
f"Load chord source '{name.strip()}' at track {track_index} slot "
|
|
106
|
+
f"{clip_slot} ({len(notes)} notes, {length_f} beats)"
|
|
107
|
+
),
|
|
108
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
109
|
+
warnings=[],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── create_drum_rack_pad ──────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _compile_create_drum_rack_pad(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
117
|
+
args = kernel.get("seed_args") or {}
|
|
118
|
+
track_index = args.get("track_index")
|
|
119
|
+
pad_note = args.get("pad_note")
|
|
120
|
+
file_path = args.get("file_path")
|
|
121
|
+
rack_device_index = args.get("rack_device_index") # optional
|
|
122
|
+
chain_name = args.get("chain_name") # optional
|
|
123
|
+
|
|
124
|
+
if track_index is None or pad_note is None or file_path is None:
|
|
125
|
+
return _empty_plan(move, [
|
|
126
|
+
"create_drum_rack_pad requires seed_args.track_index + pad_note + file_path"
|
|
127
|
+
])
|
|
128
|
+
if not isinstance(track_index, int) or not isinstance(pad_note, int):
|
|
129
|
+
return _empty_plan(move, ["track_index and pad_note must be ints"])
|
|
130
|
+
if not 0 <= pad_note <= 127:
|
|
131
|
+
return _empty_plan(move, [f"pad_note must be MIDI 0-127, got {pad_note}"])
|
|
132
|
+
if not isinstance(file_path, str) or not file_path.strip():
|
|
133
|
+
return _empty_plan(move, ["file_path must be a non-empty absolute path string"])
|
|
134
|
+
if rack_device_index is not None and not isinstance(rack_device_index, int):
|
|
135
|
+
return _empty_plan(move, ["rack_device_index (when provided) must be int"])
|
|
136
|
+
if chain_name is not None and not isinstance(chain_name, str):
|
|
137
|
+
return _empty_plan(move, ["chain_name (when provided) must be str"])
|
|
138
|
+
|
|
139
|
+
params: dict = {
|
|
140
|
+
"track_index": track_index,
|
|
141
|
+
"pad_note": pad_note,
|
|
142
|
+
"file_path": file_path,
|
|
143
|
+
}
|
|
144
|
+
if rack_device_index is not None:
|
|
145
|
+
params["rack_device_index"] = rack_device_index
|
|
146
|
+
if chain_name is not None:
|
|
147
|
+
params["chain_name"] = chain_name
|
|
148
|
+
|
|
149
|
+
step = CompiledStep(
|
|
150
|
+
tool="add_drum_rack_pad",
|
|
151
|
+
params=params,
|
|
152
|
+
description=(
|
|
153
|
+
f"Add drum rack pad on track {track_index}: MIDI note {pad_note} → "
|
|
154
|
+
f"{file_path.rsplit('/', 1)[-1]}"
|
|
155
|
+
),
|
|
156
|
+
verify_after=True,
|
|
157
|
+
backend="mcp_tool",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return CompiledPlan(
|
|
161
|
+
move_id=move.move_id,
|
|
162
|
+
intent=move.intent,
|
|
163
|
+
steps=[step],
|
|
164
|
+
risk_level=move.risk_level,
|
|
165
|
+
summary=step.description,
|
|
166
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
167
|
+
warnings=[],
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# ── Register compilers ────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
register_compiler("load_chord_source", _compile_load_chord_source)
|
|
174
|
+
register_compiler("create_drum_rack_pad", _compile_create_drum_rack_pad)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Content-domain semantic moves (v1.20) — create MIDI chord-source clips
|
|
2
|
+
and build drum rack pads one at a time.
|
|
3
|
+
|
|
4
|
+
Both moves take user targets via ``kernel["seed_args"]``. Content moves
|
|
5
|
+
complement the routing family: load_chord_source produces the source clip
|
|
6
|
+
that a build_send_chain return chain processes; create_drum_rack_pad is
|
|
7
|
+
the one-pad-at-a-time primitive for programming kits à la Dilla's
|
|
8
|
+
isolated-voice workflow.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .models import SemanticMove
|
|
12
|
+
from .registry import register
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
LOAD_CHORD_SOURCE = SemanticMove(
|
|
16
|
+
move_id="load_chord_source",
|
|
17
|
+
family="sound_design",
|
|
18
|
+
intent=(
|
|
19
|
+
"Create a named MIDI chord clip in a specific slot — the single-source "
|
|
20
|
+
"feed for dub / ambient send architectures. Takes track_index, "
|
|
21
|
+
"clip_slot, notes (list of {pitch, start_time, duration, velocity}), "
|
|
22
|
+
"name, and optional length_beats (default 4.0) via seed_args."
|
|
23
|
+
),
|
|
24
|
+
targets={"harmonic": 0.4, "depth": 0.3, "clarity": 0.3},
|
|
25
|
+
protect={"cohesion": 0.6},
|
|
26
|
+
risk_level="low",
|
|
27
|
+
plan_template=[
|
|
28
|
+
{
|
|
29
|
+
"tool": "create_clip",
|
|
30
|
+
"params": {"description": "Empty MIDI clip of length_beats"},
|
|
31
|
+
"description": "Create empty MIDI clip",
|
|
32
|
+
"backend": "remote_command",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"tool": "add_notes",
|
|
36
|
+
"params": {"description": "Add the chord voicing notes"},
|
|
37
|
+
"description": "Add chord voicing",
|
|
38
|
+
"backend": "remote_command",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"tool": "set_clip_name",
|
|
42
|
+
"params": {"description": "Name the clip so it's identifiable"},
|
|
43
|
+
"description": "Name the clip",
|
|
44
|
+
"backend": "remote_command",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
verification_plan=[
|
|
48
|
+
{
|
|
49
|
+
"tool": "get_clip_info",
|
|
50
|
+
"check": "clip exists at track/slot, has expected name + note count",
|
|
51
|
+
"backend": "remote_command",
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
CREATE_DRUM_RACK_PAD_MOVE = SemanticMove(
|
|
57
|
+
move_id="create_drum_rack_pad",
|
|
58
|
+
family="device_creation",
|
|
59
|
+
intent=(
|
|
60
|
+
"Add one pad to a Drum Rack — kick, snare, hat, etc. Takes "
|
|
61
|
+
"track_index, pad_note (MIDI 0-127), file_path (absolute), and "
|
|
62
|
+
"optional rack_device_index + chain_name via seed_args. Wraps the "
|
|
63
|
+
"Live 12.4 native replace_sample_native flow."
|
|
64
|
+
),
|
|
65
|
+
targets={"groove": 0.5, "punch": 0.3, "contrast": 0.2},
|
|
66
|
+
protect={"cohesion": 0.6},
|
|
67
|
+
risk_level="low",
|
|
68
|
+
plan_template=[
|
|
69
|
+
{
|
|
70
|
+
"tool": "add_drum_rack_pad",
|
|
71
|
+
"params": {"description": "Single atomic pad build + sample load"},
|
|
72
|
+
"description": "Build drum rack pad",
|
|
73
|
+
"backend": "mcp_tool",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
verification_plan=[
|
|
77
|
+
{
|
|
78
|
+
"tool": "get_rack_chains",
|
|
79
|
+
"check": "new chain exists on the rack, trigger note matches pad_note",
|
|
80
|
+
"backend": "remote_command",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
for _move in (LOAD_CHORD_SOURCE, CREATE_DRUM_RACK_PAD_MOVE):
|
|
87
|
+
register(_move)
|