Skip to content

Commit 576f2f8

Browse files
authored
Merge pull request #59 from Elzlxx/feat/openclaw-skillopt-sleep
feat(plugins): add OpenClaw shell for SkillOpt-Sleep
2 parents 00d07bc + 5534465 commit 576f2f8

10 files changed

Lines changed: 1244 additions & 0 deletions

‎plugins/openclaw/README.md‎

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# OpenClaw Plugin for SkillOpt-Sleep
2+
3+
Thin shell for running [SkillOpt-Sleep](https://github.com/microsoft/SkillOpt) on [OpenClaw](https://github.com/openclaw/openclaw).
4+
5+
## What it does
6+
7+
Adds a nightly "sleep cycle" to any OpenClaw agent. The cycle:
8+
9+
1. **Harvests** recent session transcripts from `~/.openclaw/agents/<name>/sessions/*.jsonl`
10+
2. **Mines** recurring task patterns using the optimizer LLM
11+
3. **Replays** each pattern with the current `SKILL.md` (baseline) and a candidate `SKILL.md` (with proposed edits)
12+
4. **Gates** the candidate against the held-out score (rejects regressions)
13+
5. **Stages** the accepted proposal in `~/.skillopt-sleep/staging/<night>/`
14+
6. Leaves adoption to the operator (Ethan)
15+
16+
Nothing live changes until you adopt. Every adopt backs up first.
17+
18+
## Install
19+
20+
The plugin is a thin wrapper around the engine at `~/.openclaw/workspace/SkillOpt/skillopt_sleep/`:
21+
22+
```bash
23+
# 1. Clone the engine (one-time)
24+
cd ~/.openclaw/workspace
25+
git clone https://github.com/microsoft/SkillOpt.git
26+
27+
# 2. Install the OpenClaw skill (this folder)
28+
ln -s /path/to/openclaw ~/.openclaw/workspace/skills/skillopt-sleep
29+
30+
# 3. Configure
31+
cp ~/.openclaw/workspace/skills/skillopt-sleep/config.json ~/.skillopt-sleep/config.json
32+
$EDITOR ~/.skillopt-sleep/config.json
33+
# Set backend = "openclaw-deepseek"
34+
# Set model = "deepseek-v4-pro" (or "deepseek-v4-flash" for budget)
35+
36+
# 4. Set API key
37+
echo 'export DEEPSEEK_API_KEY="sk-..."' >> ~/.openclaw/.env
38+
39+
# 5. Add the nightly cron
40+
(crontab -l 2>/dev/null; echo "0 3 * * * cd ~/.openclaw/workspace/skills/skillopt-sleep && bash run_sleep_cron.sh >> ~/.skillopt-sleep/nightly.log 2>&1") | crontab -
41+
```
42+
43+
## Use
44+
45+
### Manual trigger
46+
47+
```bash
48+
# Run one cycle now
49+
python3 ~/.openclaw/workspace/skills/skillopt-sleep/run_sleep.py
50+
51+
# Dry run (report only)
52+
python3 ~/.openclaw/workspace/skills/skillopt-sleep/run_sleep.py --dry-run
53+
54+
# One category only
55+
python3 ~/.openclaw/workspace/skills/skillopt-sleep/run_sleep.py --tasks tests/research-cron-tasks.json
56+
```
57+
58+
### Slash command
59+
60+
```bash
61+
# In any OpenClaw session
62+
/sleep status
63+
/sleep run
64+
/sleep run research-cron
65+
/sleep dry-run
66+
/sleep adopt # adopt most recent accepted proposal
67+
/sleep reject # discard most recent
68+
/sleep cost
69+
```
70+
71+
## Architecture
72+
73+
```
74+
plugins/openclaw/
75+
├── README.md # this file
76+
├── run_sleep_cron.sh # wrapper for cron invocation
77+
├── run_sleep.py # main entry point
78+
├── slash_sleep.py # /sleep command implementation
79+
├── skillopt_sleep_openclaw.py # DeepSeek + Ollama backend
80+
├── config.json # engine config
81+
├── SKILL.md # OpenClaw skill manifest
82+
└── tests/ # held-out test sets
83+
├── research-cron-tasks.json
84+
├── devops-tasks.json
85+
└── wiki-tasks.json
86+
```
87+
88+
The OpenClaw shell is one engine (skillopt_sleep/) + one backend (DeepSeek/Ollama) + four thin wrappers (cron, slash, skill, tests).
89+
90+
## Why this matters for OpenClaw
91+
92+
OpenClaw currently has no built-in "self-evolving skills" mechanism. The community has:
93+
94+
- **Manual skills** — Ethan writes them
95+
- **LLM-generated skills** — one-shot, no validation
96+
- **Self-revision** — unbounded, no quality bar
97+
98+
SkillOpt-Sleep adds a 4th option: **validated self-evolution**. The skill is the training target, the engine is the optimizer, the gate is the quality bar, the operator is the human-in-the-loop.
99+
100+
## Validation
101+
102+
Validated on the public [gbrain-evals](https://github.com/garrytan/gbrain-evals) `skillopt-v1` benchmark with real Claude and Codex (deficient skills 0.00 → 1.00 on held-out, all 4 seeds).
103+
104+
End-to-end test on our own 14-task held-out set: pipeline runs, gate correctly rejects non-improvements, staging artifacts land in `~/.skillopt-sleep/staging/<night>/`.
105+
106+
## Cost
107+
108+
Measured: ~$0.02/night with `deepseek-v4-pro` at 12 tasks/night. ~$0.59/month, $7.18/year.
109+
110+
## License
111+
112+
MIT (same as SkillOpt core).

‎plugins/openclaw/SKILL.md‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
name: skillopt-sleep
3+
description: Validate and refine agent skills through nightly sleep cycles with held-out gates. Wraps Microsoft's SkillOpt-Sleep engine for the OpenClaw/DeepSeek stack.
4+
---
5+
6+
# skillopt-sleep — OpenClaw Adaptation of Microsoft SkillOpt-Sleep
7+
8+
A nightly self-improvement loop that reads our session transcripts, mines recurring workflow patterns, replays them with proposed skill edits, and gates the proposals against a held-out test set. Only improvements that beat baseline are staged for human adoption.
9+
10+
## When To Use
11+
12+
- After Hermes's Weekly Skill Review (or as its replacement)
13+
- When a skill is being used 10+ times/week and could be tighter
14+
- Before promoting a new skill from `skill-proposals/` to `skills/`
15+
- When a skill regresses in observed quality
16+
17+
## What It Does (One Cycle)
18+
19+
```
20+
harvest session transcripts -> mine recurring task patterns
21+
-> replay each pattern (current skill vs proposed)
22+
-> GATE: must improve held-out score
23+
-> stage proposal
24+
-> Ethan adopts (manual)
25+
```
26+
27+
Nothing live changes until Ethan adopts. Every adopt backs up first.
28+
29+
## Architecture
30+
31+
```
32+
skills/skillopt-sleep/
33+
├── SKILL.md # this file
34+
├── config.json # engine config (backend, budgets, etc.)
35+
├── run_sleep.py # entry point
36+
└── skillopt_sleep_openclaw.py # DeepSeek/Ollama backend
37+
```
38+
39+
The engine itself is at `~/.openclaw/workspace/SkillOpt/skillopt_sleep/` (cloned from microsoft/SkillOpt).
40+
41+
## Usage
42+
43+
```bash
44+
# Run one cycle with current config
45+
cd ~/.openclaw/workspace/skills/skillopt-sleep
46+
python3 run_sleep.py
47+
48+
# Dry run (report only, no staging)
49+
python3 run_sleep.py --dry-run
50+
51+
# Use a pre-built task set (recommended for testing)
52+
python3 run_sleep.py --tasks tests/research-cron-tasks.json
53+
```
54+
55+
## Config (config.json)
56+
57+
Key knobs:
58+
- `backend: "openclaw-deepseek"` — our custom backend
59+
- `model: "deepseek-v4-pro"` — optimizer model
60+
- `edit_budget: 3` — max bounded edits per night
61+
- `gate_mode: "on"` — validation-gated (rejects regressions)
62+
- `auto_adopt: false` — require Ethan to adopt manually
63+
- `max_tasks_per_night: 12` — cap to control cost
64+
65+
## Cost Estimate
66+
67+
Per night: 12 tasks × (1 attempt + 1 judge + 1 reflect) × ~$0.005/1K tokens × ~3K tokens/call ≈ **$0.50-2.00/night**.
68+
69+
## Outputs
70+
71+
- Report: `~/.skillopt-sleep/state.json` (running totals)
72+
- Staging: `~/.skillopt-sleep/staging/<night>/`
73+
- `report.md` — readable summary
74+
- `best_skill.md` — proposed skill
75+
- `edits.json` — bounded edit list
76+
- `before.md` / `after.md` — diffs
77+
78+
## Held-Out Test Sets (Phase 2)
79+
80+
Located at `tests/<category>-tasks.json`. Each task has:
81+
- `prompt` — the recurring task
82+
- `reference` — exact-match gold answer
83+
- `rubric` — soft score rubric (0-1)
84+
- `domain` — research/devops/wiki/etc.
85+
86+
Currently building for 3 categories:
87+
- research-cron-output
88+
- devops-infrastructure-check
89+
- wiki-canonical-guide
90+
91+
## When NOT To Use
92+
93+
- For a one-off workflow (not a recurring pattern)
94+
- During a crisis/incident (humans must lead)
95+
- When session transcripts are < 24h old (not enough signal)
96+
- For skills < 300 tokens (over-optimization risk)

‎plugins/openclaw/config.json‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"_comment": "OpenClaw adaptation of skillopt-sleep. Edit and run via run_sleep.py",
3+
4+
"claude_home": "/home/ethanclaw/.openclaw/agents",
5+
"invoked_project": "/home/ethanclaw/.openclaw/workspace",
6+
"projects": "invoked",
7+
"lookback_hours": 168,
8+
9+
"max_tasks_per_night": 12,
10+
"max_tokens_per_night": 800000,
11+
"holdout_fraction": 0.34,
12+
"val_fraction": 0.34,
13+
"test_fraction": 0.0,
14+
15+
"backend": "openclaw-deepseek",
16+
"model": "deepseek-v4-pro",
17+
"gate_mode": "on",
18+
"edit_budget": 3,
19+
"gate_metric": "mixed",
20+
"gate_mixed_weight": 0.5,
21+
"replay_mode": "fresh",
22+
"evolve_memory": true,
23+
"evolve_skill": true,
24+
"llm_mine": false,
25+
26+
"auto_adopt": false,
27+
"managed_skill_name": "skillopt-sleep-learned",
28+
"redact_secrets": true,
29+
"seed": 42
30+
}

‎plugins/openclaw/run_sleep.py‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
#!/usr/bin/env python3
2+
"""run_sleep.py — OpenClaw entry point for SkillOpt-Sleep.
3+
4+
Runs one nightly sleep cycle:
5+
1. harvest recent session transcripts
6+
2. mine recurring task patterns
7+
3. replay tasks with current skill (baseline) + candidate skill (with proposed edit)
8+
4. gate candidate vs baseline on held-out accuracy
9+
5. stage the proposal in ~/.skillopt-sleep/staging/<night>/
10+
6. leave adoption to Ethan (auto_adopt=false)
11+
12+
Usage:
13+
python3 run_sleep.py # one cycle, default config
14+
python3 run_sleep.py --dry-run # compute report only, no staging
15+
python3 run_sleep.py --tasks path.json # use a pre-built task file
16+
"""
17+
from __future__ import annotations
18+
19+
import argparse
20+
import json
21+
import os
22+
import sys
23+
from pathlib import Path
24+
25+
# Ensure the skillopt_sleep package is importable (it lives in the cloned repo)
26+
REPO = Path("/home/ethanclaw/.openclaw/workspace/SkillOpt")
27+
sys.path.insert(0, str(REPO))
28+
29+
# Register our backend before importing cycle
30+
from skillopt_sleep_openclaw import OpenClawDeepSeekBackend
31+
import skillopt_sleep.backend as _b
32+
_b._BACKENDS = getattr(_b, "_BACKENDS", {})
33+
_b._BACKENDS["openclaw-deepseek"] = OpenClawDeepSeekBackend
34+
35+
# Patch get_backend to know about our backend
36+
_orig_get_backend = _b.get_backend
37+
38+
def get_backend(name, model="", codex_path=""):
39+
if name == "openclaw-deepseek":
40+
return OpenClawDeepSeekBackend(model=model or "deepseek-v4-pro")
41+
return _orig_get_backend(name, model=model, codex_path=codex_path)
42+
43+
_b.get_backend = get_backend
44+
45+
from skillopt_sleep.cycle import run_sleep_cycle
46+
from skillopt_sleep.config import load_config
47+
48+
49+
def main() -> int:
50+
ap = argparse.ArgumentParser(description="OpenClaw SkillOpt-Sleep nightly cycle")
51+
ap.add_argument("--dry-run", action="store_true", help="Compute but don't stage")
52+
ap.add_argument("--config", default="/home/ethanclaw/.openclaw/workspace/skills/skillopt-sleep/config.json")
53+
ap.add_argument("--tasks", default=None, help="Path to pre-built tasks JSON")
54+
ap.add_argument("--verbose", action="store_true")
55+
args = ap.parse_args()
56+
57+
# Load config from file then override with our defaults
58+
overrides = {}
59+
if os.path.exists(args.config):
60+
with open(args.config) as f:
61+
overrides.update(json.load(f))
62+
overrides.pop("_comment", None)
63+
64+
cfg = load_config(**overrides)
65+
66+
seed_tasks = None
67+
if args.tasks:
68+
from skillopt_sleep.types import TaskRecord
69+
with open(args.tasks) as f:
70+
raw = json.load(f)
71+
# Translate our test-set fields → TaskRecord fields
72+
seed_tasks = []
73+
for t in raw:
74+
seed_tasks.append(TaskRecord(
75+
id=t['id'],
76+
project=t.get('project', 'openclaw'),
77+
intent=t.get('intent') or t.get('prompt', ''),
78+
context_excerpt=t.get('context_excerpt', ''),
79+
attempted_solution=t.get('attempted_solution', ''),
80+
outcome=t.get('outcome', 'unknown'),
81+
reference_kind=t.get('reference_kind', 'rubric'),
82+
reference=t.get('reference', ''),
83+
judge=t.get('judge', {}),
84+
tags=t.get('tags', []),
85+
source_sessions=t.get('source_sessions', []),
86+
split=t.get('split', 'train'),
87+
))
88+
89+
print(f"[skillopt-sleep] starting cycle...")
90+
print(f" backend: {cfg.get('backend')}")
91+
print(f" project: {cfg.get('invoked_project')}")
92+
print(f" max tasks: {cfg.get('max_tasks_per_night')}")
93+
print(f" edit budget: {cfg.get('edit_budget')}")
94+
print(f" dry_run: {args.dry_run}")
95+
96+
outcome = run_sleep_cycle(cfg, seed_tasks=seed_tasks, dry_run=args.dry_run)
97+
98+
r = outcome.report
99+
print(f"\n=== Report — night {r.night} ===")
100+
print(f" sessions harvested: {r.n_sessions}")
101+
print(f" tasks mined: {r.n_tasks} (replayed: {r.n_replayed})")
102+
print(f" baseline: {r.baseline_score:.3f} -> candidate: {r.candidate_score:.3f}")
103+
print(f" gate: {r.gate_action} accepted={r.accepted}")
104+
print(f" tokens: {r.tokens_used}")
105+
if r.edits:
106+
print(f" applied edits ({len(r.edits)}):")
107+
for e in r.edits:
108+
print(f" [{e.target}/{e.op}] {e.content[:80]}...")
109+
if r.rejected_edits:
110+
print(f" rejected edits ({len(r.rejected_edits)}) — kept as negative feedback")
111+
if r.notes:
112+
for n in r.notes:
113+
print(f" note: {n}")
114+
if outcome.staging_dir:
115+
print(f"\n STAGED at: {outcome.staging_dir}")
116+
print(f" Review with: ls {outcome.staging_dir}")
117+
118+
return 0 if r.accepted or r.candidate_score >= r.baseline_score else 1
119+
120+
121+
if __name__ == "__main__":
122+
sys.exit(main())

0 commit comments

Comments
 (0)