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,407 @@
|
|
|
1
|
+
"""Read-only validation and summaries of frozen substitution trial bundles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from collections import Counter, defaultdict
|
|
7
|
+
from fractions import Fraction
|
|
8
|
+
from statistics import fmean
|
|
9
|
+
|
|
10
|
+
from agentloop.judgment_types import JudgeIdentity, canonical, fingerprint, finite, is_hash, text
|
|
11
|
+
from agentloop.quality import SUPPORTED_SCORER_TYPES
|
|
12
|
+
from agentloop.structured_quality import validate_report
|
|
13
|
+
from agentloop.substitution_evidence import read_trial_evidence
|
|
14
|
+
from agentloop.substitution_types import SUBSTITUTION_VERSION, DecisionStep
|
|
15
|
+
from agentloop.tracer import AgentTrace
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _copy(value):
|
|
19
|
+
return json.loads(canonical(value))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_bundle(bundle, max_invocations=10000):
|
|
23
|
+
owned = _copy(bundle)
|
|
24
|
+
try:
|
|
25
|
+
if owned["schema_version"] != SUBSTITUTION_VERSION or type(owned["enabled"]) is not bool:
|
|
26
|
+
raise ValueError("unsupported substitution bundle")
|
|
27
|
+
protocol, declarations = owned["protocol"], owned["implementations"]
|
|
28
|
+
if (
|
|
29
|
+
protocol["schema_version"] != SUBSTITUTION_VERSION
|
|
30
|
+
or not isinstance(declarations, dict)
|
|
31
|
+
or len(declarations) < 2
|
|
32
|
+
or owned["baseline"] not in declarations
|
|
33
|
+
):
|
|
34
|
+
raise ValueError("invalid experiment plan")
|
|
35
|
+
if owned["plan_hash"] != fingerprint(
|
|
36
|
+
{"protocol": protocol, "baseline": owned["baseline"], "implementations": declarations}
|
|
37
|
+
):
|
|
38
|
+
raise ValueError("experiment plan hash mismatch")
|
|
39
|
+
for key in ("name", "version"):
|
|
40
|
+
text(protocol[key], key)
|
|
41
|
+
step = DecisionStep(**protocol["step"])
|
|
42
|
+
if type(protocol["synthetic"]) is not bool or step.synthetic and not protocol["synthetic"]:
|
|
43
|
+
raise ValueError("synthetic source markers must be preserved")
|
|
44
|
+
for name, declaration in declarations.items():
|
|
45
|
+
text(name, "condition")
|
|
46
|
+
if declaration["name"] != name or declaration["kind"] not in {
|
|
47
|
+
"model",
|
|
48
|
+
"classifier",
|
|
49
|
+
"rule",
|
|
50
|
+
"tool",
|
|
51
|
+
"external_service",
|
|
52
|
+
}:
|
|
53
|
+
raise ValueError("invalid implementation declaration")
|
|
54
|
+
JudgeIdentity(**declaration["identity"])
|
|
55
|
+
examples = {}
|
|
56
|
+
for item in protocol["examples"]:
|
|
57
|
+
for key in ("example_id", "input_ref", "quality_ref"):
|
|
58
|
+
text(item[key], key)
|
|
59
|
+
if (
|
|
60
|
+
item["example_id"] in examples
|
|
61
|
+
or not is_hash(item["input_hash"])
|
|
62
|
+
or item["expected_hash"] is not None
|
|
63
|
+
and not is_hash(item["expected_hash"])
|
|
64
|
+
):
|
|
65
|
+
raise ValueError("invalid or duplicate example declaration")
|
|
66
|
+
examples[item["example_id"]] = item
|
|
67
|
+
if (
|
|
68
|
+
not examples
|
|
69
|
+
or type(protocol["repetitions"]) is not int
|
|
70
|
+
or protocol["repetitions"] < 1
|
|
71
|
+
or type(protocol["seed"]) is not int
|
|
72
|
+
):
|
|
73
|
+
raise ValueError("invalid examples, repetitions or seed")
|
|
74
|
+
for key in ("min_quality", "max_quality_drop"):
|
|
75
|
+
if not finite(protocol[key]) or not 0 <= protocol[key] <= 1:
|
|
76
|
+
raise ValueError("invalid quality threshold")
|
|
77
|
+
if protocol["timeout_s"] is not None and (
|
|
78
|
+
not finite(protocol["timeout_s"]) or protocol["timeout_s"] <= 0
|
|
79
|
+
):
|
|
80
|
+
raise ValueError("invalid timeout")
|
|
81
|
+
scorer = protocol["scorer"]
|
|
82
|
+
text(scorer["version"], "scorer version")
|
|
83
|
+
if scorer["type"] not in SUPPORTED_SCORER_TYPES or not is_hash(scorer["config_hash"]):
|
|
84
|
+
raise ValueError("invalid scorer identity")
|
|
85
|
+
count = len(examples) * protocol["repetitions"] * len(declarations)
|
|
86
|
+
if type(max_invocations) is not int or max_invocations < 1 or count > max_invocations:
|
|
87
|
+
raise ValueError("planned experiment exceeds max_invocations")
|
|
88
|
+
if (
|
|
89
|
+
not isinstance(owned["records"], list)
|
|
90
|
+
or not isinstance(owned["pairs"], list)
|
|
91
|
+
or not isinstance(owned["environment"], dict)
|
|
92
|
+
):
|
|
93
|
+
raise ValueError("invalid record collections or environment")
|
|
94
|
+
if set(owned["environment"]) != {"python", "system", "architecture"} or any(
|
|
95
|
+
not isinstance(value, str) for value in owned["environment"].values()
|
|
96
|
+
):
|
|
97
|
+
raise ValueError("invalid environment declaration")
|
|
98
|
+
return owned, examples
|
|
99
|
+
except (KeyError, TypeError, AttributeError):
|
|
100
|
+
raise ValueError("invalid substitution bundle") from None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def index_trials(bundle, examples):
|
|
104
|
+
indexed, extras = defaultdict(list), []
|
|
105
|
+
for index, row in enumerate(bundle["records"]):
|
|
106
|
+
if (
|
|
107
|
+
not isinstance(row, dict)
|
|
108
|
+
or not isinstance(row.get("condition"), str)
|
|
109
|
+
or row["condition"] not in bundle["implementations"]
|
|
110
|
+
or not isinstance(row.get("example_id"), str)
|
|
111
|
+
or row["example_id"] not in examples
|
|
112
|
+
or type(row.get("repetition")) is not int
|
|
113
|
+
or not 0 <= row["repetition"] < bundle["protocol"]["repetitions"]
|
|
114
|
+
):
|
|
115
|
+
extras.append(index)
|
|
116
|
+
else:
|
|
117
|
+
indexed[row["condition"], row["example_id"], row["repetition"]].append(row)
|
|
118
|
+
slots, run_ids = {}, Counter()
|
|
119
|
+
for rows in indexed.values():
|
|
120
|
+
for row in rows:
|
|
121
|
+
if isinstance(row.get("trace"), dict) and isinstance(row["trace"].get("run_id"), str):
|
|
122
|
+
run_ids[row["trace"]["run_id"]] += 1
|
|
123
|
+
for name, declaration in bundle["implementations"].items():
|
|
124
|
+
for example_id, example in examples.items():
|
|
125
|
+
for repeat in range(bundle["protocol"]["repetitions"]):
|
|
126
|
+
key = name, example_id, repeat
|
|
127
|
+
rows = indexed.get(key, [])
|
|
128
|
+
slot = {
|
|
129
|
+
"condition": name,
|
|
130
|
+
"example_id": example_id,
|
|
131
|
+
"repetition": repeat,
|
|
132
|
+
"status": "missing",
|
|
133
|
+
"trace": None,
|
|
134
|
+
"receipt": None,
|
|
135
|
+
}
|
|
136
|
+
if len(rows) > 1:
|
|
137
|
+
slot["status"] = "duplicate"
|
|
138
|
+
elif rows:
|
|
139
|
+
try:
|
|
140
|
+
trace = AgentTrace.from_dict(_copy(rows[0]["trace"]))
|
|
141
|
+
receipt = read_trial_evidence(trace)
|
|
142
|
+
if (
|
|
143
|
+
receipt is None
|
|
144
|
+
or receipt["status"] != "valid"
|
|
145
|
+
or receipt["plan_hash"] != bundle["plan_hash"]
|
|
146
|
+
or receipt["example_id"] != example_id
|
|
147
|
+
or receipt["repetition"] != repeat
|
|
148
|
+
or receipt["implementation"] != declaration
|
|
149
|
+
or receipt["input_hash"] != example["input_hash"]
|
|
150
|
+
or run_ids[trace.run_id] != 1
|
|
151
|
+
or (not bundle["enabled"] and receipt["outcome"] != "disabled")
|
|
152
|
+
or trace.metadata.get("plan_hash") != bundle["plan_hash"]
|
|
153
|
+
or trace.metadata.get("example_id") != example_id
|
|
154
|
+
or trace.metadata.get("repetition") != repeat
|
|
155
|
+
):
|
|
156
|
+
raise ValueError("trial does not match frozen plan")
|
|
157
|
+
if trace.metadata.get("synthetic") != bundle["protocol"]["synthetic"]:
|
|
158
|
+
raise ValueError("trial changed the synthetic marker")
|
|
159
|
+
slot.update(status=receipt["outcome"], trace=trace, receipt=receipt)
|
|
160
|
+
except (ValueError, TypeError, KeyError, AttributeError):
|
|
161
|
+
slot["status"] = "invalid_record"
|
|
162
|
+
slots[key] = slot
|
|
163
|
+
return slots, extras
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _quality_matches(quality, before, after, example, protocol):
|
|
167
|
+
checked = validate_report(
|
|
168
|
+
quality, baseline_trace=before["trace"], candidate_trace=after["trace"]
|
|
169
|
+
)
|
|
170
|
+
if (
|
|
171
|
+
checked["trace_ids"]
|
|
172
|
+
!= {"baseline": before["trace"].run_id, "candidate": after["trace"].run_id}
|
|
173
|
+
or checked["case_count"] != 1
|
|
174
|
+
or checked["min_score"] != protocol["min_quality"]
|
|
175
|
+
):
|
|
176
|
+
raise ValueError("quality is not bound to this trial pair")
|
|
177
|
+
case = checked["cases"][0]
|
|
178
|
+
scorer = case["scorer"]
|
|
179
|
+
if (
|
|
180
|
+
case["case_id"] != example["example_id"]
|
|
181
|
+
or case["input_ref"] != example["input_ref"]
|
|
182
|
+
or case["expected_ref"] != example["quality_ref"]
|
|
183
|
+
or case["expected_hash"] != example["expected_hash"]
|
|
184
|
+
or scorer["type"] != protocol["scorer"]["type"]
|
|
185
|
+
or scorer["version"] != protocol["scorer"]["version"]
|
|
186
|
+
or scorer["configuration_hash"] != protocol["scorer"]["config_hash"]
|
|
187
|
+
):
|
|
188
|
+
raise ValueError("quality changed the frozen example or scorer")
|
|
189
|
+
for side, trial in (("baseline", before), ("candidate", after)):
|
|
190
|
+
if case[side]["output_hash"] != trial["receipt"]["output_hash"] or case[side][
|
|
191
|
+
"output_present"
|
|
192
|
+
] != (trial["status"] == "completed"):
|
|
193
|
+
raise ValueError("quality output does not match the recorded result")
|
|
194
|
+
if trial["status"] != "completed" and checked[f"{side}_score"] is not None:
|
|
195
|
+
raise ValueError("unavailable output cannot have known quality")
|
|
196
|
+
return checked
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def paired_assessments(bundle, slots, examples):
|
|
200
|
+
indexed, extras = defaultdict(list), []
|
|
201
|
+
candidates = set(bundle["implementations"]) - {bundle["baseline"]}
|
|
202
|
+
for index, row in enumerate(bundle["pairs"]):
|
|
203
|
+
if (
|
|
204
|
+
not isinstance(row, dict)
|
|
205
|
+
or not isinstance(row.get("condition"), str)
|
|
206
|
+
or row["condition"] not in candidates
|
|
207
|
+
or not isinstance(row.get("example_id"), str)
|
|
208
|
+
or row["example_id"] not in examples
|
|
209
|
+
or type(row.get("repetition")) is not int
|
|
210
|
+
or not 0 <= row["repetition"] < bundle["protocol"]["repetitions"]
|
|
211
|
+
):
|
|
212
|
+
extras.append(index)
|
|
213
|
+
else:
|
|
214
|
+
indexed[row["condition"], row["example_id"], row["repetition"]].append(row)
|
|
215
|
+
result = []
|
|
216
|
+
for name in sorted(candidates):
|
|
217
|
+
for example_id, example in examples.items():
|
|
218
|
+
for repeat in range(bundle["protocol"]["repetitions"]):
|
|
219
|
+
key = name, example_id, repeat
|
|
220
|
+
before, after = slots[bundle["baseline"], example_id, repeat], slots[key]
|
|
221
|
+
row = {
|
|
222
|
+
"condition": name,
|
|
223
|
+
"example_id": example_id,
|
|
224
|
+
"repetition": repeat,
|
|
225
|
+
"status": "unavailable",
|
|
226
|
+
"baseline_score": None,
|
|
227
|
+
"candidate_score": None,
|
|
228
|
+
"candidate_passed": None,
|
|
229
|
+
"baseline_agreement": None,
|
|
230
|
+
"quality": None,
|
|
231
|
+
}
|
|
232
|
+
saved = indexed.get(key, [])
|
|
233
|
+
if len(saved) == 1 and before["trace"] is not None and after["trace"] is not None:
|
|
234
|
+
pair = saved[0]
|
|
235
|
+
if (
|
|
236
|
+
pair.get("baseline_run_id") == before["trace"].run_id
|
|
237
|
+
and pair.get("candidate_run_id") == after["trace"].run_id
|
|
238
|
+
):
|
|
239
|
+
if before["status"] == after["status"] == "completed":
|
|
240
|
+
row["baseline_agreement"] = (
|
|
241
|
+
before["receipt"]["output_hash"] == after["receipt"]["output_hash"]
|
|
242
|
+
)
|
|
243
|
+
if pair.get("quality") is not None:
|
|
244
|
+
try:
|
|
245
|
+
quality = _quality_matches(
|
|
246
|
+
pair["quality"], before, after, example, bundle["protocol"]
|
|
247
|
+
)
|
|
248
|
+
row.update(
|
|
249
|
+
status="assessed",
|
|
250
|
+
baseline_score=quality["baseline_score"],
|
|
251
|
+
candidate_score=quality["candidate_score"],
|
|
252
|
+
candidate_passed=quality["summaries"]["candidate"]["passed"],
|
|
253
|
+
quality=quality,
|
|
254
|
+
)
|
|
255
|
+
except (ValueError, TypeError, KeyError):
|
|
256
|
+
row["status"] = "invalid_quality"
|
|
257
|
+
elif pair.get("quality_error") == "assessment_failed":
|
|
258
|
+
row["status"] = "scoring_failed"
|
|
259
|
+
elif len(saved) > 1:
|
|
260
|
+
row["status"] = "duplicate_quality"
|
|
261
|
+
result.append(row)
|
|
262
|
+
return result, extras
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _mean(values):
|
|
266
|
+
if not values:
|
|
267
|
+
return None
|
|
268
|
+
try:
|
|
269
|
+
return fmean(values)
|
|
270
|
+
except OverflowError:
|
|
271
|
+
return float(sum((Fraction(str(value)) for value in values), Fraction()) / len(values))
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _exact_mean(values):
|
|
275
|
+
return (
|
|
276
|
+
sum((Fraction(str(value)) for value in values), Fraction()) / len(values)
|
|
277
|
+
if values
|
|
278
|
+
else None
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def summarize_substitution_experiment(bundle, *, max_invocations=10000):
|
|
283
|
+
owned, examples = load_bundle(bundle, max_invocations)
|
|
284
|
+
slots, unmatched = index_trials(owned, examples)
|
|
285
|
+
pairs, unmatched_pairs = paired_assessments(owned, slots, examples)
|
|
286
|
+
by_condition, comparisons_by_condition = defaultdict(list), defaultdict(list)
|
|
287
|
+
for row in slots.values():
|
|
288
|
+
by_condition[row["condition"]].append(row)
|
|
289
|
+
for row in pairs:
|
|
290
|
+
comparisons_by_condition[row["condition"]].append(row)
|
|
291
|
+
results = []
|
|
292
|
+
for name, declaration in owned["implementations"].items():
|
|
293
|
+
rows = by_condition[name]
|
|
294
|
+
counts = Counter(item["status"] for item in rows)
|
|
295
|
+
receipts = [item["receipt"] for item in rows if item["receipt"] is not None]
|
|
296
|
+
metrics = {}
|
|
297
|
+
for key in ("latency_ms", "input_tokens", "output_tokens", "cost_usd"):
|
|
298
|
+
values = [
|
|
299
|
+
item["latency_ms"] if key == "latency_ms" else item["usage"][key]
|
|
300
|
+
for item in receipts
|
|
301
|
+
]
|
|
302
|
+
known = [value for value in values if value is not None]
|
|
303
|
+
metrics[key] = {
|
|
304
|
+
"observed_mean": _mean(known),
|
|
305
|
+
"observed_count": len(known),
|
|
306
|
+
"missing_count": len(rows) - len(known),
|
|
307
|
+
"complete": len(known) == len(rows),
|
|
308
|
+
}
|
|
309
|
+
results.append(
|
|
310
|
+
{
|
|
311
|
+
**declaration,
|
|
312
|
+
"planned": len(rows),
|
|
313
|
+
"status_counts": dict(counts),
|
|
314
|
+
"failure_rate": sum(
|
|
315
|
+
counts[key]
|
|
316
|
+
for key in ("failed", "timed_out", "invalid_result", "invalid_record")
|
|
317
|
+
)
|
|
318
|
+
/ len(rows),
|
|
319
|
+
"unavailable_output_rate": (len(rows) - counts["completed"]) / len(rows),
|
|
320
|
+
"measurements": metrics,
|
|
321
|
+
"cost_basis_counts": dict(
|
|
322
|
+
Counter(item["usage"]["cost_basis"] for item in receipts)
|
|
323
|
+
),
|
|
324
|
+
"token_basis_counts": dict(
|
|
325
|
+
Counter(item["usage"]["token_basis"] for item in receipts)
|
|
326
|
+
),
|
|
327
|
+
}
|
|
328
|
+
)
|
|
329
|
+
comparisons = []
|
|
330
|
+
for name, values in comparisons_by_condition.items():
|
|
331
|
+
by_example = defaultdict(list)
|
|
332
|
+
for item in values:
|
|
333
|
+
by_example[item["example_id"]].append(item)
|
|
334
|
+
complete_examples = [
|
|
335
|
+
rows
|
|
336
|
+
for rows in by_example.values()
|
|
337
|
+
if len(rows) == owned["protocol"]["repetitions"]
|
|
338
|
+
and all(
|
|
339
|
+
item["baseline_score"] is not None and item["candidate_score"] is not None
|
|
340
|
+
for item in rows
|
|
341
|
+
)
|
|
342
|
+
]
|
|
343
|
+
complete = len(complete_examples) == len(examples)
|
|
344
|
+
before = _exact_mean(
|
|
345
|
+
[_exact_mean([item["baseline_score"] for item in rows]) for rows in complete_examples]
|
|
346
|
+
)
|
|
347
|
+
after = _exact_mean(
|
|
348
|
+
[_exact_mean([item["candidate_score"] for item in rows]) for rows in complete_examples]
|
|
349
|
+
)
|
|
350
|
+
agreement = [
|
|
351
|
+
float(item["baseline_agreement"])
|
|
352
|
+
for item in values
|
|
353
|
+
if item["baseline_agreement"] is not None
|
|
354
|
+
]
|
|
355
|
+
preserved = None
|
|
356
|
+
if complete:
|
|
357
|
+
preserved = (
|
|
358
|
+
all(item["candidate_passed"] for item in values)
|
|
359
|
+
and after >= Fraction(str(owned["protocol"]["min_quality"]))
|
|
360
|
+
and before - after <= Fraction(str(owned["protocol"]["max_quality_drop"]))
|
|
361
|
+
)
|
|
362
|
+
comparisons.append(
|
|
363
|
+
{
|
|
364
|
+
"condition": name,
|
|
365
|
+
"planned_pairs": len(values),
|
|
366
|
+
"complete_quality_examples": len(complete_examples),
|
|
367
|
+
"quality_complete": complete,
|
|
368
|
+
"quality_status_counts": dict(Counter(item["status"] for item in values)),
|
|
369
|
+
"observed_baseline_quality": float(before) if before is not None else None,
|
|
370
|
+
"observed_candidate_quality": float(after) if after is not None else None,
|
|
371
|
+
"observed_quality_delta": float(after - before)
|
|
372
|
+
if before is not None and after is not None
|
|
373
|
+
else None,
|
|
374
|
+
"quality_preserved_on_examples": preserved,
|
|
375
|
+
"weighting": "equal examples; all planned repetitions must have known quality",
|
|
376
|
+
"baseline_agreement": {
|
|
377
|
+
"method": "typed_canonical_json_equality",
|
|
378
|
+
"observed_rate": _mean(agreement),
|
|
379
|
+
"observed_count": len(agreement),
|
|
380
|
+
"missing_count": len(values) - len(agreement),
|
|
381
|
+
"is_independent_quality": False,
|
|
382
|
+
},
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
return {
|
|
386
|
+
"schema_version": SUBSTITUTION_VERSION,
|
|
387
|
+
"name": owned["protocol"]["name"],
|
|
388
|
+
"plan_hash": owned["plan_hash"],
|
|
389
|
+
"source_bundle_hash": fingerprint(owned),
|
|
390
|
+
"baseline": owned["baseline"],
|
|
391
|
+
"synthetic": owned["protocol"]["synthetic"],
|
|
392
|
+
"environment": owned["environment"],
|
|
393
|
+
"example_count": len(examples),
|
|
394
|
+
"unique_input_count": len({item["input_hash"] for item in examples.values()}),
|
|
395
|
+
"sample_status": "exploratory",
|
|
396
|
+
"interpretation": "Exploratory offline comparison on frozen examples; baseline agreement is not independent quality and no candidate is activated. Small samples and shared baseline observations cannot establish general model capability.",
|
|
397
|
+
"measurement_scope": "declared_decision_step",
|
|
398
|
+
"implementations": results,
|
|
399
|
+
"comparisons": comparisons,
|
|
400
|
+
"pairs": pairs,
|
|
401
|
+
"unmatched_records": unmatched,
|
|
402
|
+
"unmatched_pairs": unmatched_pairs,
|
|
403
|
+
"trial_statuses": [
|
|
404
|
+
{key: row[key] for key in ("condition", "example_id", "repetition", "status")}
|
|
405
|
+
for row in slots.values()
|
|
406
|
+
],
|
|
407
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Frozen declarations for explicit offline decision implementation experiments."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import asdict, dataclass, field
|
|
8
|
+
from functools import cached_property
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
from agentloop.judgment_types import (
|
|
12
|
+
JudgeIdentity,
|
|
13
|
+
JudgeUsage,
|
|
14
|
+
canonical,
|
|
15
|
+
fingerprint,
|
|
16
|
+
finite,
|
|
17
|
+
is_hash,
|
|
18
|
+
text,
|
|
19
|
+
)
|
|
20
|
+
from agentloop.quality import validate_quality_fixtures
|
|
21
|
+
|
|
22
|
+
SUBSTITUTION_VERSION = "1.0"
|
|
23
|
+
TRIAL_KEY = "agentloop.substitution_trial"
|
|
24
|
+
_MISSING = object()
|
|
25
|
+
DecisionIdentity = JudgeIdentity
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class DecisionStep:
|
|
30
|
+
step_id: str
|
|
31
|
+
version: str
|
|
32
|
+
source_run_id: str
|
|
33
|
+
source_span_id: str
|
|
34
|
+
source_trace_hash: str
|
|
35
|
+
synthetic: bool = False
|
|
36
|
+
|
|
37
|
+
def __post_init__(self):
|
|
38
|
+
for name in ("step_id", "version", "source_run_id", "source_span_id"):
|
|
39
|
+
text(getattr(self, name), name)
|
|
40
|
+
if not is_hash(self.source_trace_hash) or type(self.synthetic) is not bool:
|
|
41
|
+
raise ValueError("step requires a source fingerprint and boolean synthetic marker")
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def from_trace(cls, trace, span_id, *, step_id, version):
|
|
45
|
+
from agentloop.tracer import AgentTrace
|
|
46
|
+
|
|
47
|
+
owned = AgentTrace.from_dict(trace.to_dict())
|
|
48
|
+
if span_id not in {item.event_id for item in owned.events}:
|
|
49
|
+
raise ValueError("source span must exist in the source trace")
|
|
50
|
+
return cls(
|
|
51
|
+
step_id,
|
|
52
|
+
version,
|
|
53
|
+
owned.run_id,
|
|
54
|
+
span_id,
|
|
55
|
+
fingerprint(owned.to_dict()),
|
|
56
|
+
owned.metadata.get("synthetic") is True,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True, init=False)
|
|
61
|
+
class SubstitutionExample:
|
|
62
|
+
example_id: str
|
|
63
|
+
input_ref: str
|
|
64
|
+
quality_ref: str
|
|
65
|
+
_input_json: str = field(repr=False)
|
|
66
|
+
_expected_json: str | None = field(repr=False)
|
|
67
|
+
|
|
68
|
+
def __init__(self, example_id, inputs, *, input_ref, quality_ref, expected=_MISSING):
|
|
69
|
+
for name, value in (
|
|
70
|
+
("example_id", example_id),
|
|
71
|
+
("input_ref", input_ref),
|
|
72
|
+
("quality_ref", quality_ref),
|
|
73
|
+
):
|
|
74
|
+
text(value, name)
|
|
75
|
+
object.__setattr__(self, name, value)
|
|
76
|
+
object.__setattr__(self, "_input_json", canonical(inputs))
|
|
77
|
+
object.__setattr__(
|
|
78
|
+
self, "_expected_json", None if expected is _MISSING else canonical(expected)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def inputs(self):
|
|
83
|
+
return json.loads(self._input_json)
|
|
84
|
+
|
|
85
|
+
def fixture(self, scorer):
|
|
86
|
+
return {
|
|
87
|
+
"schema_version": "2.0",
|
|
88
|
+
"id": self.example_id,
|
|
89
|
+
"input_ref": self.input_ref,
|
|
90
|
+
"scorer": scorer,
|
|
91
|
+
"expected_ref": self.quality_ref,
|
|
92
|
+
**(
|
|
93
|
+
{"expected": json.loads(self._expected_json)}
|
|
94
|
+
if self._expected_json is not None
|
|
95
|
+
else {}
|
|
96
|
+
),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def declaration(self):
|
|
100
|
+
return {
|
|
101
|
+
"example_id": self.example_id,
|
|
102
|
+
"input_ref": self.input_ref,
|
|
103
|
+
"input_hash": fingerprint(self.inputs),
|
|
104
|
+
"quality_ref": self.quality_ref,
|
|
105
|
+
"expected_hash": fingerprint(json.loads(self._expected_json))
|
|
106
|
+
if self._expected_json is not None
|
|
107
|
+
else None,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass(frozen=True)
|
|
112
|
+
class DecisionRequest:
|
|
113
|
+
example_id: str
|
|
114
|
+
input_ref: str
|
|
115
|
+
step: DecisionStep
|
|
116
|
+
_input_json: str = field(repr=False)
|
|
117
|
+
|
|
118
|
+
@cached_property
|
|
119
|
+
def inputs(self):
|
|
120
|
+
"""Own one decoded input per invocation, isolated from every other trial."""
|
|
121
|
+
return json.loads(self._input_json)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True)
|
|
125
|
+
class DecisionImplementation:
|
|
126
|
+
name: str
|
|
127
|
+
identity: JudgeIdentity
|
|
128
|
+
invoke: Callable = field(repr=False)
|
|
129
|
+
kind: str = "model"
|
|
130
|
+
|
|
131
|
+
def __post_init__(self):
|
|
132
|
+
text(self.name, "implementation name")
|
|
133
|
+
if type(self.identity) is not JudgeIdentity or self.kind not in {
|
|
134
|
+
"model",
|
|
135
|
+
"classifier",
|
|
136
|
+
"rule",
|
|
137
|
+
"tool",
|
|
138
|
+
"external_service",
|
|
139
|
+
}:
|
|
140
|
+
raise ValueError("implementation requires a versioned identity and supported kind")
|
|
141
|
+
if (
|
|
142
|
+
not callable(self.invoke)
|
|
143
|
+
or inspect.iscoroutinefunction(self.invoke)
|
|
144
|
+
or inspect.isasyncgenfunction(self.invoke)
|
|
145
|
+
):
|
|
146
|
+
raise ValueError("implementation must be a trusted synchronous callable")
|
|
147
|
+
|
|
148
|
+
def declaration(self):
|
|
149
|
+
return {"name": self.name, "identity": asdict(self.identity), "kind": self.kind}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True, init=False)
|
|
153
|
+
class DecisionResult:
|
|
154
|
+
status: str
|
|
155
|
+
usage: JudgeUsage
|
|
156
|
+
cost_ref: str | None
|
|
157
|
+
_output_json: str | None = field(repr=False)
|
|
158
|
+
|
|
159
|
+
def __init__(self, output=_MISSING, *, status="completed", usage=None, cost_ref=None):
|
|
160
|
+
if status not in {"completed", "unknown"}:
|
|
161
|
+
raise ValueError("result status must be completed or unknown")
|
|
162
|
+
if (status == "completed") != (output is not _MISSING):
|
|
163
|
+
raise ValueError(
|
|
164
|
+
"completed results require an output; unknown results cannot carry one"
|
|
165
|
+
)
|
|
166
|
+
usage = JudgeUsage() if usage is None else usage
|
|
167
|
+
if type(usage) is not JudgeUsage:
|
|
168
|
+
raise ValueError("usage must be JudgeUsage")
|
|
169
|
+
text(cost_ref, "cost_ref", optional=True)
|
|
170
|
+
object.__setattr__(self, "status", status)
|
|
171
|
+
object.__setattr__(self, "usage", usage)
|
|
172
|
+
object.__setattr__(self, "cost_ref", cost_ref)
|
|
173
|
+
object.__setattr__(self, "_output_json", None if output is _MISSING else canonical(output))
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def output(self):
|
|
177
|
+
if self._output_json is None:
|
|
178
|
+
raise ValueError("result has no observed output")
|
|
179
|
+
return json.loads(self._output_json)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True, init=False)
|
|
183
|
+
class SubstitutionProtocol:
|
|
184
|
+
name: str
|
|
185
|
+
version: str
|
|
186
|
+
step: DecisionStep
|
|
187
|
+
examples: tuple[SubstitutionExample, ...]
|
|
188
|
+
repetitions: int
|
|
189
|
+
seed: int
|
|
190
|
+
timeout_s: float | None
|
|
191
|
+
min_quality: float
|
|
192
|
+
max_quality_drop: float
|
|
193
|
+
synthetic: bool
|
|
194
|
+
_scorer_json: str = field(repr=False)
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
name,
|
|
199
|
+
version,
|
|
200
|
+
step,
|
|
201
|
+
examples,
|
|
202
|
+
scorer,
|
|
203
|
+
*,
|
|
204
|
+
repetitions=1,
|
|
205
|
+
seed=0,
|
|
206
|
+
timeout_s=None,
|
|
207
|
+
min_quality=1.0,
|
|
208
|
+
max_quality_drop=0.0,
|
|
209
|
+
synthetic=False,
|
|
210
|
+
):
|
|
211
|
+
text(name, "name")
|
|
212
|
+
text(version, "version")
|
|
213
|
+
if (
|
|
214
|
+
type(step) is not DecisionStep
|
|
215
|
+
or not isinstance(examples, (tuple, list))
|
|
216
|
+
or not examples
|
|
217
|
+
or any(type(item) is not SubstitutionExample for item in examples)
|
|
218
|
+
):
|
|
219
|
+
raise ValueError("protocol requires a source step and nonempty frozen examples")
|
|
220
|
+
if len({item.example_id for item in examples}) != len(examples):
|
|
221
|
+
raise ValueError("example IDs must be unique")
|
|
222
|
+
if (
|
|
223
|
+
type(repetitions) is not int
|
|
224
|
+
or repetitions < 1
|
|
225
|
+
or type(seed) is not int
|
|
226
|
+
or type(synthetic) is not bool
|
|
227
|
+
):
|
|
228
|
+
raise ValueError("invalid repetition, seed or synthetic declaration")
|
|
229
|
+
if timeout_s is not None and (not finite(timeout_s) or timeout_s <= 0):
|
|
230
|
+
raise ValueError("timeout_s must be finite and positive")
|
|
231
|
+
for value in (min_quality, max_quality_drop):
|
|
232
|
+
if not finite(value) or not 0 <= value <= 1:
|
|
233
|
+
raise ValueError("quality thresholds must be in [0, 1]")
|
|
234
|
+
if not isinstance(scorer, dict):
|
|
235
|
+
raise ValueError("scorer must be a versioned structured quality configuration")
|
|
236
|
+
text(scorer.get("version"), "scorer version")
|
|
237
|
+
owned = json.loads(canonical(scorer))
|
|
238
|
+
validate_quality_fixtures([item.fixture(owned) for item in examples])
|
|
239
|
+
values = {
|
|
240
|
+
"name": name,
|
|
241
|
+
"version": version,
|
|
242
|
+
"step": step,
|
|
243
|
+
"examples": tuple(examples),
|
|
244
|
+
"repetitions": repetitions,
|
|
245
|
+
"seed": seed,
|
|
246
|
+
"timeout_s": timeout_s,
|
|
247
|
+
"min_quality": min_quality,
|
|
248
|
+
"max_quality_drop": max_quality_drop,
|
|
249
|
+
"synthetic": synthetic or step.synthetic,
|
|
250
|
+
"_scorer_json": canonical(owned),
|
|
251
|
+
}
|
|
252
|
+
for key, value in values.items():
|
|
253
|
+
object.__setattr__(self, key, value)
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def scorer(self):
|
|
257
|
+
return json.loads(self._scorer_json)
|
|
258
|
+
|
|
259
|
+
def declaration(self):
|
|
260
|
+
scorer = self.scorer
|
|
261
|
+
examples = []
|
|
262
|
+
for item in self.examples:
|
|
263
|
+
declared = item.declaration()
|
|
264
|
+
if item._expected_json is None and "expected" in scorer:
|
|
265
|
+
declared["expected_hash"] = fingerprint(scorer["expected"])
|
|
266
|
+
examples.append(declared)
|
|
267
|
+
return {
|
|
268
|
+
"schema_version": SUBSTITUTION_VERSION,
|
|
269
|
+
"name": self.name,
|
|
270
|
+
"version": self.version,
|
|
271
|
+
"step": asdict(self.step),
|
|
272
|
+
"examples": examples,
|
|
273
|
+
"scorer": {
|
|
274
|
+
"type": scorer["type"],
|
|
275
|
+
"version": scorer["version"],
|
|
276
|
+
"config_hash": fingerprint(scorer),
|
|
277
|
+
},
|
|
278
|
+
"repetitions": self.repetitions,
|
|
279
|
+
"seed": self.seed,
|
|
280
|
+
"timeout_s": self.timeout_s,
|
|
281
|
+
"min_quality": self.min_quality,
|
|
282
|
+
"max_quality_drop": self.max_quality_drop,
|
|
283
|
+
"synthetic": self.synthetic,
|
|
284
|
+
}
|