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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- 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")
|