@@ -47,6 +47,11 @@ class Backend:
4747 name = "base"
4848 # Optional user preferences (free text) injected into reflect as a prior.
4949 preferences : str = ""
50+ # Optional per-night evidence log (skillopt_sleep.evidence.EvidenceLog).
51+ # Attached by the cycle; None => no observability overhead. The phase tag
52+ # labels which consolidation step subsequent replay calls belong to.
53+ evidence = None
54+ evidence_phase : str = ""
5055
5156 def attempt (self , task : TaskRecord , skill : str , memory : str ,
5257 sample_id : int = 0 ) -> str :
@@ -330,11 +335,23 @@ def _call(self, prompt: str, *, max_tokens: int = 1024) -> str:
330335 raise NotImplementedError
331336
332337 def _cached_call (self , key : str , prompt : str , * , max_tokens : int = 1024 ) -> str :
338+ kind = key .split (":" , 1 )[0 ]
339+ ev = getattr (self , "evidence" , None )
333340 if key in self ._cache :
341+ # cache hits log key-only (the full text is on the original miss event)
342+ if ev is not None :
343+ ev .log ("replay" , "model_call" , kind = kind , cache_hit = True , key = key ,
344+ phase = getattr (self , "evidence_phase" , "" ), backend = self .name ,
345+ model = self .model )
334346 return self ._cache [key ]
335347 out = self ._call (prompt , max_tokens = max_tokens )
336348 self ._tokens += len (prompt ) // 4 + len (out ) // 4
337349 self ._cache [key ] = out
350+ if ev is not None :
351+ ev .log ("replay" , "model_call" , kind = kind , cache_hit = False , key = key ,
352+ phase = getattr (self , "evidence_phase" , "" ), backend = self .name ,
353+ model = self .model , prompt = prompt , response = out ,
354+ error = getattr (self , "last_call_error" , "" ) or "" )
338355 return out
339356
340357 # operations -----------------------------------------------------------
@@ -363,16 +380,15 @@ def attempt(self, task: TaskRecord, skill: str, memory: str,
363380 return self ._cached_call (key , prompt , max_tokens = 512 )
364381 # generic path (mined daily-case tasks): neutral, content-filter-safe
365382 # wording. Apply the skill/memory as guidance, not as adversarial
366- # "OVERRIDE everything" directives.
367- prompt = (
368- "Complete the following task for the user. Follow the skill and memory "
369- "guidance below, including any output-format and length requirements. "
370- "When a 'Learned preferences' rule sets an explicit limit (e.g. a length "
371- "cap), prefer that rule over more general advice it refines.\n \n "
372- f"# Skill\n { skill or '(none)' } \n \n # Memory\n { memory or '(none)' } \n \n "
373- f"# Task\n { task .intent } \n \n { task .context_excerpt } \n \n "
374- "Return ONLY the final answer text, nothing else."
375- )
383+ # "OVERRIDE everything" directives. Template lives in the prompt
384+ # registry so the dashboard can display/override it live.
385+ from skillopt_sleep import prompts as prompt_registry
386+ prompt = prompt_registry .render ("attempt" , {
387+ "__SKILL__" : skill or "(none)" ,
388+ "__MEMORY__" : memory or "(none)" ,
389+ "__INTENT__" : task .intent ,
390+ "__CONTEXT__" : task .context_excerpt ,
391+ })
376392 # cache on (task, skill, memory) so identical hold-out re-scoring is free
377393 salt = f"s{ sample_id } :" if sample_id else ""
378394 key = "attempt:" + salt + skill_hash (prompt )
@@ -395,11 +411,11 @@ def judge(self, task: TaskRecord, response: str) -> Tuple[float, float, str]:
395411 if task .reference_kind == "exact" and task .reference :
396412 hard = exact_score (task .reference , response )
397413 return hard , max (hard , keyword_soft_score (task .reference , response )), "exact(local)"
398- prompt = (
399- "Score how well the response satisfies the rubric, 0..1. "
400- 'Return ONLY JSON {"score ": <0..1>, "reason": "..."}. \n \n '
401- f"# Rubric \n { task . reference or task . intent } \n \n # Response \n { response } "
402- )
414+ from skillopt_sleep import prompts as prompt_registry
415+ prompt = prompt_registry . render ( "judge" , {
416+ "__RUBRIC__ " : task . reference or task . intent ,
417+ "__RESPONSE__" : response ,
418+ } )
403419 key = "judge:" + skill_hash (prompt )
404420 raw = self ._cached_call (key , prompt , max_tokens = 200 )
405421 obj = _extract_json (raw , "object" )
@@ -482,39 +498,20 @@ def _explain(c: str) -> str:
482498 # can't ask questions). We surface the benchmark's own rollout system
483499 # prompt (carried on TaskRecord.system) so proposed rules stay in-bounds.
484500 guard_text = _task_guardrail (failures )
485- prompt = (
486- "You are SkillOpt's optimizer. The agent keeps failing the recurring "
487- f"tasks below. Propose at most { edit_budget } bounded edits to the "
488- f"{ target } document so it stops failing. Each edit MUST be a short, "
489- "GENERAL, reusable rule or preference (never task-specific, never an "
490- "answer to a single task). If exact failing criteria are listed, your "
491- "edits MUST make future outputs satisfy every one of them.\n "
492- "BE CONCRETE: quote the exact threshold, section name, or format from "
493- "the criteria verbatim in your rule (e.g. write 'keep the entire "
494- "response under 1200 characters', NOT 'respect length limits'). Vague "
495- "rules do not change behavior; specific numeric/structural rules do.\n "
496- "IMPORTANT: your edits are APPENDED to a 'Learned preferences' block; "
497- "you CANNOT delete the existing instructions above. If the current "
498- f"{ target } text conflicts with a criterion (e.g. it says 'be exhaustive' "
499- "but outputs must be under a character limit), write an explicit, "
500- "forceful OVERRIDE rule stating it supersedes the conflicting "
501- "instruction, and put the hard requirement first.\n "
502- "HARD CONSTRAINT: every rule you write MUST be consistent with the "
503- "'Task output contract' below (if shown). NEVER propose a rule that "
504- "changes the required output format/language, tells the agent to ask "
505- "the user a question, or otherwise violates that contract — such a "
506- "rule scores ZERO because the evaluator cannot honor it.\n "
507- 'Return ONLY a JSON array: '
508- '[{"op":"add|replace|delete","content":"<rule>","anchor":"<text to replace/delete, optional>","rationale":"<why>"}].\n \n '
509- f"# Current { target } \n { cur_doc } \n "
510- f"{ guard_text } "
511- f"{ criteria_text } \n "
512- f"{ pref_text } \n \n "
513- f"# Recurring failures\n { fail_text } "
514- )
501+ from skillopt_sleep import prompts as prompt_registry
502+ prompt = prompt_registry .render ("reflect" , {
503+ "__EDIT_BUDGET__" : str (edit_budget ),
504+ "__TARGET__" : target ,
505+ "__CUR_DOC__" : cur_doc ,
506+ "__GUARD__" : guard_text ,
507+ "__CRITERIA__" : criteria_text ,
508+ "__PREFS__" : pref_text ,
509+ "__FAILURES__" : fail_text ,
510+ })
515511 # Call with one retry: transient non-JSON replies otherwise waste a whole
516512 # night (the gate sees no edits and rejects). A firmer second prompt
517513 # recovers most of these.
514+ ev = getattr (self , "evidence" , None )
518515 arr = None
519516 for attempt in range (2 ):
520517 p = prompt if attempt == 0 else (
@@ -523,6 +520,11 @@ def _explain(c: str) -> str:
523520 )
524521 raw = self ._call (p , max_tokens = 1024 )
525522 self ._tokens += len (p ) // 4 + len (raw ) // 4
523+ if ev is not None :
524+ ev .log ("reflect" , "exchange" , target = target , attempt = attempt + 1 ,
525+ backend = self .name , model = self .model ,
526+ n_failures = len (failures ), prompt = p , raw_reply = raw ,
527+ error = getattr (self , "last_call_error" , "" ) or "" )
526528 arr = _extract_json (raw , "array" )
527529 if isinstance (arr , list ) and arr :
528530 break
0 commit comments