rigorloop 0.1.0__py3-none-any.whl
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.
- rigorloop/__init__.py +18 -0
- rigorloop/_version.py +24 -0
- rigorloop/core/__init__.py +1 -0
- rigorloop/core/config_calcs.py +302 -0
- rigorloop/core/dataset_calcs.py +193 -0
- rigorloop/core/prompt_calcs.py +329 -0
- rigorloop/core/report_calcs.py +194 -0
- rigorloop/core/scoring_calcs.py +264 -0
- rigorloop/core/strategy_calcs.py +507 -0
- rigorloop/core/types.py +649 -0
- rigorloop/py.typed +0 -0
- rigorloop/shell/__init__.py +1 -0
- rigorloop/shell/agent_calls.py +120 -0
- rigorloop/shell/cli.py +1111 -0
- rigorloop/shell/io_actions.py +497 -0
- rigorloop-0.1.0.dist-info/METADATA +224 -0
- rigorloop-0.1.0.dist-info/RECORD +20 -0
- rigorloop-0.1.0.dist-info/WHEEL +4 -0
- rigorloop-0.1.0.dist-info/entry_points.txt +2 -0
- rigorloop-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
"""Pure strategy logic: context assembly, agent-reply parsing, validation
|
|
2
|
+
cadence, stopping rules, champion selection, and the dev leaderboard."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import random
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
from rigorloop.core.scoring_calcs import example_passed, significantly_better
|
|
11
|
+
from rigorloop.core.types import (
|
|
12
|
+
NOTHING,
|
|
13
|
+
BudgetExhausted,
|
|
14
|
+
ChampionArtifact,
|
|
15
|
+
ContinueDecision,
|
|
16
|
+
DevExample,
|
|
17
|
+
Directive,
|
|
18
|
+
DirectiveSpec,
|
|
19
|
+
Err,
|
|
20
|
+
ExampleResult,
|
|
21
|
+
ExecutionFailed,
|
|
22
|
+
ExecutionOk,
|
|
23
|
+
FailureSample,
|
|
24
|
+
JudgeVerdict,
|
|
25
|
+
LeaderboardEntry,
|
|
26
|
+
MalformedReply,
|
|
27
|
+
Nothing,
|
|
28
|
+
Ok,
|
|
29
|
+
Option,
|
|
30
|
+
Passed,
|
|
31
|
+
Result,
|
|
32
|
+
RunConfig,
|
|
33
|
+
RunState,
|
|
34
|
+
Some,
|
|
35
|
+
StopReason,
|
|
36
|
+
StopRequested,
|
|
37
|
+
StrategyContext,
|
|
38
|
+
StrategyDecision,
|
|
39
|
+
StrategyLogEntry,
|
|
40
|
+
StrategyUnresponsive,
|
|
41
|
+
TargetReached,
|
|
42
|
+
ValCheckpoint,
|
|
43
|
+
ValidatedCandidate,
|
|
44
|
+
ValidationPlateau,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ALPHA = 0.05
|
|
48
|
+
MAX_FALLBACKS = 2
|
|
49
|
+
_LEADERBOARD_TOP = 8
|
|
50
|
+
_FAILURE_SAMPLE_LIMIT = 3
|
|
51
|
+
_GAP_WARN = 0.15
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def initial_state() -> RunState:
|
|
55
|
+
return RunState(
|
|
56
|
+
loops_completed=0,
|
|
57
|
+
leaderboard=(),
|
|
58
|
+
strategy_log=(),
|
|
59
|
+
val_champion=NOTHING,
|
|
60
|
+
checkpoints=(),
|
|
61
|
+
peeks_used=0,
|
|
62
|
+
last_peek_loop=NOTHING,
|
|
63
|
+
consecutive_fallbacks=0,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# --------------------------------------------------------------------------
|
|
68
|
+
# Deterministic dev-subset sampling
|
|
69
|
+
# --------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def sample_dev_subset(
|
|
73
|
+
dev: tuple[DevExample, ...], k: int, seed: int, loop_index: int
|
|
74
|
+
) -> tuple[DevExample, ...]:
|
|
75
|
+
"""Resampled every loop, deterministically from the injected seed and the
|
|
76
|
+
loop index, so score movement isn't over-attributed to directives."""
|
|
77
|
+
if k >= len(dev):
|
|
78
|
+
return dev
|
|
79
|
+
rng = random.Random(seed * 1_000_003 + loop_index) # pure function of its inputs
|
|
80
|
+
return tuple(rng.sample(dev, k=k))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# --------------------------------------------------------------------------
|
|
84
|
+
# Agent-reply parsing (strategy JSON, executor fenced block, judge JSON)
|
|
85
|
+
# --------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _json_candidates(text: str) -> tuple[str, ...]:
|
|
89
|
+
fenced = tuple(re.findall(r"```(?:json)?\s*\n(.*?)```", text, re.DOTALL))
|
|
90
|
+
start, end = text.find("{"), text.rfind("}")
|
|
91
|
+
braced = (text[start : end + 1],) if 0 <= start < end else ()
|
|
92
|
+
return (text.strip(), *fenced, *braced)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _extract_json_object(text: str) -> Result[dict[str, object], MalformedReply]:
|
|
96
|
+
def try_load(chunk: str) -> dict[str, object] | None:
|
|
97
|
+
try:
|
|
98
|
+
obj = json.loads(chunk)
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
return None
|
|
101
|
+
return obj if isinstance(obj, dict) else None
|
|
102
|
+
|
|
103
|
+
loaded = next((o for o in map(try_load, _json_candidates(text)) if o is not None), None)
|
|
104
|
+
return Ok(loaded) if loaded is not None else Err(MalformedReply("no JSON object found"))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _parse_directive_spec(index: int, raw: object) -> Result[DirectiveSpec, MalformedReply]:
|
|
108
|
+
if not isinstance(raw, dict):
|
|
109
|
+
return Err(MalformedReply(f"directives[{index}] is not an object"))
|
|
110
|
+
summary = raw.get("approach_summary")
|
|
111
|
+
instructions = raw.get("instructions")
|
|
112
|
+
base = raw.get("base_on_champion", False)
|
|
113
|
+
if not isinstance(summary, str) or not summary.strip():
|
|
114
|
+
return Err(MalformedReply(f"directives[{index}].approach_summary missing or empty"))
|
|
115
|
+
if not isinstance(instructions, str) or not instructions.strip():
|
|
116
|
+
return Err(MalformedReply(f"directives[{index}].instructions missing or empty"))
|
|
117
|
+
if not isinstance(base, bool):
|
|
118
|
+
return Err(MalformedReply(f"directives[{index}].base_on_champion must be a boolean"))
|
|
119
|
+
return Ok(DirectiveSpec(summary.strip(), instructions.strip(), base))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def parse_strategy_reply(text: str) -> Result[StrategyDecision, MalformedReply]:
|
|
123
|
+
obj = _extract_json_object(text)
|
|
124
|
+
if isinstance(obj, Err):
|
|
125
|
+
return obj
|
|
126
|
+
data = obj.value
|
|
127
|
+
action = data.get("action")
|
|
128
|
+
if action == "stop":
|
|
129
|
+
reason = data.get("reason", data.get("stop_reason", ""))
|
|
130
|
+
return Ok(StopRequested(reason if isinstance(reason, str) else ""))
|
|
131
|
+
if action != "continue":
|
|
132
|
+
return Err(MalformedReply(f"action must be 'continue' or 'stop', got {action!r}"))
|
|
133
|
+
raw_directives = data.get("directives")
|
|
134
|
+
if not isinstance(raw_directives, list) or not raw_directives:
|
|
135
|
+
return Err(MalformedReply("continue decision needs a non-empty 'directives' array"))
|
|
136
|
+
parsed = [_parse_directive_spec(i, d) for i, d in enumerate(raw_directives)]
|
|
137
|
+
failures = [p for p in parsed if isinstance(p, Err)]
|
|
138
|
+
if failures:
|
|
139
|
+
return failures[0]
|
|
140
|
+
observations = data.get("observations", "")
|
|
141
|
+
hypotheses = data.get("hypotheses", "")
|
|
142
|
+
request_validation = data.get("request_validation", False)
|
|
143
|
+
if not isinstance(request_validation, bool):
|
|
144
|
+
return Err(MalformedReply("'request_validation' must be a boolean"))
|
|
145
|
+
return Ok(
|
|
146
|
+
ContinueDecision(
|
|
147
|
+
observations=observations if isinstance(observations, str) else "",
|
|
148
|
+
hypotheses=hypotheses if isinstance(hypotheses, str) else "",
|
|
149
|
+
directive_specs=tuple(p.value for p in parsed if isinstance(p, Ok)),
|
|
150
|
+
request_validation=request_validation,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def parse_executor_reply(text: str) -> Result[str, MalformedReply]:
|
|
156
|
+
"""The output contract: exactly one fenced block tagged `solution`."""
|
|
157
|
+
markers = text.count("```solution")
|
|
158
|
+
if markers == 0:
|
|
159
|
+
return Err(MalformedReply("no ```solution fenced block found"))
|
|
160
|
+
if markers > 1:
|
|
161
|
+
return Err(MalformedReply(f"expected exactly one ```solution block, found {markers}"))
|
|
162
|
+
# The block must close with the LAST fence in the reply, so solution
|
|
163
|
+
# content may itself contain fenced code blocks.
|
|
164
|
+
match = re.search(r"```solution[^\n]*\n(.*)\n```\s*\Z", text, re.DOTALL)
|
|
165
|
+
if match is None:
|
|
166
|
+
return Err(MalformedReply("```solution block is not terminated at the end of the reply"))
|
|
167
|
+
content = match.group(1)
|
|
168
|
+
if not content.strip():
|
|
169
|
+
return Err(MalformedReply("```solution block is empty"))
|
|
170
|
+
return Ok(content)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def parse_judge_reply(text: str) -> Result[JudgeVerdict, MalformedReply]:
|
|
174
|
+
obj = _extract_json_object(text)
|
|
175
|
+
if isinstance(obj, Err):
|
|
176
|
+
return obj
|
|
177
|
+
verdict = obj.value.get("pass", obj.value.get("passed"))
|
|
178
|
+
if not isinstance(verdict, bool):
|
|
179
|
+
return Err(MalformedReply("judge reply needs a boolean 'pass' field"))
|
|
180
|
+
reason = obj.value.get("reason", "")
|
|
181
|
+
return Ok(JudgeVerdict(verdict, reason if isinstance(reason, str) else ""))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --------------------------------------------------------------------------
|
|
185
|
+
# Directives and the fallback path
|
|
186
|
+
# --------------------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def build_directives(
|
|
190
|
+
specs: tuple[DirectiveSpec, ...],
|
|
191
|
+
champion: Option[ChampionArtifact],
|
|
192
|
+
loop_index: int,
|
|
193
|
+
max_directives: int,
|
|
194
|
+
) -> tuple[Directive, ...]:
|
|
195
|
+
"""Attach the champion artifact where requested. The artifact is the one
|
|
196
|
+
sanctioned carry-forward channel: solution content only."""
|
|
197
|
+
|
|
198
|
+
def build(index: int, spec: DirectiveSpec) -> Directive:
|
|
199
|
+
base: Option[ChampionArtifact] = champion if spec.base_on_champion else NOTHING
|
|
200
|
+
return Directive(
|
|
201
|
+
directive_id=f"L{loop_index}-d{index}",
|
|
202
|
+
approach_summary=spec.approach_summary,
|
|
203
|
+
instructions=spec.instructions,
|
|
204
|
+
base=base,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return tuple(build(i, spec) for i, spec in enumerate(specs[:max_directives], start=1))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def fallback_decision(has_champion: bool) -> ContinueDecision:
|
|
211
|
+
"""Substituted when the strategy agent's reply cannot be parsed twice."""
|
|
212
|
+
spec = (
|
|
213
|
+
DirectiveSpec(
|
|
214
|
+
approach_summary="Refine the current champion solution",
|
|
215
|
+
instructions=(
|
|
216
|
+
"Carefully review the provided current best solution and produce an "
|
|
217
|
+
"improved version: fix edge cases, tighten output formatting, and "
|
|
218
|
+
"keep whatever already works."
|
|
219
|
+
),
|
|
220
|
+
base_on_champion=True,
|
|
221
|
+
)
|
|
222
|
+
if has_champion
|
|
223
|
+
else DirectiveSpec(
|
|
224
|
+
approach_summary="Produce a solid first solution",
|
|
225
|
+
instructions=(
|
|
226
|
+
"Read the task description and the example inputs and outputs "
|
|
227
|
+
"closely, then produce a careful, straightforward solution."
|
|
228
|
+
),
|
|
229
|
+
base_on_champion=False,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
return ContinueDecision(
|
|
233
|
+
observations="(fallback: strategy reply was malformed)",
|
|
234
|
+
hypotheses="",
|
|
235
|
+
directive_specs=(spec,),
|
|
236
|
+
request_validation=False,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# --------------------------------------------------------------------------
|
|
241
|
+
# Leaderboard
|
|
242
|
+
# --------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def fold_leaderboard(
|
|
246
|
+
leaderboard: tuple[LeaderboardEntry, ...], new_entries: tuple[LeaderboardEntry, ...]
|
|
247
|
+
) -> tuple[LeaderboardEntry, ...]:
|
|
248
|
+
"""Ranked by pass rate (stable: earlier candidates win ties)."""
|
|
249
|
+
merged = (*leaderboard, *new_entries)
|
|
250
|
+
return tuple(sorted(merged, key=lambda e: (-e.score.pass_rate, e.loop_index, e.candidate_id)))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def dev_best(leaderboard: tuple[LeaderboardEntry, ...]) -> Option[LeaderboardEntry]:
|
|
254
|
+
return Some(leaderboard[0]) if leaderboard else NOTHING
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def champion_artifact(entry: LeaderboardEntry) -> ChampionArtifact:
|
|
258
|
+
return ChampionArtifact(entry.candidate_id, entry.kind, entry.content)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def beats_previous_best(
|
|
262
|
+
previous_best: Option[LeaderboardEntry], challenger: LeaderboardEntry
|
|
263
|
+
) -> bool:
|
|
264
|
+
"""CI-band-gated: a raw uptick is not an improvement."""
|
|
265
|
+
match previous_best:
|
|
266
|
+
case Nothing():
|
|
267
|
+
return True
|
|
268
|
+
case Some(best):
|
|
269
|
+
return significantly_better(challenger.score.pass_vector, best.score.pass_vector, ALPHA)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def render_leaderboard_lines(
|
|
273
|
+
leaderboard: tuple[LeaderboardEntry, ...], top: int = _LEADERBOARD_TOP
|
|
274
|
+
) -> tuple[str, ...]:
|
|
275
|
+
"""Aggregate scores only; differences within noise of the best are marked
|
|
276
|
+
so the strategy agent doesn't chase them."""
|
|
277
|
+
if not leaderboard:
|
|
278
|
+
return ()
|
|
279
|
+
best = leaderboard[0]
|
|
280
|
+
|
|
281
|
+
def line(rank: int, e: LeaderboardEntry) -> str:
|
|
282
|
+
score = e.score
|
|
283
|
+
within_noise = e is not best and not significantly_better(
|
|
284
|
+
best.score.pass_vector, score.pass_vector, ALPHA
|
|
285
|
+
)
|
|
286
|
+
flags = (" — not statistically distinguishable from best" if within_noise else "") + (
|
|
287
|
+
" — evaluation aborted early; missing examples count as failures"
|
|
288
|
+
if score.eval_aborted
|
|
289
|
+
else ""
|
|
290
|
+
)
|
|
291
|
+
return (
|
|
292
|
+
f"{rank}. {e.candidate_id} (loop {e.loop_index}): "
|
|
293
|
+
f"{score.pass_rate:.1%} [{score.ci_low:.1%}, {score.ci_high:.1%}] "
|
|
294
|
+
f"on n={score.n}{flags}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
return tuple(line(i, e) for i, e in enumerate(leaderboard[:top], start=1))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# --------------------------------------------------------------------------
|
|
301
|
+
# Failure patterns (dev-only, for the strategy context)
|
|
302
|
+
# --------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def failure_samples(
|
|
306
|
+
dev: tuple[DevExample, ...],
|
|
307
|
+
results: tuple[ExampleResult, ...],
|
|
308
|
+
limit: int = _FAILURE_SAMPLE_LIMIT,
|
|
309
|
+
) -> tuple[FailureSample, ...]:
|
|
310
|
+
by_id = {d.example.example_id: d for d in dev}
|
|
311
|
+
|
|
312
|
+
def sample(result: ExampleResult) -> FailureSample:
|
|
313
|
+
match result.execution:
|
|
314
|
+
case ExecutionOk(output_text):
|
|
315
|
+
actual = output_text
|
|
316
|
+
case ExecutionFailed(detail):
|
|
317
|
+
actual = f"(no output: {detail})"
|
|
318
|
+
return FailureSample(
|
|
319
|
+
dev_example=by_id[result.example_id],
|
|
320
|
+
actual_output=actual,
|
|
321
|
+
failed_checks=tuple(
|
|
322
|
+
o.check_name for o in result.outcomes if not isinstance(o.outcome, Passed)
|
|
323
|
+
),
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
failing = tuple(r for r in results if not example_passed(r) and r.example_id in by_id)
|
|
327
|
+
return tuple(sample(r) for r in failing[:limit])
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# --------------------------------------------------------------------------
|
|
331
|
+
# Validation cadence and champion selection
|
|
332
|
+
# --------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def should_validate(
|
|
336
|
+
state: RunState,
|
|
337
|
+
config: RunConfig,
|
|
338
|
+
strategy_requested: bool,
|
|
339
|
+
new_best_significant: bool,
|
|
340
|
+
) -> bool:
|
|
341
|
+
"""Peeks are budgeted; triggered peeks respect a minimum gap so early easy
|
|
342
|
+
wins can't front-load the budget."""
|
|
343
|
+
best = dev_best(state.leaderboard)
|
|
344
|
+
if isinstance(best, Nothing) or state.peeks_used >= config.validation.max_peeks:
|
|
345
|
+
return False
|
|
346
|
+
already_validated = {c.candidate_id for c in state.checkpoints}
|
|
347
|
+
if best.value.candidate_id in already_validated:
|
|
348
|
+
return False
|
|
349
|
+
scheduled = state.loops_completed % config.validation.val_every == 0
|
|
350
|
+
match state.last_peek_loop:
|
|
351
|
+
case Nothing():
|
|
352
|
+
gap_ok = True
|
|
353
|
+
case Some(last):
|
|
354
|
+
gap_ok = state.loops_completed - last >= config.validation.min_loops_between_peeks
|
|
355
|
+
triggered = (strategy_requested or new_best_significant) and gap_ok
|
|
356
|
+
return scheduled or triggered
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def select_val_champion(
|
|
360
|
+
incumbent: Option[ValidatedCandidate], challenger: ValidatedCandidate
|
|
361
|
+
) -> tuple[ValidatedCandidate, bool]:
|
|
362
|
+
"""Noise-aware selection: (winner, improvement).
|
|
363
|
+
|
|
364
|
+
A challenger displaces the incumbent only beyond the paired-test noise
|
|
365
|
+
band; within the band the dev score breaks the tie (without counting as
|
|
366
|
+
improvement for the plateau rule)."""
|
|
367
|
+
match incumbent:
|
|
368
|
+
case Nothing():
|
|
369
|
+
return challenger, True
|
|
370
|
+
case Some(current):
|
|
371
|
+
if significantly_better(
|
|
372
|
+
challenger.val_score.pass_vector, current.val_score.pass_vector, ALPHA
|
|
373
|
+
):
|
|
374
|
+
return challenger, True
|
|
375
|
+
if significantly_better(
|
|
376
|
+
current.val_score.pass_vector, challenger.val_score.pass_vector, ALPHA
|
|
377
|
+
):
|
|
378
|
+
return current, False
|
|
379
|
+
tie_break = challenger.dev_score.pass_rate > current.dev_score.pass_rate
|
|
380
|
+
return (challenger, False) if tie_break else (current, False)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
# --------------------------------------------------------------------------
|
|
384
|
+
# Stopping rules
|
|
385
|
+
# --------------------------------------------------------------------------
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def stopping_decision(state: RunState, config: RunConfig) -> Option[StopReason]:
|
|
389
|
+
"""Checked after each loop's bookkeeping. Strategy-requested stops and
|
|
390
|
+
unresponsiveness are handled where they arise; this covers the rest."""
|
|
391
|
+
if state.consecutive_fallbacks >= MAX_FALLBACKS:
|
|
392
|
+
return Some(StrategyUnresponsive(state.consecutive_fallbacks))
|
|
393
|
+
match config.validation.target_pass_rate, state.val_champion:
|
|
394
|
+
case Some(target), Some(champion):
|
|
395
|
+
if champion.val_score.pass_rate >= target:
|
|
396
|
+
return Some(TargetReached(champion.val_score.pass_rate))
|
|
397
|
+
case _, _:
|
|
398
|
+
pass
|
|
399
|
+
patience = config.validation.patience
|
|
400
|
+
recent = state.checkpoints[-patience:]
|
|
401
|
+
if len(recent) == patience and all(not c.displaced_champion for c in recent):
|
|
402
|
+
return Some(ValidationPlateau(patience))
|
|
403
|
+
if state.loops_completed >= config.loop.max_loops:
|
|
404
|
+
return Some(BudgetExhausted(config.loop.max_loops))
|
|
405
|
+
return NOTHING
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# --------------------------------------------------------------------------
|
|
409
|
+
# Strategy log and context assembly
|
|
410
|
+
# --------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def dev_summary_line(n_scored: int, n_malformed: int, best: Option[LeaderboardEntry]) -> str:
|
|
414
|
+
best_text = ""
|
|
415
|
+
match best:
|
|
416
|
+
case Some(entry):
|
|
417
|
+
best_text = (
|
|
418
|
+
f"; loop best {entry.candidate_id} at {entry.score.pass_rate:.1%} "
|
|
419
|
+
f"[{entry.score.ci_low:.1%}, {entry.score.ci_high:.1%}]"
|
|
420
|
+
)
|
|
421
|
+
case Nothing():
|
|
422
|
+
best_text = ""
|
|
423
|
+
malformed_text = f"; {n_malformed} malformed candidate(s)" if n_malformed else ""
|
|
424
|
+
return f"{n_scored} candidate(s) scored{malformed_text}{best_text}"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def val_summary_line(checkpoint: ValCheckpoint) -> str:
|
|
428
|
+
gap = checkpoint.dev_pass_rate - checkpoint.val_pass_rate
|
|
429
|
+
displaced = "new champion" if checkpoint.displaced_champion else "champion unchanged"
|
|
430
|
+
return (
|
|
431
|
+
f"validated {checkpoint.candidate_id}: {checkpoint.val_pass_rate:.1%} on validation "
|
|
432
|
+
f"(dev {checkpoint.dev_pass_rate:.1%}, gap {gap:+.1%}); {displaced}"
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def compact_log_line(entry: StrategyLogEntry) -> str:
|
|
437
|
+
approaches = "; ".join(d.approach_summary for d in entry.directives)
|
|
438
|
+
val_text = ""
|
|
439
|
+
match entry.val_summary:
|
|
440
|
+
case Some(summary):
|
|
441
|
+
val_text = f" | {summary}"
|
|
442
|
+
case Nothing():
|
|
443
|
+
val_text = ""
|
|
444
|
+
return f"loop {entry.loop_index}: [{approaches}] | {entry.dev_summary}{val_text}"
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def assemble_strategy_context(
|
|
448
|
+
task_description: str,
|
|
449
|
+
config: RunConfig,
|
|
450
|
+
state: RunState,
|
|
451
|
+
samples: tuple[FailureSample, ...],
|
|
452
|
+
check_names: tuple[str, ...],
|
|
453
|
+
) -> StrategyContext:
|
|
454
|
+
"""Full detail for the most recent loops, compact lines beyond that, the
|
|
455
|
+
dev leaderboard with CIs, dev failure patterns, the champion's content,
|
|
456
|
+
and aggregate validation scores. Nothing else."""
|
|
457
|
+
detail = config.loop.strategy_full_detail_loops
|
|
458
|
+
recent = state.strategy_log[-detail:]
|
|
459
|
+
compacted = tuple(compact_log_line(e) for e in state.strategy_log[:-detail])
|
|
460
|
+
|
|
461
|
+
best = dev_best(state.leaderboard)
|
|
462
|
+
champion: Option[ChampionArtifact] = NOTHING
|
|
463
|
+
champion_dev_line: Option[str] = NOTHING
|
|
464
|
+
match best:
|
|
465
|
+
case Some(entry):
|
|
466
|
+
champion = Some(champion_artifact(entry))
|
|
467
|
+
champion_dev_line = Some(
|
|
468
|
+
f"{entry.candidate_id}: dev {entry.score.pass_rate:.1%} "
|
|
469
|
+
f"[{entry.score.ci_low:.1%}, {entry.score.ci_high:.1%}] on n={entry.score.n}"
|
|
470
|
+
)
|
|
471
|
+
case Nothing():
|
|
472
|
+
pass
|
|
473
|
+
|
|
474
|
+
val_lines = tuple(val_summary_line(c) for c in state.checkpoints)
|
|
475
|
+
gap_warning: Option[str] = NOTHING
|
|
476
|
+
if state.checkpoints:
|
|
477
|
+
last = state.checkpoints[-1]
|
|
478
|
+
gap = last.dev_pass_rate - last.val_pass_rate
|
|
479
|
+
if gap > _GAP_WARN:
|
|
480
|
+
gap_warning = Some(
|
|
481
|
+
f"WARNING: dev-val gap is {gap:+.1%} — the loop may be overfitting "
|
|
482
|
+
"to the dev set. Prefer simpler, more general approaches."
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return StrategyContext(
|
|
486
|
+
task_description=task_description,
|
|
487
|
+
solution_kind=config.task.solution_kind,
|
|
488
|
+
loops_completed=state.loops_completed,
|
|
489
|
+
max_loops=config.loop.max_loops,
|
|
490
|
+
executors_per_loop=config.loop.executors_per_loop,
|
|
491
|
+
check_names=check_names,
|
|
492
|
+
recent_log=recent,
|
|
493
|
+
compacted_log=compacted,
|
|
494
|
+
leaderboard_lines=render_leaderboard_lines(state.leaderboard),
|
|
495
|
+
failure_samples=samples,
|
|
496
|
+
champion=champion,
|
|
497
|
+
champion_dev_line=champion_dev_line,
|
|
498
|
+
val_lines=val_lines,
|
|
499
|
+
dev_val_gap_warning=gap_warning,
|
|
500
|
+
peeks_used=state.peeks_used,
|
|
501
|
+
max_peeks=config.validation.max_peeks,
|
|
502
|
+
dev_subset_note=(
|
|
503
|
+
"Executor agents see a dev-example subset that is resampled every loop; "
|
|
504
|
+
"some loop-to-loop score movement is sample luck, and differences marked "
|
|
505
|
+
"as within noise should not be chased."
|
|
506
|
+
),
|
|
507
|
+
)
|