Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ All notable changes to SkillOpt are documented here. This project adheres to
## [Unreleased]

### Added
- **SkillOpt-Sleep opt-in `llm_dream`**: paraphrase-only dream variants from
the optimizer model, with deterministic template fallback on parse or
fidelity failure. Default template dreams stay byte-identical; generated
variants are train-only (thanks @bogdanbaciu21).
- **SkillOpt-Sleep multi-skill fan-out and reviewed subset adoption**: each
hinted skill is consolidated from its own pinned live baseline, staged as an
independent proposal with per-skill gate evidence, and promoted only through
Expand Down
1 change: 1 addition & 0 deletions docs/sleep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ correctness signal; the validation gate still governs what ships.
| `dream_rollouts` | `1` | Run each task K times → learn from the good-vs-bad contrast (contrastive reflection). |
| `recall_k` | `0` | Associative recall — pull the K most-similar past tasks (from a persisted archive) into tonight's dream. |
| `dream_factor` | `0` | Add N lightweight synthetic variants of each task. |
| `llm_dream` | `false` | Opt-in paraphrase generator for those variants. Templates stay the default and are used on any parse or fidelity failure. v1 is paraphrase-only: parent `reference`/`judge` are copied unchanged. |

## Results

Expand Down
2 changes: 1 addition & 1 deletion plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ python -m skillopt_sleep run --project "$(pwd)" \

The JSON/YAML config under `~/.skillopt-sleep/` supports additional engine keys,
including `gate_mode`, `gate_metric`, `gate_no_regression`, `dream_rollouts`,
`dream_factor`, `recall_k`, `evolve_memory`, and `evolve_skill`. These are config
`dream_factor`, `llm_dream`, `recall_k`, `evolve_memory`, and `evolve_skill`. These are config
keys, not aliases for the unsupported CLI flags listed above. Shipping defaults
are conservative: `gate_mode="on"`, `gate_no_regression=false`,
`dream_rollouts=1`, `dream_factor=0`, and `recall_k=0`.
Expand Down
1 change: 1 addition & 0 deletions skillopt_sleep/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
# ── dream + recall (opt-in; defaults reproduce the prior single-shot loop) ─
"dream_rollouts": 1, # >1 => multi-rollout contrastive reflection per task
"dream_factor": 0, # >0 => add N synthetic variants of each task to the dream
"llm_dream": False, # opt-in paraphrase generator; templates stay the default
"recall_k": 0, # >0 => recall the K most-similar past tasks into the dream
"evolve_memory": True, # consolidate CLAUDE.md
"evolve_skill": True, # consolidate the managed SKILL.md
Expand Down
14 changes: 12 additions & 2 deletions skillopt_sleep/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from skillopt_sleep import evidence
from skillopt_sleep.backend import Backend, CursorBackendError, build_backend
from skillopt_sleep.config import DEFAULTS, SleepConfig, load_config
from skillopt_sleep.dream import dream_consolidate
from skillopt_sleep.dream import backend_generate_fn, dream_consolidate
from skillopt_sleep.evidence import EvidenceLog
from skillopt_sleep.harvest_sources import harvest_for_config
from skillopt_sleep.memory import ensure_skill_scaffold
Expand Down Expand Up @@ -695,7 +695,7 @@ def run_sleep_cycle(
"target_backend", "target_model", "gate_mode", "gate_metric",
"gate_mixed_weight", "gate_no_regression", "edit_budget",
"holdout_fraction", "val_fraction", "test_fraction",
"dream_rollouts", "dream_factor", "recall_k",
"dream_rollouts", "dream_factor", "llm_dream", "recall_k",
"max_tasks_per_night", "lookback_hours", "llm_mine",
"evolve_skill", "evolve_memory")}
cycle_config["opencode_tool_replay"] = (
Expand Down Expand Up @@ -859,6 +859,11 @@ def run_sleep_cycle(
recall_k=recall_k,
dream_rollouts=int(cfg.get("dream_rollouts", 1) or 1),
dream_factor=int(cfg.get("dream_factor", 0) or 0),
llm_dream=bool(cfg.get("llm_dream", False)),
generate_fn=(
backend_generate_fn(backend) if cfg.get("llm_dream", False) else None
),
evidence=ev,
edit_budget=cfg.get("edit_budget", 4),
gate_metric=cfg.get("gate_metric", "mixed"),
gate_mixed_weight=cfg.get("gate_mixed_weight", 0.5),
Expand Down Expand Up @@ -953,6 +958,11 @@ def run_sleep_cycle(
recall_k=recall_k,
dream_rollouts=int(cfg.get("dream_rollouts", 1) or 1),
dream_factor=int(cfg.get("dream_factor", 0) or 0),
llm_dream=bool(cfg.get("llm_dream", False)),
generate_fn=(
backend_generate_fn(backend) if cfg.get("llm_dream", False) else None
),
evidence=ev,
edit_budget=cfg.get("edit_budget", 4),
gate_metric=cfg.get("gate_metric", "mixed"),
gate_mixed_weight=cfg.get("gate_mixed_weight", 0.5),
Expand Down
152 changes: 137 additions & 15 deletions skillopt_sleep/dream.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
from __future__ import annotations

import re
from typing import List, Optional
from typing import Callable, List, Optional

from skillopt_sleep.consolidate import ConsolidationResult, consolidate
from skillopt_sleep.types import TaskRecord

GenerateFn = Callable[[str], str]

# ── synthetic augmentation ("dream up" variants of today's tasks) ─────────────

_WRAPPERS = [
Expand All @@ -31,28 +33,139 @@
]


def dream_augment(real_tasks: List[TaskRecord], *, factor: int = 1) -> List[TaskRecord]:
def _template_intent(task: TaskRecord, k: int) -> str:
return _WRAPPERS[k % len(_WRAPPERS)].format(q=task.intent)


def _parse_paraphrases(raw: str, n: int) -> List[str]:
"""Accept a JSON array of strings; drop empty / short / non-string items."""
from skillopt_sleep.backend import _extract_json
parsed = _extract_json(raw or "", "array")
if not isinstance(parsed, list):
return []
out: List[str] = []
for item in parsed:
if not isinstance(item, str):
continue
text = item.strip()
if len(text) < 8:
continue
out.append(text)
if len(out) >= n:
break
return out


def _fidelity_ok(original: str, paraphrase: str) -> bool:
"""Paraphrase-only v1: keep the parent's constraints by construction.

We copy reference/judge unchanged, so a constraint-changing rewrite would
mislabel the variant. Refuse empty, identical, and prompt-echo strings.
Constraint perturbations are deferred to a later redesign of judge
propagation.
"""
text = (paraphrase or "").strip()
src = (original or "").strip()
if len(text) < 8 or not src:
return False
if text == src:
return False
if "Return ONLY a JSON array" in text:
return False
return True


def _dream_record(task: TaskRecord, k: int, intent: str, extra_tags: Optional[List[str]] = None) -> TaskRecord:
tags = list(task.tags) + ["dream"]
if extra_tags:
tags.extend(extra_tags)
return TaskRecord(
id=f"{task.id}_dream{k}", project=task.project,
intent=intent, context_excerpt=task.context_excerpt,
reference_kind=task.reference_kind, reference=task.reference,
judge=dict(task.judge), system=task.system,
tags=tags, split="train",
origin="dream", derived_from=task.id,
skill_hint=task.skill_hint,
)


def dream_augment(
real_tasks: List[TaskRecord],
*,
factor: int = 1,
llm_dream: bool = False,
generate_fn: Optional[GenerateFn] = None,
evidence=None,
) -> List[TaskRecord]:
"""Create synthetic TRAIN variants of real tasks (origin='dream').

A light, deterministic rephrasing. Dream tasks are training-only — they
carry split='train' and never enter the val/test slices the gate scores on.
Default path is a light, deterministic rephrasing. Dream tasks are
training-only: they carry split='train' and never enter the val/test
slices the gate scores on.

Opt-in ``llm_dream=True`` asks ``generate_fn`` for paraphrase-only
rewrites (parent reference/judge copied unchanged). Any parse or
fidelity failure falls back to the same wrappers as the default path,
so a night can degrade but not break. Template mode (the default) is
byte-identical to the pre-llm_dream implementation.
"""
out: List[TaskRecord] = []
use_llm = bool(llm_dream) and generate_fn is not None
if llm_dream and generate_fn is None and evidence is not None:
evidence.log(
"dream", "llm_dream_fallback",
reason="no_generate_fn", n_requested=max(0, factor),
)
for t in real_tasks:
parsed: List[str] = []
if use_llm:
try:
from skillopt_sleep import prompts as prompt_registry
prompt = prompt_registry.render("llm_dream", {
"__INTENT__": t.intent,
"__N__": str(max(0, factor)),
"__CONTEXT__": (t.context_excerpt or "")[:400],
})
parsed = _parse_paraphrases(generate_fn(prompt), max(0, factor))
except Exception:
parsed = []
n_ok = 0
for k in range(max(0, factor)):
w = _WRAPPERS[k % len(_WRAPPERS)]
out.append(TaskRecord(
id=f"{t.id}_dream{k}", project=t.project,
intent=w.format(q=t.intent), context_excerpt=t.context_excerpt,
reference_kind=t.reference_kind, reference=t.reference,
judge=dict(t.judge), system=t.system,
tags=list(t.tags) + ["dream"], split="train",
origin="dream", derived_from=t.id,
skill_hint=t.skill_hint,
))
extra: Optional[List[str]] = None
if use_llm and k < len(parsed) and _fidelity_ok(t.intent, parsed[k]):
intent = parsed[k]
extra = ["llm_dream"]
n_ok += 1
else:
intent = _template_intent(t, k)
out.append(_dream_record(t, k, intent, extra))
if use_llm and n_ok < max(0, factor) and evidence is not None:
evidence.log(
"dream", "llm_dream_fallback",
task_id=t.id,
n_fallback=max(0, factor) - n_ok,
n_requested=max(0, factor),
)
return out


def backend_generate_fn(backend) -> GenerateFn:
"""Adapter: reuse Backend.attempt so every backend can paraphrase.

The probe task is never added to the training pool.
"""
def generate(prompt: str) -> str:
probe = TaskRecord(
id="__llm_dream_probe__",
project="",
intent=prompt,
reference_kind="none",
)
return backend.attempt(probe, skill="", memory="")
return generate


# ── associative recall (experience replay of similar past tasks) ──────────────

def _tokens(text: str) -> set:
Expand Down Expand Up @@ -134,6 +247,9 @@ def dream_consolidate(
evolve_skill: bool = True,
evolve_memory: bool = True,
night: int = 1,
llm_dream: bool = False,
generate_fn: Optional[GenerateFn] = None,
evidence=None,
) -> ConsolidationResult:
"""Recall similar past experience + dream synthetic variants, then run one
gated consolidation epoch over the enlarged training pool.
Expand All @@ -157,7 +273,13 @@ def dream_consolidate(
)
if dream_factor > 0:
seed = [t for t in enlarged if t.split == "train" and t.origin != "dream"]
enlarged += dream_augment(seed, factor=dream_factor)
enlarged += dream_augment(
seed,
factor=dream_factor,
llm_dream=llm_dream,
generate_fn=generate_fn,
evidence=evidence,
)
return consolidate(
backend, enlarged, skill, memory,
edit_budget=edit_budget, gate_metric=gate_metric,
Expand Down
22 changes: 22 additions & 0 deletions skillopt_sleep/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@
"# Recurring failures\n__FAILURES__"
)

_LLM_DREAM = """You rewrite one existing task as a paraphrase-only variant.

Do NOT change the task's constraints, success criteria, required answer, tools,
or output format. Do NOT invent new requirements. Keep the same meaning.

Original intent:
__INTENT__

Optional context:
__CONTEXT__

Return ONLY a JSON array of exactly __N__ distinct paraphrase strings.
Example: ["please handle this request: ...", "for the daily report: ..."]
"""

# name -> {text, stage, role, description, placeholders}
DEFAULTS: Dict[str, Dict] = {
"miner": {
Expand Down Expand Up @@ -150,6 +165,13 @@
"__CRITERIA__", "__PREFS__", "__FAILURES__",
],
},
"llm_dream": {
"text": _LLM_DREAM,
"stage": "dream",
"role": "optimizer",
"description": "Paraphrase-only dream variants; parent judge/reference stay valid.",
"placeholders": ["__INTENT__", "__N__", "__CONTEXT__"],
},
}


Expand Down
Loading