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,329 @@
|
|
|
1
|
+
"""Pure prompt-string builders, split into two typed channels.
|
|
2
|
+
|
|
3
|
+
Agent-context builders (strategy, executor) accept Dev-typed examples and
|
|
4
|
+
aggregate scores only — passing val/test examples is a type error. Evaluation
|
|
5
|
+
builders (solution-under-test, judge) run ONE example of any split and return
|
|
6
|
+
their output to the harness as data, never into an agent-context prompt."""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from rigorloop.core.types import (
|
|
11
|
+
AgentContextPrompt,
|
|
12
|
+
Check,
|
|
13
|
+
CustomPython,
|
|
14
|
+
DevExample,
|
|
15
|
+
Directive,
|
|
16
|
+
EvalPrompt,
|
|
17
|
+
ExactMatch,
|
|
18
|
+
Example,
|
|
19
|
+
ExecutorRole,
|
|
20
|
+
GuidanceSolution,
|
|
21
|
+
JsonEquality,
|
|
22
|
+
LlmJudge,
|
|
23
|
+
NormalizedMatch,
|
|
24
|
+
Nothing,
|
|
25
|
+
NumericTolerance,
|
|
26
|
+
RegexMatch,
|
|
27
|
+
ScriptSolution,
|
|
28
|
+
SkillSolution,
|
|
29
|
+
SolutionKind,
|
|
30
|
+
Some,
|
|
31
|
+
StrategyContext,
|
|
32
|
+
StrategyLogEntry,
|
|
33
|
+
StrategyRole,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_MAX_EXAMPLE_CHARS = 1500
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def kind_label(kind: SolutionKind) -> str:
|
|
40
|
+
match kind:
|
|
41
|
+
case ScriptSolution():
|
|
42
|
+
return "an executable Python script"
|
|
43
|
+
case SkillSolution():
|
|
44
|
+
return "a skill markdown file for an AI agent (SKILL.md style)"
|
|
45
|
+
case GuidanceSolution():
|
|
46
|
+
return "a guidance markdown file for AI agents (AGENTS.md / CLAUDE.md style)"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def describe_check(check: Check) -> str:
|
|
50
|
+
match check:
|
|
51
|
+
case ExactMatch():
|
|
52
|
+
return "exact_match: the output must equal the expected output exactly"
|
|
53
|
+
case NormalizedMatch(lowercase, strip, collapse):
|
|
54
|
+
rules = ", ".join(
|
|
55
|
+
name
|
|
56
|
+
for name, on in (
|
|
57
|
+
("lowercased", lowercase),
|
|
58
|
+
("stripped", strip),
|
|
59
|
+
("whitespace-collapsed", collapse),
|
|
60
|
+
)
|
|
61
|
+
if on
|
|
62
|
+
)
|
|
63
|
+
return f"normalized_match: outputs must match after being {rules}"
|
|
64
|
+
case JsonEquality():
|
|
65
|
+
return "json_equality: the output must parse as JSON equal to the expected output"
|
|
66
|
+
case RegexMatch(pattern):
|
|
67
|
+
return f"regex_match: the output must contain a match for the pattern {pattern!r}"
|
|
68
|
+
case NumericTolerance(atol, rtol):
|
|
69
|
+
return (
|
|
70
|
+
f"numeric_tolerance: the output must be a number within atol={atol}, "
|
|
71
|
+
f"rtol={rtol} of the expected output"
|
|
72
|
+
)
|
|
73
|
+
case CustomPython(script_path):
|
|
74
|
+
return f"custom_python: a user-supplied checker script ({script_path}) must accept it"
|
|
75
|
+
case LlmJudge(rubric, n_samples, pass_threshold):
|
|
76
|
+
return (
|
|
77
|
+
f"llm_judge (majority of {n_samples} samples, threshold {pass_threshold}): {rubric}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _clip(text: str, limit: int = _MAX_EXAMPLE_CHARS) -> str:
|
|
82
|
+
return text if len(text) <= limit else text[: limit - 1] + "…"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _render_dev_example(index: int, dev: DevExample) -> str:
|
|
86
|
+
return (
|
|
87
|
+
f"### Example {index}\n"
|
|
88
|
+
f"Input:\n{_clip(dev.example.input_text)}\n"
|
|
89
|
+
f"Expected output:\n{_clip(dev.example.expected_output)}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def render_directive(directive: Directive) -> str:
|
|
94
|
+
"""Directives carry champion solution CONTENT only — never scores,
|
|
95
|
+
mistakes, or per-example failures from prior loops."""
|
|
96
|
+
base_text = ""
|
|
97
|
+
match directive.base:
|
|
98
|
+
case Some(artifact):
|
|
99
|
+
base_text = (
|
|
100
|
+
"\n\nStart from this existing solution and refine it "
|
|
101
|
+
"(keep what works, change what the instructions call out):\n"
|
|
102
|
+
f"<current-solution>\n{artifact.content}\n</current-solution>"
|
|
103
|
+
)
|
|
104
|
+
case Nothing():
|
|
105
|
+
base_text = ""
|
|
106
|
+
return (
|
|
107
|
+
f"Approach: {directive.approach_summary}\nInstructions: {directive.instructions}{base_text}"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# --------------------------------------------------------------------------
|
|
112
|
+
# Agent-context channel
|
|
113
|
+
# --------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _output_contract(kind: SolutionKind) -> str:
|
|
117
|
+
artifact = {
|
|
118
|
+
"script": (
|
|
119
|
+
"a single self-contained Python 3 script that reads the raw input text "
|
|
120
|
+
"from stdin and writes exactly the required output to stdout (no prompts, "
|
|
121
|
+
"no logging, no extra whitespace). Standard library only."
|
|
122
|
+
),
|
|
123
|
+
"skill": (
|
|
124
|
+
"a complete skill markdown document that tells an AI agent, precisely and "
|
|
125
|
+
"step by step, how to transform an input of this kind into the required "
|
|
126
|
+
"output. The agent following it will reply with the output only."
|
|
127
|
+
),
|
|
128
|
+
"guidance": (
|
|
129
|
+
"a complete guidance markdown document (AGENTS.md / CLAUDE.md style) that "
|
|
130
|
+
"steers an AI agent to transform an input of this kind into the required "
|
|
131
|
+
"output. The agent following it will reply with the output only."
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
key = (
|
|
135
|
+
"script"
|
|
136
|
+
if isinstance(kind, ScriptSolution)
|
|
137
|
+
else "skill"
|
|
138
|
+
if isinstance(kind, SkillSolution)
|
|
139
|
+
else "guidance"
|
|
140
|
+
)
|
|
141
|
+
return (
|
|
142
|
+
f"Produce {artifact[key]}\n\n"
|
|
143
|
+
"Reply with EXACTLY ONE fenced code block tagged `solution`, and make it the "
|
|
144
|
+
"last thing in your reply:\n"
|
|
145
|
+
"```solution\n<your full artifact here>\n```\n"
|
|
146
|
+
"Do not put anything after the closing fence. Do not use a ```solution fence "
|
|
147
|
+
"anywhere else in your reply."
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def build_executor_prompt(
|
|
152
|
+
task_description: str,
|
|
153
|
+
kind: SolutionKind,
|
|
154
|
+
directive: Directive,
|
|
155
|
+
checks: tuple[Check, ...],
|
|
156
|
+
dev_sample: tuple[DevExample, ...],
|
|
157
|
+
) -> AgentContextPrompt:
|
|
158
|
+
"""Executors see the task, the current directive, the checks, and a dev
|
|
159
|
+
sample. Nothing else about prior loops (leakage guarantee)."""
|
|
160
|
+
checks_text = "\n".join(f"- {describe_check(c)}" for c in checks)
|
|
161
|
+
examples_text = "\n\n".join(
|
|
162
|
+
_render_dev_example(i, d) for i, d in enumerate(dev_sample, start=1)
|
|
163
|
+
)
|
|
164
|
+
text = (
|
|
165
|
+
"You are an executor agent inside RigorLoop, an iterative build framework. "
|
|
166
|
+
"Your job is to produce one candidate solution for the task below.\n\n"
|
|
167
|
+
f"# Task\n{task_description}\n\n"
|
|
168
|
+
f"# Directive for this attempt\n{render_directive(directive)}\n\n"
|
|
169
|
+
f"# How the solution will be verified (every check must pass per example)\n"
|
|
170
|
+
f"{checks_text}\n\n"
|
|
171
|
+
f"# Representative examples\n{examples_text}\n\n"
|
|
172
|
+
f"# Required output\n{_output_contract(kind)}"
|
|
173
|
+
)
|
|
174
|
+
return AgentContextPrompt(role=ExecutorRole(), text=text)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _render_log_entry(entry: StrategyLogEntry) -> str:
|
|
178
|
+
directives = "\n".join(
|
|
179
|
+
f" {i}. {d.approach_summary}"
|
|
180
|
+
+ (" (based on champion)" if isinstance(d.base, Some) else "")
|
|
181
|
+
for i, d in enumerate(entry.directives, start=1)
|
|
182
|
+
)
|
|
183
|
+
val_text = ""
|
|
184
|
+
match entry.val_summary:
|
|
185
|
+
case Some(summary):
|
|
186
|
+
val_text = f"\n- validation: {summary}"
|
|
187
|
+
case Nothing():
|
|
188
|
+
val_text = ""
|
|
189
|
+
fallback_text = (
|
|
190
|
+
"\n- (this loop used a fallback directive: your reply was malformed)"
|
|
191
|
+
if entry.fallback
|
|
192
|
+
else ""
|
|
193
|
+
)
|
|
194
|
+
return (
|
|
195
|
+
f"## Loop {entry.loop_index}\n"
|
|
196
|
+
f"- observations: {entry.observations}\n"
|
|
197
|
+
f"- hypotheses: {entry.hypotheses}\n"
|
|
198
|
+
f"- directives issued:\n{directives}\n"
|
|
199
|
+
f"- dev results: {entry.dev_summary}{val_text}{fallback_text}"
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def build_strategy_prompt(context: StrategyContext) -> AgentContextPrompt:
|
|
204
|
+
checks_text = "\n".join(f"- {name}" for name in context.check_names)
|
|
205
|
+
compacted = (
|
|
206
|
+
"\n".join(f"- {line}" for line in context.compacted_log)
|
|
207
|
+
if context.compacted_log
|
|
208
|
+
else "(none)"
|
|
209
|
+
)
|
|
210
|
+
recent = (
|
|
211
|
+
"\n\n".join(_render_log_entry(e) for e in context.recent_log)
|
|
212
|
+
if context.recent_log
|
|
213
|
+
else "(no loops yet — this is the first loop; propose diverse initial approaches)"
|
|
214
|
+
)
|
|
215
|
+
leaderboard = "\n".join(context.leaderboard_lines) if context.leaderboard_lines else "(empty)"
|
|
216
|
+
champion_text = "(none yet)"
|
|
217
|
+
match context.champion, context.champion_dev_line:
|
|
218
|
+
case Some(artifact), Some(dev_line):
|
|
219
|
+
champion_text = (
|
|
220
|
+
f"{dev_line}\n<current-champion>\n{artifact.content}\n</current-champion>"
|
|
221
|
+
)
|
|
222
|
+
case _, _:
|
|
223
|
+
pass
|
|
224
|
+
failures = (
|
|
225
|
+
"\n\n".join(
|
|
226
|
+
f"- Input:\n{_clip(s.dev_example.example.input_text)}\n"
|
|
227
|
+
f" Expected:\n{_clip(s.dev_example.example.expected_output)}\n"
|
|
228
|
+
f" Actual:\n{_clip(s.actual_output)}\n"
|
|
229
|
+
f" Failed checks: {', '.join(s.failed_checks)}"
|
|
230
|
+
for s in context.failure_samples
|
|
231
|
+
)
|
|
232
|
+
if context.failure_samples
|
|
233
|
+
else "(none collected)"
|
|
234
|
+
)
|
|
235
|
+
val_text = "\n".join(context.val_lines) if context.val_lines else "(no checkpoints yet)"
|
|
236
|
+
gap_text = ""
|
|
237
|
+
match context.dev_val_gap_warning:
|
|
238
|
+
case Some(warning):
|
|
239
|
+
gap_text = f"\n{warning}"
|
|
240
|
+
case Nothing():
|
|
241
|
+
gap_text = ""
|
|
242
|
+
|
|
243
|
+
text = (
|
|
244
|
+
"You are the strategy agent of RigorLoop, an iterative agentic build framework. "
|
|
245
|
+
"Each loop you review results on the DEV set and direct a pool of executor "
|
|
246
|
+
"agents. Executors are stateless: they see only your directive, the task, the "
|
|
247
|
+
"checks, and a dev sample — never scores or prior mistakes. If you set "
|
|
248
|
+
"base_on_champion, the harness embeds the current champion's solution content "
|
|
249
|
+
"(content only) into that directive.\n\n"
|
|
250
|
+
f"# Task\n{context.task_description}\n\n"
|
|
251
|
+
f"Target artifact: {kind_label(context.solution_kind)}\n\n"
|
|
252
|
+
f"# Verification checks (all must pass per example)\n{checks_text}\n\n"
|
|
253
|
+
f"# Run state\n"
|
|
254
|
+
f"Loops completed: {context.loops_completed} of {context.max_loops}. "
|
|
255
|
+
f"Validation peeks used: {context.peeks_used} of {context.max_peeks}.\n"
|
|
256
|
+
f"Note: {context.dev_subset_note}\n\n"
|
|
257
|
+
f"# Your log — older loops (compacted)\n{compacted}\n\n"
|
|
258
|
+
f"# Your log — recent loops (full detail)\n{recent}\n\n"
|
|
259
|
+
f"# Dev leaderboard (95% Wilson intervals)\n{leaderboard}\n\n"
|
|
260
|
+
f"# Current champion (dev-best) solution\n{champion_text}\n\n"
|
|
261
|
+
f"# Failure patterns on dev (champion candidate, sampled)\n{failures}\n\n"
|
|
262
|
+
f"# Validation checkpoints (aggregate scores only){gap_text}\n{val_text}\n\n"
|
|
263
|
+
"# Your reply\n"
|
|
264
|
+
"Reply with a single JSON object and nothing else.\n"
|
|
265
|
+
"To continue:\n"
|
|
266
|
+
'{"action": "continue", "observations": "<what the results tell you>", '
|
|
267
|
+
'"hypotheses": "<what you believe will improve scores>", '
|
|
268
|
+
'"directives": [{"approach_summary": "<one line>", '
|
|
269
|
+
'"instructions": "<precise instructions for one executor>", '
|
|
270
|
+
'"base_on_champion": true|false}], '
|
|
271
|
+
'"request_validation": true|false}\n'
|
|
272
|
+
f"Issue 1 to {context.executors_per_loop} directives. Make them diverse when "
|
|
273
|
+
"exploring; converge on refinement when a champion is close. Set "
|
|
274
|
+
"request_validation sparingly — peeks are budgeted.\n"
|
|
275
|
+
"To stop (only when further loops are clearly wasted): "
|
|
276
|
+
'{"action": "stop", "reason": "<why>"}'
|
|
277
|
+
)
|
|
278
|
+
return AgentContextPrompt(role=StrategyRole(), text=text)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def reformat_context_prompt(prompt: AgentContextPrompt, detail: str) -> AgentContextPrompt:
|
|
282
|
+
return AgentContextPrompt(
|
|
283
|
+
role=prompt.role,
|
|
284
|
+
text=(
|
|
285
|
+
f"{prompt.text}\n\n"
|
|
286
|
+
f"IMPORTANT: your previous reply was rejected: {detail}. "
|
|
287
|
+
"Reply again, following the required format exactly."
|
|
288
|
+
),
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# --------------------------------------------------------------------------
|
|
293
|
+
# Evaluation channel (sanctioned to embed ONE example of any split)
|
|
294
|
+
# --------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def build_solution_eval_prompt(solution_content: str, example: Example) -> EvalPrompt:
|
|
298
|
+
"""Runs a skill/guidance candidate on one example. The reply is the raw
|
|
299
|
+
output to score; it returns to the harness as data only."""
|
|
300
|
+
system = (
|
|
301
|
+
"Follow the document below to transform the user's input into the required "
|
|
302
|
+
"output. Reply with the output only — no preamble, no explanation, no fences.\n\n"
|
|
303
|
+
f"{solution_content}"
|
|
304
|
+
)
|
|
305
|
+
return EvalPrompt(system_prompt=Some(system), user_prompt=example.input_text)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def build_judge_prompt(rubric: str, example: Example, actual_output: str) -> EvalPrompt:
|
|
309
|
+
text = (
|
|
310
|
+
"You are grading one candidate output against a rubric.\n\n"
|
|
311
|
+
f"# Rubric\n{rubric}\n\n"
|
|
312
|
+
f"# Input\n{example.input_text}\n\n"
|
|
313
|
+
f"# Expected output (gold standard)\n{example.expected_output}\n\n"
|
|
314
|
+
f"# Candidate output\n{actual_output}\n\n"
|
|
315
|
+
"Reply with a single JSON object and nothing else: "
|
|
316
|
+
'{"pass": true|false, "reason": "<one sentence>"}'
|
|
317
|
+
)
|
|
318
|
+
return EvalPrompt(system_prompt=Nothing(), user_prompt=text)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def reformat_eval_prompt(prompt: EvalPrompt, detail: str) -> EvalPrompt:
|
|
322
|
+
return EvalPrompt(
|
|
323
|
+
system_prompt=prompt.system_prompt,
|
|
324
|
+
user_prompt=(
|
|
325
|
+
f"{prompt.user_prompt}\n\n"
|
|
326
|
+
f"IMPORTANT: your previous reply was rejected: {detail}. "
|
|
327
|
+
"Reply again, following the required format exactly."
|
|
328
|
+
),
|
|
329
|
+
)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Pure rendering of user-facing artifacts: the final report, the pre-run
|
|
2
|
+
check summary, and the kind-aware agent-call budget estimate."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from rigorloop.core.strategy_calcs import compact_log_line
|
|
7
|
+
from rigorloop.core.types import (
|
|
8
|
+
BudgetEstimate,
|
|
9
|
+
BudgetExhausted,
|
|
10
|
+
CandidateScore,
|
|
11
|
+
Check,
|
|
12
|
+
DuplicateWarning,
|
|
13
|
+
GuidanceSolution,
|
|
14
|
+
LlmJudge,
|
|
15
|
+
PowerWarning,
|
|
16
|
+
RunConfig,
|
|
17
|
+
RunState,
|
|
18
|
+
ScriptSolution,
|
|
19
|
+
SkillSolution,
|
|
20
|
+
SolutionKind,
|
|
21
|
+
StopReason,
|
|
22
|
+
StrategyRequestedStop,
|
|
23
|
+
StrategyUnresponsive,
|
|
24
|
+
TargetReached,
|
|
25
|
+
ValidatedCandidate,
|
|
26
|
+
ValidationPlateau,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def stop_reason_label(reason: StopReason) -> str:
|
|
31
|
+
match reason:
|
|
32
|
+
case BudgetExhausted(max_loops):
|
|
33
|
+
return f"loop budget exhausted ({max_loops} loops)"
|
|
34
|
+
case ValidationPlateau(checkpoints):
|
|
35
|
+
return (
|
|
36
|
+
f"validation plateau: {checkpoints} consecutive checkpoints without "
|
|
37
|
+
"improvement beyond the CI band"
|
|
38
|
+
)
|
|
39
|
+
case TargetReached(pass_rate):
|
|
40
|
+
return f"target pass rate reached ({pass_rate:.1%} on validation)"
|
|
41
|
+
case StrategyRequestedStop(why):
|
|
42
|
+
return f"strategy agent requested stop: {why}"
|
|
43
|
+
case StrategyUnresponsive(fallbacks):
|
|
44
|
+
return f"strategy agent unresponsive ({fallbacks} consecutive malformed replies)"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _kind_name(kind: SolutionKind) -> str:
|
|
48
|
+
match kind:
|
|
49
|
+
case ScriptSolution():
|
|
50
|
+
return "script"
|
|
51
|
+
case SkillSolution():
|
|
52
|
+
return "skill"
|
|
53
|
+
case GuidanceSolution():
|
|
54
|
+
return "guidance"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _is_stochastic_kind(kind: SolutionKind) -> bool:
|
|
58
|
+
match kind:
|
|
59
|
+
case ScriptSolution():
|
|
60
|
+
return False
|
|
61
|
+
case SkillSolution() | GuidanceSolution():
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _per_example_eval_calls(kind: SolutionKind) -> int:
|
|
66
|
+
"""Skill/guidance evaluation costs one claude call per example; scripts run
|
|
67
|
+
locally for free."""
|
|
68
|
+
return 1 if _is_stochastic_kind(kind) else 0
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _judge_samples_per_example(checks: tuple[Check, ...]) -> int:
|
|
72
|
+
return sum(c.n_samples for c in checks if isinstance(c, LlmJudge))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def estimate_budget(config: RunConfig, n_dev: int, n_val: int, n_test: int) -> BudgetEstimate:
|
|
76
|
+
"""Upper bound on claude calls for a full run (retries excluded). For
|
|
77
|
+
skill/guidance kinds, candidate evaluation is the dominant cost."""
|
|
78
|
+
loops = config.loop.max_loops
|
|
79
|
+
candidates = loops * config.loop.executors_per_loop
|
|
80
|
+
per_example = _per_example_eval_calls(config.task.solution_kind)
|
|
81
|
+
judge_per_example = _judge_samples_per_example(config.checks)
|
|
82
|
+
evaluated_examples = candidates * n_dev + config.validation.max_peeks * n_val + n_test
|
|
83
|
+
return BudgetEstimate(
|
|
84
|
+
strategy_calls=loops,
|
|
85
|
+
executor_calls=candidates,
|
|
86
|
+
solution_eval_calls=evaluated_examples * per_example,
|
|
87
|
+
judge_calls=evaluated_examples * judge_per_example,
|
|
88
|
+
total_calls=loops + candidates + evaluated_examples * (per_example + judge_per_example),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def render_check_summary(
|
|
93
|
+
n_total: int,
|
|
94
|
+
n_dev: int,
|
|
95
|
+
n_val: int,
|
|
96
|
+
n_test: int,
|
|
97
|
+
duplicates: tuple[DuplicateWarning, ...],
|
|
98
|
+
warnings: tuple[PowerWarning, ...],
|
|
99
|
+
budget: BudgetEstimate,
|
|
100
|
+
model: str,
|
|
101
|
+
) -> str:
|
|
102
|
+
dup_lines = (
|
|
103
|
+
"\n".join(
|
|
104
|
+
f" - input {d.input_preview!r} appears {d.occurrences} times (collapsed to one)"
|
|
105
|
+
for d in duplicates
|
|
106
|
+
)
|
|
107
|
+
if duplicates
|
|
108
|
+
else " (none)"
|
|
109
|
+
)
|
|
110
|
+
warn_lines = "\n".join(f" - {w.message}" for w in warnings) if warnings else " (none)"
|
|
111
|
+
return (
|
|
112
|
+
"RigorLoop pre-run check\n"
|
|
113
|
+
"=======================\n"
|
|
114
|
+
f"Examples: {n_total} unique → dev {n_dev} / validation {n_val} / test {n_test}\n"
|
|
115
|
+
f"Exact duplicates collapsed:\n{dup_lines}\n"
|
|
116
|
+
f"Statistical power warnings:\n{warn_lines}\n"
|
|
117
|
+
"\n"
|
|
118
|
+
f"Agent-call budget estimate (upper bound, model {model}):\n"
|
|
119
|
+
f" strategy calls: {budget.strategy_calls}\n"
|
|
120
|
+
f" executor calls: {budget.executor_calls}\n"
|
|
121
|
+
f" solution evaluation calls: {budget.solution_eval_calls}\n"
|
|
122
|
+
f" judge calls: {budget.judge_calls}\n"
|
|
123
|
+
f" total: {budget.total_calls}\n"
|
|
124
|
+
"\n"
|
|
125
|
+
"No tokens were spent by this command."
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _score_line(name: str, score: CandidateScore) -> str:
|
|
130
|
+
aborted = " (evaluation aborted early)" if score.eval_aborted else ""
|
|
131
|
+
return (
|
|
132
|
+
f"| {name} | {score.pass_rate:.1%} | [{score.ci_low:.1%}, {score.ci_high:.1%}] "
|
|
133
|
+
f"| {score.passes}/{score.n} |{aborted}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def render_report(
|
|
138
|
+
run_id: str,
|
|
139
|
+
kind: SolutionKind,
|
|
140
|
+
eval_model: str,
|
|
141
|
+
cli_version: str,
|
|
142
|
+
stop_reason: StopReason,
|
|
143
|
+
winner: ValidatedCandidate,
|
|
144
|
+
test_score: CandidateScore,
|
|
145
|
+
state: RunState,
|
|
146
|
+
agent_calls_made: int,
|
|
147
|
+
) -> str:
|
|
148
|
+
per_check = (
|
|
149
|
+
"\n".join(
|
|
150
|
+
f"| {c.check_name} | {c.passes}/{c.n} ({(c.passes / c.n if c.n else 0):.1%}) |"
|
|
151
|
+
for c in test_score.per_check
|
|
152
|
+
)
|
|
153
|
+
or "| (none) | |"
|
|
154
|
+
)
|
|
155
|
+
history = "\n".join(f"- {compact_log_line(e)}" for e in state.strategy_log) or "(no loops)"
|
|
156
|
+
dev_rate = winner.dev_score.pass_rate
|
|
157
|
+
val_rate = winner.val_score.pass_rate
|
|
158
|
+
test_rate = test_score.pass_rate
|
|
159
|
+
stochastic_note = (
|
|
160
|
+
"\n> **Stochastic evaluator caveat:** this artifact kind is evaluated by a "
|
|
161
|
+
f"model (`{eval_model}`, claude CLI {cli_version}). Scores are conditional on "
|
|
162
|
+
"that pinned evaluator and understate total uncertainty (one sample per "
|
|
163
|
+
"example).\n"
|
|
164
|
+
if _is_stochastic_kind(kind)
|
|
165
|
+
else ""
|
|
166
|
+
)
|
|
167
|
+
return (
|
|
168
|
+
f"# RigorLoop report — run `{run_id}`\n\n"
|
|
169
|
+
f"- Artifact kind: **{_kind_name(kind)}**\n"
|
|
170
|
+
f"- Winning candidate: **{winner.candidate_id}**\n"
|
|
171
|
+
f"- Stop reason: {stop_reason_label(stop_reason)}\n"
|
|
172
|
+
f"- Loops completed: {state.loops_completed}\n"
|
|
173
|
+
f"- Validation peeks used: {state.peeks_used}\n"
|
|
174
|
+
f"- Agent calls made: {agent_calls_made}\n\n"
|
|
175
|
+
"## Scores (95% Wilson intervals)\n\n"
|
|
176
|
+
"| Set | Pass rate | CI | Passes |\n|---|---|---|---|\n"
|
|
177
|
+
f"{_score_line('dev', winner.dev_score)}\n"
|
|
178
|
+
f"{_score_line('validation', winner.val_score)}\n"
|
|
179
|
+
f"{_score_line('test', test_score)}\n\n"
|
|
180
|
+
f"Generalization gaps: dev→val {dev_rate - val_rate:+.1%}, "
|
|
181
|
+
f"val→test {val_rate - test_rate:+.1%}, dev→test {dev_rate - test_rate:+.1%}\n\n"
|
|
182
|
+
"> **Selection-bias caveat:** the winner was *selected* on its validation "
|
|
183
|
+
"score, so that number is optimistically biased. The test score — computed "
|
|
184
|
+
"exactly once, on examples no agent ever saw — is the honest number.\n"
|
|
185
|
+
f"{stochastic_note}\n"
|
|
186
|
+
"## Per-check breakdown on the test set\n\n"
|
|
187
|
+
"| Check | Passes |\n|---|---|\n"
|
|
188
|
+
f"{per_check}\n\n"
|
|
189
|
+
"## Loop history\n\n"
|
|
190
|
+
f"{history}\n\n"
|
|
191
|
+
"---\n"
|
|
192
|
+
"Re-running RigorLoop after seeing this test score burns the holdout: treat "
|
|
193
|
+
"this test set as spent and supply fresh examples for the next run.\n"
|
|
194
|
+
)
|