turnloop 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.
Files changed (132) hide show
  1. turnloop/__init__.py +3 -0
  2. turnloop/__main__.py +4 -0
  3. turnloop/agent/__init__.py +0 -0
  4. turnloop/agent/factory.py +175 -0
  5. turnloop/agent/headless.py +150 -0
  6. turnloop/agent/loop.py +396 -0
  7. turnloop/agent/subagent.py +182 -0
  8. turnloop/agent/system_prompt.py +263 -0
  9. turnloop/cli.py +202 -0
  10. turnloop/commands/__init__.py +0 -0
  11. turnloop/commands/dispatch.py +428 -0
  12. turnloop/commands/loader.py +140 -0
  13. turnloop/config.py +425 -0
  14. turnloop/context/__init__.py +0 -0
  15. turnloop/context/budget.py +74 -0
  16. turnloop/context/compaction.py +441 -0
  17. turnloop/context/memory.py +120 -0
  18. turnloop/core/__init__.py +0 -0
  19. turnloop/core/events.py +145 -0
  20. turnloop/core/ids.py +29 -0
  21. turnloop/core/messages.py +217 -0
  22. turnloop/core/tokens.py +104 -0
  23. turnloop/diagnostics.py +233 -0
  24. turnloop/errors.py +64 -0
  25. turnloop/experiments/__init__.py +0 -0
  26. turnloop/experiments/configs/bakeoff.yaml +50 -0
  27. turnloop/experiments/configs/ceiling-check.yaml +31 -0
  28. turnloop/experiments/configs/context-glm.yaml +59 -0
  29. turnloop/experiments/configs/context.yaml +65 -0
  30. turnloop/experiments/configs/glm-selfhosted.yaml +36 -0
  31. turnloop/experiments/configs/groq-free.yaml +28 -0
  32. turnloop/experiments/configs/hard-calibration.yaml +26 -0
  33. turnloop/experiments/configs/hard-glm.yaml +41 -0
  34. turnloop/experiments/configs/loop.yaml +34 -0
  35. turnloop/experiments/configs/nvidia-smoke.yaml +22 -0
  36. turnloop/experiments/configs/reliability.yaml +44 -0
  37. turnloop/experiments/configs/rewrite-calibration.yaml +24 -0
  38. turnloop/experiments/configs/smoke.yaml +22 -0
  39. turnloop/experiments/graders.py +209 -0
  40. turnloop/experiments/report.py +371 -0
  41. turnloop/experiments/runner.py +495 -0
  42. turnloop/experiments/suite/default.yaml +185 -0
  43. turnloop/experiments/suite/fixtures/bulky/module_1.py +670 -0
  44. turnloop/experiments/suite/fixtures/bulky/module_2.py +670 -0
  45. turnloop/experiments/suite/fixtures/bulky/module_3.py +670 -0
  46. turnloop/experiments/suite/fixtures/bulky/module_4.py +670 -0
  47. turnloop/experiments/suite/fixtures/bulky/module_5.py +670 -0
  48. turnloop/experiments/suite/fixtures/bulky/module_6.py +672 -0
  49. turnloop/experiments/suite/fixtures/bulky/module_7.py +670 -0
  50. turnloop/experiments/suite/fixtures/bulky/module_8.py +670 -0
  51. turnloop/experiments/suite/fixtures/circular_import/models.py +11 -0
  52. turnloop/experiments/suite/fixtures/circular_import/services.py +15 -0
  53. turnloop/experiments/suite/fixtures/circular_import/test_cancel.py +12 -0
  54. turnloop/experiments/suite/fixtures/cli_flag/app.py +16 -0
  55. turnloop/experiments/suite/fixtures/cross_file_contract/decode.py +7 -0
  56. turnloop/experiments/suite/fixtures/cross_file_contract/encode.py +10 -0
  57. turnloop/experiments/suite/fixtures/cross_file_contract/test_roundtrip.py +11 -0
  58. turnloop/experiments/suite/fixtures/cross_file_contract/test_wire_format.py +7 -0
  59. turnloop/experiments/suite/fixtures/empty/.keep +1 -0
  60. turnloop/experiments/suite/fixtures/failing_test/calc.py +14 -0
  61. turnloop/experiments/suite/fixtures/failing_test/test_calc.py +17 -0
  62. turnloop/experiments/suite/fixtures/generate_bulky.py +80 -0
  63. turnloop/experiments/suite/fixtures/grep_edit/ingest.py +5 -0
  64. turnloop/experiments/suite/fixtures/grep_edit/load.py +5 -0
  65. turnloop/experiments/suite/fixtures/grep_edit/transform.py +5 -0
  66. turnloop/experiments/suite/fixtures/misleading_traceback/checkout.py +21 -0
  67. turnloop/experiments/suite/fixtures/misleading_traceback/invoice.py +8 -0
  68. turnloop/experiments/suite/fixtures/misleading_traceback/pricing.py +11 -0
  69. turnloop/experiments/suite/fixtures/misleading_traceback/test_checkout.py +12 -0
  70. turnloop/experiments/suite/fixtures/misleading_traceback/test_invoice.py +6 -0
  71. turnloop/experiments/suite/fixtures/needle/inventory.py +18 -0
  72. turnloop/experiments/suite/fixtures/needle/pricing.py +17 -0
  73. turnloop/experiments/suite/fixtures/needle/shipping.py +13 -0
  74. turnloop/experiments/suite/fixtures/regression_trap/test_validators.py +14 -0
  75. turnloop/experiments/suite/fixtures/regression_trap/validators.py +23 -0
  76. turnloop/experiments/suite/fixtures/rename/billing.py +7 -0
  77. turnloop/experiments/suite/fixtures/rename/report.py +5 -0
  78. turnloop/experiments/suite/fixtures/rename/test_billing.py +11 -0
  79. turnloop/experiments/suite/fixtures/spec/parser.py +19 -0
  80. turnloop/experiments/suite/fixtures/spec/test_parser.py +28 -0
  81. turnloop/experiments/suite/fixtures/subtle_spec_edge/leaderboard.py +12 -0
  82. turnloop/experiments/suite/fixtures/subtle_spec_edge/test_leaderboard.py +28 -0
  83. turnloop/experiments/suite/fixtures/util/util.py +9 -0
  84. turnloop/hooks/__init__.py +0 -0
  85. turnloop/hooks/runner.py +221 -0
  86. turnloop/mcp/__init__.py +0 -0
  87. turnloop/mcp/adapter.py +109 -0
  88. turnloop/mcp/client.py +299 -0
  89. turnloop/permissions/__init__.py +0 -0
  90. turnloop/permissions/engine.py +211 -0
  91. turnloop/permissions/rules.py +325 -0
  92. turnloop/providers/__init__.py +0 -0
  93. turnloop/providers/anthropic.py +318 -0
  94. turnloop/providers/base.py +329 -0
  95. turnloop/providers/gemini.py +217 -0
  96. turnloop/providers/mock.py +226 -0
  97. turnloop/providers/openai_compat.py +357 -0
  98. turnloop/providers/pricing.py +148 -0
  99. turnloop/providers/registry.py +75 -0
  100. turnloop/providers/sse.py +114 -0
  101. turnloop/sessions/__init__.py +0 -0
  102. turnloop/sessions/models.py +139 -0
  103. turnloop/sessions/store.py +271 -0
  104. turnloop/tools/__init__.py +0 -0
  105. turnloop/tools/ask.py +133 -0
  106. turnloop/tools/base.py +286 -0
  107. turnloop/tools/bash.py +423 -0
  108. turnloop/tools/builtin.py +63 -0
  109. turnloop/tools/edit.py +238 -0
  110. turnloop/tools/glob.py +235 -0
  111. turnloop/tools/grep.py +314 -0
  112. turnloop/tools/read.py +151 -0
  113. turnloop/tools/runner.py +284 -0
  114. turnloop/tools/shell.py +222 -0
  115. turnloop/tools/skill.py +89 -0
  116. turnloop/tools/task.py +161 -0
  117. turnloop/tools/textio.py +87 -0
  118. turnloop/tools/todo.py +140 -0
  119. turnloop/tools/webfetch.py +235 -0
  120. turnloop/tools/write.py +97 -0
  121. turnloop/tui/__init__.py +0 -0
  122. turnloop/tui/app.py +330 -0
  123. turnloop/tui/app.tcss +84 -0
  124. turnloop/tui/bridge.py +142 -0
  125. turnloop/tui/widgets/__init__.py +0 -0
  126. turnloop/tui/widgets/permission.py +131 -0
  127. turnloop/tui/widgets/status.py +110 -0
  128. turnloop/tui/widgets/transcript.py +146 -0
  129. turnloop-0.1.0.dist-info/METADATA +916 -0
  130. turnloop-0.1.0.dist-info/RECORD +132 -0
  131. turnloop-0.1.0.dist-info/WHEEL +4 -0
  132. turnloop-0.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,371 @@
1
+ """Report rendering.
2
+
3
+ Medians, not means, and never a single run. Agent behavior is heavy-tailed: one
4
+ task where a model looped 20 times before giving up drags a mean somewhere that
5
+ describes nothing. The report also always shows n, because a pass rate over three
6
+ repeats is a much weaker claim than one over thirty and the reader deserves to know
7
+ which they are looking at.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from collections import defaultdict
14
+ from pathlib import Path
15
+ from statistics import median
16
+ from typing import Any
17
+
18
+
19
+ def load_results(run_dir: Path) -> list[dict]:
20
+ path = run_dir / "results.jsonl"
21
+ if not path.is_file():
22
+ return []
23
+ rows = []
24
+ for line in path.read_text(encoding="utf-8").splitlines():
25
+ if line.strip():
26
+ try:
27
+ rows.append(json.loads(line))
28
+ except json.JSONDecodeError:
29
+ continue
30
+ return rows
31
+
32
+
33
+ def render_report(run_dir: Path) -> str:
34
+ run_dir = Path(run_dir)
35
+ rows = load_results(run_dir)
36
+ if not rows:
37
+ return f"no results in {run_dir}"
38
+
39
+ config = {}
40
+ config_path = run_dir / "config.json"
41
+ if config_path.is_file():
42
+ config = json.loads(config_path.read_text(encoding="utf-8"))
43
+
44
+ out: list[str] = [
45
+ f"# {config.get('experiment', run_dir.name)}",
46
+ "",
47
+ f"{len(rows)} runs · {len({r['arm'] for r in rows})} arms · "
48
+ f"{len({r['task'] for r in rows})} tasks · {config.get('repeats', '?')} repeats",
49
+ f"artifacts: `{run_dir}`",
50
+ "",
51
+ ]
52
+
53
+ by_arm: dict[str, list[dict]] = defaultdict(list)
54
+ for row in rows:
55
+ by_arm[row["arm"]].append(row)
56
+
57
+ out += _arm_table(by_arm)
58
+ out += _pass_rate_gate_note(by_arm)
59
+ out += _reliability_table(by_arm)
60
+ out += _efficiency_section(by_arm)
61
+ out += _efficiency_by_task(rows)
62
+ out += _task_matrix(rows)
63
+ out += _delta_section(by_arm)
64
+ out += _failures(rows)
65
+ out += _skipped_section(rows)
66
+ return "\n".join(out)
67
+
68
+
69
+ def _pass_rate_gate_note(by_arm: dict[str, list[dict]]) -> list[str]:
70
+ """Say plainly when pass rate has stopped being able to tell arms apart.
71
+
72
+ GLM-5.2 clears every task in the default suite, every ablation, every
73
+ repeat — so a table of identical 100%s answers nothing about which harness
74
+ configuration is better. It still has to run (a crash is not a valid
75
+ efficiency datapoint), it just isn't where the finding lives anymore.
76
+ """
77
+ rates = []
78
+ for rows in by_arm.values():
79
+ ran = [r for r in rows if not r.get("skipped")]
80
+ if ran:
81
+ rates.append(sum(1 for r in ran if r["passed"]) / len(ran))
82
+ if rates and all(r == 1.0 for r in rates):
83
+ return [
84
+ "Every arm passed every task here — pass rate is a gate, not a "
85
+ "discriminator, once it saturates like this. A run still has to clear "
86
+ "it (a crash is not a valid efficiency datapoint), but the finding in "
87
+ "this report lives in the Efficiency section below, not this table.",
88
+ "",
89
+ ]
90
+ return []
91
+
92
+
93
+ def _arm_table(by_arm: dict[str, list[dict]]) -> list[str]:
94
+ lines = [
95
+ "## Outcome by arm",
96
+ "",
97
+ "| arm | model | pass | n | skipped | median cost | median wall | median TTFT | "
98
+ "tok in | tok out | compactions |",
99
+ "|---|---|---|---|---|---|---|---|---|---|---|",
100
+ ]
101
+ any_wall_clock = False
102
+ for arm, rows in sorted(by_arm.items()):
103
+ ran = [r for r in rows if not r.get("skipped")]
104
+ skipped = len(rows) - len(ran)
105
+ passed = sum(1 for r in ran if r["passed"])
106
+ ttfts = [r["ttft_ms"] for r in ran if r.get("ttft_ms")]
107
+ wall_clock = any(r.get("cost_basis") == "wall_clock" for r in ran)
108
+ any_wall_clock = any_wall_clock or wall_clock
109
+ # A wall-clock-billed arm's cost tracks GPU time, not tokens — marked
110
+ # so it is never read as comparable to a token-billed arm's cost. See
111
+ # the footnote below the table.
112
+ cost_marker = "†" if wall_clock else ""
113
+ lines.append(
114
+ "| {arm} | {model} | {rate} | {n} | {skipped} | ${cost:.4f}{marker} | "
115
+ "{wall:.1f}s | {ttft} | {tin:,} | {tout:,} | {comp} |".format(
116
+ arm=arm,
117
+ model=rows[0].get("model", "?"),
118
+ rate=f"{passed / len(ran):.0%}" if ran else "—",
119
+ n=len(ran),
120
+ skipped=skipped,
121
+ cost=median(r["cost_usd"] for r in ran) if ran else 0.0,
122
+ marker=cost_marker,
123
+ wall=median(r["wall_s"] for r in ran) if ran else 0.0,
124
+ ttft=f"{median(ttfts):.0f}ms" if ttfts else "—",
125
+ tin=int(median(r["tokens_in"] for r in ran)) if ran else 0,
126
+ tout=int(median(r["tokens_out"] for r in ran)) if ran else 0,
127
+ comp=int(median(r["compactions"] for r in ran)) if ran else 0,
128
+ )
129
+ )
130
+ lines.append("")
131
+ if any_wall_clock:
132
+ lines.append(
133
+ "† cost is derived from wall-clock time on a per-hour self-hosted GPU, not "
134
+ "from token usage — a token delta and this cost delta are not measuring the "
135
+ "same thing and should not be read side by side."
136
+ )
137
+ lines.append("")
138
+ return lines
139
+
140
+
141
+ def _reliability_table(by_arm: dict[str, list[dict]]) -> list[str]:
142
+ lines = [
143
+ "## Tool-calling reliability",
144
+ "",
145
+ "Measured passively from the traces: every dispatch records why it failed.",
146
+ "",
147
+ "| arm | tool calls | error rate | malformed args | schema violations | recovery |",
148
+ "|---|---|---|---|---|---|",
149
+ ]
150
+ for arm, rows in sorted(by_arm.items()):
151
+ calls = sum(r["tool_calls"] for r in rows)
152
+ errors = sum(sum(r["tool_errors"].values()) for r in rows)
153
+ recoveries = [r["recovery_rate"] for r in rows if r.get("recovery_rate") is not None]
154
+ lines.append(
155
+ "| {arm} | {calls} | {rate} | {malformed} | {schema} | {rec} |".format(
156
+ arm=arm,
157
+ calls=calls,
158
+ rate=f"{errors / calls:.1%}" if calls else "—",
159
+ malformed=sum(r["malformed_args"] for r in rows),
160
+ schema=sum(r["schema_violations"] for r in rows),
161
+ rec=f"{median(recoveries):.0%}" if recoveries else "—",
162
+ )
163
+ )
164
+ lines.append("")
165
+ return lines
166
+
167
+
168
+ def _efficiency_section(by_arm: dict[str, list[dict]]) -> list[str]:
169
+ """The metrics that still have headroom once pass rate has saturated.
170
+
171
+ Same shape as the reliability table above by design — this is the section
172
+ meant to carry the actual finding when every arm passes everything, so it
173
+ puts tokens and tool-error rate in the same table as pass-rate-adjacent
174
+ numbers (recovery, wall time) instead of scattering them.
175
+ """
176
+ lines = [
177
+ "## Efficiency",
178
+ "",
179
+ "| arm | tok in (median) | tok out (median) | tool calls | error rate | "
180
+ "recovery | median wall |",
181
+ "|---|---|---|---|---|---|---|",
182
+ ]
183
+ for arm, rows in sorted(by_arm.items()):
184
+ ran = [r for r in rows if not r.get("skipped")]
185
+ if not ran:
186
+ lines.append(f"| {arm} | — | — | — | — | — | — |")
187
+ continue
188
+ calls = sum(r["tool_calls"] for r in ran)
189
+ errors = sum(sum(r["tool_errors"].values()) for r in ran)
190
+ recoveries = [r["recovery_rate"] for r in ran if r.get("recovery_rate") is not None]
191
+ lines.append(
192
+ "| {arm} | {tin:,} | {tout:,} | {calls} | {rate} | {rec} | {wall:.1f}s |".format(
193
+ arm=arm,
194
+ tin=int(median(r["tokens_in"] for r in ran)),
195
+ tout=int(median(r["tokens_out"] for r in ran)),
196
+ calls=calls,
197
+ rate=f"{errors / calls:.1%}" if calls else "—",
198
+ rec=f"{median(recoveries):.0%}" if recoveries else "—",
199
+ wall=median(r["wall_s"] for r in ran),
200
+ )
201
+ )
202
+ lines.append("")
203
+ return lines
204
+
205
+
206
+ def _efficiency_by_task(rows: list[dict], min_tasks: int = 2) -> list[str]:
207
+ """Median input tokens per (task, arm) — an arm-level median can hide a
208
+ difference that only shows up on the tasks big enough to expose it."""
209
+ tasks = sorted({r["task"] for r in rows})
210
+ if len(tasks) < min_tasks:
211
+ return []
212
+ arms = sorted({r["arm"] for r in rows})
213
+ grid: dict[tuple[str, str], list[int]] = defaultdict(list)
214
+ for row in rows:
215
+ if not row.get("skipped"):
216
+ grid[(row["task"], row["arm"])].append(row["tokens_in"])
217
+
218
+ lines = [
219
+ "## Efficiency by task (median input tokens)",
220
+ "",
221
+ "| task | " + " | ".join(arms) + " |",
222
+ "|---" * (len(arms) + 1) + "|",
223
+ ]
224
+ for task in tasks:
225
+ cells = []
226
+ for arm in arms:
227
+ values = grid.get((task, arm), [])
228
+ cells.append(f"{int(median(values)):,}" if values else "—")
229
+ lines.append(f"| {task} | " + " | ".join(cells) + " |")
230
+ lines.append("")
231
+ return lines
232
+
233
+
234
+ def _task_matrix(rows: list[dict]) -> list[str]:
235
+ arms = sorted({r["arm"] for r in rows})
236
+ tasks = sorted({r["task"] for r in rows})
237
+ grid: dict[tuple[str, str], list[bool]] = defaultdict(list)
238
+ for row in rows:
239
+ grid[(row["task"], row["arm"])].append(bool(row["passed"]))
240
+
241
+ lines = ["## Pass rate by task", "", "| task | " + " | ".join(arms) + " |",
242
+ "|---" * (len(arms) + 1) + "|"]
243
+ for task in tasks:
244
+ cells = []
245
+ for arm in arms:
246
+ outcomes = grid.get((task, arm), [])
247
+ if not outcomes:
248
+ cells.append("—")
249
+ else:
250
+ passed = sum(outcomes)
251
+ cells.append(f"{passed}/{len(outcomes)}")
252
+ lines.append(f"| {task} | " + " | ".join(cells) + " |")
253
+ lines.append("")
254
+ return lines
255
+
256
+
257
+ def _delta_section(by_arm: dict[str, list[dict]]) -> list[str]:
258
+ """Compare every arm to the first, which is the declared baseline."""
259
+ names = sorted(by_arm)
260
+ if len(names) < 2:
261
+ return []
262
+ baseline = names[0]
263
+ base = [r for r in by_arm[baseline] if not r.get("skipped")]
264
+ if not base:
265
+ return []
266
+ base_pass = sum(1 for r in base if r["passed"]) / len(base)
267
+ base_cost = median(r["cost_usd"] for r in base) or 0.0
268
+ base_tokens = median(r["tokens_in"] for r in base)
269
+ base_calls = median(r["tool_calls"] for r in base)
270
+ base_call_total = sum(r["tool_calls"] for r in base)
271
+ base_call_errors = sum(sum(r["tool_errors"].values()) for r in base)
272
+ base_err_rate = base_call_errors / base_call_total if base_call_total else 0.0
273
+ base_wall_clock = any(r.get("cost_basis") == "wall_clock" for r in base)
274
+
275
+ lines = [f"## Change vs `{baseline}`", ""]
276
+ for arm in names[1:]:
277
+ rows = [r for r in by_arm[arm] if not r.get("skipped")]
278
+ if not rows:
279
+ lines.append(f"- **{arm}**: skipped — provider was down for this run.")
280
+ continue
281
+ rate = sum(1 for r in rows if r["passed"]) / len(rows)
282
+ cost = median(r["cost_usd"] for r in rows)
283
+ tokens = median(r["tokens_in"] for r in rows)
284
+ calls = median(r["tool_calls"] for r in rows)
285
+ call_total = sum(r["tool_calls"] for r in rows)
286
+ call_errors = sum(sum(r["tool_errors"].values()) for r in rows)
287
+ err_rate = call_errors / call_total if call_total else 0.0
288
+ wall_clock = base_wall_clock or any(r.get("cost_basis") == "wall_clock" for r in rows)
289
+ # A wall-clock-billed cost tracks GPU time, not tokens, so a token
290
+ # delta and this cost delta can move in opposite directions without
291
+ # contradicting each other. Labeling it prevents reading them as the
292
+ # same measurement.
293
+ cost_label = "cost (wall-clock time, not tokens)" if wall_clock else "cost"
294
+ lines.append(
295
+ f"- **{arm}**: pass {_signed(rate - base_pass, pct=True)}, "
296
+ f"input tokens {_signed_ratio(tokens, base_tokens)}, "
297
+ f"{cost_label} {_signed_ratio(cost, base_cost)}, "
298
+ f"tool calls {_signed_ratio(calls, base_calls)}, "
299
+ f"tool-call error rate {_signed_pp(err_rate - base_err_rate)}"
300
+ )
301
+ lines.append("")
302
+ return lines
303
+
304
+
305
+ def _failures(rows: list[dict], limit: int = 12) -> list[str]:
306
+ """Rows that actually ran and did not pass — never a skipped row.
307
+
308
+ A skipped row and a genuine failure both have `passed=False`, but they mean
309
+ opposite things: one is the agent trying and getting it wrong, the other is
310
+ a run that never happened. Mixing them here would hide a provider outage
311
+ inside what looks like a normal failure list.
312
+ """
313
+ failed = [r for r in rows if not r["passed"] and not r.get("skipped")]
314
+ if not failed:
315
+ return ["## Failures", "", "None.", ""]
316
+ lines = ["## Failures", "", "| arm | task | rep | why |", "|---|---|---|---|"]
317
+ for row in failed[:limit]:
318
+ why = row.get("error") or row.get("grader_detail") or "?"
319
+ lines.append(
320
+ f"| {row['arm']} | {row['task']} | {row['repeat']} | {_clean(why)} |"
321
+ )
322
+ if len(failed) > limit:
323
+ lines.append(f"| … | | | {len(failed) - limit} more |")
324
+ lines.append("")
325
+ return lines
326
+
327
+
328
+ def _skipped_section(rows: list[dict]) -> list[str]:
329
+ """Runs that never executed because their provider was declared down.
330
+
331
+ Kept separate from `_failures` on purpose: reporting a provider outage as
332
+ a wall of ordinary task failures is exactly the "tidy table of zeros" that
333
+ invites a fabricated finding.
334
+ """
335
+ skipped = [r for r in rows if r.get("skipped")]
336
+ if not skipped:
337
+ return []
338
+ by_provider: dict[str, int] = defaultdict(int)
339
+ for row in skipped:
340
+ by_provider[row["provider"]] += 1
341
+ lines = ["## Skipped (provider outage)", ""]
342
+ for provider, count in sorted(by_provider.items()):
343
+ lines.append(f"- **{provider}**: {count} run(s) skipped without executing.")
344
+ lines.append("")
345
+ return lines
346
+
347
+
348
+ def _signed(value: float, pct: bool = False) -> str:
349
+ formatted = f"{value:+.0%}" if pct else f"{value:+.2f}"
350
+ return formatted
351
+
352
+
353
+ def _signed_ratio(value: float, baseline: float) -> str:
354
+ if not baseline:
355
+ return f"{value:,.4g} (no baseline)"
356
+ change = (value - baseline) / baseline
357
+ return f"{change:+.0%}"
358
+
359
+
360
+ def _signed_pp(value: float) -> str:
361
+ """A percentage-*point* delta (4.1% -> 0.0% is "-4.1pp"), not a ratio.
362
+
363
+ A ratio breaks the moment the baseline error rate is 0%, which a
364
+ tool-calling arm worth shipping should be aiming for.
365
+ """
366
+ return f"{value * 100:+.1f}pp"
367
+
368
+
369
+ def _clean(text: Any, limit: int = 110) -> str:
370
+ flat = " ".join(str(text).split()).replace("|", "\\|")
371
+ return flat if len(flat) <= limit else flat[:limit] + "…"