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.
- package/CHANGELOG.md +219 -0
- package/README.md +7 -7
- 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 +34 -0
- package/mcp_server/branches/types.py +286 -0
- package/mcp_server/composer/__init__.py +10 -1
- package/mcp_server/composer/branch_producer.py +349 -0
- package/mcp_server/composer/tools.py +58 -1
- package/mcp_server/evaluation/policy.py +227 -2
- package/mcp_server/experiment/engine.py +47 -11
- package/mcp_server/experiment/models.py +112 -8
- package/mcp_server/experiment/tools.py +502 -38
- 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/server.py +1 -0
- 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 +273 -0
- package/mcp_server/synthesis_brain/adapters/base.py +86 -0
- package/mcp_server/synthesis_brain/adapters/drift.py +271 -0
- package/mcp_server/synthesis_brain/adapters/meld.py +261 -0
- package/mcp_server/synthesis_brain/adapters/operator.py +292 -0
- package/mcp_server/synthesis_brain/adapters/wavetable.py +364 -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/synthesis_brain/tools.py +231 -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 +3 -3
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
Generates contextually different creative variants ranked by
|
|
4
4
|
taste, identity, and coherence. Each variant is built from a
|
|
5
5
|
real semantic move matched to the request.
|
|
6
|
+
|
|
7
|
+
PR6 adds a branch-native assembly path (generate_branch_seeds) that emits
|
|
8
|
+
BranchSeed objects from multiple producers — semantic_move, technique
|
|
9
|
+
memory, sacred-element inversion, and corpus-hint freeform seeds. The
|
|
10
|
+
existing generate_wonder_variants path is untouched; callers that speak
|
|
11
|
+
BranchSeed can consume the new function, callers that speak "variants"
|
|
12
|
+
keep the old one.
|
|
6
13
|
"""
|
|
7
14
|
|
|
8
15
|
from __future__ import annotations
|
|
@@ -13,6 +20,8 @@ import logging
|
|
|
13
20
|
import math
|
|
14
21
|
from typing import Optional
|
|
15
22
|
|
|
23
|
+
from ..branches import BranchSeed, seed_from_move_id, freeform_seed
|
|
24
|
+
|
|
16
25
|
logger = logging.getLogger(__name__)
|
|
17
26
|
|
|
18
27
|
|
|
@@ -626,3 +635,318 @@ def _wonder_id(request_text: str, kernel_id: str) -> str:
|
|
|
626
635
|
"""Deterministic variant ID prefix — no timestamp."""
|
|
627
636
|
seed = json.dumps({"r": request_text, "k": kernel_id}, sort_keys=True)
|
|
628
637
|
return "wm_" + hashlib.sha256(seed.encode()).hexdigest()[:10]
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ── PR6 — branch-native seed assembly ────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _stable_seed_short_id(prefix: str, key: str) -> str:
|
|
644
|
+
"""Deterministic short id for a seed, no timestamp."""
|
|
645
|
+
h = hashlib.sha256(f"{prefix}:{key}".encode()).hexdigest()[:10]
|
|
646
|
+
return f"{prefix}_{h}"
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
_MOVE_NOVELTY_BY_INDEX = ("safe", "strong", "unexpected")
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def generate_branch_seeds(
|
|
653
|
+
request_text: str,
|
|
654
|
+
kernel: Optional[dict] = None,
|
|
655
|
+
song_brain: Optional[dict] = None,
|
|
656
|
+
active_constraints: object = None,
|
|
657
|
+
taste_graph: object = None,
|
|
658
|
+
max_seeds: int = 3,
|
|
659
|
+
) -> list[BranchSeed]:
|
|
660
|
+
"""Assemble BranchSeeds from multiple creative sources.
|
|
661
|
+
|
|
662
|
+
Branch-native companion to ``generate_wonder_variants``. Instead of
|
|
663
|
+
returning pre-built variant dicts tied to moves, emits BranchSeed
|
|
664
|
+
objects that can be fed to ``create_experiment_from_seeds`` or
|
|
665
|
+
``create_experiment(seeds=[...])`` directly.
|
|
666
|
+
|
|
667
|
+
Sources (assembled in this order until max_seeds is reached):
|
|
668
|
+
|
|
669
|
+
1. ``semantic_move`` — one seed per distinct move discovered by
|
|
670
|
+
``discover_moves`` + ``select_distinct_variants``. Novelty tier
|
|
671
|
+
is assigned positionally: first move = safe, second = strong,
|
|
672
|
+
third = unexpected.
|
|
673
|
+
|
|
674
|
+
2. ``technique`` — freeform seeds built from kernel.session_memory
|
|
675
|
+
entries whose category is "technique" or "success". Low-novelty
|
|
676
|
+
by design (known-good).
|
|
677
|
+
|
|
678
|
+
3. sacred-element inversion — freeform seeds that deliberately
|
|
679
|
+
contrast a sacred element from ``song_brain.sacred_elements``.
|
|
680
|
+
Only emitted when kernel.freshness >= 0.5. High-novelty,
|
|
681
|
+
high-risk. These seeds are typically analytical until a
|
|
682
|
+
producer (Wonder itself or synthesis_brain) compiles them.
|
|
683
|
+
|
|
684
|
+
4. corpus hints — freeform seeds built from ``_get_corpus_hints``.
|
|
685
|
+
Medium novelty, grounded in the corpus knowledge base.
|
|
686
|
+
|
|
687
|
+
Distinctness rules:
|
|
688
|
+
- At most one seed with each exact hypothesis (case-insensitive)
|
|
689
|
+
- semantic_move seeds already pass through select_distinct_variants
|
|
690
|
+
so their (family, plan_shape) distinctness is preserved
|
|
691
|
+
|
|
692
|
+
When kernel is None, the function still works — it just skips the
|
|
693
|
+
kernel-driven sources (technique memory, freshness-gated inversion).
|
|
694
|
+
|
|
695
|
+
Does NOT include synthesis or composer seeds — those emit
|
|
696
|
+
(seed, compiled_plan) pairs and must be handled by callers that
|
|
697
|
+
can thread plans through. Use generate_branch_seeds_and_plans() for
|
|
698
|
+
the full multi-producer assembly.
|
|
699
|
+
"""
|
|
700
|
+
kernel = kernel or {}
|
|
701
|
+
song_brain = song_brain or {}
|
|
702
|
+
seeds: list[BranchSeed] = []
|
|
703
|
+
used_hypotheses: set[str] = set()
|
|
704
|
+
freshness = float(kernel.get("freshness", 0.5) or 0.5)
|
|
705
|
+
|
|
706
|
+
# ── 1. semantic_move seeds ────────────────────────────────────────
|
|
707
|
+
moves = discover_moves(
|
|
708
|
+
request_text,
|
|
709
|
+
taste_graph=taste_graph,
|
|
710
|
+
active_constraints=active_constraints,
|
|
711
|
+
)
|
|
712
|
+
distinct_moves = select_distinct_variants(moves)
|
|
713
|
+
|
|
714
|
+
for i, move in enumerate(distinct_moves):
|
|
715
|
+
if len(seeds) >= max_seeds:
|
|
716
|
+
return seeds
|
|
717
|
+
label = _MOVE_NOVELTY_BY_INDEX[i] if i < len(_MOVE_NOVELTY_BY_INDEX) else "strong"
|
|
718
|
+
hypothesis = move.get("intent") or f"Apply {move.get('move_id', '')}"
|
|
719
|
+
seed = seed_from_move_id(
|
|
720
|
+
move_id=move.get("move_id", ""),
|
|
721
|
+
hypothesis=hypothesis,
|
|
722
|
+
novelty_label=label,
|
|
723
|
+
risk_label=move.get("risk_level", "low"),
|
|
724
|
+
distinctness_reason=_explain_distinctness(move, distinct_moves, i),
|
|
725
|
+
)
|
|
726
|
+
seeds.append(seed)
|
|
727
|
+
used_hypotheses.add(hypothesis.lower())
|
|
728
|
+
|
|
729
|
+
# ── 2. technique seeds from session memory ────────────────────────
|
|
730
|
+
session_mem = kernel.get("session_memory") or []
|
|
731
|
+
for mem in session_mem:
|
|
732
|
+
if len(seeds) >= max_seeds:
|
|
733
|
+
return seeds
|
|
734
|
+
if mem.get("category") not in ("technique", "success"):
|
|
735
|
+
continue
|
|
736
|
+
content = (mem.get("content") or "").strip()
|
|
737
|
+
if not content:
|
|
738
|
+
continue
|
|
739
|
+
hyp = f"Replay: {content[:100]}"
|
|
740
|
+
if hyp.lower() in used_hypotheses:
|
|
741
|
+
continue
|
|
742
|
+
seed = freeform_seed(
|
|
743
|
+
seed_id=_stable_seed_short_id("tech", content),
|
|
744
|
+
hypothesis=hyp,
|
|
745
|
+
source="technique",
|
|
746
|
+
novelty_label="safe",
|
|
747
|
+
risk_label="low",
|
|
748
|
+
distinctness_reason="recalled from session memory — known to work",
|
|
749
|
+
)
|
|
750
|
+
seeds.append(seed)
|
|
751
|
+
used_hypotheses.add(hyp.lower())
|
|
752
|
+
|
|
753
|
+
# ── 3. sacred-element inversion (freshness-gated) ────────────────
|
|
754
|
+
if freshness >= 0.5:
|
|
755
|
+
sacred = song_brain.get("sacred_elements") or []
|
|
756
|
+
for s in sacred[:2]:
|
|
757
|
+
if len(seeds) >= max_seeds:
|
|
758
|
+
return seeds
|
|
759
|
+
elem_type = s.get("element_type", "element")
|
|
760
|
+
desc = s.get("description") or elem_type
|
|
761
|
+
hyp = f"What if we invert {desc}?"
|
|
762
|
+
if hyp.lower() in used_hypotheses:
|
|
763
|
+
continue
|
|
764
|
+
# Protect every OTHER sacred element — the point is to contrast
|
|
765
|
+
# one of them deliberately.
|
|
766
|
+
other_protected = [
|
|
767
|
+
other.get("element_type", "")
|
|
768
|
+
for other in sacred
|
|
769
|
+
if other.get("element_type") != elem_type
|
|
770
|
+
]
|
|
771
|
+
seed = freeform_seed(
|
|
772
|
+
seed_id=_stable_seed_short_id("invert", elem_type),
|
|
773
|
+
hypothesis=hyp,
|
|
774
|
+
source="freeform",
|
|
775
|
+
novelty_label="unexpected",
|
|
776
|
+
risk_label="high",
|
|
777
|
+
protected_qualities=[p for p in other_protected if p],
|
|
778
|
+
distinctness_reason=(
|
|
779
|
+
f"deliberately contrasts the '{desc}' that baseline "
|
|
780
|
+
f"branches preserve"
|
|
781
|
+
),
|
|
782
|
+
)
|
|
783
|
+
seeds.append(seed)
|
|
784
|
+
used_hypotheses.add(hyp.lower())
|
|
785
|
+
|
|
786
|
+
# ── 4. corpus-hint seeds ──────────────────────────────────────────
|
|
787
|
+
corpus_hints = _get_corpus_hints(request_text, song_brain.get("diagnosis"))
|
|
788
|
+
if corpus_hints:
|
|
789
|
+
for kind, hint in corpus_hints.items():
|
|
790
|
+
if len(seeds) >= max_seeds:
|
|
791
|
+
return seeds
|
|
792
|
+
if not isinstance(hint, dict):
|
|
793
|
+
continue
|
|
794
|
+
if kind == "emotional_recipe":
|
|
795
|
+
hyp = (
|
|
796
|
+
f"Apply {hint.get('emotion', 'emotional')} recipe "
|
|
797
|
+
f"({hint.get('technique_count', 0)} techniques)"
|
|
798
|
+
)
|
|
799
|
+
elif kind == "genre_chain":
|
|
800
|
+
devices = hint.get("devices", []) or []
|
|
801
|
+
hyp = (
|
|
802
|
+
f"Build {hint.get('genre', 'genre')} chain: "
|
|
803
|
+
f"{', '.join(devices[:3])}"
|
|
804
|
+
)
|
|
805
|
+
elif kind == "physical_model":
|
|
806
|
+
devices = hint.get("devices", []) or []
|
|
807
|
+
hyp = (
|
|
808
|
+
f"Model {hint.get('material', 'material')} via "
|
|
809
|
+
f"{', '.join(devices[:3])}"
|
|
810
|
+
)
|
|
811
|
+
else:
|
|
812
|
+
continue
|
|
813
|
+
|
|
814
|
+
if hyp.lower() in used_hypotheses:
|
|
815
|
+
continue
|
|
816
|
+
seed = freeform_seed(
|
|
817
|
+
seed_id=_stable_seed_short_id("corpus", f"{kind}:{hyp}"),
|
|
818
|
+
hypothesis=hyp,
|
|
819
|
+
source="freeform",
|
|
820
|
+
novelty_label="strong",
|
|
821
|
+
risk_label="medium",
|
|
822
|
+
distinctness_reason=f"corpus-grounded {kind.replace('_', ' ')}",
|
|
823
|
+
)
|
|
824
|
+
seeds.append(seed)
|
|
825
|
+
used_hypotheses.add(hyp.lower())
|
|
826
|
+
|
|
827
|
+
return seeds
|
|
828
|
+
|
|
829
|
+
|
|
830
|
+
def generate_branch_seeds_and_plans(
|
|
831
|
+
request_text: str,
|
|
832
|
+
kernel: Optional[dict] = None,
|
|
833
|
+
song_brain: Optional[dict] = None,
|
|
834
|
+
active_constraints: object = None,
|
|
835
|
+
taste_graph: object = None,
|
|
836
|
+
max_seeds: int = 3,
|
|
837
|
+
synth_profiles: Optional[list] = None,
|
|
838
|
+
composer_request: Optional[str] = None,
|
|
839
|
+
composer_count: int = 2,
|
|
840
|
+
) -> tuple[list[BranchSeed], dict[str, dict]]:
|
|
841
|
+
"""Full multi-producer branch assembly with pre-compiled plans.
|
|
842
|
+
|
|
843
|
+
Extends ``generate_branch_seeds`` to reach the synthesis and composer
|
|
844
|
+
producers — both of which emit ``(seed, compiled_plan)`` pairs that
|
|
845
|
+
cannot be expressed through the seed-only return type of the base
|
|
846
|
+
function. Returns a tuple:
|
|
847
|
+
|
|
848
|
+
(seeds, compiled_plans_by_seed_id)
|
|
849
|
+
|
|
850
|
+
where ``compiled_plans_by_seed_id`` maps seed_id → plan dict. Seeds
|
|
851
|
+
from producers that don't compile ship with their seed_id absent
|
|
852
|
+
from the dict (e.g. analytical seeds, corpus hints).
|
|
853
|
+
|
|
854
|
+
Additional inputs beyond the base function:
|
|
855
|
+
|
|
856
|
+
synth_profiles: list of :class:`SynthProfile` objects (from
|
|
857
|
+
``synthesis_brain.analyze_synth_patch``). When non-empty, each
|
|
858
|
+
profile is passed to ``propose_synth_branches`` and the returned
|
|
859
|
+
pairs are merged into the output. Typically the caller fetches
|
|
860
|
+
device parameters via ``ableton.send_command('get_device_parameters')``
|
|
861
|
+
and builds a profile per device before calling this function.
|
|
862
|
+
|
|
863
|
+
composer_request: natural-language composition prompt. When set,
|
|
864
|
+
``propose_composer_branches`` is invoked with it and the emitted
|
|
865
|
+
pairs are merged. For composition-shaped requests, pass
|
|
866
|
+
``request_text`` here.
|
|
867
|
+
|
|
868
|
+
composer_count: max composer branches to emit (default 2).
|
|
869
|
+
|
|
870
|
+
Ordering matches generate_branch_seeds where sources overlap:
|
|
871
|
+
semantic_move → technique → synthesis → sacred-inversion → composer
|
|
872
|
+
→ corpus hints. max_seeds still caps total output.
|
|
873
|
+
"""
|
|
874
|
+
# Base seeds (semantic_move, technique, sacred-inversion, corpus)
|
|
875
|
+
base_seeds = generate_branch_seeds(
|
|
876
|
+
request_text=request_text,
|
|
877
|
+
kernel=kernel,
|
|
878
|
+
song_brain=song_brain,
|
|
879
|
+
active_constraints=active_constraints,
|
|
880
|
+
taste_graph=taste_graph,
|
|
881
|
+
max_seeds=max_seeds,
|
|
882
|
+
)
|
|
883
|
+
|
|
884
|
+
# Copy the base seeds so we can interleave producer seeds without
|
|
885
|
+
# mutating the cached list (if the caller happens to share it).
|
|
886
|
+
seeds: list[BranchSeed] = list(base_seeds)
|
|
887
|
+
plans_by_seed: dict[str, dict] = {}
|
|
888
|
+
used_hypotheses = {s.hypothesis.lower() for s in seeds}
|
|
889
|
+
budget_remaining = max(0, max_seeds - len(seeds))
|
|
890
|
+
|
|
891
|
+
# ── synthesis producer ────────────────────────────────────────────
|
|
892
|
+
if budget_remaining > 0 and synth_profiles:
|
|
893
|
+
try:
|
|
894
|
+
from ..synthesis_brain import propose_synth_branches
|
|
895
|
+
except ImportError as exc:
|
|
896
|
+
logger.warning("synthesis_brain unavailable: %s", exc)
|
|
897
|
+
propose_synth_branches = None
|
|
898
|
+
|
|
899
|
+
if propose_synth_branches is not None:
|
|
900
|
+
for profile in synth_profiles:
|
|
901
|
+
if budget_remaining <= 0:
|
|
902
|
+
break
|
|
903
|
+
try:
|
|
904
|
+
pairs = propose_synth_branches(profile, kernel=kernel)
|
|
905
|
+
except Exception as exc:
|
|
906
|
+
logger.warning(
|
|
907
|
+
"propose_synth_branches failed for %s: %s",
|
|
908
|
+
getattr(profile, "device_name", "?"),
|
|
909
|
+
exc,
|
|
910
|
+
)
|
|
911
|
+
continue
|
|
912
|
+
for seed, plan in pairs:
|
|
913
|
+
if budget_remaining <= 0:
|
|
914
|
+
break
|
|
915
|
+
if seed.hypothesis.lower() in used_hypotheses:
|
|
916
|
+
continue
|
|
917
|
+
seeds.append(seed)
|
|
918
|
+
plans_by_seed[seed.seed_id] = plan
|
|
919
|
+
used_hypotheses.add(seed.hypothesis.lower())
|
|
920
|
+
budget_remaining -= 1
|
|
921
|
+
|
|
922
|
+
# ── composer producer ────────────────────────────────────────────
|
|
923
|
+
if budget_remaining > 0 and composer_request:
|
|
924
|
+
try:
|
|
925
|
+
from ..composer import propose_composer_branches
|
|
926
|
+
except ImportError as exc:
|
|
927
|
+
logger.warning("composer branch producer unavailable: %s", exc)
|
|
928
|
+
propose_composer_branches = None
|
|
929
|
+
|
|
930
|
+
if propose_composer_branches is not None:
|
|
931
|
+
try:
|
|
932
|
+
comp_pairs = propose_composer_branches(
|
|
933
|
+
request_text=composer_request,
|
|
934
|
+
kernel=kernel,
|
|
935
|
+
count=min(composer_count, budget_remaining),
|
|
936
|
+
)
|
|
937
|
+
except Exception as exc:
|
|
938
|
+
logger.warning("propose_composer_branches failed: %s", exc)
|
|
939
|
+
comp_pairs = []
|
|
940
|
+
|
|
941
|
+
for seed, plan in comp_pairs:
|
|
942
|
+
if budget_remaining <= 0:
|
|
943
|
+
break
|
|
944
|
+
if seed.hypothesis.lower() in used_hypotheses:
|
|
945
|
+
continue
|
|
946
|
+
seeds.append(seed)
|
|
947
|
+
if plan: # composer may return {} for analytical-only branches
|
|
948
|
+
plans_by_seed[seed.seed_id] = plan
|
|
949
|
+
used_hypotheses.add(seed.hypothesis.lower())
|
|
950
|
+
budget_remaining -= 1
|
|
951
|
+
|
|
952
|
+
return seeds, plans_by_seed
|
|
@@ -17,6 +17,96 @@ from . import engine
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def _build_synth_profiles_for_wonder(ctx, request_text: str, diagnosis: dict) -> list:
|
|
21
|
+
"""Build SynthProfile objects for every track holding a native synth.
|
|
22
|
+
|
|
23
|
+
Wires the synthesis_brain producer into enter_wonder_mode's runtime
|
|
24
|
+
flow — without this helper, ``propose_synth_branches`` is registered
|
|
25
|
+
but unreachable from the MCP surface.
|
|
26
|
+
|
|
27
|
+
Returns a list of SynthProfile objects (possibly empty). All errors
|
|
28
|
+
are swallowed and logged — missing devices, disconnected Ableton,
|
|
29
|
+
unsupported devices all land as "no synth branches" rather than
|
|
30
|
+
failing the whole wonder session.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
from ..synthesis_brain import analyze_synth_patch, supported_devices
|
|
34
|
+
except ImportError as exc:
|
|
35
|
+
logger.debug("synthesis_brain not importable: %s", exc)
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
ableton = ctx.lifespan_context.get("ableton")
|
|
39
|
+
if ableton is None:
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
session = ableton.send_command("get_session_info", {})
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
logger.debug("session fetch for synth profiles failed: %s", exc)
|
|
46
|
+
return []
|
|
47
|
+
if not isinstance(session, dict) or "error" in session:
|
|
48
|
+
return []
|
|
49
|
+
|
|
50
|
+
native_names = set(supported_devices())
|
|
51
|
+
profiles: list = []
|
|
52
|
+
|
|
53
|
+
tracks = session.get("tracks") or []
|
|
54
|
+
for track in tracks:
|
|
55
|
+
track_index = track.get("index")
|
|
56
|
+
devices = track.get("devices") or []
|
|
57
|
+
if track_index is None or not devices:
|
|
58
|
+
continue
|
|
59
|
+
for dev in devices:
|
|
60
|
+
dev_name = dev.get("name") or dev.get("class_name") or ""
|
|
61
|
+
if dev_name not in native_names:
|
|
62
|
+
continue
|
|
63
|
+
dev_index = dev.get("index")
|
|
64
|
+
if dev_index is None:
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
params_result = ableton.send_command(
|
|
68
|
+
"get_device_parameters",
|
|
69
|
+
{"track_index": track_index, "device_index": dev_index},
|
|
70
|
+
)
|
|
71
|
+
except Exception as exc:
|
|
72
|
+
logger.debug(
|
|
73
|
+
"get_device_parameters failed for track=%s device=%s: %s",
|
|
74
|
+
track_index, dev_index, exc,
|
|
75
|
+
)
|
|
76
|
+
continue
|
|
77
|
+
if not isinstance(params_result, dict) or "error" in params_result:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
parameter_state = {}
|
|
81
|
+
display_values = {}
|
|
82
|
+
for p in params_result.get("parameters", []) or []:
|
|
83
|
+
name = p.get("name")
|
|
84
|
+
if name is None:
|
|
85
|
+
continue
|
|
86
|
+
parameter_state[name] = p.get("value")
|
|
87
|
+
if "value_string" in p:
|
|
88
|
+
display_values[name] = p["value_string"]
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
profile = analyze_synth_patch(
|
|
92
|
+
device_name=dev_name,
|
|
93
|
+
track_index=track_index,
|
|
94
|
+
device_index=dev_index,
|
|
95
|
+
parameter_state=parameter_state,
|
|
96
|
+
display_values=display_values,
|
|
97
|
+
role_hint=track.get("name", ""),
|
|
98
|
+
)
|
|
99
|
+
profiles.append(profile)
|
|
100
|
+
except Exception as exc:
|
|
101
|
+
logger.warning(
|
|
102
|
+
"analyze_synth_patch failed for %s on track %s: %s",
|
|
103
|
+
dev_name, track_index, exc,
|
|
104
|
+
)
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
return profiles
|
|
108
|
+
|
|
109
|
+
|
|
20
110
|
def _get_song_brain_dict() -> dict:
|
|
21
111
|
try:
|
|
22
112
|
from ..song_brain.tools import _current_brain
|
|
@@ -164,7 +254,7 @@ def enter_wonder_mode(
|
|
|
164
254
|
except Exception as exc:
|
|
165
255
|
logger.warning("session_info fetch for kernel failed: %s", exc)
|
|
166
256
|
|
|
167
|
-
# 2. Generate variants
|
|
257
|
+
# 2. Generate variants (legacy path)
|
|
168
258
|
result = engine.generate_wonder_variants(
|
|
169
259
|
request_text=request_text,
|
|
170
260
|
diagnosis=diag_dict,
|
|
@@ -176,6 +266,58 @@ def enter_wonder_mode(
|
|
|
176
266
|
sample_context=sample_context,
|
|
177
267
|
)
|
|
178
268
|
|
|
269
|
+
# 2b. PR6 — also emit BranchSeeds for the branch-native experiment flow.
|
|
270
|
+
# Pull session_memory from the conversation lifespan when available so
|
|
271
|
+
# technique seeds can be sourced. Kernel state here is a lightweight
|
|
272
|
+
# dict — enter_wonder_mode doesn't always have a full SessionKernel.
|
|
273
|
+
branch_kernel: dict = {}
|
|
274
|
+
try:
|
|
275
|
+
from ..memory.session_memory import SessionMemoryStore
|
|
276
|
+
mem_store = ctx.lifespan_context.setdefault("session_memory", SessionMemoryStore())
|
|
277
|
+
branch_kernel["session_memory"] = [
|
|
278
|
+
entry.to_dict() for entry in mem_store.get_recent(limit=10)
|
|
279
|
+
]
|
|
280
|
+
except Exception as exc:
|
|
281
|
+
logger.debug("session_memory fetch for seeds failed: %s", exc)
|
|
282
|
+
# Freshness defaults to 0.5 — enter_wonder_mode is inherently exploratory,
|
|
283
|
+
# so lean slightly toward surprise.
|
|
284
|
+
branch_kernel["freshness"] = 0.65
|
|
285
|
+
|
|
286
|
+
# Build SynthProfiles for tracks mentioned in the request or for every
|
|
287
|
+
# track that holds a native synth. Lets propose_synth_branches reach
|
|
288
|
+
# the runtime — without this the producer is dark despite being
|
|
289
|
+
# registered in the conductor.
|
|
290
|
+
synth_profiles = _build_synth_profiles_for_wonder(ctx, request_text, diag_dict)
|
|
291
|
+
|
|
292
|
+
# Composer branches fire when the base conductor routes to
|
|
293
|
+
# composition — otherwise we'd be emitting composition scaffolding
|
|
294
|
+
# for every mix/transition request, which is noise.
|
|
295
|
+
composer_request: str = ""
|
|
296
|
+
try:
|
|
297
|
+
from ..tools._conductor import classify_request
|
|
298
|
+
base_plan = classify_request(request_text)
|
|
299
|
+
if base_plan.routes and base_plan.routes[0].engine == "composition":
|
|
300
|
+
composer_request = request_text
|
|
301
|
+
except Exception as exc:
|
|
302
|
+
logger.debug("composer routing check failed: %s", exc)
|
|
303
|
+
|
|
304
|
+
try:
|
|
305
|
+
branch_seeds, compiled_plans_by_seed = engine.generate_branch_seeds_and_plans(
|
|
306
|
+
request_text=request_text,
|
|
307
|
+
kernel=branch_kernel,
|
|
308
|
+
song_brain=song_brain,
|
|
309
|
+
active_constraints=active_constraints,
|
|
310
|
+
taste_graph=taste_graph,
|
|
311
|
+
synth_profiles=synth_profiles,
|
|
312
|
+
composer_request=composer_request or None,
|
|
313
|
+
)
|
|
314
|
+
branch_seeds_dicts = [s.to_dict() for s in branch_seeds]
|
|
315
|
+
except Exception as exc:
|
|
316
|
+
# Seed assembly is additive — never let it break wonder mode.
|
|
317
|
+
logger.warning("generate_branch_seeds_and_plans failed: %s", exc)
|
|
318
|
+
branch_seeds_dicts = []
|
|
319
|
+
compiled_plans_by_seed = {}
|
|
320
|
+
|
|
179
321
|
# 3. Create WonderSession (unique per invocation, not deterministic)
|
|
180
322
|
import hashlib, time
|
|
181
323
|
_seed = f"{request_text}:{kernel_id}:{time.time()}"
|
|
@@ -217,6 +359,16 @@ def enter_wonder_mode(
|
|
|
217
359
|
"recommended": result.get("recommended", ""),
|
|
218
360
|
"variant_count_actual": result.get("variant_count_actual", 0),
|
|
219
361
|
"degraded_reason": ws.degraded_reason,
|
|
362
|
+
# Branch-native seeds from six producers (semantic_move, technique,
|
|
363
|
+
# synthesis, sacred-inversion, composer, corpus). Feed directly to
|
|
364
|
+
# create_experiment(seeds=[...], compiled_plans=[...]) — align the
|
|
365
|
+
# compiled_plans list with branch_seeds by matching seed_ids.
|
|
366
|
+
"branch_seeds": branch_seeds_dicts,
|
|
367
|
+
# seed_id → pre-compiled plan dict. Only synthesis and composer
|
|
368
|
+
# seeds currently populate this — other sources either defer
|
|
369
|
+
# compilation to run_experiment (semantic_move) or stay analytical
|
|
370
|
+
# (technique reuse, sacred-inversion, corpus hints).
|
|
371
|
+
"compiled_plans_by_seed_id": compiled_plans_by_seed,
|
|
220
372
|
"mode": "wonder",
|
|
221
373
|
}
|
|
222
374
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "livepilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"mcpName": "io.github.dreamrec/livepilot",
|
|
5
|
-
"description": "Agentic production system for Ableton Live 12 —
|
|
5
|
+
"description": "Agentic production system for Ableton Live 12 — 402 tools, 52 domains. Device atlas (1305 devices), sample engine (Splice + browser + filesystem), auto-composition, spectral perception, technique memory, creative intelligence (12 engines)",
|
|
6
6
|
"author": "Pilot Studio",
|
|
7
7
|
"license": "BSL-1.1",
|
|
8
8
|
"type": "commonjs",
|
|
@@ -5,7 +5,7 @@ Entry point for the ControlSurface. Ableton calls create_instance(c_instance)
|
|
|
5
5
|
when this script is selected in Preferences > Link, Tempo & MIDI.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.14.0"
|
|
9
9
|
|
|
10
10
|
from _Framework.ControlSurface import ControlSurface
|
|
11
11
|
from . import router
|
package/server.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.dreamrec/livepilot",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "402-tool agentic MCP production system for Ableton Live 12 — device atlas, sample engine, composer",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "https://github.com/dreamrec/LivePilot",
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
|
-
"version": "1.
|
|
9
|
+
"version": "1.14.0",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "livepilot",
|
|
14
|
-
"version": "1.
|
|
14
|
+
"version": "1.14.0",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
}
|