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.
- package/CHANGELOG.md +207 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_director/hybrid.py +11 -2
- package/mcp_server/experiment/tools.py +11 -0
- 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,212 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.20.0 — Item C phased cutover: 10 new semantic moves + Director Phase 6 rewrite (April 24 2026)
|
|
4
|
+
|
|
5
|
+
Implements the plan in `docs/plans/v1.20-structural-plan.md`. Ships 10
|
|
6
|
+
new semantic moves across four family-themed commits, rewrites the
|
|
7
|
+
creative director's Phase 6 to make `apply_semantic_move` the default
|
|
8
|
+
execution surface with a documented + tracked escape hatch, and hardens
|
|
9
|
+
three systemic issues surfaced during live pressure testing. Registry:
|
|
10
|
+
33 → 43 moves across the same 7 canonical families. Full test suite:
|
|
11
|
+
2858 → 2985 pass (+127, zero regressions).
|
|
12
|
+
|
|
13
|
+
### New semantic moves (10 total)
|
|
14
|
+
|
|
15
|
+
**Routing family (`mcp_server/semantic_moves/routing_moves.py`):**
|
|
16
|
+
- `build_send_chain` (device_creation, medium risk) — load an ordered
|
|
17
|
+
chain of devices onto a return track; the Basic Channel / dub-techno
|
|
18
|
+
/ ambient send-architecture primitive. `protect: low_end=0.6`.
|
|
19
|
+
- `configure_send_architecture` (mix, low risk) — set send levels
|
|
20
|
+
across multiple source tracks in one move.
|
|
21
|
+
- `set_track_routing` (mix, medium risk) — rewire a track's output
|
|
22
|
+
routing, e.g. "Sends Only" for bus architectures.
|
|
23
|
+
|
|
24
|
+
**Device-mutation family:**
|
|
25
|
+
- `configure_device` (sound_design, low risk) — bulk-configure N
|
|
26
|
+
parameters on an existing device in a single undoable move. Takes
|
|
27
|
+
`param_overrides: dict` (preset library deferred to v1.21).
|
|
28
|
+
- `remove_device` (sound_design, medium risk, protects
|
|
29
|
+
`signal_integrity=0.9`) — destructive removal with a required
|
|
30
|
+
`reason` string auto-logged to session memory for audit.
|
|
31
|
+
|
|
32
|
+
**Content family:**
|
|
33
|
+
- `load_chord_source` (sound_design, low risk) — create+voice+name a
|
|
34
|
+
MIDI chord clip in one move; feeds `build_send_chain` return chains.
|
|
35
|
+
- `create_drum_rack_pad` (device_creation, low risk) — add one pad to
|
|
36
|
+
a Drum Rack, Dilla-style kit-at-a-time.
|
|
37
|
+
|
|
38
|
+
**Metadata family:**
|
|
39
|
+
- `configure_groove` (arrangement, low risk) — the Dilla-swing
|
|
40
|
+
primitive; assigns a groove + optionally tunes its timing_amount.
|
|
41
|
+
- `set_scene_metadata` (arrangement, low risk) — conditional
|
|
42
|
+
name/color/tempo in one move.
|
|
43
|
+
- `set_track_metadata` (mix, low risk) — bundled rename + color, since
|
|
44
|
+
the two are always paired in Phase 6 usage.
|
|
45
|
+
|
|
46
|
+
### Director SKILL — Phase 6 rewrite
|
|
47
|
+
|
|
48
|
+
- **Decision table (authoritative):** each uncovered-pattern row now
|
|
49
|
+
points at a specific v1.20 move (e.g. "Set multiple params on a
|
|
50
|
+
device" → `configure_device`). 10 NEW rows marked explicitly.
|
|
51
|
+
- **Default execution surface**: `apply_semantic_move` +
|
|
52
|
+
`commit_experiment`, replacing the pre-v1.20 "raw tools + manual
|
|
53
|
+
`add_session_memory(move_executed)` marker" pattern.
|
|
54
|
+
- **Escape hatch policy** (v1.20 transitional state): when no move
|
|
55
|
+
covers the pattern, raw-tool execution is permitted only with the
|
|
56
|
+
three-call discipline — the raw call, an `add_session_memory(
|
|
57
|
+
category="move_executed")` marker, AND an `add_session_memory(
|
|
58
|
+
category="tech_debt")` log naming the uncovered pattern. Both
|
|
59
|
+
categories are mandatory; they serve different consumers (ledger
|
|
60
|
+
vs release planning).
|
|
61
|
+
- **New reference doc** `phase-6-execution.md` (349 lines) — full
|
|
62
|
+
contract (seed_args, compiled steps, risk/protect, typical caller)
|
|
63
|
+
for each of the 10 moves, plus a worked escape-hatch example.
|
|
64
|
+
|
|
65
|
+
### Architectural extension (commit 1)
|
|
66
|
+
|
|
67
|
+
`apply_semantic_move(args: dict)` and `preview_semantic_move(args: dict)`
|
|
68
|
+
now accept user seed parameters that flow into the compiler's kernel as
|
|
69
|
+
`kernel["seed_args"]`. Pre-v1.20 moves are unaffected (they read only
|
|
70
|
+
from `session_info`); the new routing/content/metadata moves read from
|
|
71
|
+
`seed_args` for user targets like `return_track_index`, `device_chain`,
|
|
72
|
+
`notes`, `track_index`, etc.
|
|
73
|
+
|
|
74
|
+
### Live-test hardening (bugs caught during the 6 pressure-test gate)
|
|
75
|
+
|
|
76
|
+
**Wire-format compiler fix.** `configure_device` and `set_track_routing`
|
|
77
|
+
initially emitted MCP-tool-input keys (`parameter_name`,
|
|
78
|
+
`output_routing_type`) which the MCP tool layer would normalize — but
|
|
79
|
+
compiled plans use the `remote_command` backend that goes directly to
|
|
80
|
+
`ableton.send_command()`, bypassing the MCP tool entirely. Ableton's
|
|
81
|
+
Remote Script reads wire-format keys (`name_or_index`, `output_type`)
|
|
82
|
+
exclusively. Fix: both compilers emit wire format. New regression suite
|
|
83
|
+
`tests/test_compiler_wire_format_parity.py` — 10 parametrized cases,
|
|
84
|
+
one per v1.20 move, asserting every compiled step's params match the
|
|
85
|
+
Remote Script handler's actual key inventory.
|
|
86
|
+
|
|
87
|
+
**Automatic ledger write.** `apply_semantic_move` in explore mode now
|
|
88
|
+
writes a LedgerEntry to `SessionLedger` (family, intent, per-step
|
|
89
|
+
actions, provisional `kept=True`, `score = success_fraction`). Returns
|
|
90
|
+
a `ledger_entry_id` in the response so callers can correlate with
|
|
91
|
+
post-hoc `evaluate_move` evaluation. Pre-v1.20 docs pointed
|
|
92
|
+
anti-repetition at `memory_list` which actually reads the persistent
|
|
93
|
+
technique library — wrong store. Director SKILL now points at
|
|
94
|
+
`get_action_ledger_summary`. `commit_experiment` auto-ledger is v1.21
|
|
95
|
+
scope.
|
|
96
|
+
|
|
97
|
+
**Session memory categories.** `_VALID_CATEGORIES` in
|
|
98
|
+
`mcp_server/memory/session_memory.py` now includes the three v1.20
|
|
99
|
+
director Phase 6 categories: `move_executed`, `tech_debt`, and
|
|
100
|
+
`override`. Pre-v1.20 categories preserved (backward compat);
|
|
101
|
+
arbitrary strings still rejected. 7 new contract tests.
|
|
102
|
+
|
|
103
|
+
### New tests (+127 across the release)
|
|
104
|
+
|
|
105
|
+
- `tests/test_registry_uniqueness.py` (4) — guard against dict-insertion
|
|
106
|
+
collisions; baseline move count.
|
|
107
|
+
- `tests/test_apply_semantic_move_args.py` (13) — seed_args threading +
|
|
108
|
+
ledger-write contract for each mode.
|
|
109
|
+
- `tests/test_routing_moves.py` (21) — per-move + cross-family.
|
|
110
|
+
- `tests/test_device_mutation_moves.py` (15).
|
|
111
|
+
- `tests/test_content_moves.py` (14).
|
|
112
|
+
- `tests/test_metadata_moves.py` (15).
|
|
113
|
+
- `tests/test_director_move_coverage.py` (8) — SKILL ↔ registry drift
|
|
114
|
+
detection; phase-6-execution.md contract coverage.
|
|
115
|
+
- `tests/test_compiler_wire_format_parity.py` (10) — wire-format
|
|
116
|
+
invariant across all 10 v1.20 moves.
|
|
117
|
+
- `tests/test_v1_20_session_memory_categories.py` (7) — allowlist
|
|
118
|
+
contract.
|
|
119
|
+
- Various in-place additions (execution_router / mcp_dispatch
|
|
120
|
+
classifier entries for `add_session_memory` and `add_drum_rack_pad`;
|
|
121
|
+
test_device_creation_moves invariant generalized to admit
|
|
122
|
+
device-loading moves alongside Device Forge moves).
|
|
123
|
+
|
|
124
|
+
### Live pressure-test results (the 6 plan §5 scenarios, all passing)
|
|
125
|
+
|
|
126
|
+
1. `build_send_chain` on Return A with Echo + Auto Filter + Hybrid
|
|
127
|
+
Reverb — 4 steps, 4 successes.
|
|
128
|
+
2. `configure_device` on the Reverb with dub-cathedral overrides
|
|
129
|
+
(Decay 25.5s, Room Size 339.89, Dry/Wet 40%, Predelay 8.19ms,
|
|
130
|
+
Diffusion 77%) — 5 params set in one batch_set_parameters call.
|
|
131
|
+
3. `configure_send_architecture` on track 0 → Send A at 0.4 — single
|
|
132
|
+
step, success.
|
|
133
|
+
4. `load_chord_source` on track 0 slot 0 with a C minor 7 voicing —
|
|
134
|
+
create_clip + add_notes + set_clip_name, 3/3 success.
|
|
135
|
+
5. 4 moves in sequence → `get_action_ledger_summary` returns 4 entries
|
|
136
|
+
(engine=semantic_moves, mix+arrangement families), zero `tech_debt`
|
|
137
|
+
entries — automatic ledger write confirmed end-to-end.
|
|
138
|
+
6. Escape hatch: raw `set_track_arm(track=2, armed=true)` + both
|
|
139
|
+
mandatory `add_session_memory` markers; `get_session_memory(
|
|
140
|
+
category="tech_debt")` returns the log entry as expected.
|
|
141
|
+
|
|
142
|
+
### Scope / non-goals
|
|
143
|
+
|
|
144
|
+
Not in v1.20, explicitly deferred:
|
|
145
|
+
- Hard cutover (closing the escape hatch). v1.21 target, conditional
|
|
146
|
+
on zero `tech_debt` entries over one month of production use.
|
|
147
|
+
- Preset YAML library for `configure_device`. The move's
|
|
148
|
+
`param_overrides` dict already accepts a pre-resolved preset; the
|
|
149
|
+
library is the layer that produces those dicts.
|
|
150
|
+
- `commit_experiment` automatic ledger write. Tracked as tech-debt.
|
|
151
|
+
- Rewriting the existing 33 moves to a new shape. v1.22+.
|
|
152
|
+
- Director Phase 6 compiler that picks moves automatically from user
|
|
153
|
+
intent. Current Phase 6 is user/director-selected; auto-selection
|
|
154
|
+
is a separate feature.
|
|
155
|
+
|
|
156
|
+
## 1.19.1 — v1.19.0 polish (April 24 2026)
|
|
157
|
+
|
|
158
|
+
Patch release addressing the three "Known gaps" documented at the
|
|
159
|
+
end of the v1.19.0 CHANGELOG entry. All three were cosmetic or
|
|
160
|
+
observability issues — no correctness changes. 3 new tests + 1
|
|
161
|
+
pre-existing test tolerance widened. Test suite 2854 → 2858 pass.
|
|
162
|
+
|
|
163
|
+
### Fixes
|
|
164
|
+
|
|
165
|
+
- **#1 `baseline_transport` not exposed via `compare_experiments`.**
|
|
166
|
+
The field was populated internally on `ExperimentSet` (verified
|
|
167
|
+
by unit tests) but `compare_experiments`' MCP response omitted
|
|
168
|
+
it — operators had no surface-level path to verify the
|
|
169
|
+
between-branch drift fix was actually firing. Now present on
|
|
170
|
+
every response (`None` when the experiment hasn't run yet, so
|
|
171
|
+
clients can rely on key presence and check
|
|
172
|
+
`result["baseline_transport"] is None` without `in` guards).
|
|
173
|
+
|
|
174
|
+
- **#2 Tempo warning midpoint rounds to int while range is exact.**
|
|
175
|
+
Pre-v1.19.1 `compile_hybrid_brief` with disjoint tempo ranges
|
|
176
|
+
reported warning text "midpoint 108 BPM" while the returned
|
|
177
|
+
range was 105-110 (centered on 107.5). Two rounding
|
|
178
|
+
conventions — human-facing text rounded to `:0f`, machine-facing
|
|
179
|
+
range kept the exact float. Fix: `:g` format in the warning
|
|
180
|
+
produces the shortest accurate representation (107.5 stays
|
|
181
|
+
"107.5"; 128.0 renders as "128") so both surfaces agree.
|
|
182
|
+
|
|
183
|
+
- **#3 `weights` display full float precision.**
|
|
184
|
+
Uniform 3-packet hybrids rendered weights as
|
|
185
|
+
`0.3333333333333333` — noisy output that contrasted with
|
|
186
|
+
`evaluation_bias.target_dimensions` values already being
|
|
187
|
+
rounded to 4 decimal places. Weights are now rounded to 4 dp
|
|
188
|
+
in the response dict (`[0.3333, 0.3333, 0.3333]`). Internal
|
|
189
|
+
computation still uses full precision; only the output is
|
|
190
|
+
rounded.
|
|
191
|
+
|
|
192
|
+
### Tests added
|
|
193
|
+
|
|
194
|
+
- `test_compare_experiments_surfaces_baseline_transport` — round-trip
|
|
195
|
+
seed a distinctive baseline on ExperimentSet, assert
|
|
196
|
+
`compare_experiments` surfaces all fields (is_playing, song_time,
|
|
197
|
+
track_states, captured_at_ms).
|
|
198
|
+
- `test_compare_experiments_baseline_none_when_not_captured` — fresh
|
|
199
|
+
experiment has `baseline_transport: None` in the response rather
|
|
200
|
+
than an omitted key.
|
|
201
|
+
- `test_tempo_warning_midpoint_matches_range_center` — regex-parse
|
|
202
|
+
the warning text and assert its numeric midpoint matches the
|
|
203
|
+
returned range's center within 0.01 BPM.
|
|
204
|
+
- `test_weights_rounded_to_4dp` — uniform 3-packet weights must be
|
|
205
|
+
representable at 4 dp precision (`round(w, 4) == w`).
|
|
206
|
+
|
|
207
|
+
Test suite: 2858 pass, 1 skipped. Zero regressions. `sync_metadata
|
|
208
|
+
--check` clean.
|
|
209
|
+
|
|
3
210
|
## 1.19.0 — Experiment baseline + hybrid packet compilation (April 24 2026)
|
|
4
211
|
|
|
5
212
|
Minor version bump. Ships two of the three open items documented in
|
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.0"
|
|
@@ -333,9 +333,15 @@ def _compile_from_packets(
|
|
|
333
333
|
f"{name or 'packet'} {lo:.0f}-{hi:.0f}"
|
|
334
334
|
for lo, hi, name in tempo_ranges
|
|
335
335
|
)
|
|
336
|
+
# v1.19.1 #2 — :g format keeps warning midpoint consistent with
|
|
337
|
+
# the returned range center. Pre-v1.19.1 used :.0f (int-rounded)
|
|
338
|
+
# so BC+Dilla reported 'midpoint 108 BPM' while range was
|
|
339
|
+
# 105-110 centered on 107.5 — two rounding conventions.
|
|
340
|
+
# :g gives the shortest accurate representation: 107.5 stays
|
|
341
|
+
# "107.5", 128.0 becomes "128".
|
|
336
342
|
warnings.append(
|
|
337
343
|
f"Tempo ranges don't overlap ({range_desc}) — defaulting "
|
|
338
|
-
f"to midpoint {midpoint
|
|
344
|
+
f"to midpoint {midpoint:g} BPM. Specify which anchor "
|
|
339
345
|
f"you want or pick a single packet."
|
|
340
346
|
)
|
|
341
347
|
|
|
@@ -346,7 +352,10 @@ def _compile_from_packets(
|
|
|
346
352
|
return {
|
|
347
353
|
"type": "hybrid",
|
|
348
354
|
"source_packets": list(packet_ids),
|
|
349
|
-
|
|
355
|
+
# v1.19.1 #3 — round weights to 4 dp for clean display, matching the
|
|
356
|
+
# convention target_dimensions already uses. Pre-v1.19.1 uniform
|
|
357
|
+
# 3-packet weights rendered as 0.3333333333333333 — noisy output.
|
|
358
|
+
"weights": [round(w, 4) for w in weights],
|
|
350
359
|
"name": hybrid_name,
|
|
351
360
|
"sonic_identity": sonic_identity,
|
|
352
361
|
"reach_for": reach_for,
|
|
@@ -601,10 +601,21 @@ def compare_experiments(
|
|
|
601
601
|
"evaluation": b.evaluation,
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
+
# v1.19.1 #1 — surface baseline_transport for operator observability.
|
|
605
|
+
# Always present in the response (None when not captured) so clients
|
|
606
|
+
# can `result["baseline_transport"] is None` instead of checking for
|
|
607
|
+
# key presence first. Populated during run_experiment's first pass.
|
|
608
|
+
baseline_dict = (
|
|
609
|
+
experiment.baseline_transport.to_dict()
|
|
610
|
+
if experiment.baseline_transport is not None
|
|
611
|
+
else None
|
|
612
|
+
)
|
|
613
|
+
|
|
604
614
|
return {
|
|
605
615
|
"experiment_id": experiment_id,
|
|
606
616
|
"request": experiment.request_text,
|
|
607
617
|
"branch_count": experiment.branch_count,
|
|
618
|
+
"baseline_transport": baseline_dict,
|
|
608
619
|
"ranking": [
|
|
609
620
|
{
|
|
610
621
|
"rank": i + 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)
|