livepilot 1.12.2 → 1.14.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/README.md +7 -7
  3. package/m4l_device/LivePilot_Analyzer.amxd +0 -0
  4. package/m4l_device/livepilot_bridge.js +1 -1
  5. package/mcp_server/__init__.py +1 -1
  6. package/mcp_server/branches/__init__.py +34 -0
  7. package/mcp_server/branches/types.py +286 -0
  8. package/mcp_server/composer/__init__.py +10 -1
  9. package/mcp_server/composer/branch_producer.py +349 -0
  10. package/mcp_server/composer/tools.py +58 -1
  11. package/mcp_server/evaluation/policy.py +227 -2
  12. package/mcp_server/experiment/engine.py +47 -11
  13. package/mcp_server/experiment/models.py +112 -8
  14. package/mcp_server/experiment/tools.py +502 -38
  15. package/mcp_server/memory/taste_graph.py +84 -11
  16. package/mcp_server/persistence/taste_store.py +21 -5
  17. package/mcp_server/runtime/session_kernel.py +46 -0
  18. package/mcp_server/runtime/tools.py +29 -3
  19. package/mcp_server/server.py +1 -0
  20. package/mcp_server/synthesis_brain/__init__.py +53 -0
  21. package/mcp_server/synthesis_brain/adapters/__init__.py +34 -0
  22. package/mcp_server/synthesis_brain/adapters/analog.py +273 -0
  23. package/mcp_server/synthesis_brain/adapters/base.py +86 -0
  24. package/mcp_server/synthesis_brain/adapters/drift.py +271 -0
  25. package/mcp_server/synthesis_brain/adapters/meld.py +261 -0
  26. package/mcp_server/synthesis_brain/adapters/operator.py +292 -0
  27. package/mcp_server/synthesis_brain/adapters/wavetable.py +364 -0
  28. package/mcp_server/synthesis_brain/engine.py +91 -0
  29. package/mcp_server/synthesis_brain/models.py +121 -0
  30. package/mcp_server/synthesis_brain/timbre.py +194 -0
  31. package/mcp_server/synthesis_brain/tools.py +231 -0
  32. package/mcp_server/tools/_conductor.py +144 -0
  33. package/mcp_server/wonder_mode/engine.py +324 -0
  34. package/mcp_server/wonder_mode/tools.py +153 -1
  35. package/package.json +2 -2
  36. package/remote_script/LivePilot/__init__.py +1 -1
  37. package/server.json +3 -3
@@ -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 create_experiment(
43
+ def create_experiment_from_seeds(
43
44
  request_text: str,
44
- move_ids: list[str],
45
+ seeds: list[BranchSeed],
45
46
  kernel_id: str = "",
47
+ compiled_plans: Optional[list] = None,
46
48
  ) -> ExperimentSet:
47
- """Create an experiment set with branches for each semantic move.
49
+ """Create an experiment set from BranchSeeds (PR3 canonical path).
48
50
 
49
- Does NOT execute anything just creates the branch structures.
50
- Call run_experiment() to actually trial each branch.
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, move_id in enumerate(move_ids):
57
- branch = ExperimentBranch(
58
- branch_id=_gen_id("br", f"{move_id}_{i}"),
59
- name=f"Branch {i+1}: {move_id}",
60
- move_id=move_id,
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
- status="pending",
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 of a semantic move against the
4
- current session state. Multiple branches form an experiment set that can
5
- be compared and ranked.
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,15 +18,42 @@ 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:
17
- """Captured state before or after a branch experiment."""
26
+ """Captured state before or after a branch experiment.
27
+
28
+ Pre-PR4 fields (spectrum / rms / peak / track_meters) stay the same —
29
+ they remain the fast-path evidence when render-verify isn't available
30
+ or wasn't opted in.
31
+
32
+ PR4 adds render-based fields that are populated only when the
33
+ experiment runs with render_verify=True:
34
+
35
+ capture_path: path to the captured audio file (useful for re-analysis
36
+ or user audition of the branch output).
37
+ loudness: {lufs, lra, rms, peak, crest} from analyze_loudness.
38
+ spectral_shape: {centroid, flatness, rolloff, crest} from FluCoMa or
39
+ the offline analyzer.
40
+ fingerprint: TimbralFingerprint.to_dict() extracted from the
41
+ captured audio.
42
+
43
+ The fingerprint is what classify_branch_outcome reads to derive a
44
+ real goal_progress + measurable_count instead of relying on the
45
+ inline meter-based heuristic alone.
46
+ """
18
47
  spectrum: Optional[dict] = None
19
48
  rms: Optional[float] = None
20
49
  peak: Optional[float] = None
21
50
  track_meters: Optional[list] = None
22
51
  timestamp_ms: int = 0
52
+ # PR4 — render-based evidence (opt-in via render_verify flag)
53
+ capture_path: Optional[str] = None
54
+ loudness: Optional[dict] = None
55
+ spectral_shape: Optional[dict] = None
56
+ fingerprint: Optional[dict] = None # TimbralFingerprint.to_dict()
23
57
 
24
58
  def to_dict(self) -> dict:
25
59
  d = {}
@@ -31,20 +65,40 @@ class BranchSnapshot:
31
65
  d["peak"] = self.peak
32
66
  if self.track_meters is not None:
33
67
  d["track_meters"] = self.track_meters
68
+ if self.capture_path is not None:
69
+ d["capture_path"] = self.capture_path
70
+ if self.loudness is not None:
71
+ d["loudness"] = self.loudness
72
+ if self.spectral_shape is not None:
73
+ d["spectral_shape"] = self.spectral_shape
74
+ if self.fingerprint is not None:
75
+ d["fingerprint"] = self.fingerprint
34
76
  d["timestamp_ms"] = self.timestamp_ms
35
77
  return d
36
78
 
37
79
 
38
80
  @dataclass
39
81
  class ExperimentBranch:
40
- """One trial branch in an experiment set."""
82
+ """One trial branch in an experiment set.
83
+
84
+ move_id is retained as an optional convenience (empty for freeform /
85
+ synthesis / composer seeds) so pre-PR3 callers that read
86
+ ``branch.move_id`` directly keep working. The authoritative source of
87
+ branch intent is ``seed`` when present.
88
+ """
41
89
  branch_id: str
42
90
  name: str
43
- move_id: str
91
+ # PR3 — was a required positional; now defaults to "" for seeds whose
92
+ # source is not "semantic_move". When seed is present, move_id mirrors
93
+ # seed.move_id (populated by ExperimentBranch.from_seed).
94
+ move_id: str = ""
44
95
  source_kernel_id: str = ""
45
- status: str = "pending" # pending | running | evaluated | committed | discarded
96
+ status: str = "pending" # pending | running | evaluated | committed | discarded | interesting_but_failed
46
97
 
47
- # Compiled plan for this branch
98
+ # Compiled plan for this branch. Pre-PR3 this was always filled in at
99
+ # run_experiment time. Post-PR3, freeform / synthesis / composer producers
100
+ # MAY pre-populate it on the seed path; run_experiment respects a
101
+ # pre-existing plan and only compiles when it's None.
48
102
  compiled_plan: Optional[dict] = None
49
103
 
50
104
  # Captured snapshots
@@ -65,6 +119,44 @@ class ExperimentBranch:
65
119
  created_at_ms: int = 0
66
120
  executed_at_ms: int = 0
67
121
 
122
+ # PR3 — branch-native seed. None for legacy move-only branches built via
123
+ # the bare constructor; populated when built through from_seed() or via
124
+ # create_experiment_from_seeds.
125
+ seed: Optional[BranchSeed] = None
126
+
127
+ @classmethod
128
+ def from_seed(
129
+ cls,
130
+ seed: BranchSeed,
131
+ branch_id: str,
132
+ name: str = "",
133
+ source_kernel_id: str = "",
134
+ compiled_plan: Optional[dict] = None,
135
+ created_at_ms: int = 0,
136
+ ) -> "ExperimentBranch":
137
+ """Construct an ExperimentBranch from a BranchSeed.
138
+
139
+ ``move_id`` is mirrored from ``seed.move_id`` (empty for freeform /
140
+ synthesis / composer / technique seeds). When ``compiled_plan`` is
141
+ provided, the producer has already compiled — run_experiment will
142
+ skip compilation for this branch. When None, compilation defers to
143
+ the semantic_moves.compiler at run time and only succeeds for
144
+ source="semantic_move" seeds.
145
+ """
146
+ default_name = (
147
+ f"Branch ({seed.source}:{seed.move_id or seed.seed_id[:8]})"
148
+ )
149
+ return cls(
150
+ branch_id=branch_id,
151
+ name=name or default_name,
152
+ move_id=seed.move_id,
153
+ source_kernel_id=source_kernel_id,
154
+ status="pending",
155
+ compiled_plan=compiled_plan,
156
+ created_at_ms=created_at_ms,
157
+ seed=seed,
158
+ )
159
+
68
160
  def to_dict(self) -> dict:
69
161
  d = {
70
162
  "branch_id": self.branch_id,
@@ -87,6 +179,18 @@ class ExperimentBranch:
87
179
  d["execution_log"] = self.execution_log
88
180
  d["steps_ok"] = sum(1 for e in self.execution_log if e.get("ok"))
89
181
  d["steps_failed"] = sum(1 for e in self.execution_log if not e.get("ok"))
182
+ if self.seed is not None:
183
+ d["seed"] = self.seed.to_dict()
184
+ d["branch_source"] = self.seed.source
185
+ d["analytical_only"] = (
186
+ self.seed.analytical_only or self.compiled_plan is None
187
+ )
188
+ # Shortcut to the seed's producer_payload so downstream callers
189
+ # (composer winner-commit, synthesis re-target, provenance logs)
190
+ # don't have to reach into d["seed"]["producer_payload"] every
191
+ # time. The full seed dict is still available for producers
192
+ # that need other fields.
193
+ d["producer_payload"] = dict(self.seed.producer_payload or {})
90
194
  return d
91
195
 
92
196