dsh-open-dream-rsi 0.3.8 → 0.3.9
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/open_dream_rsi/__init__.py +1 -1
- package/open_dream_rsi/bench.py +1 -1
- package/open_dream_rsi/bench_policy.py +0 -1
- package/open_dream_rsi/cli.py +159 -10
- package/open_dream_rsi/core/curator.py +25 -7
- package/open_dream_rsi/core/policygen.py +88 -7
- package/open_dream_rsi/dashboard.py +2 -2
- package/open_dream_rsi/dream.py +8 -2
- package/open_dream_rsi/holdout.py +121 -0
- package/open_dream_rsi/lifecycle.py +8 -8
- package/open_dream_rsi/llm.py +5 -1
- package/open_dream_rsi/loop.py +16 -3
- package/open_dream_rsi/proxy.py +1 -1
- package/open_dream_rsi/sandbox.py +107 -3
- package/open_dream_rsi/sessions.py +192 -1
- package/open_dream_rsi/share.py +291 -10
- package/open_dream_rsi/tools.py +19 -5
- package/package.json +1 -1
package/open_dream_rsi/bench.py
CHANGED
|
@@ -57,7 +57,7 @@ def _make_client(provider: str, model: Optional[str]) -> Any:
|
|
|
57
57
|
def _default_tasks() -> List[Task]:
|
|
58
58
|
from open_dream_rsi.dashboard import DEMOS
|
|
59
59
|
|
|
60
|
-
return [Task(max_attempts
|
|
60
|
+
return [Task(**{**t, "max_attempts": 3}) for t in DEMOS]
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
def run_arm(
|
package/open_dream_rsi/cli.py
CHANGED
|
@@ -109,6 +109,7 @@ def cmd_sessions(args: argparse.Namespace) -> int:
|
|
|
109
109
|
from open_dream_rsi.sessions import (
|
|
110
110
|
episode_failures,
|
|
111
111
|
load_episodes,
|
|
112
|
+
read_claude_code_sessions,
|
|
112
113
|
read_cursor_sessions,
|
|
113
114
|
read_hermes_sessions,
|
|
114
115
|
write_episodes,
|
|
@@ -120,6 +121,9 @@ def cmd_sessions(args: argparse.Namespace) -> int:
|
|
|
120
121
|
source=args.filter,
|
|
121
122
|
limit=args.limit,
|
|
122
123
|
redact=not args.no_redact)
|
|
124
|
+
elif args.source == "claude":
|
|
125
|
+
eps = read_claude_code_sessions(args.db, limit=args.limit,
|
|
126
|
+
redact=not args.no_redact)
|
|
123
127
|
else:
|
|
124
128
|
# --db accepts either the Cursor User dir or a state.vscdb copy
|
|
125
129
|
db_arg = args.db
|
|
@@ -243,7 +247,8 @@ def cmd_dream(args: argparse.Namespace) -> int:
|
|
|
243
247
|
cost), so any trigger — a Hermes hook, the MCP tool, or this CLI —
|
|
244
248
|
can fire as often as it likes without overrunning the budget."""
|
|
245
249
|
from open_dream_rsi.dream import dream_once
|
|
246
|
-
from open_dream_rsi.sessions import
|
|
250
|
+
from open_dream_rsi.sessions import (default_claude_projects_dir,
|
|
251
|
+
default_cursor_user_dir)
|
|
247
252
|
|
|
248
253
|
client = None
|
|
249
254
|
if not args.offline:
|
|
@@ -252,6 +257,9 @@ def cmd_dream(args: argparse.Namespace) -> int:
|
|
|
252
257
|
# the Cursor store is a User DIR (state.vscdb inside), not a db file
|
|
253
258
|
sessions_db = args.sessions or args.cursor_user_dir \
|
|
254
259
|
or str(default_cursor_user_dir())
|
|
260
|
+
elif args.source == "claude":
|
|
261
|
+
# Claude Code transcripts live in a projects DIR of JSONL files
|
|
262
|
+
sessions_db = args.sessions or str(default_claude_projects_dir())
|
|
255
263
|
else:
|
|
256
264
|
sessions_db = args.sessions
|
|
257
265
|
report = dream_once(
|
|
@@ -264,10 +272,12 @@ def cmd_dream(args: argparse.Namespace) -> int:
|
|
|
264
272
|
|
|
265
273
|
|
|
266
274
|
def cmd_lessons(args: argparse.Namespace) -> int:
|
|
267
|
-
"""Git-backed
|
|
275
|
+
"""Git-backed stores: publish lessons/worlds/policies, import others'
|
|
268
276
|
knowledge as staging (see README 'Sharing lessons')."""
|
|
269
277
|
from open_dream_rsi.share import (LessonShareError, export_lessons,
|
|
270
|
-
|
|
278
|
+
export_policies, export_worlds,
|
|
279
|
+
import_lessons, import_policies,
|
|
280
|
+
import_worlds, store_status)
|
|
271
281
|
|
|
272
282
|
memory = DreamMemory(args.memory)
|
|
273
283
|
try:
|
|
@@ -284,6 +294,31 @@ def cmd_lessons(args: argparse.Namespace) -> int:
|
|
|
284
294
|
print(f"[odr] {cat}: +{n} staging lesson(s) — activate via "
|
|
285
295
|
f"the paired-replay gate (odr loop)")
|
|
286
296
|
return 0
|
|
297
|
+
elif args.lessons_cmd == "worlds-export":
|
|
298
|
+
st = export_worlds(memory, args.store, task_ids=args.task,
|
|
299
|
+
message=args.message, push=not args.no_push)
|
|
300
|
+
elif args.lessons_cmd == "worlds-import":
|
|
301
|
+
counts = import_worlds(args.store, memory, task_ids=args.task)
|
|
302
|
+
if not counts:
|
|
303
|
+
print("[odr] nothing imported (empty/filtered store)")
|
|
304
|
+
return 0
|
|
305
|
+
for tid, n in sorted(counts.items()):
|
|
306
|
+
print(f"[odr] {tid}: imported world ({n} nodes) -> "
|
|
307
|
+
f"trees/imported/{tid}.json (evidence, never a live task)")
|
|
308
|
+
return 0
|
|
309
|
+
elif args.lessons_cmd == "policies-export":
|
|
310
|
+
st = export_policies(memory, args.store, categories=args.category,
|
|
311
|
+
message=args.message, push=not args.no_push)
|
|
312
|
+
elif args.lessons_cmd == "policies-import":
|
|
313
|
+
counts = import_policies(args.store, memory,
|
|
314
|
+
categories=args.category)
|
|
315
|
+
if not counts:
|
|
316
|
+
print("[odr] nothing imported (empty/filtered store)")
|
|
317
|
+
return 0
|
|
318
|
+
for cat, n in sorted(counts.items()):
|
|
319
|
+
print(f"[odr] {cat}: +{n} policy program(s) registered as "
|
|
320
|
+
f"CANDIDATE->VALIDATED in the ALM (adoption stays local)")
|
|
321
|
+
return 0
|
|
287
322
|
else: # status
|
|
288
323
|
st = store_status(args.store)
|
|
289
324
|
except LessonShareError as exc:
|
|
@@ -294,13 +329,59 @@ def cmd_lessons(args: argparse.Namespace) -> int:
|
|
|
294
329
|
else:
|
|
295
330
|
print(f"[odr] store: {st.location} "
|
|
296
331
|
f"({'git' if st.git else 'plain dir'}, ok={st.ok})")
|
|
332
|
+
label = {"export": "lesson(s)", "worlds-export": "node(s)",
|
|
333
|
+
"policies-export": "program(s)"}.get(args.lessons_cmd, "entry")
|
|
297
334
|
for cat, n in sorted(st.categories.items()):
|
|
298
|
-
|
|
335
|
+
if ":" in cat: # status mixes sections; derive the label per key
|
|
336
|
+
label = {"world": "node(s)", "policy": "program(s)"}[
|
|
337
|
+
cat.split(":", 1)[0]]
|
|
338
|
+
print(f" {cat}: {n} {label}")
|
|
339
|
+
else:
|
|
340
|
+
print(f" {cat}: {n} {label}")
|
|
299
341
|
for e in st.errors:
|
|
300
342
|
print(f" ! {e}")
|
|
301
343
|
return 0 if st.ok else 1
|
|
302
344
|
|
|
303
345
|
|
|
346
|
+
def cmd_holdout(args: argparse.Namespace) -> int:
|
|
347
|
+
"""Single-use holdout reservations: a preregistered eval task is consumed
|
|
348
|
+
once; a later phase cannot silently reuse it (see README 'Holdout hygiene')."""
|
|
349
|
+
from open_dream_rsi.holdout import HoldoutRegistry, split_reserved
|
|
350
|
+
|
|
351
|
+
reg = HoldoutRegistry(args.memory)
|
|
352
|
+
if args.holdout_cmd == "claim":
|
|
353
|
+
refused = [t for t in args.ids if not reg.claim(t, args.purpose,
|
|
354
|
+
phase=args.phase)]
|
|
355
|
+
ok = [t for t in args.ids if t not in refused]
|
|
356
|
+
print(f"[odr] reserved {len(ok)} task id(s) for '{args.purpose}'"
|
|
357
|
+
+ (f" (phase {args.phase})" if args.phase else ""))
|
|
358
|
+
if refused:
|
|
359
|
+
print(f"[odr] REFUSED (already held for another purpose): "
|
|
360
|
+
f"{', '.join(refused)}", file=sys.stderr)
|
|
361
|
+
return 1
|
|
362
|
+
return 0
|
|
363
|
+
if args.holdout_cmd == "release":
|
|
364
|
+
released = [t for t in args.ids if reg.release(t)]
|
|
365
|
+
print(f"[odr] released {len(released)} reservation(s): "
|
|
366
|
+
f"{', '.join(released) or '(none)'}")
|
|
367
|
+
return 0
|
|
368
|
+
if args.holdout_cmd == "show":
|
|
369
|
+
entries = reg.entries()
|
|
370
|
+
if not entries:
|
|
371
|
+
print("[odr] no reservations")
|
|
372
|
+
for tid, meta in sorted(entries.items()):
|
|
373
|
+
print(f" {tid} purpose={meta.get('purpose')}"
|
|
374
|
+
+ (f" phase={meta['phase']}" if meta.get("phase") else ""))
|
|
375
|
+
return 0
|
|
376
|
+
# split: report which tasks of a queue file are holdout-reserved
|
|
377
|
+
tasks = json.loads(Path(args.tasks).read_text(encoding="utf-8"))
|
|
378
|
+
allowed, held = split_reserved(tasks, reg)
|
|
379
|
+
print(f"[odr] {len(allowed)} allowed, {len(held)} holdout-reserved")
|
|
380
|
+
for t in held:
|
|
381
|
+
print(f" held: {t.get('task_id') if isinstance(t, dict) else t}")
|
|
382
|
+
return 0
|
|
383
|
+
|
|
384
|
+
|
|
304
385
|
def cmd_skills(args: argparse.Namespace) -> int:
|
|
305
386
|
"""Render the curated lesson KB into Hermes-style skills (issue #3
|
|
306
387
|
boundary: only lessons that EARNED activation become a prompt)."""
|
|
@@ -399,9 +480,11 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
399
480
|
dr.add_argument("--sessions", default=None,
|
|
400
481
|
help="host session store to import evidence from "
|
|
401
482
|
"(hermes: state.db path; cursor: the User dir)")
|
|
402
|
-
dr.add_argument("--source", default="hermes",
|
|
483
|
+
dr.add_argument("--source", default="hermes",
|
|
484
|
+
choices=["hermes", "cursor", "claude"],
|
|
403
485
|
help="kind of the --sessions store (default hermes; "
|
|
404
|
-
"cursor reads state.vscdb directly, no MCP needed
|
|
486
|
+
"cursor reads state.vscdb directly, no MCP needed; "
|
|
487
|
+
"claude reads ~/.claude/projects JSONL)")
|
|
405
488
|
dr.add_argument("--cursor-user-dir", default=None,
|
|
406
489
|
help="Cursor User dir for --source cursor (default: the "
|
|
407
490
|
"platform location, e.g. %%APPDATA%%\\Cursor\\User)")
|
|
@@ -459,16 +542,19 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
459
542
|
|
|
460
543
|
sess = sub.add_parser(
|
|
461
544
|
"sessions",
|
|
462
|
-
help="import host transcripts (Hermes state.db / Cursor
|
|
545
|
+
help="import host transcripts (Hermes state.db / Cursor "
|
|
546
|
+
"state.vscdb / Claude Code projects JSONL) "
|
|
463
547
|
"as curator evidence — export to JSONL, then distill into "
|
|
464
548
|
"staging lessons (promotion gate still decides activation)")
|
|
465
549
|
sess_sub = sess.add_subparsers(dest="sessions_cmd", required=True)
|
|
466
550
|
|
|
467
551
|
exp = sess_sub.add_parser("export", help="read a host store, write episodes.jsonl")
|
|
468
|
-
exp.add_argument("--source", required=True,
|
|
552
|
+
exp.add_argument("--source", required=True,
|
|
553
|
+
choices=["hermes", "cursor", "claude"])
|
|
469
554
|
exp.add_argument("--db", default=None,
|
|
470
|
-
help="path to state.db (hermes)
|
|
471
|
-
"(default:
|
|
555
|
+
help="path to state.db (hermes), the Cursor User dir, "
|
|
556
|
+
"or the Claude Code projects dir (default: the "
|
|
557
|
+
"platform location)")
|
|
472
558
|
exp.add_argument("--filter", default=None,
|
|
473
559
|
help="hermes only: session source filter (e.g. cli, desktop)")
|
|
474
560
|
exp.add_argument("--limit", type=int, default=None)
|
|
@@ -532,11 +618,74 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
532
618
|
limp.add_argument("--category", action="append", default=None)
|
|
533
619
|
limp.set_defaults(func=cmd_lessons)
|
|
534
620
|
|
|
621
|
+
wexp = ls_sub.add_parser("worlds-export",
|
|
622
|
+
help="add archived discovery trees to a store")
|
|
623
|
+
wexp.add_argument("--store", required=True,
|
|
624
|
+
help="local dir or git URL")
|
|
625
|
+
wexp.add_argument("--task", action="append", default=None,
|
|
626
|
+
help="limit to these task ids (repeatable)")
|
|
627
|
+
wexp.add_argument("--message", default=None, help="git commit message")
|
|
628
|
+
wexp.add_argument("--no-push", action="store_true",
|
|
629
|
+
help="commit locally but do not push")
|
|
630
|
+
wexp.set_defaults(func=cmd_lessons)
|
|
631
|
+
|
|
632
|
+
wimp = ls_sub.add_parser("worlds-import",
|
|
633
|
+
help="import a store's worlds as evidence "
|
|
634
|
+
"(trees/imported/, never live tasks)")
|
|
635
|
+
wimp.add_argument("--store", required=True, help="local dir or git URL")
|
|
636
|
+
wimp.add_argument("--task", action="append", default=None,
|
|
637
|
+
help="limit to these task ids (repeatable)")
|
|
638
|
+
wimp.set_defaults(func=cmd_lessons)
|
|
639
|
+
|
|
640
|
+
pexp = ls_sub.add_parser("policies-export",
|
|
641
|
+
help="add promoted exploration-policy programs "
|
|
642
|
+
"(structural gate applies on export)")
|
|
643
|
+
pexp.add_argument("--store", required=True, help="local dir or git URL")
|
|
644
|
+
pexp.add_argument("--category", action="append", default=None,
|
|
645
|
+
help="limit to these categories (repeatable)")
|
|
646
|
+
pexp.add_argument("--message", default=None, help="git commit message")
|
|
647
|
+
pexp.add_argument("--no-push", action="store_true",
|
|
648
|
+
help="commit locally but do not push")
|
|
649
|
+
pexp.set_defaults(func=cmd_lessons)
|
|
650
|
+
|
|
651
|
+
pimp = ls_sub.add_parser("policies-import",
|
|
652
|
+
help="register a store's policy programs as "
|
|
653
|
+
"ALM CANDIDATE->VALIDATED (never incumbents)")
|
|
654
|
+
pimp.add_argument("--store", required=True, help="local dir or git URL")
|
|
655
|
+
pimp.add_argument("--category", action="append", default=None)
|
|
656
|
+
pimp.set_defaults(func=cmd_lessons)
|
|
657
|
+
|
|
535
658
|
lst = ls_sub.add_parser("status", help="verify manifest + checksums")
|
|
536
659
|
lst.add_argument("--store", required=True)
|
|
537
660
|
lst.add_argument("--json", action="store_true")
|
|
538
661
|
lst.set_defaults(func=cmd_lessons)
|
|
539
662
|
|
|
663
|
+
hld = sub.add_parser(
|
|
664
|
+
"holdout",
|
|
665
|
+
help="single-use holdout reservations — a preregistered eval task "
|
|
666
|
+
"is consumed once; later phases cannot silently reuse it")
|
|
667
|
+
hld_sub = hld.add_subparsers(dest="holdout_cmd", required=True)
|
|
668
|
+
|
|
669
|
+
hc = hld_sub.add_parser("claim", help="reserve task ids for a purpose")
|
|
670
|
+
hc.add_argument("--ids", nargs="+", required=True, help="task id(s)")
|
|
671
|
+
hc.add_argument("--purpose", required=True,
|
|
672
|
+
help="e.g. travelplanner_eval, spolki_holdout")
|
|
673
|
+
hc.add_argument("--phase", default=None,
|
|
674
|
+
help="optional phase label recorded with the reservation")
|
|
675
|
+
hc.set_defaults(func=cmd_holdout)
|
|
676
|
+
|
|
677
|
+
hr = hld_sub.add_parser("release", help="forget a reservation (correction path)")
|
|
678
|
+
hr.add_argument("--ids", nargs="+", required=True)
|
|
679
|
+
hr.set_defaults(func=cmd_holdout)
|
|
680
|
+
|
|
681
|
+
hs = hld_sub.add_parser("show", help="list all reservations")
|
|
682
|
+
hs.set_defaults(func=cmd_holdout)
|
|
683
|
+
|
|
684
|
+
hsplit = hld_sub.add_parser(
|
|
685
|
+
"split", help="partition a task queue into allowed vs held-back")
|
|
686
|
+
hsplit.add_argument("--tasks", required=True, help="tasks JSON file")
|
|
687
|
+
hsplit.set_defaults(func=cmd_holdout)
|
|
688
|
+
|
|
540
689
|
dash = sub.add_parser("dashboard", help="serve the live web dashboard")
|
|
541
690
|
dash.add_argument("--host", default="0.0.0.0",
|
|
542
691
|
help="bind address (default 0.0.0.0 — reachable on the LAN)")
|
|
@@ -88,7 +88,8 @@ LESSON_CONTRACT = (
|
|
|
88
88
|
"Reply with ONLY one fenced ```json``` block containing an array of "
|
|
89
89
|
"objects, each exactly: {{\"trigger\": \"...\", \"text\": \"...\"}}. "
|
|
90
90
|
"'trigger' is 1-4 lowercase keywords tying the lesson to this failure "
|
|
91
|
-
"family
|
|
91
|
+
"family — separate the keywords with SINGLE SPACES, never commas or "
|
|
92
|
+
"semicolons; 'text' is one actionable insight in at most 3000 characters, "
|
|
92
93
|
"grounded ONLY in the evidence shown. Be SPECIFIC, not terse: name the "
|
|
93
94
|
"observable signal (the exact error shape, tool, or state that "
|
|
94
95
|
"recurs), the mechanism that made the attempt fail, and the bounded "
|
|
@@ -144,18 +145,28 @@ def validate_lesson_items(items: Any, max_lessons: int) -> List[Dict[str, str]]:
|
|
|
144
145
|
raise LessonValidationError("payload is not a JSON array")
|
|
145
146
|
out: List[Dict[str, str]] = []
|
|
146
147
|
seen = set()
|
|
148
|
+
reasons: List[str] = []
|
|
147
149
|
for it in items:
|
|
148
150
|
if not isinstance(it, dict):
|
|
149
|
-
|
|
151
|
+
reasons.append("entry is not an object")
|
|
152
|
+
continue
|
|
150
153
|
trig = str(it.get("trigger", "")).strip()
|
|
151
154
|
text = str(it.get("text", "")).strip()
|
|
152
155
|
words = normalize(trig).split()
|
|
153
156
|
if not (MIN_TRIGGER_WORDS <= len(words) <= MAX_TRIGGER_WORDS):
|
|
154
|
-
|
|
155
|
-
|
|
157
|
+
# Tolerate comma/semicolon-separated keyword lists (LLMs produce
|
|
158
|
+
# "traceback, timeout" despite the contract): fold to the
|
|
159
|
+
# fragments when the joined form fits the word budget.
|
|
160
|
+
folded = " ".join(w for w in re.split(r"[,;/]+", trig) if w.strip())
|
|
161
|
+
if MIN_TRIGGER_WORDS <= len(folded.split()) <= MAX_TRIGGER_WORDS:
|
|
162
|
+
trig = folded
|
|
163
|
+
words = folded.split()
|
|
164
|
+
else:
|
|
165
|
+
reasons.append(f"trigger width {trig!r}")
|
|
166
|
+
continue
|
|
156
167
|
if not (20 <= len(text) <= MAX_LESSON_CHARS):
|
|
157
|
-
|
|
158
|
-
|
|
168
|
+
reasons.append(f"text len {len(text)}")
|
|
169
|
+
continue
|
|
159
170
|
key = lesson_key({"trigger": trig, "text": text})
|
|
160
171
|
if key in seen:
|
|
161
172
|
continue
|
|
@@ -163,6 +174,11 @@ def validate_lesson_items(items: Any, max_lessons: int) -> List[Dict[str, str]]:
|
|
|
163
174
|
out.append({"trigger": trig, "text": text})
|
|
164
175
|
if len(out) >= max_lessons:
|
|
165
176
|
break
|
|
177
|
+
if not out:
|
|
178
|
+
# All-or-nothing (the old behaviour) destroyed mixed payloads: one
|
|
179
|
+
# malformed trigger discarded every good lesson with it. Raise only
|
|
180
|
+
# when nothing is salvageable; the message feeds the repair loop.
|
|
181
|
+
raise LessonValidationError("; ".join(reasons[:3]) or "empty payload")
|
|
166
182
|
return out
|
|
167
183
|
|
|
168
184
|
|
|
@@ -359,7 +375,9 @@ def lesson_gate_verdict(results: Dict[str, List[Tuple[bool, bool]]]) -> GateVerd
|
|
|
359
375
|
unbounded exploration instruction must never activate on a greedy
|
|
360
376
|
solver no matter what a small sample says.
|
|
361
377
|
"""
|
|
362
|
-
promoted
|
|
378
|
+
promoted: List[str] = []
|
|
379
|
+
rejected: List[str] = []
|
|
380
|
+
detail: Dict[str, Any] = {}
|
|
363
381
|
for key, pairs in results.items():
|
|
364
382
|
gains = sum(1 for w, wo in pairs if w and not wo)
|
|
365
383
|
regs = sum(1 for w, wo in pairs if wo and not w)
|
|
@@ -61,7 +61,8 @@ from pathlib import Path
|
|
|
61
61
|
from typing import Any, Dict, List, Optional, Tuple
|
|
62
62
|
|
|
63
63
|
from open_dream_rsi.core.tree import DiscoveryTree
|
|
64
|
-
from open_dream_rsi.sandbox import
|
|
64
|
+
from open_dream_rsi.sandbox import (SandboxCache, cached_run_isolated,
|
|
65
|
+
content_key, run_isolated)
|
|
65
66
|
|
|
66
67
|
#: Weight of branch diversity in the rollout score (the paper's beta_2:
|
|
67
68
|
#: replay objectives reward visiting *different* branches, not just high-
|
|
@@ -289,8 +290,11 @@ class PolicySandbox:
|
|
|
289
290
|
gated by :func:`validate_policy_source` before we ever spawn a process.
|
|
290
291
|
"""
|
|
291
292
|
|
|
292
|
-
def __init__(self, timeout: float = 5.0
|
|
293
|
+
def __init__(self, timeout: float = 5.0,
|
|
294
|
+
cache: Optional[SandboxCache] = None):
|
|
293
295
|
self.timeout = timeout
|
|
296
|
+
#: Optional content-addressed result cache (opt-in; None = uncached).
|
|
297
|
+
self.cache = cache
|
|
294
298
|
|
|
295
299
|
def choose(self, source: str, frontier: List[Dict[str, Any]], step: int) -> PolicyRun:
|
|
296
300
|
try:
|
|
@@ -305,10 +309,14 @@ class PolicySandbox:
|
|
|
305
309
|
fr_path.write_text(json.dumps(frontier), encoding="utf-8")
|
|
306
310
|
harness_path.write_text(_POLICY_HARNESS, encoding="utf-8")
|
|
307
311
|
try:
|
|
308
|
-
proc =
|
|
312
|
+
proc = cached_run_isolated(
|
|
313
|
+
run_isolated,
|
|
309
314
|
[sys.executable, "-I", str(harness_path), str(src_path), str(fr_path), str(step)],
|
|
315
|
+
key=content_key({"harness": _POLICY_HARNESS, "policy": source,
|
|
316
|
+
"frontier": frontier}, timeout=self.timeout),
|
|
310
317
|
timeout=self.timeout,
|
|
311
318
|
cwd=d,
|
|
319
|
+
cache=self.cache,
|
|
312
320
|
)
|
|
313
321
|
except subprocess.TimeoutExpired:
|
|
314
322
|
return PolicyRun(None, "policy execution timed out")
|
|
@@ -346,10 +354,14 @@ class PolicySandbox:
|
|
|
346
354
|
world_path.write_text(json.dumps(world), encoding="utf-8")
|
|
347
355
|
harness_path.write_text(_ROLLOUT_HARNESS, encoding="utf-8")
|
|
348
356
|
try:
|
|
349
|
-
proc =
|
|
357
|
+
proc = cached_run_isolated(
|
|
358
|
+
run_isolated,
|
|
350
359
|
[sys.executable, "-I", str(harness_path), str(src_path), str(world_path)],
|
|
360
|
+
key=content_key({"harness": _ROLLOUT_HARNESS, "policy": source,
|
|
361
|
+
"world": world}, timeout=self.timeout),
|
|
351
362
|
timeout=self.timeout,
|
|
352
363
|
cwd=d,
|
|
364
|
+
cache=self.cache,
|
|
353
365
|
)
|
|
354
366
|
except subprocess.TimeoutExpired:
|
|
355
367
|
return None, [], 0, "policy rollout timed out"
|
|
@@ -410,7 +422,7 @@ def frontier_entry(node: Any, outcomes: Dict[str, float]) -> Dict[str, Any]:
|
|
|
410
422
|
``outcome`` per step with the as-of-now value over revealed descendants
|
|
411
423
|
only (prefix rule).
|
|
412
424
|
"""
|
|
413
|
-
errors = []
|
|
425
|
+
errors: List[Any] = []
|
|
414
426
|
result = node.result
|
|
415
427
|
if isinstance(result, dict):
|
|
416
428
|
errors = result.get("errors") or []
|
|
@@ -635,6 +647,25 @@ Previous candidate rejection reason: {feedback}
|
|
|
635
647
|
Return an improved complete policy as one python code block."""
|
|
636
648
|
|
|
637
649
|
|
|
650
|
+
REVISE_USER_TEMPLATE = """Category: {category}
|
|
651
|
+
|
|
652
|
+
Your current policy program (the incumbent — keep its successful strategy,
|
|
653
|
+
fix only what the replay feedback indicts):
|
|
654
|
+
```python
|
|
655
|
+
{incumbent}
|
|
656
|
+
```
|
|
657
|
+
|
|
658
|
+
Replay metrics of the current policy: {metrics}
|
|
659
|
+
|
|
660
|
+
Discovery history summary ({nodes} nodes, best score {best_score}):
|
|
661
|
+
{summary}
|
|
662
|
+
|
|
663
|
+
Why the last challenger was rejected: {feedback}
|
|
664
|
+
|
|
665
|
+
Return a REVISED complete policy as one python code block (full program,
|
|
666
|
+
not a patch)."""
|
|
667
|
+
|
|
668
|
+
|
|
638
669
|
@dataclass
|
|
639
670
|
class GenerationResult:
|
|
640
671
|
source: Optional[str]
|
|
@@ -664,6 +695,7 @@ class PolicyGenerator:
|
|
|
664
695
|
incumbent_score: float,
|
|
665
696
|
tree: DiscoveryTree,
|
|
666
697
|
api_calls: Optional[List[int]] = None,
|
|
698
|
+
revise: bool = False,
|
|
667
699
|
) -> GenerationResult:
|
|
668
700
|
steps = sum(1 for n in tree.nodes.values() if n.parent_id)
|
|
669
701
|
if steps < 2:
|
|
@@ -671,8 +703,13 @@ class PolicyGenerator:
|
|
|
671
703
|
summary = self._tree_summary(tree)
|
|
672
704
|
feedback = ""
|
|
673
705
|
source: Optional[str] = None
|
|
706
|
+
user_template = REVISE_USER_TEMPLATE if revise else GEN_USER_TEMPLATE
|
|
707
|
+
system_prompt = (
|
|
708
|
+
POLICY_CONTRACT + " You are REVISING the incumbent policy: keep "
|
|
709
|
+
"its successful strategy, change only what the feedback indicts."
|
|
710
|
+
if revise else POLICY_CONTRACT)
|
|
674
711
|
for attempt in range(1 + self.max_repair):
|
|
675
|
-
user =
|
|
712
|
+
user = user_template.format(
|
|
676
713
|
category=category,
|
|
677
714
|
incumbent=incumbent_source or "(none — first policy)",
|
|
678
715
|
metrics=f"{incumbent_score:.4f}",
|
|
@@ -686,7 +723,7 @@ class PolicyGenerator:
|
|
|
686
723
|
if self.max_tokens:
|
|
687
724
|
kwargs["max_tokens"] = self.max_tokens
|
|
688
725
|
reply = self.client.chat(
|
|
689
|
-
[{"role": "system", "content":
|
|
726
|
+
[{"role": "system", "content": system_prompt},
|
|
690
727
|
{"role": "user", "content": user}],
|
|
691
728
|
**kwargs,
|
|
692
729
|
)
|
|
@@ -720,6 +757,50 @@ class PolicyGenerator:
|
|
|
720
757
|
return GenerationResult(source, score, "")
|
|
721
758
|
return GenerationResult(None, float("-inf"), feedback or "generation failed")
|
|
722
759
|
|
|
760
|
+
@staticmethod
|
|
761
|
+
def _replay_brief(tree: DiscoveryTree, horizon: Optional[int] = None,
|
|
762
|
+
max_steps: int = 24) -> str:
|
|
763
|
+
"""Greedy replay trace: the counterfactual the incumbent must beat.
|
|
764
|
+
|
|
765
|
+
Mirrors :func:`_simulate_step_rewards` (greedy pick = best visible
|
|
766
|
+
score) and reports, per step, what the greedy policy would harvest —
|
|
767
|
+
the model sees WHERE the greedy replay stalls (repeated exhausted
|
|
768
|
+
picks, zero-reward steps) and revises against that evidence instead
|
|
769
|
+
of rewriting blind.
|
|
770
|
+
"""
|
|
771
|
+
if not tree.nodes:
|
|
772
|
+
return "(empty)"
|
|
773
|
+
world = rollout_world_payload(tree, horizon)
|
|
774
|
+
|
|
775
|
+
def pick(visible, step):
|
|
776
|
+
return max(visible, key=lambda e: e["score"])["node_id"] if visible else None
|
|
777
|
+
|
|
778
|
+
rewards, picks, _invalid, _err = _simulate_step_rewards(world, pick)
|
|
779
|
+
lines = []
|
|
780
|
+
for i, (pick_id, reward) in enumerate(list(zip(picks, rewards))[:max_steps]):
|
|
781
|
+
repeat = " REPEAT" if picks[:i].count(pick_id) else ""
|
|
782
|
+
lines.append(f"step {i}: expand {pick_id} -> reward {reward:.3f}{repeat}")
|
|
783
|
+
exhausted = [p for p in picks if picks.count(p) > 1]
|
|
784
|
+
if exhausted:
|
|
785
|
+
lines.append(f"greedy re-picks: {sorted(set(exhausted))[:4]} "
|
|
786
|
+
"(exhausted branches — the incumbent camps here)")
|
|
787
|
+
return "\n".join(lines) or "(no steps)"
|
|
788
|
+
|
|
789
|
+
def generate_with_brief(self, category, incumbent_source, incumbent_score,
|
|
790
|
+
tree, api_calls=None):
|
|
791
|
+
"""Revise mode with the greedy replay brief as structured feedback.
|
|
792
|
+
|
|
793
|
+
A candidate promoted here faced strictly stronger evidence than a
|
|
794
|
+
blind rewrite: same promotion gate, same strict win requirement —
|
|
795
|
+
only the prompt carries the incumbent's measured replay trace.
|
|
796
|
+
"""
|
|
797
|
+
brief = self._replay_brief(tree)
|
|
798
|
+
result = self.generate(category, incumbent_source, incumbent_score,
|
|
799
|
+
tree, api_calls=api_calls, revise=True)
|
|
800
|
+
if result.source is None and brief:
|
|
801
|
+
result.error = f"{result.error} | greedy replay: {brief[:200]}".strip(" |")
|
|
802
|
+
return result
|
|
803
|
+
|
|
723
804
|
@staticmethod
|
|
724
805
|
def _tree_summary(tree: DiscoveryTree, max_nodes: int = 12) -> str:
|
|
725
806
|
lines = []
|
|
@@ -51,7 +51,7 @@ SOLUTIONS = {
|
|
|
51
51
|
),
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
DEMOS = [
|
|
54
|
+
DEMOS: List[Dict[str, Any]] = [
|
|
55
55
|
dict(task_id="add1", category="add",
|
|
56
56
|
prompt="Implement add(a, b) returning the sum of two numbers.",
|
|
57
57
|
tests=[{"call": "add(2, 3)", "expected": 5}, {"call": "add(-1, 1)", "expected": 0}]),
|
|
@@ -224,7 +224,7 @@ def run_dashboard(host: str = "0.0.0.0", port: int = 8765, provider: str = "mock
|
|
|
224
224
|
raw = json.loads(open(tasks_file, encoding="utf-8").read())
|
|
225
225
|
demo_tasks = [Task(**t) for t in raw]
|
|
226
226
|
else:
|
|
227
|
-
demo_tasks = [Task(max_attempts
|
|
227
|
+
demo_tasks = [Task(**{**t, "max_attempts": 3}) for t in DEMOS]
|
|
228
228
|
|
|
229
229
|
if provider == "mock":
|
|
230
230
|
client: Any = MockLLM()
|
package/open_dream_rsi/dream.py
CHANGED
|
@@ -194,6 +194,8 @@ def ingest_sessions(memory, sessions_db: "str | Path", *,
|
|
|
194
194
|
``source="hermes"`` reads a Hermes ``state.db``; ``source="cursor"``
|
|
195
195
|
reads the Cursor ``User`` dir (``state.vscdb``) directly — no MCP
|
|
196
196
|
server involved, so this also works where MCP is blocked by policy.
|
|
197
|
+
``source="claude"`` reads Claude Code JSONL transcripts under
|
|
198
|
+
``~/.claude/projects`` (or the given projects dir).
|
|
197
199
|
|
|
198
200
|
Returns {'new': n_sessions, 'added': n_lessons, 'probes': n_tasks,
|
|
199
201
|
'deferred': n_deferred (endpoint failed; session left unseen)}.
|
|
@@ -214,13 +216,17 @@ def ingest_sessions(memory, sessions_db: "str | Path", *,
|
|
|
214
216
|
state = _load_state(root)
|
|
215
217
|
seen = set(state.get("seen_sessions", []))
|
|
216
218
|
|
|
217
|
-
if source not in ("hermes", "cursor"):
|
|
219
|
+
if source not in ("hermes", "cursor", "claude"):
|
|
218
220
|
raise ValueError(f"unknown session source {source!r} "
|
|
219
|
-
"(expected 'hermes' or '
|
|
221
|
+
"(expected 'hermes', 'cursor' or 'claude')")
|
|
220
222
|
if source == "cursor":
|
|
221
223
|
from open_dream_rsi.sessions import read_cursor_sessions
|
|
222
224
|
eps = read_cursor_sessions(user_dir=sessions_db,
|
|
223
225
|
limit=limit + len(seen))
|
|
226
|
+
elif source == "claude":
|
|
227
|
+
from open_dream_rsi.sessions import read_claude_code_sessions
|
|
228
|
+
eps = read_claude_code_sessions(projects_dir=sessions_db,
|
|
229
|
+
limit=limit + len(seen))
|
|
224
230
|
else:
|
|
225
231
|
eps = read_hermes_sessions(sessions_db, limit=limit + len(seen))
|
|
226
232
|
new = [e for e in eps if e.session_id not in seen][:limit]
|