livepilot 1.12.2 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +82 -0
- package/README.md +3 -3
- package/m4l_device/LivePilot_Analyzer.amxd +0 -0
- package/m4l_device/livepilot_bridge.js +1 -1
- package/mcp_server/__init__.py +1 -1
- package/mcp_server/branches/__init__.py +32 -0
- package/mcp_server/branches/types.py +230 -0
- package/mcp_server/composer/__init__.py +10 -1
- package/mcp_server/composer/branch_producer.py +229 -0
- package/mcp_server/evaluation/policy.py +129 -2
- package/mcp_server/experiment/engine.py +47 -11
- package/mcp_server/experiment/models.py +72 -7
- package/mcp_server/experiment/tools.py +231 -35
- package/mcp_server/memory/taste_graph.py +84 -11
- package/mcp_server/persistence/taste_store.py +21 -5
- package/mcp_server/runtime/session_kernel.py +46 -0
- package/mcp_server/runtime/tools.py +29 -3
- package/mcp_server/synthesis_brain/__init__.py +53 -0
- package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
- package/mcp_server/synthesis_brain/adapters/analog.py +167 -0
- package/mcp_server/synthesis_brain/adapters/base.py +86 -0
- package/mcp_server/synthesis_brain/adapters/drift.py +166 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +151 -0
- package/mcp_server/synthesis_brain/adapters/operator.py +169 -0
- package/mcp_server/synthesis_brain/adapters/wavetable.py +228 -0
- package/mcp_server/synthesis_brain/engine.py +91 -0
- package/mcp_server/synthesis_brain/models.py +121 -0
- package/mcp_server/synthesis_brain/timbre.py +194 -0
- package/mcp_server/tools/_conductor.py +144 -0
- package/mcp_server/wonder_mode/engine.py +324 -0
- package/mcp_server/wonder_mode/tools.py +153 -1
- package/package.json +2 -2
- package/remote_script/LivePilot/__init__.py +1 -1
- package/server.json +2 -2
|
@@ -4,10 +4,24 @@ Consistent keep/undo semantics shared across sonic, composition,
|
|
|
4
4
|
and all future evaluators.
|
|
5
5
|
|
|
6
6
|
Design: EVALUATION_FABRIC_V1.md, section 8
|
|
7
|
+
|
|
8
|
+
PR7 adds ``classify_branch_outcome`` — a branch-lifecycle classifier that
|
|
9
|
+
maps a score (and optional hard-rule inputs) to one of three statuses:
|
|
10
|
+
"keep", "undo", "interesting_but_failed". The third status exists for
|
|
11
|
+
exploration mode: a branch that failed technical gates but surfaced a
|
|
12
|
+
novel idea is kept for audit and never re-applied. Protection violations
|
|
13
|
+
still force undo regardless of exploration mode — that's a safety
|
|
14
|
+
invariant, not a taste judgment.
|
|
7
15
|
"""
|
|
8
16
|
|
|
9
17
|
from __future__ import annotations
|
|
10
18
|
|
|
19
|
+
from dataclasses import asdict, dataclass, field
|
|
20
|
+
from typing import Literal, Optional
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
BranchOutcomeStatus = Literal["keep", "undo", "interesting_but_failed"]
|
|
24
|
+
|
|
11
25
|
|
|
12
26
|
def apply_hard_rules(
|
|
13
27
|
goal_progress: float,
|
|
@@ -16,11 +30,17 @@ def apply_hard_rules(
|
|
|
16
30
|
measurable_count: int,
|
|
17
31
|
score: float,
|
|
18
32
|
target_count: int,
|
|
33
|
+
defer_on_unmeasurable: bool = True,
|
|
19
34
|
) -> tuple[bool, list[str]]:
|
|
20
35
|
"""Enforce hard rules and return (keep_change, failure_reasons).
|
|
21
36
|
|
|
22
37
|
Rules (evaluated in order):
|
|
23
|
-
1. All targets unmeasurable + no protection violation
|
|
38
|
+
1. (optional) All targets unmeasurable + no protection violation
|
|
39
|
+
-> defer to agent. Fires only when defer_on_unmeasurable=True
|
|
40
|
+
(the default). The evaluation fabric relies on this to mark
|
|
41
|
+
decision_mode="deferred". Score-producing evaluators (branch
|
|
42
|
+
lifecycle, PR7) pass defer_on_unmeasurable=False because the
|
|
43
|
+
score IS the judgment — no deferral needed.
|
|
24
44
|
2. Protection violated -> force undo
|
|
25
45
|
3. Measurable delta <= 0 when measurable targets exist -> force undo
|
|
26
46
|
4. Score < 0.40 -> force undo
|
|
@@ -32,6 +52,10 @@ def apply_hard_rules(
|
|
|
32
52
|
measurable_count: how many target dimensions were measurable
|
|
33
53
|
score: composite quality score (0-1)
|
|
34
54
|
target_count: total number of target dimensions
|
|
55
|
+
defer_on_unmeasurable: when True (default), rule 1 returns
|
|
56
|
+
(True, [defer message]) as soon as no measurable targets
|
|
57
|
+
exist. When False, rule 1 is skipped and rules 2-4 run
|
|
58
|
+
unconditionally.
|
|
35
59
|
|
|
36
60
|
Returns:
|
|
37
61
|
(keep_change, list_of_rule_failure_reasons)
|
|
@@ -39,7 +63,11 @@ def apply_hard_rules(
|
|
|
39
63
|
failures: list[str] = []
|
|
40
64
|
|
|
41
65
|
# Rule 1: all unmeasurable + no protection violation -> defer
|
|
42
|
-
if
|
|
66
|
+
if (
|
|
67
|
+
defer_on_unmeasurable
|
|
68
|
+
and measurable_count == 0
|
|
69
|
+
and not protection_violated
|
|
70
|
+
):
|
|
43
71
|
return True, [
|
|
44
72
|
"No measurable target dimensions — deferring keep/undo "
|
|
45
73
|
"to agent musical judgment"
|
|
@@ -65,3 +93,102 @@ def apply_hard_rules(
|
|
|
65
93
|
|
|
66
94
|
keep_change = len(failures) == 0
|
|
67
95
|
return keep_change, failures
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ── PR7 — branch-lifecycle classifier ────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class BranchOutcome:
|
|
103
|
+
"""Unified branch evaluation result.
|
|
104
|
+
|
|
105
|
+
Fields:
|
|
106
|
+
status: terminal classification — "keep" | "undo" | "interesting_but_failed"
|
|
107
|
+
keep_change: True ⇒ status == "keep"; never True for the other statuses.
|
|
108
|
+
score: the composite score that informed the decision.
|
|
109
|
+
failure_reasons: human-readable list of failed hard rules (empty on keep).
|
|
110
|
+
note: optional explanation aimed at the user.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
status: BranchOutcomeStatus
|
|
114
|
+
keep_change: bool
|
|
115
|
+
score: float
|
|
116
|
+
failure_reasons: list[str] = field(default_factory=list)
|
|
117
|
+
note: str = ""
|
|
118
|
+
|
|
119
|
+
def to_dict(self) -> dict:
|
|
120
|
+
return asdict(self)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def classify_branch_outcome(
|
|
124
|
+
score: float,
|
|
125
|
+
*,
|
|
126
|
+
protection_violated: bool = False,
|
|
127
|
+
measurable_count: int = 0,
|
|
128
|
+
target_count: int = 0,
|
|
129
|
+
goal_progress: float = 0.0,
|
|
130
|
+
exploration_rules: bool = False,
|
|
131
|
+
) -> BranchOutcome:
|
|
132
|
+
"""Classify a branch's terminal status from a score + optional hard-rule inputs.
|
|
133
|
+
|
|
134
|
+
Delegates to apply_hard_rules with ``defer_on_unmeasurable=False`` — a
|
|
135
|
+
score-producing evaluator DID make a judgment, so rule 1's deferral
|
|
136
|
+
path is not appropriate here. The score alone is enough to push a
|
|
137
|
+
branch toward undo / interesting_but_failed.
|
|
138
|
+
|
|
139
|
+
Post-processing:
|
|
140
|
+
- ``exploration_rules=False`` (technical safety, default):
|
|
141
|
+
any hard-rule failure ⇒ status="undo".
|
|
142
|
+
- ``exploration_rules=True`` (creative exploration):
|
|
143
|
+
protection violations still force undo (safety invariant);
|
|
144
|
+
all other failures downgrade to "interesting_but_failed".
|
|
145
|
+
|
|
146
|
+
Returns a BranchOutcome that callers can plug into branch.score /
|
|
147
|
+
.status / .evaluation without further interpretation.
|
|
148
|
+
"""
|
|
149
|
+
keep_change, failures = apply_hard_rules(
|
|
150
|
+
goal_progress=goal_progress,
|
|
151
|
+
collateral_damage=0.0, # not threaded here — branch lifecycle doesn't compute it yet
|
|
152
|
+
protection_violated=protection_violated,
|
|
153
|
+
measurable_count=measurable_count,
|
|
154
|
+
score=score,
|
|
155
|
+
target_count=target_count,
|
|
156
|
+
defer_on_unmeasurable=False,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if keep_change:
|
|
160
|
+
return BranchOutcome(
|
|
161
|
+
status="keep",
|
|
162
|
+
keep_change=True,
|
|
163
|
+
score=score,
|
|
164
|
+
failure_reasons=[],
|
|
165
|
+
note="",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Failed — decide between undo and interesting_but_failed.
|
|
169
|
+
protection_failure = any("protected dimension" in f for f in failures)
|
|
170
|
+
|
|
171
|
+
if exploration_rules and not protection_failure:
|
|
172
|
+
return BranchOutcome(
|
|
173
|
+
status="interesting_but_failed",
|
|
174
|
+
keep_change=False,
|
|
175
|
+
score=score,
|
|
176
|
+
failure_reasons=failures,
|
|
177
|
+
note=(
|
|
178
|
+
"Exploration rule: branch failed technical gates but is "
|
|
179
|
+
"retained for audit. Not re-applied."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return BranchOutcome(
|
|
184
|
+
status="undo",
|
|
185
|
+
keep_change=False,
|
|
186
|
+
score=score,
|
|
187
|
+
failure_reasons=failures,
|
|
188
|
+
note=(
|
|
189
|
+
"Protection violation — branch rolled back regardless of "
|
|
190
|
+
"exploration mode."
|
|
191
|
+
if protection_failure
|
|
192
|
+
else "Branch rolled back per hard rules."
|
|
193
|
+
),
|
|
194
|
+
)
|
|
@@ -22,6 +22,7 @@ import time
|
|
|
22
22
|
from typing import Optional
|
|
23
23
|
|
|
24
24
|
from .models import ExperimentSet, ExperimentBranch, BranchSnapshot
|
|
25
|
+
from ..branches import BranchSeed, seed_from_move_id
|
|
25
26
|
import logging
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
@@ -39,27 +40,43 @@ def _gen_id(prefix: str, seed: str) -> str:
|
|
|
39
40
|
# ── Create experiments ───────────────────────────────────────────────────────
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
def
|
|
43
|
+
def create_experiment_from_seeds(
|
|
43
44
|
request_text: str,
|
|
44
|
-
|
|
45
|
+
seeds: list[BranchSeed],
|
|
45
46
|
kernel_id: str = "",
|
|
47
|
+
compiled_plans: Optional[list] = None,
|
|
46
48
|
) -> ExperimentSet:
|
|
47
|
-
"""Create an experiment set
|
|
49
|
+
"""Create an experiment set from BranchSeeds (PR3 canonical path).
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
seeds: one BranchSeed per desired branch. Can be any source — semantic_move,
|
|
52
|
+
freeform, synthesis, composer, technique.
|
|
53
|
+
compiled_plans: optional parallel list; when entry ``i`` is a dict, that
|
|
54
|
+
plan is attached to branch ``i`` (used by freeform / synthesis / composer
|
|
55
|
+
producers that do their own compilation). When None or entry is None,
|
|
56
|
+
run_experiment compiles from the seed at run time — which only succeeds
|
|
57
|
+
for source="semantic_move" seeds.
|
|
58
|
+
|
|
59
|
+
Does NOT execute anything — call run_experiment() to trial each branch.
|
|
51
60
|
"""
|
|
61
|
+
if compiled_plans is not None and len(compiled_plans) != len(seeds):
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"compiled_plans length ({len(compiled_plans)}) must match "
|
|
64
|
+
f"seeds length ({len(seeds)})"
|
|
65
|
+
)
|
|
66
|
+
|
|
52
67
|
exp_id = _gen_id("exp", request_text)
|
|
53
68
|
now = int(time.time() * 1000)
|
|
54
69
|
|
|
55
70
|
branches = []
|
|
56
|
-
for i,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
for i, seed in enumerate(seeds):
|
|
72
|
+
plan = compiled_plans[i] if compiled_plans else None
|
|
73
|
+
display = seed.move_id or (seed.hypothesis[:32] if seed.hypothesis else seed.seed_id)
|
|
74
|
+
branch = ExperimentBranch.from_seed(
|
|
75
|
+
seed=seed,
|
|
76
|
+
branch_id=_gen_id("br", f"{seed.seed_id}_{i}"),
|
|
77
|
+
name=f"Branch {i+1}: {display}",
|
|
61
78
|
source_kernel_id=kernel_id,
|
|
62
|
-
|
|
79
|
+
compiled_plan=plan,
|
|
63
80
|
created_at_ms=now,
|
|
64
81
|
)
|
|
65
82
|
branches.append(branch)
|
|
@@ -76,6 +93,25 @@ def create_experiment(
|
|
|
76
93
|
return experiment
|
|
77
94
|
|
|
78
95
|
|
|
96
|
+
def create_experiment(
|
|
97
|
+
request_text: str,
|
|
98
|
+
move_ids: list[str],
|
|
99
|
+
kernel_id: str = "",
|
|
100
|
+
) -> ExperimentSet:
|
|
101
|
+
"""Create an experiment set with one semantic_move branch per move_id.
|
|
102
|
+
|
|
103
|
+
Legacy API — kept for back-compat. Internally builds one BranchSeed per
|
|
104
|
+
move_id via seed_from_move_id and delegates to create_experiment_from_seeds.
|
|
105
|
+
Branch naming, ids, and lifecycle are unchanged for existing callers.
|
|
106
|
+
"""
|
|
107
|
+
seeds = [seed_from_move_id(mid) for mid in move_ids]
|
|
108
|
+
return create_experiment_from_seeds(
|
|
109
|
+
request_text=request_text,
|
|
110
|
+
seeds=seeds,
|
|
111
|
+
kernel_id=kernel_id,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
79
115
|
def get_experiment(experiment_id: str) -> Optional[ExperimentSet]:
|
|
80
116
|
"""Get an experiment by ID."""
|
|
81
117
|
return _EXPERIMENTS.get(experiment_id)
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"""Experiment branch data models.
|
|
2
2
|
|
|
3
|
-
An ExperimentBranch represents one trial
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
An ExperimentBranch represents one trial against the current session state.
|
|
4
|
+
|
|
5
|
+
Pre-PR3 every branch was tied to a semantic_move (positional required
|
|
6
|
+
``move_id``). PR3 widens this: a branch may now be built from any
|
|
7
|
+
:class:`mcp_server.branches.BranchSeed` — semantic_move, freeform,
|
|
8
|
+
synthesis, composer, or technique. The ``move_id`` field is retained as
|
|
9
|
+
an optional convenience that mirrors ``seed.move_id`` for back-compat with
|
|
10
|
+
callers that read ``branch.move_id`` directly.
|
|
11
|
+
|
|
12
|
+
Multiple branches form an experiment set that can be compared and ranked.
|
|
6
13
|
"""
|
|
7
14
|
|
|
8
15
|
from __future__ import annotations
|
|
@@ -11,6 +18,8 @@ import time
|
|
|
11
18
|
from dataclasses import dataclass, field
|
|
12
19
|
from typing import Any, Optional
|
|
13
20
|
|
|
21
|
+
from ..branches import BranchSeed
|
|
22
|
+
|
|
14
23
|
|
|
15
24
|
@dataclass
|
|
16
25
|
class BranchSnapshot:
|
|
@@ -37,14 +46,26 @@ class BranchSnapshot:
|
|
|
37
46
|
|
|
38
47
|
@dataclass
|
|
39
48
|
class ExperimentBranch:
|
|
40
|
-
"""One trial branch in an experiment set.
|
|
49
|
+
"""One trial branch in an experiment set.
|
|
50
|
+
|
|
51
|
+
move_id is retained as an optional convenience (empty for freeform /
|
|
52
|
+
synthesis / composer seeds) so pre-PR3 callers that read
|
|
53
|
+
``branch.move_id`` directly keep working. The authoritative source of
|
|
54
|
+
branch intent is ``seed`` when present.
|
|
55
|
+
"""
|
|
41
56
|
branch_id: str
|
|
42
57
|
name: str
|
|
43
|
-
|
|
58
|
+
# PR3 — was a required positional; now defaults to "" for seeds whose
|
|
59
|
+
# source is not "semantic_move". When seed is present, move_id mirrors
|
|
60
|
+
# seed.move_id (populated by ExperimentBranch.from_seed).
|
|
61
|
+
move_id: str = ""
|
|
44
62
|
source_kernel_id: str = ""
|
|
45
|
-
status: str = "pending" # pending | running | evaluated | committed | discarded
|
|
63
|
+
status: str = "pending" # pending | running | evaluated | committed | discarded | interesting_but_failed
|
|
46
64
|
|
|
47
|
-
# Compiled plan for this branch
|
|
65
|
+
# Compiled plan for this branch. Pre-PR3 this was always filled in at
|
|
66
|
+
# run_experiment time. Post-PR3, freeform / synthesis / composer producers
|
|
67
|
+
# MAY pre-populate it on the seed path; run_experiment respects a
|
|
68
|
+
# pre-existing plan and only compiles when it's None.
|
|
48
69
|
compiled_plan: Optional[dict] = None
|
|
49
70
|
|
|
50
71
|
# Captured snapshots
|
|
@@ -65,6 +86,44 @@ class ExperimentBranch:
|
|
|
65
86
|
created_at_ms: int = 0
|
|
66
87
|
executed_at_ms: int = 0
|
|
67
88
|
|
|
89
|
+
# PR3 — branch-native seed. None for legacy move-only branches built via
|
|
90
|
+
# the bare constructor; populated when built through from_seed() or via
|
|
91
|
+
# create_experiment_from_seeds.
|
|
92
|
+
seed: Optional[BranchSeed] = None
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def from_seed(
|
|
96
|
+
cls,
|
|
97
|
+
seed: BranchSeed,
|
|
98
|
+
branch_id: str,
|
|
99
|
+
name: str = "",
|
|
100
|
+
source_kernel_id: str = "",
|
|
101
|
+
compiled_plan: Optional[dict] = None,
|
|
102
|
+
created_at_ms: int = 0,
|
|
103
|
+
) -> "ExperimentBranch":
|
|
104
|
+
"""Construct an ExperimentBranch from a BranchSeed.
|
|
105
|
+
|
|
106
|
+
``move_id`` is mirrored from ``seed.move_id`` (empty for freeform /
|
|
107
|
+
synthesis / composer / technique seeds). When ``compiled_plan`` is
|
|
108
|
+
provided, the producer has already compiled — run_experiment will
|
|
109
|
+
skip compilation for this branch. When None, compilation defers to
|
|
110
|
+
the semantic_moves.compiler at run time and only succeeds for
|
|
111
|
+
source="semantic_move" seeds.
|
|
112
|
+
"""
|
|
113
|
+
default_name = (
|
|
114
|
+
f"Branch ({seed.source}:{seed.move_id or seed.seed_id[:8]})"
|
|
115
|
+
)
|
|
116
|
+
return cls(
|
|
117
|
+
branch_id=branch_id,
|
|
118
|
+
name=name or default_name,
|
|
119
|
+
move_id=seed.move_id,
|
|
120
|
+
source_kernel_id=source_kernel_id,
|
|
121
|
+
status="pending",
|
|
122
|
+
compiled_plan=compiled_plan,
|
|
123
|
+
created_at_ms=created_at_ms,
|
|
124
|
+
seed=seed,
|
|
125
|
+
)
|
|
126
|
+
|
|
68
127
|
def to_dict(self) -> dict:
|
|
69
128
|
d = {
|
|
70
129
|
"branch_id": self.branch_id,
|
|
@@ -87,6 +146,12 @@ class ExperimentBranch:
|
|
|
87
146
|
d["execution_log"] = self.execution_log
|
|
88
147
|
d["steps_ok"] = sum(1 for e in self.execution_log if e.get("ok"))
|
|
89
148
|
d["steps_failed"] = sum(1 for e in self.execution_log if not e.get("ok"))
|
|
149
|
+
if self.seed is not None:
|
|
150
|
+
d["seed"] = self.seed.to_dict()
|
|
151
|
+
d["branch_source"] = self.seed.source
|
|
152
|
+
d["analytical_only"] = (
|
|
153
|
+
self.seed.analytical_only or self.compiled_plan is None
|
|
154
|
+
)
|
|
90
155
|
return d
|
|
91
156
|
|
|
92
157
|
|