livepilot 1.9.22 → 1.9.24
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 +3 -3
- package/CHANGELOG.md +84 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +141 -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 -23
- package/livepilot/commands/mix.md +34 -19
- package/livepilot/commands/perform.md +31 -19
- package/livepilot/commands/sounddesign.md +38 -25
- 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 +29 -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/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 +365 -0
- package/mcp_server/hook_hunter/models.py +58 -0
- package/mcp_server/hook_hunter/tools.py +588 -0
- package/mcp_server/memory/taste_graph.py +328 -0
- package/mcp_server/memory/tools.py +99 -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 +434 -0
- package/mcp_server/musical_intelligence/phrase_critic.py +163 -0
- package/mcp_server/musical_intelligence/tools.py +224 -0
- package/mcp_server/persistence/__init__.py +1 -0
- package/mcp_server/persistence/base_store.py +82 -0
- package/mcp_server/persistence/project_store.py +106 -0
- package/mcp_server/persistence/taste_store.py +122 -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 +74 -0
- package/mcp_server/preview_studio/tools.py +466 -0
- package/mcp_server/runtime/capability.py +66 -0
- package/mcp_server/runtime/capability_probe.py +118 -0
- package/mcp_server/runtime/execution_router.py +139 -0
- package/mcp_server/runtime/remote_commands.py +82 -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 +205 -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/services/__init__.py +1 -0
- package/mcp_server/services/motif_service.py +67 -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 +263 -0
- package/mcp_server/song_brain/__init__.py +6 -0
- package/mcp_server/song_brain/builder.py +504 -0
- package/mcp_server/song_brain/models.py +136 -0
- package/mcp_server/song_brain/tools.py +312 -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 +290 -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
- package/scripts/sync_metadata.py +132 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""Stuckness detection engine — pure computation, zero I/O.
|
|
2
|
+
|
|
3
|
+
Analyzes action history, session state, and patterns to detect
|
|
4
|
+
when the user is stuck and suggest rescue strategies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import Counter
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
from .models import RescueSuggestion, StucknessReport, StucknessSignal
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# ── Main detection ────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def detect_stuckness(
|
|
19
|
+
action_history: list[dict],
|
|
20
|
+
session_info: Optional[dict] = None,
|
|
21
|
+
song_brain: Optional[dict] = None,
|
|
22
|
+
section_count: int = 0,
|
|
23
|
+
) -> StucknessReport:
|
|
24
|
+
"""Detect whether the session is stuck.
|
|
25
|
+
|
|
26
|
+
Analyzes action history for repeated undos, local tweaking,
|
|
27
|
+
long loops without structural edits, and other stuckness signals.
|
|
28
|
+
"""
|
|
29
|
+
session_info = session_info or {}
|
|
30
|
+
song_brain = song_brain or {}
|
|
31
|
+
signals: list[StucknessSignal] = []
|
|
32
|
+
|
|
33
|
+
# 1. Repeated undos
|
|
34
|
+
undo_signal = _check_repeated_undos(action_history)
|
|
35
|
+
if undo_signal:
|
|
36
|
+
signals.append(undo_signal)
|
|
37
|
+
|
|
38
|
+
# 2. Local tweaking (many small changes in one area)
|
|
39
|
+
tweak_signal = _check_local_tweaking(action_history)
|
|
40
|
+
if tweak_signal:
|
|
41
|
+
signals.append(tweak_signal)
|
|
42
|
+
|
|
43
|
+
# 3. Long loop time without structural edits
|
|
44
|
+
loop_signal = _check_loop_without_structure(action_history, section_count)
|
|
45
|
+
if loop_signal:
|
|
46
|
+
signals.append(loop_signal)
|
|
47
|
+
|
|
48
|
+
# 4. Repeated asks without acceptance
|
|
49
|
+
repeat_signal = _check_repeated_requests(action_history)
|
|
50
|
+
if repeat_signal:
|
|
51
|
+
signals.append(repeat_signal)
|
|
52
|
+
|
|
53
|
+
# 5. Too many decorative layers
|
|
54
|
+
density_signal = _check_decoration_overload(session_info)
|
|
55
|
+
if density_signal:
|
|
56
|
+
signals.append(density_signal)
|
|
57
|
+
|
|
58
|
+
# 6. Identity unclear
|
|
59
|
+
identity_signal = _check_identity_unclear(song_brain)
|
|
60
|
+
if identity_signal:
|
|
61
|
+
signals.append(identity_signal)
|
|
62
|
+
|
|
63
|
+
# Compute overall confidence
|
|
64
|
+
if not signals:
|
|
65
|
+
return StucknessReport(confidence=0.0, level="flowing")
|
|
66
|
+
|
|
67
|
+
# Compound: strongest signal + 0.15 per additional signal (don't average)
|
|
68
|
+
strengths = sorted((s.strength for s in signals), reverse=True)
|
|
69
|
+
confidence = strengths[0]
|
|
70
|
+
for extra in strengths[1:]:
|
|
71
|
+
confidence += extra * 0.15
|
|
72
|
+
confidence = min(1.0, round(confidence, 3))
|
|
73
|
+
|
|
74
|
+
# Determine level
|
|
75
|
+
if confidence > 0.7:
|
|
76
|
+
level = "deeply_stuck"
|
|
77
|
+
elif confidence > 0.45:
|
|
78
|
+
level = "stuck"
|
|
79
|
+
elif confidence > 0.2:
|
|
80
|
+
level = "slowing"
|
|
81
|
+
else:
|
|
82
|
+
level = "flowing"
|
|
83
|
+
|
|
84
|
+
# Determine rescue types
|
|
85
|
+
primary, secondary = _classify_rescue_type(signals, song_brain, session_info)
|
|
86
|
+
diagnosis = _build_diagnosis(signals, level)
|
|
87
|
+
|
|
88
|
+
return StucknessReport(
|
|
89
|
+
confidence=confidence,
|
|
90
|
+
level=level,
|
|
91
|
+
signals=signals,
|
|
92
|
+
diagnosis=diagnosis,
|
|
93
|
+
primary_rescue_type=primary,
|
|
94
|
+
secondary_rescue_types=secondary,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── Signal checkers ───────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _check_repeated_undos(history: list[dict]) -> Optional[StucknessSignal]:
|
|
102
|
+
"""Check for repeated undone moves (kept=False in ledger entries)."""
|
|
103
|
+
recent = history[-20:] if len(history) > 20 else history
|
|
104
|
+
undo_count = sum(1 for a in recent if a.get("kept") is False)
|
|
105
|
+
|
|
106
|
+
if undo_count >= 4:
|
|
107
|
+
return StucknessSignal(
|
|
108
|
+
signal_type="repeated_undo",
|
|
109
|
+
strength=min(0.8, undo_count * 0.15),
|
|
110
|
+
evidence=f"{undo_count} undone moves in last {len(recent)} entries",
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _check_local_tweaking(history: list[dict]) -> Optional[StucknessSignal]:
|
|
116
|
+
"""Check for many small parameter changes in one local area."""
|
|
117
|
+
recent = history[-15:] if len(history) > 15 else history
|
|
118
|
+
param_tools = {"set_device_parameter", "set_track_volume", "set_track_pan",
|
|
119
|
+
"set_send_level", "set_clip_loop", "batch_set_parameters"}
|
|
120
|
+
param_entries = []
|
|
121
|
+
for entry in recent:
|
|
122
|
+
tools_used = [a.get("tool", "") for a in entry.get("actions", [])]
|
|
123
|
+
if any(t in param_tools for t in tools_used):
|
|
124
|
+
param_entries.append(entry)
|
|
125
|
+
|
|
126
|
+
if len(param_entries) >= 6:
|
|
127
|
+
scopes = Counter(
|
|
128
|
+
entry.get("scope", {}).get("track", entry.get("intent", ""))
|
|
129
|
+
for entry in param_entries
|
|
130
|
+
)
|
|
131
|
+
most_common = scopes.most_common(1)
|
|
132
|
+
if most_common and most_common[0][1] >= 4:
|
|
133
|
+
return StucknessSignal(
|
|
134
|
+
signal_type="local_tweaking",
|
|
135
|
+
strength=min(0.7, len(param_entries) * 0.1),
|
|
136
|
+
evidence=f"{len(param_entries)} parameter tweaks, mostly on {most_common[0][0]}",
|
|
137
|
+
)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _check_loop_without_structure(
|
|
142
|
+
history: list[dict], section_count: int
|
|
143
|
+
) -> Optional[StucknessSignal]:
|
|
144
|
+
"""Check for long work without structural changes."""
|
|
145
|
+
recent = history[-30:] if len(history) > 30 else history
|
|
146
|
+
structural_tools = {"create_clip", "delete_clip", "create_midi_track",
|
|
147
|
+
"create_audio_track", "delete_track", "duplicate_clip"}
|
|
148
|
+
structural = 0
|
|
149
|
+
for entry in recent:
|
|
150
|
+
tools_used = {a.get("tool", "") for a in entry.get("actions", [])}
|
|
151
|
+
if tools_used & structural_tools:
|
|
152
|
+
structural += 1
|
|
153
|
+
|
|
154
|
+
if len(recent) >= 15 and structural == 0:
|
|
155
|
+
return StucknessSignal(
|
|
156
|
+
signal_type="long_loop_no_structure",
|
|
157
|
+
strength=0.5,
|
|
158
|
+
evidence=f"{len(recent)} moves without any structural changes",
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if section_count <= 1 and len(recent) > 20:
|
|
162
|
+
return StucknessSignal(
|
|
163
|
+
signal_type="single_loop",
|
|
164
|
+
strength=0.4,
|
|
165
|
+
evidence="Working in a single loop/scene for extended period",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _check_repeated_requests(history: list[dict]) -> Optional[StucknessSignal]:
|
|
172
|
+
"""Check for repeated similar intents without acceptance."""
|
|
173
|
+
recent = history[-10:] if len(history) > 10 else history
|
|
174
|
+
intents = [a.get("intent", "").lower() for a in recent if a.get("intent")]
|
|
175
|
+
|
|
176
|
+
if len(intents) >= 3:
|
|
177
|
+
words = Counter()
|
|
178
|
+
for intent in intents:
|
|
179
|
+
for word in intent.split():
|
|
180
|
+
if len(word) > 3:
|
|
181
|
+
words[word] += 1
|
|
182
|
+
|
|
183
|
+
repeated = {w: c for w, c in words.items() if c >= 3}
|
|
184
|
+
if repeated:
|
|
185
|
+
return StucknessSignal(
|
|
186
|
+
signal_type="repeated_requests",
|
|
187
|
+
strength=0.5,
|
|
188
|
+
evidence=f"Repeated intent keywords: {', '.join(repeated.keys())}",
|
|
189
|
+
)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _check_decoration_overload(session_info: dict) -> Optional[StucknessSignal]:
|
|
194
|
+
"""Check for too many decorative layers without role clarity."""
|
|
195
|
+
track_count = session_info.get("track_count", 0)
|
|
196
|
+
if track_count > 16:
|
|
197
|
+
return StucknessSignal(
|
|
198
|
+
signal_type="high_density",
|
|
199
|
+
strength=min(0.6, (track_count - 16) * 0.05),
|
|
200
|
+
evidence=f"{track_count} tracks — may be too dense to progress",
|
|
201
|
+
)
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _check_identity_unclear(song_brain: dict) -> Optional[StucknessSignal]:
|
|
206
|
+
"""Check if song identity is unclear."""
|
|
207
|
+
confidence = song_brain.get("identity_confidence", 0.5)
|
|
208
|
+
if confidence < 0.3:
|
|
209
|
+
return StucknessSignal(
|
|
210
|
+
signal_type="identity_unclear",
|
|
211
|
+
strength=0.5,
|
|
212
|
+
evidence="Song identity is not clearly established",
|
|
213
|
+
)
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── Rescue classification ─────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _classify_rescue_type(
|
|
221
|
+
signals: list[StucknessSignal],
|
|
222
|
+
song_brain: dict,
|
|
223
|
+
session_info: dict,
|
|
224
|
+
) -> tuple[str, list[str]]:
|
|
225
|
+
"""Determine the best rescue type from signals."""
|
|
226
|
+
signal_types = {s.signal_type for s in signals}
|
|
227
|
+
|
|
228
|
+
primary = "contrast_needed" # default
|
|
229
|
+
secondary: list[str] = []
|
|
230
|
+
|
|
231
|
+
if "identity_unclear" in signal_types:
|
|
232
|
+
primary = "identity_unclear"
|
|
233
|
+
secondary = ["hook_underdeveloped", "too_safe_to_progress"]
|
|
234
|
+
elif "single_loop" in signal_types:
|
|
235
|
+
primary = "overpolished_loop"
|
|
236
|
+
secondary = ["section_missing", "contrast_needed"]
|
|
237
|
+
elif "high_density" in signal_types:
|
|
238
|
+
primary = "too_dense_to_progress"
|
|
239
|
+
secondary = ["contrast_needed", "identity_unclear"]
|
|
240
|
+
elif "local_tweaking" in signal_types:
|
|
241
|
+
primary = "overpolished_loop"
|
|
242
|
+
secondary = ["contrast_needed", "section_missing"]
|
|
243
|
+
elif "repeated_undo" in signal_types:
|
|
244
|
+
primary = "contrast_needed"
|
|
245
|
+
secondary = ["hook_underdeveloped", "too_safe_to_progress"]
|
|
246
|
+
elif "long_loop_no_structure" in signal_types:
|
|
247
|
+
primary = "section_missing"
|
|
248
|
+
secondary = ["contrast_needed", "transition_not_earned"]
|
|
249
|
+
|
|
250
|
+
return primary, secondary
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
# ── Rescue suggestions ────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def suggest_rescue(
|
|
257
|
+
report: StucknessReport,
|
|
258
|
+
mode: str = "gentle",
|
|
259
|
+
) -> list[RescueSuggestion]:
|
|
260
|
+
"""Generate rescue suggestions based on stuckness analysis."""
|
|
261
|
+
suggestions: list[RescueSuggestion] = []
|
|
262
|
+
|
|
263
|
+
rescue_strategies = {
|
|
264
|
+
"contrast_needed": RescueSuggestion(
|
|
265
|
+
rescue_type="contrast_needed",
|
|
266
|
+
title="Add contrast to break the plateau",
|
|
267
|
+
description="The session needs a moment that feels different from what's been happening.",
|
|
268
|
+
strategies=[
|
|
269
|
+
"Strip everything except the hook for 4-8 bars, then re-enter",
|
|
270
|
+
"Introduce a new timbral element that wasn't there before",
|
|
271
|
+
"Change the harmonic context (try a relative minor/major shift)",
|
|
272
|
+
"Create a rhythmic break — half-time or double-time feel",
|
|
273
|
+
],
|
|
274
|
+
),
|
|
275
|
+
"section_missing": RescueSuggestion(
|
|
276
|
+
rescue_type="section_missing",
|
|
277
|
+
title="Add a new section for structural progress",
|
|
278
|
+
description="The track needs more form — a new section would create momentum.",
|
|
279
|
+
strategies=[
|
|
280
|
+
"Create a B section that contrasts the current loop",
|
|
281
|
+
"Add an intro that sets up the main idea",
|
|
282
|
+
"Build a breakdown section that strips to essentials",
|
|
283
|
+
"Design a transition that earns the next section",
|
|
284
|
+
],
|
|
285
|
+
),
|
|
286
|
+
"hook_underdeveloped": RescueSuggestion(
|
|
287
|
+
rescue_type="hook_underdeveloped",
|
|
288
|
+
title="Develop the hook before adding more layers",
|
|
289
|
+
description="The most memorable idea needs more attention before the arrangement grows.",
|
|
290
|
+
strategies=[
|
|
291
|
+
"Write a variation of the hook for a different section",
|
|
292
|
+
"Add a countermelody that complements the hook",
|
|
293
|
+
"Create a stripped version of the hook for contrast sections",
|
|
294
|
+
"Make the hook hit harder — better sound design or arrangement support",
|
|
295
|
+
],
|
|
296
|
+
),
|
|
297
|
+
"transition_not_earned": RescueSuggestion(
|
|
298
|
+
rescue_type="transition_not_earned",
|
|
299
|
+
title="Build better transitions between sections",
|
|
300
|
+
description="Sections jump abruptly — earn the transitions.",
|
|
301
|
+
strategies=[
|
|
302
|
+
"Add a 2-4 bar transition between sections",
|
|
303
|
+
"Use filter sweeps or risers to build anticipation",
|
|
304
|
+
"Create drum fills or melodic ornaments at section boundaries",
|
|
305
|
+
"Use silence or space before the next section arrives",
|
|
306
|
+
],
|
|
307
|
+
),
|
|
308
|
+
"overpolished_loop": RescueSuggestion(
|
|
309
|
+
rescue_type="overpolished_loop",
|
|
310
|
+
title="Stop polishing — move forward structurally",
|
|
311
|
+
description="This loop is getting over-refined. Time to build form.",
|
|
312
|
+
strategies=[
|
|
313
|
+
"Duplicate the scene and subtract elements for a contrasting section",
|
|
314
|
+
"Record a live take over the loop to find new directions",
|
|
315
|
+
"Commit to the current state and start the arrangement",
|
|
316
|
+
"Create a completely different section from scratch",
|
|
317
|
+
],
|
|
318
|
+
),
|
|
319
|
+
"identity_unclear": RescueSuggestion(
|
|
320
|
+
rescue_type="identity_unclear",
|
|
321
|
+
title="Define the track's identity before adding more",
|
|
322
|
+
description="It's hard to progress when the track doesn't know what it is.",
|
|
323
|
+
strategies=[
|
|
324
|
+
"Identify or create one defining melodic/rhythmic idea",
|
|
325
|
+
"Choose a reference track and distill its key principles",
|
|
326
|
+
"Remove tracks that don't serve a clear purpose",
|
|
327
|
+
"Write a one-sentence description of what this track should feel like",
|
|
328
|
+
],
|
|
329
|
+
),
|
|
330
|
+
"too_dense_to_progress": RescueSuggestion(
|
|
331
|
+
rescue_type="too_dense_to_progress",
|
|
332
|
+
title="Subtract before adding more",
|
|
333
|
+
description="Too many elements fighting for attention. Simplify first.",
|
|
334
|
+
strategies=[
|
|
335
|
+
"Mute all tracks, then bring back only the essential ones",
|
|
336
|
+
"Delete or freeze tracks with no clear role",
|
|
337
|
+
"Create a stripped version as a new starting point",
|
|
338
|
+
"Focus on making 3-4 elements work perfectly instead of 12 elements existing",
|
|
339
|
+
],
|
|
340
|
+
),
|
|
341
|
+
"too_safe_to_progress": RescueSuggestion(
|
|
342
|
+
rescue_type="too_safe_to_progress",
|
|
343
|
+
title="Take a risk — the safe path isn't working",
|
|
344
|
+
description="Everything is technically correct but uninspired. Time for a bold move.",
|
|
345
|
+
strategies=[
|
|
346
|
+
"Try a dramatic sound design change on a key element",
|
|
347
|
+
"Add an unexpected harmonic or rhythmic element",
|
|
348
|
+
"Radically change the arrangement structure",
|
|
349
|
+
"Experiment with an extreme processing chain (distortion, granular, etc.)",
|
|
350
|
+
],
|
|
351
|
+
),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Primary suggestion
|
|
355
|
+
primary_strat = rescue_strategies.get(report.primary_rescue_type)
|
|
356
|
+
if primary_strat:
|
|
357
|
+
primary_strat.urgency = "high" if report.confidence > 0.6 else "medium"
|
|
358
|
+
suggestions.append(primary_strat)
|
|
359
|
+
|
|
360
|
+
# Secondary suggestions (in gentle mode, only show primary)
|
|
361
|
+
if mode == "direct":
|
|
362
|
+
for rt in report.secondary_rescue_types[:2]:
|
|
363
|
+
sec = rescue_strategies.get(rt)
|
|
364
|
+
if sec:
|
|
365
|
+
sec.urgency = "medium"
|
|
366
|
+
suggestions.append(sec)
|
|
367
|
+
|
|
368
|
+
return suggestions
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# ── Diagnosis builder ─────────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _build_diagnosis(signals: list[StucknessSignal], level: str) -> str:
|
|
375
|
+
"""Build a human-readable diagnosis from signals."""
|
|
376
|
+
if level == "flowing":
|
|
377
|
+
return "Session is flowing well — no intervention needed"
|
|
378
|
+
|
|
379
|
+
signal_descriptions = {
|
|
380
|
+
"repeated_undo": "frequent undos suggest dissatisfaction with results",
|
|
381
|
+
"local_tweaking": "lots of small parameter changes in one area",
|
|
382
|
+
"long_loop_no_structure": "no structural changes for a while",
|
|
383
|
+
"single_loop": "working in a single loop without expanding",
|
|
384
|
+
"repeated_requests": "similar requests being repeated",
|
|
385
|
+
"high_density": "track count is very high",
|
|
386
|
+
"identity_unclear": "the song's identity isn't clear yet",
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
parts = []
|
|
390
|
+
for s in signals:
|
|
391
|
+
desc = signal_descriptions.get(s.signal_type, s.signal_type)
|
|
392
|
+
parts.append(desc)
|
|
393
|
+
|
|
394
|
+
prefix = {
|
|
395
|
+
"slowing": "The session is slowing down",
|
|
396
|
+
"stuck": "The session appears stuck",
|
|
397
|
+
"deeply_stuck": "The session is deeply stuck",
|
|
398
|
+
}.get(level, "Momentum issue detected")
|
|
399
|
+
|
|
400
|
+
return f"{prefix}: {'; '.join(parts)}"
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Stuckness Detector data models — pure dataclasses, zero I/O."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass, field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
RESCUE_TYPES = [
|
|
9
|
+
"contrast_needed",
|
|
10
|
+
"section_missing",
|
|
11
|
+
"hook_underdeveloped",
|
|
12
|
+
"transition_not_earned",
|
|
13
|
+
"overpolished_loop",
|
|
14
|
+
"identity_unclear",
|
|
15
|
+
"too_dense_to_progress",
|
|
16
|
+
"too_safe_to_progress",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class StucknessSignal:
|
|
22
|
+
"""A single signal contributing to stuckness detection."""
|
|
23
|
+
|
|
24
|
+
signal_type: str = "" # "repeated_undo", "local_tweaking", "long_loop", etc.
|
|
25
|
+
strength: float = 0.0 # 0-1
|
|
26
|
+
evidence: str = ""
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict:
|
|
29
|
+
return asdict(self)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class StucknessReport:
|
|
34
|
+
"""Full stuckness analysis for a session."""
|
|
35
|
+
|
|
36
|
+
confidence: float = 0.0 # 0-1 how stuck the session is
|
|
37
|
+
level: str = "flowing" # "flowing", "slowing", "stuck", "deeply_stuck"
|
|
38
|
+
signals: list[StucknessSignal] = field(default_factory=list)
|
|
39
|
+
diagnosis: str = ""
|
|
40
|
+
primary_rescue_type: str = ""
|
|
41
|
+
secondary_rescue_types: list[str] = field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict:
|
|
44
|
+
return {
|
|
45
|
+
"confidence": round(self.confidence, 3),
|
|
46
|
+
"level": self.level,
|
|
47
|
+
"signals": [s.to_dict() for s in self.signals],
|
|
48
|
+
"diagnosis": self.diagnosis,
|
|
49
|
+
"primary_rescue_type": self.primary_rescue_type,
|
|
50
|
+
"secondary_rescue_types": self.secondary_rescue_types,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class RescueSuggestion:
|
|
56
|
+
"""A momentum rescue suggestion."""
|
|
57
|
+
|
|
58
|
+
rescue_type: str = ""
|
|
59
|
+
title: str = ""
|
|
60
|
+
description: str = ""
|
|
61
|
+
urgency: str = "medium" # "low", "medium", "high"
|
|
62
|
+
strategies: list[str] = field(default_factory=list)
|
|
63
|
+
identity_effect: str = "preserves"
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> dict:
|
|
66
|
+
return asdict(self)
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Stuckness Detector MCP tools — 3 tools for momentum rescue.
|
|
2
|
+
|
|
3
|
+
detect_stuckness — identify whether the session is losing momentum
|
|
4
|
+
suggest_momentum_rescue — get strategic rescue suggestions
|
|
5
|
+
start_rescue_workflow — structured step-by-step rescue for a stuckness type
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from fastmcp import Context
|
|
11
|
+
|
|
12
|
+
from ..server import mcp
|
|
13
|
+
from . import detector
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_ableton(ctx: Context):
|
|
17
|
+
return ctx.lifespan_context["ableton"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _get_action_history(ctx: Context) -> list[dict]:
|
|
21
|
+
"""Get recent action history from the session-scoped action ledger.
|
|
22
|
+
|
|
23
|
+
Returns move entries as dicts for stuckness pattern analysis:
|
|
24
|
+
repeated undos, local-tweaking, loop-without-structure detection.
|
|
25
|
+
Falls back to empty list when no ledger data exists (graceful degradation).
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
from ..runtime.action_ledger import SessionLedger
|
|
29
|
+
ledger = ctx.lifespan_context.get("action_ledger")
|
|
30
|
+
if isinstance(ledger, SessionLedger):
|
|
31
|
+
recent = ledger.get_recent_moves(limit=20)
|
|
32
|
+
return [e.to_dict() for e in recent]
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_session_and_brain(ctx: Context) -> tuple[dict, dict, int]:
|
|
39
|
+
"""Fetch session info, song brain, and section count."""
|
|
40
|
+
ableton = _get_ableton(ctx)
|
|
41
|
+
session_info: dict = {}
|
|
42
|
+
song_brain: dict = {}
|
|
43
|
+
section_count = 0
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
session_info = ableton.send_command("get_session_info", {})
|
|
47
|
+
section_count = session_info.get("scene_count", 0)
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
from ..song_brain.tools import _current_brain
|
|
53
|
+
if _current_brain is not None:
|
|
54
|
+
song_brain = _current_brain.to_dict()
|
|
55
|
+
except Exception as _e:
|
|
56
|
+
if __debug__:
|
|
57
|
+
import sys
|
|
58
|
+
print(f"LivePilot: SongBrain unavailable in stuckness_detector: {_e}", file=sys.stderr)
|
|
59
|
+
|
|
60
|
+
return session_info, song_brain, section_count
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@mcp.tool()
|
|
64
|
+
def detect_stuckness(ctx: Context) -> dict:
|
|
65
|
+
"""Detect whether the session is losing momentum.
|
|
66
|
+
|
|
67
|
+
Analyzes action history for stuckness signals:
|
|
68
|
+
- repeated undos
|
|
69
|
+
- many low-impact parameter changes in one area
|
|
70
|
+
- long loop time with no structural edits
|
|
71
|
+
- repeated requests without acceptance
|
|
72
|
+
- too many decorative layers without role clarity
|
|
73
|
+
- unclear song identity
|
|
74
|
+
|
|
75
|
+
Returns confidence level, diagnosis, and recommended rescue type.
|
|
76
|
+
Use this proactively when the user seems to be going in circles.
|
|
77
|
+
"""
|
|
78
|
+
history = _get_action_history(ctx)
|
|
79
|
+
session_info, song_brain, section_count = _get_session_and_brain(ctx)
|
|
80
|
+
|
|
81
|
+
report = detector.detect_stuckness(
|
|
82
|
+
action_history=history,
|
|
83
|
+
session_info=session_info,
|
|
84
|
+
song_brain=song_brain,
|
|
85
|
+
section_count=section_count,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return report.to_dict()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@mcp.tool()
|
|
92
|
+
def suggest_momentum_rescue(
|
|
93
|
+
ctx: Context,
|
|
94
|
+
mode: str = "gentle",
|
|
95
|
+
) -> dict:
|
|
96
|
+
"""Suggest strategic moves to restore session momentum.
|
|
97
|
+
|
|
98
|
+
First detects stuckness, then generates rescue suggestions.
|
|
99
|
+
In "gentle" mode, provides the top suggestion. In "direct" mode,
|
|
100
|
+
provides up to 3 rescue strategies.
|
|
101
|
+
|
|
102
|
+
mode: "gentle" (one suggestion) or "direct" (up to 3 suggestions)
|
|
103
|
+
|
|
104
|
+
Returns rescue suggestions with strategies and identity effects.
|
|
105
|
+
"""
|
|
106
|
+
if mode not in ("gentle", "direct"):
|
|
107
|
+
mode = "gentle"
|
|
108
|
+
|
|
109
|
+
history = _get_action_history(ctx)
|
|
110
|
+
session_info, song_brain, section_count = _get_session_and_brain(ctx)
|
|
111
|
+
|
|
112
|
+
report = detector.detect_stuckness(
|
|
113
|
+
action_history=history,
|
|
114
|
+
session_info=session_info,
|
|
115
|
+
song_brain=song_brain,
|
|
116
|
+
section_count=section_count,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
if report.level == "flowing":
|
|
120
|
+
return {
|
|
121
|
+
"stuckness": report.to_dict(),
|
|
122
|
+
"note": "Session is flowing well — no rescue needed",
|
|
123
|
+
"suggestions": [],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
suggestions = detector.suggest_rescue(report, mode)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
"stuckness": report.to_dict(),
|
|
130
|
+
"suggestions": [s.to_dict() for s in suggestions],
|
|
131
|
+
"suggestion_count": len(suggestions),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@mcp.tool()
|
|
136
|
+
def start_rescue_workflow(
|
|
137
|
+
ctx: Context,
|
|
138
|
+
rescue_type: str = "",
|
|
139
|
+
kernel_id: str = "",
|
|
140
|
+
) -> dict:
|
|
141
|
+
"""Start a structured rescue workflow for a specific stuckness type.
|
|
142
|
+
|
|
143
|
+
Provides a step-by-step action plan to restore session momentum.
|
|
144
|
+
Each rescue type has targeted strategies with identity-preserving defaults.
|
|
145
|
+
|
|
146
|
+
rescue_type: one of "contrast_needed", "section_missing",
|
|
147
|
+
"hook_underdeveloped", "transition_not_earned",
|
|
148
|
+
"overpolished_loop", "identity_unclear",
|
|
149
|
+
"too_dense_to_progress", "too_safe_to_progress"
|
|
150
|
+
kernel_id: optional session kernel reference
|
|
151
|
+
"""
|
|
152
|
+
from .models import RESCUE_TYPES
|
|
153
|
+
|
|
154
|
+
if not rescue_type:
|
|
155
|
+
return {
|
|
156
|
+
"error": "rescue_type is required",
|
|
157
|
+
"available_types": RESCUE_TYPES,
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if rescue_type not in RESCUE_TYPES:
|
|
161
|
+
return {
|
|
162
|
+
"error": f"Unknown rescue type: {rescue_type}",
|
|
163
|
+
"available_types": RESCUE_TYPES,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Build a rescue suggestion for this specific type
|
|
167
|
+
from .models import StucknessReport
|
|
168
|
+
report = StucknessReport(
|
|
169
|
+
confidence=0.6,
|
|
170
|
+
level="stuck",
|
|
171
|
+
primary_rescue_type=rescue_type,
|
|
172
|
+
secondary_rescue_types=[],
|
|
173
|
+
)
|
|
174
|
+
suggestions = detector.suggest_rescue(report, mode="direct")
|
|
175
|
+
|
|
176
|
+
if not suggestions:
|
|
177
|
+
return {"error": f"No rescue strategies available for {rescue_type}"}
|
|
178
|
+
|
|
179
|
+
rescue = suggestions[0]
|
|
180
|
+
|
|
181
|
+
# Build workflow steps from strategies
|
|
182
|
+
steps = [
|
|
183
|
+
{"step": i + 1, "action": strategy, "done": False}
|
|
184
|
+
for i, strategy in enumerate(rescue.strategies)
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
"rescue_type": rescue_type,
|
|
189
|
+
"title": rescue.title,
|
|
190
|
+
"description": rescue.description,
|
|
191
|
+
"steps": steps,
|
|
192
|
+
"identity_effect": rescue.identity_effect,
|
|
193
|
+
"urgency": rescue.urgency,
|
|
194
|
+
"note": "Complete steps in order. Each step should be followed by evaluation.",
|
|
195
|
+
}
|