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,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()
|