self-evolve-framework 1.3.0 → 1.4.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/skills/ponytail/SKILL.md +16 -0
- package/template/skills/ponytail/scripts/hooks/claude-codex-hooks.json +44 -0
- package/template/skills/ponytail/scripts/hooks/copilot-hooks.json +21 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-activate.js +91 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-config.js +122 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-instructions.js +94 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-mode-tracker.js +55 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-runtime.js +68 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-statusline.ps1 +21 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-statusline.sh +12 -0
- package/template/skills/ponytail/scripts/hooks/ponytail-subagent.js +22 -0
- package/template/skills/ponytail/scripts/mcp/README.md +46 -0
- package/template/skills/ponytail/scripts/mcp/index.js +48 -0
- package/template/skills/ponytail/scripts/mcp/instructions.js +26 -0
- package/template/skills/ponytail/scripts/mcp/package.json +13 -0
- package/template/skills/ponytail/scripts/mcp/test/instructions.test.js +22 -0
- 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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — turn a sweep JSONL into a presented Markdown scorecard.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
python -m skillopt_sleep.experiments.report --in docs/sleep/sweep.jsonl \
|
|
5
|
+
--out docs/sleep/benchmark_report.md
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
from typing import Any, Dict, List
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _load(path: str) -> List[Dict[str, Any]]:
|
|
17
|
+
rows = []
|
|
18
|
+
if os.path.exists(path):
|
|
19
|
+
with open(path) as f:
|
|
20
|
+
for line in f:
|
|
21
|
+
line = line.strip()
|
|
22
|
+
if line:
|
|
23
|
+
try:
|
|
24
|
+
rows.append(json.loads(line))
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
return rows
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fmt_model(backend: str, model: str) -> str:
|
|
31
|
+
m = model or "default"
|
|
32
|
+
return f"{backend}:{m}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def render(rows: List[Dict[str, Any]]) -> str:
|
|
36
|
+
direct = [r for r in rows if r.get("cfg", {}).get("kind") in ("direct", "dual") and "error" not in r]
|
|
37
|
+
transfer = [r for r in rows if r.get("cfg", {}).get("kind") == "transfer" and "error" not in r]
|
|
38
|
+
errors = [r for r in rows if "error" in r]
|
|
39
|
+
|
|
40
|
+
out: List[str] = []
|
|
41
|
+
out.append("# SkillOpt-Sleep — benchmark report")
|
|
42
|
+
out.append("")
|
|
43
|
+
out.append("Auto-generated from `sweep.jsonl`. Benchmark: "
|
|
44
|
+
"[gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1` "
|
|
45
|
+
"(deficient skills, train/held-out split, local rule judge — no judge-API).")
|
|
46
|
+
out.append("Held-out scores are computed by the harness, not the optimizer.")
|
|
47
|
+
out.append("")
|
|
48
|
+
|
|
49
|
+
# ── direct improvement table ──────────────────────────────────────────
|
|
50
|
+
out.append("## Direct improvement (optimize, then deploy)")
|
|
51
|
+
out.append("")
|
|
52
|
+
out.append("| Optimizer → Target | Seed | Held-out before | Held-out after | Nights | Tokens |")
|
|
53
|
+
out.append("|---|---|---|---|---|---|")
|
|
54
|
+
for r in direct:
|
|
55
|
+
c = r["cfg"]
|
|
56
|
+
if c.get("kind") == "dual":
|
|
57
|
+
label = (f"{_fmt_model(c['optimizer_backend'], c.get('optimizer_model',''))}"
|
|
58
|
+
f" → {_fmt_model(c['target_backend'], c.get('target_model',''))}")
|
|
59
|
+
else:
|
|
60
|
+
m = _fmt_model(c["backend"], c.get("model", ""))
|
|
61
|
+
label = f"{m} → {m}"
|
|
62
|
+
out.append(f"| {label} | {c['seed']} | "
|
|
63
|
+
f"{r['baseline']:.2f} | **{r['after']:.2f}** | {c['nights']} | "
|
|
64
|
+
f"{r.get('tokens','?')} |")
|
|
65
|
+
if direct:
|
|
66
|
+
n_imp = sum(1 for r in direct if r.get("improved"))
|
|
67
|
+
out.append("")
|
|
68
|
+
out.append(f"**{n_imp}/{len(direct)} configurations improved on held-out.**")
|
|
69
|
+
out.append("")
|
|
70
|
+
|
|
71
|
+
# ── transfer table ────────────────────────────────────────────────────
|
|
72
|
+
if transfer:
|
|
73
|
+
out.append("## Cross-model transfer (optimize on SOURCE, deploy frozen on TARGET)")
|
|
74
|
+
out.append("")
|
|
75
|
+
out.append("The price-difference story: spend cheap tokens optimizing overnight, "
|
|
76
|
+
"then deploy the frozen skill on any model with no further optimization.")
|
|
77
|
+
out.append("")
|
|
78
|
+
out.append("| Source (optimizer) | Target (deploy) | Seed | Target baseline | Transferred | Gain |")
|
|
79
|
+
out.append("|---|---|---|---|---|---|")
|
|
80
|
+
for r in transfer:
|
|
81
|
+
c = r["cfg"]
|
|
82
|
+
s = _fmt_model(c["source_backend"], c.get("source_model", ""))
|
|
83
|
+
t = _fmt_model(c["target_backend"], c.get("target_model", ""))
|
|
84
|
+
out.append(f"| {s} | {t} | {c['seed']} | {r['baseline_target']:.2f} | "
|
|
85
|
+
f"**{r['transferred']:.2f}** | {r['transfer_gain']:+.2f} |")
|
|
86
|
+
n_pos = sum(1 for r in transfer if r.get("transfer_gain", 0) > 0)
|
|
87
|
+
out.append("")
|
|
88
|
+
out.append(f"**{n_pos}/{len(transfer)} transfers were positive** "
|
|
89
|
+
"(frozen skill helped a different model than it was optimized on).")
|
|
90
|
+
out.append("")
|
|
91
|
+
|
|
92
|
+
# ── errors (honest reporting) ─────────────────────────────────────────
|
|
93
|
+
if errors:
|
|
94
|
+
out.append("## Configs that errored (reported, not hidden)")
|
|
95
|
+
out.append("")
|
|
96
|
+
for r in errors:
|
|
97
|
+
out.append(f"- `{json.dumps(r['cfg'])}` → {r['error']}")
|
|
98
|
+
out.append("")
|
|
99
|
+
|
|
100
|
+
out.append("## How to reproduce")
|
|
101
|
+
out.append("")
|
|
102
|
+
out.append("```bash")
|
|
103
|
+
out.append("git clone https://github.com/garrytan/gbrain-evals /tmp/gbrain-evals")
|
|
104
|
+
out.append("python -m skillopt_sleep.experiments.sweep --plan full \\")
|
|
105
|
+
out.append(" --data-root /tmp/gbrain-evals/eval/data/skillopt-v1 --out docs/sleep/sweep.jsonl")
|
|
106
|
+
out.append("python -m skillopt_sleep.experiments.report \\")
|
|
107
|
+
out.append(" --in docs/sleep/sweep.jsonl --out docs/sleep/benchmark_report.md")
|
|
108
|
+
out.append("```")
|
|
109
|
+
out.append("")
|
|
110
|
+
return "\n".join(out)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv=None) -> int:
|
|
114
|
+
ap = argparse.ArgumentParser(description="Render SkillOpt-Sleep sweep report")
|
|
115
|
+
ap.add_argument("--in", dest="inp", default="docs/sleep/sweep.jsonl")
|
|
116
|
+
ap.add_argument("--out", default="docs/sleep/benchmark_report.md")
|
|
117
|
+
args = ap.parse_args(argv)
|
|
118
|
+
|
|
119
|
+
rows = _load(args.inp)
|
|
120
|
+
if not rows:
|
|
121
|
+
print(f"no rows in {args.inp}", file=sys.stderr)
|
|
122
|
+
return 1
|
|
123
|
+
md = render(rows)
|
|
124
|
+
os.makedirs(os.path.dirname(args.out) or ".", exist_ok=True)
|
|
125
|
+
with open(args.out, "w") as f:
|
|
126
|
+
f.write(md)
|
|
127
|
+
print(f"wrote {args.out} ({len(rows)} rows)")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if __name__ == "__main__":
|
|
132
|
+
sys.exit(main())
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — validation experiment.
|
|
2
|
+
|
|
3
|
+
Answers the question the user posed: *does nightly offline self-evolution
|
|
4
|
+
actually improve the agent?* Runs deterministically with the MockBackend
|
|
5
|
+
(no API key, reproducible) and is the acceptance test for the whole idea.
|
|
6
|
+
|
|
7
|
+
What it proves:
|
|
8
|
+
1. MONOTONIC LIFT — over N sleep nights, the held-out score rises from a
|
|
9
|
+
baseline (empty skill/memory) toward 1.0 as the gate accepts the
|
|
10
|
+
general rules the persona's tasks require.
|
|
11
|
+
2. GATE SAFETY — an injected harmful edit is REJECTED (held-out score does
|
|
12
|
+
not improve), so a bad nightly proposal can never be adopted.
|
|
13
|
+
3. PLUMBING — harvest->mine->replay->consolidate->stage->adopt all run and
|
|
14
|
+
the adopted artifact, re-scored, retains the lift.
|
|
15
|
+
|
|
16
|
+
Run:
|
|
17
|
+
python -m skillopt_sleep.experiments.run_experiment
|
|
18
|
+
python -m skillopt_sleep.experiments.run_experiment --persona programmer --nights 3
|
|
19
|
+
python -m skillopt_sleep.experiments.run_experiment --backend anthropic # real lift
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import tempfile
|
|
28
|
+
from typing import List
|
|
29
|
+
|
|
30
|
+
from skillopt_sleep.backend import get_backend
|
|
31
|
+
from skillopt_sleep.consolidate import consolidate
|
|
32
|
+
from skillopt_sleep.experiments.personas import (
|
|
33
|
+
PERSONAS,
|
|
34
|
+
harmful_edit_task,
|
|
35
|
+
researcher_persona,
|
|
36
|
+
)
|
|
37
|
+
from skillopt_sleep.memory import ensure_skill_scaffold
|
|
38
|
+
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
|
39
|
+
from skillopt_sleep.types import TaskRecord
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _score_holdout(backend, tasks: List[TaskRecord], skill: str, memory: str,
|
|
43
|
+
metric: str = "mixed", w: float = 0.5) -> float:
|
|
44
|
+
from skillopt_sleep.consolidate import select_gate_score
|
|
45
|
+
# the persona experiment uses a 2-way split (train/val, no test); score on val
|
|
46
|
+
holdout = [t for t in tasks if t.split in ("val", "holdout")] or tasks
|
|
47
|
+
pairs = replay_batch(backend, holdout, skill, memory)
|
|
48
|
+
h, s = aggregate_scores(pairs)
|
|
49
|
+
return select_gate_score(h, s, metric, w)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def run(persona: str = "researcher", nights: int = 4, backend_name: str = "mock",
|
|
53
|
+
edit_budget: int = 4, seed: int = 42, model: str = "", codex_path: str = "",
|
|
54
|
+
limit_tasks: int = 0) -> dict:
|
|
55
|
+
from skillopt_sleep.mine import assign_splits
|
|
56
|
+
|
|
57
|
+
make = PERSONAS.get(persona, researcher_persona)
|
|
58
|
+
items = make()
|
|
59
|
+
if limit_tasks and limit_tasks < len(items):
|
|
60
|
+
items = items[:limit_tasks]
|
|
61
|
+
tasks = assign_splits(items, holdout_fraction=0.34, seed=seed)
|
|
62
|
+
backend = get_backend(backend_name, model=model, codex_path=codex_path)
|
|
63
|
+
is_mock = (backend.name == "mock")
|
|
64
|
+
|
|
65
|
+
# start from an empty managed skill + empty memory
|
|
66
|
+
skill = ensure_skill_scaffold("", name="skillopt-sleep-learned",
|
|
67
|
+
description="Learned preferences.")
|
|
68
|
+
memory = ""
|
|
69
|
+
|
|
70
|
+
baseline = _score_holdout(backend, tasks, skill, memory)
|
|
71
|
+
trace = [{"night": 0, "holdout_score": round(baseline, 4), "action": "baseline",
|
|
72
|
+
"n_edits": 0}]
|
|
73
|
+
|
|
74
|
+
for night in range(1, nights + 1):
|
|
75
|
+
res = consolidate(
|
|
76
|
+
backend, tasks, skill, memory,
|
|
77
|
+
edit_budget=edit_budget, gate_metric="mixed", gate_mixed_weight=0.5,
|
|
78
|
+
evolve_skill=True, evolve_memory=True, night=night,
|
|
79
|
+
)
|
|
80
|
+
if res.accepted:
|
|
81
|
+
skill, memory = res.new_skill, res.new_memory
|
|
82
|
+
trace.append({
|
|
83
|
+
"night": night,
|
|
84
|
+
"holdout_score": round(res.candidate_score, 4),
|
|
85
|
+
"action": res.gate_action,
|
|
86
|
+
"accepted": res.accepted,
|
|
87
|
+
"n_edits": len(res.applied_edits),
|
|
88
|
+
"edits": [e.content for e in res.applied_edits],
|
|
89
|
+
"n_rejected": len(res.rejected_edits),
|
|
90
|
+
})
|
|
91
|
+
# converged: stop early if perfect
|
|
92
|
+
if res.candidate_score >= 0.999:
|
|
93
|
+
break
|
|
94
|
+
|
|
95
|
+
after = _score_holdout(backend, tasks, skill, memory)
|
|
96
|
+
|
|
97
|
+
# ── gate-safety probe (mock only; it relies on the mock's known bad rule) ──
|
|
98
|
+
harmful_rejected = None
|
|
99
|
+
if is_mock:
|
|
100
|
+
harmful_tasks = assign_splits([harmful_edit_task()] + make()[:3],
|
|
101
|
+
holdout_fraction=0.5, seed=seed)
|
|
102
|
+
_ = _score_holdout(backend, harmful_tasks, skill, memory)
|
|
103
|
+
res_h = consolidate(backend, harmful_tasks, skill, memory,
|
|
104
|
+
edit_budget=edit_budget, gate_metric="mixed",
|
|
105
|
+
evolve_skill=True, evolve_memory=False, night=nights + 1)
|
|
106
|
+
harmful_rule_text = get_backend("mock").RULE_TEXT["__harmful__"] # type: ignore[attr-defined]
|
|
107
|
+
harmful_rejected = (harmful_rule_text not in res_h.new_skill)
|
|
108
|
+
|
|
109
|
+
result = {
|
|
110
|
+
"persona": persona,
|
|
111
|
+
"backend": backend.name,
|
|
112
|
+
"model": model or "(default)",
|
|
113
|
+
"n_tasks": len(tasks),
|
|
114
|
+
"nights_run": len(trace) - 1,
|
|
115
|
+
"baseline_holdout": round(baseline, 4),
|
|
116
|
+
"after_holdout": round(after, 4),
|
|
117
|
+
"lift": round(after - baseline, 4),
|
|
118
|
+
"improved": after > baseline,
|
|
119
|
+
"gate_blocks_harmful": harmful_rejected, # None for real backends
|
|
120
|
+
"tokens_used": backend.tokens_used(),
|
|
121
|
+
"final_skill_excerpt": skill[-500:],
|
|
122
|
+
"trace": trace,
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _assert(cond: bool, msg: str) -> None:
|
|
128
|
+
if not cond:
|
|
129
|
+
print(f"FAIL: {msg}")
|
|
130
|
+
raise SystemExit(1)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main(argv=None) -> int:
|
|
134
|
+
ap = argparse.ArgumentParser(description="SkillOpt-Sleep validation experiment")
|
|
135
|
+
ap.add_argument("--persona", default="researcher", choices=list(PERSONAS.keys()))
|
|
136
|
+
ap.add_argument("--nights", type=int, default=4)
|
|
137
|
+
ap.add_argument("--backend", default="mock", choices=["mock", "claude", "codex", "copilot"])
|
|
138
|
+
ap.add_argument("--model", default="", help="backend model override")
|
|
139
|
+
ap.add_argument("--codex-path", default="", help="path to the real @openai/codex binary")
|
|
140
|
+
ap.add_argument("--edit-budget", type=int, default=4)
|
|
141
|
+
ap.add_argument("--limit-tasks", type=int, default=0, help="cap #tasks (control API cost)")
|
|
142
|
+
ap.add_argument("--json", action="store_true")
|
|
143
|
+
ap.add_argument("--assert-improves", action="store_true",
|
|
144
|
+
help="exit nonzero unless lift>0 (and, for mock, gate blocks harmful edit)")
|
|
145
|
+
args = ap.parse_args(argv)
|
|
146
|
+
|
|
147
|
+
res = run(args.persona, nights=args.nights, backend_name=args.backend,
|
|
148
|
+
edit_budget=args.edit_budget, model=args.model,
|
|
149
|
+
codex_path=args.codex_path, limit_tasks=args.limit_tasks)
|
|
150
|
+
|
|
151
|
+
if args.json:
|
|
152
|
+
print(json.dumps(res, ensure_ascii=False, indent=2))
|
|
153
|
+
else:
|
|
154
|
+
print(f"=== SkillOpt-Sleep experiment: persona={res['persona']} "
|
|
155
|
+
f"backend={res['backend']} model={res['model']} ===")
|
|
156
|
+
print(f"tasks: {res['n_tasks']} tokens(approx): {res['tokens_used']}")
|
|
157
|
+
print(f"baseline held-out : {res['baseline_holdout']}")
|
|
158
|
+
print(f"after held-out : {res['after_holdout']} (lift {res['lift']:+.4f})")
|
|
159
|
+
if res["gate_blocks_harmful"] is not None:
|
|
160
|
+
print(f"gate blocks harmful edit: {res['gate_blocks_harmful']}")
|
|
161
|
+
print("trace:")
|
|
162
|
+
for row in res["trace"]:
|
|
163
|
+
edits = "; ".join(row.get("edits", []))[:80]
|
|
164
|
+
print(f" night {row['night']}: holdout={row['holdout_score']} "
|
|
165
|
+
f"{row['action']} (+{row['n_edits']} edits) {edits}")
|
|
166
|
+
|
|
167
|
+
if args.assert_improves:
|
|
168
|
+
_assert(res["improved"], "held-out score did not improve")
|
|
169
|
+
if res["gate_blocks_harmful"] is not None:
|
|
170
|
+
_assert(res["gate_blocks_harmful"], "gate failed to block harmful edit")
|
|
171
|
+
print("\nPASS: nightly consolidation improves held-out score AND gate blocks regressions.")
|
|
172
|
+
else:
|
|
173
|
+
print("\nPASS: nightly consolidation improves held-out score (real backend).")
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
sys.exit(main())
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — run the gbrain-evals skillopt-v1 benchmark with our engine.
|
|
2
|
+
|
|
3
|
+
Reproduces gbrain's "Result 1 — skills measurably improve" scorecard
|
|
4
|
+
(docs/benchmarks/2026-06-03-skillopt.md) using SkillOpt-Sleep's
|
|
5
|
+
consolidate() loop and either the claude or codex backend.
|
|
6
|
+
|
|
7
|
+
For each deficient seed skill:
|
|
8
|
+
1. score the held-out tasks with the ORIGINAL skill -> before
|
|
9
|
+
2. run N consolidation nights on the training tasks (gated) -> evolve skill
|
|
10
|
+
3. score the held-out tasks with the EVOLVED skill -> after
|
|
11
|
+
|
|
12
|
+
Held-out scoring is done locally by the rule judge (no judge API). Only the
|
|
13
|
+
agent's `attempt` (and the optimizer's `reflect`) spend tokens.
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
python -m skillopt_sleep.experiments.run_gbrain --backend mock
|
|
17
|
+
python -m skillopt_sleep.experiments.run_gbrain --backend claude --seeds brief-writer --nights 2
|
|
18
|
+
python -m skillopt_sleep.experiments.run_gbrain --backend codex --data-root /tmp/gbrain-evals/eval/data/skillopt-v1
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import json
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Dict, List, Optional
|
|
26
|
+
|
|
27
|
+
from skillopt_sleep.backend import build_backend, get_backend
|
|
28
|
+
from skillopt_sleep.consolidate import consolidate, select_gate_score
|
|
29
|
+
from skillopt_sleep.experiments.gbrain_bench import (
|
|
30
|
+
available_seeds,
|
|
31
|
+
find_data_root,
|
|
32
|
+
load_seed,
|
|
33
|
+
)
|
|
34
|
+
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _score(backend, tasks, skill, memory, split="test", metric="mixed", w=0.5):
|
|
38
|
+
sub = [t for t in tasks if t.split == split]
|
|
39
|
+
if not sub: # fall back to val, then everything, so we never score on nothing
|
|
40
|
+
sub = [t for t in tasks if t.split == "val"] or tasks
|
|
41
|
+
pairs = replay_batch(backend, sub, skill, memory)
|
|
42
|
+
h, s = aggregate_scores(pairs)
|
|
43
|
+
return h, s, select_gate_score(h, s, metric, w)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def run_seed(backend, seed: str, skill: str, tasks: List, *,
|
|
47
|
+
nights: int = 3, edit_budget: int = 4, gate_mode: str = "on",
|
|
48
|
+
slow_update: bool = True, rollouts_k: int = 1,
|
|
49
|
+
limit_replay: int = 0, limit_holdout: int = 0) -> dict:
|
|
50
|
+
memory = ""
|
|
51
|
+
# optionally cap each split to control API cost / latency.
|
|
52
|
+
# limit_replay caps train; limit_holdout caps BOTH val and test.
|
|
53
|
+
if limit_replay or limit_holdout:
|
|
54
|
+
train = [t for t in tasks if t.split == "train"]
|
|
55
|
+
val = [t for t in tasks if t.split == "val"]
|
|
56
|
+
test = [t for t in tasks if t.split == "test"]
|
|
57
|
+
if limit_replay:
|
|
58
|
+
train = train[:limit_replay]
|
|
59
|
+
if limit_holdout:
|
|
60
|
+
val = val[:limit_holdout]
|
|
61
|
+
test = test[:limit_holdout]
|
|
62
|
+
tasks = train + val + test
|
|
63
|
+
# final measure is TEST (the gbrain held-out set); val gates internally
|
|
64
|
+
bh, bs, bscore = _score(backend, tasks, skill, memory, split="test")
|
|
65
|
+
trace = [{"night": 0, "test_hard": round(bh, 3), "action": "baseline"}]
|
|
66
|
+
cur = skill
|
|
67
|
+
first_night_skill = skill
|
|
68
|
+
for night in range(1, nights + 1):
|
|
69
|
+
res = consolidate(
|
|
70
|
+
backend, tasks, cur, memory,
|
|
71
|
+
edit_budget=edit_budget, gate_metric="mixed", gate_mixed_weight=0.5,
|
|
72
|
+
gate_mode=gate_mode, rollouts_k=rollouts_k,
|
|
73
|
+
evolve_skill=True, evolve_memory=False, night=night,
|
|
74
|
+
)
|
|
75
|
+
if res.accepted:
|
|
76
|
+
cur = res.new_skill
|
|
77
|
+
if night == 1:
|
|
78
|
+
first_night_skill = cur
|
|
79
|
+
# report the TEST score each night (independent of the val gate)
|
|
80
|
+
th, _ts, _ = _score(backend, tasks, cur, memory, split="test")
|
|
81
|
+
trace.append({
|
|
82
|
+
"night": night,
|
|
83
|
+
"val_hard": round(res.holdout_candidate, 3),
|
|
84
|
+
"test_hard": round(th, 3),
|
|
85
|
+
"action": res.gate_action,
|
|
86
|
+
"accepted": res.accepted,
|
|
87
|
+
"edits": [e.content for e in res.applied_edits],
|
|
88
|
+
})
|
|
89
|
+
if th >= 0.999:
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
# ── SLOW UPDATE: consolidate cross-night experience into the protected
|
|
93
|
+
# long-term field. Runs regardless of gate mode (it is what preserves
|
|
94
|
+
# long-term memory even when the gate is OFF).
|
|
95
|
+
slow_text = None
|
|
96
|
+
if nights >= 2 and slow_update:
|
|
97
|
+
try:
|
|
98
|
+
from skillopt_sleep.slow_update import run_slow_update, replace_slow_field
|
|
99
|
+
val_tasks = [t for t in tasks if t.split == "val"] or tasks
|
|
100
|
+
prev_pairs = replay_batch(backend, val_tasks, first_night_skill, memory)
|
|
101
|
+
curr_pairs = replay_batch(backend, val_tasks, cur, memory)
|
|
102
|
+
slow_text = run_slow_update(
|
|
103
|
+
backend, prev_skill=first_night_skill, curr_skill=cur,
|
|
104
|
+
prev_pairs=[(t, r) for t, r in prev_pairs],
|
|
105
|
+
curr_pairs=[(t, r) for t, r in curr_pairs],
|
|
106
|
+
)
|
|
107
|
+
if slow_text:
|
|
108
|
+
cur = replace_slow_field(cur, slow_text)
|
|
109
|
+
except Exception:
|
|
110
|
+
slow_text = None
|
|
111
|
+
|
|
112
|
+
ah, as_, ascore = _score(backend, tasks, cur, memory, split="test")
|
|
113
|
+
return {
|
|
114
|
+
"seed": seed,
|
|
115
|
+
"held_out_before": round(bh, 3),
|
|
116
|
+
"held_out_after": round(ah, 3),
|
|
117
|
+
"improved": ah > bh,
|
|
118
|
+
"nights": len(trace) - 1,
|
|
119
|
+
"trace": trace,
|
|
120
|
+
"slow_update": slow_text,
|
|
121
|
+
"final_skill_tail": cur[-400:],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main(argv=None) -> int:
|
|
126
|
+
ap = argparse.ArgumentParser(description="Run gbrain-evals skillopt-v1 with SkillOpt-Sleep")
|
|
127
|
+
ap.add_argument("--backend", default="mock", choices=["mock", "claude", "codex"])
|
|
128
|
+
ap.add_argument("--model", default="")
|
|
129
|
+
ap.add_argument("--optimizer-backend", default="", help="route reflect/judge here (dual)")
|
|
130
|
+
ap.add_argument("--optimizer-model", default="")
|
|
131
|
+
ap.add_argument("--target-backend", default="", help="route attempt here (dual)")
|
|
132
|
+
ap.add_argument("--target-model", default="")
|
|
133
|
+
ap.add_argument("--codex-path", default="")
|
|
134
|
+
ap.add_argument("--data-root", default="", help="path to eval/data/skillopt-v1")
|
|
135
|
+
ap.add_argument("--seeds", default="", help="comma list; default = all available")
|
|
136
|
+
ap.add_argument("--nights", type=int, default=3)
|
|
137
|
+
ap.add_argument("--edit-budget", type=int, default=4)
|
|
138
|
+
ap.add_argument("--gate", default="on", choices=["on", "off", "hard", "soft"],
|
|
139
|
+
help="on/hard/soft = validation-gated; off = greedy (no hard filter)")
|
|
140
|
+
ap.add_argument("--rollouts-k", type=int, default=1,
|
|
141
|
+
help=">1 = multi-rollout contrastive reflection per task")
|
|
142
|
+
ap.add_argument("--budget-tokens", type=int, default=0,
|
|
143
|
+
help="approx token budget; auto-plans nights x rollouts when set")
|
|
144
|
+
ap.add_argument("--budget-minutes", type=float, default=0.0)
|
|
145
|
+
ap.add_argument("--preferences", default="", help="free-text user preferences (prior for reflect)")
|
|
146
|
+
ap.add_argument("--limit-replay", type=int, default=0, help="cap #train tasks (cost control)")
|
|
147
|
+
ap.add_argument("--limit-holdout", type=int, default=0, help="cap #val and #test tasks (cost control)")
|
|
148
|
+
ap.add_argument("--json", action="store_true")
|
|
149
|
+
args = ap.parse_args(argv)
|
|
150
|
+
|
|
151
|
+
data_root = find_data_root(args.data_root)
|
|
152
|
+
if not data_root:
|
|
153
|
+
print("ERROR: could not find eval/data/skillopt-v1. Clone gbrain-evals and pass --data-root.",
|
|
154
|
+
file=sys.stderr)
|
|
155
|
+
return 2
|
|
156
|
+
|
|
157
|
+
seeds = [s.strip() for s in args.seeds.split(",") if s.strip()] or available_seeds(data_root)
|
|
158
|
+
backend = build_backend(
|
|
159
|
+
backend=args.backend, model=args.model,
|
|
160
|
+
optimizer_backend=args.optimizer_backend, optimizer_model=args.optimizer_model,
|
|
161
|
+
target_backend=args.target_backend, target_model=args.target_model,
|
|
162
|
+
codex_path=args.codex_path, preferences=args.preferences,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
results = []
|
|
166
|
+
for seed in seeds:
|
|
167
|
+
skill, tasks = load_seed(data_root, seed)
|
|
168
|
+
if not tasks:
|
|
169
|
+
continue
|
|
170
|
+
# budget auto-planning: derive nights x rollouts_k from a token budget
|
|
171
|
+
nights, rollouts_k = args.nights, args.rollouts_k
|
|
172
|
+
if args.budget_tokens:
|
|
173
|
+
from skillopt_sleep.budget import Budget, plan_depth
|
|
174
|
+
n_train = len([t for t in tasks if t.split == "train"]) or len(tasks)
|
|
175
|
+
nights, rollouts_k = plan_depth(
|
|
176
|
+
Budget(max_tokens=args.budget_tokens), n_tasks=n_train,
|
|
177
|
+
default_nights=args.nights, default_k=args.rollouts_k,
|
|
178
|
+
)
|
|
179
|
+
if not args.json:
|
|
180
|
+
print(f" [budget] {args.budget_tokens} tok -> nights={nights} rollouts_k={rollouts_k}")
|
|
181
|
+
r = run_seed(backend, seed, skill, tasks, nights=nights,
|
|
182
|
+
edit_budget=args.edit_budget, rollouts_k=rollouts_k,
|
|
183
|
+
gate_mode=("off" if args.gate == "off" else "on"),
|
|
184
|
+
limit_replay=args.limit_replay, limit_holdout=args.limit_holdout)
|
|
185
|
+
results.append(r)
|
|
186
|
+
if not args.json:
|
|
187
|
+
print(f" {seed:<18} held-out {r['held_out_before']:.2f} -> {r['held_out_after']:.2f}"
|
|
188
|
+
f" ({'IMPROVED' if r['improved'] else 'no change'}, {r['nights']} nights)")
|
|
189
|
+
|
|
190
|
+
n_improved = sum(1 for r in results if r["improved"])
|
|
191
|
+
summary = {
|
|
192
|
+
"benchmark": "gbrain-evals/skillopt-v1",
|
|
193
|
+
"backend": backend.name,
|
|
194
|
+
"model": args.model or "(default)",
|
|
195
|
+
"n_seeds": len(results),
|
|
196
|
+
"n_improved": n_improved,
|
|
197
|
+
"tokens_used": backend.tokens_used(),
|
|
198
|
+
"results": results,
|
|
199
|
+
}
|
|
200
|
+
if args.json:
|
|
201
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
202
|
+
else:
|
|
203
|
+
print(f"\n=== {n_improved}/{len(results)} seeds improved on held-out "
|
|
204
|
+
f"(backend={backend.name}, ~{backend.tokens_used()} tokens) ===")
|
|
205
|
+
return 0
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == "__main__":
|
|
209
|
+
sys.exit(main())
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""SkillOpt-Sleep — skill-transfer experiment (sleep scenario).
|
|
2
|
+
|
|
3
|
+
Answers: "if I optimize a skill while the agent sleeps using a CHEAP model,
|
|
4
|
+
does the learned skill still help an EXPENSIVE model at deploy time?" — and the
|
|
5
|
+
reverse. This is the SkillOpt paper's cross-model transfer result, reproduced
|
|
6
|
+
in the sleep setting, and it is the core price-difference value proposition:
|
|
7
|
+
spend cheap tokens overnight, deploy the frozen skill anywhere.
|
|
8
|
+
|
|
9
|
+
Protocol, per gbrain seed:
|
|
10
|
+
1. baseline_target = held-out score of the DEFICIENT skill, run on TARGET model
|
|
11
|
+
2. optimize the skill for N nights using the SOURCE model (attempt+reflect)
|
|
12
|
+
3. transferred = held-out score of the LEARNED skill, run on TARGET model,
|
|
13
|
+
with NO further optimization
|
|
14
|
+
4. (reference) direct = held-out score of a skill optimized AND run on TARGET
|
|
15
|
+
|
|
16
|
+
Report baseline / direct / transferred, mirroring SkillOpt Table "transfer".
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python -m skillopt_sleep.experiments.run_transfer \
|
|
20
|
+
--source-backend claude --source-model haiku \
|
|
21
|
+
--target-backend claude --target-model sonnet \
|
|
22
|
+
--seeds brief-writer --nights 2
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
from typing import List, Optional
|
|
30
|
+
|
|
31
|
+
from skillopt_sleep.backend import get_backend
|
|
32
|
+
from skillopt_sleep.consolidate import consolidate, select_gate_score
|
|
33
|
+
from skillopt_sleep.experiments.gbrain_bench import (
|
|
34
|
+
available_seeds, find_data_root, load_seed,
|
|
35
|
+
)
|
|
36
|
+
from skillopt_sleep.replay import aggregate_scores, replay_batch
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _holdout_hard(backend, tasks, skill, memory="") -> float:
|
|
40
|
+
# transfer is measured on the true held-out TEST split
|
|
41
|
+
ho = [t for t in tasks if t.split == "test"]
|
|
42
|
+
if not ho:
|
|
43
|
+
ho = [t for t in tasks if t.split in ("val", "holdout")] or tasks
|
|
44
|
+
pairs = replay_batch(backend, ho, skill, memory)
|
|
45
|
+
h, _s = aggregate_scores(pairs)
|
|
46
|
+
return h
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _optimize(backend, skill, tasks, *, nights, edit_budget) -> str:
|
|
50
|
+
cur = skill
|
|
51
|
+
for night in range(1, nights + 1):
|
|
52
|
+
res = consolidate(backend, tasks, cur, "",
|
|
53
|
+
edit_budget=edit_budget, gate_metric="mixed",
|
|
54
|
+
evolve_skill=True, evolve_memory=False, night=night)
|
|
55
|
+
if res.accepted:
|
|
56
|
+
cur = res.new_skill
|
|
57
|
+
if res.holdout_candidate >= 0.999:
|
|
58
|
+
break
|
|
59
|
+
return cur
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run_seed(seed, skill, tasks, *, source, target, nights, edit_budget,
|
|
63
|
+
limit_replay, limit_holdout, do_direct=True) -> dict:
|
|
64
|
+
if limit_replay or limit_holdout:
|
|
65
|
+
train = [t for t in tasks if t.split == "train"]
|
|
66
|
+
val = [t for t in tasks if t.split == "val"]
|
|
67
|
+
test = [t for t in tasks if t.split == "test"]
|
|
68
|
+
if limit_replay:
|
|
69
|
+
train = train[:limit_replay]
|
|
70
|
+
if limit_holdout:
|
|
71
|
+
val = val[:limit_holdout]
|
|
72
|
+
test = test[:limit_holdout]
|
|
73
|
+
tasks = train + val + test
|
|
74
|
+
|
|
75
|
+
baseline_target = _holdout_hard(target, tasks, skill)
|
|
76
|
+
|
|
77
|
+
# optimize on SOURCE, evaluate frozen skill on TARGET
|
|
78
|
+
learned_on_source = _optimize(source, skill, tasks, nights=nights, edit_budget=edit_budget)
|
|
79
|
+
transferred = _holdout_hard(target, tasks, learned_on_source)
|
|
80
|
+
|
|
81
|
+
direct = None
|
|
82
|
+
if do_direct:
|
|
83
|
+
learned_on_target = _optimize(target, skill, tasks, nights=nights, edit_budget=edit_budget)
|
|
84
|
+
direct = _holdout_hard(target, tasks, learned_on_target)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
"seed": seed,
|
|
88
|
+
"baseline_target": round(baseline_target, 3),
|
|
89
|
+
"direct_target": (round(direct, 3) if direct is not None else None),
|
|
90
|
+
"transferred": round(transferred, 3),
|
|
91
|
+
"transfer_gain": round(transferred - baseline_target, 3),
|
|
92
|
+
"learned_skill_tail": learned_on_source[-300:],
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def main(argv=None) -> int:
|
|
97
|
+
ap = argparse.ArgumentParser(description="SkillOpt-Sleep cross-model transfer")
|
|
98
|
+
ap.add_argument("--source-backend", default="claude")
|
|
99
|
+
ap.add_argument("--source-model", default="haiku")
|
|
100
|
+
ap.add_argument("--target-backend", default="claude")
|
|
101
|
+
ap.add_argument("--target-model", default="sonnet")
|
|
102
|
+
ap.add_argument("--codex-path", default="")
|
|
103
|
+
ap.add_argument("--data-root", default="")
|
|
104
|
+
ap.add_argument("--seeds", default="brief-writer")
|
|
105
|
+
ap.add_argument("--nights", type=int, default=2)
|
|
106
|
+
ap.add_argument("--edit-budget", type=int, default=4)
|
|
107
|
+
ap.add_argument("--limit-replay", type=int, default=3)
|
|
108
|
+
ap.add_argument("--limit-holdout", type=int, default=3)
|
|
109
|
+
ap.add_argument("--no-direct", action="store_true", help="skip the direct reference (saves cost)")
|
|
110
|
+
ap.add_argument("--json", action="store_true")
|
|
111
|
+
args = ap.parse_args(argv)
|
|
112
|
+
|
|
113
|
+
data_root = find_data_root(args.data_root)
|
|
114
|
+
if not data_root:
|
|
115
|
+
print("ERROR: gbrain-evals skillopt-v1 data not found; pass --data-root", file=sys.stderr)
|
|
116
|
+
return 2
|
|
117
|
+
|
|
118
|
+
source = get_backend(args.source_backend, model=args.source_model, codex_path=args.codex_path)
|
|
119
|
+
target = get_backend(args.target_backend, model=args.target_model, codex_path=args.codex_path)
|
|
120
|
+
|
|
121
|
+
seeds = [s.strip() for s in args.seeds.split(",") if s.strip()] or available_seeds(data_root)
|
|
122
|
+
results = []
|
|
123
|
+
for seed in seeds:
|
|
124
|
+
skill, tasks = load_seed(data_root, seed)
|
|
125
|
+
if not tasks:
|
|
126
|
+
continue
|
|
127
|
+
r = run_seed(seed, skill, tasks, source=source, target=target,
|
|
128
|
+
nights=args.nights, edit_budget=args.edit_budget,
|
|
129
|
+
limit_replay=args.limit_replay, limit_holdout=args.limit_holdout,
|
|
130
|
+
do_direct=not args.no_direct)
|
|
131
|
+
results.append(r)
|
|
132
|
+
if not args.json:
|
|
133
|
+
d = f" direct={r['direct_target']}" if r['direct_target'] is not None else ""
|
|
134
|
+
print(f" {seed:<16} baseline={r['baseline_target']:.2f}"
|
|
135
|
+
f" transferred={r['transferred']:.2f}{d}"
|
|
136
|
+
f" (gain {r['transfer_gain']:+.2f})")
|
|
137
|
+
|
|
138
|
+
summary = {
|
|
139
|
+
"experiment": "skillopt-sleep/transfer",
|
|
140
|
+
"source": f"{args.source_backend}:{args.source_model}",
|
|
141
|
+
"target": f"{args.target_backend}:{args.target_model}",
|
|
142
|
+
"tokens_source": source.tokens_used(),
|
|
143
|
+
"tokens_target": target.tokens_used(),
|
|
144
|
+
"results": results,
|
|
145
|
+
}
|
|
146
|
+
if args.json:
|
|
147
|
+
print(json.dumps(summary, ensure_ascii=False, indent=2))
|
|
148
|
+
else:
|
|
149
|
+
print(f"\n=== transfer {summary['source']} -> {summary['target']}: "
|
|
150
|
+
f"{sum(1 for r in results if r['transfer_gain'] > 0)}/{len(results)} positive ===")
|
|
151
|
+
return 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
sys.exit(main())
|