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,199 @@
|
|
|
1
|
+
"""Frozen decision-step measurements, separate from model-only profile totals."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
|
|
8
|
+
from agentloop.events import AgentEvent
|
|
9
|
+
from agentloop.judgment_types import (
|
|
10
|
+
JudgeIdentity,
|
|
11
|
+
JudgeUsage,
|
|
12
|
+
canonical,
|
|
13
|
+
fingerprint,
|
|
14
|
+
finite,
|
|
15
|
+
is_hash,
|
|
16
|
+
text,
|
|
17
|
+
)
|
|
18
|
+
from agentloop.substitution_types import SUBSTITUTION_VERSION, TRIAL_KEY
|
|
19
|
+
from agentloop.workflow_types import stage_summary, workflow_summary
|
|
20
|
+
|
|
21
|
+
_STATES = {"completed", "unknown", "failed", "timed_out", "invalid_result", "disabled", "missing"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _binding(trace):
|
|
25
|
+
events = []
|
|
26
|
+
for event in trace.events:
|
|
27
|
+
value = AgentEvent.from_dict(event.to_dict()).to_dict()
|
|
28
|
+
value["metadata"] = {
|
|
29
|
+
key: item
|
|
30
|
+
for key, item in value["metadata"].items()
|
|
31
|
+
if key not in {"otel_span_id", "otel_trace_id"}
|
|
32
|
+
}
|
|
33
|
+
events.append(value)
|
|
34
|
+
return fingerprint(
|
|
35
|
+
{
|
|
36
|
+
"run_id": trace.run_id,
|
|
37
|
+
"started_at": trace.started_at,
|
|
38
|
+
"ended_at": trace.ended_at,
|
|
39
|
+
"elapsed_ms": float(trace.elapsed_ms) if trace.elapsed_ms is not None else None,
|
|
40
|
+
"events": events,
|
|
41
|
+
"workflow": trace.metadata.get("agentloop.workflow"),
|
|
42
|
+
"trial_metadata": {
|
|
43
|
+
key: trace.metadata.get(key)
|
|
44
|
+
for key in (
|
|
45
|
+
"plan_hash",
|
|
46
|
+
"example_id",
|
|
47
|
+
"task_id",
|
|
48
|
+
"repetition",
|
|
49
|
+
"condition",
|
|
50
|
+
"synthetic",
|
|
51
|
+
"success",
|
|
52
|
+
)
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def attach_trial_evidence(
|
|
59
|
+
trace,
|
|
60
|
+
*,
|
|
61
|
+
plan_hash,
|
|
62
|
+
example_id,
|
|
63
|
+
repetition,
|
|
64
|
+
implementation,
|
|
65
|
+
status,
|
|
66
|
+
latency_ms,
|
|
67
|
+
usage,
|
|
68
|
+
cost_ref,
|
|
69
|
+
input_hash,
|
|
70
|
+
output_hash,
|
|
71
|
+
):
|
|
72
|
+
record = {
|
|
73
|
+
"schema_version": SUBSTITUTION_VERSION,
|
|
74
|
+
"run_id": trace.run_id,
|
|
75
|
+
"plan_hash": plan_hash,
|
|
76
|
+
"example_id": example_id,
|
|
77
|
+
"repetition": repetition,
|
|
78
|
+
"implementation": implementation,
|
|
79
|
+
"outcome": status,
|
|
80
|
+
"latency_ms": latency_ms,
|
|
81
|
+
"usage": asdict(usage),
|
|
82
|
+
"cost_ref": cost_ref,
|
|
83
|
+
"input_hash": input_hash,
|
|
84
|
+
"output_hash": output_hash,
|
|
85
|
+
"source_hash": _binding(trace),
|
|
86
|
+
"scope": "declared_decision_step",
|
|
87
|
+
"timing_basis": "callback_wall_time" if latency_ms is not None else "not_observed",
|
|
88
|
+
}
|
|
89
|
+
record["evidence_hash"] = fingerprint(record)
|
|
90
|
+
if TRIAL_KEY in trace.metadata:
|
|
91
|
+
raise ValueError("trial evidence cannot replace an existing receipt")
|
|
92
|
+
trace.metadata[TRIAL_KEY] = json.loads(canonical(record))
|
|
93
|
+
if read_trial_evidence(trace)["status"] != "valid":
|
|
94
|
+
trace.metadata.pop(TRIAL_KEY)
|
|
95
|
+
raise ValueError("invalid decision trial evidence")
|
|
96
|
+
return json.loads(canonical(record))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def read_trial_evidence(trace):
|
|
100
|
+
if TRIAL_KEY not in trace.metadata:
|
|
101
|
+
return (
|
|
102
|
+
{"status": "invalid", "scope": "declared_decision_step"}
|
|
103
|
+
if trace.metadata.get("source") == "agentloop_substitution"
|
|
104
|
+
else None
|
|
105
|
+
)
|
|
106
|
+
empty = {"status": "invalid", "scope": "declared_decision_step"}
|
|
107
|
+
try:
|
|
108
|
+
record = json.loads(canonical(trace.metadata[TRIAL_KEY]))
|
|
109
|
+
if not isinstance(record, dict):
|
|
110
|
+
return empty
|
|
111
|
+
if record.get("schema_version") != SUBSTITUTION_VERSION:
|
|
112
|
+
return {**empty, "status": "unsupported"}
|
|
113
|
+
digest = record.pop("evidence_hash")
|
|
114
|
+
if (
|
|
115
|
+
digest != fingerprint(record)
|
|
116
|
+
or record["run_id"] != trace.run_id
|
|
117
|
+
or record["source_hash"] != _binding(trace)
|
|
118
|
+
):
|
|
119
|
+
return empty
|
|
120
|
+
if record["scope"] != "declared_decision_step" or record["outcome"] not in _STATES:
|
|
121
|
+
return empty
|
|
122
|
+
for key in ("plan_hash", "input_hash"):
|
|
123
|
+
if not is_hash(record[key]):
|
|
124
|
+
return empty
|
|
125
|
+
if record["output_hash"] is not None and not is_hash(record["output_hash"]):
|
|
126
|
+
return empty
|
|
127
|
+
if (record["outcome"] == "completed") != (record["output_hash"] is not None):
|
|
128
|
+
return empty
|
|
129
|
+
text(record["example_id"], "example_id")
|
|
130
|
+
if type(record["repetition"]) is not int or record["repetition"] < 0:
|
|
131
|
+
return empty
|
|
132
|
+
declaration = record["implementation"]
|
|
133
|
+
text(declaration["name"], "implementation name")
|
|
134
|
+
JudgeIdentity(**declaration["identity"])
|
|
135
|
+
if declaration["kind"] not in {"model", "classifier", "rule", "tool", "external_service"}:
|
|
136
|
+
return empty
|
|
137
|
+
JudgeUsage(**record["usage"])
|
|
138
|
+
for key in ("plan_hash", "example_id", "repetition"):
|
|
139
|
+
if trace.metadata.get(key) != record[key]:
|
|
140
|
+
return empty
|
|
141
|
+
if (
|
|
142
|
+
trace.metadata.get("condition") != declaration["name"]
|
|
143
|
+
or type(trace.metadata.get("synthetic")) is not bool
|
|
144
|
+
):
|
|
145
|
+
return empty
|
|
146
|
+
if trace.metadata.get("task_id") != record["example_id"]:
|
|
147
|
+
return empty
|
|
148
|
+
workflow = workflow_summary(trace.metadata)
|
|
149
|
+
if workflow is None or workflow.get("schema_status") != "supported":
|
|
150
|
+
return empty
|
|
151
|
+
failed = record["outcome"] in {"failed", "timed_out", "invalid_result"}
|
|
152
|
+
expected_state = (
|
|
153
|
+
"failed"
|
|
154
|
+
if failed
|
|
155
|
+
else ("unknown" if record["outcome"] in {"disabled", "missing"} else "completed")
|
|
156
|
+
)
|
|
157
|
+
if workflow["status"] != expected_state:
|
|
158
|
+
return empty
|
|
159
|
+
text(record["cost_ref"], "cost_ref", optional=True)
|
|
160
|
+
latency = record["latency_ms"]
|
|
161
|
+
if latency is None:
|
|
162
|
+
if (
|
|
163
|
+
record["outcome"] not in {"disabled", "missing"}
|
|
164
|
+
or record["timing_basis"] != "not_observed"
|
|
165
|
+
or trace.events
|
|
166
|
+
):
|
|
167
|
+
return empty
|
|
168
|
+
elif (
|
|
169
|
+
not finite(latency)
|
|
170
|
+
or latency < 0
|
|
171
|
+
or latency != trace.elapsed_ms
|
|
172
|
+
or record["timing_basis"] != "callback_wall_time"
|
|
173
|
+
or len(trace.events) != 1
|
|
174
|
+
or trace.events[0].duration_ms != latency
|
|
175
|
+
):
|
|
176
|
+
return empty
|
|
177
|
+
if latency is not None and trace.events[0].status != ("error" if failed else "ok"):
|
|
178
|
+
return empty
|
|
179
|
+
if latency is not None:
|
|
180
|
+
stage = stage_summary(trace.events[0])
|
|
181
|
+
if (
|
|
182
|
+
stage is None
|
|
183
|
+
or stage.get("schema_status") != "supported"
|
|
184
|
+
or stage.get("version") != declaration["identity"]["version"]
|
|
185
|
+
or stage.get("kind") != declaration["kind"]
|
|
186
|
+
or stage.get("outcome") != record["outcome"]
|
|
187
|
+
or stage.get("output_ref")
|
|
188
|
+
!= (
|
|
189
|
+
"sha256:" + record["output_hash"] if record["output_hash"] is not None else None
|
|
190
|
+
)
|
|
191
|
+
):
|
|
192
|
+
return empty
|
|
193
|
+
if record["outcome"] == "disabled" and JudgeUsage(**record["usage"]) != JudgeUsage(
|
|
194
|
+
0, 0, 0, "not_dispatched", "not_dispatched"
|
|
195
|
+
):
|
|
196
|
+
return empty
|
|
197
|
+
return {**record, "status": "valid", "evidence_hash": digest}
|
|
198
|
+
except (TypeError, ValueError, KeyError, AttributeError):
|
|
199
|
+
return empty
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Export frozen decision trials without invoking an implementation or scorer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from agentloop.markdown import markdown_heading, markdown_table_cell
|
|
9
|
+
from agentloop.structured_quality import attach_quality_report
|
|
10
|
+
from agentloop.studies import study_to_markdown, summarize_study
|
|
11
|
+
from agentloop.substitution_reports import (
|
|
12
|
+
_copy,
|
|
13
|
+
index_trials,
|
|
14
|
+
load_bundle,
|
|
15
|
+
paired_assessments,
|
|
16
|
+
summarize_substitution_experiment,
|
|
17
|
+
)
|
|
18
|
+
from agentloop.tracer import AgentTrace
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def substitution_to_markdown(report):
|
|
22
|
+
lines = [
|
|
23
|
+
f"# Model substitution: {markdown_heading(report['name'])}",
|
|
24
|
+
"",
|
|
25
|
+
report["interpretation"],
|
|
26
|
+
"",
|
|
27
|
+
"Measurement scope: declared decision-step usage and callback latency. Reported/calculated usage is separate from model-only profile totals; unknown values are not zero.",
|
|
28
|
+
"",
|
|
29
|
+
]
|
|
30
|
+
if report["synthetic"]:
|
|
31
|
+
lines.extend(
|
|
32
|
+
[
|
|
33
|
+
"Synthetic data: these results do not establish real-model quality, latency or savings.",
|
|
34
|
+
"",
|
|
35
|
+
]
|
|
36
|
+
)
|
|
37
|
+
lines.extend(
|
|
38
|
+
[
|
|
39
|
+
"## Measurements",
|
|
40
|
+
"",
|
|
41
|
+
"| Implementation | Planned | Unavailable output rate | Observed latency ms | Observed cost USD | Cost basis |",
|
|
42
|
+
"| --- | ---: | ---: | ---: | ---: | --- |",
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
for item in report["implementations"]:
|
|
46
|
+
cells = [
|
|
47
|
+
item["name"],
|
|
48
|
+
item["planned"],
|
|
49
|
+
item["unavailable_output_rate"],
|
|
50
|
+
item["measurements"]["latency_ms"]["observed_mean"],
|
|
51
|
+
item["measurements"]["cost_usd"]["observed_mean"],
|
|
52
|
+
json.dumps(item["cost_basis_counts"], sort_keys=True),
|
|
53
|
+
]
|
|
54
|
+
lines.append(
|
|
55
|
+
"| "
|
|
56
|
+
+ " | ".join(
|
|
57
|
+
markdown_table_cell("unavailable" if value is None else str(value))
|
|
58
|
+
for value in cells
|
|
59
|
+
)
|
|
60
|
+
+ " |"
|
|
61
|
+
)
|
|
62
|
+
lines.extend(
|
|
63
|
+
[
|
|
64
|
+
"",
|
|
65
|
+
"## Independent quality and baseline agreement",
|
|
66
|
+
"",
|
|
67
|
+
"A full quality decision requires every planned example and repetition. Observed quality means use completely assessed examples only. Baseline agreement is a separate descriptive metric, not independent quality evidence.",
|
|
68
|
+
"",
|
|
69
|
+
"| Candidate | Complete examples | Baseline quality | Candidate quality | Quality preserved on examples | Baseline agreement |",
|
|
70
|
+
"| --- | ---: | ---: | ---: | --- | ---: |",
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
for item in report["comparisons"]:
|
|
74
|
+
cells = [
|
|
75
|
+
item["condition"],
|
|
76
|
+
item["complete_quality_examples"],
|
|
77
|
+
item["observed_baseline_quality"],
|
|
78
|
+
item["observed_candidate_quality"],
|
|
79
|
+
item["quality_preserved_on_examples"],
|
|
80
|
+
item["baseline_agreement"]["observed_rate"],
|
|
81
|
+
]
|
|
82
|
+
lines.append(
|
|
83
|
+
"| "
|
|
84
|
+
+ " | ".join(
|
|
85
|
+
markdown_table_cell("unavailable" if value is None else str(value))
|
|
86
|
+
for value in cells
|
|
87
|
+
)
|
|
88
|
+
+ " |"
|
|
89
|
+
)
|
|
90
|
+
return "\n".join(lines) + "\n"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def export_substitution_studies(bundle, out, *, max_invocations=10000):
|
|
94
|
+
"""Write native quality-bound traces and one paired study per candidate.
|
|
95
|
+
|
|
96
|
+
Failed/unknown outputs have real trial receipts and remain in each study.
|
|
97
|
+
Missing or ambiguous source records are not fabricated during export.
|
|
98
|
+
Baseline executions are shared controls across these separate comparisons.
|
|
99
|
+
"""
|
|
100
|
+
owned, examples = load_bundle(bundle, max_invocations)
|
|
101
|
+
slots, extras = index_trials(owned, examples)
|
|
102
|
+
pairs, pair_extras = paired_assessments(owned, slots, examples)
|
|
103
|
+
if (
|
|
104
|
+
extras
|
|
105
|
+
or pair_extras
|
|
106
|
+
or any(item["trace"] is None for item in slots.values())
|
|
107
|
+
or any(item["quality"] is None for item in pairs)
|
|
108
|
+
):
|
|
109
|
+
raise ValueError(
|
|
110
|
+
"study export requires unambiguous receipts and saved quality assessments; failed outputs remain valid study inputs"
|
|
111
|
+
)
|
|
112
|
+
root = Path(out).resolve()
|
|
113
|
+
documents, manifests = {}, []
|
|
114
|
+
for index, name in enumerate(
|
|
115
|
+
key for key in owned["implementations"] if key != owned["baseline"]
|
|
116
|
+
):
|
|
117
|
+
directory = f"candidate-{index:03d}"
|
|
118
|
+
conditions = {"baseline": [], "candidate": []}
|
|
119
|
+
selected = [item for item in pairs if item["condition"] == name]
|
|
120
|
+
for row_index, pair in enumerate(selected):
|
|
121
|
+
for side, condition in (("baseline", owned["baseline"]), ("candidate", name)):
|
|
122
|
+
trial = slots[condition, pair["example_id"], pair["repetition"]]
|
|
123
|
+
trace = AgentTrace.from_dict(_copy(trial["trace"].to_dict()))
|
|
124
|
+
attach_quality_report(trace, pair["quality"], side=side)
|
|
125
|
+
filename = f"{side}-{row_index:06d}.json"
|
|
126
|
+
documents[f"{directory}/{filename}"] = trace.to_dict()
|
|
127
|
+
conditions[side].append(filename)
|
|
128
|
+
manifest = {
|
|
129
|
+
"schema_version": "1.0",
|
|
130
|
+
"name": owned["protocol"]["name"] + " / " + name,
|
|
131
|
+
"baseline": "baseline",
|
|
132
|
+
"conditions": conditions,
|
|
133
|
+
"pairing_keys": ["plan_hash", "example_id", "repetition"],
|
|
134
|
+
}
|
|
135
|
+
documents[f"{directory}/study.json"] = manifest
|
|
136
|
+
manifests.append(root / directory / "study.json")
|
|
137
|
+
comparison = summarize_substitution_experiment(owned, max_invocations=max_invocations)
|
|
138
|
+
documents["bundle.json"] = owned
|
|
139
|
+
documents["comparison.json"] = comparison
|
|
140
|
+
encoded = {
|
|
141
|
+
relative: json.dumps(value, indent=2, allow_nan=False) + "\n"
|
|
142
|
+
for relative, value in documents.items()
|
|
143
|
+
}
|
|
144
|
+
encoded["comparison.md"] = substitution_to_markdown(comparison)
|
|
145
|
+
for relative, content in encoded.items():
|
|
146
|
+
target = root / relative
|
|
147
|
+
if not target.resolve().is_relative_to(root):
|
|
148
|
+
raise ValueError("artifact path escapes the output directory")
|
|
149
|
+
if target.exists() and target.read_text(encoding="utf-8") != content:
|
|
150
|
+
raise FileExistsError(
|
|
151
|
+
f"refusing to replace a different experiment artifact: {relative}"
|
|
152
|
+
)
|
|
153
|
+
# Derived reports must also be protected. Their content is deterministic for
|
|
154
|
+
# these saved traces; checking after computation never reruns a scorer.
|
|
155
|
+
for manifest in manifests:
|
|
156
|
+
targets = [manifest.with_name(name) for name in ("study-report.json", "study-report.md")]
|
|
157
|
+
if any(not target.resolve().is_relative_to(root) for target in targets):
|
|
158
|
+
raise ValueError("study report path escapes the output directory")
|
|
159
|
+
if any(target.exists() for target in targets):
|
|
160
|
+
if not manifest.exists():
|
|
161
|
+
raise FileExistsError("orphaned study report would be overwritten")
|
|
162
|
+
existing = summarize_study(manifest)
|
|
163
|
+
expected = [
|
|
164
|
+
json.dumps(existing, indent=2, allow_nan=False) + "\n",
|
|
165
|
+
study_to_markdown(existing),
|
|
166
|
+
]
|
|
167
|
+
for target, content in zip(targets, expected):
|
|
168
|
+
if target.exists() and target.read_text(encoding="utf-8") != content:
|
|
169
|
+
raise FileExistsError("refusing to replace a different saved study report")
|
|
170
|
+
for relative, content in encoded.items():
|
|
171
|
+
target = root / relative
|
|
172
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
if not target.exists():
|
|
174
|
+
target.write_text(content, encoding="utf-8")
|
|
175
|
+
reports = []
|
|
176
|
+
for manifest in manifests:
|
|
177
|
+
report = summarize_study(manifest)
|
|
178
|
+
derived = {
|
|
179
|
+
"study-report.json": json.dumps(report, indent=2, allow_nan=False) + "\n",
|
|
180
|
+
"study-report.md": study_to_markdown(report),
|
|
181
|
+
}
|
|
182
|
+
for name, content in derived.items():
|
|
183
|
+
target = manifest.with_name(name)
|
|
184
|
+
if target.exists() and target.read_text(encoding="utf-8") != content:
|
|
185
|
+
raise FileExistsError("refusing to replace a different saved study report")
|
|
186
|
+
for name, content in derived.items():
|
|
187
|
+
if not manifest.with_name(name).exists():
|
|
188
|
+
manifest.with_name(name).write_text(content, encoding="utf-8")
|
|
189
|
+
reports.append(report)
|
|
190
|
+
return {"directory": str(root), "studies": reports, "comparison": comparison}
|