self-evolve-framework 1.3.0 → 1.5.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/package.json +1 -1
- package/template/rules/ponytail.mdc +98 -23
- package/template/skills/skillopt-sleep/SKILL.md +48 -34
- package/template/skills/skillopt-sleep/scripts/python/__init__.py +20 -0
- package/template/skills/skillopt-sleep/scripts/python/__main__.py +343 -0
- package/template/skills/skillopt-sleep/scripts/python/backend.py +1371 -0
- package/template/skills/skillopt-sleep/scripts/python/budget.py +75 -0
- package/template/skills/skillopt-sleep/scripts/python/config.py +162 -0
- package/template/skills/skillopt-sleep/scripts/python/consolidate.py +238 -0
- package/template/skills/skillopt-sleep/scripts/python/cycle.py +291 -0
- package/template/skills/skillopt-sleep/scripts/python/dream.py +138 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/__init__.py +1 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/gbrain_bench.py +119 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/personas.py +86 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/report.py +132 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/run_experiment.py +178 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/run_gbrain.py +209 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/run_transfer.py +155 -0
- package/template/skills/skillopt-sleep/scripts/python/experiments/sweep.py +164 -0
- package/template/skills/skillopt-sleep/scripts/python/gate.py +50 -0
- package/template/skills/skillopt-sleep/scripts/python/harvest.py +304 -0
- package/template/skills/skillopt-sleep/scripts/python/harvest_codex.py +253 -0
- package/template/skills/skillopt-sleep/scripts/python/harvest_sources.py +41 -0
- package/template/skills/skillopt-sleep/scripts/python/judges.py +84 -0
- package/template/skills/skillopt-sleep/scripts/python/llm_miner.py +134 -0
- package/template/skills/skillopt-sleep/scripts/python/memory.py +129 -0
- package/template/skills/skillopt-sleep/scripts/python/mine.py +312 -0
- package/template/skills/skillopt-sleep/scripts/python/replay.py +146 -0
- package/template/skills/skillopt-sleep/scripts/python/rollout.py +153 -0
- package/template/skills/skillopt-sleep/scripts/python/scheduler.py +138 -0
- package/template/skills/skillopt-sleep/scripts/python/slow_update.py +142 -0
- package/template/skills/skillopt-sleep/scripts/python/staging.py +103 -0
- package/template/skills/skillopt-sleep/scripts/python/state.py +96 -0
- package/template/skills/skillopt-sleep/scripts/python/tasks_file.py +81 -0
- package/template/skills/skillopt-sleep/scripts/python/types.py +146 -0
- package/template/skills/skillopt-sleep/scripts/shell/__init__.py +0 -0
- package/template/skills/skillopt-sleep/scripts/shell/eval_only.py +466 -0
- package/template/skills/skillopt-sleep/scripts/shell/materialize_searchqa.py +148 -0
- package/template/skills/skillopt-sleep/scripts/shell/run_alfworld.sh +60 -0
- package/template/skills/skillopt-sleep/scripts/shell/run_searchqa.sh +40 -0
- package/template/skills/skillopt-sleep/scripts/shell/run_spreadsheetbench.sh +39 -0
- package/template/skills/skillopt-sleep/scripts/shell/train.py +556 -0
- package/template/skills/ponytail/SKILL.md +0 -117
- package/template/skills/ponytail-audit/SKILL.md +0 -41
- package/template/skills/ponytail-debt/SKILL.md +0 -44
- package/template/skills/ponytail-gain/SKILL.md +0 -50
- package/template/skills/ponytail-help/SKILL.md +0 -69
- package/template/skills/ponytail-review/SKILL.md +0 -57
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — budget controller.
|
|
2
|
+
|
|
3
|
+
Lets the user say how much they're willing to spend on a night's "dreaming",
|
|
4
|
+
in tokens or wall-clock minutes, and the engine schedules depth (how many
|
|
5
|
+
rollouts × how many nights) within that budget. Stops cleanly when exhausted
|
|
6
|
+
and reports what it skipped (no silent truncation).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Budget:
|
|
16
|
+
max_tokens: Optional[int] = None # None = unlimited
|
|
17
|
+
max_minutes: Optional[float] = None # None = unlimited
|
|
18
|
+
_start_time: Optional[float] = None
|
|
19
|
+
_tokens_at_start: int = 0
|
|
20
|
+
|
|
21
|
+
def start(self, clock_fn, tokens_now: int) -> None:
|
|
22
|
+
self._start_time = clock_fn()
|
|
23
|
+
self._tokens_at_start = tokens_now
|
|
24
|
+
|
|
25
|
+
def tokens_spent(self, tokens_now: int) -> int:
|
|
26
|
+
return max(0, tokens_now - self._tokens_at_start)
|
|
27
|
+
|
|
28
|
+
def minutes_elapsed(self, clock_fn) -> float:
|
|
29
|
+
if self._start_time is None:
|
|
30
|
+
return 0.0
|
|
31
|
+
return (clock_fn() - self._start_time) / 60.0
|
|
32
|
+
|
|
33
|
+
def remaining_fraction(self, *, tokens_now: int, clock_fn) -> float:
|
|
34
|
+
"""Smallest remaining fraction across all active limits (1.0 = fresh)."""
|
|
35
|
+
fracs = [1.0]
|
|
36
|
+
if self.max_tokens:
|
|
37
|
+
fracs.append(max(0.0, 1.0 - self.tokens_spent(tokens_now) / self.max_tokens))
|
|
38
|
+
if self.max_minutes:
|
|
39
|
+
fracs.append(max(0.0, 1.0 - self.minutes_elapsed(clock_fn) / self.max_minutes))
|
|
40
|
+
return min(fracs)
|
|
41
|
+
|
|
42
|
+
def exhausted(self, *, tokens_now: int, clock_fn) -> bool:
|
|
43
|
+
if self.max_tokens and self.tokens_spent(tokens_now) >= self.max_tokens:
|
|
44
|
+
return True
|
|
45
|
+
if self.max_minutes and self.minutes_elapsed(clock_fn) >= self.max_minutes:
|
|
46
|
+
return True
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def status(self, *, tokens_now: int, clock_fn) -> str:
|
|
50
|
+
parts = []
|
|
51
|
+
if self.max_tokens:
|
|
52
|
+
parts.append(f"tokens {self.tokens_spent(tokens_now)}/{self.max_tokens}")
|
|
53
|
+
if self.max_minutes:
|
|
54
|
+
parts.append(f"minutes {self.minutes_elapsed(clock_fn):.1f}/{self.max_minutes}")
|
|
55
|
+
return ", ".join(parts) or "unbounded"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def plan_depth(budget: Budget, *, n_tasks: int,
|
|
59
|
+
default_nights: int = 2, default_k: int = 1) -> tuple:
|
|
60
|
+
"""Heuristically choose (nights, rollouts_per_task) from a token budget.
|
|
61
|
+
|
|
62
|
+
Rough cost model: one rollout ≈ 1 unit; a night does ~n_tasks*k rollouts
|
|
63
|
+
plus reflect/gate (~2*n_tasks). We scale k and nights up with more budget.
|
|
64
|
+
Returns (nights, k). With no budget set, returns the defaults.
|
|
65
|
+
"""
|
|
66
|
+
if not budget.max_tokens:
|
|
67
|
+
return default_nights, default_k
|
|
68
|
+
# assume ~1.5k tokens per rollout as a planning constant
|
|
69
|
+
rollouts_affordable = budget.max_tokens / 1500.0
|
|
70
|
+
per_night = max(1, n_tasks) * 3 # rollouts + reflect + gate, k=1
|
|
71
|
+
nights = max(1, min(4, int(rollouts_affordable // per_night)))
|
|
72
|
+
# spend surplus on more rollouts-per-task (contrastive signal)
|
|
73
|
+
surplus = rollouts_affordable - nights * per_night
|
|
74
|
+
k = max(1, min(5, 1 + int(surplus // max(1, n_tasks))))
|
|
75
|
+
return nights, k
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — configuration.
|
|
2
|
+
|
|
3
|
+
Config is JSON-first (yaml optional) so the engine and the deterministic
|
|
4
|
+
experiment run with zero external dependencies. Defaults are safe:
|
|
5
|
+
review-gated adoption, single-project scope, bounded token/task budgets.
|
|
6
|
+
|
|
7
|
+
Resolution order (later wins):
|
|
8
|
+
1. built-in DEFAULTS
|
|
9
|
+
2. ~/.skillopt-sleep/config.json (or .yaml if PyYAML available)
|
|
10
|
+
3. explicit overrides passed to load_config(**overrides)
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, Optional
|
|
18
|
+
|
|
19
|
+
HOME_STATE_DIR = os.path.expanduser("~/.skillopt-sleep")
|
|
20
|
+
CLAUDE_HOME = os.path.expanduser("~/.claude")
|
|
21
|
+
CODEX_HOME = os.path.expanduser("~/.codex")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DEFAULTS: Dict[str, Any] = {
|
|
25
|
+
# ── scope ──────────────────────────────────────────────────────────────
|
|
26
|
+
"claude_home": CLAUDE_HOME,
|
|
27
|
+
"codex_home": CODEX_HOME,
|
|
28
|
+
"transcript_source": "claude", # "claude" | "codex" | "auto"
|
|
29
|
+
"projects": "invoked", # "invoked" | "all" | [list of abs paths]
|
|
30
|
+
"invoked_project": "", # filled at runtime (cwd) when projects == "invoked"
|
|
31
|
+
"lookback_hours": 72, # harvest window when no prior sleep recorded
|
|
32
|
+
# ── budgets ────────────────────────────────────────────────────────────
|
|
33
|
+
"max_tasks_per_night": 40,
|
|
34
|
+
"max_tokens_per_night": 400_000,
|
|
35
|
+
"holdout_fraction": 0.34, # legacy alias for val_fraction
|
|
36
|
+
"val_fraction": 0.34, # real tasks reserved to gate updates
|
|
37
|
+
"test_fraction": 0.0, # real tasks reserved as the final held-out measure
|
|
38
|
+
# ── optimizer ──────────────────────────────────────────────────────────
|
|
39
|
+
"backend": "mock", # "mock" | "claude" | "codex" | "copilot"
|
|
40
|
+
"model": "", # backend-specific; "" => backend default
|
|
41
|
+
"gate_mode": "on", # "on" (validation-gated) | "off" (greedy, no hard filter)
|
|
42
|
+
"codex_path": "", # "" => auto-detect the real @openai/codex binary
|
|
43
|
+
"edit_budget": 4, # textual learning rate (max edits/night)
|
|
44
|
+
"gate_metric": "mixed", # hard | soft | mixed (mixed best for tiny holdouts)
|
|
45
|
+
"gate_mixed_weight": 0.5,
|
|
46
|
+
"replay_mode": "mock", # "mock" (sandboxed prompt) | "fresh" (worktree)
|
|
47
|
+
# ── dream + recall (opt-in; defaults reproduce the prior single-shot loop) ─
|
|
48
|
+
"dream_rollouts": 1, # >1 => multi-rollout contrastive reflection per task
|
|
49
|
+
"dream_factor": 0, # >0 => add N synthetic variants of each task to the dream
|
|
50
|
+
"recall_k": 0, # >0 => recall the K most-similar past tasks into the dream
|
|
51
|
+
"evolve_memory": True, # consolidate CLAUDE.md
|
|
52
|
+
"evolve_skill": True, # consolidate the managed SKILL.md
|
|
53
|
+
"llm_mine": True, # use the backend to mine checkable tasks (real backends)
|
|
54
|
+
"target_skill_path": "", # explicit SKILL.md target for repo-scoped agents
|
|
55
|
+
"target_task_filter": True, # prefer mined tasks matching target_skill_path/text
|
|
56
|
+
"progress": False, # print phase progress to stderr
|
|
57
|
+
# ── adoption / safety ──────────────────────────────────────────────────
|
|
58
|
+
"auto_adopt": False, # default: stage + require explicit `adopt`
|
|
59
|
+
"managed_skill_name": "skillopt-sleep-learned",
|
|
60
|
+
"redact_secrets": True,
|
|
61
|
+
"seed": 42,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class SleepConfig:
|
|
67
|
+
data: Dict[str, Any] = field(default_factory=lambda: dict(DEFAULTS))
|
|
68
|
+
|
|
69
|
+
# convenient attribute access -------------------------------------------
|
|
70
|
+
def __getattr__(self, name: str) -> Any:
|
|
71
|
+
# only called when normal attribute lookup fails
|
|
72
|
+
data = object.__getattribute__(self, "data")
|
|
73
|
+
if name in data:
|
|
74
|
+
return data[name]
|
|
75
|
+
raise AttributeError(name)
|
|
76
|
+
|
|
77
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
78
|
+
return self.data.get(key, default)
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
81
|
+
return dict(self.data)
|
|
82
|
+
|
|
83
|
+
# paths ------------------------------------------------------------------
|
|
84
|
+
@property
|
|
85
|
+
def state_dir(self) -> str:
|
|
86
|
+
# Allow full isolation: if the caller overrides state_dir explicitly,
|
|
87
|
+
# honor it; else derive from claude_home's parent so a single
|
|
88
|
+
# --claude-home flag isolates transcripts AND state together; else the
|
|
89
|
+
# default ~/.skillopt-sleep.
|
|
90
|
+
explicit = self.data.get("state_dir")
|
|
91
|
+
if explicit:
|
|
92
|
+
return explicit
|
|
93
|
+
ch = self.data.get("claude_home", CLAUDE_HOME)
|
|
94
|
+
if os.path.abspath(ch) != os.path.abspath(CLAUDE_HOME):
|
|
95
|
+
return os.path.join(os.path.dirname(os.path.abspath(ch)), ".skillopt-sleep")
|
|
96
|
+
return HOME_STATE_DIR
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def state_path(self) -> str:
|
|
100
|
+
return os.path.join(self.state_dir, "state.json")
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def transcripts_dir(self) -> str:
|
|
104
|
+
return os.path.join(self.data["claude_home"], "projects")
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def codex_archived_sessions_dir(self) -> str:
|
|
108
|
+
return os.path.join(self.data["codex_home"], "archived_sessions")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def history_path(self) -> str:
|
|
112
|
+
return os.path.join(self.data["claude_home"], "history.jsonl")
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def skills_dir(self) -> str:
|
|
116
|
+
return os.path.join(self.data["claude_home"], "skills")
|
|
117
|
+
|
|
118
|
+
def managed_skill_path(self) -> str:
|
|
119
|
+
target = self.data.get("target_skill_path") or ""
|
|
120
|
+
if target:
|
|
121
|
+
target = os.path.expanduser(str(target))
|
|
122
|
+
if not os.path.isabs(target):
|
|
123
|
+
base = self.data.get("invoked_project") or os.getcwd()
|
|
124
|
+
target = os.path.join(base, target)
|
|
125
|
+
return os.path.abspath(target)
|
|
126
|
+
return os.path.join(
|
|
127
|
+
self.skills_dir, self.data["managed_skill_name"], "SKILL.md"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _user_config_path() -> Optional[str]:
|
|
132
|
+
for name in ("config.json", "config.yaml", "config.yml"):
|
|
133
|
+
p = os.path.join(HOME_STATE_DIR, name)
|
|
134
|
+
if os.path.exists(p):
|
|
135
|
+
return p
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _load_file(path: str) -> Dict[str, Any]:
|
|
140
|
+
if path.endswith((".yaml", ".yml")):
|
|
141
|
+
try:
|
|
142
|
+
import yaml # optional
|
|
143
|
+
with open(path) as f:
|
|
144
|
+
return yaml.safe_load(f) or {}
|
|
145
|
+
except Exception:
|
|
146
|
+
return {}
|
|
147
|
+
with open(path) as f:
|
|
148
|
+
return json.load(f)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_config(**overrides: Any) -> SleepConfig:
|
|
152
|
+
data = dict(DEFAULTS)
|
|
153
|
+
path = _user_config_path()
|
|
154
|
+
if path:
|
|
155
|
+
try:
|
|
156
|
+
data.update(_load_file(path) or {})
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
data.update({k: v for k, v in overrides.items() if v is not None})
|
|
160
|
+
if data.get("projects") == "invoked" and not data.get("invoked_project"):
|
|
161
|
+
data["invoked_project"] = os.getcwd()
|
|
162
|
+
return SleepConfig(data=data)
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — Stage 4: consolidate (one SkillOpt epoch).
|
|
2
|
+
|
|
3
|
+
This is the core that makes nightly evolution *safe*: it proposes bounded
|
|
4
|
+
edits from replayed failures, applies them to a candidate skill/memory, then
|
|
5
|
+
**gates** the candidate on a held-out slice of the user's own tasks. Only a
|
|
6
|
+
candidate that strictly improves the held-out score is accepted — the SkillOpt
|
|
7
|
+
validation gate, vendored self-contained in ``skillopt_sleep.gate``.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import List, Optional, Tuple
|
|
14
|
+
|
|
15
|
+
from skillopt_sleep.backend import Backend
|
|
16
|
+
from skillopt_sleep.memory import apply_edits
|
|
17
|
+
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
|
18
|
+
from skillopt_sleep.types import EditRecord, ReplayResult, TaskRecord
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Self-contained validation gate (vendored from SkillOpt; zero dependency on the
|
|
22
|
+
# research package, so this open-source tool stays decoupled from the paper code).
|
|
23
|
+
from skillopt_sleep.gate import evaluate_gate, select_gate_score
|
|
24
|
+
_HAVE_REPO_GATE = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class ConsolidationResult:
|
|
29
|
+
accepted: bool
|
|
30
|
+
gate_action: str
|
|
31
|
+
baseline_score: float
|
|
32
|
+
candidate_score: float
|
|
33
|
+
new_skill: str
|
|
34
|
+
new_memory: str
|
|
35
|
+
applied_edits: List[EditRecord]
|
|
36
|
+
rejected_edits: List[EditRecord]
|
|
37
|
+
holdout_baseline: float
|
|
38
|
+
holdout_candidate: float
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _split(tasks: List[TaskRecord]) -> Tuple[List[TaskRecord], List[TaskRecord]]:
|
|
42
|
+
"""Return (train_tasks, val_tasks).
|
|
43
|
+
|
|
44
|
+
train drives reflect; val gates updates. test is held out entirely from
|
|
45
|
+
consolidation and is scored by the caller. Accepts legacy split names
|
|
46
|
+
(replay->train, holdout->val) for robustness.
|
|
47
|
+
"""
|
|
48
|
+
def _norm(s: str) -> str:
|
|
49
|
+
return {"replay": "train", "holdout": "val"}.get(s, s)
|
|
50
|
+
|
|
51
|
+
train = [t for t in tasks if _norm(t.split) == "train"]
|
|
52
|
+
val = [t for t in tasks if _norm(t.split) == "val"]
|
|
53
|
+
# be robust if a split is empty: fall back so a night still does something,
|
|
54
|
+
# but never silently use test as val.
|
|
55
|
+
test = [t for t in tasks if _norm(t.split) == "test"]
|
|
56
|
+
if not val:
|
|
57
|
+
# prefer train as the gate reference over nothing; last resort all-but-test
|
|
58
|
+
val = train or [t for t in tasks if _norm(t.split) != "test"] or tasks
|
|
59
|
+
if not train:
|
|
60
|
+
train = val
|
|
61
|
+
return train, val
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def consolidate(
|
|
65
|
+
backend: Backend,
|
|
66
|
+
tasks: List[TaskRecord],
|
|
67
|
+
skill: str,
|
|
68
|
+
memory: str,
|
|
69
|
+
*,
|
|
70
|
+
edit_budget: int = 4,
|
|
71
|
+
gate_metric: str = "mixed",
|
|
72
|
+
gate_mixed_weight: float = 0.5,
|
|
73
|
+
gate_mode: str = "on", # "on" (hard/soft per gate_metric) | "off" (greedy)
|
|
74
|
+
rollouts_k: int = 1, # >1 => multi-rollout contrastive reflection
|
|
75
|
+
evolve_skill: bool = True,
|
|
76
|
+
evolve_memory: bool = True,
|
|
77
|
+
night: int = 1,
|
|
78
|
+
) -> ConsolidationResult:
|
|
79
|
+
"""Run one consolidation epoch: reflect -> bounded edit -> gate.
|
|
80
|
+
|
|
81
|
+
train tasks drive reflect; val tasks gate the update (test is held out by the
|
|
82
|
+
caller). With ``gate_mode='off'`` edits are accepted greedily (no val-improve
|
|
83
|
+
requirement) — the user opts out of hard filtering — but val scores are still
|
|
84
|
+
recorded so the report shows whether quality moved.
|
|
85
|
+
|
|
86
|
+
Skill and memory are evolved in sequence (skill first if both enabled).
|
|
87
|
+
"""
|
|
88
|
+
train_tasks, val_tasks = _split(tasks)
|
|
89
|
+
gate_off = str(gate_mode).strip().lower() in {"off", "none", "false", "greedy"}
|
|
90
|
+
|
|
91
|
+
# ── baseline on the VAL slice (the gate reference) ────────────────────
|
|
92
|
+
# When the gate is OFF the user has opted out of holding out a validation set
|
|
93
|
+
# (the daily-use design): we accept edits greedily and judge quality only on
|
|
94
|
+
# the real test set, scored by the caller. So we SKIP all val scoring — it is
|
|
95
|
+
# both wasted cost and contrary to the "no val set required" design.
|
|
96
|
+
if gate_off:
|
|
97
|
+
base_hard, base_soft = 0.0, 0.0
|
|
98
|
+
else:
|
|
99
|
+
base_pairs = replay_batch(backend, val_tasks, skill, memory)
|
|
100
|
+
base_hard, base_soft = aggregate_scores(base_pairs)
|
|
101
|
+
base_score = select_gate_score(base_hard, base_soft, gate_metric, gate_mixed_weight)
|
|
102
|
+
|
|
103
|
+
# ── reflect over TRAIN-split failures/successes ───────────────────────
|
|
104
|
+
train_pairs = replay_batch(backend, train_tasks, skill, memory)
|
|
105
|
+
failures = [(t, r) for (t, r) in train_pairs if r.hard < 1.0]
|
|
106
|
+
successes = [(t, r) for (t, r) in train_pairs if r.hard >= 1.0]
|
|
107
|
+
|
|
108
|
+
cand_skill, cand_memory = skill, memory
|
|
109
|
+
all_applied: List[EditRecord] = []
|
|
110
|
+
all_rejected: List[EditRecord] = []
|
|
111
|
+
|
|
112
|
+
def _gate_apply(doc: str, edits: List[EditRecord], which: str) -> str:
|
|
113
|
+
nonlocal cand_skill, cand_memory, base_score, all_applied, all_rejected
|
|
114
|
+
if not edits:
|
|
115
|
+
return doc
|
|
116
|
+
new_doc, applied = apply_edits(doc, edits)
|
|
117
|
+
if not applied:
|
|
118
|
+
return doc
|
|
119
|
+
# gate OFF: accept greedily with NO val scoring (the daily-use path)
|
|
120
|
+
if gate_off:
|
|
121
|
+
all_applied.extend(applied)
|
|
122
|
+
return new_doc
|
|
123
|
+
# gate ON: score the candidate on the VAL slice, keep only if it improves
|
|
124
|
+
trial_skill = new_doc if which == "skill" else cand_skill
|
|
125
|
+
trial_memory = new_doc if which == "memory" else cand_memory
|
|
126
|
+
pairs = replay_batch(backend, val_tasks, trial_skill, trial_memory)
|
|
127
|
+
h, s = aggregate_scores(pairs)
|
|
128
|
+
cand_score = select_gate_score(h, s, gate_metric, gate_mixed_weight)
|
|
129
|
+
if cand_score > base_score:
|
|
130
|
+
base_score = max(base_score, cand_score)
|
|
131
|
+
all_applied.extend(applied)
|
|
132
|
+
return new_doc
|
|
133
|
+
all_rejected.extend(applied)
|
|
134
|
+
return doc
|
|
135
|
+
|
|
136
|
+
if evolve_skill:
|
|
137
|
+
if rollouts_k > 1:
|
|
138
|
+
# multi-rollout contrastive reflection: run each train task K times
|
|
139
|
+
# and distill a rule from the good-vs-bad contrast (the imagination signal).
|
|
140
|
+
from skillopt_sleep.rollout import multi_rollout, contrastive_reflect
|
|
141
|
+
# Parallelize across tasks (each multi_rollout also parallelizes its K
|
|
142
|
+
# attempts). This dream phase is the dominant cost; serial execution
|
|
143
|
+
# times out on real backends. Cap total in-flight at the worker env.
|
|
144
|
+
import os
|
|
145
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
146
|
+
try:
|
|
147
|
+
_w = int(os.environ.get("SKILLOPT_SLEEP_WORKERS", "1"))
|
|
148
|
+
except ValueError:
|
|
149
|
+
_w = 1
|
|
150
|
+
if _w > 1 and len(train_tasks) > 1:
|
|
151
|
+
# split the worker budget between task-parallelism and per-task K
|
|
152
|
+
task_workers = max(1, min(len(train_tasks), _w))
|
|
153
|
+
per_task = max(1, _w // task_workers)
|
|
154
|
+
with ThreadPoolExecutor(max_workers=task_workers) as ex:
|
|
155
|
+
sets = list(ex.map(
|
|
156
|
+
lambda t: multi_rollout(backend, t, cand_skill, cand_memory,
|
|
157
|
+
k=rollouts_k, workers=per_task),
|
|
158
|
+
train_tasks))
|
|
159
|
+
else:
|
|
160
|
+
sets = [multi_rollout(backend, t, cand_skill, cand_memory,
|
|
161
|
+
k=rollouts_k, workers=1)
|
|
162
|
+
for t in train_tasks]
|
|
163
|
+
edits = contrastive_reflect(
|
|
164
|
+
backend, sets, cand_skill, cand_memory,
|
|
165
|
+
edit_budget=edit_budget, target="skill",
|
|
166
|
+
)
|
|
167
|
+
# fall back to single-shot reflect if contrast yielded nothing
|
|
168
|
+
if not edits:
|
|
169
|
+
edits = backend.reflect(
|
|
170
|
+
failures, successes, cand_skill, cand_memory,
|
|
171
|
+
edit_budget=edit_budget, evolve_skill=True, evolve_memory=False,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
edits = backend.reflect(
|
|
175
|
+
failures, successes, cand_skill, cand_memory,
|
|
176
|
+
edit_budget=edit_budget, evolve_skill=True, evolve_memory=False,
|
|
177
|
+
)
|
|
178
|
+
cand_skill = _gate_apply(cand_skill, edits, "skill")
|
|
179
|
+
|
|
180
|
+
if evolve_memory:
|
|
181
|
+
# re-evaluate failures under the (possibly improved) skill
|
|
182
|
+
train_pairs2 = replay_batch(backend, train_tasks, cand_skill, cand_memory)
|
|
183
|
+
failures2 = [(t, r) for (t, r) in train_pairs2 if r.hard < 1.0]
|
|
184
|
+
successes2 = [(t, r) for (t, r) in train_pairs2 if r.hard >= 1.0]
|
|
185
|
+
edits_m = backend.reflect(
|
|
186
|
+
failures2, successes2, cand_skill, cand_memory,
|
|
187
|
+
edit_budget=edit_budget, evolve_skill=False, evolve_memory=True,
|
|
188
|
+
)
|
|
189
|
+
cand_memory = _gate_apply(cand_memory, edits_m, "memory")
|
|
190
|
+
|
|
191
|
+
# ── final decision ────────────────────────────────────────────────────
|
|
192
|
+
if gate_off:
|
|
193
|
+
# greedy mode: no val scoring at all. Keep whatever edits we applied; the
|
|
194
|
+
# caller measures real quality on the test set. We report holdout_candidate
|
|
195
|
+
# as 0.0 (val intentionally not computed in this variant).
|
|
196
|
+
final_hard, final_soft = 0.0, 0.0
|
|
197
|
+
final_score = 0.0
|
|
198
|
+
accepted = bool(all_applied)
|
|
199
|
+
action = "greedy_applied" if all_applied else "greedy_noop"
|
|
200
|
+
base_gate_score = 0.0
|
|
201
|
+
else:
|
|
202
|
+
# scored on the VAL slice (the gate reference)
|
|
203
|
+
final_pairs = replay_batch(backend, val_tasks, cand_skill, cand_memory)
|
|
204
|
+
final_hard, final_soft = aggregate_scores(final_pairs)
|
|
205
|
+
final_score = select_gate_score(final_hard, final_soft, gate_metric, gate_mixed_weight)
|
|
206
|
+
base_gate_score = select_gate_score(base_hard, base_soft, gate_metric, gate_mixed_weight)
|
|
207
|
+
if _HAVE_REPO_GATE:
|
|
208
|
+
gate = evaluate_gate(
|
|
209
|
+
candidate_skill=cand_skill,
|
|
210
|
+
cand_hard=final_hard,
|
|
211
|
+
current_skill=skill,
|
|
212
|
+
current_score=base_gate_score,
|
|
213
|
+
best_skill=skill,
|
|
214
|
+
best_score=base_gate_score,
|
|
215
|
+
best_step=night - 1,
|
|
216
|
+
global_step=night,
|
|
217
|
+
cand_soft=final_soft,
|
|
218
|
+
metric=gate_metric,
|
|
219
|
+
mixed_weight=gate_mixed_weight,
|
|
220
|
+
)
|
|
221
|
+
action = gate.action
|
|
222
|
+
accepted = bool(all_applied) and final_score > base_gate_score
|
|
223
|
+
else:
|
|
224
|
+
action = "accept" if final_score > base_gate_score else "reject"
|
|
225
|
+
accepted = bool(all_applied) and final_score > base_gate_score
|
|
226
|
+
|
|
227
|
+
return ConsolidationResult(
|
|
228
|
+
accepted=accepted,
|
|
229
|
+
gate_action=action,
|
|
230
|
+
baseline_score=base_gate_score,
|
|
231
|
+
candidate_score=final_score,
|
|
232
|
+
new_skill=cand_skill if accepted else skill,
|
|
233
|
+
new_memory=cand_memory if accepted else memory,
|
|
234
|
+
applied_edits=all_applied,
|
|
235
|
+
rejected_edits=all_rejected,
|
|
236
|
+
holdout_baseline=base_hard,
|
|
237
|
+
holdout_candidate=final_hard,
|
|
238
|
+
)
|