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,304 @@
1
+ """Conservative offline eligibility using native ablation and calibration data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import timedelta
6
+ from math import isclose
7
+
8
+ from agentloop.ablation_protocol import timestamp
9
+ from agentloop.ablations import build_ablation_report
10
+ from agentloop.budgets import BudgetLimits, budget_policy
11
+ from agentloop.calibration import _digest
12
+ from agentloop.harness_evidence import validate_evidence
13
+ from agentloop.promotion_types import BudgetProposal, utc_now
14
+ from agentloop.study_statistics import task_weighted_summary
15
+
16
+
17
+ def assess_promotion(proposal, *, now=None):
18
+ if type(proposal) is not BudgetProposal:
19
+ raise ValueError("promotion assessment requires BudgetProposal")
20
+ moment = timestamp(now or utc_now())
21
+ data = proposal.to_dict()
22
+ spec = data["protocol"]["specification"]
23
+ thresholds = data["thresholds"]
24
+ reasons, diagnostics = [], {}
25
+ earliest = moment - timedelta(days=thresholds["max_age_days"])
26
+ if not earliest <= timestamp(spec["frozen_at"]) <= moment or moment >= timestamp(
27
+ data["valid_until"]
28
+ ):
29
+ reasons.append("evidence_expired_or_future")
30
+ if spec["synthetic"]:
31
+ reasons.append("synthetic_evidence")
32
+ if spec["bootstrap"]["confidence"] < 0.95 or spec["bootstrap"]["samples"] < 200:
33
+ reasons.append("uncertainty_settings_insufficient")
34
+ capability = data["capability"]
35
+ if (
36
+ not capability["passed"]
37
+ or capability["check_count"] == 0
38
+ or capability["adapter"] != "python"
39
+ or capability["source_version"] != spec["versions"]["source"]
40
+ or capability["policy_config_hash"] != data["policy_config_hash"]
41
+ or not earliest <= timestamp(capability["tested_at"]) <= moment
42
+ ):
43
+ reasons.append("capability_evidence_ineligible")
44
+ policy = budget_policy(BudgetLimits(**data["candidate"]))
45
+ expected = {policy.policy_id: policy.version + ":" + policy.config_hash}
46
+ matching = {
47
+ condition["mode"]: name
48
+ for name, condition in spec["conditions"].items()
49
+ if condition["policies"] == expected
50
+ }
51
+ if set(matching) != {"shadow", "enforce"}:
52
+ reasons.append("policy_configuration_mismatch")
53
+ if (
54
+ spec["quality_gate"]["min_score"] < thresholds["min_quality_score"]
55
+ or spec["quality_gate"]["max_regression"] > thresholds["max_quality_regression"]
56
+ ):
57
+ reasons.append("quality_threshold_mismatch")
58
+ try:
59
+ report = build_ablation_report(data["protocol"], data["observations"])
60
+ except ValueError:
61
+ reasons.append("invalid_ablation_evidence")
62
+ report = None
63
+ if report is not None:
64
+ diagnostics["planned_observations"] = report["planned_observation_count"]
65
+ diagnostics["observed_count"] = report["observed_count"]
66
+ diagnostics["status_counts"] = report["status_counts"]
67
+ if (
68
+ report["unplanned_observation_count"]
69
+ or any(row["issues"] for row in report["observations"])
70
+ or report["planned_observation_count"] != report["observed_count"]
71
+ ):
72
+ reasons.append("incomplete_or_mismatched_observations")
73
+ if any(
74
+ timestamp(row["observation"]["started_at"]) > moment for row in report["observations"]
75
+ ):
76
+ reasons.append("future_observations")
77
+ _coverage(data, report, matching, reasons, diagnostics, moment)
78
+ relevant = [
79
+ item
80
+ for item in report["comparisons"]
81
+ if item["split"] == "held_out"
82
+ and (
83
+ (
84
+ item["kind"] == "total_policy_effect"
85
+ and item["candidate"] == matching.get("enforce")
86
+ )
87
+ or (
88
+ item["kind"] == "policy_shadow_overhead"
89
+ and item["candidate"] == matching.get("shadow")
90
+ )
91
+ )
92
+ ]
93
+ diagnostics["held_out_comparisons"] = relevant
94
+ if not relevant:
95
+ reasons.append("missing_held_out_comparison")
96
+ for item in relevant:
97
+ latency = item["descriptive_deltas"]["latency_ms"]
98
+ if (
99
+ latency["planned_task_count"] < thresholds["min_held_out_tasks"]
100
+ or item["planned_pair_count"] < thresholds["min_held_out_pairs"]
101
+ ):
102
+ reasons.append("held_out_sample_too_small")
103
+ counts = item["quality_gate_counts"]
104
+ if counts["accepted"] != item["planned_pair_count"]:
105
+ reasons.append("held_out_quality_ineligible")
106
+ if (
107
+ latency["interval"]["status"] != "computed"
108
+ or latency["pair_summary"]["count"] != item["planned_pair_count"]
109
+ ):
110
+ reasons.append("latency_evidence_missing")
111
+ elif (
112
+ item["kind"] == "total_policy_effect"
113
+ and latency["interval"]["upper"] > -thresholds["min_latency_improvement_ms"]
114
+ ):
115
+ reasons.append("performance_threshold_failed")
116
+ elif (
117
+ item["kind"] == "policy_shadow_overhead"
118
+ and latency["interval"]["upper"] > thresholds["max_shadow_overhead_ms"]
119
+ ):
120
+ reasons.append("shadow_overhead_exceeded")
121
+ cost = item["descriptive_deltas"]["provider_cost_usd"]
122
+ # One price basis is required; mixed or inferred operating cost does
123
+ # not authorize a deployment. Calculated cost remains qualified.
124
+ if (
125
+ cost["pair_summary"]["count"] != item["planned_pair_count"]
126
+ or cost["interval"]["status"] != "computed"
127
+ ):
128
+ reasons.append("cost_evidence_missing")
129
+ elif (
130
+ item["kind"] == "total_policy_effect"
131
+ and cost["interval"]["upper"] > thresholds["max_cost_regression_usd"]
132
+ ):
133
+ reasons.append("cost_regression")
134
+ _calibration(data, spec, moment, earliest, reasons, diagnostics)
135
+ return {
136
+ "schema_version": "1.0",
137
+ "proposal_hash": proposal.proposal_hash,
138
+ "eligible": not reasons,
139
+ "reasons": sorted(set(reasons)),
140
+ "diagnostics": diagnostics,
141
+ "assessed_at": moment.isoformat(),
142
+ "enforcement_authorized": False,
143
+ "interpretation": "Eligibility is conditional evidence for operator review; shadow coverage is not counterfactual benefit and no deployment is activated.",
144
+ }
145
+
146
+
147
+ def _coverage(data, report, matching, reasons, diagnostics, moment):
148
+ seen, covered, planned = set(), 0, 0
149
+ modes = {value: key for key, value in matching.items()}
150
+ for wrapper in report["observations"]:
151
+ row = wrapper["observation"]
152
+ if row["condition"] not in modes:
153
+ continue
154
+ planned += 1
155
+ try:
156
+ identity = row["trace_run_id"]
157
+ if identity is None or identity in seen:
158
+ raise ValueError()
159
+ seen.add(identity)
160
+ envelope = validate_evidence(data["harness_evidence"][identity], trace_id=identity)
161
+ records = [
162
+ item
163
+ for item in envelope["decisions"].values()
164
+ if item["policy_config_hash"] == data["policy_config_hash"]
165
+ and item["mode"] == modes[row["condition"]]
166
+ ]
167
+ started = timestamp(row["started_at"])
168
+ duration = row["metrics"]["latency_ms"]
169
+ if duration is None or any(
170
+ not started
171
+ <= timestamp(item["timing"]["started_at"])
172
+ <= min(moment, started + timedelta(milliseconds=duration + 1000))
173
+ for item in records
174
+ ):
175
+ raise ValueError()
176
+ before = {item["call_id"] for item in records if item["phase"] == "before"}
177
+ after = {item["call_id"] for item in records if item["phase"] == "after"}
178
+ if (
179
+ not before
180
+ or before != after
181
+ or any(item["outcome"] == "failed" for item in records)
182
+ or envelope["capture_errors"]
183
+ ):
184
+ raise ValueError()
185
+ for boundary, metric in (("model", "model_calls"), ("tool", "tool_calls")):
186
+ observed = sum(
187
+ item["phase"] == "after" and item["boundary"] == boundary and item["dispatched"]
188
+ for item in records
189
+ )
190
+ if row["metrics"][metric] != observed:
191
+ raise ValueError()
192
+ costs, usage_ids = [], set()
193
+ for item in records:
194
+ if (
195
+ item["phase"] != "after"
196
+ or item["boundary"] != "model"
197
+ or not item["dispatched"]
198
+ ):
199
+ continue
200
+ usage = item["budget_snapshot"]["last_usage"]
201
+ if (
202
+ not usage
203
+ or not usage["complete"]
204
+ or not usage["exclusive"]
205
+ or usage["cost_provenance"] != "provider_reported"
206
+ or usage["cost_usd"] is None
207
+ ):
208
+ raise ValueError()
209
+ usage_id = usage["usage_id"]
210
+ if usage_id is None or usage_id not in usage_ids:
211
+ costs.append(usage["cost_usd"])
212
+ usage_ids.add(usage_id)
213
+ if row["metrics"]["cost_usd"] is None or not isclose(
214
+ sum(costs), row["metrics"]["cost_usd"], rel_tol=1e-9, abs_tol=1e-12
215
+ ):
216
+ raise ValueError()
217
+ covered += 1
218
+ except (TypeError, ValueError, KeyError):
219
+ reasons.append("decision_coverage_incomplete")
220
+ diagnostics["decision_coverage"] = {"planned": planned, "verified": covered}
221
+
222
+
223
+ def _calibration(data, spec, moment, earliest, reasons, diagnostics):
224
+ report = data["calibration"]
225
+ try:
226
+ if (
227
+ not isinstance(report, dict)
228
+ or report.get("schema_version") != "1.0"
229
+ or report.get("artifact_hash")
230
+ != _digest({key: value for key, value in report.items() if key != "artifact_hash"})
231
+ ):
232
+ raise ValueError()
233
+ if report["selection_inventory_complete"] is not True:
234
+ reasons.append("calibration_inventory_incomplete")
235
+ if (
236
+ not earliest
237
+ <= timestamp(report["validity"]["as_of"])
238
+ <= moment
239
+ < timestamp(report["validity"]["valid_until"])
240
+ ):
241
+ reasons.append("calibration_expired_or_future")
242
+ cohort = next(
243
+ item for item in report["cohorts"] if item["cohort_id"] == data["calibration_cohort"]
244
+ )
245
+ context = cohort["context"]
246
+ cohort_identity = {"context": context, "estimator": cohort["estimator"]}
247
+ if cohort["cohort_id"] != "cal_" + _digest(cohort_identity):
248
+ reasons.append("calibration_context_mismatch")
249
+ if context["cost_basis"] != "provider_reported":
250
+ reasons.append("calibration_cost_basis_mismatch")
251
+ policy = budget_policy(BudgetLimits(**data["candidate"]))
252
+ if (
253
+ context["policy"]
254
+ != {
255
+ "id": policy.policy_id,
256
+ "version": policy.version,
257
+ "config_hash": policy.config_hash,
258
+ }
259
+ or context["workload"] != spec["workload_id"]
260
+ or any(
261
+ context[key] != spec["versions"][key]
262
+ for key in ("model", "provider", "environment", "scorer")
263
+ )
264
+ or context["quality_gate"] != spec["quality_gate"]
265
+ ):
266
+ reasons.append("calibration_context_mismatch")
267
+ rows = [
268
+ row
269
+ for row in report["registrations"]
270
+ if row["cohort_id"] == data["calibration_cohort"] and row["split"] == "held_out"
271
+ ]
272
+ if any(row["cohort"] != cohort_identity for row in rows):
273
+ reasons.append("calibration_context_mismatch")
274
+ diagnostics["calibration_held_out_registrations"] = len(rows)
275
+ diagnostics["calibration_synthetic_count"] = sum(row["synthetic"] for row in rows)
276
+ diagnostics["calibration_selected_count"] = sum(
277
+ row["selection"] == "selected" for row in rows
278
+ )
279
+ for metric, limit in (
280
+ ("latency", "max_calibration_latency_mae_ms"),
281
+ ("cost", "max_calibration_cost_mae_usd"),
282
+ ):
283
+ values = [
284
+ (
285
+ row["task_key"],
286
+ abs(row["metrics"][metric]["signed_error"])
287
+ if row["synthetic"] is False
288
+ and not row["issues"]
289
+ and row["metrics"][metric]["empirical_eligible"] is True
290
+ and row["metrics"][metric]["signed_error"] is not None
291
+ else None,
292
+ )
293
+ for row in rows
294
+ ]
295
+ summary = task_weighted_summary(values, spec["bootstrap"])
296
+ diagnostics["calibration_" + metric] = summary
297
+ if summary["interval"]["task_count"] < data["thresholds"][
298
+ "min_calibration_tasks"
299
+ ] or summary["observation_summary"]["count"] != len(rows):
300
+ reasons.append("calibration_evidence_missing_or_small")
301
+ elif summary["task_summary"]["mean"] > data["thresholds"][limit]:
302
+ reasons.append("calibration_error_exceeded")
303
+ except (TypeError, ValueError, KeyError, StopIteration, OverflowError):
304
+ reasons.append("invalid_or_missing_calibration")
@@ -0,0 +1,209 @@
1
+ """Frozen budget-policy promotion declarations and trusted operator attestations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+ from datetime import datetime, timezone
8
+
9
+ from agentloop.ablation_protocol import AblationProtocol, number, timestamp
10
+ from agentloop.budgets import BudgetLimits, budget_policy
11
+ from agentloop.context_types import canonical, fingerprint, identifier
12
+
13
+
14
+ def utc_now():
15
+ return datetime.now(timezone.utc).isoformat()
16
+
17
+
18
+ def digest(value, label):
19
+ if (
20
+ not isinstance(value, str)
21
+ or len(value) != 64
22
+ or any(char not in "0123456789abcdef" for char in value)
23
+ ):
24
+ raise ValueError(label + " requires a SHA-256 digest")
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class PromotionThresholds:
29
+ min_held_out_tasks: int = 20
30
+ min_held_out_pairs: int = 40
31
+ min_calibration_tasks: int = 20
32
+ max_age_days: float = 30
33
+ min_quality_score: float = 0.95
34
+ max_quality_regression: float = 0
35
+ min_latency_improvement_ms: float = 1
36
+ max_cost_regression_usd: float = 0
37
+ max_shadow_overhead_ms: float = 10
38
+ max_calibration_latency_mae_ms: float = 100
39
+ max_calibration_cost_mae_usd: float = 0.01
40
+
41
+ def __post_init__(self):
42
+ for name, value in asdict(self).items():
43
+ number(
44
+ value,
45
+ name,
46
+ integer=name
47
+ in {"min_held_out_tasks", "min_held_out_pairs", "min_calibration_tasks"},
48
+ )
49
+ if (
50
+ min(self.min_held_out_tasks, self.min_calibration_tasks) < 20
51
+ or self.min_held_out_pairs < self.min_held_out_tasks
52
+ ):
53
+ raise ValueError("promotion requires independent held-out tasks and complete pairs")
54
+ if (
55
+ not 0 < self.max_age_days <= 3650
56
+ or self.min_latency_improvement_ms <= 0
57
+ or max(self.min_quality_score, self.max_quality_regression) > 1
58
+ ):
59
+ raise ValueError("invalid promotion quality/time/performance threshold")
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class CanaryLimits:
64
+ max_admissions: int
65
+ max_duration_s: float
66
+ max_latency_ms: float
67
+ max_cost_usd: float
68
+ min_quality_score: float = 0.95
69
+
70
+ def __post_init__(self):
71
+ for name, value in asdict(self).items():
72
+ number(value, name, integer=name == "max_admissions")
73
+ if (
74
+ not 1 <= self.max_admissions <= 1000
75
+ or not 0 < self.max_duration_s <= 86400
76
+ or self.min_quality_score > 1
77
+ ):
78
+ raise ValueError("canary limits must be finite and bounded")
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class CapabilityEvidence:
83
+ adapter: str
84
+ source_version: str
85
+ policy_config_hash: str
86
+ artifact_hash: str
87
+ tested_at: str
88
+ passed: bool
89
+ check_count: int
90
+
91
+ def __post_init__(self):
92
+ identifier(self.adapter, "adapter")
93
+ if not isinstance(self.source_version, str) or not self.source_version:
94
+ raise ValueError("capability evidence needs a source version")
95
+ digest(self.policy_config_hash, "policy_config_hash")
96
+ digest(self.artifact_hash, "artifact_hash")
97
+ timestamp(self.tested_at)
98
+ if (
99
+ type(self.passed) is not bool
100
+ or type(self.check_count) is not int
101
+ or self.check_count < 0
102
+ ):
103
+ raise ValueError("capability evidence needs explicit results")
104
+
105
+
106
+ @dataclass(frozen=True, init=False)
107
+ class BudgetProposal:
108
+ """Own all evidence; hashes bind a review, not evidence authenticity."""
109
+
110
+ _json: str = field(repr=False)
111
+ proposal_hash: str
112
+
113
+ def __init__(
114
+ self,
115
+ proposal_id,
116
+ *,
117
+ baseline,
118
+ candidate,
119
+ protocol,
120
+ observations,
121
+ calibration,
122
+ calibration_cohort,
123
+ capability,
124
+ thresholds=PromotionThresholds(),
125
+ canary,
126
+ valid_until,
127
+ harness_evidence=None,
128
+ ):
129
+ identifier(proposal_id, "proposal_id")
130
+ if type(baseline) is not BudgetLimits or type(candidate) is not BudgetLimits:
131
+ raise ValueError("only built-in budget policy promotion is supported")
132
+ if baseline != BudgetLimits():
133
+ raise ValueError("initial promotion supports a reviewed tracing-only baseline")
134
+ if baseline.deadline_at is not None or candidate.deadline_at is not None:
135
+ raise ValueError("absolute process deadlines are not portable rollout configuration")
136
+ if (
137
+ type(thresholds) is not PromotionThresholds
138
+ or type(canary) is not CanaryLimits
139
+ or type(capability) is not CapabilityEvidence
140
+ ):
141
+ raise ValueError("typed thresholds, canary and capability evidence required")
142
+ if canary.min_quality_score < thresholds.min_quality_score:
143
+ raise ValueError("canary quality threshold cannot weaken reviewed quality")
144
+ frozen = AblationProtocol.from_dict(
145
+ protocol.to_dict() if isinstance(protocol, AblationProtocol) else protocol
146
+ )
147
+ specification = frozen.to_dict()["specification"]
148
+ if (
149
+ len(specification["tasks"]) > 1000
150
+ or len(specification["schedule"]) > 5000
151
+ or specification["bootstrap"]["samples"] > 10000
152
+ ):
153
+ raise ValueError("promotion study exceeds bounded offline evaluation limits")
154
+ if not isinstance(observations, list) or len(observations) > 100000:
155
+ raise ValueError("promotion observations must be a bounded list")
156
+ timestamp(valid_until)
157
+ if not isinstance(calibration_cohort, str) or not calibration_cohort:
158
+ raise ValueError("explicit calibration cohort reference required")
159
+ data = {
160
+ "schema_version": "1.0",
161
+ "proposal_id": proposal_id,
162
+ "family": "budget",
163
+ "baseline": asdict(baseline),
164
+ "candidate": asdict(candidate),
165
+ "baseline_mode": "disabled",
166
+ "policy_config_hash": budget_policy(candidate).config_hash,
167
+ "protocol": frozen.to_dict(),
168
+ "observations": observations,
169
+ "harness_evidence": {} if harness_evidence is None else harness_evidence,
170
+ "calibration": calibration,
171
+ "calibration_cohort": calibration_cohort,
172
+ "capability": asdict(capability),
173
+ "thresholds": asdict(thresholds),
174
+ "canary": asdict(canary),
175
+ "valid_until": valid_until,
176
+ }
177
+ encoded = canonical(data)
178
+ if len(encoded.encode("utf-8")) > 20_000_000:
179
+ raise ValueError("promotion evidence exceeds twenty million bytes")
180
+ object.__setattr__(self, "_json", encoded)
181
+ object.__setattr__(self, "proposal_hash", fingerprint(data))
182
+
183
+ def to_dict(self):
184
+ return json.loads(self._json)
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class OperatorApproval:
189
+ proposal_hash: str
190
+ reviewer: str
191
+ review_ref: str
192
+ approved: bool
193
+ scopes: tuple[str, ...]
194
+ approved_at: str
195
+ valid_until: str
196
+
197
+ def __post_init__(self):
198
+ digest(self.proposal_hash, "proposal_hash")
199
+ identifier(self.reviewer, "reviewer")
200
+ identifier(self.review_ref, "review_ref")
201
+ if type(self.approved) is not bool:
202
+ raise ValueError("operator decision must be explicit")
203
+ if not isinstance(self.scopes, (tuple, list)) or not 1 <= len(self.scopes) <= 64:
204
+ raise ValueError("approval needs bounded explicit rollout scopes")
205
+ for scope in self.scopes:
206
+ identifier(scope, "scope")
207
+ object.__setattr__(self, "scopes", tuple(sorted(set(self.scopes))))
208
+ if timestamp(self.approved_at) >= timestamp(self.valid_until):
209
+ raise ValueError("operator approval must have a finite validity window")