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,1109 @@
1
+ """Completed Harbor job inventory, measured phases and external verifier data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import re
8
+ import unicodedata
9
+ from dataclasses import dataclass, field
10
+ from hashlib import sha256
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from agentloop.events import AgentEvent
15
+ from agentloop.integrations.harbor.atif import (
16
+ ATIF_CONTRACT_REVISION,
17
+ AtifImportResult,
18
+ AtifOptions,
19
+ _Importer,
20
+ _native_bytes,
21
+ )
22
+ from agentloop.integrations.harbor.atif_validation import fail, label, timestamp
23
+ from agentloop.integrations.harbor.trial_evidence import attach_trial
24
+ from agentloop.integrations.harbor.verifier import (
25
+ ScoringContract,
26
+ external_outcome,
27
+ reward_dimensions,
28
+ )
29
+ from agentloop.interoperability.contracts import (
30
+ IDENTITY_FIELDS,
31
+ ImportReceipt,
32
+ external_id,
33
+ receipt_id,
34
+ )
35
+ from agentloop.interoperability.validation import (
36
+ ImportBudget,
37
+ ImportLimits,
38
+ ImportValidationError,
39
+ JsonArtifact,
40
+ indirect_path,
41
+ load_json_artifact,
42
+ relative_reference,
43
+ )
44
+ from agentloop.interventions import canonical_json
45
+ from agentloop.tracer import AgentTrace
46
+
47
+ TRIAL_CONTRACT_REVISION = "d5ac1be17f575852eaf4fffc4072fd18481c209b"
48
+ _HEX = re.compile(r"^[0-9a-f]{64}$")
49
+
50
+
51
+ def _hash(value: Any) -> str:
52
+ return sha256(canonical_json(value).encode()).hexdigest()
53
+
54
+
55
+ def _object(value: Any, field_name: str) -> dict:
56
+ if value is None:
57
+ return {}
58
+ if not isinstance(value, dict):
59
+ fail(field_name, "metadata must be an object or null")
60
+ return value
61
+
62
+
63
+ def _digest(value: Any) -> str | None:
64
+ if value is None:
65
+ return None
66
+ if not isinstance(value, str):
67
+ fail("task_digest", "task digest must be a SHA-256 string")
68
+ raw = value[7:] if value.startswith("sha256:") else value
69
+ if not _HEX.fullmatch(raw):
70
+ fail("task_digest", "task digest must use lowercase SHA-256")
71
+ return "sha256:" + raw
72
+
73
+
74
+ def _phase(value: Any, field_name: str) -> dict:
75
+ if value is None:
76
+ return {
77
+ "started_at": None,
78
+ "finished_at": None,
79
+ "duration_ms": None,
80
+ "provenance": "unknown",
81
+ }
82
+ if not isinstance(value, dict):
83
+ fail(field_name, "phase timing must be an object")
84
+ start, finish = value.get("started_at"), value.get("finished_at")
85
+ a = timestamp(start, field_name + ".started_at") if start is not None else None
86
+ b = timestamp(finish, field_name + ".finished_at") if finish is not None else None
87
+ duration = None
88
+ if a is not None and b is not None and a.tzinfo is not None and b.tzinfo is not None:
89
+ duration = (b - a).total_seconds() * 1000
90
+ if duration < 0:
91
+ fail(field_name, "phase ends before it begins")
92
+ return {
93
+ "started_at": start,
94
+ "finished_at": finish,
95
+ "duration_ms": duration,
96
+ "provenance": "external_reported",
97
+ }
98
+
99
+
100
+ def _usage(context: Any) -> dict:
101
+ keys = {
102
+ "input_tokens": "n_input_tokens",
103
+ "output_tokens": "n_output_tokens",
104
+ "cached_input_tokens": "n_cache_tokens",
105
+ "cost_usd": "cost_usd",
106
+ }
107
+ if context is None:
108
+ return dict.fromkeys(keys)
109
+ if not isinstance(context, dict):
110
+ fail("agent_result", "agent context must be an object")
111
+ result = {target: context.get(source) for target, source in keys.items()}
112
+ for key, value in result.items():
113
+ if value is not None and (
114
+ type(value) not in ({int, float} if key == "cost_usd" else {int})
115
+ or not math.isfinite(value)
116
+ or value < 0
117
+ ):
118
+ fail("agent_result." + key, "usage/cost must be finite nonnegative or null")
119
+ if (
120
+ result["input_tokens"] is not None
121
+ and result["cached_input_tokens"] is not None
122
+ and result["cached_input_tokens"] > result["input_tokens"]
123
+ ):
124
+ fail("agent_result", "cache tokens are an input subset")
125
+ return result
126
+
127
+
128
+ def _exception(value: Any) -> dict | None:
129
+ if value is None:
130
+ return None
131
+ if not isinstance(value, dict):
132
+ fail("exception_info", "exception info must be an object")
133
+ name = label(value.get("exception_type"), "exception_info.exception_type")
134
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_.]{0,127}", name):
135
+ fail("exception_info.exception_type", "exception class must be a bounded identifier")
136
+ occurred = value.get("occurred_at")
137
+ if occurred is not None:
138
+ timestamp(occurred, "exception_info.occurred_at")
139
+ return {
140
+ "exception_type": name,
141
+ "occurred_at": occurred,
142
+ "message": {
143
+ "capture": "omitted",
144
+ "size_bytes": len(str(value.get("exception_message", "")).encode()),
145
+ },
146
+ "traceback": {
147
+ "capture": "omitted",
148
+ "size_bytes": len(str(value.get("exception_traceback", "")).encode()),
149
+ },
150
+ }
151
+
152
+
153
+ def _execution(exception: dict | None, phase: dict) -> str:
154
+ if exception is not None:
155
+ name = exception["exception_type"]
156
+ if name == "CancelledError":
157
+ return "cancelled"
158
+ if "Timeout" in name or "TimedOut" in name:
159
+ return "timed_out"
160
+ return "failed"
161
+ return (
162
+ "completed"
163
+ if phase["finished_at"] is not None
164
+ else "partial"
165
+ if phase["started_at"] is not None
166
+ else "unknown"
167
+ )
168
+
169
+
170
+ def _safe_reference(value: Any) -> dict | None:
171
+ if value is None:
172
+ return None
173
+ if not isinstance(value, str):
174
+ fail("source_reference", "source reference must be a string")
175
+ try:
176
+ return {"relative_reference": relative_reference(value)}
177
+ except ImportValidationError:
178
+ return {"reference_sha256": sha256(value.encode()).hexdigest(), "capture": "omitted"}
179
+
180
+
181
+ @dataclass(frozen=True)
182
+ class HarborOptions:
183
+ scoring: ScoringContract | None = None
184
+ scoring_by_task_digest: dict[str, ScoringContract] = field(default_factory=dict)
185
+ job_id: str | None = None
186
+ condition: str | None = None
187
+ protocol_id: str | None = None
188
+ trial_context: dict[str, dict] = field(default_factory=dict)
189
+ expected_hashes: dict[str, str] = field(default_factory=dict)
190
+ continue_on_error: bool = True
191
+ allow_legacy_results_name: bool = False
192
+ synthetic_fixture: bool = False
193
+
194
+ def __post_init__(self) -> None:
195
+ if self.scoring is not None and not isinstance(self.scoring, ScoringContract):
196
+ fail("options.scoring", "scoring must be a validated scoring contract")
197
+ if not isinstance(self.scoring_by_task_digest, dict):
198
+ fail("options.scoring_by_task_digest", "task scoring must be an object")
199
+ for digest, scorer in self.scoring_by_task_digest.items():
200
+ if _digest(digest) != digest or not isinstance(scorer, ScoringContract):
201
+ fail(
202
+ "options.scoring_by_task_digest",
203
+ "require prefixed task digests and validated scoring contracts",
204
+ )
205
+ if not isinstance(self.trial_context, dict) or not isinstance(self.expected_hashes, dict):
206
+ fail("options", "context and expected hashes must be objects")
207
+ for name in ("continue_on_error", "allow_legacy_results_name", "synthetic_fixture"):
208
+ if type(getattr(self, name)) is not bool:
209
+ fail("options", "import flags must be boolean")
210
+ for name in ("job_id", "condition", "protocol_id"):
211
+ if getattr(self, name) is not None:
212
+ label(getattr(self, name), "options." + name)
213
+ for reference, digest in self.expected_hashes.items():
214
+ relative_reference(reference)
215
+ if not isinstance(digest, str) or not _HEX.fullmatch(digest):
216
+ fail("expected_hashes", "expected artifact hashes must be SHA-256")
217
+ for name, context in self.trial_context.items():
218
+ # The empty key addresses a directly selected single-trial root.
219
+ if name != "":
220
+ relative_reference(name)
221
+ if not isinstance(context, dict) or set(context) - {
222
+ "condition",
223
+ "protocol_id",
224
+ "repetition",
225
+ }:
226
+ fail("trial_context", "only explicit condition/protocol/repetition are supported")
227
+ for key, value in context.items():
228
+ if key in {"condition", "protocol_id"}:
229
+ label(value, "trial_context." + key)
230
+ if (
231
+ type(value) not in {str, int, float, bool}
232
+ or isinstance(value, float)
233
+ and not math.isfinite(value)
234
+ ):
235
+ fail("trial_context", "context values must be finite typed scalars")
236
+
237
+
238
+ @dataclass(frozen=True)
239
+ class HarborImportResult:
240
+ traces: tuple[AgentTrace, ...]
241
+ trajectory_receipts: tuple[ImportReceipt, ...]
242
+ trial_traces: tuple[AgentTrace, ...]
243
+ trial_receipts: tuple[ImportReceipt, ...]
244
+ _inventory_json: str
245
+
246
+ def inventory(self) -> dict:
247
+ return json.loads(self._inventory_json)
248
+
249
+ def write(self, out: str | Path) -> Path:
250
+ from agentloop.interoperability.artifacts import write_artifact, write_artifact_chunks
251
+
252
+ root = Path(out)
253
+ # Reuse byte-bound export for both source projections and measured trials.
254
+ AtifImportResult(
255
+ (*self.traces, *self.trial_traces), (*self.trajectory_receipts, *self.trial_receipts)
256
+ ).write(root)
257
+ target = root / "harbor-inventory.json"
258
+ content = (self._inventory_json + "\n").encode()
259
+ try:
260
+ write_artifact(root.resolve(), target.name, content)
261
+ except ImportValidationError as exc:
262
+ if exc.code != "output_conflict":
263
+ raise
264
+ fail("output", "existing Harbor inventory conflicts", "output_conflict")
265
+ inventory = self.inventory()
266
+ header = {key: value for key, value in inventory.items() if key != "trial_rows"}
267
+ records = (
268
+ {"schema_version": "1.0", "record_type": "trial", "trial": row}
269
+ for row in inventory["trial_rows"]
270
+ )
271
+ from itertools import chain
272
+
273
+ chunks = (
274
+ (canonical_json(record) + "\n").encode()
275
+ for record in chain(
276
+ ({"schema_version": "1.0", "record_type": "summary", "summary": header},),
277
+ records,
278
+ )
279
+ )
280
+ write_artifact_chunks(root, "harbor-trials.jsonl", chunks)
281
+ return target
282
+
283
+
284
+ class _JobImporter:
285
+ def __init__(self, root: Path, options: HarborOptions, limits: ImportLimits):
286
+ self.root, self.options, self.limits = root, options, limits
287
+ self.budget = ImportBudget(limits)
288
+ self.artifacts: dict[str, JsonArtifact] = {}
289
+ self.artifact_hashes_by_prefix: dict[str, dict[str, str]] = {}
290
+ self.rows: list[dict] = []
291
+ self.traces: list[AgentTrace] = []
292
+ self.trajectory_receipts: list[ImportReceipt] = []
293
+ self.trial_traces: list[AgentTrace] = []
294
+ self.trial_receipts: list[ImportReceipt] = []
295
+ self.job_id = options.job_id
296
+ self.job_id_provenance = "external_reported" if options.job_id else "calculated"
297
+ self.producer_version = self.producer_revision = None
298
+ self.job_witness: JsonArtifact | None = None
299
+ self.job_notices: list[dict] = []
300
+
301
+ def notice(
302
+ self,
303
+ notices: list,
304
+ code: str,
305
+ reference: str,
306
+ field_name: str | None = None,
307
+ severity: str = "warning",
308
+ ):
309
+ notices.append(
310
+ {
311
+ "code": code,
312
+ "severity": severity,
313
+ "artifact": reference,
314
+ "field": field_name,
315
+ "record": None,
316
+ }
317
+ )
318
+
319
+ def load(self, reference: str, notices: list, *, optional: bool = True) -> JsonArtifact | None:
320
+ try:
321
+ if reference in self.artifacts:
322
+ return self.artifacts[reference]
323
+ artifact = load_json_artifact(
324
+ self.root, reference, limits=self.limits, budget=self.budget
325
+ )
326
+ expected = self.options.expected_hashes.get(reference)
327
+ if expected is not None and expected != artifact.artifact_sha256:
328
+ fail(
329
+ "artifact_sha256",
330
+ "source hash differs from the expected artifact",
331
+ "hash_mismatch",
332
+ )
333
+ if not isinstance(artifact.payload, dict):
334
+ fail("artifact", "Harbor metadata must be an object")
335
+ self.artifacts[reference] = artifact
336
+ parts = reference.split("/")
337
+ for depth in range(len(parts)):
338
+ prefix = "/".join(parts[:depth]) + "/" if depth else ""
339
+ self.artifact_hashes_by_prefix.setdefault(prefix, {})[reference] = (
340
+ artifact.artifact_sha256
341
+ )
342
+ return artifact
343
+ except ImportValidationError as exc:
344
+ if exc.code == "limit_exceeded" or (
345
+ not self.options.continue_on_error and exc.code != "missing_artifact"
346
+ ):
347
+ raise
348
+ if exc.code == "missing_artifact" and reference in self.options.expected_hashes:
349
+ self.notice(notices, "expected_artifact_missing", reference, exc.field, "error")
350
+ if not optional or exc.code != "missing_artifact":
351
+ self.notice(notices, exc.code, reference, exc.field, "error")
352
+ return None
353
+
354
+ def discover(self) -> tuple[list[str], dict, list[dict]]:
355
+ notices = []
356
+ config = self.load("config.json", notices)
357
+ lock = self.load("lock.json", notices)
358
+ result = self.load("result.json", notices)
359
+ self.job_witness = result or config or lock
360
+ job = result.payload if result is not None else {}
361
+ single = ("trial_name" in job and "task_checksum" in job) or (
362
+ config is not None and "task" in config.payload and "agent" in config.payload
363
+ )
364
+ if self.job_id is None:
365
+ raw = None if single else job.get("id")
366
+ self.job_id = (
367
+ label(raw, "job.id")
368
+ if raw is not None
369
+ else external_id(
370
+ "group",
371
+ "harbor",
372
+ {
373
+ "selected_directory": self.root.name,
374
+ "config_sha256": config.artifact_sha256 if config else None,
375
+ "lock_sha256": lock.artifact_sha256 if lock else None,
376
+ },
377
+ )
378
+ )
379
+ if raw is not None:
380
+ self.job_id_provenance = "external_reported"
381
+ if lock is not None and isinstance(lock.payload.get("harbor"), dict):
382
+ info = lock.payload["harbor"]
383
+ self.producer_version = (
384
+ label(info["version"], "harbor.version")
385
+ if info.get("version") is not None
386
+ else None
387
+ )
388
+ revision = info.get("git_commit_hash")
389
+ self.producer_revision = (
390
+ revision
391
+ if isinstance(revision, str) and re.fullmatch(r"[0-9a-f]{40}", revision)
392
+ else None
393
+ )
394
+ names = []
395
+ ignored = []
396
+ if single:
397
+ names = [""]
398
+ else:
399
+ seen = set()
400
+ for entry in sorted(self.root.iterdir(), key=lambda item: item.name):
401
+ if not entry.is_dir():
402
+ continue
403
+ name = relative_reference(entry.name, self.limits)
404
+ if "/" in name:
405
+ fail("trial_name", "trial names must be single path components")
406
+ if indirect_path(entry):
407
+ ignored.append({"directory": name, "reason": "unsafe_directory"})
408
+ self.notice(notices, "unsafe_directory", name, severity="error")
409
+ continue
410
+ recognized = any(
411
+ (entry / file).exists()
412
+ for file in ("result.json", "config.json", "lock.json", "results.json")
413
+ )
414
+ if not recognized:
415
+ ignored.append({"directory": name, "reason": "no_trial_metadata"})
416
+ continue
417
+ key = unicodedata.normalize("NFC", name).casefold()
418
+ if key in seen:
419
+ fail("trial_name", "trial directory names collide across supported filesystems")
420
+ seen.add(key)
421
+ names.append(name)
422
+ embedded = job.get("trial_results")
423
+ if embedded is None:
424
+ embedded = []
425
+ if not isinstance(embedded, list):
426
+ fail("job.trial_results", "trial_results must be an array")
427
+ for item in embedded:
428
+ if not isinstance(item, dict):
429
+ fail("job.trial_results", "trial results must be objects")
430
+ name = relative_reference(label(item.get("trial_name"), "trial_name"), self.limits)
431
+ if "/" in name:
432
+ fail("trial_name", "trial names must be single path components")
433
+ if name not in names:
434
+ names.append(name)
435
+ if len(names) > self.limits.max_trials:
436
+ fail("trials", "trial count exceeds configured bounds", "limit_exceeded")
437
+ if not names and not (job and job.get("n_total_trials") is not None):
438
+ fail("job", "no recognizable Harbor trials were found")
439
+ self.job_notices = notices
440
+ return (
441
+ names,
442
+ {"metadata": job, "ignored_directories": ignored, "notices": notices},
443
+ (job.get("trial_results") or []) if not single else [],
444
+ )
445
+
446
+ def trial(self, name: str, embedded: dict | None) -> None:
447
+ self.budget.consume(trials=1)
448
+ prefix = name + "/" if name else ""
449
+ notices = list(self.job_notices)
450
+ result_artifact = self.load(prefix + "result.json", notices)
451
+ if (
452
+ result_artifact is None
453
+ and not (self.root / (prefix + "result.json")).exists()
454
+ and self.options.allow_legacy_results_name
455
+ ):
456
+ result_artifact = self.load(prefix + "results.json", notices)
457
+ elif (self.root / (prefix + "results.json")).exists():
458
+ self.notice(notices, "legacy_result_name_not_used", prefix + "results.json")
459
+ config_artifact = self.load(prefix + "config.json", notices)
460
+ lock_artifact = self.load(prefix + "lock.json", notices)
461
+ witness = result_artifact or config_artifact or lock_artifact or self.job_witness
462
+ if witness is None:
463
+ self.rows.append(
464
+ {
465
+ "trial_directory": name,
466
+ "trace_files": [],
467
+ "receipt_id": None,
468
+ "import_status": "missing",
469
+ "reason": "no_source_metadata",
470
+ }
471
+ )
472
+ return
473
+ result = result_artifact.payload if result_artifact is not None else embedded or {}
474
+ config = (
475
+ config_artifact.payload
476
+ if config_artifact is not None
477
+ else _object(result.get("config"), "config")
478
+ )
479
+ lock = lock_artifact.payload if lock_artifact is not None else {}
480
+ if lock and (
481
+ type(lock.get("schema_version")) is not int or lock["schema_version"] not in {2, 3}
482
+ ):
483
+ fail("lock.schema_version", "only trial lock schema 2/3 projections are supported")
484
+ if not isinstance(config, dict):
485
+ fail("config", "trial config must be an object")
486
+ raw_id = result.get("id")
487
+ trial_id = (
488
+ label(raw_id, "trial.id")
489
+ if raw_id is not None
490
+ else external_id(
491
+ "group",
492
+ "harbor",
493
+ {"job_id": self.job_id, "trial_directory": name or self.root.name},
494
+ )
495
+ )
496
+ checksum = _digest(result.get("task_checksum"))
497
+ task_lock = _object(lock.get("task"), "lock.task")
498
+ if not isinstance(task_lock, dict):
499
+ fail("lock.task", "task lock must be an object")
500
+ locked_digest = _digest(task_lock.get("digest"))
501
+ if checksum is not None and locked_digest is not None and checksum != locked_digest:
502
+ self.notice(
503
+ notices, "task_digest_mismatch", witness.reference, "task_checksum", "error"
504
+ )
505
+ task_digest = checksum or locked_digest
506
+ scoring = self.options.scoring_by_task_digest.get(task_digest, self.options.scoring)
507
+ task_id_data = _object(result.get("task_id"), "task_id")
508
+ if not isinstance(task_id_data, dict):
509
+ fail("task_id", "task identity must be an object")
510
+ task_id = (
511
+ external_id("group", "harbor", {"task_identity_sha256": _hash(task_id_data)})
512
+ if task_id_data
513
+ else None
514
+ )
515
+ identity = dict.fromkeys(sorted(IDENTITY_FIELDS))
516
+ identity.update(
517
+ job_id=self.job_id, trial_id=trial_id, task_id=task_id, task_digest=task_digest
518
+ )
519
+ provenance = {
520
+ key: "external_reported" if value is not None else "unknown"
521
+ for key, value in identity.items()
522
+ }
523
+ provenance["task_id"] = "calculated" if task_id is not None else "unknown"
524
+ provenance["job_id"] = self.job_id_provenance
525
+ if raw_id is None:
526
+ provenance["trial_id"] = "calculated"
527
+ phase = _phase(result.get("agent_execution"), "agent_execution")
528
+ exception = _exception(result.get("exception_info"))
529
+ execution = _execution(exception, phase)
530
+ if not result:
531
+ execution = "missing"
532
+ self.notice(notices, "missing_trial_result", prefix + "result.json")
533
+ for key in ("trial_name", "task_name"):
534
+ if result.get(key) is not None:
535
+ label(result[key], key)
536
+ steps = result.get("step_results")
537
+ if steps is None:
538
+ steps = []
539
+ if not isinstance(steps, list):
540
+ fail("step_results", "step results must be an array")
541
+ step_rows = []
542
+ step_names = set()
543
+ for step in steps:
544
+ if not isinstance(step, dict):
545
+ fail("step_results", "step result must be an object")
546
+ step_name = relative_reference(label(step.get("step_name"), "step_name"), self.limits)
547
+ key = unicodedata.normalize("NFC", step_name).casefold()
548
+ if "/" in step_name or key in step_names:
549
+ fail("step_name", "step names must be distinct portable components")
550
+ step_names.add(key)
551
+ step_rows.append((step_name, step))
552
+ usage = _usage(result.get("agent_result"))
553
+ if result.get("agent_result") is None and step_rows:
554
+ contexts = [_usage(step.get("agent_result")) for _, step in step_rows]
555
+ usage = {
556
+ key: sum(item[key] for item in contexts)
557
+ if all(item[key] is not None for item in contexts)
558
+ else None
559
+ for key in usage
560
+ }
561
+ isolation = result.get("verifier_environment_mode")
562
+ if isolation is None:
563
+ isolation = _object(lock.get("verifier"), "lock.verifier").get("environment_mode")
564
+ if isolation is None:
565
+ isolation = "unknown"
566
+ if not isinstance(isolation, str) or isolation not in {"shared", "separate", "unknown"}:
567
+ fail("verifier_environment_mode", "unsupported verifier isolation")
568
+ quality_status, dimensions = self.verifier(
569
+ prefix, result.get("verifier_result"), notices, exception
570
+ )
571
+ step_details = []
572
+ trajectory_paths = []
573
+ if step_rows:
574
+ for step_name, step in step_rows:
575
+ step_prefix = prefix + "steps/" + step_name + "/"
576
+ step_config = self.load(step_prefix + "config.json", notices)
577
+ configured_mode = (
578
+ _object(step_config.payload.get("verifier"), "step.config.verifier").get(
579
+ "environment_mode"
580
+ )
581
+ if step_config
582
+ else None
583
+ ) or "unknown"
584
+ # Current Harbor StepResult does not record resolved isolation.
585
+ # A requested setting alone is not observed execution metadata.
586
+ mode = step.get("verifier_environment_mode")
587
+ if mode is None:
588
+ mode = "unknown"
589
+ if not isinstance(mode, str) or mode not in {"shared", "separate", "unknown"}:
590
+ fail("step.verifier_environment_mode", "unsupported step verifier isolation")
591
+ step_exception = _exception(step.get("exception_info"))
592
+ step_phase = _phase(step.get("agent_execution"), "step.agent_execution")
593
+ status, scores = self.verifier(
594
+ step_prefix, step.get("verifier_result"), notices, step_exception
595
+ )
596
+ step_details.append(
597
+ {
598
+ "step_name": step_name,
599
+ "agent_execution": step_phase,
600
+ "verifier_timing": _phase(step.get("verifier"), "step.verifier"),
601
+ "configured_verifier_mode": configured_mode,
602
+ "exception_info": step_exception,
603
+ "usage": _usage(step.get("agent_result")),
604
+ "verifier_rewards": scores,
605
+ "outcome": external_outcome(
606
+ execution_status=_execution(step_exception, step_phase),
607
+ verifier_status=status,
608
+ dimensions=scores,
609
+ isolation=mode,
610
+ scoring=scoring,
611
+ ),
612
+ }
613
+ )
614
+ trajectory_paths.append((step_name, step_prefix + "agent/trajectory.json"))
615
+ isolation = (
616
+ "mixed"
617
+ if len({item["outcome"]["verifier_isolation"] for item in step_details}) > 1
618
+ else step_details[0]["outcome"]["verifier_isolation"]
619
+ )
620
+ if result.get("verifier_result") is None:
621
+ # Per-step results retain their individual dimensions; no invented
622
+ # scalar reward or silent averaging across scales.
623
+ dimensions = {}
624
+ quality_status = "missing"
625
+ elif any(
626
+ item["outcome"]["verifier_status"] in {"failed", "invalid"}
627
+ or item["outcome"]["quality_pass"] is False
628
+ for item in step_details
629
+ ):
630
+ quality_status = "invalid"
631
+ self.notice(
632
+ notices,
633
+ "trial_step_quality_conflict",
634
+ witness.reference,
635
+ "step_results",
636
+ "error",
637
+ )
638
+ else:
639
+ trajectory_paths = [(None, prefix + "agent/trajectory.json")]
640
+ outcome = external_outcome(
641
+ execution_status=execution,
642
+ verifier_status=quality_status,
643
+ dimensions=dimensions,
644
+ isolation=isolation,
645
+ scoring=scoring,
646
+ )
647
+ if step_details and result.get("verifier_result") is None and scoring is not None:
648
+ # Preserve per-step predicates; no trial-level scoring rule is inferred.
649
+ outcome["quality_pass"] = None # trial has no compatible trial-level reward contract
650
+ self.notice(
651
+ notices, "step_quality_requires_trial_protocol", witness.reference, "step_results"
652
+ )
653
+ imported = []
654
+ for step_name, reference in trajectory_paths:
655
+ importer = _Importer(
656
+ self.root,
657
+ self.limits,
658
+ AtifOptions(
659
+ strict=not self.options.continue_on_error,
660
+ synthetic_fixture=self.options.synthetic_fixture,
661
+ ),
662
+ {
663
+ key: value
664
+ for key, value in {**identity, "step_id": step_name}.items()
665
+ if key in {"job_id", "trial_id", "task_id", "task_digest", "step_id"}
666
+ and value is not None
667
+ },
668
+ )
669
+ try:
670
+ # All job records share limits, including reference traversals.
671
+ importer.budget = self.budget
672
+ importer.file(reference)
673
+ except ImportValidationError as exc:
674
+ if exc.code == "limit_exceeded" or not self.options.continue_on_error:
675
+ raise
676
+ self.notice(
677
+ notices,
678
+ exc.code,
679
+ reference,
680
+ exc.field,
681
+ "warning" if exc.code == "missing_artifact" else "error",
682
+ )
683
+ if reference in self.options.expected_hashes:
684
+ self.notice(
685
+ notices, "expected_artifact_unavailable", reference, severity="error"
686
+ )
687
+ for source_receipt in importer.receipts:
688
+ source = source_receipt.to_dict()
689
+ source_ref = source["source"]["artifact_reference"]
690
+ expected = self.options.expected_hashes.get(source_ref)
691
+ if expected is not None and expected != source["source"]["artifact_sha256"]:
692
+ self.notice(notices, "hash_mismatch", source_ref, "artifact_sha256", "error")
693
+ if source["missing_trace_reason"] == "invalid_artifact":
694
+ self.notice(notices, "invalid_trajectory", source_ref, severity="error")
695
+ imported.extend(importer.traces)
696
+ self.traces.extend(importer.traces)
697
+ self.trajectory_receipts.extend(importer.receipts)
698
+ if not imported:
699
+ self.notice(notices, "missing_trajectory", witness.reference)
700
+ if (
701
+ embedded is not None
702
+ and result_artifact is not None
703
+ and embedded != result_artifact.payload
704
+ ):
705
+ self.notice(
706
+ notices,
707
+ "embedded_trial_result_conflict",
708
+ witness.reference,
709
+ "trial_results",
710
+ "error",
711
+ )
712
+ errors = any(notice["severity"] == "error" for notice in notices)
713
+ if errors and not self.options.continue_on_error:
714
+ fail("trial", "source metadata is invalid or contradictory")
715
+ if errors:
716
+ outcome = external_outcome(
717
+ execution_status=execution,
718
+ verifier_status="invalid",
719
+ dimensions=dimensions,
720
+ isolation=isolation,
721
+ scoring=scoring,
722
+ )
723
+ receipt = {
724
+ "schema_version": "1.0",
725
+ "receipt_id": "pending",
726
+ "source": {
727
+ "system": "harbor",
728
+ "producer_version": self.producer_version,
729
+ "producer_revision": self.producer_revision,
730
+ "format": "harbor_trial",
731
+ "format_version": None,
732
+ "artifact_reference": witness.reference,
733
+ "artifact_sha256": witness.artifact_sha256,
734
+ "trust": "external_reported",
735
+ },
736
+ "external_identity": identity,
737
+ "identity_provenance": provenance,
738
+ "traces": [],
739
+ "missing_trace_reason": "invalid_artifact" if errors else "missing_artifact",
740
+ "outcome": outcome,
741
+ "completeness": {
742
+ "trajectory": "complete" if imported else "missing",
743
+ "usage": "complete"
744
+ if usage["input_tokens"] is not None and usage["output_tokens"] is not None
745
+ else "partial",
746
+ "quality": "complete" if outcome["verifier_status"] == "available" else "missing",
747
+ "parentage": "unknown",
748
+ "timing": "complete" if phase["duration_ms"] is not None else "missing",
749
+ "cost": "complete" if usage["cost_usd"] is not None else "unknown",
750
+ },
751
+ "relationships": [],
752
+ "notices": notices,
753
+ "source_metadata": {
754
+ "trial_directory": name,
755
+ "trial_name": result.get("trial_name"),
756
+ "task_name": result.get("task_name"),
757
+ "task_type": task_lock.get("type")
758
+ or (
759
+ "git"
760
+ if "git_url" in task_id_data
761
+ else "package"
762
+ if "org" in task_id_data
763
+ else "local"
764
+ if "path" in task_id_data
765
+ else "unknown"
766
+ ),
767
+ "task_reference": {
768
+ "path": _safe_reference(task_id_data.get("path")),
769
+ "git_url": _safe_reference(task_id_data.get("git_url")),
770
+ "git_commit_id": task_id_data.get("git_commit_id"),
771
+ "org": task_id_data.get("org"),
772
+ "name": task_id_data.get("name"),
773
+ "ref": task_id_data.get("ref"),
774
+ },
775
+ "agent_info": self.agent_info(result.get("agent_info")),
776
+ "source": _safe_reference(result.get("source")),
777
+ "source_trial": self.source_trial(
778
+ lock.get("source_trial") or config.get("source_trial")
779
+ ),
780
+ "environment": {
781
+ "type": _object(
782
+ lock.get("environment") or config.get("environment"), "environment"
783
+ ).get("type"),
784
+ "config_sha256": _hash(
785
+ lock.get("environment") or config.get("environment") or {}
786
+ ),
787
+ },
788
+ "phase_timing": {
789
+ key: _phase(result.get(key), key)
790
+ for key in ("agent_execution", "agent_setup", "environment_setup", "verifier")
791
+ },
792
+ "exception_info": exception,
793
+ "usage": usage,
794
+ "trial_wall_timing": _phase(
795
+ {
796
+ "started_at": result.get("started_at"),
797
+ "finished_at": result.get("finished_at"),
798
+ },
799
+ "trial_wall_timing",
800
+ ),
801
+ "verifier_config_hash": _hash(
802
+ {
803
+ "requested": config.get("verifier"),
804
+ "resolved_lock": lock.get("verifier"),
805
+ "isolation": isolation,
806
+ }
807
+ ),
808
+ "agent_config_hash": _hash(config.get("agent") or {}),
809
+ "dataset": _safe_reference(result.get("source")),
810
+ "dataset_version": result.get("dataset_version"),
811
+ "verifier_rewards": dimensions,
812
+ "usage_scope": "agent context, including reported subagents; setup/verifier scope not inferred",
813
+ "artifact_hashes": dict(self.artifact_hashes_by_prefix.get(prefix, {})),
814
+ "step_results": step_details,
815
+ "adapter_contract_revision": TRIAL_CONTRACT_REVISION,
816
+ "trajectory_run_ids": [trace.run_id for trace in imported],
817
+ "source_results_present": result_artifact is not None,
818
+ },
819
+ }
820
+ receipt["receipt_id"] = receipt_id(receipt)
821
+ context = {
822
+ "condition": self.options.condition or result.get("condition"),
823
+ "protocol_id": self.options.protocol_id or result.get("protocol_id"),
824
+ "repetition": result.get("repetition"),
825
+ **self.options.trial_context.get(name, {}),
826
+ }
827
+ environment = lock.get("environment") or config.get("environment") or {}
828
+ pairing = {
829
+ "task_digest": task_digest,
830
+ "scorer_config_hash": scoring.to_dict()["config_sha256"] if scoring else None,
831
+ "environment_hash": _hash(environment) if environment else None,
832
+ "protocol_id": context.get("protocol_id"),
833
+ "repetition": context.get("repetition"),
834
+ }
835
+ if imported and not errors:
836
+ run_id = external_id(
837
+ "run",
838
+ "harbor",
839
+ {
840
+ "job_id": self.job_id,
841
+ "trial_id": trial_id,
842
+ "scope": "agent_execution_measurement",
843
+ },
844
+ )
845
+ trace = AgentTrace(
846
+ name=result.get("trial_name") or name or "harbor_trial",
847
+ run_id=run_id,
848
+ started_at=phase["started_at"] or "unknown",
849
+ ended_at=phase["finished_at"],
850
+ elapsed_ms=phase["duration_ms"],
851
+ metadata={
852
+ "source": "harbor_trial",
853
+ **pairing,
854
+ "condition": context.get("condition"),
855
+ "source_receipt_id": receipt["receipt_id"],
856
+ **({"synthetic": True} if self.options.synthetic_fixture else {}),
857
+ },
858
+ )
859
+ trace.events = [
860
+ AgentEvent(
861
+ event_id=external_id(
862
+ "event", "harbor", {"run_id": run_id, "scope": "agent_execution"}
863
+ ),
864
+ run_id=run_id,
865
+ event_type="source_phase",
866
+ name="agent_execution",
867
+ started_at=phase["started_at"] or "unknown",
868
+ ended_at=phase["finished_at"] or "unknown",
869
+ duration_ms=phase["duration_ms"] or 0.0,
870
+ status="error" if execution in {"failed", "timed_out", "cancelled"} else "ok",
871
+ error="External agent phase did not complete"
872
+ if execution in {"failed", "timed_out", "cancelled"}
873
+ else None,
874
+ metadata={
875
+ "operation_kind": "agent",
876
+ "external_evidence_schema": "1.0",
877
+ "timing_available": phase["duration_ms"] is not None,
878
+ "source_status_available": execution
879
+ in {"completed", "failed", "timed_out", "cancelled"},
880
+ "source_execution_status": execution,
881
+ "error_type": exception["exception_type"] if exception else None,
882
+ },
883
+ )
884
+ ]
885
+ attach_trial(
886
+ trace,
887
+ {
888
+ "receipt_id": receipt["receipt_id"],
889
+ "source_result": {
890
+ "reference": witness.reference,
891
+ "sha256": witness.artifact_sha256,
892
+ },
893
+ "measurement": {
894
+ "runtime_ms": phase["duration_ms"],
895
+ "scope": "agent_execution",
896
+ **usage,
897
+ "usage_provenance": "external_reported",
898
+ "cost_scope": "agent_context",
899
+ },
900
+ "outcome": outcome,
901
+ "pairing": pairing,
902
+ },
903
+ )
904
+ self.trial_traces.append(trace)
905
+ receipt["traces"] = [
906
+ {
907
+ "run_id": trace.run_id,
908
+ "trace_file": f"traces/{trace.run_id}.json",
909
+ "trace_sha256": sha256(_native_bytes(trace)).hexdigest(),
910
+ }
911
+ ]
912
+ receipt["missing_trace_reason"] = None
913
+ frozen = ImportReceipt.from_dict(receipt, limits=self.limits)
914
+ self.trial_receipts.append(frozen)
915
+ self.rows.append(
916
+ {
917
+ "trial_directory": name,
918
+ "trial_id": trial_id,
919
+ "receipt_id": frozen.receipt_id,
920
+ "trace_files": [item["trace_file"] for item in receipt["traces"]],
921
+ "trajectory_run_ids": [trace.run_id for trace in imported],
922
+ "import_status": "invalid"
923
+ if errors
924
+ else "imported"
925
+ if imported
926
+ else "missing_trajectory",
927
+ "execution_status": execution,
928
+ "verifier_status": outcome["verifier_status"],
929
+ "quality_pass": outcome["quality_pass"],
930
+ "pairing": pairing,
931
+ "condition": context.get("condition"),
932
+ }
933
+ )
934
+
935
+ def verifier(
936
+ self, prefix: str, embedded: Any, notices: list, exception: dict | None
937
+ ) -> tuple[str, dict | None]:
938
+ dimensions = None
939
+ status = "missing"
940
+ try:
941
+ if embedded is not None:
942
+ if not isinstance(embedded, dict):
943
+ fail("verifier_result", "verifier result must be an object")
944
+ dimensions = reward_dimensions(embedded.get("rewards"))
945
+ status = "available" if dimensions is not None else "missing"
946
+ except ImportValidationError as exc:
947
+ self.notice(notices, exc.code, prefix + "result.json", exc.field, "error")
948
+ status = "invalid"
949
+ reference = prefix + "verifier/reward.json"
950
+ file = self.load(reference, notices)
951
+ if file is not None:
952
+ try:
953
+ rewards = reward_dimensions(file.payload)
954
+ if dimensions is not None and rewards != dimensions:
955
+ self.notice(notices, "reward_result_conflict", reference, "rewards", "error")
956
+ status = "invalid"
957
+ elif status != "invalid":
958
+ dimensions, status = rewards, "available"
959
+ except ImportValidationError as exc:
960
+ self.notice(notices, exc.code, reference, exc.field, "error")
961
+ status = "invalid"
962
+ if any(
963
+ notice["artifact"] == reference and notice["severity"] == "error" for notice in notices
964
+ ):
965
+ status = "invalid"
966
+ if exception is not None and "Verifier" in exception["exception_type"]:
967
+ status = "failed"
968
+ return status, dimensions
969
+
970
+ def agent_info(self, value: Any) -> dict | None:
971
+ if value is None:
972
+ return None
973
+ if not isinstance(value, dict):
974
+ fail("agent_info", "agent info must be an object")
975
+ model = value.get("model_info") or {}
976
+ if not isinstance(model, dict):
977
+ fail("agent_info.model_info", "model info must be an object")
978
+ return {
979
+ "name": value.get("name"),
980
+ "version": value.get("version"),
981
+ "model_info": {"name": model.get("name"), "provider": model.get("provider")},
982
+ }
983
+
984
+ def source_trial(self, value: Any) -> dict | None:
985
+ if value is None:
986
+ return None
987
+ source = _object(value, "source_trial")
988
+ return {
989
+ "action": source.get("action"),
990
+ "type": source.get("type"),
991
+ "trial_id": source.get("trial_id"),
992
+ "path": _safe_reference(source.get("path")),
993
+ }
994
+
995
+ def run(self) -> HarborImportResult:
996
+ names, job, embedded = self.discover()
997
+ embedded_by_name = {}
998
+ for item in embedded:
999
+ name = item["trial_name"]
1000
+ if name in embedded_by_name:
1001
+ fail("trial_results", "duplicate embedded trial name")
1002
+ embedded_by_name[name] = item
1003
+ for name in names:
1004
+ destinations = (
1005
+ self.traces,
1006
+ self.trajectory_receipts,
1007
+ self.trial_traces,
1008
+ self.trial_receipts,
1009
+ self.rows,
1010
+ )
1011
+ before = [len(items) for items in destinations]
1012
+ try:
1013
+ self.trial(name, embedded_by_name.get(name))
1014
+ except ImportValidationError as exc:
1015
+ if not self.options.continue_on_error or exc.code == "limit_exceeded":
1016
+ raise
1017
+ # A late receipt/measurement failure must not leave orphan native
1018
+ # projections or inflate counts in an otherwise partial import.
1019
+ for items, length in zip(destinations, before):
1020
+ del items[length:]
1021
+ self.rows.append(
1022
+ {
1023
+ "trial_directory": name,
1024
+ "receipt_id": None,
1025
+ "trace_files": [],
1026
+ "import_status": "invalid",
1027
+ "error": exc.to_dict(),
1028
+ }
1029
+ )
1030
+ ids = [row["trial_id"] for row in self.rows if row.get("trial_id") is not None]
1031
+ if len(ids) != len(set(ids)):
1032
+ fail("trial.id", "duplicate trial identity within one job")
1033
+ planned = job["metadata"].get("n_total_trials")
1034
+ if planned is not None and (
1035
+ type(planned) is not int or planned < 0 or planned > self.limits.max_trials
1036
+ ):
1037
+ fail("n_total_trials", "planned trial count outside configured bounds")
1038
+ inventory = {
1039
+ "schema_version": "1.0",
1040
+ "source": "harbor",
1041
+ "synthetic": self.options.synthetic_fixture,
1042
+ "job_id": self.job_id,
1043
+ "source_versions": {
1044
+ "producer_version": self.producer_version,
1045
+ "producer_revision": self.producer_revision,
1046
+ "adapter_contract_revision": TRIAL_CONTRACT_REVISION,
1047
+ "atif_contract_revision": ATIF_CONTRACT_REVISION,
1048
+ },
1049
+ "trials_discovered": len(names),
1050
+ "trials_planned": planned,
1051
+ "planned_unidentified_trials": max(0, planned - len(names))
1052
+ if planned is not None
1053
+ else None,
1054
+ "trials_imported": sum(row["import_status"] == "imported" for row in self.rows),
1055
+ "missing_trajectories": sum(
1056
+ row["import_status"] == "missing_trajectory" for row in self.rows
1057
+ ),
1058
+ "invalid_records": sum(row["import_status"] == "invalid" for row in self.rows),
1059
+ "verifier_available": sum(
1060
+ row.get("verifier_status") == "available" for row in self.rows
1061
+ ),
1062
+ "external_quality_uninterpreted": sum(
1063
+ receipt.to_dict()["outcome"]["quality_basis"] == "external_uninterpreted_reward"
1064
+ for receipt in self.trial_receipts
1065
+ ),
1066
+ "quality_pass": sum(row.get("quality_pass") is True for row in self.rows),
1067
+ "quality_fail": sum(row.get("quality_pass") is False for row in self.rows),
1068
+ "quality_indeterminate": sum(row.get("quality_pass") is None for row in self.rows),
1069
+ "trial_rows": self.rows,
1070
+ "job_notices": job["notices"],
1071
+ "ignored_directories": job["ignored_directories"],
1072
+ "job_artifact_hashes": {
1073
+ name: self.artifacts[name].artifact_sha256
1074
+ for name in ("result.json", "config.json", "lock.json")
1075
+ if name in self.artifacts
1076
+ },
1077
+ "job_timing": None
1078
+ if "trial_name" in job["metadata"] and "task_checksum" in job["metadata"]
1079
+ else _phase(
1080
+ {
1081
+ "started_at": job["metadata"].get("started_at"),
1082
+ "finished_at": job["metadata"].get("finished_at"),
1083
+ },
1084
+ "job_timing",
1085
+ ),
1086
+ "job_timing_scope": "selected_job_metadata; single-trial roots are not job orchestration measurements",
1087
+ "job_concurrency": self.artifacts["config.json"].payload.get("n_concurrent_trials")
1088
+ if "config.json" in self.artifacts
1089
+ else None,
1090
+ }
1091
+ return HarborImportResult(
1092
+ tuple(self.traces),
1093
+ tuple(self.trajectory_receipts),
1094
+ tuple(self.trial_traces),
1095
+ tuple(self.trial_receipts),
1096
+ canonical_json(inventory),
1097
+ )
1098
+
1099
+
1100
+ def import_harbor(
1101
+ path: str | Path,
1102
+ *,
1103
+ options: HarborOptions = HarborOptions(),
1104
+ limits: ImportLimits = ImportLimits(),
1105
+ ) -> HarborImportResult:
1106
+ root = Path(path).resolve(strict=True)
1107
+ if not root.is_dir():
1108
+ fail("path", "Harbor import requires a completed job/trial directory")
1109
+ return _JobImporter(root, options, limits).run()