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,545 @@
|
|
|
1
|
+
"""Read-only experiment evidence and native store/study export adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from fractions import Fraction
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from agentloop.ablation_protocol import timestamp
|
|
11
|
+
from agentloop.budget_types import Reservation, ResourceUsage, money_text
|
|
12
|
+
from agentloop.experiment_artifacts import (
|
|
13
|
+
journal_lock,
|
|
14
|
+
local_path,
|
|
15
|
+
read_json,
|
|
16
|
+
write_bytes_once,
|
|
17
|
+
write_once,
|
|
18
|
+
)
|
|
19
|
+
from agentloop.experiment_observations import summarize_observations
|
|
20
|
+
from agentloop.experiment_types import canonical, fingerprint
|
|
21
|
+
from agentloop.experiments import slot_id
|
|
22
|
+
from agentloop.harness_evidence import METADATA_KEY, validate_evidence
|
|
23
|
+
from agentloop.interventions import InterventionRecord
|
|
24
|
+
from agentloop.markdown import markdown_heading, markdown_table_cell
|
|
25
|
+
from agentloop.structured_quality import EVIDENCE_KEY, attach_quality_report, validate_report
|
|
26
|
+
from agentloop.studies import summarize_study
|
|
27
|
+
from agentloop.tracer import AgentTrace
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def read_experiment(root):
|
|
31
|
+
root = Path(root).resolve()
|
|
32
|
+
envelope = read_json(local_path(root, "experiment.json"))
|
|
33
|
+
spec = envelope["specification"]
|
|
34
|
+
identity = "experiment_" + fingerprint(spec)
|
|
35
|
+
if envelope["experiment_id"] != identity or spec["schema_version"] != "1.0":
|
|
36
|
+
raise ValueError("experiment declaration hash or schema changed")
|
|
37
|
+
rows, planned = [], set()
|
|
38
|
+
baselines = {}
|
|
39
|
+
for case in spec["cases"]:
|
|
40
|
+
key = fingerprint(case["case_id"])
|
|
41
|
+
baseline = read_json(local_path(root, f"baselines/{key}.json"))
|
|
42
|
+
diagnosis = read_json(local_path(root, f"predictions/{key}.json"))
|
|
43
|
+
if (
|
|
44
|
+
fingerprint(baseline) != case["baseline_hash"]
|
|
45
|
+
or fingerprint(diagnosis) != case["diagnosis_hash"]
|
|
46
|
+
):
|
|
47
|
+
raise ValueError("frozen baseline or prediction changed")
|
|
48
|
+
if timestamp(baseline["ended_at"]) > timestamp(spec["frozen_at"]):
|
|
49
|
+
raise ValueError("baseline outcome follows the frozen candidate plan")
|
|
50
|
+
selected = sorted(
|
|
51
|
+
(
|
|
52
|
+
item
|
|
53
|
+
for item in diagnosis["findings"]
|
|
54
|
+
if item["finding_id"] in case["source_finding_ids"]
|
|
55
|
+
),
|
|
56
|
+
key=lambda item: item["finding_id"],
|
|
57
|
+
)
|
|
58
|
+
if selected != case["expected_effects"] or diagnosis["run_id"] != baseline["run_id"]:
|
|
59
|
+
raise ValueError("expected effects are not bound to their source baseline")
|
|
60
|
+
baselines[case["case_id"]] = baseline
|
|
61
|
+
for runner in spec["runners"]:
|
|
62
|
+
slot = slot_id(identity, case["case_id"], runner["candidate_id"])
|
|
63
|
+
planned.add(slot)
|
|
64
|
+
directory = f"attempts/{slot}"
|
|
65
|
+
row = {
|
|
66
|
+
"slot_id": slot,
|
|
67
|
+
"case_id": case["case_id"],
|
|
68
|
+
"candidate_id": runner["candidate_id"],
|
|
69
|
+
"status": "not_started",
|
|
70
|
+
"admission": None,
|
|
71
|
+
"receipt": None,
|
|
72
|
+
"trace": None,
|
|
73
|
+
"quality": None,
|
|
74
|
+
"intervention": None,
|
|
75
|
+
}
|
|
76
|
+
start = local_path(root, directory + "/started.json")
|
|
77
|
+
finish = local_path(root, directory + "/receipt.json")
|
|
78
|
+
if start.exists():
|
|
79
|
+
admission = read_json(start)
|
|
80
|
+
if (
|
|
81
|
+
admission.get("experiment_id") != identity
|
|
82
|
+
or admission.get("slot_id") != slot
|
|
83
|
+
or admission.get("case_id") != case["case_id"]
|
|
84
|
+
or admission.get("candidate_id") != runner["candidate_id"]
|
|
85
|
+
or admission.get("runner") != runner
|
|
86
|
+
):
|
|
87
|
+
raise ValueError("admission record differs from frozen experiment")
|
|
88
|
+
row["status"] = "unresolved"
|
|
89
|
+
if timestamp(admission["started_at"]) < timestamp(spec["frozen_at"]):
|
|
90
|
+
raise ValueError("candidate admission precedes frozen expected effects")
|
|
91
|
+
row["admission"] = admission
|
|
92
|
+
if finish.exists():
|
|
93
|
+
receipt = read_json(finish)
|
|
94
|
+
if (
|
|
95
|
+
receipt.get("schema_version") != "1.0"
|
|
96
|
+
or receipt.get("experiment_id") != identity
|
|
97
|
+
or receipt.get("slot_id") != slot
|
|
98
|
+
or receipt.get("started_hash") != fingerprint(admission)
|
|
99
|
+
or receipt.get("status")
|
|
100
|
+
not in {"completed", "failed", "timed_out", "cancelled", "stopped"}
|
|
101
|
+
or set(receipt["artifact_hashes"])
|
|
102
|
+
!= {"trace.json", "quality.json", "intervention.json"}
|
|
103
|
+
):
|
|
104
|
+
raise ValueError("invalid experiment receipt")
|
|
105
|
+
for key, filename in (
|
|
106
|
+
("trace", "trace.json"),
|
|
107
|
+
("quality", "quality.json"),
|
|
108
|
+
("intervention", "intervention.json"),
|
|
109
|
+
):
|
|
110
|
+
value = read_json(local_path(root, directory + "/" + filename))
|
|
111
|
+
if fingerprint(value) != receipt["artifact_hashes"][filename]:
|
|
112
|
+
raise ValueError("experiment result artifact changed")
|
|
113
|
+
row[key] = value
|
|
114
|
+
trace = AgentTrace.from_dict(row["trace"])
|
|
115
|
+
if (
|
|
116
|
+
timestamp(trace.started_at) < timestamp(admission["started_at"])
|
|
117
|
+
or trace.ended_at is None
|
|
118
|
+
or timestamp(trace.ended_at) < timestamp(trace.started_at)
|
|
119
|
+
):
|
|
120
|
+
raise ValueError("candidate timing differs from its admission")
|
|
121
|
+
validate_report(
|
|
122
|
+
row["quality"],
|
|
123
|
+
baseline_trace=AgentTrace.from_dict(baseline),
|
|
124
|
+
candidate_trace=trace,
|
|
125
|
+
)
|
|
126
|
+
record = InterventionRecord.from_dict(row["intervention"]).to_dict()
|
|
127
|
+
native = validate_evidence(trace.metadata[METADATA_KEY], trace_id=trace.run_id)
|
|
128
|
+
controls = [
|
|
129
|
+
item
|
|
130
|
+
for item in native["decisions"].values()
|
|
131
|
+
if item["call_id"] == receipt["budget_call_id"]
|
|
132
|
+
and item["harness_config_hash"] == receipt["budget_config_hash"]
|
|
133
|
+
and item["policy_id"] == "budget"
|
|
134
|
+
]
|
|
135
|
+
if (
|
|
136
|
+
len(controls) != 2
|
|
137
|
+
or {item["phase"] for item in controls} != {"before", "after"}
|
|
138
|
+
or receipt["halted"]
|
|
139
|
+
!= any(
|
|
140
|
+
item["mode"] == "enforce"
|
|
141
|
+
and item["resolved_action"] in {"stop", "escalate"}
|
|
142
|
+
for item in controls
|
|
143
|
+
)
|
|
144
|
+
or receipt["budget_dispatched"]
|
|
145
|
+
!= next(item["dispatched"] for item in controls if item["phase"] == "after")
|
|
146
|
+
):
|
|
147
|
+
raise ValueError("experiment admission budget evidence changed")
|
|
148
|
+
quality_case = row["quality"]["cases"][0]
|
|
149
|
+
execution_status = {
|
|
150
|
+
"completed": "completed",
|
|
151
|
+
"cancelled": "cancelled",
|
|
152
|
+
"timed_out": "timed_out",
|
|
153
|
+
}.get(receipt["status"], "failed")
|
|
154
|
+
if quality_case["candidate"]["execution_status"] != execution_status or receipt[
|
|
155
|
+
"invoked"
|
|
156
|
+
] != any(event.event_id == "experiment_root_" + slot for event in trace.events):
|
|
157
|
+
raise ValueError("candidate execution state differs from observed evidence")
|
|
158
|
+
if (
|
|
159
|
+
len(row["quality"]["cases"]) != 1
|
|
160
|
+
or quality_case["case_id"] != case["case_id"]
|
|
161
|
+
or quality_case["expected_hash"] != case["expected_hash"]
|
|
162
|
+
or quality_case["baseline"]["output_hash"] != case["baseline_output_hash"]
|
|
163
|
+
or quality_case["scorer"]["configuration_hash"] != spec["scorer_hash"]
|
|
164
|
+
or (
|
|
165
|
+
receipt["status"] == "completed"
|
|
166
|
+
and quality_case["candidate"]["output_hash"] != receipt["output_hash"]
|
|
167
|
+
)
|
|
168
|
+
):
|
|
169
|
+
raise ValueError("quality assessment differs from frozen inputs or scorer")
|
|
170
|
+
if (
|
|
171
|
+
trace.run_id != "experiment_run_" + slot
|
|
172
|
+
or trace.metadata["agentloop.experiment"]["status"] != receipt["status"]
|
|
173
|
+
or type(receipt["halted"]) is not bool
|
|
174
|
+
or type(receipt["invoked"]) is not bool
|
|
175
|
+
or type(receipt["budget_dispatched"]) is not bool
|
|
176
|
+
or record["measured"]["quality"] != row["quality"]
|
|
177
|
+
or record["measured"]["gates"]["config"] != spec["gates"]
|
|
178
|
+
):
|
|
179
|
+
raise ValueError("experiment outcome or gate binding changed")
|
|
180
|
+
if (
|
|
181
|
+
record["trace_fingerprints"]
|
|
182
|
+
!= {
|
|
183
|
+
"baseline": fingerprint(baseline),
|
|
184
|
+
"candidate": fingerprint(trace.to_dict()),
|
|
185
|
+
}
|
|
186
|
+
or record["predicted"]["findings"] != case["expected_effects"]
|
|
187
|
+
or record["configuration"]["experiment_id"] != identity
|
|
188
|
+
or record["configuration"]["slot_id"] != slot
|
|
189
|
+
or record["gates_passed"] != receipt["gates_passed"]
|
|
190
|
+
or row["quality"]["passed"] != receipt["quality_passed"]
|
|
191
|
+
):
|
|
192
|
+
raise ValueError("native intervention is not bound to this experiment")
|
|
193
|
+
row.update(status=receipt["status"], receipt=receipt)
|
|
194
|
+
elif finish.exists():
|
|
195
|
+
raise ValueError("receipt exists without an admission record")
|
|
196
|
+
rows.append(row)
|
|
197
|
+
attempts = local_path(root, "attempts")
|
|
198
|
+
if attempts.exists() and any(
|
|
199
|
+
path.name not in planned for path in attempts.iterdir() if path.is_dir()
|
|
200
|
+
):
|
|
201
|
+
raise ValueError("unplanned experiment attempt directory")
|
|
202
|
+
return {"experiment_id": identity, "specification": spec, "baselines": baselines, "rows": rows}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def summarize_experiment(root):
|
|
206
|
+
evidence = read_experiment(root)
|
|
207
|
+
rows = evidence["rows"]
|
|
208
|
+
reservations = [
|
|
209
|
+
Reservation(**row["admission"]["runner"]["reservation"])
|
|
210
|
+
for row in rows
|
|
211
|
+
if row["admission"] is not None
|
|
212
|
+
]
|
|
213
|
+
usages = [
|
|
214
|
+
None
|
|
215
|
+
if row["receipt"] is None or row["receipt"]["usage"] is None
|
|
216
|
+
else ResourceUsage(**row["receipt"]["usage"])
|
|
217
|
+
for row in rows
|
|
218
|
+
if row["admission"] is not None
|
|
219
|
+
]
|
|
220
|
+
budget_summary = {
|
|
221
|
+
"scope": evidence["specification"]["budget_scope"],
|
|
222
|
+
"limits": evidence["specification"]["budget"],
|
|
223
|
+
"admitted_attempts": len(reservations),
|
|
224
|
+
"reserved_known_tokens": sum(item.tokens for item in reservations if item.tokens_known),
|
|
225
|
+
"reserved_known_cost_usd": money_text(
|
|
226
|
+
sum(
|
|
227
|
+
(Fraction(str(item.cost_usd)) for item in reservations if item.cost_known),
|
|
228
|
+
Fraction(0),
|
|
229
|
+
)
|
|
230
|
+
),
|
|
231
|
+
"unknown_token_reservations": sum(not item.tokens_known for item in reservations),
|
|
232
|
+
"unknown_cost_reservations": sum(not item.cost_known for item in reservations),
|
|
233
|
+
"unavailable_actual_token_count": sum(
|
|
234
|
+
item is None or not item.tokens_known for item in usages
|
|
235
|
+
),
|
|
236
|
+
"unavailable_actual_cost_count": sum(
|
|
237
|
+
item is None or not item.cost_known for item in usages
|
|
238
|
+
),
|
|
239
|
+
"reservation_refunds": 0,
|
|
240
|
+
"hard_spend_cap": False,
|
|
241
|
+
}
|
|
242
|
+
budget_summary["reported_total_tokens"] = (
|
|
243
|
+
None
|
|
244
|
+
if budget_summary["unavailable_actual_token_count"]
|
|
245
|
+
else sum(item.tokens for item in usages)
|
|
246
|
+
)
|
|
247
|
+
budget_summary["reported_total_cost_usd"] = (
|
|
248
|
+
None
|
|
249
|
+
if budget_summary["unavailable_actual_cost_count"]
|
|
250
|
+
else money_text(sum((Fraction(str(item.cost_usd)) for item in usages), Fraction(0)))
|
|
251
|
+
)
|
|
252
|
+
comparisons = []
|
|
253
|
+
for runner in evidence["specification"]["runners"]:
|
|
254
|
+
selected = [row for row in rows if row["candidate_id"] == runner["candidate_id"]]
|
|
255
|
+
complete = [row for row in selected if row["receipt"] is not None]
|
|
256
|
+
comparisons.append(
|
|
257
|
+
{
|
|
258
|
+
"candidate_id": runner["candidate_id"],
|
|
259
|
+
"planned_pairs": len(selected),
|
|
260
|
+
"observed_pairs": len(complete),
|
|
261
|
+
"missing_pairs": len(selected) - len(complete),
|
|
262
|
+
"status_counts": dict(sorted(Counter(row["status"] for row in selected).items())),
|
|
263
|
+
"quality_pass_count": sum(row["receipt"]["quality_passed"] for row in complete),
|
|
264
|
+
"gates_pass_count": sum(row["receipt"]["gates_passed"] for row in complete),
|
|
265
|
+
"all_planned_gates_passed": len(complete) == len(selected)
|
|
266
|
+
and all(row["receipt"]["gates_passed"] for row in complete),
|
|
267
|
+
"observations": summarize_observations(selected),
|
|
268
|
+
"measured_pairs": [
|
|
269
|
+
{
|
|
270
|
+
"case_id": row["case_id"],
|
|
271
|
+
"quality_passed": row["receipt"]["quality_passed"],
|
|
272
|
+
"gates_passed": row["receipt"]["gates_passed"],
|
|
273
|
+
"deltas": row["intervention"]["measured"]["deltas"],
|
|
274
|
+
}
|
|
275
|
+
for row in complete
|
|
276
|
+
],
|
|
277
|
+
"predicted_effects": [
|
|
278
|
+
{"case_id": case["case_id"], "findings": case["expected_effects"]}
|
|
279
|
+
for case in evidence["specification"]["cases"]
|
|
280
|
+
],
|
|
281
|
+
}
|
|
282
|
+
)
|
|
283
|
+
result = {
|
|
284
|
+
"schema_version": "1.0",
|
|
285
|
+
"experiment_id": evidence["experiment_id"],
|
|
286
|
+
"name": evidence["specification"]["name"],
|
|
287
|
+
"synthetic": evidence["specification"]["synthetic"],
|
|
288
|
+
"planned_attempts": len(rows),
|
|
289
|
+
"observed_attempts": sum(row["receipt"] is not None for row in rows),
|
|
290
|
+
"status_counts": dict(sorted(Counter(row["status"] for row in rows).items())),
|
|
291
|
+
"comparisons": comparisons,
|
|
292
|
+
"attempts": [
|
|
293
|
+
{key: row[key] for key in ("slot_id", "case_id", "candidate_id", "status", "receipt")}
|
|
294
|
+
for row in rows
|
|
295
|
+
],
|
|
296
|
+
"interpretation": "Linked historical baselines and caller-run candidates; every planned slot remains in the denominator. Counterfactual benefit requires the explicit independent quality and performance gates. Unknown cost is not zero. Historical baselines are not randomized contemporaneous controls.",
|
|
297
|
+
}
|
|
298
|
+
result["budget"] = budget_summary
|
|
299
|
+
result["measurement_scope"] = {
|
|
300
|
+
"native_replay_cost": "recorded_model_profile_estimate",
|
|
301
|
+
"runner_usage": "separate_exclusive_caller_report",
|
|
302
|
+
"latency": "protected_candidate_runner_including_lifecycle_overhead",
|
|
303
|
+
"hidden_work": "not_intercepted",
|
|
304
|
+
}
|
|
305
|
+
result["snapshot_hash"] = fingerprint(result)
|
|
306
|
+
return result
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def experiment_to_markdown(report):
|
|
310
|
+
lines = [
|
|
311
|
+
"# Optimization experiment: " + markdown_heading(report["name"]),
|
|
312
|
+
"",
|
|
313
|
+
report["interpretation"],
|
|
314
|
+
"",
|
|
315
|
+
]
|
|
316
|
+
if report["synthetic"]:
|
|
317
|
+
lines += ["Synthetic evidence; no real-workload improvement claim.", ""]
|
|
318
|
+
lines += [
|
|
319
|
+
"| Candidate | Planned | Observed | Missing | Quality pass | All gates passed |",
|
|
320
|
+
"| --- | ---: | ---: | ---: | ---: | --- |",
|
|
321
|
+
]
|
|
322
|
+
for item in report["comparisons"]:
|
|
323
|
+
values = [
|
|
324
|
+
item["candidate_id"],
|
|
325
|
+
item["planned_pairs"],
|
|
326
|
+
item["observed_pairs"],
|
|
327
|
+
item["missing_pairs"],
|
|
328
|
+
item["quality_pass_count"],
|
|
329
|
+
item["all_planned_gates_passed"],
|
|
330
|
+
]
|
|
331
|
+
lines.append("| " + " | ".join(markdown_table_cell(str(value)) for value in values) + " |")
|
|
332
|
+
for item in report["comparisons"]:
|
|
333
|
+
lines += [
|
|
334
|
+
"",
|
|
335
|
+
"## Observations: " + markdown_heading(item["candidate_id"]),
|
|
336
|
+
"",
|
|
337
|
+
"Realized pair deltas remain separate from frozen predicted effects. Quality-preserving observations are conditional and retain the full planned denominator.",
|
|
338
|
+
"",
|
|
339
|
+
"```json",
|
|
340
|
+
canonical(
|
|
341
|
+
{
|
|
342
|
+
"observations": item.get("observations", {}),
|
|
343
|
+
"measured_pairs": item.get("measured_pairs", []),
|
|
344
|
+
"predicted_effects": item.get("predicted_effects", []),
|
|
345
|
+
}
|
|
346
|
+
),
|
|
347
|
+
"```",
|
|
348
|
+
]
|
|
349
|
+
lines += [
|
|
350
|
+
"",
|
|
351
|
+
"## Retained attempt states",
|
|
352
|
+
"",
|
|
353
|
+
"| Case | Candidate | State |",
|
|
354
|
+
"| --- | --- | --- |",
|
|
355
|
+
]
|
|
356
|
+
for item in report["attempts"]:
|
|
357
|
+
lines.append(
|
|
358
|
+
"| "
|
|
359
|
+
+ " | ".join(
|
|
360
|
+
markdown_table_cell(item[key]) for key in ("case_id", "candidate_id", "status")
|
|
361
|
+
)
|
|
362
|
+
+ " |"
|
|
363
|
+
)
|
|
364
|
+
return "\n".join(lines) + "\n"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def experiment_to_html(report):
|
|
368
|
+
def table(headers, rows):
|
|
369
|
+
return (
|
|
370
|
+
"<table><thead><tr>"
|
|
371
|
+
+ "".join('<th scope="col">' + html.escape(value) + "</th>" for value in headers)
|
|
372
|
+
+ "</tr></thead><tbody>"
|
|
373
|
+
+ "".join(
|
|
374
|
+
"<tr>"
|
|
375
|
+
+ "".join("<td>" + html.escape(str(value)) + "</td>" for value in row)
|
|
376
|
+
+ "</tr>"
|
|
377
|
+
for row in rows
|
|
378
|
+
)
|
|
379
|
+
+ "</tbody></table>"
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
summary = table(
|
|
383
|
+
("Candidate", "Planned", "Observed", "Missing", "Quality pass", "All gates passed"),
|
|
384
|
+
(
|
|
385
|
+
[
|
|
386
|
+
item[key]
|
|
387
|
+
for key in (
|
|
388
|
+
"candidate_id",
|
|
389
|
+
"planned_pairs",
|
|
390
|
+
"observed_pairs",
|
|
391
|
+
"missing_pairs",
|
|
392
|
+
"quality_pass_count",
|
|
393
|
+
"all_planned_gates_passed",
|
|
394
|
+
)
|
|
395
|
+
]
|
|
396
|
+
for item in report["comparisons"]
|
|
397
|
+
),
|
|
398
|
+
)
|
|
399
|
+
attempts = table(
|
|
400
|
+
("Case", "Candidate", "State"),
|
|
401
|
+
(
|
|
402
|
+
[item[key] for key in ("case_id", "candidate_id", "status")]
|
|
403
|
+
for item in report["attempts"]
|
|
404
|
+
),
|
|
405
|
+
)
|
|
406
|
+
notice = (
|
|
407
|
+
"<p>Synthetic evidence; no real-workload improvement claim.</p>"
|
|
408
|
+
if report["synthetic"]
|
|
409
|
+
else ""
|
|
410
|
+
)
|
|
411
|
+
detail = "".join(
|
|
412
|
+
"<details><summary>Observations, measured deltas and separate predictions: "
|
|
413
|
+
+ html.escape(item["candidate_id"])
|
|
414
|
+
+ "</summary><pre>"
|
|
415
|
+
+ html.escape(
|
|
416
|
+
canonical(
|
|
417
|
+
{
|
|
418
|
+
"observations": item.get("observations", {}),
|
|
419
|
+
"measured_pairs": item.get("measured_pairs", []),
|
|
420
|
+
"predicted_effects": item.get("predicted_effects", []),
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
+ "</pre></details>"
|
|
425
|
+
for item in report["comparisons"]
|
|
426
|
+
)
|
|
427
|
+
return (
|
|
428
|
+
'<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'; base-uri \'none\'"><title>Experiment evidence</title><style>body{font:16px/1.5 system-ui;margin:2rem auto;max-width:72rem;padding:0 1rem}table{border-collapse:collapse;width:100%}th,td{text-align:left;border-bottom:1px solid #ccc;padding:.6rem}pre{white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><main><h1>'
|
|
429
|
+
+ html.escape(report["name"])
|
|
430
|
+
+ "</h1>"
|
|
431
|
+
+ notice
|
|
432
|
+
+ "<p>"
|
|
433
|
+
+ html.escape(report["interpretation"])
|
|
434
|
+
+ "</p><h2>Candidate comparisons</h2>"
|
|
435
|
+
+ summary
|
|
436
|
+
+ detail
|
|
437
|
+
+ "<h2>Retained attempts</h2>"
|
|
438
|
+
+ attempts
|
|
439
|
+
+ "<details><summary>Budget and measurement scope</summary><pre>"
|
|
440
|
+
+ html.escape(
|
|
441
|
+
canonical(
|
|
442
|
+
{"budget": report["budget"], "measurement_scope": report["measurement_scope"]}
|
|
443
|
+
)
|
|
444
|
+
)
|
|
445
|
+
+ "</pre></details></main></body></html>"
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def export_experiment(root):
|
|
450
|
+
"""Export the same snapshot idempotently; never invoke a runner or scorer."""
|
|
451
|
+
root = Path(root).resolve()
|
|
452
|
+
with journal_lock(root):
|
|
453
|
+
evidence, report = read_experiment(root), summarize_experiment(root)
|
|
454
|
+
directory = "reports/" + report["snapshot_hash"]
|
|
455
|
+
studies = {}
|
|
456
|
+
for runner in evidence["specification"]["runners"]:
|
|
457
|
+
name = runner["candidate_id"]
|
|
458
|
+
selected = [row for row in evidence["rows"] if row["candidate_id"] == name]
|
|
459
|
+
if not any(row["trace"] is not None for row in selected):
|
|
460
|
+
studies[name] = {
|
|
461
|
+
"status": "unavailable",
|
|
462
|
+
"reason": "no_candidate_trace",
|
|
463
|
+
"planned_pairs": len(selected),
|
|
464
|
+
}
|
|
465
|
+
continue
|
|
466
|
+
folder = directory + "/" + fingerprint(name)
|
|
467
|
+
conditions = {"baseline": [], "candidate": []}
|
|
468
|
+
for row in selected:
|
|
469
|
+
baseline = AgentTrace.from_dict(evidence["baselines"][row["case_id"]])
|
|
470
|
+
baseline.metadata["agentloop.experiment_view"] = {
|
|
471
|
+
"source_trace_hash": fingerprint(evidence["baselines"][row["case_id"]]),
|
|
472
|
+
"derived_fields": ["pairing", "quality"],
|
|
473
|
+
}
|
|
474
|
+
baseline.metadata.update(
|
|
475
|
+
experiment_id=evidence["experiment_id"], case_id=row["case_id"]
|
|
476
|
+
)
|
|
477
|
+
if row["quality"] is not None:
|
|
478
|
+
attach_quality_report(baseline, row["quality"], side="baseline")
|
|
479
|
+
else:
|
|
480
|
+
# A missing candidate cannot certify independent baseline
|
|
481
|
+
# task quality. Keep the baseline run for unmatched pairing.
|
|
482
|
+
baseline.metadata.pop("quality_score", None)
|
|
483
|
+
baseline.metadata.pop(EVIDENCE_KEY, None)
|
|
484
|
+
filename = "baseline-" + fingerprint(row["case_id"]) + ".json"
|
|
485
|
+
write_once(root, folder + "/" + filename, baseline.to_dict())
|
|
486
|
+
conditions["baseline"].append(filename)
|
|
487
|
+
if row["trace"] is not None:
|
|
488
|
+
candidate = AgentTrace.from_dict(row["trace"])
|
|
489
|
+
candidate.metadata["agentloop.experiment_view"] = {
|
|
490
|
+
"source_trace_hash": fingerprint(row["trace"]),
|
|
491
|
+
"derived_fields": ["quality"],
|
|
492
|
+
}
|
|
493
|
+
attach_quality_report(candidate, row["quality"], side="candidate")
|
|
494
|
+
filename = "candidate-" + row["slot_id"] + ".json"
|
|
495
|
+
write_once(root, folder + "/" + filename, candidate.to_dict())
|
|
496
|
+
conditions["candidate"].append(filename)
|
|
497
|
+
manifest = {
|
|
498
|
+
"schema_version": "1.0",
|
|
499
|
+
"name": report["name"] + " / " + name,
|
|
500
|
+
"baseline": "baseline",
|
|
501
|
+
"conditions": conditions,
|
|
502
|
+
"pairing_keys": ["experiment_id", "case_id"],
|
|
503
|
+
}
|
|
504
|
+
path = write_once(root, folder + "/study.json", manifest)
|
|
505
|
+
studies[name] = summarize_study(path)
|
|
506
|
+
payload = {"experiment": report, "studies": studies}
|
|
507
|
+
write_once(root, directory + "/evidence.json", payload)
|
|
508
|
+
markdown = experiment_to_markdown(report)
|
|
509
|
+
page = experiment_to_html(report)
|
|
510
|
+
for name, text in (("evidence.md", markdown), ("evidence.html", page)):
|
|
511
|
+
write_bytes_once(root, directory + "/" + name, text.encode("utf-8"))
|
|
512
|
+
return {"directory": str(local_path(root, directory)), **payload}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def persist_experiment(root, store, *, project_id="default"):
|
|
516
|
+
"""Use the existing trace/finding/intervention store; no new database schema."""
|
|
517
|
+
evidence = read_experiment(root)
|
|
518
|
+
ids = []
|
|
519
|
+
traces = [AgentTrace.from_dict(value) for value in evidence["baselines"].values()] + [
|
|
520
|
+
AgentTrace.from_dict(row["trace"]) for row in evidence["rows"] if row["trace"] is not None
|
|
521
|
+
]
|
|
522
|
+
existing = {}
|
|
523
|
+
for trace in traces:
|
|
524
|
+
saved = store.get_trace(trace.run_id, project_id=project_id)
|
|
525
|
+
if saved is not None and fingerprint(saved.to_dict()) != fingerprint(trace.to_dict()):
|
|
526
|
+
raise ValueError("native store already has conflicting evidence for a run identity")
|
|
527
|
+
existing[trace.run_id] = saved is not None
|
|
528
|
+
for case in evidence["specification"]["cases"]:
|
|
529
|
+
trace = AgentTrace.from_dict(evidence["baselines"][case["case_id"]])
|
|
530
|
+
if not existing[trace.run_id]:
|
|
531
|
+
store.save_trace(trace, project_id=project_id)
|
|
532
|
+
store.save_diagnosis(
|
|
533
|
+
read_json(local_path(root, f"predictions/{fingerprint(case['case_id'])}.json")),
|
|
534
|
+
project_id=project_id,
|
|
535
|
+
)
|
|
536
|
+
for row in evidence["rows"]:
|
|
537
|
+
if row["trace"] is None:
|
|
538
|
+
continue
|
|
539
|
+
trace = AgentTrace.from_dict(row["trace"])
|
|
540
|
+
if not existing[trace.run_id]:
|
|
541
|
+
store.save_trace(trace, project_id=project_id)
|
|
542
|
+
record = InterventionRecord.from_dict(row["intervention"])
|
|
543
|
+
store.save_intervention(record, project_id=project_id)
|
|
544
|
+
ids.append(record.intervention_id)
|
|
545
|
+
return ids
|