livepilot 1.9.21 → 1.9.23
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/.claude-plugin/marketplace.json +3 -3
- package/.mcpbignore +40 -0
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +47 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +47 -72
- package/bin/livepilot.js +135 -0
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +13 -0
- package/livepilot/commands/arrange.md +42 -14
- package/livepilot/commands/beat.md +68 -21
- package/livepilot/commands/evaluate.md +23 -13
- package/livepilot/commands/mix.md +35 -11
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -17
- package/livepilot/skills/livepilot-arrangement/SKILL.md +2 -1
- package/livepilot/skills/livepilot-composition-engine/references/transition-archetypes.md +2 -2
- package/livepilot/skills/livepilot-core/SKILL.md +60 -4
- package/livepilot/skills/livepilot-core/references/device-atlas/distortion-and-character.md +11 -11
- package/livepilot/skills/livepilot-core/references/device-atlas/drums-and-percussion.md +25 -25
- package/livepilot/skills/livepilot-core/references/device-atlas/dynamics-and-punch.md +21 -21
- package/livepilot/skills/livepilot-core/references/device-atlas/eq-and-filtering.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/midi-tools.md +13 -13
- package/livepilot/skills/livepilot-core/references/device-atlas/movement-and-modulation.md +5 -5
- package/livepilot/skills/livepilot-core/references/device-atlas/space-and-depth.md +16 -16
- package/livepilot/skills/livepilot-core/references/device-atlas/spectral-and-weird.md +40 -40
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +3 -3
- package/livepilot/skills/livepilot-core/references/overview.md +4 -4
- package/livepilot/skills/livepilot-evaluation/SKILL.md +12 -8
- package/livepilot/skills/livepilot-evaluation/references/memory-promotion.md +2 -2
- package/livepilot/skills/livepilot-mix-engine/SKILL.md +1 -1
- package/livepilot/skills/livepilot-mix-engine/references/mix-moves.md +2 -2
- package/livepilot/skills/livepilot-mixing/SKILL.md +3 -1
- package/livepilot/skills/livepilot-notes/SKILL.md +2 -1
- package/livepilot/skills/livepilot-release/SKILL.md +15 -15
- package/livepilot/skills/livepilot-sound-design-engine/SKILL.md +2 -2
- package/livepilot/skills/livepilot-wonder/SKILL.md +62 -0
- package/livepilot.mcpb +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/manifest.json +91 -0
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/creative_constraints/__init__.py +6 -0
- package/mcp_server/creative_constraints/engine.py +277 -0
- package/mcp_server/creative_constraints/models.py +75 -0
- package/mcp_server/creative_constraints/tools.py +341 -0
- package/mcp_server/experiment/__init__.py +6 -0
- package/mcp_server/experiment/engine.py +213 -0
- package/mcp_server/experiment/models.py +120 -0
- package/mcp_server/experiment/tools.py +263 -0
- package/mcp_server/hook_hunter/__init__.py +5 -0
- package/mcp_server/hook_hunter/analyzer.py +342 -0
- package/mcp_server/hook_hunter/models.py +57 -0
- package/mcp_server/hook_hunter/tools.py +586 -0
- package/mcp_server/memory/taste_graph.py +261 -0
- package/mcp_server/memory/tools.py +88 -0
- package/mcp_server/mix_engine/critics.py +2 -2
- package/mcp_server/mix_engine/models.py +1 -1
- package/mcp_server/mix_engine/state_builder.py +2 -2
- package/mcp_server/musical_intelligence/__init__.py +8 -0
- package/mcp_server/musical_intelligence/detectors.py +421 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +221 -0
- package/mcp_server/preview_studio/__init__.py +5 -0
- package/mcp_server/preview_studio/engine.py +280 -0
- package/mcp_server/preview_studio/models.py +73 -0
- package/mcp_server/preview_studio/tools.py +423 -0
- package/mcp_server/runtime/session_kernel.py +96 -0
- package/mcp_server/runtime/tools.py +90 -1
- package/mcp_server/semantic_moves/__init__.py +13 -0
- package/mcp_server/semantic_moves/compiler.py +116 -0
- package/mcp_server/semantic_moves/mix_compilers.py +291 -0
- package/mcp_server/semantic_moves/mix_moves.py +157 -0
- package/mcp_server/semantic_moves/models.py +46 -0
- package/mcp_server/semantic_moves/performance_compilers.py +208 -0
- package/mcp_server/semantic_moves/performance_moves.py +81 -0
- package/mcp_server/semantic_moves/registry.py +32 -0
- package/mcp_server/semantic_moves/resolvers.py +126 -0
- package/mcp_server/semantic_moves/sound_design_compilers.py +266 -0
- package/mcp_server/semantic_moves/sound_design_moves.py +78 -0
- package/mcp_server/semantic_moves/tools.py +204 -0
- package/mcp_server/semantic_moves/transition_compilers.py +222 -0
- package/mcp_server/semantic_moves/transition_moves.py +76 -0
- package/mcp_server/server.py +10 -0
- package/mcp_server/session_continuity/__init__.py +6 -0
- package/mcp_server/session_continuity/models.py +86 -0
- package/mcp_server/session_continuity/tools.py +230 -0
- package/mcp_server/session_continuity/tracker.py +235 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +477 -0
- package/mcp_server/song_brain/models.py +132 -0
- package/mcp_server/song_brain/tools.py +294 -0
- package/mcp_server/stuckness_detector/__init__.py +5 -0
- package/mcp_server/stuckness_detector/detector.py +400 -0
- package/mcp_server/stuckness_detector/models.py +66 -0
- package/mcp_server/stuckness_detector/tools.py +195 -0
- package/mcp_server/tools/_conductor.py +104 -6
- package/mcp_server/tools/analyzer.py +1 -1
- package/mcp_server/tools/devices.py +34 -0
- package/mcp_server/wonder_mode/__init__.py +6 -0
- package/mcp_server/wonder_mode/diagnosis.py +84 -0
- package/mcp_server/wonder_mode/engine.py +493 -0
- package/mcp_server/wonder_mode/session.py +114 -0
- package/mcp_server/wonder_mode/tools.py +285 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/browser.py +4 -1
- package/remote_script/LivePilot/devices.py +29 -0
- package/remote_script/LivePilot/tracks.py +11 -4
- package/scripts/generate_tool_catalog.py +131 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Compilers for transition-domain semantic moves."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .compiler import CompiledPlan, CompiledStep, register_compiler
|
|
6
|
+
from .models import SemanticMove
|
|
7
|
+
from . import resolvers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _compile_increase_forward_motion(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
11
|
+
"""Compile 'increase_forward_motion': rising filter + rhythm push."""
|
|
12
|
+
steps = []
|
|
13
|
+
descriptions = []
|
|
14
|
+
|
|
15
|
+
melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
|
|
16
|
+
drums = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
|
|
17
|
+
|
|
18
|
+
for mt in melodic[:1]:
|
|
19
|
+
steps.append(CompiledStep(
|
|
20
|
+
tool="apply_automation_shape",
|
|
21
|
+
params={
|
|
22
|
+
"track_index": mt["index"],
|
|
23
|
+
"clip_index": 0,
|
|
24
|
+
"parameter_type": "device",
|
|
25
|
+
"device_index": 0,
|
|
26
|
+
"parameter_index": 0,
|
|
27
|
+
"curve_type": "exponential",
|
|
28
|
+
"start": 0.2,
|
|
29
|
+
"end": 0.7,
|
|
30
|
+
"duration": 4,
|
|
31
|
+
},
|
|
32
|
+
description=f"Rising filter sweep on {mt['name']} over 4 bars",
|
|
33
|
+
))
|
|
34
|
+
descriptions.append(f"Rising filter on {mt['name']}")
|
|
35
|
+
|
|
36
|
+
for dt in drums[:1]:
|
|
37
|
+
steps.append(CompiledStep(
|
|
38
|
+
tool="set_track_volume",
|
|
39
|
+
params={"track_index": dt["index"], "volume": 0.75},
|
|
40
|
+
description=f"Push {dt['name']} to 0.75 for forward drive",
|
|
41
|
+
))
|
|
42
|
+
descriptions.append(f"Push {dt['name']} forward")
|
|
43
|
+
|
|
44
|
+
for mt in melodic[:1]:
|
|
45
|
+
steps.append(CompiledStep(
|
|
46
|
+
tool="set_track_send",
|
|
47
|
+
params={"track_index": mt["index"], "send_index": 0, "value": 0.30},
|
|
48
|
+
description=f"Build reverb wash on {mt['name']}",
|
|
49
|
+
))
|
|
50
|
+
descriptions.append(f"Reverb wash on {mt['name']}")
|
|
51
|
+
|
|
52
|
+
steps.append(CompiledStep(
|
|
53
|
+
tool="get_track_meters",
|
|
54
|
+
params={"include_stereo": True},
|
|
55
|
+
description="Verify forward momentum — energy increasing",
|
|
56
|
+
))
|
|
57
|
+
|
|
58
|
+
return CompiledPlan(
|
|
59
|
+
move_id=move.move_id,
|
|
60
|
+
intent=move.intent,
|
|
61
|
+
steps=steps,
|
|
62
|
+
risk_level="low",
|
|
63
|
+
summary="; ".join(descriptions) if descriptions else "No melodic tracks for motion",
|
|
64
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _compile_open_chorus(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
69
|
+
"""Compile 'open_chorus': maximize width, energy, brightness."""
|
|
70
|
+
steps = []
|
|
71
|
+
descriptions = []
|
|
72
|
+
|
|
73
|
+
melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
|
|
74
|
+
drums = resolvers.find_tracks_by_role(kernel, ["drums"])
|
|
75
|
+
|
|
76
|
+
# Push all melodic tracks
|
|
77
|
+
for mt in melodic:
|
|
78
|
+
steps.append(CompiledStep(
|
|
79
|
+
tool="set_track_volume",
|
|
80
|
+
params={"track_index": mt["index"], "volume": 0.75},
|
|
81
|
+
description=f"Push {mt['name']} to 0.75 for chorus energy",
|
|
82
|
+
))
|
|
83
|
+
descriptions.append(f"Push {mt['name']}")
|
|
84
|
+
|
|
85
|
+
# Widen chords
|
|
86
|
+
for ct in resolvers.find_tracks_by_role(kernel, ["chords"])[:1]:
|
|
87
|
+
steps.append(CompiledStep(
|
|
88
|
+
tool="set_track_pan",
|
|
89
|
+
params={"track_index": ct["index"], "pan": -0.30},
|
|
90
|
+
description=f"Pan {ct['name']} left for width",
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
for lt in resolvers.find_tracks_by_role(kernel, ["lead"])[:1]:
|
|
94
|
+
steps.append(CompiledStep(
|
|
95
|
+
tool="set_track_pan",
|
|
96
|
+
params={"track_index": lt["index"], "pan": 0.25},
|
|
97
|
+
description=f"Pan {lt['name']} right for width",
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
# Increase sends for spaciousness
|
|
101
|
+
for mt in melodic[:2]:
|
|
102
|
+
steps.append(CompiledStep(
|
|
103
|
+
tool="set_track_send",
|
|
104
|
+
params={"track_index": mt["index"], "send_index": 0, "value": 0.30},
|
|
105
|
+
description=f"Add reverb space to {mt['name']}",
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
descriptions.append("Widen stereo + add space")
|
|
109
|
+
|
|
110
|
+
steps.append(CompiledStep(
|
|
111
|
+
tool="get_track_meters",
|
|
112
|
+
params={"include_stereo": True},
|
|
113
|
+
description="Verify chorus energy — all tracks at high level",
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
return CompiledPlan(
|
|
117
|
+
move_id=move.move_id,
|
|
118
|
+
intent=move.intent,
|
|
119
|
+
steps=steps,
|
|
120
|
+
risk_level="medium",
|
|
121
|
+
summary="; ".join(descriptions) if descriptions else "No tracks for chorus",
|
|
122
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _compile_create_breakdown(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
127
|
+
"""Compile 'create_breakdown': strip to minimal elements."""
|
|
128
|
+
steps = []
|
|
129
|
+
descriptions = []
|
|
130
|
+
|
|
131
|
+
drums = resolvers.find_tracks_by_role(kernel, ["drums", "percussion"])
|
|
132
|
+
bass = resolvers.find_tracks_by_role(kernel, ["bass"])
|
|
133
|
+
pads = resolvers.find_tracks_by_role(kernel, ["pad"])
|
|
134
|
+
|
|
135
|
+
for dt in drums:
|
|
136
|
+
steps.append(CompiledStep(
|
|
137
|
+
tool="set_track_volume",
|
|
138
|
+
params={"track_index": dt["index"], "volume": 0.25},
|
|
139
|
+
description=f"Strip {dt['name']} to 0.25 for breakdown",
|
|
140
|
+
))
|
|
141
|
+
descriptions.append(f"Strip {dt['name']}")
|
|
142
|
+
|
|
143
|
+
for bt in bass[:1]:
|
|
144
|
+
steps.append(CompiledStep(
|
|
145
|
+
tool="set_track_volume",
|
|
146
|
+
params={"track_index": bt["index"], "volume": 0.30},
|
|
147
|
+
description=f"Reduce {bt['name']} to 0.30",
|
|
148
|
+
))
|
|
149
|
+
descriptions.append(f"Reduce {bt['name']}")
|
|
150
|
+
|
|
151
|
+
# Increase reverb on remaining pads for depth
|
|
152
|
+
for pt in pads[:1]:
|
|
153
|
+
steps.append(CompiledStep(
|
|
154
|
+
tool="set_track_send",
|
|
155
|
+
params={"track_index": pt["index"], "send_index": 0, "value": 0.50},
|
|
156
|
+
description=f"Deep reverb on {pt['name']} for breakdown atmosphere",
|
|
157
|
+
))
|
|
158
|
+
descriptions.append(f"Deep reverb on {pt['name']}")
|
|
159
|
+
|
|
160
|
+
steps.append(CompiledStep(
|
|
161
|
+
tool="get_track_meters",
|
|
162
|
+
params={"include_stereo": True},
|
|
163
|
+
description="Verify breakdown — energy low, atmosphere present",
|
|
164
|
+
))
|
|
165
|
+
|
|
166
|
+
return CompiledPlan(
|
|
167
|
+
move_id=move.move_id,
|
|
168
|
+
intent=move.intent,
|
|
169
|
+
steps=steps,
|
|
170
|
+
risk_level="medium",
|
|
171
|
+
summary="; ".join(descriptions) if descriptions else "No tracks for breakdown",
|
|
172
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _compile_bridge_sections(move: SemanticMove, kernel: dict) -> CompiledPlan:
|
|
177
|
+
"""Compile 'bridge_sections': gentle filter motion + volume crossfade."""
|
|
178
|
+
steps = []
|
|
179
|
+
descriptions = []
|
|
180
|
+
|
|
181
|
+
melodic = resolvers.find_tracks_by_role(kernel, ["chords", "lead", "pad"])
|
|
182
|
+
|
|
183
|
+
for mt in melodic[:1]:
|
|
184
|
+
steps.append(CompiledStep(
|
|
185
|
+
tool="apply_automation_shape",
|
|
186
|
+
params={
|
|
187
|
+
"track_index": mt["index"],
|
|
188
|
+
"clip_index": 0,
|
|
189
|
+
"parameter_type": "send",
|
|
190
|
+
"send_index": 0,
|
|
191
|
+
"curve_type": "cosine",
|
|
192
|
+
"center": 0.25,
|
|
193
|
+
"amplitude": 0.15,
|
|
194
|
+
"duration": 4,
|
|
195
|
+
"density": 8,
|
|
196
|
+
},
|
|
197
|
+
description=f"Gentle reverb motion on {mt['name']} across bridge",
|
|
198
|
+
))
|
|
199
|
+
descriptions.append(f"Bridge motion on {mt['name']}")
|
|
200
|
+
|
|
201
|
+
steps.append(CompiledStep(
|
|
202
|
+
tool="get_track_meters",
|
|
203
|
+
params={"include_stereo": True},
|
|
204
|
+
description="Verify smooth bridge — no dropouts",
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
return CompiledPlan(
|
|
208
|
+
move_id=move.move_id,
|
|
209
|
+
intent=move.intent,
|
|
210
|
+
steps=steps,
|
|
211
|
+
risk_level="low",
|
|
212
|
+
summary="; ".join(descriptions) if descriptions else "No tracks for bridge",
|
|
213
|
+
requires_approval=(kernel.get("mode", "improve") != "explore"),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── Register ────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
register_compiler("increase_forward_motion", _compile_increase_forward_motion)
|
|
220
|
+
register_compiler("open_chorus", _compile_open_chorus)
|
|
221
|
+
register_compiler("create_breakdown", _compile_create_breakdown)
|
|
222
|
+
register_compiler("bridge_sections", _compile_bridge_sections)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Transition-domain semantic moves — musical intents for section transitions."""
|
|
2
|
+
|
|
3
|
+
from .models import SemanticMove
|
|
4
|
+
from .registry import register
|
|
5
|
+
|
|
6
|
+
INCREASE_FORWARD_MOTION = SemanticMove(
|
|
7
|
+
move_id="increase_forward_motion",
|
|
8
|
+
family="transition",
|
|
9
|
+
intent="Increase forward motion and momentum toward the next section",
|
|
10
|
+
targets={"motion": 0.5, "energy": 0.3, "tension": 0.2},
|
|
11
|
+
protect={"clarity": 0.6},
|
|
12
|
+
risk_level="low",
|
|
13
|
+
compile_plan=[
|
|
14
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "exponential", "description": "Rising filter cutoff over 4 bars"}, "description": "Rising filter sweep"},
|
|
15
|
+
{"tool": "set_track_volume", "params": {"description": "Push rhythm elements +5-8%"}, "description": "Push rhythm forward"},
|
|
16
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "linear", "description": "Rising reverb send for anticipation"}, "description": "Build reverb wash"},
|
|
17
|
+
],
|
|
18
|
+
verification_plan=[
|
|
19
|
+
{"tool": "get_track_meters", "check": "energy increasing, all tracks alive"},
|
|
20
|
+
],
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
OPEN_CHORUS = SemanticMove(
|
|
24
|
+
move_id="open_chorus",
|
|
25
|
+
family="transition",
|
|
26
|
+
intent="Open into a chorus — maximize width, energy, and brightness",
|
|
27
|
+
targets={"energy": 0.4, "width": 0.3, "contrast": 0.3},
|
|
28
|
+
protect={"clarity": 0.6, "cohesion": 0.5},
|
|
29
|
+
risk_level="medium",
|
|
30
|
+
compile_plan=[
|
|
31
|
+
{"tool": "set_track_volume", "params": {"description": "Push all melodic tracks +10-15%"}, "description": "Push chorus energy"},
|
|
32
|
+
{"tool": "set_track_pan", "params": {"description": "Widen stereo field on chords/pads"}, "description": "Widen stereo"},
|
|
33
|
+
{"tool": "set_track_send", "params": {"description": "Increase reverb/delay sends for spaciousness"}, "description": "Add space"},
|
|
34
|
+
],
|
|
35
|
+
verification_plan=[
|
|
36
|
+
{"tool": "get_track_meters", "check": "overall energy increased, stereo field wider"},
|
|
37
|
+
{"tool": "analyze_mix", "check": "no clipping, stereo.mono_risk is false"},
|
|
38
|
+
],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
CREATE_BREAKDOWN = SemanticMove(
|
|
42
|
+
move_id="create_breakdown",
|
|
43
|
+
family="transition",
|
|
44
|
+
intent="Create a breakdown — strip to minimal elements for contrast before rebuild",
|
|
45
|
+
targets={"contrast": 0.5, "depth": 0.3, "clarity": 0.2},
|
|
46
|
+
protect={"cohesion": 0.5},
|
|
47
|
+
risk_level="medium",
|
|
48
|
+
compile_plan=[
|
|
49
|
+
{"tool": "set_track_volume", "params": {"description": "Pull drums to 20-30%"}, "description": "Strip drums"},
|
|
50
|
+
{"tool": "set_track_volume", "params": {"description": "Pull bass to 30-40%"}, "description": "Reduce bass"},
|
|
51
|
+
{"tool": "set_track_send", "params": {"description": "Increase reverb send on remaining elements"}, "description": "Add reverb depth"},
|
|
52
|
+
],
|
|
53
|
+
verification_plan=[
|
|
54
|
+
{"tool": "get_track_meters", "check": "energy significantly reduced, at least one element still prominent"},
|
|
55
|
+
],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
BRIDGE_SECTIONS = SemanticMove(
|
|
59
|
+
move_id="bridge_sections",
|
|
60
|
+
family="transition",
|
|
61
|
+
intent="Bridge between two sections with a transitional passage — filter sweep + density shift",
|
|
62
|
+
targets={"motion": 0.4, "contrast": 0.3, "cohesion": 0.3},
|
|
63
|
+
protect={"clarity": 0.6},
|
|
64
|
+
risk_level="low",
|
|
65
|
+
compile_plan=[
|
|
66
|
+
{"tool": "apply_automation_shape", "params": {"curve_type": "cosine", "description": "Gentle filter sweep across bridge"}, "description": "Bridge filter motion"},
|
|
67
|
+
{"tool": "set_track_volume", "params": {"description": "Gentle volume crossfade between section elements"}, "description": "Crossfade elements"},
|
|
68
|
+
],
|
|
69
|
+
verification_plan=[
|
|
70
|
+
{"tool": "get_track_meters", "check": "smooth level transition, no dropouts"},
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Register all transition moves
|
|
75
|
+
for _move in [INCREASE_FORWARD_MOTION, OPEN_CHORUS, CREATE_BREAKDOWN, BRIDGE_SECTIONS]:
|
|
76
|
+
register(_move)
|
package/mcp_server/server.py
CHANGED
|
@@ -130,6 +130,16 @@ from .reference_engine import tools as reference_tools # noqa: F401, E402
|
|
|
130
130
|
from .translation_engine import tools as translation_tools # noqa: F401, E402
|
|
131
131
|
from .performance_engine import tools as performance_tools # noqa: F401, E402
|
|
132
132
|
from .runtime import safety_tools # noqa: F401, E402
|
|
133
|
+
from .semantic_moves import tools as semantic_move_tools # noqa: F401, E402
|
|
134
|
+
from .experiment import tools as experiment_tools # noqa: F401, E402
|
|
135
|
+
from .musical_intelligence import tools as musical_intel_tools # noqa: F401, E402
|
|
136
|
+
from .song_brain import tools as song_brain_tools # noqa: F401, E402
|
|
137
|
+
from .preview_studio import tools as preview_studio_tools # noqa: F401, E402
|
|
138
|
+
from .hook_hunter import tools as hook_hunter_tools # noqa: F401, E402
|
|
139
|
+
from .stuckness_detector import tools as stuckness_tools # noqa: F401, E402
|
|
140
|
+
from .wonder_mode import tools as wonder_mode_tools # noqa: F401, E402
|
|
141
|
+
from .session_continuity import tools as session_cont_tools # noqa: F401, E402
|
|
142
|
+
from .creative_constraints import tools as constraints_tools # noqa: F401, E402
|
|
133
143
|
|
|
134
144
|
|
|
135
145
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Session Continuity data models — pure dataclasses, zero I/O."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class CreativeThread:
|
|
12
|
+
"""An unresolved creative goal or direction."""
|
|
13
|
+
|
|
14
|
+
thread_id: str = ""
|
|
15
|
+
description: str = ""
|
|
16
|
+
domain: str = "" # "arrangement", "sound_design", "mix", "harmony", "identity"
|
|
17
|
+
status: str = "open" # "open", "resolved", "abandoned", "stale"
|
|
18
|
+
priority: float = 0.5
|
|
19
|
+
created_at_ms: int = 0
|
|
20
|
+
last_touched_ms: int = 0
|
|
21
|
+
|
|
22
|
+
def to_dict(self) -> dict:
|
|
23
|
+
return asdict(self)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_stale(self) -> bool:
|
|
27
|
+
"""A thread is stale if untouched for >30 minutes."""
|
|
28
|
+
now = int(time.time() * 1000)
|
|
29
|
+
return (now - self.last_touched_ms) > 30 * 60 * 1000 if self.last_touched_ms else False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class TurnResolution:
|
|
34
|
+
"""What happened in a single creative turn."""
|
|
35
|
+
|
|
36
|
+
turn_id: str = ""
|
|
37
|
+
request_text: str = ""
|
|
38
|
+
outcome: str = "" # "accepted", "rejected", "modified", "undone"
|
|
39
|
+
move_applied: str = ""
|
|
40
|
+
identity_effect: str = ""
|
|
41
|
+
user_sentiment: str = "" # "loved", "liked", "neutral", "disliked", "hated"
|
|
42
|
+
timestamp_ms: int = 0
|
|
43
|
+
|
|
44
|
+
def to_dict(self) -> dict:
|
|
45
|
+
return asdict(self)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass
|
|
49
|
+
class SessionStory:
|
|
50
|
+
"""The narrative of the current session."""
|
|
51
|
+
|
|
52
|
+
song_id: str = ""
|
|
53
|
+
identity_summary: str = ""
|
|
54
|
+
what_changed_last: str = ""
|
|
55
|
+
what_still_feels_open: list[str] = field(default_factory=list)
|
|
56
|
+
threads: list[CreativeThread] = field(default_factory=list)
|
|
57
|
+
turns: list[TurnResolution] = field(default_factory=list)
|
|
58
|
+
mood_arc: list[str] = field(default_factory=list) # sequence of moods
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
return {
|
|
62
|
+
"song_id": self.song_id,
|
|
63
|
+
"identity_summary": self.identity_summary,
|
|
64
|
+
"what_changed_last": self.what_changed_last,
|
|
65
|
+
"what_still_feels_open": self.what_still_feels_open,
|
|
66
|
+
"threads": [t.to_dict() for t in self.threads if t.status == "open"],
|
|
67
|
+
"recent_turns": [t.to_dict() for t in self.turns[-5:]],
|
|
68
|
+
"mood_arc": self.mood_arc[-10:],
|
|
69
|
+
"total_turns": len(self.turns),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class TasteIdentityRanking:
|
|
75
|
+
"""Result of ranking candidates with separated taste and identity."""
|
|
76
|
+
|
|
77
|
+
candidate_id: str = ""
|
|
78
|
+
taste_score: float = 0.0
|
|
79
|
+
identity_score: float = 0.0
|
|
80
|
+
composite_score: float = 0.0
|
|
81
|
+
taste_explanation: str = ""
|
|
82
|
+
identity_explanation: str = ""
|
|
83
|
+
recommendation: str = ""
|
|
84
|
+
|
|
85
|
+
def to_dict(self) -> dict:
|
|
86
|
+
return asdict(self)
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Session Continuity MCP tools — 7 tools for collaborative memory.
|
|
2
|
+
|
|
3
|
+
get_session_story — what the track was becoming, what changed, what's open
|
|
4
|
+
resume_last_intent — pick up where you left off
|
|
5
|
+
record_turn_resolution — log what happened in a creative turn
|
|
6
|
+
rank_by_taste_and_identity — rank candidates with separated taste/identity
|
|
7
|
+
open_creative_thread — open a new creative thread for exploration
|
|
8
|
+
list_open_creative_threads — list all open non-stale creative threads
|
|
9
|
+
explain_preference_vs_identity — explain taste vs identity tension
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from fastmcp import Context
|
|
15
|
+
|
|
16
|
+
from ..server import mcp
|
|
17
|
+
from . import tracker
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@mcp.tool()
|
|
21
|
+
def get_session_story(ctx: Context) -> dict:
|
|
22
|
+
"""Get the narrative of the current session.
|
|
23
|
+
|
|
24
|
+
At the start of a resumed session, the agent can say what the track
|
|
25
|
+
was trying to become, what changed last time, and what still feels open.
|
|
26
|
+
|
|
27
|
+
Returns identity summary, recent turns, open creative threads,
|
|
28
|
+
and mood arc.
|
|
29
|
+
"""
|
|
30
|
+
song_brain = _get_song_brain_dict()
|
|
31
|
+
story = tracker.get_session_story(song_brain)
|
|
32
|
+
return story.to_dict()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@mcp.tool()
|
|
36
|
+
def resume_last_intent(ctx: Context) -> dict:
|
|
37
|
+
"""Resume the most recent unresolved creative intent.
|
|
38
|
+
|
|
39
|
+
Finds the latest open creative thread and suggests continuing it.
|
|
40
|
+
Stale threads (untouched for >30 minutes) are excluded.
|
|
41
|
+
"""
|
|
42
|
+
return tracker.resume_last_intent()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
def record_turn_resolution(
|
|
47
|
+
ctx: Context,
|
|
48
|
+
request_text: str,
|
|
49
|
+
outcome: str = "accepted",
|
|
50
|
+
move_applied: str = "",
|
|
51
|
+
identity_effect: str = "",
|
|
52
|
+
user_sentiment: str = "neutral",
|
|
53
|
+
) -> dict:
|
|
54
|
+
"""Record what happened in a creative turn.
|
|
55
|
+
|
|
56
|
+
Call this after each significant creative action to build the
|
|
57
|
+
session story. Tracks outcomes, identity effects, and user sentiment.
|
|
58
|
+
|
|
59
|
+
request_text: what was requested
|
|
60
|
+
outcome: "accepted", "rejected", "modified", or "undone"
|
|
61
|
+
move_applied: which semantic move was used (if any)
|
|
62
|
+
identity_effect: "preserves", "evolves", "contrasts", or "resets"
|
|
63
|
+
user_sentiment: "loved", "liked", "neutral", "disliked", or "hated"
|
|
64
|
+
"""
|
|
65
|
+
turn = tracker.record_turn_resolution(
|
|
66
|
+
request_text=request_text,
|
|
67
|
+
outcome=outcome,
|
|
68
|
+
move_applied=move_applied,
|
|
69
|
+
identity_effect=identity_effect,
|
|
70
|
+
user_sentiment=user_sentiment,
|
|
71
|
+
)
|
|
72
|
+
return turn.to_dict()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@mcp.tool()
|
|
76
|
+
def rank_by_taste_and_identity(
|
|
77
|
+
ctx: Context,
|
|
78
|
+
candidates: list[dict] | None = None,
|
|
79
|
+
) -> dict:
|
|
80
|
+
"""Rank candidates with separated taste and identity scoring.
|
|
81
|
+
|
|
82
|
+
Taste (cross-session preference) ranks options.
|
|
83
|
+
Identity (in-song) constrains/shapes options.
|
|
84
|
+
Explicit user instructions override both.
|
|
85
|
+
|
|
86
|
+
candidates: list of dicts with at least "id", "novelty_level",
|
|
87
|
+
and "identity_effect" fields
|
|
88
|
+
|
|
89
|
+
Returns ranked list with taste_score, identity_score, composite,
|
|
90
|
+
and explanations for each.
|
|
91
|
+
"""
|
|
92
|
+
if not candidates:
|
|
93
|
+
return {"error": "No candidates provided", "rankings": []}
|
|
94
|
+
|
|
95
|
+
taste_graph = _get_taste_graph(ctx)
|
|
96
|
+
song_brain = _get_song_brain_dict()
|
|
97
|
+
|
|
98
|
+
rankings = tracker.rank_by_taste_and_identity(
|
|
99
|
+
candidates=candidates,
|
|
100
|
+
taste_graph=taste_graph,
|
|
101
|
+
song_brain=song_brain,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
"rankings": [r.to_dict() for r in rankings],
|
|
106
|
+
"note": "Identity has stronger weight inside a session; taste has stronger weight when choosing among viable options",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
def open_creative_thread(
|
|
112
|
+
ctx: Context,
|
|
113
|
+
description: str,
|
|
114
|
+
domain: str = "",
|
|
115
|
+
priority: float = 0.5,
|
|
116
|
+
) -> dict:
|
|
117
|
+
"""Open a new creative thread — an unresolved creative goal.
|
|
118
|
+
|
|
119
|
+
Use this to track intentions that span multiple actions, like
|
|
120
|
+
"develop the chorus hook" or "fix the transition energy."
|
|
121
|
+
Threads are surfaced by get_session_story and resume_last_intent.
|
|
122
|
+
|
|
123
|
+
description: what the creative goal is
|
|
124
|
+
domain: "arrangement", "sound_design", "mix", "harmony", "identity"
|
|
125
|
+
priority: 0-1 importance level
|
|
126
|
+
"""
|
|
127
|
+
if not description.strip():
|
|
128
|
+
return {"error": "description cannot be empty"}
|
|
129
|
+
|
|
130
|
+
thread = tracker.open_thread(description, domain=domain, priority=priority)
|
|
131
|
+
return thread.to_dict()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@mcp.tool()
|
|
135
|
+
def list_open_creative_threads(ctx: Context) -> dict:
|
|
136
|
+
"""List all open (non-stale) creative threads in the session.
|
|
137
|
+
|
|
138
|
+
Returns unresolved creative goals, abandoned directions worth
|
|
139
|
+
revisiting, and what the next best unresolved question is.
|
|
140
|
+
Stale threads (untouched for >30 minutes) are excluded.
|
|
141
|
+
"""
|
|
142
|
+
threads = tracker.list_open_threads()
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"threads": [t.to_dict() for t in threads],
|
|
146
|
+
"thread_count": len(threads),
|
|
147
|
+
"next_best": threads[0].to_dict() if threads else None,
|
|
148
|
+
"note": "Threads decay after 30 minutes of inactivity",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
def explain_preference_vs_identity(
|
|
154
|
+
ctx: Context,
|
|
155
|
+
candidate_id: str = "",
|
|
156
|
+
novelty_level: float = 0.5,
|
|
157
|
+
identity_effect: str = "preserves",
|
|
158
|
+
) -> dict:
|
|
159
|
+
"""Explain how taste preference and song identity score a candidate.
|
|
160
|
+
|
|
161
|
+
Shows the tension between what the user tends to like (taste)
|
|
162
|
+
and what the current song needs (identity). Useful for understanding
|
|
163
|
+
why a variant was ranked the way it was.
|
|
164
|
+
|
|
165
|
+
candidate_id: the candidate to explain
|
|
166
|
+
novelty_level: 0-1 how novel the candidate is
|
|
167
|
+
identity_effect: "preserves", "evolves", "contrasts", or "resets"
|
|
168
|
+
"""
|
|
169
|
+
taste_graph = _get_taste_graph(ctx)
|
|
170
|
+
song_brain = _get_song_brain_dict()
|
|
171
|
+
|
|
172
|
+
candidates = [{
|
|
173
|
+
"id": candidate_id,
|
|
174
|
+
"novelty_level": novelty_level,
|
|
175
|
+
"identity_effect": identity_effect,
|
|
176
|
+
}]
|
|
177
|
+
|
|
178
|
+
rankings = tracker.rank_by_taste_and_identity(
|
|
179
|
+
candidates=candidates,
|
|
180
|
+
taste_graph=taste_graph,
|
|
181
|
+
song_brain=song_brain,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not rankings:
|
|
185
|
+
return {"error": "Could not rank candidate"}
|
|
186
|
+
|
|
187
|
+
r = rankings[0]
|
|
188
|
+
return {
|
|
189
|
+
"candidate_id": candidate_id,
|
|
190
|
+
"taste_score": r.taste_score,
|
|
191
|
+
"identity_score": r.identity_score,
|
|
192
|
+
"composite_score": r.composite_score,
|
|
193
|
+
"taste_explanation": r.taste_explanation,
|
|
194
|
+
"identity_explanation": r.identity_explanation,
|
|
195
|
+
"recommendation": r.recommendation,
|
|
196
|
+
"tension": (
|
|
197
|
+
"aligned" if abs(r.taste_score - r.identity_score) < 0.2
|
|
198
|
+
else "in tension — taste and identity disagree"
|
|
199
|
+
),
|
|
200
|
+
"note": "Identity has stronger weight inside a session (0.65 vs 0.35)",
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ── Helpers ───────────────────────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _get_song_brain_dict() -> dict:
|
|
208
|
+
try:
|
|
209
|
+
from ..song_brain.tools import _current_brain
|
|
210
|
+
if _current_brain is not None:
|
|
211
|
+
return _current_brain.to_dict()
|
|
212
|
+
except Exception as _e:
|
|
213
|
+
if __debug__:
|
|
214
|
+
import sys
|
|
215
|
+
print(f"LivePilot: SongBrain unavailable in session_continuity: {_e}", file=sys.stderr)
|
|
216
|
+
return {}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _get_taste_graph(ctx: Context) -> dict:
|
|
220
|
+
"""Session-scoped taste graph — matches preview_studio pattern."""
|
|
221
|
+
try:
|
|
222
|
+
from ..memory.taste_graph import build_taste_graph
|
|
223
|
+
from ..memory.taste_memory import TasteMemoryStore
|
|
224
|
+
from ..memory.anti_memory import AntiMemoryStore
|
|
225
|
+
taste_store = ctx.lifespan_context.setdefault("taste_memory", TasteMemoryStore())
|
|
226
|
+
anti_store = ctx.lifespan_context.setdefault("anti_memory", AntiMemoryStore())
|
|
227
|
+
return build_taste_graph(taste_store=taste_store, anti_store=anti_store).to_dict()
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
return {}
|