feat(sleep): SkillOpt-Sleep plugin update (preview) — engine robustness + scheduling
Updates the SkillOpt-Sleep plugin on top of the current main. User-facing and engine improvements since the initial drop: * Command renamed /sleep -> /skillopt-sleep across Claude Code + Codex shells; refreshed plugin READMEs and install scripts. * Built-in scheduling (skillopt_sleep/scheduler.py + __main__): schedule / unschedule the nightly cycle without external cron wiring. * Backend robustness: bounded retry with backoff (no more silent empty-string on transient 429/timeout), content-filter-safe rollout prompt, an output-contract guardrail that rejects edits violating the task's required format, and a per-sample cache key so repeated dream rollouts are independent samples (fixes degenerate single-sample reflection). * consolidate / rollout / replay: parallel multi-rollout dreaming, gate-mode controls, TaskRecord.system framing field. Scope: this commit ships only the plugin engine + shells. Research/benchmark harnesses and their data are intentionally not included; the public package has no dependency on them (the one research-evaluator import is now guarded). Marked as an early preview in the README; we'll keep iterating. 99/99 unit tests pass. Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
This commit is contained in:
@@ -89,8 +89,15 @@ def consolidate(
|
||||
gate_off = str(gate_mode).strip().lower() in {"off", "none", "false", "greedy"}
|
||||
|
||||
# ── baseline on the VAL slice (the gate reference) ────────────────────
|
||||
base_pairs = replay_batch(backend, val_tasks, skill, memory)
|
||||
base_hard, base_soft = aggregate_scores(base_pairs)
|
||||
# When the gate is OFF the user has opted out of holding out a validation set
|
||||
# (the daily-use design): we accept edits greedily and judge quality only on
|
||||
# the real test set, scored by the caller. So we SKIP all val scoring — it is
|
||||
# both wasted cost and contrary to the "no val set required" design.
|
||||
if gate_off:
|
||||
base_hard, base_soft = 0.0, 0.0
|
||||
else:
|
||||
base_pairs = replay_batch(backend, val_tasks, skill, memory)
|
||||
base_hard, base_soft = aggregate_scores(base_pairs)
|
||||
base_score = select_gate_score(base_hard, base_soft, gate_metric, gate_mixed_weight)
|
||||
|
||||
# ── reflect over TRAIN-split failures/successes ───────────────────────
|
||||
@@ -109,14 +116,17 @@ def consolidate(
|
||||
new_doc, applied = apply_edits(doc, edits)
|
||||
if not applied:
|
||||
return doc
|
||||
# score the candidate on the VAL slice
|
||||
# gate OFF: accept greedily with NO val scoring (the daily-use path)
|
||||
if gate_off:
|
||||
all_applied.extend(applied)
|
||||
return new_doc
|
||||
# gate ON: score the candidate on the VAL slice, keep only if it improves
|
||||
trial_skill = new_doc if which == "skill" else cand_skill
|
||||
trial_memory = new_doc if which == "memory" else cand_memory
|
||||
pairs = replay_batch(backend, val_tasks, trial_skill, trial_memory)
|
||||
h, s = aggregate_scores(pairs)
|
||||
cand_score = select_gate_score(h, s, gate_metric, gate_mixed_weight)
|
||||
# gate OFF: accept greedily (no regression check); gate ON: strict improve
|
||||
if gate_off or cand_score > base_score:
|
||||
if cand_score > base_score:
|
||||
base_score = max(base_score, cand_score)
|
||||
all_applied.extend(applied)
|
||||
return new_doc
|
||||
@@ -128,8 +138,28 @@ def consolidate(
|
||||
# multi-rollout contrastive reflection: run each train task K times
|
||||
# and distill a rule from the good-vs-bad contrast (the imagination signal).
|
||||
from skillopt_sleep.rollout import multi_rollout, contrastive_reflect
|
||||
sets = [multi_rollout(backend, t, cand_skill, cand_memory, k=rollouts_k)
|
||||
for t in train_tasks]
|
||||
# Parallelize across tasks (each multi_rollout also parallelizes its K
|
||||
# attempts). This dream phase is the dominant cost; serial execution
|
||||
# times out on real backends. Cap total in-flight at the worker env.
|
||||
import os
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
try:
|
||||
_w = int(os.environ.get("SKILLOPT_SLEEP_WORKERS", "1"))
|
||||
except ValueError:
|
||||
_w = 1
|
||||
if _w > 1 and len(train_tasks) > 1:
|
||||
# split the worker budget between task-parallelism and per-task K
|
||||
task_workers = max(1, min(len(train_tasks), _w))
|
||||
per_task = max(1, _w // task_workers)
|
||||
with ThreadPoolExecutor(max_workers=task_workers) as ex:
|
||||
sets = list(ex.map(
|
||||
lambda t: multi_rollout(backend, t, cand_skill, cand_memory,
|
||||
k=rollouts_k, workers=per_task),
|
||||
train_tasks))
|
||||
else:
|
||||
sets = [multi_rollout(backend, t, cand_skill, cand_memory,
|
||||
k=rollouts_k, workers=1)
|
||||
for t in train_tasks]
|
||||
edits = contrastive_reflect(
|
||||
backend, sets, cand_skill, cand_memory,
|
||||
edit_budget=edit_budget, target="skill",
|
||||
@@ -158,40 +188,41 @@ def consolidate(
|
||||
)
|
||||
cand_memory = _gate_apply(cand_memory, edits_m, "memory")
|
||||
|
||||
# ── final decision, scored on the VAL slice ───────────────────────────
|
||||
final_pairs = replay_batch(backend, val_tasks, cand_skill, cand_memory)
|
||||
final_hard, final_soft = aggregate_scores(final_pairs)
|
||||
final_score = select_gate_score(final_hard, final_soft, gate_metric, gate_mixed_weight)
|
||||
base_gate_score = select_gate_score(base_hard, base_soft, gate_metric, gate_mixed_weight)
|
||||
|
||||
# ── final decision ────────────────────────────────────────────────────
|
||||
if gate_off:
|
||||
# greedy mode: keep whatever edits we applied; report quality movement
|
||||
# greedy mode: no val scoring at all. Keep whatever edits we applied; the
|
||||
# caller measures real quality on the test set. We report holdout_candidate
|
||||
# as 0.0 (val intentionally not computed in this variant).
|
||||
final_hard, final_soft = 0.0, 0.0
|
||||
final_score = 0.0
|
||||
accepted = bool(all_applied)
|
||||
if final_score > base_gate_score:
|
||||
action = "greedy_improved"
|
||||
elif final_score < base_gate_score:
|
||||
action = "greedy_regressed"
|
||||
else:
|
||||
action = "greedy_flat" if all_applied else "greedy_noop"
|
||||
elif _HAVE_REPO_GATE:
|
||||
gate = evaluate_gate(
|
||||
candidate_skill=cand_skill,
|
||||
cand_hard=final_hard,
|
||||
current_skill=skill,
|
||||
current_score=base_gate_score,
|
||||
best_skill=skill,
|
||||
best_score=base_gate_score,
|
||||
best_step=night - 1,
|
||||
global_step=night,
|
||||
cand_soft=final_soft,
|
||||
metric=gate_metric,
|
||||
mixed_weight=gate_mixed_weight,
|
||||
)
|
||||
action = gate.action
|
||||
accepted = bool(all_applied) and final_score > base_gate_score
|
||||
action = "greedy_applied" if all_applied else "greedy_noop"
|
||||
base_gate_score = 0.0
|
||||
else:
|
||||
action = "accept" if final_score > base_gate_score else "reject"
|
||||
accepted = bool(all_applied) and final_score > base_gate_score
|
||||
# scored on the VAL slice (the gate reference)
|
||||
final_pairs = replay_batch(backend, val_tasks, cand_skill, cand_memory)
|
||||
final_hard, final_soft = aggregate_scores(final_pairs)
|
||||
final_score = select_gate_score(final_hard, final_soft, gate_metric, gate_mixed_weight)
|
||||
base_gate_score = select_gate_score(base_hard, base_soft, gate_metric, gate_mixed_weight)
|
||||
if _HAVE_REPO_GATE:
|
||||
gate = evaluate_gate(
|
||||
candidate_skill=cand_skill,
|
||||
cand_hard=final_hard,
|
||||
current_skill=skill,
|
||||
current_score=base_gate_score,
|
||||
best_skill=skill,
|
||||
best_score=base_gate_score,
|
||||
best_step=night - 1,
|
||||
global_step=night,
|
||||
cand_soft=final_soft,
|
||||
metric=gate_metric,
|
||||
mixed_weight=gate_mixed_weight,
|
||||
)
|
||||
action = gate.action
|
||||
accepted = bool(all_applied) and final_score > base_gate_score
|
||||
else:
|
||||
action = "accept" if final_score > base_gate_score else "reject"
|
||||
accepted = bool(all_applied) and final_score > base_gate_score
|
||||
|
||||
return ConsolidationResult(
|
||||
accepted=accepted,
|
||||
|
||||
Reference in New Issue
Block a user