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,355 @@
|
|
|
1
|
+
"""Versioned declarations for finding-linked, caller-run offline experiments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict, dataclass, field
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
from time import monotonic
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from agentloop.ablation_protocol import number, timestamp
|
|
12
|
+
from agentloop.budget_types import Reservation, ResourceUsage
|
|
13
|
+
from agentloop.context_types import canonical as context_canonical
|
|
14
|
+
from agentloop.context_types import identifier, synchronous
|
|
15
|
+
from agentloop.events import utc_now_iso
|
|
16
|
+
from agentloop.findings import build_diagnosis
|
|
17
|
+
from agentloop.quality import validate_quality_fixtures
|
|
18
|
+
from agentloop.replay import ReplayGates
|
|
19
|
+
from agentloop.tracer import AgentTrace
|
|
20
|
+
|
|
21
|
+
EXPERIMENT_VERSION = "1.0"
|
|
22
|
+
EXPERIMENT_KEY = "agentloop.experiment"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def canonical(value):
|
|
26
|
+
# Validate finite JSON/string keys, then share the existing native evidence
|
|
27
|
+
# escaping convention so Unicode trace/output hashes bind without translation.
|
|
28
|
+
normalized = json.loads(context_canonical(value))
|
|
29
|
+
return json.dumps(normalized, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def fingerprint(value):
|
|
33
|
+
return sha256(canonical(value).encode("utf-8")).hexdigest()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def owned(value):
|
|
37
|
+
return json.loads(canonical(value))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def reference(value, name):
|
|
41
|
+
if not isinstance(value, str) or not value.strip() or len(value) > 1024:
|
|
42
|
+
raise ValueError(name + " must be a bounded nonempty reference")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class ExperimentBudget:
|
|
47
|
+
max_invocations: int
|
|
48
|
+
timeout_s: float
|
|
49
|
+
max_tokens: int | None = None
|
|
50
|
+
max_cost_usd: float | None = None
|
|
51
|
+
|
|
52
|
+
def __post_init__(self):
|
|
53
|
+
number(self.max_invocations, "max_invocations", integer=True, maximum=10000)
|
|
54
|
+
number(self.timeout_s, "timeout_s", maximum=86400)
|
|
55
|
+
number(self.max_tokens, "max_tokens", integer=True, nullable=True)
|
|
56
|
+
number(self.max_cost_usd, "max_cost_usd", nullable=True)
|
|
57
|
+
if not self.max_invocations or not self.timeout_s:
|
|
58
|
+
raise ValueError("experiment invocation and time limits must be positive")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class ExperimentRunner:
|
|
63
|
+
candidate_id: str
|
|
64
|
+
version: str
|
|
65
|
+
implementation_ref: str
|
|
66
|
+
configuration_ref: str
|
|
67
|
+
invoke: Callable = field(repr=False, compare=False)
|
|
68
|
+
reservation: Reservation = field(default_factory=Reservation)
|
|
69
|
+
|
|
70
|
+
def __post_init__(self):
|
|
71
|
+
identifier(self.candidate_id, "candidate_id")
|
|
72
|
+
identifier(self.version, "runner version")
|
|
73
|
+
reference(self.implementation_ref, "implementation_ref")
|
|
74
|
+
reference(self.configuration_ref, "configuration_ref")
|
|
75
|
+
if not synchronous(self.invoke) or type(self.reservation) is not Reservation:
|
|
76
|
+
raise ValueError("runner requires a trusted synchronous callback and reservation")
|
|
77
|
+
|
|
78
|
+
def declaration(self):
|
|
79
|
+
return owned(
|
|
80
|
+
{
|
|
81
|
+
"candidate_id": self.candidate_id,
|
|
82
|
+
"version": self.version,
|
|
83
|
+
"implementation_ref": self.implementation_ref,
|
|
84
|
+
"configuration_ref": self.configuration_ref,
|
|
85
|
+
"reservation": self.reservation.to_dict(),
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, init=False)
|
|
91
|
+
class ExperimentCase:
|
|
92
|
+
case_id: str
|
|
93
|
+
example_id: str
|
|
94
|
+
repetition: str
|
|
95
|
+
_json: str = field(repr=False)
|
|
96
|
+
|
|
97
|
+
def __init__(
|
|
98
|
+
self,
|
|
99
|
+
case_id,
|
|
100
|
+
baseline,
|
|
101
|
+
*,
|
|
102
|
+
target_finding_ids,
|
|
103
|
+
inputs,
|
|
104
|
+
expected,
|
|
105
|
+
baseline_output,
|
|
106
|
+
input_ref,
|
|
107
|
+
quality_ref,
|
|
108
|
+
baseline_implementation_ref,
|
|
109
|
+
baseline_configuration_ref,
|
|
110
|
+
example_id=None,
|
|
111
|
+
repetition="0",
|
|
112
|
+
):
|
|
113
|
+
identifier(case_id, "case_id")
|
|
114
|
+
identifier(example_id or case_id, "example_id")
|
|
115
|
+
identifier(repetition, "repetition")
|
|
116
|
+
for name, value in (
|
|
117
|
+
("input_ref", input_ref),
|
|
118
|
+
("quality_ref", quality_ref),
|
|
119
|
+
("baseline_implementation_ref", baseline_implementation_ref),
|
|
120
|
+
("baseline_configuration_ref", baseline_configuration_ref),
|
|
121
|
+
):
|
|
122
|
+
reference(value, name)
|
|
123
|
+
if not isinstance(baseline, AgentTrace):
|
|
124
|
+
raise ValueError("case requires an existing baseline trace")
|
|
125
|
+
trace = AgentTrace.from_dict(baseline.to_dict())
|
|
126
|
+
if trace.ended_at is None:
|
|
127
|
+
raise ValueError("baseline trace must be complete before planning")
|
|
128
|
+
if "output" in trace.metadata and fingerprint(trace.metadata["output"]) != fingerprint(
|
|
129
|
+
baseline_output
|
|
130
|
+
):
|
|
131
|
+
raise ValueError("baseline output differs from the recorded workflow output")
|
|
132
|
+
diagnosis = build_diagnosis(trace)
|
|
133
|
+
if (
|
|
134
|
+
not isinstance(target_finding_ids, (list, tuple))
|
|
135
|
+
or not target_finding_ids
|
|
136
|
+
or len(target_finding_ids) > 64
|
|
137
|
+
):
|
|
138
|
+
raise ValueError("explicit source finding IDs required")
|
|
139
|
+
ids = sorted(set(target_finding_ids))
|
|
140
|
+
available = {item["finding_id"]: item for item in diagnosis["findings"]}
|
|
141
|
+
if any(identity not in available for identity in ids):
|
|
142
|
+
raise ValueError("source finding must belong to this actual baseline")
|
|
143
|
+
data = {
|
|
144
|
+
"baseline": trace.to_dict(),
|
|
145
|
+
"diagnosis": diagnosis,
|
|
146
|
+
"target_finding_ids": ids,
|
|
147
|
+
"inputs": inputs,
|
|
148
|
+
"expected": expected,
|
|
149
|
+
"baseline_output": baseline_output,
|
|
150
|
+
"input_ref": input_ref,
|
|
151
|
+
"quality_ref": quality_ref,
|
|
152
|
+
"baseline_implementation_ref": baseline_implementation_ref,
|
|
153
|
+
"baseline_configuration_ref": baseline_configuration_ref,
|
|
154
|
+
}
|
|
155
|
+
encoded = canonical(data)
|
|
156
|
+
if len(encoded.encode("utf-8")) > 10_000_000:
|
|
157
|
+
raise ValueError("case exceeds ten million bytes")
|
|
158
|
+
for name, value in (
|
|
159
|
+
("case_id", case_id),
|
|
160
|
+
("example_id", example_id or case_id),
|
|
161
|
+
("repetition", repetition),
|
|
162
|
+
("_json", encoded),
|
|
163
|
+
):
|
|
164
|
+
object.__setattr__(self, name, value)
|
|
165
|
+
|
|
166
|
+
def payload(self):
|
|
167
|
+
return json.loads(self._json)
|
|
168
|
+
|
|
169
|
+
def declaration(self):
|
|
170
|
+
data = self.payload()
|
|
171
|
+
return {
|
|
172
|
+
"case_id": self.case_id,
|
|
173
|
+
"example_id": self.example_id,
|
|
174
|
+
"repetition": self.repetition,
|
|
175
|
+
"baseline_run_id": data["baseline"]["run_id"],
|
|
176
|
+
"baseline_hash": fingerprint(data["baseline"]),
|
|
177
|
+
"source_finding_ids": data["target_finding_ids"],
|
|
178
|
+
"expected_effects": sorted(
|
|
179
|
+
(
|
|
180
|
+
item
|
|
181
|
+
for item in data["diagnosis"]["findings"]
|
|
182
|
+
if item["finding_id"] in data["target_finding_ids"]
|
|
183
|
+
),
|
|
184
|
+
key=lambda item: item["finding_id"],
|
|
185
|
+
),
|
|
186
|
+
"diagnosis_hash": fingerprint(data["diagnosis"]),
|
|
187
|
+
"input_ref": data["input_ref"],
|
|
188
|
+
"input_hash": fingerprint(data["inputs"]),
|
|
189
|
+
"quality_ref": data["quality_ref"],
|
|
190
|
+
"expected_hash": fingerprint(data["expected"]),
|
|
191
|
+
"baseline_output_hash": fingerprint(data["baseline_output"]),
|
|
192
|
+
"baseline_implementation_ref": data["baseline_implementation_ref"],
|
|
193
|
+
"baseline_configuration_ref": data["baseline_configuration_ref"],
|
|
194
|
+
"synthetic": data["baseline"]["metadata"].get("synthetic") is True,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass(frozen=True)
|
|
199
|
+
class ExperimentRequest:
|
|
200
|
+
experiment_id: str
|
|
201
|
+
case_id: str
|
|
202
|
+
candidate_id: str
|
|
203
|
+
input_ref: str
|
|
204
|
+
deadline: float
|
|
205
|
+
_input_json: str = field(repr=False)
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def inputs(self):
|
|
209
|
+
return json.loads(self._input_json)
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def remaining_s(self):
|
|
213
|
+
return max(0.0, self.deadline - monotonic())
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass(frozen=True, init=False)
|
|
217
|
+
class ExperimentResult:
|
|
218
|
+
_json: str = field(repr=False)
|
|
219
|
+
usage: ResourceUsage
|
|
220
|
+
|
|
221
|
+
def __init__(self, output, *, usage=None):
|
|
222
|
+
usage = ResourceUsage() if usage is None else usage
|
|
223
|
+
if type(usage) is not ResourceUsage:
|
|
224
|
+
raise ValueError("experiment usage must be ResourceUsage")
|
|
225
|
+
encoded = canonical(output)
|
|
226
|
+
if len(encoded.encode("utf-8")) > 1_000_000:
|
|
227
|
+
raise ValueError("experiment output exceeds one million bytes")
|
|
228
|
+
object.__setattr__(self, "_json", encoded)
|
|
229
|
+
object.__setattr__(self, "usage", usage)
|
|
230
|
+
|
|
231
|
+
@property
|
|
232
|
+
def output(self):
|
|
233
|
+
return json.loads(self._json)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
@dataclass(frozen=True, init=False)
|
|
237
|
+
class ExperimentPlan:
|
|
238
|
+
_json: str = field(repr=False)
|
|
239
|
+
experiment_id: str
|
|
240
|
+
|
|
241
|
+
def __init__(
|
|
242
|
+
self,
|
|
243
|
+
name,
|
|
244
|
+
*,
|
|
245
|
+
cases,
|
|
246
|
+
runners,
|
|
247
|
+
intervention_type,
|
|
248
|
+
intervention_version,
|
|
249
|
+
scorer,
|
|
250
|
+
gate_version,
|
|
251
|
+
gates,
|
|
252
|
+
budget,
|
|
253
|
+
permission_ref,
|
|
254
|
+
frozen_at=None,
|
|
255
|
+
synthetic=False,
|
|
256
|
+
):
|
|
257
|
+
identifier(name, "experiment name")
|
|
258
|
+
identifier(intervention_type, "intervention_type")
|
|
259
|
+
identifier(intervention_version, "intervention_version")
|
|
260
|
+
identifier(gate_version, "gate_version")
|
|
261
|
+
reference(permission_ref, "permission_ref")
|
|
262
|
+
if (
|
|
263
|
+
not isinstance(cases, (tuple, list))
|
|
264
|
+
or not 1 <= len(cases) <= 1000
|
|
265
|
+
or any(type(case) is not ExperimentCase for case in cases)
|
|
266
|
+
):
|
|
267
|
+
raise ValueError("experiment requires between one and one thousand frozen cases")
|
|
268
|
+
if (
|
|
269
|
+
not isinstance(runners, (tuple, list))
|
|
270
|
+
or not 1 <= len(runners) <= 16
|
|
271
|
+
or any(type(runner) is not ExperimentRunner for runner in runners)
|
|
272
|
+
):
|
|
273
|
+
raise ValueError("experiment requires bounded explicit runner bindings")
|
|
274
|
+
if (
|
|
275
|
+
len({case.case_id for case in cases}) != len(cases)
|
|
276
|
+
or len({(case.example_id, case.repetition) for case in cases}) != len(cases)
|
|
277
|
+
or len({case.payload()["baseline"]["run_id"] for case in cases}) != len(cases)
|
|
278
|
+
or len({runner.candidate_id for runner in runners}) != len(runners)
|
|
279
|
+
):
|
|
280
|
+
raise ValueError("case, baseline-run, pairing and candidate identities must be unique")
|
|
281
|
+
if (
|
|
282
|
+
type(budget) is not ExperimentBudget
|
|
283
|
+
or type(gates) is not ReplayGates
|
|
284
|
+
or type(synthetic) is not bool
|
|
285
|
+
):
|
|
286
|
+
raise ValueError("typed budget/gates and explicit synthetic marker required")
|
|
287
|
+
if gates.min_quality_score is None:
|
|
288
|
+
raise ValueError("experiment gates require independent quality")
|
|
289
|
+
for key, value in asdict(gates).items():
|
|
290
|
+
if key.startswith("require_"):
|
|
291
|
+
if type(value) is not bool:
|
|
292
|
+
raise ValueError("gate flags must be booleans")
|
|
293
|
+
else:
|
|
294
|
+
number(value, key, maximum=1 if key == "min_quality_score" else None)
|
|
295
|
+
scorer = owned(scorer)
|
|
296
|
+
if not isinstance(scorer, dict) or scorer.get("type") == "custom":
|
|
297
|
+
raise ValueError(
|
|
298
|
+
"experiment scorer must be deterministic; executable imports are unsupported"
|
|
299
|
+
)
|
|
300
|
+
identifier(scorer.get("version"), "scorer version")
|
|
301
|
+
validate_quality_fixtures(
|
|
302
|
+
[
|
|
303
|
+
{
|
|
304
|
+
"schema_version": "2.0",
|
|
305
|
+
"id": case.case_id,
|
|
306
|
+
"expected": case.payload()["expected"],
|
|
307
|
+
"scorer": scorer,
|
|
308
|
+
}
|
|
309
|
+
for case in cases
|
|
310
|
+
]
|
|
311
|
+
)
|
|
312
|
+
for runner in runners:
|
|
313
|
+
if (
|
|
314
|
+
budget.max_tokens is not None
|
|
315
|
+
and not runner.reservation.tokens_known
|
|
316
|
+
or budget.max_cost_usd is not None
|
|
317
|
+
and not runner.reservation.cost_known
|
|
318
|
+
):
|
|
319
|
+
raise ValueError("resource limits require declared conservative reservation bounds")
|
|
320
|
+
when = frozen_at or utc_now_iso()
|
|
321
|
+
timestamp(when)
|
|
322
|
+
if any(
|
|
323
|
+
timestamp(case.payload()["baseline"]["ended_at"]) > timestamp(when) for case in cases
|
|
324
|
+
):
|
|
325
|
+
raise ValueError("baseline outcomes must precede the frozen candidate plan")
|
|
326
|
+
data = {
|
|
327
|
+
"schema_version": EXPERIMENT_VERSION,
|
|
328
|
+
"name": name,
|
|
329
|
+
"frozen_at": when,
|
|
330
|
+
"permission_ref": permission_ref,
|
|
331
|
+
"intervention": {"type": intervention_type, "version": intervention_version},
|
|
332
|
+
"cases": [case.declaration() for case in cases],
|
|
333
|
+
"runners": [runner.declaration() for runner in runners],
|
|
334
|
+
"scorer": scorer,
|
|
335
|
+
"scorer_hash": fingerprint(scorer),
|
|
336
|
+
"gate_version": gate_version,
|
|
337
|
+
"gates": asdict(gates),
|
|
338
|
+
"budget": asdict(budget),
|
|
339
|
+
"budget_scope": "new_candidate_attempt_admissions_conservative_reservations_no_refunds",
|
|
340
|
+
"pairing_keys": ["experiment_id", "case_id"],
|
|
341
|
+
"artifact_layout": {
|
|
342
|
+
"baselines": "baselines/<case-sha256>.json",
|
|
343
|
+
"attempts": "attempts/<slot-sha256>/",
|
|
344
|
+
"reports": "reports/<snapshot-sha256>/",
|
|
345
|
+
},
|
|
346
|
+
"synthetic": synthetic or any(case.declaration()["synthetic"] for case in cases),
|
|
347
|
+
}
|
|
348
|
+
encoded = canonical(data)
|
|
349
|
+
if len(encoded.encode("utf-8")) > 20_000_000:
|
|
350
|
+
raise ValueError("experiment plan exceeds twenty million bytes")
|
|
351
|
+
object.__setattr__(self, "_json", encoded)
|
|
352
|
+
object.__setattr__(self, "experiment_id", "experiment_" + fingerprint(data))
|
|
353
|
+
|
|
354
|
+
def to_dict(self):
|
|
355
|
+
return json.loads(self._json)
|