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.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,681 @@
1
+ """Finished-trial cohorts using native studies and independent quality gates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ from collections import Counter, defaultdict
8
+ from dataclasses import dataclass
9
+ from hashlib import sha256
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from agentloop.integrations.harbor.trial_evidence import PAIRING_KEYS, attach_trial
14
+ from agentloop.integrations.harbor.trials import HarborImportResult, HarborOptions, import_harbor
15
+ from agentloop.integrations.harbor.verifier import ScoringContract, external_outcome
16
+ from agentloop.interoperability.artifacts import (
17
+ native_bytes,
18
+ output_artifact_path,
19
+ write_artifact,
20
+ )
21
+ from agentloop.interoperability.cohort_evidence import (
22
+ COHORT_KEY,
23
+ COHORT_PAIRING_KEYS,
24
+ cohort_record,
25
+ read_cohort,
26
+ )
27
+ from agentloop.interoperability.contracts import external_id
28
+ from agentloop.interoperability.validation import (
29
+ ImportLimits,
30
+ ImportValidationError,
31
+ indirect_path,
32
+ load_json_artifact,
33
+ relative_reference,
34
+ validate_json_tree,
35
+ )
36
+ from agentloop.interventions import InterventionRecord, canonical_json
37
+ from agentloop.studies import _quantile_sorted, summarize_study, summarize_values
38
+ from agentloop.study_statistics import task_weighted_summary
39
+ from agentloop.tracer import AgentTrace
40
+
41
+
42
+ def _fail(field: str, reason: str) -> None:
43
+ raise ImportValidationError("invalid_cohort", field, reason)
44
+
45
+
46
+ def _label(value: Any, field: str) -> None:
47
+ if not isinstance(value, str) or not value.strip() or len(value.encode()) > 512:
48
+ _fail(field, "must be a bounded nonempty label")
49
+
50
+
51
+ def _hash(value: Any) -> str:
52
+ return sha256(canonical_json(value).encode()).hexdigest()
53
+
54
+
55
+ def _summary(values: list) -> dict:
56
+ result = summarize_values(values)
57
+ ordered = sorted(value for value in values if value is not None)
58
+ result["p50"] = result["median"]
59
+ result["p90"] = _quantile_sorted(ordered, 0.9)
60
+ return result
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class CohortProtocol:
65
+ protocol_id: str
66
+ comparison_kind: str = "system_level_confounded"
67
+ missingness_policy: str = "indeterminate"
68
+ minimum_runtime_improvement_pct: float = 0.0
69
+ bootstrap_samples: int = 1000
70
+ bootstrap_seed: int = 0
71
+ bootstrap_confidence: float = 0.95
72
+
73
+ def __post_init__(self) -> None:
74
+ _label(self.protocol_id, "protocol_id")
75
+ if (
76
+ not isinstance(self.comparison_kind, str)
77
+ or self.comparison_kind
78
+ not in {
79
+ "system_level_confounded",
80
+ "controlled_harness_ablation",
81
+ }
82
+ or not isinstance(self.missingness_policy, str)
83
+ or self.missingness_policy not in {"indeterminate", "reject"}
84
+ ):
85
+ _fail("protocol", "unsupported comparison/missingness policy")
86
+ if (
87
+ type(self.minimum_runtime_improvement_pct) not in {int, float}
88
+ or not math.isfinite(self.minimum_runtime_improvement_pct)
89
+ or self.minimum_runtime_improvement_pct < 0
90
+ ):
91
+ _fail("minimum_runtime_improvement_pct", "must be finite nonnegative")
92
+ if (
93
+ type(self.bootstrap_samples) is not int
94
+ or not 1 <= self.bootstrap_samples <= 10000
95
+ or type(self.bootstrap_seed) is not int
96
+ or type(self.bootstrap_confidence) not in {int, float}
97
+ or not 0 < self.bootstrap_confidence < 1
98
+ ):
99
+ _fail("bootstrap", "invalid task-cluster bootstrap settings")
100
+
101
+ def uncertainty_settings(self) -> dict:
102
+ return {
103
+ "samples": self.bootstrap_samples,
104
+ "seed": self.bootstrap_seed,
105
+ "confidence": self.bootstrap_confidence,
106
+ }
107
+
108
+
109
+ @dataclass(frozen=True)
110
+ class CohortStudyResult:
111
+ manifest_path: Path | None
112
+ _report_json: str
113
+
114
+ def report(self) -> dict:
115
+ return json.loads(self._report_json)
116
+
117
+
118
+ def _trial_metrics(receipt: dict | None) -> dict:
119
+ metadata = receipt["source_metadata"] if receipt else {}
120
+ usage = metadata.get("usage", {})
121
+ phases = metadata.get("phase_timing", {})
122
+ return {
123
+ "runtime_ms": phases.get("agent_execution", {}).get("duration_ms"),
124
+ "trial_runtime_ms": metadata.get("trial_wall_timing", {}).get("duration_ms"),
125
+ "environment_setup_ms": phases.get("environment_setup", {}).get("duration_ms"),
126
+ "agent_setup_ms": phases.get("agent_setup", {}).get("duration_ms"),
127
+ "verifier_ms": phases.get("verifier", {}).get("duration_ms"),
128
+ "input_tokens": usage.get("input_tokens"),
129
+ "output_tokens": usage.get("output_tokens"),
130
+ "cached_input_tokens": usage.get("cached_input_tokens"),
131
+ "cost_usd": usage.get("cost_usd"),
132
+ "model_call_count": None,
133
+ "tool_call_count": None,
134
+ "retry_count": None,
135
+ }
136
+
137
+
138
+ def _observation(
139
+ result: HarborImportResult,
140
+ row: dict,
141
+ condition: str,
142
+ protocol: CohortProtocol,
143
+ index: int,
144
+ *,
145
+ inventory: dict,
146
+ receipt: dict | None,
147
+ trace_index: dict,
148
+ inventory_sha256: str,
149
+ inventory_identity_sha256: str,
150
+ ) -> tuple[AgentTrace, dict]:
151
+ source_identity = (
152
+ receipt["external_identity"]
153
+ if receipt
154
+ else {
155
+ "job_id": inventory["job_id"],
156
+ "trial_id": row.get("trial_id"),
157
+ "task_id": None,
158
+ "task_digest": None,
159
+ }
160
+ )
161
+ pairing = {key: row.get("pairing", {}).get(key) for key in PAIRING_KEYS}
162
+ if pairing["protocol_id"] is not None and pairing["protocol_id"] != protocol.protocol_id:
163
+ _fail("protocol_id", "cohort protocol differs from captured source protocol")
164
+ metadata = receipt["source_metadata"] if receipt else {}
165
+ task_id, verifier_hash = source_identity.get("task_id"), metadata.get("verifier_config_hash")
166
+ metrics = _trial_metrics(receipt)
167
+ trajectories = bool(row.get("trajectory_run_ids"))
168
+ if trajectories and row["import_status"] == "imported":
169
+ projected = [
170
+ trace_index[run_id] for run_id in row["trajectory_run_ids"] if run_id in trace_index
171
+ ]
172
+ source_counts = [
173
+ trace.metadata.get("agentloop.external", {}).get("reported_model_call_count")
174
+ for trace in projected
175
+ ]
176
+ metrics["model_call_count"] = (
177
+ sum(source_counts)
178
+ if len(projected) == len(row["trajectory_run_ids"])
179
+ and all(value is not None for value in source_counts)
180
+ else None
181
+ )
182
+ metrics["tool_call_count"] = (
183
+ sum(event.operation_kind == "tool" for trace in projected for event in trace.events)
184
+ if len(projected) == len(row["trajectory_run_ids"])
185
+ else None
186
+ )
187
+ usage = metrics["input_tokens"] is not None and metrics["output_tokens"] is not None
188
+ outcome = (
189
+ receipt["outcome"]
190
+ if receipt
191
+ else external_outcome(
192
+ execution_status="unknown", verifier_status="invalid", dimensions=None
193
+ )
194
+ )
195
+ source_ref = (
196
+ receipt["source"]
197
+ if receipt
198
+ else {
199
+ "artifact_reference": "harbor-inventory.json",
200
+ "artifact_sha256": inventory_sha256,
201
+ }
202
+ )
203
+ captured_id = (
204
+ receipt["receipt_id"]
205
+ if receipt
206
+ else external_id(
207
+ "receipt",
208
+ "harbor",
209
+ {
210
+ "job_id": inventory["job_id"],
211
+ "inventory_sha256": inventory_identity_sha256,
212
+ "row": index,
213
+ },
214
+ )
215
+ )
216
+ run_id = external_id(
217
+ "run", "harbor", {"source_receipt_id": captured_id, "scope": "cohort_observation"}
218
+ )
219
+ record = cohort_record(
220
+ source_receipt=receipt,
221
+ import_status=row["import_status"],
222
+ trajectory_available=trajectories,
223
+ usage_available=usage,
224
+ missingness_policy=protocol.missingness_policy,
225
+ condition=condition,
226
+ task_id=task_id,
227
+ verifier_config_hash=verifier_hash,
228
+ )
229
+ trace = AgentTrace(
230
+ name="external_cohort_observation",
231
+ run_id=run_id,
232
+ started_at="unknown",
233
+ metadata={
234
+ "source": "harbor_cohort",
235
+ "synthetic": inventory.get("synthetic") is True,
236
+ "execution_data_present": False,
237
+ "condition": condition,
238
+ **pairing,
239
+ "task_id": task_id,
240
+ "verifier_config_hash": verifier_hash,
241
+ COHORT_KEY: record,
242
+ },
243
+ )
244
+ attach_trial(
245
+ trace,
246
+ {
247
+ "receipt_id": captured_id,
248
+ "source_result": {
249
+ "reference": source_ref["artifact_reference"],
250
+ "sha256": source_ref["artifact_sha256"],
251
+ },
252
+ "measurement": {
253
+ "runtime_ms": metrics["runtime_ms"],
254
+ "scope": "agent_execution",
255
+ "input_tokens": metrics["input_tokens"],
256
+ "output_tokens": metrics["output_tokens"],
257
+ "cached_input_tokens": metrics["cached_input_tokens"],
258
+ "cost_usd": metrics["cost_usd"],
259
+ "usage_provenance": "external_reported",
260
+ "cost_scope": "agent_context",
261
+ },
262
+ "outcome": outcome,
263
+ "pairing": pairing,
264
+ },
265
+ )
266
+ accepted = read_cohort(trace)["accepted"]
267
+ facts = {
268
+ "run_id": run_id,
269
+ "source_receipt_id": receipt["receipt_id"] if receipt else None,
270
+ "source_identity": source_identity,
271
+ "trial_directory": row["trial_directory"],
272
+ "trial_id": source_identity.get("trial_id"),
273
+ "import_status": row["import_status"],
274
+ "outcome": outcome,
275
+ "accepted": accepted,
276
+ "trajectory_available": trajectories,
277
+ "usage_available": usage,
278
+ "pairing": {**pairing, "task_id": task_id, "verifier_config_hash": verifier_hash},
279
+ "metrics": metrics,
280
+ "agent_info": metadata.get("agent_info"),
281
+ "agent_config_hash": metadata.get("agent_config_hash"),
282
+ "environment": metadata.get("environment"),
283
+ "dataset": metadata.get("dataset"),
284
+ "dataset_version": metadata.get("dataset_version"),
285
+ "exception_info": metadata.get("exception_info"),
286
+ "source_versions": inventory["source_versions"],
287
+ "prediction_attribution": "unavailable unless an original finding-linked intervention record is supplied",
288
+ "measurement_provenance": {
289
+ "runtime_scope": "agent_execution",
290
+ "usage": "external_reported_agent_context",
291
+ "cost": "external_reported_agent_context",
292
+ "provider_billing_verified": False,
293
+ "local_estimated_usd": None,
294
+ "self_hosted_operating_cost": None,
295
+ },
296
+ }
297
+ return trace, facts
298
+
299
+
300
+ def _confounders(left: dict, right: dict, protocol: CohortProtocol) -> list[str]:
301
+ result = []
302
+ for field in ("agent_info", "agent_config_hash", "dataset", "dataset_version"):
303
+ a, b = left.get(field), right.get(field)
304
+ if a is None or b is None:
305
+ result.append(field + "_unrecorded")
306
+ elif a != b:
307
+ result.append(field + "_differs")
308
+ if protocol.comparison_kind == "system_level_confounded":
309
+ result.append("system_level_descriptive_design")
310
+ result.extend(
311
+ [
312
+ "provider_queue_cache_and_rate_limit_control_unestablished",
313
+ "independent_held_out_design_unestablished",
314
+ ]
315
+ )
316
+ return result
317
+
318
+
319
+ def _compare(
320
+ native: dict | None,
321
+ left: list[dict],
322
+ right: list[dict],
323
+ right_inventory: dict,
324
+ protocol: CohortProtocol,
325
+ ) -> dict:
326
+ by_id = {row["run_id"]: row for row in (*left, *right)}
327
+ pairs = []
328
+ for pair in native["pairs"] if native else []:
329
+ before, after = by_id[pair["baseline_run_id"]], by_id[pair["candidate_run_id"]]
330
+ correct = (
331
+ before["outcome"]["quality_pass"] is True and after["outcome"]["quality_pass"] is True
332
+ )
333
+ rejected = any(row["accepted"] is False for row in (before, after))
334
+ complete = before["accepted"] is True and after["accepted"] is True
335
+ a, b = before["metrics"]["runtime_ms"], after["metrics"]["runtime_ms"]
336
+ improvement = 100 * (a - b) / a if a is not None and b is not None and a > 0 else None
337
+ improved = (
338
+ improvement is not None
339
+ and b < a
340
+ and improvement >= protocol.minimum_runtime_improvement_pct
341
+ )
342
+ state = (
343
+ "rejected"
344
+ if rejected
345
+ else "verified_quality_preserving_improvement"
346
+ if complete and correct and improved
347
+ else "no_verified_improvement"
348
+ if complete and improvement is not None
349
+ else "indeterminate"
350
+ )
351
+ confounders = _confounders(before, after, protocol)
352
+ pairs.append(
353
+ {
354
+ **pair,
355
+ "state": state,
356
+ "external_correctness_pass": correct,
357
+ "source_acceptance_complete": complete,
358
+ "runtime_improvement_pct": improvement,
359
+ "confounders": confounders,
360
+ "causal_harness_effect": "not_established",
361
+ "source_trial_ids": {
362
+ "baseline": before["trial_id"],
363
+ "candidate": after["trial_id"],
364
+ },
365
+ }
366
+ )
367
+ unidentified = right_inventory["planned_unidentified_trials"] or 0
368
+ denominator = len(right) + unidentified
369
+ wins = sum(pair["state"] == "verified_quality_preserving_improvement" for pair in pairs)
370
+ observations = [
371
+ (pair["pairing_metadata"]["task_id"], pair["deltas"]["runtime_ms"]) for pair in pairs
372
+ ]
373
+ uncertainty = task_weighted_summary(observations, protocol.uncertainty_settings())
374
+ native_unmatched = (
375
+ native["unmatched"]
376
+ if native
377
+ else [
378
+ {
379
+ "side": side,
380
+ "run_id": row["run_id"],
381
+ "reason": "no_native_manifest_for_empty_condition",
382
+ }
383
+ for side, rows in (("baseline", left), ("candidate", right))
384
+ for row in rows
385
+ ]
386
+ )
387
+ mismatch = []
388
+ attempt_indexes = {}
389
+ for side, rows in (("baseline", left), ("candidate", right)):
390
+ indexed = defaultdict(list)
391
+ for row in rows:
392
+ repetition = row["pairing"].get("repetition")
393
+ if repetition is not None:
394
+ indexed[(row["pairing"].get("task_id"), type(repetition), repetition)].append(row)
395
+ attempt_indexes[side] = indexed
396
+ for unmatched in native_unmatched:
397
+ row = by_id[unmatched["run_id"]]
398
+ repetition = row["pairing"].get("repetition")
399
+ compatible_attempt = attempt_indexes[
400
+ "candidate" if unmatched["side"] == "baseline" else "baseline"
401
+ ].get((row["pairing"].get("task_id"), type(repetition), repetition), [])
402
+ reasons = sorted(
403
+ {
404
+ key + "_incompatible"
405
+ for other in compatible_attempt
406
+ for key in COHORT_PAIRING_KEYS
407
+ if row["pairing"].get(key) != other["pairing"].get(key)
408
+ }
409
+ )
410
+ mismatch.append({**unmatched, "source_exclusion_reasons": reasons or [unmatched["reason"]]})
411
+ return {
412
+ "pairs": pairs,
413
+ "paired_count": len(pairs),
414
+ "unmatched": mismatch,
415
+ "unpaired_count": len(mismatch),
416
+ "quality_preserving_intervention_rate": {
417
+ "numerator": wins,
418
+ "denominator": denominator,
419
+ "value": wins / denominator if denominator else None,
420
+ "selection_policy": "all observed candidate attempts plus planned unidentified candidate attempts; unmatched/incomplete attempts cannot win",
421
+ "unidentified_planned_attempts": unidentified,
422
+ "interpretation": "verified wins divided by the full candidate attempt population; unknown/unpaired attempts remain in the denominator, so this is a conservative observed fraction, not an exact causal rate",
423
+ },
424
+ "runtime_task_uncertainty": uncertainty,
425
+ "comparison_kind": protocol.comparison_kind,
426
+ "controlled_design_validated": False,
427
+ "limitations": [
428
+ "source hashes establish supplied-byte consistency, not authenticity",
429
+ "causal model/tool/prompt control and held-out allocation are not proven by equal labels",
430
+ "planned unidentified tasks cannot contribute fabricated task clusters",
431
+ "job concurrency/image cache/provider queuing may confound runtime",
432
+ ],
433
+ "state_counts": dict(sorted(Counter(pair["state"] for pair in pairs).items())),
434
+ }
435
+
436
+
437
+ def write_cohort_study(
438
+ conditions: dict[str, HarborImportResult],
439
+ out: str | Path,
440
+ *,
441
+ baseline: str,
442
+ protocol: CohortProtocol,
443
+ name="External task-paired study",
444
+ original_interventions: tuple[InterventionRecord, ...] = (),
445
+ ) -> CohortStudyResult:
446
+ """Materialize all observed attempts without execution spans or success invention."""
447
+ if not isinstance(conditions, dict) or len(conditions) < 2 or baseline not in conditions:
448
+ _fail("conditions", "at least two explicit conditions and a baseline are required")
449
+ _label(name, "name")
450
+ for key, value in conditions.items():
451
+ _label(key, "condition")
452
+ if not isinstance(value, HarborImportResult):
453
+ _fail("conditions", "sources must be captured Harbor import results")
454
+ if any(not isinstance(record, InterventionRecord) for record in original_interventions):
455
+ _fail(
456
+ "interventions", "original intervention records must use the existing native contract"
457
+ )
458
+ root = Path(out)
459
+ for record in original_interventions:
460
+ InterventionRecord.from_dict(record.to_dict())
461
+ root.mkdir(parents=True, exist_ok=True)
462
+ root = root.resolve()
463
+ folders = {
464
+ condition: output_artifact_path(root, f"condition-{index}")
465
+ for index, condition in enumerate(sorted(conditions))
466
+ }
467
+ paths, populations, observations = {}, {}, {}
468
+ for index, (condition, result) in enumerate(sorted(conditions.items())):
469
+ folder = f"condition-{index}"
470
+ result.write(folders[condition])
471
+ inventory = result.inventory()
472
+ inventory_serialized = canonical_json(inventory).encode()
473
+ inventory_sha256 = sha256(inventory_serialized + b"\n").hexdigest()
474
+ inventory_identity_sha256 = sha256(inventory_serialized).hexdigest()
475
+ receipt_index = {item.receipt_id: item.to_dict() for item in result.trial_receipts}
476
+ trace_index = {trace.run_id: trace for trace in result.traces}
477
+ facts, references = [], []
478
+ for ordinal, row in enumerate(inventory["trial_rows"]):
479
+ trace, fact = _observation(
480
+ result,
481
+ row,
482
+ condition,
483
+ protocol,
484
+ ordinal,
485
+ inventory=inventory,
486
+ receipt=receipt_index.get(row.get("receipt_id")),
487
+ trace_index=trace_index,
488
+ inventory_sha256=inventory_sha256,
489
+ inventory_identity_sha256=inventory_identity_sha256,
490
+ )
491
+ reference = f"{folder}/observations/{trace.run_id}.json"
492
+ write_artifact(root, reference, native_bytes(trace))
493
+ facts.append(fact)
494
+ references.append(reference)
495
+ paths[condition] = references
496
+ observations[condition] = facts
497
+ planned_gap = inventory["planned_unidentified_trials"] or 0
498
+ populations[condition] = {
499
+ "observed_attempts": len(facts),
500
+ "planned_attempts": inventory["trials_planned"],
501
+ "planned_unidentified_attempts": planned_gap,
502
+ "denominator": len(facts) + planned_gap,
503
+ "import_status_counts": dict(
504
+ sorted(Counter(fact["import_status"] for fact in facts).items())
505
+ ),
506
+ "execution_status_counts": dict(
507
+ sorted(Counter(fact["outcome"]["execution_status"] for fact in facts).items())
508
+ ),
509
+ "verifier_status_counts": dict(
510
+ sorted(Counter(fact["outcome"]["verifier_status"] for fact in facts).items())
511
+ ),
512
+ "quality_pass": sum(fact["outcome"]["quality_pass"] is True for fact in facts),
513
+ "quality_fail": sum(fact["outcome"]["quality_pass"] is False for fact in facts),
514
+ "quality_indeterminate": sum(fact["outcome"]["quality_pass"] is None for fact in facts)
515
+ + planned_gap,
516
+ "accepted": sum(fact["accepted"] is True for fact in facts),
517
+ "rejected": sum(fact["accepted"] is False for fact in facts),
518
+ "acceptance_indeterminate": sum(fact["accepted"] is None for fact in facts)
519
+ + planned_gap,
520
+ "metrics": {
521
+ key: _summary([fact["metrics"].get(key) for fact in facts] + [None] * planned_gap)
522
+ for key in _trial_metrics(None)
523
+ },
524
+ "job_timing": inventory.get("job_timing"),
525
+ "job_concurrency": inventory.get("job_concurrency"),
526
+ "source_versions": inventory["source_versions"],
527
+ "trial_rows": facts,
528
+ }
529
+ native = None
530
+ manifest_path = None
531
+ if all(paths.values()):
532
+ manifest = {
533
+ "schema_version": "1.0",
534
+ "name": name,
535
+ "baseline": baseline,
536
+ "conditions": paths,
537
+ "pairing_keys": list(COHORT_PAIRING_KEYS),
538
+ }
539
+ # Native manifest/statistics semantics remain unchanged. Clustered
540
+ # uncertainty is added by the existing shared task-weighted helper.
541
+ write_artifact(root, "study.json", (json.dumps(manifest, indent=2) + "\n").encode())
542
+ manifest_path = root / "study.json"
543
+ native = summarize_study(manifest_path)
544
+ comparisons = {
545
+ candidate: _compare(
546
+ native["comparisons"][candidate] if native else None,
547
+ observations[baseline],
548
+ observations[candidate],
549
+ conditions[candidate].inventory(),
550
+ protocol,
551
+ )
552
+ for candidate in conditions
553
+ if candidate != baseline
554
+ }
555
+ report = {
556
+ "schema_version": "1.0",
557
+ "name": name,
558
+ "baseline": baseline,
559
+ "protocol": {
560
+ "protocol_id": protocol.protocol_id,
561
+ "comparison_kind": protocol.comparison_kind,
562
+ "missingness_policy": protocol.missingness_policy,
563
+ "minimum_runtime_improvement_pct": protocol.minimum_runtime_improvement_pct,
564
+ },
565
+ "conditions": populations,
566
+ "comparisons": comparisons,
567
+ "native_study": native,
568
+ "native_study_scope": "one receipt-only observation per discovered attempt, including failures/missing trajectories; planned unidentified attempts remain count-only gaps",
569
+ "native_manifest": "study.json" if manifest_path else None,
570
+ "native_missing_reason": None
571
+ if manifest_path
572
+ else "a condition has no identified observed attempts",
573
+ "original_intervention_records": [record.to_dict() for record in original_interventions],
574
+ "prediction_attribution": "original supplied records preserved; no estimator attribution invented for arbitrary harness differences",
575
+ "instrumentation_overhead": {
576
+ "status": "unavailable",
577
+ "reason": "no recording-disabled control supplied",
578
+ },
579
+ "synthetic": any(
580
+ result.inventory().get("synthetic") is True for result in conditions.values()
581
+ ),
582
+ }
583
+ serialized = canonical_json(report)
584
+ write_artifact(root, "cohort-report.json", (serialized + "\n").encode())
585
+ from agentloop.html_report import cohort_to_html
586
+
587
+ write_artifact(root, "cohort-report.html", cohort_to_html(report).encode())
588
+ return CohortStudyResult(manifest_path, serialized)
589
+
590
+
591
+ def _source_directory(root: Path, reference: str) -> Path:
592
+ reference = relative_reference(reference)
593
+ current = root.resolve(strict=True)
594
+ base = current
595
+ for part in reference.split("/"):
596
+ current = current / part
597
+ if indirect_path(current):
598
+ _fail("job_path", "source directories cannot traverse symlinks/junctions")
599
+ current = current.resolve(strict=True)
600
+ if not current.is_dir() or not current.is_relative_to(base):
601
+ _fail("job_path", "must be a local directory under the selected source root")
602
+ return current
603
+
604
+
605
+ def materialize_source_study(
606
+ path: str | Path, out: str | Path, *, limits: ImportLimits = ImportLimits()
607
+ ) -> CohortStudyResult:
608
+ """Load a strict data-only source manifest; never launch a benchmark/runtime."""
609
+ path = Path(path)
610
+ payload = load_json_artifact(path.parent, path.name, limits=limits).payload
611
+ validate_json_tree(payload, limits)
612
+ if (
613
+ not isinstance(payload, dict)
614
+ or set(payload) != {"schema_version", "name", "baseline", "sources", "protocol"}
615
+ or payload["schema_version"] != "1.0"
616
+ or not isinstance(payload["sources"], dict)
617
+ ):
618
+ _fail("manifest", "must contain exactly the documented source manifest fields")
619
+ if not isinstance(payload["protocol"], dict) or set(payload["protocol"]) - set(
620
+ CohortProtocol.__dataclass_fields__
621
+ ):
622
+ _fail("protocol", "unsupported protocol fields")
623
+ if "protocol_id" not in payload["protocol"]:
624
+ _fail("protocol", "an explicit protocol identity is required")
625
+ protocol = CohortProtocol(**payload["protocol"])
626
+ sources = {}
627
+ for condition, source in payload["sources"].items():
628
+ required = {
629
+ "system",
630
+ "job_path",
631
+ "scoring",
632
+ "task_scoring",
633
+ "trial_context",
634
+ "expected_hashes",
635
+ "expected_agent",
636
+ "expected_model",
637
+ }
638
+ if (
639
+ not isinstance(source, dict)
640
+ or not required <= set(source)
641
+ or set(source) - (required | {"synthetic"})
642
+ or source["system"] != "harbor"
643
+ ):
644
+ _fail("sources", "initial source manifests require strict completed-Harbor fields")
645
+ scoring = (
646
+ ScoringContract.from_dict(source["scoring"]) if source["scoring"] is not None else None
647
+ )
648
+ if not isinstance(source["task_scoring"], dict):
649
+ _fail("task_scoring", "must be task digest to scoring definitions")
650
+ by_task = {
651
+ key: ScoringContract.from_dict(value) for key, value in source["task_scoring"].items()
652
+ }
653
+ result = import_harbor(
654
+ _source_directory(path.parent, source["job_path"]),
655
+ options=HarborOptions(
656
+ scoring=scoring,
657
+ scoring_by_task_digest=by_task,
658
+ condition=condition,
659
+ protocol_id=protocol.protocol_id,
660
+ trial_context=source["trial_context"],
661
+ expected_hashes=source["expected_hashes"],
662
+ synthetic_fixture=source.get("synthetic", False),
663
+ ),
664
+ limits=limits,
665
+ )
666
+ for receipt in result.trial_receipts:
667
+ info = receipt.to_dict()["source_metadata"].get("agent_info") or {}
668
+ model = (info.get("model_info") or {}).get("name")
669
+ if (
670
+ source["expected_agent"] is not None
671
+ and source["expected_agent"] != info.get("name")
672
+ or source["expected_model"] is not None
673
+ and source["expected_model"] != model
674
+ ):
675
+ _fail(
676
+ "source_identity", "declared model/agent differs from recorded source evidence"
677
+ )
678
+ sources[condition] = result
679
+ return write_cohort_study(
680
+ sources, out, baseline=payload["baseline"], protocol=protocol, name=payload["name"]
681
+ )