livepilot 1.9.13 → 1.9.15
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/AGENTS.md +3 -3
- package/CHANGELOG.md +51 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +7 -7
- package/bin/livepilot.js +32 -8
- package/installer/install.js +21 -2
- package/livepilot/.Codex-plugin/plugin.json +2 -2
- package/livepilot/.claude-plugin/plugin.json +2 -2
- package/livepilot/agents/livepilot-producer/AGENT.md +243 -49
- package/livepilot/skills/livepilot-core/SKILL.md +81 -6
- package/livepilot/skills/livepilot-core/references/m4l-devices.md +2 -2
- package/livepilot/skills/livepilot-core/references/overview.md +3 -3
- package/livepilot/skills/livepilot-core/references/sound-design.md +3 -2
- package/livepilot/skills/livepilot-release/SKILL.md +13 -13
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +6 -3
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/curves.py +11 -3
- package/mcp_server/evaluation/__init__.py +1 -0
- package/mcp_server/evaluation/fabric.py +575 -0
- package/mcp_server/evaluation/feature_extractors.py +84 -0
- package/mcp_server/evaluation/policy.py +67 -0
- package/mcp_server/evaluation/tools.py +53 -0
- package/mcp_server/memory/__init__.py +11 -2
- package/mcp_server/memory/anti_memory.py +78 -0
- package/mcp_server/memory/promotion.py +94 -0
- package/mcp_server/memory/session_memory.py +108 -0
- package/mcp_server/memory/taste_memory.py +158 -0
- package/mcp_server/memory/technique_store.py +2 -1
- package/mcp_server/memory/tools.py +112 -0
- package/mcp_server/mix_engine/__init__.py +1 -0
- package/mcp_server/mix_engine/critics.py +299 -0
- package/mcp_server/mix_engine/models.py +152 -0
- package/mcp_server/mix_engine/planner.py +103 -0
- package/mcp_server/mix_engine/state_builder.py +316 -0
- package/mcp_server/mix_engine/tools.py +214 -0
- package/mcp_server/performance_engine/__init__.py +1 -0
- package/mcp_server/performance_engine/models.py +148 -0
- package/mcp_server/performance_engine/planner.py +267 -0
- package/mcp_server/performance_engine/safety.py +162 -0
- package/mcp_server/performance_engine/tools.py +183 -0
- package/mcp_server/project_brain/__init__.py +6 -0
- package/mcp_server/project_brain/arrangement_graph.py +64 -0
- package/mcp_server/project_brain/automation_graph.py +72 -0
- package/mcp_server/project_brain/builder.py +123 -0
- package/mcp_server/project_brain/capability_graph.py +64 -0
- package/mcp_server/project_brain/models.py +282 -0
- package/mcp_server/project_brain/refresh.py +80 -0
- package/mcp_server/project_brain/role_graph.py +103 -0
- package/mcp_server/project_brain/session_graph.py +51 -0
- package/mcp_server/project_brain/tools.py +144 -0
- package/mcp_server/reference_engine/__init__.py +1 -0
- package/mcp_server/reference_engine/gap_analyzer.py +239 -0
- package/mcp_server/reference_engine/models.py +105 -0
- package/mcp_server/reference_engine/profile_builder.py +149 -0
- package/mcp_server/reference_engine/tactic_router.py +117 -0
- package/mcp_server/reference_engine/tools.py +235 -0
- package/mcp_server/runtime/__init__.py +1 -0
- package/mcp_server/runtime/action_ledger.py +117 -0
- package/mcp_server/runtime/action_ledger_models.py +84 -0
- package/mcp_server/runtime/action_tools.py +57 -0
- package/mcp_server/runtime/capability_state.py +218 -0
- package/mcp_server/runtime/safety_kernel.py +339 -0
- package/mcp_server/runtime/safety_tools.py +42 -0
- package/mcp_server/runtime/tools.py +64 -0
- package/mcp_server/server.py +23 -1
- package/mcp_server/sound_design/__init__.py +1 -0
- package/mcp_server/sound_design/critics.py +297 -0
- package/mcp_server/sound_design/models.py +147 -0
- package/mcp_server/sound_design/planner.py +104 -0
- package/mcp_server/sound_design/tools.py +297 -0
- package/mcp_server/tools/_agent_os_engine.py +947 -0
- package/mcp_server/tools/_composition_engine.py +1530 -0
- package/mcp_server/tools/_conductor.py +199 -0
- package/mcp_server/tools/_conductor_budgets.py +222 -0
- package/mcp_server/tools/_evaluation_contracts.py +91 -0
- package/mcp_server/tools/_form_engine.py +416 -0
- package/mcp_server/tools/_motif_engine.py +351 -0
- package/mcp_server/tools/_planner_engine.py +516 -0
- package/mcp_server/tools/_research_engine.py +542 -0
- package/mcp_server/tools/_research_provider.py +185 -0
- package/mcp_server/tools/_snapshot_normalizer.py +49 -0
- package/mcp_server/tools/agent_os.py +440 -0
- package/mcp_server/tools/analyzer.py +18 -0
- package/mcp_server/tools/automation.py +25 -10
- package/mcp_server/tools/composition.py +563 -0
- package/mcp_server/tools/motif.py +104 -0
- package/mcp_server/tools/planner.py +144 -0
- package/mcp_server/tools/research.py +223 -0
- package/mcp_server/tools/tracks.py +18 -3
- package/mcp_server/tools/transport.py +10 -2
- package/mcp_server/transition_engine/__init__.py +6 -0
- package/mcp_server/transition_engine/archetypes.py +167 -0
- package/mcp_server/transition_engine/critics.py +340 -0
- package/mcp_server/transition_engine/models.py +90 -0
- package/mcp_server/transition_engine/tools.py +291 -0
- package/mcp_server/translation_engine/__init__.py +5 -0
- package/mcp_server/translation_engine/critics.py +297 -0
- package/mcp_server/translation_engine/models.py +27 -0
- package/mcp_server/translation_engine/tools.py +74 -0
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/remote_script/LivePilot/arrangement.py +12 -2
- package/requirements.txt +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Conductor — intelligent request routing to specialized engines.
|
|
2
|
+
|
|
3
|
+
Analyzes a natural-language production request and determines which engines
|
|
4
|
+
should handle it, in what order, with what priority. This is the "brain"
|
|
5
|
+
that connects all the specialist engines into a coherent workflow.
|
|
6
|
+
|
|
7
|
+
Zero external dependencies beyond stdlib.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import asdict, dataclass, field
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# ── Engine Registry ──────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class EngineRoute:
|
|
21
|
+
"""A routing decision for a single engine."""
|
|
22
|
+
engine: str
|
|
23
|
+
priority: int # 1=primary, 2=secondary, 3=supporting
|
|
24
|
+
reason: str
|
|
25
|
+
entry_tool: str # which MCP tool to call first
|
|
26
|
+
follow_up_tools: list[str] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def to_dict(self) -> dict:
|
|
29
|
+
return asdict(self)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ConductorPlan:
|
|
34
|
+
"""Full routing plan for a production request."""
|
|
35
|
+
request: str
|
|
36
|
+
request_type: str # "mix", "composition", "sound_design", "transition", etc.
|
|
37
|
+
routes: list[EngineRoute] = field(default_factory=list)
|
|
38
|
+
capability_requirements: list[str] = field(default_factory=list)
|
|
39
|
+
notes: list[str] = field(default_factory=list)
|
|
40
|
+
budget: Optional[dict] = None
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict:
|
|
43
|
+
result = {
|
|
44
|
+
"request": self.request,
|
|
45
|
+
"request_type": self.request_type,
|
|
46
|
+
"routes": [r.to_dict() for r in self.routes],
|
|
47
|
+
"engine_count": len(self.routes),
|
|
48
|
+
"primary_engine": self.routes[0].engine if self.routes else None,
|
|
49
|
+
"capability_requirements": self.capability_requirements,
|
|
50
|
+
"notes": self.notes,
|
|
51
|
+
}
|
|
52
|
+
if self.budget is not None:
|
|
53
|
+
result["budget"] = self.budget
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ── Request Classification ───────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
# Keyword → (engine, request_type, entry_tool, follow_up_tools)
|
|
60
|
+
_ROUTING_PATTERNS: list[tuple[str, str, str, str, list[str]]] = [
|
|
61
|
+
# Mix requests
|
|
62
|
+
(r"clean|mud|muddy|low.?mid|eq|equaliz", "mix_engine", "mix", "analyze_mix", ["plan_mix_move", "evaluate_mix_move"]),
|
|
63
|
+
(r"punch|punchy|transient|dynamics|compress", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
64
|
+
(r"wide|wider|width|stereo|narrow|mono.?compat", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
65
|
+
(r"glue|cohes|bus.?comp|mix.?bus", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
66
|
+
(r"balance|level|volume.?balanc|gain.?stag", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
67
|
+
(r"headroom|clip|peak|limit", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
68
|
+
(r"depth|dry|wet|reverb.?mix|send", "mix_engine", "mix", "analyze_mix", ["plan_mix_move"]),
|
|
69
|
+
(r"mask|frequency.?collis|overlap", "mix_engine", "mix", "get_masking_report", ["plan_mix_move"]),
|
|
70
|
+
|
|
71
|
+
# Composition requests
|
|
72
|
+
(r"arrange|arrangement|song.?structure|loop.?to.?song", "composition", "composition", "plan_arrangement", ["analyze_composition"]),
|
|
73
|
+
(r"section|verse|chorus|drop|intro|outro|bridge|breakdown", "composition", "composition", "analyze_composition", ["get_section_graph"]),
|
|
74
|
+
(r"phrase|motif|pattern|repetit|variation", "composition", "composition", "analyze_composition", ["get_motif_graph"]),
|
|
75
|
+
(r"tension|energy.?arc|emotional|build.?up", "composition", "composition", "get_emotional_arc", ["analyze_composition"]),
|
|
76
|
+
(r"form|structure|reorder|expand|compress|split|insert", "composition", "composition", "transform_section", ["analyze_composition"]),
|
|
77
|
+
|
|
78
|
+
# Sound design requests
|
|
79
|
+
(r"synth|patch|oscillat|timbre|timbral|wavetable|operator", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
80
|
+
(r"haunted|lush|aggressive|warm.?pad|fat.?bass|bright.?lead", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
81
|
+
(r"modulation|lfo|movement|evolv|texture", "sound_design", "sound_design", "get_patch_model", ["analyze_sound_design"]),
|
|
82
|
+
(r"layer|sub.?layer|transient.?layer|body", "sound_design", "sound_design", "analyze_sound_design", ["plan_sound_design_move"]),
|
|
83
|
+
|
|
84
|
+
# Transition requests
|
|
85
|
+
(r"transition|handoff|arrival|drop.?feel|feel.?earned", "transition_engine", "transition", "analyze_transition", ["plan_transition"]),
|
|
86
|
+
(r"smooth|seamless|boundary|crossfade", "transition_engine", "transition", "analyze_transition", ["plan_transition"]),
|
|
87
|
+
|
|
88
|
+
# Reference requests
|
|
89
|
+
(r"reference|sound.?like|style.?of|burial|daft.?punk|inspired.?by", "reference_engine", "reference", "build_reference_profile", ["analyze_reference_gaps", "plan_reference_moves"]),
|
|
90
|
+
(r"compare|match|closer.?to", "reference_engine", "reference", "build_reference_profile", ["analyze_reference_gaps"]),
|
|
91
|
+
|
|
92
|
+
# Translation requests
|
|
93
|
+
(r"translat|mono|phone.?speaker|small.?speaker|earbud|headphone", "translation_engine", "translation", "check_translation", ["get_translation_issues"]),
|
|
94
|
+
(r"harsh|bright.?hurt|sibilant|ear.?fatigue", "translation_engine", "translation", "check_translation", []),
|
|
95
|
+
|
|
96
|
+
# Performance requests
|
|
97
|
+
(r"live|perform|set|scene.?steer|safe.?mode|improv", "performance_engine", "performance", "get_performance_state", ["get_performance_safe_moves"]),
|
|
98
|
+
(r"scene.?transition|handoff.?scene|energy.?steer", "performance_engine", "performance", "plan_scene_handoff", ["get_performance_safe_moves"]),
|
|
99
|
+
|
|
100
|
+
# Research requests
|
|
101
|
+
(r"research|how.?to|technique|tutorial|learn", "research", "research", "research_technique", []),
|
|
102
|
+
(r"style.?tactic|production.?style|genre.?approach", "research", "research", "get_style_tactics", []),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def classify_request(request: str) -> ConductorPlan:
|
|
107
|
+
"""Analyze a production request and route to the right engines.
|
|
108
|
+
|
|
109
|
+
Returns a ConductorPlan with ranked engine routes and capability requirements.
|
|
110
|
+
"""
|
|
111
|
+
lower = request.lower().strip()
|
|
112
|
+
if not lower:
|
|
113
|
+
return ConductorPlan(request=request, request_type="unknown",
|
|
114
|
+
notes=["Empty request — ask the user what they want to do"])
|
|
115
|
+
|
|
116
|
+
# Score each engine by how many patterns match
|
|
117
|
+
engine_scores: dict[str, dict] = {}
|
|
118
|
+
|
|
119
|
+
for pattern, engine, req_type, entry_tool, follow_ups in _ROUTING_PATTERNS:
|
|
120
|
+
if re.search(pattern, lower):
|
|
121
|
+
if engine not in engine_scores:
|
|
122
|
+
engine_scores[engine] = {
|
|
123
|
+
"score": 0, "request_type": req_type,
|
|
124
|
+
"entry_tool": entry_tool, "follow_ups": follow_ups,
|
|
125
|
+
}
|
|
126
|
+
engine_scores[engine]["score"] += 1
|
|
127
|
+
|
|
128
|
+
if not engine_scores:
|
|
129
|
+
# Default: try Agent OS core loop (general "make it better")
|
|
130
|
+
return ConductorPlan(
|
|
131
|
+
request=request,
|
|
132
|
+
request_type="general",
|
|
133
|
+
routes=[EngineRoute(
|
|
134
|
+
engine="agent_os",
|
|
135
|
+
priority=1,
|
|
136
|
+
reason="No specific engine matched — using core Agent OS loop",
|
|
137
|
+
entry_tool="build_world_model",
|
|
138
|
+
follow_up_tools=["evaluate_move"],
|
|
139
|
+
)],
|
|
140
|
+
capability_requirements=["session_access"],
|
|
141
|
+
notes=["General request — Agent OS core loop with goal vector"],
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Sort engines by score (most matches = primary)
|
|
145
|
+
sorted_engines = sorted(engine_scores.items(), key=lambda x: -x[1]["score"])
|
|
146
|
+
|
|
147
|
+
routes: list[EngineRoute] = []
|
|
148
|
+
for i, (engine, info) in enumerate(sorted_engines):
|
|
149
|
+
routes.append(EngineRoute(
|
|
150
|
+
engine=engine,
|
|
151
|
+
priority=i + 1,
|
|
152
|
+
reason=f"Matched {info['score']} keyword pattern(s)",
|
|
153
|
+
entry_tool=info["entry_tool"],
|
|
154
|
+
follow_up_tools=info["follow_ups"],
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
primary_type = sorted_engines[0][1]["request_type"]
|
|
158
|
+
|
|
159
|
+
# Determine capability requirements
|
|
160
|
+
caps = ["session_access"]
|
|
161
|
+
if any(r.engine == "mix_engine" for r in routes):
|
|
162
|
+
caps.append("analyzer")
|
|
163
|
+
if any(r.engine in ("reference_engine",) for r in routes):
|
|
164
|
+
caps.append("offline_perception")
|
|
165
|
+
if any(r.engine == "performance_engine" for r in routes):
|
|
166
|
+
caps.append("live_performance_safe")
|
|
167
|
+
|
|
168
|
+
# Always suggest starting with Project Brain for complex multi-engine tasks
|
|
169
|
+
notes = []
|
|
170
|
+
if len(routes) > 1:
|
|
171
|
+
notes.append("Multi-engine task — call build_project_brain first for shared state")
|
|
172
|
+
if any(r.engine == "mix_engine" for r in routes):
|
|
173
|
+
notes.append("Mix engine works best with analyzer data — check get_capability_state")
|
|
174
|
+
|
|
175
|
+
return ConductorPlan(
|
|
176
|
+
request=request,
|
|
177
|
+
request_type=primary_type,
|
|
178
|
+
routes=routes,
|
|
179
|
+
capability_requirements=caps,
|
|
180
|
+
notes=notes,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def create_conductor_plan(
|
|
185
|
+
request: str,
|
|
186
|
+
mode: str = "improve",
|
|
187
|
+
aggression: float = 0.5,
|
|
188
|
+
) -> ConductorPlan:
|
|
189
|
+
"""Create a full ConductorPlan with routing + budget.
|
|
190
|
+
|
|
191
|
+
Combines classify_request (routing) with create_budget (resource limits)
|
|
192
|
+
into a single plan the agent can consume.
|
|
193
|
+
"""
|
|
194
|
+
from . import _conductor_budgets as budgets
|
|
195
|
+
|
|
196
|
+
plan = classify_request(request)
|
|
197
|
+
budget = budgets.create_budget(mode=mode, aggression=aggression)
|
|
198
|
+
plan.budget = budget.to_dict()
|
|
199
|
+
return plan
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Conductor Budget System — prevents the agent from overcommitting.
|
|
2
|
+
|
|
3
|
+
Every turn maintains six resource pools: latency, risk, novelty, change,
|
|
4
|
+
undo, and research. Mode shapes the initial budget; spend functions enforce
|
|
5
|
+
limits and return (updated_budget, allowed) tuples.
|
|
6
|
+
|
|
7
|
+
Zero external dependencies beyond stdlib.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import asdict, dataclass
|
|
13
|
+
from typing import Tuple
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# ── TurnBudget ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TurnBudget:
|
|
20
|
+
"""Resource pools for a single agent turn."""
|
|
21
|
+
|
|
22
|
+
# Limits
|
|
23
|
+
latency_ms: int = 30000 # max 30s per turn
|
|
24
|
+
risk_points: float = 1.0 # 0-1, how much risk left
|
|
25
|
+
novelty_points: float = 0.5 # 0-1, how much novelty allowed
|
|
26
|
+
change_count: int = 3 # max moves per turn
|
|
27
|
+
undo_count: int = 3 # max consecutive undos before stop
|
|
28
|
+
research_calls: int = 2 # max research calls per turn
|
|
29
|
+
|
|
30
|
+
# Tracking
|
|
31
|
+
elapsed_ms: int = 0
|
|
32
|
+
risk_spent: float = 0.0
|
|
33
|
+
novelty_spent: float = 0.0
|
|
34
|
+
changes_made: int = 0
|
|
35
|
+
undos_consecutive: int = 0
|
|
36
|
+
research_used: int = 0
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return asdict(self)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── Mode Presets ────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
# Each mode overrides specific budget fields.
|
|
45
|
+
# Keys map to TurnBudget field names.
|
|
46
|
+
_MODE_PRESETS: dict[str, dict] = {
|
|
47
|
+
"observe": {
|
|
48
|
+
"risk_points": 0.1,
|
|
49
|
+
"novelty_points": 0.1,
|
|
50
|
+
"change_count": 0,
|
|
51
|
+
"research_calls": 0,
|
|
52
|
+
"latency_ms": 15000,
|
|
53
|
+
},
|
|
54
|
+
"improve": {
|
|
55
|
+
# Default values — no overrides needed
|
|
56
|
+
},
|
|
57
|
+
"explore": {
|
|
58
|
+
"risk_points": 1.0,
|
|
59
|
+
"novelty_points": 1.0,
|
|
60
|
+
"change_count": 5,
|
|
61
|
+
"research_calls": 3,
|
|
62
|
+
"latency_ms": 45000,
|
|
63
|
+
},
|
|
64
|
+
"finish": {
|
|
65
|
+
"risk_points": 0.3,
|
|
66
|
+
"novelty_points": 0.1,
|
|
67
|
+
"change_count": 2,
|
|
68
|
+
"research_calls": 1,
|
|
69
|
+
"latency_ms": 20000,
|
|
70
|
+
},
|
|
71
|
+
"diagnose": {
|
|
72
|
+
"risk_points": 0.0,
|
|
73
|
+
"novelty_points": 0.0,
|
|
74
|
+
"change_count": 0,
|
|
75
|
+
"research_calls": 3,
|
|
76
|
+
"latency_ms": 20000,
|
|
77
|
+
},
|
|
78
|
+
"performance": {
|
|
79
|
+
"risk_points": 0.2,
|
|
80
|
+
"novelty_points": 0.1,
|
|
81
|
+
"change_count": 2,
|
|
82
|
+
"undo_count": 1,
|
|
83
|
+
"research_calls": 0,
|
|
84
|
+
"latency_ms": 10000,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Budget Factory ──────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
def create_budget(mode: str = "improve", aggression: float = 0.5) -> TurnBudget:
|
|
92
|
+
"""Create a TurnBudget shaped by mode and aggression.
|
|
93
|
+
|
|
94
|
+
mode: observe | improve | explore | finish | diagnose | performance
|
|
95
|
+
aggression: 0.0 (subtle) to 1.0 (bold) — scales risk and change limits.
|
|
96
|
+
"""
|
|
97
|
+
aggression = max(0.0, min(1.0, float(aggression)))
|
|
98
|
+
|
|
99
|
+
budget = TurnBudget()
|
|
100
|
+
|
|
101
|
+
# Apply mode preset
|
|
102
|
+
preset = _MODE_PRESETS.get(mode, {})
|
|
103
|
+
for key, value in preset.items():
|
|
104
|
+
setattr(budget, key, value)
|
|
105
|
+
|
|
106
|
+
# Aggression scales risk_points and change_count (never below preset floor)
|
|
107
|
+
if mode not in ("observe", "diagnose"):
|
|
108
|
+
base_risk = budget.risk_points
|
|
109
|
+
budget.risk_points = round(base_risk * (0.5 + aggression * 0.5), 3)
|
|
110
|
+
base_changes = budget.change_count
|
|
111
|
+
budget.change_count = max(1, int(base_changes * (0.5 + aggression * 0.5)))
|
|
112
|
+
|
|
113
|
+
return budget
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ── Spend Functions ─────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def spend_risk(budget: TurnBudget, amount: float) -> Tuple[TurnBudget, bool]:
|
|
119
|
+
"""Spend risk points. Returns (updated_budget, allowed)."""
|
|
120
|
+
amount = max(0.0, float(amount))
|
|
121
|
+
remaining = budget.risk_points - budget.risk_spent
|
|
122
|
+
if amount > remaining + 1e-9:
|
|
123
|
+
return budget, False
|
|
124
|
+
budget.risk_spent = round(budget.risk_spent + amount, 6)
|
|
125
|
+
return budget, True
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def spend_change(budget: TurnBudget) -> Tuple[TurnBudget, bool]:
|
|
129
|
+
"""Record one change. Returns (updated_budget, allowed)."""
|
|
130
|
+
if budget.changes_made >= budget.change_count:
|
|
131
|
+
return budget, False
|
|
132
|
+
budget.changes_made += 1
|
|
133
|
+
# A successful change resets consecutive undo count
|
|
134
|
+
budget.undos_consecutive = 0
|
|
135
|
+
return budget, True
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def record_undo(budget: TurnBudget) -> Tuple[TurnBudget, bool]:
|
|
139
|
+
"""Record a consecutive undo. Returns False if limit exceeded (should stop)."""
|
|
140
|
+
budget.undos_consecutive += 1
|
|
141
|
+
if budget.undos_consecutive > budget.undo_count:
|
|
142
|
+
return budget, False
|
|
143
|
+
return budget, True
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def spend_research(budget: TurnBudget) -> Tuple[TurnBudget, bool]:
|
|
147
|
+
"""Spend one research call. Returns (updated_budget, allowed)."""
|
|
148
|
+
if budget.research_used >= budget.research_calls:
|
|
149
|
+
return budget, False
|
|
150
|
+
budget.research_used += 1
|
|
151
|
+
return budget, True
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def spend_novelty(budget: TurnBudget, amount: float) -> Tuple[TurnBudget, bool]:
|
|
155
|
+
"""Spend novelty points. Returns (updated_budget, allowed)."""
|
|
156
|
+
amount = max(0.0, float(amount))
|
|
157
|
+
remaining = budget.novelty_points - budget.novelty_spent
|
|
158
|
+
if amount > remaining + 1e-9:
|
|
159
|
+
return budget, False
|
|
160
|
+
budget.novelty_spent = round(budget.novelty_spent + amount, 6)
|
|
161
|
+
return budget, True
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── Budget Queries ──────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def is_budget_exhausted(budget: TurnBudget) -> bool:
|
|
167
|
+
"""Check if any budget dimension is fully spent."""
|
|
168
|
+
if budget.elapsed_ms >= budget.latency_ms:
|
|
169
|
+
return True
|
|
170
|
+
if budget.risk_spent >= budget.risk_points:
|
|
171
|
+
return True
|
|
172
|
+
if budget.changes_made >= budget.change_count:
|
|
173
|
+
return True
|
|
174
|
+
if budget.undos_consecutive > budget.undo_count:
|
|
175
|
+
return True
|
|
176
|
+
if budget.novelty_spent >= budget.novelty_points:
|
|
177
|
+
return True
|
|
178
|
+
# research_used exhaustion alone doesn't exhaust the budget —
|
|
179
|
+
# running out of research calls just blocks further research.
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def get_budget_summary(budget: TurnBudget) -> dict:
|
|
184
|
+
"""Return a human-readable summary of the current budget state."""
|
|
185
|
+
return {
|
|
186
|
+
"latency": {
|
|
187
|
+
"used_ms": budget.elapsed_ms,
|
|
188
|
+
"limit_ms": budget.latency_ms,
|
|
189
|
+
"remaining_ms": max(0, budget.latency_ms - budget.elapsed_ms),
|
|
190
|
+
"exhausted": budget.elapsed_ms >= budget.latency_ms,
|
|
191
|
+
},
|
|
192
|
+
"risk": {
|
|
193
|
+
"spent": round(budget.risk_spent, 3),
|
|
194
|
+
"limit": round(budget.risk_points, 3),
|
|
195
|
+
"remaining": round(max(0.0, budget.risk_points - budget.risk_spent), 3),
|
|
196
|
+
"exhausted": budget.risk_spent >= budget.risk_points,
|
|
197
|
+
},
|
|
198
|
+
"novelty": {
|
|
199
|
+
"spent": round(budget.novelty_spent, 3),
|
|
200
|
+
"limit": round(budget.novelty_points, 3),
|
|
201
|
+
"remaining": round(max(0.0, budget.novelty_points - budget.novelty_spent), 3),
|
|
202
|
+
"exhausted": budget.novelty_spent >= budget.novelty_points,
|
|
203
|
+
},
|
|
204
|
+
"changes": {
|
|
205
|
+
"made": budget.changes_made,
|
|
206
|
+
"limit": budget.change_count,
|
|
207
|
+
"remaining": max(0, budget.change_count - budget.changes_made),
|
|
208
|
+
"exhausted": budget.changes_made >= budget.change_count,
|
|
209
|
+
},
|
|
210
|
+
"undos": {
|
|
211
|
+
"consecutive": budget.undos_consecutive,
|
|
212
|
+
"limit": budget.undo_count,
|
|
213
|
+
"should_stop": budget.undos_consecutive > budget.undo_count,
|
|
214
|
+
},
|
|
215
|
+
"research": {
|
|
216
|
+
"used": budget.research_used,
|
|
217
|
+
"limit": budget.research_calls,
|
|
218
|
+
"remaining": max(0, budget.research_calls - budget.research_used),
|
|
219
|
+
"exhausted": budget.research_used >= budget.research_calls,
|
|
220
|
+
},
|
|
221
|
+
"overall_exhausted": is_budget_exhausted(budget),
|
|
222
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Evaluation Contracts — shared types for all engine evaluators.
|
|
2
|
+
|
|
3
|
+
Defines the canonical evaluation request/result types and the
|
|
4
|
+
authoritative registry of which quality dimensions are measurable.
|
|
5
|
+
|
|
6
|
+
All engines should produce EvaluationResult objects. The Evaluation
|
|
7
|
+
Fabric (Phase 1D) will consume these through a unified interface.
|
|
8
|
+
|
|
9
|
+
Design: EVALUATION_FABRIC_V1.md, section 6
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import asdict, dataclass, field
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Dimension Registry ───────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
# Authoritative registry: dimensions with working spectral proxies.
|
|
21
|
+
# If it's not here, it's unmeasurable in current phase and the evaluator
|
|
22
|
+
# must report confidence=0.0 for that dimension.
|
|
23
|
+
MEASURABLE_DIMENSIONS: frozenset[str] = frozenset({
|
|
24
|
+
"brightness", "warmth", "weight", "clarity",
|
|
25
|
+
"density", "energy", "punch",
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
# All valid quality dimensions (measurable + unmeasurable).
|
|
29
|
+
ALL_DIMENSIONS: frozenset[str] = frozenset({
|
|
30
|
+
"energy", "punch", "weight", "density", "brightness", "warmth",
|
|
31
|
+
"width", "depth", "motion", "contrast", "clarity", "cohesion",
|
|
32
|
+
"groove", "tension", "novelty", "polish", "emotion",
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_dimension_measurable(dim: str) -> bool:
|
|
37
|
+
"""Check if a dimension has a working spectral proxy."""
|
|
38
|
+
return dim in MEASURABLE_DIMENSIONS
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── Evaluation Request ───────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class EvaluationRequest:
|
|
45
|
+
"""Canonical evaluation request — engine-agnostic.
|
|
46
|
+
|
|
47
|
+
All engines submit evaluation through this shape. The Evaluation
|
|
48
|
+
Fabric routes to the appropriate engine-specific evaluator.
|
|
49
|
+
"""
|
|
50
|
+
engine: str
|
|
51
|
+
goal: dict = field(default_factory=dict)
|
|
52
|
+
before: dict = field(default_factory=dict)
|
|
53
|
+
after: dict = field(default_factory=dict)
|
|
54
|
+
protect: dict = field(default_factory=dict)
|
|
55
|
+
context: dict = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return asdict(self)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ── Evaluation Result ────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class EvaluationResult:
|
|
65
|
+
"""Canonical evaluation result — all engines produce this shape.
|
|
66
|
+
|
|
67
|
+
Fields:
|
|
68
|
+
engine: which engine produced this result
|
|
69
|
+
score: 0-1 composite quality score
|
|
70
|
+
keep_change: should the move be kept?
|
|
71
|
+
goal_progress: -1 to 1, how much the goal improved
|
|
72
|
+
collateral_damage: 0-1, harm to protected dimensions
|
|
73
|
+
hard_rule_failures: list of rule names that triggered
|
|
74
|
+
dimension_changes: {dim: {before, after, delta}}
|
|
75
|
+
notes: human-readable explanation
|
|
76
|
+
decision_mode: "measured", "judgment", or "deferred"
|
|
77
|
+
memory_candidate: should this outcome be saved to memory?
|
|
78
|
+
"""
|
|
79
|
+
engine: str
|
|
80
|
+
score: float = 0.0
|
|
81
|
+
keep_change: bool = True
|
|
82
|
+
goal_progress: float = 0.0
|
|
83
|
+
collateral_damage: float = 0.0
|
|
84
|
+
hard_rule_failures: list[str] = field(default_factory=list)
|
|
85
|
+
dimension_changes: dict = field(default_factory=dict)
|
|
86
|
+
notes: list[str] = field(default_factory=list)
|
|
87
|
+
decision_mode: str = "measured"
|
|
88
|
+
memory_candidate: bool = False
|
|
89
|
+
|
|
90
|
+
def to_dict(self) -> dict:
|
|
91
|
+
return asdict(self)
|