agentloop-profiler 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 (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,606 @@
1
+ """Frozen, label-blind backend comparisons with explicit missing-case accounting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import platform
7
+ import random
8
+ from collections import Counter, defaultdict
9
+ from dataclasses import asdict, dataclass
10
+ from fractions import Fraction
11
+ from itertools import combinations
12
+ from statistics import fmean
13
+
14
+ from agentloop.judgment_benchmark_types import BENCHMARK_VERSION, JudgmentBenchmark
15
+ from agentloop.judgment_types import JudgeIdentity, JudgmentBackend, canonical, fingerprint, text
16
+ from agentloop.judgments import JudgmentSession, validate_judgment_record
17
+ from agentloop.markdown import markdown_table_cell, markdown_text
18
+
19
+ _CATEGORIES = {"deterministic", "local_predictor", "llm", "typed_service"}
20
+ _AVAILABILITY = {"ready", "not_configured", "missing_credentials"}
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class BenchmarkBackend:
25
+ name: str
26
+ backend: JudgmentBackend
27
+ category: str
28
+ simulated: bool = False
29
+
30
+ def __post_init__(self):
31
+ text(self.name, "backend name")
32
+ if self.category not in _CATEGORIES or type(self.simulated) is not bool:
33
+ raise ValueError("backend requires a supported category and boolean simulated flag")
34
+ if type(self.backend.identity) is not JudgeIdentity or not callable(self.backend.judge):
35
+ raise ValueError("backend must implement JudgmentBackend")
36
+
37
+ def declaration(self):
38
+ return {
39
+ "name": self.name,
40
+ "category": self.category,
41
+ "simulated": self.simulated,
42
+ "judge": asdict(self.backend.identity),
43
+ }
44
+
45
+
46
+ def _copy(value):
47
+ return json.loads(canonical(value))
48
+
49
+
50
+ def _availability(backend):
51
+ value = getattr(backend, "availability", "ready")
52
+ if value not in _AVAILABILITY:
53
+ raise ValueError("unsupported backend availability declaration")
54
+ return value
55
+
56
+
57
+ def run_judgment_benchmark(
58
+ protocol: JudgmentBenchmark, backends, *, enabled=False, max_invocations=10000
59
+ ):
60
+ """Execute a finite schedule; imported artifacts never configure executable code.
61
+
62
+ Backends are explicit host-owned objects. No retries, warmups, cache hits or
63
+ production routing are hidden in the benchmark. This bounds call count, not
64
+ provider spend; the host must authorize and bound any remote transport.
65
+ """
66
+ if type(protocol) is not JudgmentBenchmark or type(enabled) is not bool:
67
+ raise ValueError("benchmark requires a typed protocol and boolean enabled flag")
68
+ if (
69
+ not isinstance(backends, (list, tuple))
70
+ or not backends
71
+ or any(type(item) is not BenchmarkBackend for item in backends)
72
+ ):
73
+ raise ValueError("backends must be a nonempty list or tuple of BenchmarkBackend")
74
+ names = [item.name for item in backends]
75
+ if len(set(names)) != len(names):
76
+ raise ValueError("backend names must be unique")
77
+ count = len(backends) * len(protocol.examples) * protocol.repetitions
78
+ if type(max_invocations) is not int or max_invocations < 1 or count > max_invocations:
79
+ raise ValueError("planned benchmark exceeds max_invocations")
80
+ configurations = {item.name: item for item in backends}
81
+ declarations = [configurations[name].declaration() for name in sorted(names)]
82
+ plan = {"protocol": protocol.to_dict(), "backends": declarations}
83
+ sessions = {name: JudgmentSession(configurations[name].backend, cache_size=0) for name in names}
84
+ jobs = [
85
+ (name, index, repeat)
86
+ for name in sorted(names)
87
+ for index in range(len(protocol.examples))
88
+ for repeat in range(protocol.repetitions)
89
+ ]
90
+ random.Random(protocol.seed).shuffle(jobs) # nosec B311
91
+ rows = []
92
+ for ordinal, (name, index, repeat) in enumerate(jobs):
93
+ example = protocol.examples[index]
94
+ row = {
95
+ "ordinal": ordinal,
96
+ "backend": name,
97
+ "example_id": example.example_id,
98
+ "repetition": repeat,
99
+ "availability": "ready",
100
+ "receipt": None,
101
+ }
102
+ try:
103
+ row["availability"] = _availability(configurations[name].backend)
104
+ # Expected answers and label references are never passed to a backend.
105
+ row["receipt"] = sessions[name].evaluate(
106
+ example.request, enabled=enabled, timeout_s=protocol.timeout_s
107
+ )
108
+ row["status"] = "recorded"
109
+ except Exception:
110
+ row["status"] = "execution_error"
111
+ rows.append(row)
112
+ return {
113
+ "schema_version": BENCHMARK_VERSION,
114
+ **plan,
115
+ "plan_hash": fingerprint(plan),
116
+ "enabled": enabled,
117
+ "environment": {
118
+ "python": platform.python_version(),
119
+ "implementation": platform.python_implementation(),
120
+ "system": platform.system(),
121
+ "architecture": platform.machine(),
122
+ },
123
+ "records": rows,
124
+ }
125
+
126
+
127
+ def _load_bundle(bundle):
128
+ owned = _copy(bundle)
129
+ try:
130
+ if owned["schema_version"] != BENCHMARK_VERSION or type(owned["enabled"]) is not bool:
131
+ raise ValueError("unsupported benchmark bundle")
132
+ protocol = JudgmentBenchmark.from_dict(owned["protocol"])
133
+ declarations = owned["backends"]
134
+ if not isinstance(declarations, list) or not declarations:
135
+ raise ValueError("benchmark requires backend declarations")
136
+ for item in declarations:
137
+ text(item["name"], "backend name")
138
+ JudgeIdentity(**item["judge"])
139
+ if item["category"] not in _CATEGORIES or type(item["simulated"]) is not bool:
140
+ raise ValueError("invalid backend declaration")
141
+ if len({item["name"] for item in declarations}) != len(declarations):
142
+ raise ValueError("duplicate backend declarations")
143
+ if owned["plan_hash"] != fingerprint(
144
+ {"protocol": protocol.to_dict(), "backends": declarations}
145
+ ):
146
+ raise ValueError("benchmark plan hash mismatch")
147
+ if not isinstance(owned["records"], list):
148
+ raise ValueError("benchmark records must be a list")
149
+ environment = owned["environment"]
150
+ if (
151
+ not isinstance(environment, dict)
152
+ or set(environment) != {"python", "implementation", "system", "architecture"}
153
+ or any(not isinstance(value, str) for value in environment.values())
154
+ ):
155
+ raise ValueError("invalid benchmark environment")
156
+ return owned, protocol, {item["name"]: item for item in declarations}
157
+ except (KeyError, TypeError, AttributeError):
158
+ raise ValueError("invalid judgment benchmark bundle") from None
159
+
160
+
161
+ def _slots(bundle, protocol, declarations):
162
+ examples = {item.example_id: item for item in protocol.examples}
163
+ indexed, unmatched = defaultdict(list), []
164
+ for index, row in enumerate(bundle["records"]):
165
+ if (
166
+ not isinstance(row, dict)
167
+ or not isinstance(row.get("backend"), str)
168
+ or row["backend"] not in declarations
169
+ or not isinstance(row.get("example_id"), str)
170
+ or row["example_id"] not in examples
171
+ or type(row.get("repetition")) is not int
172
+ or not 0 <= row["repetition"] < protocol.repetitions
173
+ ):
174
+ unmatched.append({"record_index": index, "reason": "unmatched_record"})
175
+ continue
176
+ indexed[(row["backend"], row["example_id"], row["repetition"])].append(row)
177
+ slots = {}
178
+ invocation_ids = Counter()
179
+ for rows in indexed.values():
180
+ for row in rows:
181
+ invocation = (
182
+ row.get("receipt", {}).get("invocation", {})
183
+ if isinstance(row.get("receipt"), dict)
184
+ else {}
185
+ )
186
+ if isinstance(invocation, dict) and isinstance(invocation.get("id"), str):
187
+ invocation_ids[invocation["id"]] += 1
188
+ for name, declaration in declarations.items():
189
+ for example in protocol.examples:
190
+ for repeat in range(protocol.repetitions):
191
+ key = name, example.example_id, repeat
192
+ rows = indexed.get(key, [])
193
+ slot = {
194
+ "backend": name,
195
+ "example_id": example.example_id,
196
+ "repetition": repeat,
197
+ "status": "missing",
198
+ "value": None,
199
+ "receipt": None,
200
+ "availability": "unknown",
201
+ }
202
+ if len(rows) > 1:
203
+ slot["status"] = "duplicate"
204
+ elif rows:
205
+ row = rows[0]
206
+ try:
207
+ if row.get("availability") not in _AVAILABILITY:
208
+ raise ValueError("invalid availability")
209
+ slot["availability"] = row["availability"]
210
+ if row.get("status") == "execution_error" and row.get("receipt") is None:
211
+ slot["status"] = "error"
212
+ elif row.get("status") == "recorded":
213
+ receipt = validate_judgment_record(row["receipt"])
214
+ if (
215
+ receipt["request"] != example.request.to_dict()
216
+ or receipt["judge"] != declaration["judge"]
217
+ or receipt["invocation"]["cache_hit"]
218
+ or invocation_ids[receipt["invocation"]["id"]] != 1
219
+ or (not bundle["enabled"] and receipt["invocation"]["dispatched"])
220
+ ):
221
+ raise ValueError("receipt does not match its frozen benchmark slot")
222
+ result = receipt["evaluation"]
223
+ slot.update(
224
+ status=result["status"], value=result["value"], receipt=receipt
225
+ )
226
+ else:
227
+ raise ValueError("invalid record status")
228
+ except (ValueError, TypeError, KeyError, AttributeError):
229
+ slot["status"] = "invalid_record"
230
+ slots[key] = slot
231
+ return slots, unmatched
232
+
233
+
234
+ def _mean(values):
235
+ if not values:
236
+ return None
237
+ try:
238
+ return fmean(values)
239
+ except OverflowError:
240
+ return float(sum((Fraction(str(item)) for item in values), Fraction()) / len(values))
241
+
242
+
243
+ def _median(values):
244
+ if not values:
245
+ return None
246
+ ordered = sorted(values)
247
+ middle = len(ordered) // 2
248
+ return ordered[middle] if len(ordered) % 2 else _mean(ordered[middle - 1 : middle + 1])
249
+
250
+
251
+ def _normalized_difference(left, right, low, high):
252
+ return float(
253
+ abs(Fraction(str(left)) - Fraction(str(right))) / (Fraction(str(high)) - Fraction(str(low)))
254
+ )
255
+
256
+
257
+ def _calibration(cases):
258
+ """Descriptive probability calibration of per-example mean forecasts."""
259
+ if len(cases) < 20 or len({label for _, label in cases}) < 2:
260
+ return {
261
+ "status": "insufficient_support",
262
+ "example_count": len(cases),
263
+ "minimum_examples": 20,
264
+ "expected_calibration_error": None,
265
+ "bins": [],
266
+ "calibrated": False,
267
+ }
268
+ bins = [[] for _ in range(10)]
269
+ for probability, label in cases:
270
+ bins[min(int(probability * 10), 9)].append((probability, label))
271
+ summaries, error = [], 0.0
272
+ for index, values in enumerate(bins):
273
+ forecast = _mean([value for value, _ in values])
274
+ frequency = _mean([int(label) for _, label in values])
275
+ if values:
276
+ error += len(values) / len(cases) * abs(forecast - frequency)
277
+ summaries.append(
278
+ {
279
+ "lower": index / 10,
280
+ "upper": (index + 1) / 10,
281
+ "count": len(values),
282
+ "mean_probability": forecast,
283
+ "positive_frequency": frequency,
284
+ }
285
+ )
286
+ return {
287
+ "status": "descriptive",
288
+ "example_count": len(cases),
289
+ "expected_calibration_error": error,
290
+ "bins": summaries,
291
+ "calibrated": False,
292
+ "basis": "one mean probability per completely observed independently labelled example",
293
+ }
294
+
295
+
296
+ def _quality(protocol, name, slots):
297
+ groups = defaultdict(list)
298
+ for example in protocol.examples:
299
+ groups[fingerprint(asdict(example.request.spec))].append(example)
300
+ results = []
301
+ for key, examples in groups.items():
302
+ spec = examples[0].request.spec
303
+ labelled = [item for item in examples if item.label_status == "independent"]
304
+ scores, probabilities = [], []
305
+ for example in labelled:
306
+ values = [
307
+ slots[name, example.example_id, repeat] for repeat in range(protocol.repetitions)
308
+ ]
309
+ if any(item["status"] != "known" for item in values):
310
+ continue
311
+ answers = [item["value"] for item in values]
312
+ if spec.kind == "probability":
313
+ scores.append(fmean((value - int(example.expected)) ** 2 for value in answers))
314
+ probabilities.append((fmean(answers), example.expected))
315
+ elif spec.kind == "score":
316
+ scores.append(
317
+ fmean(
318
+ _normalized_difference(value, example.expected, spec.minimum, spec.maximum)
319
+ for value in answers
320
+ )
321
+ )
322
+ else:
323
+ scores.append(fmean(float(value == example.expected) for value in answers))
324
+ metric = {"probability": "brier_loss", "score": "normalized_absolute_error"}.get(
325
+ spec.kind, "accuracy"
326
+ )
327
+ complete = len(scores) == len(examples) and bool(scores)
328
+ result = {
329
+ "cohort_id": key,
330
+ "spec": asdict(spec),
331
+ "planned_examples": len(examples),
332
+ "independently_labelled_examples": len(labelled),
333
+ "complete_labelled_examples": len(scores),
334
+ "unlabelled_examples": len(examples) - len(labelled),
335
+ "incomplete_labelled_examples": len(labelled) - len(scores),
336
+ "coverage": len(scores) / len(examples),
337
+ "complete": complete,
338
+ "metric": metric,
339
+ "value": _mean(scores) if complete else None,
340
+ "observed_value": _mean(scores),
341
+ "better": "higher" if metric == "accuracy" else "lower",
342
+ "weighting": "equal examples; mean across planned repetitions only when all are known",
343
+ "label_versions": sorted({item.label_version for item in labelled}),
344
+ }
345
+ if spec.kind == "probability":
346
+ result["calibration"] = {**_calibration(probabilities), "full_coverage": complete}
347
+ results.append(result)
348
+ return results
349
+
350
+
351
+ def _performance(values):
352
+ timings, costs, inputs, outputs = [], [], [], []
353
+ cost_bases, token_bases = Counter(), Counter()
354
+ for slot in values:
355
+ if slot["receipt"] is None:
356
+ continue
357
+ invocation = slot["receipt"]["invocation"]
358
+ if invocation["dispatched"]:
359
+ timings.append(invocation["latency_ms"])
360
+ usage = invocation["usage"]
361
+ for key, target in (
362
+ ("cost_usd", costs),
363
+ ("input_tokens", inputs),
364
+ ("output_tokens", outputs),
365
+ ):
366
+ if usage[key] is not None:
367
+ target.append(usage[key])
368
+ cost_bases[usage["cost_basis"]] += 1
369
+ token_bases[usage["token_basis"]] += 1
370
+
371
+ def total(items):
372
+ # Exact decimal accumulation avoids an intermediate overflow or a large
373
+ # sum silently becoming infinity in portable JSON.
374
+ value = sum((Fraction(str(item)) for item in items), Fraction())
375
+ try:
376
+ return float(value)
377
+ except OverflowError:
378
+ return None
379
+
380
+ known_cost = total(costs)
381
+ return {
382
+ "latency_ms": {
383
+ "mean": _mean(timings),
384
+ "median": _median(timings),
385
+ "observed_invocations": len(timings),
386
+ "planned_invocations": len(values),
387
+ },
388
+ "cost": {
389
+ "known_usd": known_cost,
390
+ "complete": len(costs) == len(values) and known_cost is not None,
391
+ "aggregation_status": "out_of_range" if known_cost is None else "finite",
392
+ "observed_invocations": len(costs),
393
+ "basis_counts": dict(cost_bases),
394
+ },
395
+ "tokens": {
396
+ "known_input": sum(inputs),
397
+ "known_output": sum(outputs),
398
+ "input_complete": len(inputs) == len(values),
399
+ "output_complete": len(outputs) == len(values),
400
+ "basis_counts": dict(token_bases),
401
+ },
402
+ "interpretation": "Observed offline invocation overhead; unknown/missing measurements are not zero. No cached invocations are admitted.",
403
+ }
404
+
405
+
406
+ def _disagreements(protocol, declarations, slots):
407
+ result = []
408
+ cohort_ids = {
409
+ example.example_id: fingerprint(asdict(example.request.spec))
410
+ for example in protocol.examples
411
+ }
412
+ for left, right in combinations(sorted(declarations), 2):
413
+ groups = defaultdict(list)
414
+ for example in protocol.examples:
415
+ spec = example.request.spec
416
+ for repeat in range(protocol.repetitions):
417
+ a, b = (
418
+ slots[left, example.example_id, repeat],
419
+ slots[right, example.example_id, repeat],
420
+ )
421
+ pair = {
422
+ "example_id": example.example_id,
423
+ "repetition": repeat,
424
+ "status": "unavailable",
425
+ "left": None,
426
+ "right": None,
427
+ "difference": None,
428
+ }
429
+ if a["status"] == b["status"] == "known":
430
+ pair.update(status="observed", left=a["value"], right=b["value"])
431
+ if spec.kind in {"boolean", "choice"}:
432
+ pair["difference"] = float(a["value"] != b["value"])
433
+ else:
434
+ low, high = (
435
+ (0, 1) if spec.kind == "probability" else (spec.minimum, spec.maximum)
436
+ )
437
+ pair["difference"] = _normalized_difference(
438
+ a["value"], b["value"], low, high
439
+ )
440
+ groups[cohort_ids[example.example_id]].append(pair)
441
+ for key, pairs in groups.items():
442
+ observed = [item["difference"] for item in pairs if item["status"] == "observed"]
443
+ result.append(
444
+ {
445
+ "left_backend": left,
446
+ "right_backend": right,
447
+ "cohort_id": key,
448
+ "planned_pairs": len(pairs),
449
+ "observed_pairs": len(observed),
450
+ "observed_mean_difference": _mean(observed),
451
+ "pairs": pairs,
452
+ "interpretation": "Disagreement is not independent correctness evidence.",
453
+ }
454
+ )
455
+ return result
456
+
457
+
458
+ def summarize_judgment_benchmark(bundle, *, max_invocations=10000, max_pairwise_comparisons=100000):
459
+ """Analyze saved receipts without importing, configuring or executing a backend."""
460
+ owned, protocol, declarations = _load_bundle(bundle)
461
+ planned = len(protocol.examples) * protocol.repetitions * len(declarations)
462
+ if type(max_invocations) is not int or max_invocations < 1 or planned > max_invocations:
463
+ raise ValueError("planned benchmark exceeds max_invocations")
464
+ pairwise = (
465
+ len(declarations)
466
+ * (len(declarations) - 1)
467
+ // 2
468
+ * len(protocol.examples)
469
+ * protocol.repetitions
470
+ )
471
+ if (
472
+ type(max_pairwise_comparisons) is not int
473
+ or max_pairwise_comparisons < 0
474
+ or pairwise > max_pairwise_comparisons
475
+ ):
476
+ raise ValueError("planned benchmark exceeds max_pairwise_comparisons")
477
+ slots, unmatched = _slots(owned, protocol, declarations)
478
+ summaries = []
479
+ by_backend = defaultdict(list)
480
+ for item in slots.values():
481
+ by_backend[item["backend"]].append(item)
482
+ for name, declaration in declarations.items():
483
+ values = by_backend[name]
484
+ counts = Counter(item["status"] for item in values)
485
+ availability = Counter(item["availability"] for item in values)
486
+ denominator = len(values)
487
+ summaries.append(
488
+ {
489
+ **declaration,
490
+ "planned_invocations": denominator,
491
+ "status_counts": dict(counts),
492
+ "known_rate": counts["known"] / denominator,
493
+ "unavailable_rate": (denominator - counts["known"]) / denominator,
494
+ "failure_rate": sum(
495
+ counts[key] for key in ("error", "invalid_result", "invalid_record")
496
+ )
497
+ / denominator,
498
+ "timeout_rate": counts["timeout"] / denominator,
499
+ "abstention_rate": counts["unknown"] / denominator,
500
+ "availability_counts": dict(availability),
501
+ "quality": _quality(protocol, name, slots),
502
+ "performance": _performance(values),
503
+ }
504
+ )
505
+ return {
506
+ "schema_version": BENCHMARK_VERSION,
507
+ "name": protocol.name,
508
+ "version": protocol.version,
509
+ "plan_hash": owned["plan_hash"],
510
+ "protocol_hash": protocol.protocol_hash,
511
+ "source_bundle_hash": fingerprint(owned),
512
+ "environment": owned["environment"],
513
+ "synthetic": protocol.synthetic,
514
+ "simulated_backends": [item["name"] for item in declarations.values() if item["simulated"]],
515
+ "enabled": owned["enabled"],
516
+ "planned_invocations": len(slots),
517
+ "record_count": len(owned["records"]),
518
+ "unmatched_records": unmatched,
519
+ "backends": summaries,
520
+ "disagreements": _disagreements(protocol, declarations, slots),
521
+ "cases": list(slots.values()),
522
+ "interpretation": "Descriptive comparison on frozen examples and caller-declared independent labels. Agreement is not ground truth; no backend ranking or production substitution is authorized.",
523
+ }
524
+
525
+
526
+ def judgment_benchmark_markdown(report):
527
+ lines = [
528
+ f"# Judgment benchmark: {markdown_text(report['name'])}",
529
+ "",
530
+ report["interpretation"],
531
+ "",
532
+ ]
533
+ if report["synthetic"] or report["simulated_backends"]:
534
+ lines.extend(
535
+ [
536
+ "Synthetic inputs or simulated backends are present. These results do not establish real-model performance or empirical optimization gains.",
537
+ "",
538
+ ]
539
+ )
540
+ lines.extend(
541
+ [
542
+ "## Availability and performance",
543
+ "",
544
+ "| Backend | Planned | Known | Failure | Timeout | Abstention | Known cost USD | Cost complete | Observed latency ms |",
545
+ "| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | ---: |",
546
+ ]
547
+ )
548
+ for backend in report["backends"]:
549
+ cells = [
550
+ backend["name"],
551
+ backend["planned_invocations"],
552
+ backend["known_rate"],
553
+ backend["failure_rate"],
554
+ backend["timeout_rate"],
555
+ backend["abstention_rate"],
556
+ backend["performance"]["cost"]["known_usd"],
557
+ backend["performance"]["cost"]["complete"],
558
+ backend["performance"]["latency_ms"]["mean"],
559
+ ]
560
+ lines.append(
561
+ "| "
562
+ + " | ".join(
563
+ markdown_table_cell("unavailable" if item is None else str(item)) for item in cells
564
+ )
565
+ + " |"
566
+ )
567
+ lines.extend(
568
+ [
569
+ "",
570
+ "## Independent-label quality",
571
+ "",
572
+ "Full metric values require complete labelled coverage. Observed values are conditional on the completely observed labelled examples; coverage and failures remain part of the comparison.",
573
+ "",
574
+ "| Backend | Question | Metric | Full value | Observed value | Coverage | Calibration |",
575
+ "| --- | --- | --- | --- | --- | --- | --- |",
576
+ ]
577
+ )
578
+ for backend in report["backends"]:
579
+ for cohort in backend["quality"]:
580
+ cells = [
581
+ backend["name"],
582
+ cohort["spec"]["question"],
583
+ cohort["metric"],
584
+ cohort["value"],
585
+ cohort["observed_value"],
586
+ cohort["coverage"],
587
+ cohort.get("calibration", {}).get("status", "not applicable"),
588
+ ]
589
+ lines.append(
590
+ "| "
591
+ + " | ".join(
592
+ markdown_table_cell("unavailable" if item is None else str(item))
593
+ for item in cells
594
+ )
595
+ + " |"
596
+ )
597
+ lines.extend(
598
+ [
599
+ "",
600
+ "## Backend disagreement",
601
+ "",
602
+ "Agreement with another backend is not independent correctness evidence. Pairwise counts, differences, unavailable pairs and original receipts are retained in the JSON report.",
603
+ "",
604
+ ]
605
+ )
606
+ return "\n".join(lines)