livepilot 1.13.0 → 1.14.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 +195 -0
- package/README.md +6 -6
- 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/branches/__init__.py +2 -0
- package/mcp_server/branches/types.py +57 -1
- package/mcp_server/composer/__init__.py +2 -2
- package/mcp_server/composer/branch_producer.py +120 -0
- package/mcp_server/composer/tools.py +58 -1
- package/mcp_server/evaluation/policy.py +98 -0
- package/mcp_server/experiment/models.py +40 -1
- package/mcp_server/experiment/tools.py +283 -15
- package/mcp_server/runtime/remote_commands.py +2 -0
- package/mcp_server/server.py +1 -0
- package/mcp_server/synthesis_brain/adapters/analog.py +158 -52
- package/mcp_server/synthesis_brain/adapters/drift.py +156 -51
- package/mcp_server/synthesis_brain/adapters/meld.py +150 -40
- package/mcp_server/synthesis_brain/adapters/operator.py +137 -14
- package/mcp_server/synthesis_brain/adapters/wavetable.py +156 -20
- package/mcp_server/synthesis_brain/tools.py +231 -0
- package/mcp_server/tools/devices.py +14 -2
- package/mcp_server/tools/diagnostics.py +21 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +100 -43
- package/remote_script/LivePilot/devices.py +75 -9
- package/remote_script/LivePilot/mixing.py +154 -34
- package/server.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,200 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.1 — reload_handlers workflow + device/mixing fixes (April 21 2026)
|
|
4
|
+
|
|
5
|
+
Patch release that lands the post-1.14.0 audit work: one new diagnostics
|
|
6
|
+
tool, three bug fixes, and a new plugin-sync verification script.
|
|
7
|
+
|
|
8
|
+
**Tool count**: 402 → **403** (added `reload_handlers`).
|
|
9
|
+
**Domain count**: unchanged at 52.
|
|
10
|
+
**Tests**: 2467 → **2485 passing** (+18 new), 0 regressions.
|
|
11
|
+
|
|
12
|
+
### New tool: `reload_handlers`
|
|
13
|
+
|
|
14
|
+
- Replaces the manual "toggle Control Surface in Live → Preferences →
|
|
15
|
+
Link/MIDI" step that every Remote Script edit required. New workflow:
|
|
16
|
+
after `node installer/install.js`, call `reload_handlers` via the MCP
|
|
17
|
+
tool. The Remote Script side uses `pkgutil` + `importlib.reload()` to
|
|
18
|
+
re-fire all `@register` decorators in place in <1s, without dropping
|
|
19
|
+
the MCP TCP connection on port 9878.
|
|
20
|
+
- Ships with a pkgutil-based module-discovery helper in
|
|
21
|
+
`remote_script/LivePilot/__init__.py`, so new handler modules added to
|
|
22
|
+
`remote_script/LivePilot/` are picked up automatically on reload.
|
|
23
|
+
- Exception: the very first bootstrap (no prior `LivePilot.*` in
|
|
24
|
+
`sys.modules`) still needs one full Ableton restart. After that,
|
|
25
|
+
`reload_handlers` works forever.
|
|
26
|
+
- Domain: `diagnostics`. Added to `docs/manual/tool-catalog.md` to keep
|
|
27
|
+
the CI skill-contract test green.
|
|
28
|
+
|
|
29
|
+
### Bug fixes
|
|
30
|
+
|
|
31
|
+
- **`find_and_load_device` duplicate loads** — the tool was no-oping
|
|
32
|
+
only on exact name match; changed to also treat cases where the
|
|
33
|
+
target device is already the tail of the chain as a no-op. Prevents
|
|
34
|
+
the "load Simpler, load Simpler, load Simpler" cascade when the MCP
|
|
35
|
+
server retries a loader.
|
|
36
|
+
- **`get_device_parameters` "Invalid display value"** — certain Live
|
|
37
|
+
parameters (especially plugin wrappers on AU/VST) raise
|
|
38
|
+
`RuntimeError("Invalid display value")` when their
|
|
39
|
+
`str_for_value()` is queried before the parameter has settled. The
|
|
40
|
+
handler now swallows that specific error and returns the raw float
|
|
41
|
+
instead of 500-ing the whole request.
|
|
42
|
+
- **Sidechain LOM reopen (BUG-A3 redux)** — Compressor2 moved its
|
|
43
|
+
sidechain block into a nested property in a recent Live update, so
|
|
44
|
+
`compressor_set_sidechain` lost the ability to toggle. The handler
|
|
45
|
+
now probes the LOM surface at tool-call time and falls back to the
|
|
46
|
+
flat path when the nested one isn't exposed.
|
|
47
|
+
- **Mixing `channel` lazy-get** — channel objects were resolved eagerly
|
|
48
|
+
at import time, breaking in edge cases where the Song came up before
|
|
49
|
+
the mixer. Now resolved on first use.
|
|
50
|
+
|
|
51
|
+
### New: plugin-sync verification
|
|
52
|
+
|
|
53
|
+
- `scripts/verify_plugin_sync.py` — catches the v1.14.0 regression
|
|
54
|
+
class where `.mcp.json` went missing from
|
|
55
|
+
`~/.claude/plugins/cache/dreamrec-LivePilot/livepilot/$VERSION/`. All
|
|
56
|
+
four sync targets (active plugin dir, cache version dir, marketplace
|
|
57
|
+
snapshot, `installed_plugins.json`) are now verified by one command.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 1.14.0 — Branch-native v2: producer context, synth intelligence, render verify (April 20 2026)
|
|
62
|
+
|
|
63
|
+
Five-PR follow-up to v1.13.0 that closes the loops the first pass left
|
|
64
|
+
open. Producer context flows through the branch lifecycle via a versioned
|
|
65
|
+
`producer_payload`; synthesis adapters decode algorithm topology instead
|
|
66
|
+
of always targeting the same operator; composer winners commit the full
|
|
67
|
+
resolved plan instead of the audition scaffold; render-verify captures
|
|
68
|
+
audio before/after each branch and feeds spectral movement into the
|
|
69
|
+
hard-rule classifier; and four dedicated MCP tools expose the new
|
|
70
|
+
producers to the LLM.
|
|
71
|
+
|
|
72
|
+
**Tool count**: 398 → **402** (added `analyze_synth_patch`,
|
|
73
|
+
`propose_synth_branches`, `extract_timbre_fingerprint`,
|
|
74
|
+
`propose_composer_branches`). **Domain count**: unchanged at 52.
|
|
75
|
+
**Tests**: 2409 → **2467 passing** (+58 new), 0 regressions.
|
|
76
|
+
|
|
77
|
+
### New substrate
|
|
78
|
+
|
|
79
|
+
- **`producer_payload: dict` on `BranchSeed`** (PR1) — versioned
|
|
80
|
+
opaque dict producers populate with regeneration / provenance /
|
|
81
|
+
winner-escalation context. Always carries `schema_version` (default 1)
|
|
82
|
+
so older payloads don't break newer readers. Lives on the seed, not
|
|
83
|
+
`compiled_plan`, so analytical-only branches can carry context too.
|
|
84
|
+
Canonical shapes documented per producer (synthesis / composer /
|
|
85
|
+
semantic_move / freeform / technique).
|
|
86
|
+
|
|
87
|
+
- **`BranchSnapshot` gains render-based fields** (PR4) —
|
|
88
|
+
`capture_path`, `loudness`, `spectral_shape`, `fingerprint`. Populated
|
|
89
|
+
only when `run_experiment(render_verify=True)` opts in. Pre-v2
|
|
90
|
+
consumers see no shape change when render-verify is off.
|
|
91
|
+
|
|
92
|
+
### Synthesis adapters get topology awareness (PR2)
|
|
93
|
+
|
|
94
|
+
- **Wavetable**: position→region classification (sub / mid / bright /
|
|
95
|
+
complex) drives shift direction. Target brightness biases the chosen
|
|
96
|
+
target region — not just freshness scaling.
|
|
97
|
+
|
|
98
|
+
- **Operator**: static `_ALGO_TOPOLOGY` table maps all 11 algorithms
|
|
99
|
+
to their carrier/modulator roles. Targeting picks the modulator with
|
|
100
|
+
the highest Level for the ratio shift; additive algorithms (5, 9)
|
|
101
|
+
fall back to the dominant carrier. No more "always Osc B".
|
|
102
|
+
|
|
103
|
+
- **Analog / Drift / Meld**: single fixed proposers become strategy
|
|
104
|
+
registries. Gates honor `role_hint` ("bass" skips `detune_warmth`,
|
|
105
|
+
"pad" skips `filter_pluck`, silent engines skip `engine_mix_shift`)
|
|
106
|
+
and target fingerprint dimensions (`target.brightness` picks
|
|
107
|
+
`filter_sweep_open` vs `filter_sweep_close`).
|
|
108
|
+
|
|
109
|
+
### Composer winner escalation (PR3)
|
|
110
|
+
|
|
111
|
+
- Composer seeds now carry their `CompositionIntent` in
|
|
112
|
+
`producer_payload` at emit time. On `commit_experiment`,
|
|
113
|
+
`escalate_composer_branch` rehydrates the intent and runs the full
|
|
114
|
+
`ComposerEngine.compose()` pipeline — Splice / filesystem / browser
|
|
115
|
+
sample resolution — then swaps the scaffold plan for the resolved one
|
|
116
|
+
BEFORE `commit_branch_async` runs it through the async router. The
|
|
117
|
+
scaffold is preserved on `branch.evaluation` for audit.
|
|
118
|
+
|
|
119
|
+
- Graceful fallback when compose yields zero executable layers:
|
|
120
|
+
commit runs the scaffold instead of erroring. User gets tracks +
|
|
121
|
+
scenes they can populate manually, with `composer_escalation.error`
|
|
122
|
+
explaining why escalation couldn't complete.
|
|
123
|
+
|
|
124
|
+
- **Latency note**: composer winner commit now takes 10-30s (Splice +
|
|
125
|
+
filesystem resolution) vs ~0.5s pre-v2. Documented; a progress
|
|
126
|
+
callback version is future work.
|
|
127
|
+
|
|
128
|
+
### Render-verify + classifier wiring (PR4)
|
|
129
|
+
|
|
130
|
+
- `run_experiment(render_verify=False, render_duration_seconds=2.0)` —
|
|
131
|
+
opt-in per-branch audio capture → offline loudness + spectrum
|
|
132
|
+
analysis → `TimbralFingerprint` extraction. ~2 * duration seconds +
|
|
133
|
+
~1-2s analysis overhead per branch; default off preserves speed.
|
|
134
|
+
|
|
135
|
+
- New `derive_goal_progress_from_fingerprint(diff, target?)` turns
|
|
136
|
+
TimbralFingerprint diffs into `(goal_progress, measurable_count)`.
|
|
137
|
+
Dimensions below 0.02 epsilon are dropped as noise. With a target:
|
|
138
|
+
sign(target) * diff gives signed progress. Without: magnitude-only
|
|
139
|
+
contribution (branch moved = measurable).
|
|
140
|
+
|
|
141
|
+
- `classify_branch_outcome` accepts `fingerprint_diff` + `timbral_target`
|
|
142
|
+
kwargs. When set and caller didn't supply their own measurable inputs,
|
|
143
|
+
the classifier derives them from the diff. Caller-supplied values
|
|
144
|
+
still take precedence (back-compat). Protection violations still
|
|
145
|
+
trump fingerprint evidence — safety invariant preserved.
|
|
146
|
+
|
|
147
|
+
- `compare_experiments` automatically surfaces `fingerprint_diff` +
|
|
148
|
+
`fingerprint_before` + `fingerprint_after` on each branch's
|
|
149
|
+
`evaluation` dict via the existing pass-through.
|
|
150
|
+
|
|
151
|
+
### Four new MCP tools (PR5, 398 → 402)
|
|
152
|
+
|
|
153
|
+
- **`analyze_synth_patch(track_index, device_index, role_hint="")`** —
|
|
154
|
+
SynthProfile for any supported native synth. Fetches parameter state
|
|
155
|
+
+ display values, hands to the adapter. Opaque fallback for
|
|
156
|
+
non-supported devices.
|
|
157
|
+
|
|
158
|
+
- **`propose_synth_branches(track_index, device_index, target?,
|
|
159
|
+
freshness?, role_hint?)`** — algorithm/topology-aware branch seeds
|
|
160
|
+
with pre-compiled plans. Feeds directly to
|
|
161
|
+
`create_experiment(seeds=..., compiled_plans=...)`.
|
|
162
|
+
|
|
163
|
+
- **`extract_timbre_fingerprint(spectrum?, loudness?, spectral_shape?)`**
|
|
164
|
+
— pure transform from analysis dicts to 9-dimensional
|
|
165
|
+
`TimbralFingerprint`. For callers that already have analysis data.
|
|
166
|
+
|
|
167
|
+
- **`propose_composer_branches(request_text, count=2, freshness=0.65)`**
|
|
168
|
+
— N compositional hypotheses (canonical / energy_shift /
|
|
169
|
+
layer_contrast) with producer_payload-captured intents for
|
|
170
|
+
winner-commit escalation.
|
|
171
|
+
|
|
172
|
+
### Migration notes for callers
|
|
173
|
+
|
|
174
|
+
- All additions are optional-param / new-function shaped. Pre-v2
|
|
175
|
+
callers see no behavior change unless they opt in.
|
|
176
|
+
- Pre-v2 serialized branches deserialize fine: `producer_payload`
|
|
177
|
+
defaults to `{"schema_version": 1}` when absent.
|
|
178
|
+
- `create_experiment(move_ids=...)` identical behavior.
|
|
179
|
+
- `enter_wonder_mode` response shape stable; `branch_seeds` /
|
|
180
|
+
`compiled_plans_by_seed_id` still additive.
|
|
181
|
+
- `run_experiment(render_verify=False)` default matches v1.13.0
|
|
182
|
+
behavior exactly.
|
|
183
|
+
|
|
184
|
+
### Known limitations
|
|
185
|
+
|
|
186
|
+
See [`docs/manual/branch-native-migration.md`](docs/manual/branch-native-migration.md#known-limitations)
|
|
187
|
+
for the full list. Headlines:
|
|
188
|
+
|
|
189
|
+
- Wavetable region classification is a coarse heuristic on raw
|
|
190
|
+
`Osc 1 Pos`. Future work: render-based per-wavetable mapping.
|
|
191
|
+
- Composer commit latency (10-30s) with no progress callback yet.
|
|
192
|
+
- Render-verify requires the M4L analyzer bridge + LivePilot_Analyzer
|
|
193
|
+
on master; silently degrades to fast-path when either is missing.
|
|
194
|
+
- End-to-end render-verify path (capture_audio + bridge) is hardware-
|
|
195
|
+
dependent and not unit-tested; wiring is covered via classifier +
|
|
196
|
+
fingerprint derivation tests.
|
|
197
|
+
|
|
3
198
|
## 1.13.0 — Branch-native architecture (April 20 2026)
|
|
4
199
|
|
|
5
200
|
Twelve-PR migration from "match request → pick move → compile move" to
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
An agentic production system for Ableton Live 12.<br>
|
|
20
|
-
|
|
20
|
+
403 tools. 52 domains. Device atlas. Splice integration. Auto-composition. Spectral perception. Technique memory.
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
<br>
|
|
@@ -79,7 +79,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
79
79
|
│ └─────────────────┼──────────────────┘ │
|
|
80
80
|
│ ▼ │
|
|
81
81
|
│ ┌─────────────────┐ │
|
|
82
|
-
│ │
|
|
82
|
+
│ │ 403 MCP Tools │ │
|
|
83
83
|
│ │ 52 domains │ │
|
|
84
84
|
│ └────────┬────────┘ │
|
|
85
85
|
│ │ │
|
|
@@ -120,7 +120,7 @@ Most MCP servers are tool collections — they execute commands. LivePilot is an
|
|
|
120
120
|
|
|
121
121
|
## The Intelligence Layer
|
|
122
122
|
|
|
123
|
-
12 engines sit on top of the
|
|
123
|
+
12 engines sit on top of the 403 tools. They give the AI musical judgment, not just musical execution.
|
|
124
124
|
|
|
125
125
|
### SongBrain — What the Song Is
|
|
126
126
|
|
|
@@ -172,7 +172,7 @@ Every engine follows: **measure before → act → measure after → compare**.
|
|
|
172
172
|
|
|
173
173
|
## Tools
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
403 tools across 52 domains. Highlights below — [full catalog here](docs/manual/tool-catalog.md).
|
|
176
176
|
|
|
177
177
|
<br>
|
|
178
178
|
|
|
@@ -360,7 +360,7 @@ The V2 intelligence layer. These tools analyze, diagnose, plan, evaluate, and le
|
|
|
360
360
|
| Creative Constraints | 5 | constraint activation, reference-inspired variants |
|
|
361
361
|
| Preview Studio | 5 | variant creation, preview rendering, comparison, commit |
|
|
362
362
|
|
|
363
|
-
> **[View all
|
|
363
|
+
> **[View all 403 tools →](docs/manual/tool-catalog.md)**
|
|
364
364
|
|
|
365
365
|
<br>
|
|
366
366
|
|
|
@@ -587,7 +587,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for architecture details, code guidelines
|
|
|
587
587
|
|
|
588
588
|
| Document | What's inside |
|
|
589
589
|
|----------|---------------|
|
|
590
|
-
| [Manual](docs/manual/index.md) | Complete reference: architecture, all
|
|
590
|
+
| [Manual](docs/manual/index.md) | Complete reference: architecture, all 403 tools, workflows |
|
|
591
591
|
| [Intelligence Layer](docs/manual/intelligence.md) | How the 12 engines connect — conductor, moves, preview, evaluation |
|
|
592
592
|
| [Device Atlas](docs/manual/device-atlas.md) | 1305 devices indexed — search, suggest, chain building |
|
|
593
593
|
| [Samples & Slicing](docs/manual/samples.md) | 3-source search, fitness critics, slice workflows |
|
|
Binary file
|
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.14.1"
|
|
@@ -15,6 +15,7 @@ from .types import (
|
|
|
15
15
|
BranchSource,
|
|
16
16
|
RiskLabel,
|
|
17
17
|
NoveltyLabel,
|
|
18
|
+
PRODUCER_PAYLOAD_SCHEMA_VERSION,
|
|
18
19
|
seed_from_move_id,
|
|
19
20
|
freeform_seed,
|
|
20
21
|
analytical_seed,
|
|
@@ -26,6 +27,7 @@ __all__ = [
|
|
|
26
27
|
"BranchSource",
|
|
27
28
|
"RiskLabel",
|
|
28
29
|
"NoveltyLabel",
|
|
30
|
+
"PRODUCER_PAYLOAD_SCHEMA_VERSION",
|
|
29
31
|
"seed_from_move_id",
|
|
30
32
|
"freeform_seed",
|
|
31
33
|
"analytical_seed",
|
|
@@ -38,6 +38,9 @@ RiskLabel = Literal["low", "medium", "high"]
|
|
|
38
38
|
NoveltyLabel = Literal["safe", "strong", "unexpected"]
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
PRODUCER_PAYLOAD_SCHEMA_VERSION = 1
|
|
42
|
+
|
|
43
|
+
|
|
41
44
|
@dataclass
|
|
42
45
|
class BranchSeed:
|
|
43
46
|
"""Pre-compilation creative intent.
|
|
@@ -52,10 +55,32 @@ class BranchSeed:
|
|
|
52
55
|
hypothesis: one-line human-readable prediction of what the branch does.
|
|
53
56
|
protected_qualities: dimension names the producer promises not to regress.
|
|
54
57
|
affected_scope: {track_indices, device_paths, section_ids, clip_slots}.
|
|
58
|
+
Session-scope information: which tracks/devices/sections the branch
|
|
59
|
+
affects. Distinct from ``producer_payload`` which carries regeneration
|
|
60
|
+
context.
|
|
55
61
|
distinctness_reason: why this seed is different from siblings in a set.
|
|
56
62
|
risk_label: execution safety tier.
|
|
57
63
|
novelty_label: creative novelty tier — maps to the safe/strong/unexpected UX triptych.
|
|
58
64
|
analytical_only: true ⇒ no plan will be compiled; branch is directional only.
|
|
65
|
+
producer_payload: free-form dict the emitting producer populates with
|
|
66
|
+
whatever it needs for regeneration / provenance / winner escalation.
|
|
67
|
+
Consumers must check ``schema_version`` (defaults to 1) before reading
|
|
68
|
+
specific keys so older serialized branches don't break newer readers.
|
|
69
|
+
|
|
70
|
+
Canonical shapes per producer:
|
|
71
|
+
semantic_move: {schema_version: 1} (empty — move_id is the key)
|
|
72
|
+
freeform / technique: {schema_version: 1} (optional)
|
|
73
|
+
synthesis:
|
|
74
|
+
{schema_version: 1,
|
|
75
|
+
device_name: "Wavetable",
|
|
76
|
+
track_index: int, device_index: int,
|
|
77
|
+
strategy: "osc_position_shift" | "voice_width_variant" | ...,
|
|
78
|
+
topology_hint: {...}} (used by PR4 render-verify to re-target)
|
|
79
|
+
composer:
|
|
80
|
+
{schema_version: 1,
|
|
81
|
+
strategy: "canonical" | "energy_shift" | "layer_contrast",
|
|
82
|
+
intent: {<CompositionIntent.to_dict()>}} (used by PR3 to
|
|
83
|
+
rehydrate for winner-commit escalation)
|
|
59
84
|
"""
|
|
60
85
|
|
|
61
86
|
seed_id: str
|
|
@@ -68,9 +93,20 @@ class BranchSeed:
|
|
|
68
93
|
risk_label: RiskLabel = "low"
|
|
69
94
|
novelty_label: NoveltyLabel = "strong"
|
|
70
95
|
analytical_only: bool = False
|
|
96
|
+
producer_payload: dict = field(
|
|
97
|
+
default_factory=lambda: {"schema_version": PRODUCER_PAYLOAD_SCHEMA_VERSION}
|
|
98
|
+
)
|
|
71
99
|
|
|
72
100
|
def to_dict(self) -> dict:
|
|
73
|
-
|
|
101
|
+
d = asdict(self)
|
|
102
|
+
# Guarantee schema_version in the payload even if the dict was
|
|
103
|
+
# mutated to empty by a caller.
|
|
104
|
+
payload = d.get("producer_payload") or {}
|
|
105
|
+
if "schema_version" not in payload:
|
|
106
|
+
payload = dict(payload)
|
|
107
|
+
payload["schema_version"] = PRODUCER_PAYLOAD_SCHEMA_VERSION
|
|
108
|
+
d["producer_payload"] = payload
|
|
109
|
+
return d
|
|
74
110
|
|
|
75
111
|
|
|
76
112
|
@dataclass
|
|
@@ -158,6 +194,7 @@ def seed_from_move_id(
|
|
|
158
194
|
risk_label: RiskLabel = "low",
|
|
159
195
|
protected_qualities: Optional[list[str]] = None,
|
|
160
196
|
distinctness_reason: str = "",
|
|
197
|
+
producer_payload: Optional[dict] = None,
|
|
161
198
|
) -> BranchSeed:
|
|
162
199
|
"""Build a semantic_move seed — the baseline producer path.
|
|
163
200
|
|
|
@@ -176,6 +213,7 @@ def seed_from_move_id(
|
|
|
176
213
|
risk_label=risk_label,
|
|
177
214
|
protected_qualities=protected_qualities or [],
|
|
178
215
|
distinctness_reason=distinctness_reason,
|
|
216
|
+
producer_payload=_normalize_payload(producer_payload),
|
|
179
217
|
)
|
|
180
218
|
|
|
181
219
|
|
|
@@ -188,6 +226,7 @@ def freeform_seed(
|
|
|
188
226
|
novelty_label: NoveltyLabel = "strong",
|
|
189
227
|
risk_label: RiskLabel = "medium",
|
|
190
228
|
source: BranchSource = "freeform",
|
|
229
|
+
producer_payload: Optional[dict] = None,
|
|
191
230
|
) -> BranchSeed:
|
|
192
231
|
"""Build a freeform seed — producer has a concrete hypothesis without a move.
|
|
193
232
|
|
|
@@ -204,6 +243,7 @@ def freeform_seed(
|
|
|
204
243
|
distinctness_reason=distinctness_reason,
|
|
205
244
|
novelty_label=novelty_label,
|
|
206
245
|
risk_label=risk_label,
|
|
246
|
+
producer_payload=_normalize_payload(producer_payload),
|
|
207
247
|
)
|
|
208
248
|
|
|
209
249
|
|
|
@@ -212,6 +252,7 @@ def analytical_seed(
|
|
|
212
252
|
hypothesis: str,
|
|
213
253
|
source: BranchSource = "freeform",
|
|
214
254
|
protected_qualities: Optional[list[str]] = None,
|
|
255
|
+
producer_payload: Optional[dict] = None,
|
|
215
256
|
) -> BranchSeed:
|
|
216
257
|
"""Build an analytical-only seed — no plan will be compiled.
|
|
217
258
|
|
|
@@ -227,4 +268,19 @@ def analytical_seed(
|
|
|
227
268
|
hypothesis=hypothesis,
|
|
228
269
|
protected_qualities=protected_qualities or [],
|
|
229
270
|
analytical_only=True,
|
|
271
|
+
producer_payload=_normalize_payload(producer_payload),
|
|
230
272
|
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _normalize_payload(payload: Optional[dict]) -> dict:
|
|
276
|
+
"""Ensure a producer_payload always has schema_version set.
|
|
277
|
+
|
|
278
|
+
Producers that don't care about versioning pass None / empty dict and
|
|
279
|
+
get the default schema_version=PRODUCER_PAYLOAD_SCHEMA_VERSION back.
|
|
280
|
+
Producers that carry their own version number have it preserved.
|
|
281
|
+
"""
|
|
282
|
+
if not payload:
|
|
283
|
+
return {"schema_version": PRODUCER_PAYLOAD_SCHEMA_VERSION}
|
|
284
|
+
out = dict(payload)
|
|
285
|
+
out.setdefault("schema_version", PRODUCER_PAYLOAD_SCHEMA_VERSION)
|
|
286
|
+
return out
|
|
@@ -5,6 +5,6 @@ multiple section-hypothesis BranchSeeds alongside the existing
|
|
|
5
5
|
single-plan compose() entry point.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from .branch_producer import propose_composer_branches
|
|
8
|
+
from .branch_producer import propose_composer_branches, escalate_composer_branch
|
|
9
9
|
|
|
10
|
-
__all__ = ["propose_composer_branches"]
|
|
10
|
+
__all__ = ["propose_composer_branches", "escalate_composer_branch"]
|
|
@@ -29,6 +29,7 @@ from typing import Optional
|
|
|
29
29
|
from ..branches import BranchSeed, freeform_seed
|
|
30
30
|
from .prompt_parser import parse_prompt, CompositionIntent
|
|
31
31
|
from .layer_planner import plan_layers, plan_sections
|
|
32
|
+
from .engine import ComposerEngine, CompositionResult
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
# Strategy registry — each function takes an intent and returns (modified
|
|
@@ -146,6 +147,15 @@ def propose_composer_branches(
|
|
|
146
147
|
novelty_label=novelty,
|
|
147
148
|
risk_label=risk,
|
|
148
149
|
distinctness_reason=reason,
|
|
150
|
+
# PR3 — carry the variant intent + strategy so commit_experiment
|
|
151
|
+
# can rehydrate and run the full ComposerEngine.compose()
|
|
152
|
+
# pipeline on the winner instead of committing the scaffold.
|
|
153
|
+
producer_payload={
|
|
154
|
+
"strategy": name,
|
|
155
|
+
"intent": variant_intent.to_dict(),
|
|
156
|
+
"request_text": request_text,
|
|
157
|
+
"reason": reason,
|
|
158
|
+
},
|
|
149
159
|
)
|
|
150
160
|
results.append((seed, plan))
|
|
151
161
|
except Exception as exc:
|
|
@@ -159,6 +169,116 @@ def propose_composer_branches(
|
|
|
159
169
|
return results
|
|
160
170
|
|
|
161
171
|
|
|
172
|
+
async def escalate_composer_branch(
|
|
173
|
+
producer_payload: dict,
|
|
174
|
+
search_roots: Optional[list] = None,
|
|
175
|
+
splice_client: object = None,
|
|
176
|
+
browser_client: object = None,
|
|
177
|
+
max_credits: int = 10,
|
|
178
|
+
) -> dict:
|
|
179
|
+
"""Run the full ComposerEngine.compose() pipeline on a committed
|
|
180
|
+
composer branch, using the CompositionIntent captured in the seed's
|
|
181
|
+
producer_payload at emit time.
|
|
182
|
+
|
|
183
|
+
Returns a dict with:
|
|
184
|
+
ok: bool
|
|
185
|
+
plan: list of executable steps (the full resolved plan, not the
|
|
186
|
+
scaffolding the branch was auditioned with)
|
|
187
|
+
step_count: int
|
|
188
|
+
layer_count: int
|
|
189
|
+
resolved_samples: dict (role → local_path)
|
|
190
|
+
warnings: list (unresolved layers, missing samples, etc.)
|
|
191
|
+
error: str (when ok=False)
|
|
192
|
+
|
|
193
|
+
When ok=False, callers should fall back to committing the scaffold
|
|
194
|
+
plan instead of dropping the branch — the scaffolding is still
|
|
195
|
+
useful as a track/scene skeleton the user can populate manually.
|
|
196
|
+
|
|
197
|
+
This function is async because ComposerEngine.compose() is async
|
|
198
|
+
(it awaits Splice / filesystem sample resolution).
|
|
199
|
+
"""
|
|
200
|
+
import logging
|
|
201
|
+
logger = logging.getLogger(__name__)
|
|
202
|
+
|
|
203
|
+
schema_version = producer_payload.get("schema_version") if producer_payload else None
|
|
204
|
+
intent_dict = (producer_payload or {}).get("intent")
|
|
205
|
+
|
|
206
|
+
if not intent_dict:
|
|
207
|
+
return {
|
|
208
|
+
"ok": False,
|
|
209
|
+
"error": (
|
|
210
|
+
"Composer branch producer_payload missing 'intent'. "
|
|
211
|
+
"This branch was likely emitted before PR3/v2 and cannot "
|
|
212
|
+
"be escalated — commit the scaffold plan instead."
|
|
213
|
+
),
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Rehydrate CompositionIntent from the payload dict. Tolerate unknown
|
|
217
|
+
# keys by only pulling the fields CompositionIntent understands — older
|
|
218
|
+
# schemas may have fewer fields, newer may have more.
|
|
219
|
+
try:
|
|
220
|
+
intent_fields = {
|
|
221
|
+
k: v for k, v in intent_dict.items()
|
|
222
|
+
if k in (
|
|
223
|
+
"genre", "sub_genre", "mood", "tempo", "key",
|
|
224
|
+
"descriptors", "explicit_elements", "energy",
|
|
225
|
+
"layer_count", "duration_bars",
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
intent = CompositionIntent(**intent_fields)
|
|
229
|
+
except Exception as exc:
|
|
230
|
+
return {
|
|
231
|
+
"ok": False,
|
|
232
|
+
"error": (
|
|
233
|
+
f"Failed to rehydrate CompositionIntent from producer_payload "
|
|
234
|
+
f"(schema_version={schema_version}): {exc}"
|
|
235
|
+
),
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
engine = ComposerEngine()
|
|
239
|
+
try:
|
|
240
|
+
result: CompositionResult = await engine.compose(
|
|
241
|
+
intent=intent,
|
|
242
|
+
dry_run=False,
|
|
243
|
+
max_credits=max_credits,
|
|
244
|
+
search_roots=search_roots or [],
|
|
245
|
+
splice_client=splice_client,
|
|
246
|
+
browser_client=browser_client,
|
|
247
|
+
)
|
|
248
|
+
except Exception as exc:
|
|
249
|
+
return {
|
|
250
|
+
"ok": False,
|
|
251
|
+
"error": f"ComposerEngine.compose() raised: {exc}",
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# Fallback when no layers resolved — explicit signal so callers can
|
|
255
|
+
# fall back to the scaffold instead of silently shipping an empty
|
|
256
|
+
# plan.
|
|
257
|
+
if not result.plan or len(result.layers) == 0:
|
|
258
|
+
return {
|
|
259
|
+
"ok": False,
|
|
260
|
+
"error": (
|
|
261
|
+
"ComposerEngine.compose() produced zero executable layers. "
|
|
262
|
+
"Sample resolution likely failed — check Splice credits, "
|
|
263
|
+
"filesystem roots, or browser connectivity. Falling back "
|
|
264
|
+
"to scaffold commit is the correct action."
|
|
265
|
+
),
|
|
266
|
+
"warnings": list(result.warnings),
|
|
267
|
+
"resolved_samples": dict(result.resolved_samples),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"ok": True,
|
|
272
|
+
"plan": list(result.plan),
|
|
273
|
+
"step_count": len(result.plan),
|
|
274
|
+
"layer_count": len(result.layers),
|
|
275
|
+
"resolved_samples": dict(result.resolved_samples),
|
|
276
|
+
"credits_estimated": result.credits_estimated,
|
|
277
|
+
"warnings": list(result.warnings),
|
|
278
|
+
"intent_used": intent.to_dict(),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
|
|
162
282
|
def _build_section_hypothesis_plan(intent: CompositionIntent, strategy_name: str) -> dict:
|
|
163
283
|
"""Build a lightweight, executable plan from an intent.
|
|
164
284
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
"""Composer Engine MCP tools —
|
|
1
|
+
"""Composer Engine MCP tools — 4 tools for auto-composition.
|
|
2
2
|
|
|
3
3
|
compose: full multi-layer composition from text prompt
|
|
4
4
|
augment_with_samples: add layers to existing session
|
|
5
5
|
get_composition_plan: dry run preview
|
|
6
|
+
propose_composer_branches (PR5/v2): multi-strategy branch hypotheses for
|
|
7
|
+
exploratory workflows (feeds create_experiment(seeds=...))
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
from __future__ import annotations
|
|
@@ -213,3 +215,58 @@ async def get_composition_plan(
|
|
|
213
215
|
"then step through each tool call in sequence."
|
|
214
216
|
)
|
|
215
217
|
return plan
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@mcp.tool()
|
|
221
|
+
def propose_composer_branches(
|
|
222
|
+
ctx: Context,
|
|
223
|
+
request_text: str,
|
|
224
|
+
count: int = 2,
|
|
225
|
+
freshness: float = 0.65,
|
|
226
|
+
) -> dict:
|
|
227
|
+
"""Emit N distinct compositional hypotheses for a single prompt (PR5/v2).
|
|
228
|
+
|
|
229
|
+
Branch-native companion to compose(): instead of one deterministic
|
|
230
|
+
layer plan, produces up to ``count`` BranchSeeds with different
|
|
231
|
+
strategic angles the user can audition via create_experiment +
|
|
232
|
+
run_experiment. Each seed carries a pre-compiled scaffolding plan
|
|
233
|
+
(set_tempo + create_midi_track per layer + create_scene per section)
|
|
234
|
+
that gets escalated to a fully resolved plan by commit_experiment
|
|
235
|
+
when the winning branch is chosen.
|
|
236
|
+
|
|
237
|
+
Strategies (gated on freshness):
|
|
238
|
+
canonical — intent unchanged, genre defaults
|
|
239
|
+
(shipped at every freshness level)
|
|
240
|
+
energy_shift — intent.energy inverted around 0.5
|
|
241
|
+
(freshness >= 0.4)
|
|
242
|
+
layer_contrast — one role swapped (pad-anchor instead of bass)
|
|
243
|
+
(freshness >= 0.7)
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
{
|
|
247
|
+
"request_text": str,
|
|
248
|
+
"branch_count": int,
|
|
249
|
+
"seeds": [BranchSeed.to_dict(), ...],
|
|
250
|
+
"compiled_plans": [plan_dict, ...] (parallel to seeds; scaffold),
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
Each seed's producer_payload carries {strategy, intent,
|
|
254
|
+
request_text, reason} so commit_experiment can rehydrate the
|
|
255
|
+
CompositionIntent and run the full ComposerEngine.compose() for
|
|
256
|
+
the winner.
|
|
257
|
+
"""
|
|
258
|
+
from .branch_producer import propose_composer_branches as _propose
|
|
259
|
+
|
|
260
|
+
pairs = _propose(
|
|
261
|
+
request_text=request_text,
|
|
262
|
+
kernel={"freshness": float(freshness)},
|
|
263
|
+
count=int(count),
|
|
264
|
+
)
|
|
265
|
+
seeds = [s.to_dict() for s, _ in pairs]
|
|
266
|
+
plans = [p for _, p in pairs]
|
|
267
|
+
return {
|
|
268
|
+
"request_text": request_text,
|
|
269
|
+
"branch_count": len(seeds),
|
|
270
|
+
"seeds": seeds,
|
|
271
|
+
"compiled_plans": plans,
|
|
272
|
+
}
|