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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Compilers for device-mutation semantic moves (v1.20).
|
|
2
|
+
|
|
3
|
+
Pure functions. Read ``kernel["seed_args"]`` for the user's target, emit
|
|
4
|
+
CompiledPlan steps matching the expected tool signatures
|
|
5
|
+
(``batch_set_parameters``, ``delete_device``, ``add_session_memory``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from .compiler import CompiledPlan, CompiledStep, register_compiler
|
|
11
|
+
from .models import SemanticMove
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _empty_plan(move: SemanticMove, warnings: list[str]) -> CompiledPlan:
|
|
15
|
+
return CompiledPlan(
|
|
16
|
+
move_id=move.move_id,
|
|
17
|
+
intent=move.intent,
|
|
18
|
+
steps=[],
|
|
19
|
+
risk_level=move.risk_level,
|
|
20
|
+
summary="; ".join(warnings) if warnings else "No plan compiled",
|
|
21
|
+
requires_approval=True,
|
|
22
|
+
warnings=warnings,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── configure_device ──────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _compile_configure_device(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
30
|
+
args = kernel.get("seed_args") or {}
|
|
31
|
+
track_index = args.get("track_index")
|
|
32
|
+
device_index = args.get("device_index")
|
|
33
|
+
overrides = args.get("param_overrides")
|
|
34
|
+
|
|
35
|
+
if track_index is None or device_index is None or overrides is None:
|
|
36
|
+
return _empty_plan(move, [
|
|
37
|
+
"configure_device requires seed_args.track_index + device_index + param_overrides"
|
|
38
|
+
])
|
|
39
|
+
if not isinstance(track_index, int) or not isinstance(device_index, int):
|
|
40
|
+
return _empty_plan(move, [
|
|
41
|
+
f"track_index and device_index must be ints, got "
|
|
42
|
+
f"{type(track_index).__name__}/{type(device_index).__name__}"
|
|
43
|
+
])
|
|
44
|
+
if device_index < 0:
|
|
45
|
+
return _empty_plan(move, [f"device_index must be non-negative, got {device_index}"])
|
|
46
|
+
if not isinstance(overrides, dict):
|
|
47
|
+
return _empty_plan(move, [
|
|
48
|
+
f"param_overrides must be a dict[str, Any], got {type(overrides).__name__}"
|
|
49
|
+
])
|
|
50
|
+
if not overrides:
|
|
51
|
+
return _empty_plan(move, [
|
|
52
|
+
"param_overrides is empty — nothing to configure (delete_device "
|
|
53
|
+
"is a different move)"
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
# WIRE-FORMAT NOTE: compiled steps use the remote_command backend,
|
|
57
|
+
# which calls ableton.send_command() directly — bypassing the MCP
|
|
58
|
+
# tool's ergonomic rename at mcp_server/tools/devices.py:292
|
|
59
|
+
# (_normalize_batch_entry). Ableton's Remote Script handler
|
|
60
|
+
# (remote_script/LivePilot/devices.py:149) reads `name_or_index`
|
|
61
|
+
# exclusively. Emit that key directly.
|
|
62
|
+
parameters = [
|
|
63
|
+
{"name_or_index": str(name), "value": value}
|
|
64
|
+
for name, value in overrides.items()
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
step = CompiledStep(
|
|
68
|
+
tool="batch_set_parameters",
|
|
69
|
+
params={
|
|
70
|
+
"track_index": track_index,
|
|
71
|
+
"device_index": device_index,
|
|
72
|
+
"parameters": parameters,
|
|
73
|
+
},
|
|
74
|
+
description=(
|
|
75
|
+
f"Configure device at track {track_index}, device_index {device_index} — "
|
|
76
|
+
f"set {len(parameters)} parameter(s): "
|
|
77
|
+
f"{', '.join(p['name_or_index'] for p in parameters)}"
|
|
78
|
+
),
|
|
79
|
+
verify_after=True,
|
|
80
|
+
backend="remote_command",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
return CompiledPlan(
|
|
84
|
+
move_id=move.move_id,
|
|
85
|
+
intent=move.intent,
|
|
86
|
+
steps=[step],
|
|
87
|
+
risk_level=move.risk_level,
|
|
88
|
+
summary=step.description,
|
|
89
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
90
|
+
warnings=[],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── remove_device ──────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _compile_remove_device(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
98
|
+
args = kernel.get("seed_args") or {}
|
|
99
|
+
track_index = args.get("track_index")
|
|
100
|
+
device_index = args.get("device_index")
|
|
101
|
+
reason = args.get("reason")
|
|
102
|
+
|
|
103
|
+
if track_index is None or device_index is None:
|
|
104
|
+
return _empty_plan(move, [
|
|
105
|
+
"remove_device requires seed_args.track_index + device_index"
|
|
106
|
+
])
|
|
107
|
+
if not isinstance(track_index, int) or not isinstance(device_index, int):
|
|
108
|
+
return _empty_plan(move, [
|
|
109
|
+
"track_index and device_index must be ints"
|
|
110
|
+
])
|
|
111
|
+
if device_index < 0:
|
|
112
|
+
return _empty_plan(move, [f"device_index must be non-negative, got {device_index}"])
|
|
113
|
+
if not isinstance(reason, str) or not reason.strip():
|
|
114
|
+
return _empty_plan(move, [
|
|
115
|
+
"remove_device requires a non-empty seed_args.reason — "
|
|
116
|
+
"destructive moves must be justified for the audit trail"
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
delete_step = CompiledStep(
|
|
120
|
+
tool="delete_device",
|
|
121
|
+
params={"track_index": track_index, "device_index": device_index},
|
|
122
|
+
description=(
|
|
123
|
+
f"Delete device at track {track_index}, device_index {device_index}"
|
|
124
|
+
),
|
|
125
|
+
verify_after=True,
|
|
126
|
+
backend="remote_command",
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
memory_step = CompiledStep(
|
|
130
|
+
tool="add_session_memory",
|
|
131
|
+
params={
|
|
132
|
+
"category": "device_removal",
|
|
133
|
+
"content": (
|
|
134
|
+
f"Removed track={track_index} device_index={device_index}: "
|
|
135
|
+
f"{reason.strip()}"
|
|
136
|
+
),
|
|
137
|
+
},
|
|
138
|
+
description="Log removal reason to session memory for audit",
|
|
139
|
+
verify_after=False,
|
|
140
|
+
backend="mcp_tool",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return CompiledPlan(
|
|
144
|
+
move_id=move.move_id,
|
|
145
|
+
intent=move.intent,
|
|
146
|
+
steps=[delete_step, memory_step],
|
|
147
|
+
risk_level=move.risk_level,
|
|
148
|
+
summary=f"{delete_step.description} — reason: {reason.strip()}",
|
|
149
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
150
|
+
warnings=[],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ── Register compilers ────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
register_compiler("configure_device", _compile_configure_device)
|
|
157
|
+
register_compiler("remove_device", _compile_remove_device)
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Device-mutation semantic moves (v1.20) — configure an existing device
|
|
2
|
+
in bulk, or remove one with an audit reason.
|
|
3
|
+
|
|
4
|
+
Both moves take user targets via ``kernel["seed_args"]``. configure_device
|
|
5
|
+
uses explicit ``param_overrides: dict`` rather than a preset library —
|
|
6
|
+
the preset-YAML infrastructure the original plan called for is deferred
|
|
7
|
+
to v1.21 so this commit can ship at-budget without widening blast radius.
|
|
8
|
+
Once a preset library lands, it can layer on top: the preset is just a
|
|
9
|
+
resolved param_overrides dict.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .models import SemanticMove
|
|
13
|
+
from .registry import register
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
CONFIGURE_DEVICE = SemanticMove(
|
|
17
|
+
move_id="configure_device",
|
|
18
|
+
family="sound_design",
|
|
19
|
+
intent=(
|
|
20
|
+
"Reconfigure an existing device in bulk — set multiple parameters "
|
|
21
|
+
"in a single undoable move. Takes track_index, device_index, and "
|
|
22
|
+
"a param_overrides dict ({param_name: value}) via seed_args."
|
|
23
|
+
),
|
|
24
|
+
# Reconfiguring a device touches timbre + depth + clarity in a general
|
|
25
|
+
# way; caller scopes intent through the specific param_overrides dict
|
|
26
|
+
# they pass, and taste alignment happens per-dimension at rank time.
|
|
27
|
+
targets={"timbre": 0.4, "depth": 0.3, "clarity": 0.3},
|
|
28
|
+
protect={},
|
|
29
|
+
risk_level="low",
|
|
30
|
+
plan_template=[
|
|
31
|
+
{
|
|
32
|
+
"tool": "batch_set_parameters",
|
|
33
|
+
"params": {
|
|
34
|
+
"description": (
|
|
35
|
+
"Apply param_overrides dict as a single batch_set_parameters call"
|
|
36
|
+
),
|
|
37
|
+
},
|
|
38
|
+
"description": "Configure device parameters in one move",
|
|
39
|
+
"backend": "remote_command",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
verification_plan=[
|
|
43
|
+
{
|
|
44
|
+
"tool": "get_device_parameters",
|
|
45
|
+
"check": "requested parameter values match the overrides",
|
|
46
|
+
"backend": "remote_command",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
REMOVE_DEVICE = SemanticMove(
|
|
52
|
+
move_id="remove_device",
|
|
53
|
+
family="sound_design",
|
|
54
|
+
intent=(
|
|
55
|
+
"Remove a device from a track — destructive but undoable via Live's "
|
|
56
|
+
"undo stack. Takes track_index, device_index, and a human-readable "
|
|
57
|
+
"reason via seed_args. Reason is logged to session memory for audit."
|
|
58
|
+
),
|
|
59
|
+
targets={},
|
|
60
|
+
# Removing a device on an active signal path can silence audio entirely.
|
|
61
|
+
# Caller is responsible for ensuring the device isn't load-bearing.
|
|
62
|
+
protect={"signal_integrity": 0.9},
|
|
63
|
+
risk_level="medium",
|
|
64
|
+
plan_template=[
|
|
65
|
+
{
|
|
66
|
+
"tool": "delete_device",
|
|
67
|
+
"params": {"description": "Delete the target device"},
|
|
68
|
+
"description": "Remove the device",
|
|
69
|
+
"backend": "remote_command",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"tool": "add_session_memory",
|
|
73
|
+
"params": {
|
|
74
|
+
"description": (
|
|
75
|
+
"Log the reason under category=device_removal so the audit "
|
|
76
|
+
"trail survives anti-repetition scrubbing"
|
|
77
|
+
),
|
|
78
|
+
},
|
|
79
|
+
"description": "Log removal reason",
|
|
80
|
+
"backend": "mcp_tool",
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
verification_plan=[
|
|
84
|
+
{
|
|
85
|
+
"tool": "get_track_info",
|
|
86
|
+
"check": "device count on track decreased by 1",
|
|
87
|
+
"backend": "remote_command",
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
for _move in (CONFIGURE_DEVICE, REMOVE_DEVICE):
|
|
94
|
+
register(_move)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Compilers for metadata-family semantic moves (v1.20).
|
|
2
|
+
|
|
3
|
+
Pure functions. Each compiler emits one step per *provided* optional
|
|
4
|
+
field — the move lets the director collapse multiple one-line raw tool
|
|
5
|
+
calls into a single named intent without forcing the caller to fill in
|
|
6
|
+
fields they don't care about.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from .compiler import CompiledPlan, CompiledStep, register_compiler
|
|
12
|
+
from .models import SemanticMove
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _empty_plan(move: SemanticMove, warnings: list[str]) -> CompiledPlan:
|
|
16
|
+
return CompiledPlan(
|
|
17
|
+
move_id=move.move_id,
|
|
18
|
+
intent=move.intent,
|
|
19
|
+
steps=[],
|
|
20
|
+
risk_level=move.risk_level,
|
|
21
|
+
summary="; ".join(warnings) if warnings else "No plan compiled",
|
|
22
|
+
requires_approval=True,
|
|
23
|
+
warnings=warnings,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── configure_groove ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _compile_configure_groove(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
31
|
+
args = kernel.get("seed_args") or {}
|
|
32
|
+
track_index = args.get("track_index")
|
|
33
|
+
clip_indices = args.get("clip_indices")
|
|
34
|
+
groove_id = args.get("groove_id")
|
|
35
|
+
timing_amount = args.get("timing_amount") # optional
|
|
36
|
+
|
|
37
|
+
if track_index is None or clip_indices is None or groove_id is None:
|
|
38
|
+
return _empty_plan(move, [
|
|
39
|
+
"configure_groove requires seed_args.track_index + clip_indices + groove_id"
|
|
40
|
+
])
|
|
41
|
+
if not isinstance(track_index, int) or not isinstance(groove_id, int):
|
|
42
|
+
return _empty_plan(move, ["track_index and groove_id must be ints"])
|
|
43
|
+
if not isinstance(clip_indices, (list, tuple)) or not clip_indices:
|
|
44
|
+
return _empty_plan(move, ["clip_indices must be a non-empty list of ints"])
|
|
45
|
+
if not all(isinstance(ci, int) and ci >= 0 for ci in clip_indices):
|
|
46
|
+
return _empty_plan(move, [
|
|
47
|
+
"clip_indices entries must be non-negative ints"
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
warnings: list[str] = []
|
|
51
|
+
clamped_timing: float | None = None
|
|
52
|
+
if timing_amount is not None:
|
|
53
|
+
try:
|
|
54
|
+
t = float(timing_amount)
|
|
55
|
+
except (TypeError, ValueError):
|
|
56
|
+
return _empty_plan(move, [f"timing_amount must be numeric, got {timing_amount!r}"])
|
|
57
|
+
clamped_timing = max(0.0, min(1.0, t))
|
|
58
|
+
if clamped_timing != t:
|
|
59
|
+
warnings.append(
|
|
60
|
+
f"Clamped timing_amount {t} → {clamped_timing} (must be 0.0-1.0)"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
steps: list[CompiledStep] = []
|
|
64
|
+
for ci in clip_indices:
|
|
65
|
+
steps.append(CompiledStep(
|
|
66
|
+
tool="assign_clip_groove",
|
|
67
|
+
params={
|
|
68
|
+
"track_index": track_index,
|
|
69
|
+
"clip_index": ci,
|
|
70
|
+
"groove_id": groove_id,
|
|
71
|
+
},
|
|
72
|
+
description=f"Assign groove_id={groove_id} to track {track_index} clip {ci}",
|
|
73
|
+
verify_after=True,
|
|
74
|
+
backend="remote_command",
|
|
75
|
+
))
|
|
76
|
+
if clamped_timing is not None:
|
|
77
|
+
steps.append(CompiledStep(
|
|
78
|
+
tool="set_groove_params",
|
|
79
|
+
params={
|
|
80
|
+
"groove_id": groove_id,
|
|
81
|
+
"timing_amount": clamped_timing,
|
|
82
|
+
},
|
|
83
|
+
description=f"Set groove {groove_id} timing_amount to {clamped_timing}",
|
|
84
|
+
verify_after=True,
|
|
85
|
+
backend="remote_command",
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
return CompiledPlan(
|
|
89
|
+
move_id=move.move_id,
|
|
90
|
+
intent=move.intent,
|
|
91
|
+
steps=steps,
|
|
92
|
+
risk_level=move.risk_level,
|
|
93
|
+
summary=(
|
|
94
|
+
f"Assign groove {groove_id} to {len(clip_indices)} clip(s) on track {track_index}"
|
|
95
|
+
+ (f"; timing={clamped_timing}" if clamped_timing is not None else "")
|
|
96
|
+
),
|
|
97
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
98
|
+
warnings=warnings,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── set_scene_metadata ────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _compile_set_scene_metadata(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
106
|
+
args = kernel.get("seed_args") or {}
|
|
107
|
+
scene_index = args.get("scene_index")
|
|
108
|
+
name = args.get("name")
|
|
109
|
+
color_index = args.get("color_index")
|
|
110
|
+
tempo = args.get("tempo")
|
|
111
|
+
|
|
112
|
+
if scene_index is None:
|
|
113
|
+
return _empty_plan(move, ["set_scene_metadata requires seed_args.scene_index"])
|
|
114
|
+
if not isinstance(scene_index, int) or scene_index < 0:
|
|
115
|
+
return _empty_plan(move, [
|
|
116
|
+
f"scene_index must be a non-negative int, got {scene_index!r}"
|
|
117
|
+
])
|
|
118
|
+
if name is None and color_index is None and tempo is None:
|
|
119
|
+
return _empty_plan(move, [
|
|
120
|
+
"set_scene_metadata requires at least one of: name, color_index, tempo"
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
steps: list[CompiledStep] = []
|
|
124
|
+
summary_parts: list[str] = []
|
|
125
|
+
if name is not None:
|
|
126
|
+
if not isinstance(name, str):
|
|
127
|
+
return _empty_plan(move, ["name must be a string"])
|
|
128
|
+
steps.append(CompiledStep(
|
|
129
|
+
tool="set_scene_name",
|
|
130
|
+
params={"scene_index": scene_index, "name": name},
|
|
131
|
+
description=f"Rename scene {scene_index} → '{name}'",
|
|
132
|
+
verify_after=True,
|
|
133
|
+
backend="remote_command",
|
|
134
|
+
))
|
|
135
|
+
summary_parts.append(f"name='{name}'")
|
|
136
|
+
if color_index is not None:
|
|
137
|
+
if not isinstance(color_index, int):
|
|
138
|
+
return _empty_plan(move, ["color_index must be int"])
|
|
139
|
+
steps.append(CompiledStep(
|
|
140
|
+
tool="set_scene_color",
|
|
141
|
+
params={"scene_index": scene_index, "color_index": color_index},
|
|
142
|
+
description=f"Color scene {scene_index} → index {color_index}",
|
|
143
|
+
verify_after=True,
|
|
144
|
+
backend="remote_command",
|
|
145
|
+
))
|
|
146
|
+
summary_parts.append(f"color={color_index}")
|
|
147
|
+
if tempo is not None:
|
|
148
|
+
try:
|
|
149
|
+
tempo_f = float(tempo)
|
|
150
|
+
except (TypeError, ValueError):
|
|
151
|
+
return _empty_plan(move, [f"tempo must be numeric, got {tempo!r}"])
|
|
152
|
+
steps.append(CompiledStep(
|
|
153
|
+
tool="set_scene_tempo",
|
|
154
|
+
params={"scene_index": scene_index, "tempo": tempo_f},
|
|
155
|
+
description=f"Set scene {scene_index} tempo → {tempo_f}",
|
|
156
|
+
verify_after=True,
|
|
157
|
+
backend="remote_command",
|
|
158
|
+
))
|
|
159
|
+
summary_parts.append(f"tempo={tempo_f}")
|
|
160
|
+
|
|
161
|
+
return CompiledPlan(
|
|
162
|
+
move_id=move.move_id,
|
|
163
|
+
intent=move.intent,
|
|
164
|
+
steps=steps,
|
|
165
|
+
risk_level=move.risk_level,
|
|
166
|
+
summary=f"Scene {scene_index} metadata: {', '.join(summary_parts)}",
|
|
167
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
168
|
+
warnings=[],
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ── set_track_metadata ────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _compile_set_track_metadata(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
176
|
+
args = kernel.get("seed_args") or {}
|
|
177
|
+
track_index = args.get("track_index")
|
|
178
|
+
name = args.get("name")
|
|
179
|
+
color_index = args.get("color_index")
|
|
180
|
+
|
|
181
|
+
if track_index is None:
|
|
182
|
+
return _empty_plan(move, ["set_track_metadata requires seed_args.track_index"])
|
|
183
|
+
if not isinstance(track_index, int):
|
|
184
|
+
return _empty_plan(move, [f"track_index must be int, got {track_index!r}"])
|
|
185
|
+
if name is None and color_index is None:
|
|
186
|
+
return _empty_plan(move, [
|
|
187
|
+
"set_track_metadata requires at least one of: name, color_index"
|
|
188
|
+
])
|
|
189
|
+
|
|
190
|
+
steps: list[CompiledStep] = []
|
|
191
|
+
summary_parts: list[str] = []
|
|
192
|
+
if name is not None:
|
|
193
|
+
if not isinstance(name, str):
|
|
194
|
+
return _empty_plan(move, ["name must be a string"])
|
|
195
|
+
steps.append(CompiledStep(
|
|
196
|
+
tool="set_track_name",
|
|
197
|
+
params={"track_index": track_index, "name": name},
|
|
198
|
+
description=f"Rename track {track_index} → '{name}'",
|
|
199
|
+
verify_after=True,
|
|
200
|
+
backend="remote_command",
|
|
201
|
+
))
|
|
202
|
+
summary_parts.append(f"name='{name}'")
|
|
203
|
+
if color_index is not None:
|
|
204
|
+
if not isinstance(color_index, int):
|
|
205
|
+
return _empty_plan(move, ["color_index must be int"])
|
|
206
|
+
steps.append(CompiledStep(
|
|
207
|
+
tool="set_track_color",
|
|
208
|
+
params={"track_index": track_index, "color_index": color_index},
|
|
209
|
+
description=f"Color track {track_index} → index {color_index}",
|
|
210
|
+
verify_after=True,
|
|
211
|
+
backend="remote_command",
|
|
212
|
+
))
|
|
213
|
+
summary_parts.append(f"color={color_index}")
|
|
214
|
+
|
|
215
|
+
return CompiledPlan(
|
|
216
|
+
move_id=move.move_id,
|
|
217
|
+
intent=move.intent,
|
|
218
|
+
steps=steps,
|
|
219
|
+
risk_level=move.risk_level,
|
|
220
|
+
summary=f"Track {track_index} metadata: {', '.join(summary_parts)}",
|
|
221
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
222
|
+
warnings=[],
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── Register compilers ────────────────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
register_compiler("configure_groove", _compile_configure_groove)
|
|
229
|
+
register_compiler("set_scene_metadata", _compile_set_scene_metadata)
|
|
230
|
+
register_compiler("set_track_metadata", _compile_set_track_metadata)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Metadata-domain semantic moves (v1.20) — groove configuration, scene
|
|
2
|
+
metadata, and bundled track rename/color.
|
|
3
|
+
|
|
4
|
+
These moves collapse the most common "touch a few fields at once"
|
|
5
|
+
patterns from Phase 6 into single named intents. set_track_metadata
|
|
6
|
+
specifically bundles rename + color since they're always paired when a
|
|
7
|
+
director is labelling a new track.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .models import SemanticMove
|
|
11
|
+
from .registry import register
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
CONFIGURE_GROOVE = SemanticMove(
|
|
15
|
+
move_id="configure_groove",
|
|
16
|
+
family="arrangement",
|
|
17
|
+
intent=(
|
|
18
|
+
"Assign a groove to one or more clips and optionally tune its "
|
|
19
|
+
"timing amount — the Dilla-swing primitive. Takes track_index, "
|
|
20
|
+
"clip_indices (list), groove_id, and optional timing_amount "
|
|
21
|
+
"(0.0-1.0) via seed_args. Agent pre-resolves groove_id via list_grooves()."
|
|
22
|
+
),
|
|
23
|
+
targets={"groove": 0.6, "motion": 0.2, "contrast": 0.2},
|
|
24
|
+
protect={"clarity": 0.5},
|
|
25
|
+
risk_level="low",
|
|
26
|
+
plan_template=[
|
|
27
|
+
{
|
|
28
|
+
"tool": "assign_clip_groove",
|
|
29
|
+
"params": {"description": "Assign groove to each clip in clip_indices"},
|
|
30
|
+
"description": "Assign groove to clips",
|
|
31
|
+
"backend": "remote_command",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"tool": "set_groove_params",
|
|
35
|
+
"params": {"description": "Tune groove timing_amount if provided"},
|
|
36
|
+
"description": "Tune groove timing",
|
|
37
|
+
"backend": "remote_command",
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
verification_plan=[
|
|
41
|
+
{
|
|
42
|
+
"tool": "get_clip_groove",
|
|
43
|
+
"check": "each clip now shows the expected groove_id",
|
|
44
|
+
"backend": "remote_command",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
SET_SCENE_METADATA = SemanticMove(
|
|
50
|
+
move_id="set_scene_metadata",
|
|
51
|
+
family="arrangement",
|
|
52
|
+
intent=(
|
|
53
|
+
"Set scene metadata (name/color/tempo) in a single move. Each "
|
|
54
|
+
"field is optional — the compiler emits one step per provided "
|
|
55
|
+
"field. set_scene_tempo does affect playback timing when the "
|
|
56
|
+
"scene is fired; caller should consider."
|
|
57
|
+
),
|
|
58
|
+
targets={},
|
|
59
|
+
protect={},
|
|
60
|
+
risk_level="low",
|
|
61
|
+
plan_template=[
|
|
62
|
+
{
|
|
63
|
+
"tool": "set_scene_name",
|
|
64
|
+
"params": {"description": "When name is provided in seed_args"},
|
|
65
|
+
"description": "Rename scene",
|
|
66
|
+
"backend": "remote_command",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"tool": "set_scene_color",
|
|
70
|
+
"params": {"description": "When color_index is provided"},
|
|
71
|
+
"description": "Color scene",
|
|
72
|
+
"backend": "remote_command",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"tool": "set_scene_tempo",
|
|
76
|
+
"params": {"description": "When tempo is provided"},
|
|
77
|
+
"description": "Set scene tempo",
|
|
78
|
+
"backend": "remote_command",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
verification_plan=[
|
|
82
|
+
{
|
|
83
|
+
"tool": "get_scenes_info",
|
|
84
|
+
"check": "scene shows the new metadata fields",
|
|
85
|
+
"backend": "remote_command",
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
SET_TRACK_METADATA = SemanticMove(
|
|
91
|
+
move_id="set_track_metadata",
|
|
92
|
+
family="mix",
|
|
93
|
+
intent=(
|
|
94
|
+
"Set track name and/or color in a single bundled move. Both "
|
|
95
|
+
"fields are optional; at least one required. The bundling is "
|
|
96
|
+
"intentional — Phase 6 usage always pairs rename with color."
|
|
97
|
+
),
|
|
98
|
+
targets={},
|
|
99
|
+
protect={},
|
|
100
|
+
risk_level="low",
|
|
101
|
+
plan_template=[
|
|
102
|
+
{
|
|
103
|
+
"tool": "set_track_name",
|
|
104
|
+
"params": {"description": "When name is provided"},
|
|
105
|
+
"description": "Rename track",
|
|
106
|
+
"backend": "remote_command",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"tool": "set_track_color",
|
|
110
|
+
"params": {"description": "When color_index is provided"},
|
|
111
|
+
"description": "Color track",
|
|
112
|
+
"backend": "remote_command",
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
verification_plan=[
|
|
116
|
+
{
|
|
117
|
+
"tool": "get_track_info",
|
|
118
|
+
"check": "track name / color match the requested values",
|
|
119
|
+
"backend": "remote_command",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
for _move in (CONFIGURE_GROOVE, SET_SCENE_METADATA, SET_TRACK_METADATA):
|
|
126
|
+
register(_move)
|