livepilot 1.10.4 → 1.10.6

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.
Files changed (74) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/AGENTS.md +3 -3
  3. package/CHANGELOG.md +148 -0
  4. package/CONTRIBUTING.md +1 -1
  5. package/README.md +6 -6
  6. package/livepilot/.Codex-plugin/plugin.json +2 -2
  7. package/livepilot/.claude-plugin/plugin.json +2 -2
  8. package/livepilot/skills/livepilot-core/SKILL.md +4 -4
  9. package/livepilot/skills/livepilot-core/references/overview.md +3 -3
  10. package/livepilot/skills/livepilot-evaluation/references/capability-modes.md +1 -1
  11. package/livepilot/skills/livepilot-release/SKILL.md +5 -5
  12. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  13. package/m4l_device/livepilot_bridge.js +12 -1
  14. package/manifest.json +3 -3
  15. package/mcp_server/__init__.py +1 -1
  16. package/mcp_server/composer/sample_resolver.py +10 -6
  17. package/mcp_server/composer/tools.py +10 -6
  18. package/mcp_server/connection.py +6 -1
  19. package/mcp_server/creative_constraints/tools.py +9 -8
  20. package/mcp_server/experiment/engine.py +9 -5
  21. package/mcp_server/experiment/tools.py +9 -9
  22. package/mcp_server/hook_hunter/tools.py +14 -9
  23. package/mcp_server/m4l_bridge.py +11 -0
  24. package/mcp_server/memory/taste_graph.py +7 -2
  25. package/mcp_server/mix_engine/tools.py +8 -3
  26. package/mcp_server/musical_intelligence/tools.py +15 -10
  27. package/mcp_server/performance_engine/tools.py +6 -2
  28. package/mcp_server/preview_studio/tools.py +21 -15
  29. package/mcp_server/project_brain/tools.py +18 -10
  30. package/mcp_server/reference_engine/tools.py +7 -5
  31. package/mcp_server/runtime/capability_probe.py +10 -4
  32. package/mcp_server/runtime/tools.py +8 -2
  33. package/mcp_server/sample_engine/tools.py +394 -33
  34. package/mcp_server/semantic_moves/tools.py +5 -1
  35. package/mcp_server/server.py +10 -9
  36. package/mcp_server/services/motif_service.py +9 -3
  37. package/mcp_server/session_continuity/tools.py +7 -3
  38. package/mcp_server/session_continuity/tracker.py +9 -8
  39. package/mcp_server/song_brain/tools.py +17 -12
  40. package/mcp_server/splice_client/client.py +19 -6
  41. package/mcp_server/stuckness_detector/tools.py +8 -5
  42. package/mcp_server/tools/_agent_os_engine/__init__.py +52 -0
  43. package/mcp_server/tools/_agent_os_engine/critics.py +134 -0
  44. package/mcp_server/tools/_agent_os_engine/evaluation.py +206 -0
  45. package/mcp_server/tools/_agent_os_engine/models.py +132 -0
  46. package/mcp_server/tools/_agent_os_engine/taste.py +192 -0
  47. package/mcp_server/tools/_agent_os_engine/techniques.py +161 -0
  48. package/mcp_server/tools/_agent_os_engine/world_model.py +170 -0
  49. package/mcp_server/tools/_composition_engine/__init__.py +67 -0
  50. package/mcp_server/tools/_composition_engine/analysis.py +174 -0
  51. package/mcp_server/tools/_composition_engine/critics.py +522 -0
  52. package/mcp_server/tools/_composition_engine/gestures.py +230 -0
  53. package/mcp_server/tools/_composition_engine/harmony.py +70 -0
  54. package/mcp_server/tools/_composition_engine/models.py +193 -0
  55. package/mcp_server/tools/_composition_engine/sections.py +371 -0
  56. package/mcp_server/tools/_perception_engine.py +18 -11
  57. package/mcp_server/tools/agent_os.py +23 -15
  58. package/mcp_server/tools/analyzer.py +166 -7
  59. package/mcp_server/tools/automation.py +6 -1
  60. package/mcp_server/tools/composition.py +25 -16
  61. package/mcp_server/tools/devices.py +10 -6
  62. package/mcp_server/tools/motif.py +7 -2
  63. package/mcp_server/tools/planner.py +6 -2
  64. package/mcp_server/tools/research.py +13 -10
  65. package/mcp_server/transition_engine/tools.py +6 -1
  66. package/mcp_server/translation_engine/tools.py +8 -6
  67. package/mcp_server/wonder_mode/engine.py +8 -3
  68. package/mcp_server/wonder_mode/tools.py +29 -21
  69. package/package.json +2 -2
  70. package/remote_script/LivePilot/__init__.py +1 -1
  71. package/requirements.txt +6 -0
  72. package/livepilot.mcpb +0 -0
  73. package/mcp_server/tools/_agent_os_engine.py +0 -947
  74. package/mcp_server/tools/_composition_engine.py +0 -1530
@@ -0,0 +1,132 @@
1
+ """Part of the _agent_os_engine package — extracted from the single-file engine.
2
+
3
+ Pure-computation core. Callers should import from the package facade
4
+ (`from mcp_server.tools._agent_os_engine import X`), which re-exports from
5
+ these sub-modules.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import Any, Optional
13
+
14
+
15
+ # ── Shared utility ────────────────────────────────────────────────────
16
+ def _clamp(value: float, lo: float = 0.0, hi: float = 1.0) -> float:
17
+ """Clamp value to [lo, hi] range. Shared across evaluation + taste."""
18
+ return max(lo, min(hi, value))
19
+
20
+
21
+ # ── Quality Dimensions ────────────────────────────────────────────────
22
+ QUALITY_DIMENSIONS = frozenset({
23
+ "energy", "punch", "weight", "density", "brightness", "warmth",
24
+ "width", "depth", "motion", "contrast", "clarity", "cohesion",
25
+ "groove", "tension", "novelty", "polish", "emotion",
26
+ })
27
+
28
+ MEASURABLE_PROXIES: dict[str, str] = {
29
+ "brightness": "high + presence bands (averaged)",
30
+ "warmth": "low_mid band energy",
31
+ "weight": "sub + low bands (averaged)",
32
+ "clarity": "inverse of low_mid congestion",
33
+ "density": "spectral flatness (geometric/arithmetic mean ratio)",
34
+ "energy": "RMS level",
35
+ "punch": "crest factor in dB (20*log10(peak/rms))",
36
+ }
37
+
38
+ VALID_MODES = frozenset({"observe", "improve", "explore", "finish", "diagnose"})
39
+
40
+ VALID_RESEARCH_MODES = frozenset({"none", "targeted", "deep"})
41
+
42
+
43
+ # ── GoalVector ────────────────────────────────────────────────────────
44
+ @dataclass
45
+ class GoalVector:
46
+ """Compiled user intent as a machine-usable goal.
47
+
48
+ targets: dimension → weight (0-1). Weights should approximately sum to 1.0.
49
+ protect: dimension → minimum acceptable value (0-1). If a dimension drops
50
+ below this value after a move, the move is undone.
51
+ """
52
+ request_text: str
53
+ targets: dict[str, float] = field(default_factory=dict)
54
+ protect: dict[str, float] = field(default_factory=dict)
55
+ mode: str = "improve"
56
+ aggression: float = 0.5
57
+ research_mode: str = "none"
58
+
59
+ def to_dict(self) -> dict:
60
+ return asdict(self)
61
+
62
+ _ROLE_PATTERNS: list[tuple[str, str]] = [
63
+ (r"kick|bd|bass\s*drum", "kick"),
64
+ (r"snare|sd|snr", "snare"),
65
+ (r"clap|cp|hand\s*clap", "clap"),
66
+ (r"h(?:i)?[\s\-]?hat|hh|hat", "hihat"),
67
+ (r"perc|percussion|conga|bongo|shaker|tamb", "percussion"),
68
+ (r"sub\s*bass|sub", "sub_bass"),
69
+ (r"bass|low", "bass"),
70
+ (r"pad|atmosphere|atmo|ambient|drone", "pad"),
71
+ (r"lead|melody|mel|synth\s*lead", "lead"),
72
+ (r"chord|keys|piano|organ|rhodes", "chords"),
73
+ (r"vocal|vox|voice", "vocal"),
74
+ (r"fx|sfx|riser|sweep|noise|texture|tape", "texture"),
75
+ (r"string", "strings"),
76
+ (r"brass", "brass"),
77
+ (r"resamp|bounce|bus|group|master", "utility"),
78
+ ]
79
+
80
+ @dataclass
81
+ class WorldModel:
82
+ """Session state snapshot for critic analysis."""
83
+ topology: dict = field(default_factory=dict)
84
+ sonic: Optional[dict] = None
85
+ technical: dict = field(default_factory=dict)
86
+ track_roles: dict = field(default_factory=dict)
87
+
88
+ def to_dict(self) -> dict:
89
+ return asdict(self)
90
+
91
+
92
+ # ── Critics ───────────────────────────────────────────────────────────
93
+ @dataclass
94
+ class Issue:
95
+ """A diagnosed problem or opportunity."""
96
+ type: str
97
+ critic: str # "sonic" or "technical"
98
+ severity: float # 0.0-1.0
99
+ confidence: float # 0.0-1.0
100
+ affected_dimensions: list[str] = field(default_factory=list)
101
+ evidence: list[str] = field(default_factory=list)
102
+ recommended_actions: list[str] = field(default_factory=list)
103
+
104
+ def to_dict(self) -> dict:
105
+ return asdict(self)
106
+
107
+
108
+ # ── Technique Cards (Round 2) ─────────────────────────────────────────
109
+ @dataclass
110
+ class TechniqueCard:
111
+ """A structured, reusable production recipe — not just text."""
112
+ problem: str
113
+ context: list[str] = field(default_factory=list) # genre/style tags
114
+ devices: list[str] = field(default_factory=list) # what to load
115
+ method: str = "" # step-by-step instructions
116
+ verification: list[str] = field(default_factory=list) # what to check after
117
+ evidence: dict = field(default_factory=dict) # {sources, in_session_tested}
118
+
119
+ def to_dict(self) -> dict:
120
+ return asdict(self)
121
+
122
+ def to_memory_payload(self) -> dict:
123
+ """Convert to a payload suitable for memory_learn(type='technique_card')."""
124
+ return {
125
+ "problem": self.problem,
126
+ "context": self.context,
127
+ "devices": self.devices,
128
+ "method": self.method,
129
+ "verification": self.verification,
130
+ "evidence": self.evidence,
131
+ }
132
+
@@ -0,0 +1,192 @@
1
+ """Part of the _agent_os_engine package — extracted from the single-file engine.
2
+
3
+ Pure-computation core. Callers should import from the package facade
4
+ (`from mcp_server.tools._agent_os_engine import X`), which re-exports from
5
+ these sub-modules.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import Any, Optional
13
+
14
+ from .models import QUALITY_DIMENSIONS, _clamp
15
+
16
+
17
+ # ── Outcome Memory Analysis (Round 1) ────────────────────────────────
18
+ def analyze_outcome_history(outcomes: list[dict]) -> dict:
19
+ """Analyze accumulated outcome memories to identify user taste patterns.
20
+
21
+ outcomes: list of outcome technique payloads from memory_list(type="outcome")
22
+ Returns taste analysis: keep rate, dimension success, inferred preferences.
23
+ """
24
+ if not outcomes:
25
+ return {
26
+ "total_outcomes": 0,
27
+ "keep_rate": 0.0,
28
+ "dimension_success": {},
29
+ "common_kept_moves": [],
30
+ "common_undone_moves": [],
31
+ "taste_vector": {},
32
+ "notes": ["No outcome history — use the evaluation loop to build taste data"],
33
+ }
34
+
35
+ total = len(outcomes)
36
+ kept = [o for o in outcomes if o.get("kept", False)]
37
+ undone = [o for o in outcomes if not o.get("kept", False)]
38
+ keep_rate = len(kept) / total
39
+
40
+ # Dimension success: average improvement per dimension when kept
41
+ dimension_success: dict[str, list[float]] = {}
42
+ for o in kept:
43
+ for dim, change in o.get("dimension_changes", {}).items():
44
+ delta = change.get("delta", 0) if isinstance(change, dict) else 0
45
+ dimension_success.setdefault(dim, []).append(delta)
46
+
47
+ avg_dimension_success = {
48
+ dim: round(sum(vals) / len(vals), 4)
49
+ for dim, vals in dimension_success.items()
50
+ if vals
51
+ }
52
+
53
+ # Common move types
54
+ kept_moves = {}
55
+ undone_moves = {}
56
+ for o in kept:
57
+ move_name = o.get("move", {}).get("name", "unknown") if isinstance(o.get("move"), dict) else "unknown"
58
+ kept_moves[move_name] = kept_moves.get(move_name, 0) + 1
59
+ for o in undone:
60
+ move_name = o.get("move", {}).get("name", "unknown") if isinstance(o.get("move"), dict) else "unknown"
61
+ undone_moves[move_name] = undone_moves.get(move_name, 0) + 1
62
+
63
+ common_kept = sorted(kept_moves.items(), key=lambda x: -x[1])[:5]
64
+ common_undone = sorted(undone_moves.items(), key=lambda x: -x[1])[:5]
65
+
66
+ # Taste vector: which dimensions does this user care about?
67
+ # Weight by how often each dimension appears in kept outcomes
68
+ taste_vector: dict[str, float] = {}
69
+ for o in kept:
70
+ gv = o.get("goal_vector", {})
71
+ targets = gv.get("targets", {}) if isinstance(gv, dict) else {}
72
+ for dim, weight in targets.items():
73
+ taste_vector[dim] = taste_vector.get(dim, 0) + weight
74
+
75
+ # Normalize
76
+ taste_total = sum(taste_vector.values())
77
+ if taste_total > 0:
78
+ taste_vector = {k: round(v / taste_total, 3) for k, v in taste_vector.items()}
79
+
80
+ notes = []
81
+ if keep_rate < 0.3:
82
+ notes.append(f"Low keep rate ({keep_rate:.0%}) — agent may be too aggressive")
83
+ if keep_rate > 0.8:
84
+ notes.append(f"High keep rate ({keep_rate:.0%}) — agent is well-calibrated or too conservative")
85
+
86
+ return {
87
+ "total_outcomes": total,
88
+ "kept": len(kept),
89
+ "undone": len(undone),
90
+ "keep_rate": round(keep_rate, 3),
91
+ "dimension_success": avg_dimension_success,
92
+ "common_kept_moves": [{"move": m, "count": c} for m, c in common_kept],
93
+ "common_undone_moves": [{"move": m, "count": c} for m, c in common_undone],
94
+ "taste_vector": taste_vector,
95
+ "notes": notes,
96
+ }
97
+
98
+
99
+ # ── Taste Model (Round 4) ────────────────────────────────────────────
100
+ def compute_taste_fit(
101
+ goal: GoalVector,
102
+ outcome_history: Optional[list[dict]] = None,
103
+ ) -> float:
104
+ """Compute how well a goal aligns with the user's accumulated taste preferences.
105
+
106
+ Analyzes outcome history to build a taste vector (which dimensions matter
107
+ most to this user), then scores the current goal's alignment.
108
+
109
+ Returns 0.0-1.0 where:
110
+ - 0.0 = no data or goal doesn't match taste
111
+ - 1.0 = goal perfectly aligns with user's demonstrated preferences
112
+ """
113
+ if not outcome_history:
114
+ return 0.0
115
+
116
+ # Build taste vector from kept outcomes
117
+ taste_vector: dict[str, float] = {}
118
+ total_kept = 0
119
+
120
+ for o in outcome_history:
121
+ if not o.get("kept", False):
122
+ continue
123
+ total_kept += 1
124
+ gv = o.get("goal_vector", {})
125
+ targets = gv.get("targets", {}) if isinstance(gv, dict) else {}
126
+ for dim, weight in targets.items():
127
+ taste_vector[dim] = taste_vector.get(dim, 0) + weight
128
+
129
+ if not taste_vector or total_kept == 0:
130
+ return 0.0
131
+
132
+ # Normalize taste vector
133
+ taste_total = sum(taste_vector.values())
134
+ if taste_total > 0:
135
+ taste_vector = {k: v / taste_total for k, v in taste_vector.items()}
136
+
137
+ # Score: how much does the current goal overlap with taste preferences?
138
+ # Dot product of normalized goal weights and taste weights
139
+ goal_targets = goal.targets
140
+ if not goal_targets:
141
+ return 0.0
142
+
143
+ goal_total = sum(goal_targets.values())
144
+ if goal_total <= 0:
145
+ return 0.0
146
+
147
+ overlap = 0.0
148
+ for dim, weight in goal_targets.items():
149
+ normalized_weight = weight / goal_total
150
+ taste_weight = taste_vector.get(dim, 0)
151
+ overlap += normalized_weight * taste_weight
152
+
153
+ # Scale: overlap is typically small (product of two normalized distributions)
154
+ # Amplify so that moderate overlap gives a meaningful score
155
+ return _clamp(overlap * 4.0)
156
+
157
+ def get_taste_profile(outcome_history: list[dict]) -> dict:
158
+ """Build a full taste profile from outcome history.
159
+
160
+ Returns: {taste_vector, preferred_dimensions, avoided_dimensions,
161
+ keep_rate, sample_size}
162
+ """
163
+ analysis = analyze_outcome_history(outcome_history)
164
+ taste_vector = analysis.get("taste_vector", {})
165
+
166
+ # Identify preferred and avoided dimensions
167
+ preferred = sorted(taste_vector.items(), key=lambda x: -x[1])[:5]
168
+ avoided_dims: dict[str, float] = {}
169
+ for o in outcome_history:
170
+ if o.get("kept", False):
171
+ continue # Only look at undone moves
172
+ gv = o.get("goal_vector", {})
173
+ targets = gv.get("targets", {}) if isinstance(gv, dict) else {}
174
+ for dim, weight in targets.items():
175
+ avoided_dims[dim] = avoided_dims.get(dim, 0) + weight
176
+
177
+ if avoided_dims:
178
+ avoid_total = sum(avoided_dims.values())
179
+ if avoid_total > 0:
180
+ avoided_dims = {k: v / avoid_total for k, v in avoided_dims.items()}
181
+
182
+ avoided = sorted(avoided_dims.items(), key=lambda x: -x[1])[:5]
183
+
184
+ return {
185
+ "taste_vector": taste_vector,
186
+ "preferred_dimensions": [{"dim": d, "weight": round(w, 3)} for d, w in preferred],
187
+ "avoided_dimensions": [{"dim": d, "weight": round(w, 3)} for d, w in avoided],
188
+ "keep_rate": analysis.get("keep_rate", 0),
189
+ "sample_size": analysis.get("total_outcomes", 0),
190
+ "notes": analysis.get("notes", []),
191
+ }
192
+
@@ -0,0 +1,161 @@
1
+ """Part of the _agent_os_engine package — extracted from the single-file engine.
2
+
3
+ Pure-computation core. Callers should import from the package facade
4
+ (`from mcp_server.tools._agent_os_engine import X`), which re-exports from
5
+ these sub-modules.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import Any, Optional
13
+
14
+ from .models import TechniqueCard
15
+
16
+ def build_technique_card_from_outcome(outcome: dict) -> Optional[TechniqueCard]:
17
+ """Extract a technique card from a successful outcome.
18
+
19
+ Only produces a card if the outcome was kept and had meaningful improvement.
20
+ """
21
+ if not outcome.get("kept", False):
22
+ return None
23
+ if outcome.get("score", 0) < 0.6:
24
+ return None
25
+
26
+ gv = outcome.get("goal_vector", {})
27
+ move = outcome.get("move", {})
28
+ dim_changes = outcome.get("dimension_changes", {})
29
+
30
+ # Build problem description from goal
31
+ targets = gv.get("targets", {})
32
+ if not targets:
33
+ return None
34
+
35
+ top_dim = max(targets.items(), key=lambda x: x[1])[0] if targets else "general"
36
+ problem = f"Improve {top_dim} in production"
37
+
38
+ # Build method from move
39
+ method = move.get("name", "unknown technique")
40
+ if isinstance(move.get("actions"), list):
41
+ method = " → ".join(move["actions"])
42
+
43
+ # Build verification from dimension changes
44
+ verification = []
45
+ for dim, change in dim_changes.items():
46
+ if isinstance(change, dict) and change.get("delta", 0) > 0:
47
+ verification.append(f"{dim} should improve (was +{change['delta']:.3f})")
48
+
49
+ return TechniqueCard(
50
+ problem=problem,
51
+ context=list(gv.get("tags", [])) if isinstance(gv.get("tags"), list) else [],
52
+ devices=move.get("devices", []) if isinstance(move.get("devices"), list) else [],
53
+ method=method,
54
+ verification=verification,
55
+ evidence={"score": outcome.get("score", 0), "in_session_tested": True},
56
+ )
57
+
58
+
59
+ # ── Background Technique Mining (Round 3) ───────────────────────────
60
+ def should_mine_technique(
61
+ outcome: dict,
62
+ existing_techniques: Optional[list[dict]] = None,
63
+ ) -> bool:
64
+ """Determine if an outcome is novel enough to auto-create a technique card.
65
+
66
+ Returns True if:
67
+ - Score > 0.7 (high quality)
68
+ - At least one dimension improved by > 0.15
69
+ - No similar technique already exists in memory
70
+ """
71
+ if not outcome.get("kept", False):
72
+ return False
73
+ if outcome.get("score", 0) < 0.7:
74
+ return False
75
+
76
+ # Check for meaningful dimension improvement
77
+ dim_changes = outcome.get("dimension_changes", {})
78
+ has_significant_improvement = False
79
+ for dim, change in dim_changes.items():
80
+ delta = change.get("delta", 0) if isinstance(change, dict) else 0
81
+ if delta > 0.15:
82
+ has_significant_improvement = True
83
+ break
84
+
85
+ if not has_significant_improvement:
86
+ return False
87
+
88
+ # Check for novelty — don't create duplicate techniques
89
+ if existing_techniques:
90
+ move = outcome.get("move", {})
91
+ move_name = move.get("name", "") if isinstance(move, dict) else ""
92
+ if move_name:
93
+ for tech in existing_techniques:
94
+ payload = tech.get("payload", {})
95
+ existing_method = payload.get("method", "")
96
+ if move_name.lower() in existing_method.lower():
97
+ return False # Similar technique already exists
98
+
99
+ return True
100
+
101
+ def mine_technique_from_outcome(outcome: dict) -> Optional[TechniqueCard]:
102
+ """Extract a technique card from a high-quality outcome.
103
+
104
+ This is the "background mining" — when the agent detects a novel
105
+ approach that worked well, it auto-creates a technique card for future use.
106
+ """
107
+ if not outcome.get("kept", False):
108
+ return None
109
+
110
+ gv = outcome.get("goal_vector", {})
111
+ move = outcome.get("move", {})
112
+ dim_changes = outcome.get("dimension_changes", {})
113
+ score = outcome.get("score", 0)
114
+
115
+ # Build problem description
116
+ targets = gv.get("targets", {})
117
+ if targets:
118
+ top_dims = sorted(targets.items(), key=lambda x: -x[1])[:2]
119
+ problem = f"Improve {' and '.join(d for d, _ in top_dims)}"
120
+ else:
121
+ problem = "General production improvement"
122
+
123
+ # Build method
124
+ move_name = move.get("name", "unknown") if isinstance(move, dict) else str(move)
125
+ actions = move.get("actions", []) if isinstance(move, dict) else []
126
+ if isinstance(actions, list) and actions:
127
+ method = f"{move_name}: {' → '.join(str(a) for a in actions)}"
128
+ else:
129
+ method = move_name
130
+
131
+ # Build verification from what actually improved
132
+ verification = []
133
+ for dim, change in dim_changes.items():
134
+ if isinstance(change, dict) and change.get("delta", 0) > 0.05:
135
+ verification.append(
136
+ f"{dim} should improve (observed +{change['delta']:.3f})"
137
+ )
138
+
139
+ # Devices used
140
+ devices = move.get("devices", []) if isinstance(move, dict) else []
141
+ if not isinstance(devices, list):
142
+ devices = []
143
+
144
+ return TechniqueCard(
145
+ problem=problem,
146
+ context=list(gv.get("tags", [])) if isinstance(gv.get("tags"), list) else [],
147
+ devices=devices,
148
+ method=method,
149
+ verification=verification,
150
+ evidence={
151
+ "score": score,
152
+ "in_session_tested": True,
153
+ "auto_mined": True,
154
+ "dimension_improvements": {
155
+ dim: change.get("delta", 0)
156
+ for dim, change in dim_changes.items()
157
+ if isinstance(change, dict) and change.get("delta", 0) > 0
158
+ },
159
+ },
160
+ )
161
+
@@ -0,0 +1,170 @@
1
+ """Part of the _agent_os_engine package — extracted from the single-file engine.
2
+
3
+ Pure-computation core. Callers should import from the package facade
4
+ (`from mcp_server.tools._agent_os_engine import X`), which re-exports from
5
+ these sub-modules.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import math
10
+ import re
11
+ from dataclasses import asdict, dataclass, field
12
+ from typing import Any, Optional
13
+
14
+ from .models import QUALITY_DIMENSIONS, MEASURABLE_PROXIES, VALID_MODES, VALID_RESEARCH_MODES, _ROLE_PATTERNS, GoalVector, WorldModel
15
+
16
+ def validate_goal_vector(
17
+ request_text: str,
18
+ targets: dict[str, float],
19
+ protect: dict[str, float],
20
+ mode: str,
21
+ aggression: float,
22
+ research_mode: str,
23
+ ) -> GoalVector:
24
+ """Validate and construct a GoalVector. Raises ValueError on invalid input."""
25
+ if not request_text or not request_text.strip():
26
+ raise ValueError("request_text cannot be empty")
27
+
28
+ # Validate dimensions
29
+ for dim in targets:
30
+ if dim not in QUALITY_DIMENSIONS:
31
+ raise ValueError(
32
+ f"Unknown target dimension '{dim}'. "
33
+ f"Valid: {sorted(QUALITY_DIMENSIONS)}"
34
+ )
35
+ for dim in protect:
36
+ if dim not in QUALITY_DIMENSIONS:
37
+ raise ValueError(
38
+ f"Unknown protect dimension '{dim}'. "
39
+ f"Valid: {sorted(QUALITY_DIMENSIONS)}"
40
+ )
41
+
42
+ # Validate weights are non-negative
43
+ for dim, w in targets.items():
44
+ if w < 0.0:
45
+ raise ValueError(f"Target weight for '{dim}' must be >= 0.0, got {w}")
46
+ for dim, w in protect.items():
47
+ if not 0.0 <= w <= 1.0:
48
+ raise ValueError(f"Protect threshold for '{dim}' must be 0.0-1.0, got {w}")
49
+
50
+ if mode not in VALID_MODES:
51
+ raise ValueError(f"mode must be one of {sorted(VALID_MODES)}, got '{mode}'")
52
+ if research_mode not in VALID_RESEARCH_MODES:
53
+ raise ValueError(
54
+ f"research_mode must be one of {sorted(VALID_RESEARCH_MODES)}, "
55
+ f"got '{research_mode}'"
56
+ )
57
+ if not 0.0 <= aggression <= 1.0:
58
+ raise ValueError(f"aggression must be 0.0-1.0, got {aggression}")
59
+
60
+ # Normalize target weights to sum to ~1.0 if they don't already
61
+ total = sum(targets.values())
62
+ if targets and total > 0:
63
+ if abs(total - 1.0) > 0.01:
64
+ targets = {k: v / total for k, v in targets.items()}
65
+
66
+ return GoalVector(
67
+ request_text=request_text.strip(),
68
+ targets=targets,
69
+ protect=protect,
70
+ mode=mode,
71
+ aggression=aggression,
72
+ research_mode=research_mode,
73
+ )
74
+
75
+ def infer_track_role(track_name: str) -> str:
76
+ """Infer a track's musical role from its name. Returns 'unknown' if no match."""
77
+ name_lower = track_name.lower().strip()
78
+ for pattern, role in _ROLE_PATTERNS:
79
+ if re.search(pattern, name_lower):
80
+ return role
81
+ return "unknown"
82
+
83
+ def build_world_model_from_data(
84
+ session_info: dict,
85
+ spectrum: Optional[dict] = None,
86
+ rms: Optional[dict] = None,
87
+ detected_key: Optional[dict] = None,
88
+ flucoma_status: Optional[dict] = None,
89
+ track_infos: Optional[list[dict]] = None,
90
+ ) -> WorldModel:
91
+ """Assemble a WorldModel from raw tool outputs.
92
+
93
+ All parameters are optional — the model degrades gracefully when
94
+ analyzer data is unavailable.
95
+ """
96
+ # Topology
97
+ tracks = session_info.get("tracks", [])
98
+ topology = {
99
+ "tempo": session_info.get("tempo"),
100
+ "time_signature": f"{session_info.get('signature_numerator', 4)}/{session_info.get('signature_denominator', 4)}",
101
+ "track_count": session_info.get("track_count", 0),
102
+ "return_count": session_info.get("return_track_count", 0),
103
+ "scene_count": session_info.get("scene_count", 0),
104
+ "is_playing": session_info.get("is_playing", False),
105
+ "tracks": [
106
+ {
107
+ "index": t.get("index"),
108
+ "name": t.get("name", ""),
109
+ "has_midi": t.get("has_midi_input", False),
110
+ "has_audio": t.get("has_audio_input", False),
111
+ "mute": t.get("mute", False),
112
+ "solo": t.get("solo", False),
113
+ "arm": t.get("arm", False),
114
+ }
115
+ for t in tracks
116
+ ],
117
+ }
118
+
119
+ # Track roles
120
+ track_roles = {}
121
+ for t in tracks:
122
+ idx = t.get("index", 0)
123
+ name = t.get("name", "")
124
+ track_roles[idx] = infer_track_role(name)
125
+
126
+ # Sonic state (None if analyzer unavailable)
127
+ sonic = None
128
+ if spectrum and spectrum.get("bands"):
129
+ sonic = {
130
+ "spectrum": spectrum.get("bands", {}),
131
+ "rms": rms.get("rms") if rms else None,
132
+ "peak": rms.get("peak") if rms else None,
133
+ "key": detected_key.get("key") if detected_key else None,
134
+ "scale": detected_key.get("scale") if detected_key else None,
135
+ "key_confidence": detected_key.get("confidence") if detected_key else None,
136
+ }
137
+
138
+ # Technical state
139
+ analyzer_available = spectrum is not None and bool(spectrum.get("bands"))
140
+ flucoma_available = (
141
+ flucoma_status is not None
142
+ and flucoma_status.get("flucoma_available", False)
143
+ )
144
+
145
+ # Check plugin health from track_infos if provided
146
+ unhealthy_devices = []
147
+ if track_infos:
148
+ for ti in track_infos:
149
+ for dev in ti.get("devices", []):
150
+ flags = dev.get("health_flags", [])
151
+ if "opaque_or_failed_plugin" in flags:
152
+ unhealthy_devices.append({
153
+ "track": ti.get("index"),
154
+ "device": dev.get("name"),
155
+ "flag": "opaque_or_failed_plugin",
156
+ })
157
+
158
+ technical = {
159
+ "analyzer_available": analyzer_available,
160
+ "flucoma_available": flucoma_available,
161
+ "unhealthy_devices": unhealthy_devices,
162
+ }
163
+
164
+ return WorldModel(
165
+ topology=topology,
166
+ sonic=sonic,
167
+ technical=technical,
168
+ track_roles=track_roles,
169
+ )
170
+