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,982 @@
|
|
|
1
|
+
"""Read-only ATIF import with qualified native events and immutable receipts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agentloop.events import AgentEvent
|
|
11
|
+
from agentloop.integrations.harbor.atif_validation import (
|
|
12
|
+
fail,
|
|
13
|
+
label,
|
|
14
|
+
timestamp,
|
|
15
|
+
validate_trajectory,
|
|
16
|
+
)
|
|
17
|
+
from agentloop.interoperability.artifacts import native_bytes as _native_bytes
|
|
18
|
+
from agentloop.interoperability.artifacts import write_import_bundle
|
|
19
|
+
from agentloop.interoperability.contracts import (
|
|
20
|
+
IDENTITY_FIELDS,
|
|
21
|
+
ImportReceipt,
|
|
22
|
+
external_id,
|
|
23
|
+
receipt_id,
|
|
24
|
+
summarize_receipts,
|
|
25
|
+
)
|
|
26
|
+
from agentloop.interoperability.evidence import EXTERNAL_KEY
|
|
27
|
+
from agentloop.interoperability.validation import (
|
|
28
|
+
ImportBudget,
|
|
29
|
+
ImportLimits,
|
|
30
|
+
ImportValidationError,
|
|
31
|
+
JsonArtifact,
|
|
32
|
+
load_json_artifact,
|
|
33
|
+
relative_reference,
|
|
34
|
+
)
|
|
35
|
+
from agentloop.interventions import canonical_json
|
|
36
|
+
from agentloop.tracer import AgentTrace
|
|
37
|
+
|
|
38
|
+
ATIF_CONTRACT_REVISION = "07ad34000e4c481451b1a0ea30a4a09548d6de8b"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class AtifOptions:
|
|
43
|
+
capture_content: bool = False
|
|
44
|
+
capture_reasoning: bool = False
|
|
45
|
+
capture_token_ids: bool = False
|
|
46
|
+
strict: bool = True
|
|
47
|
+
synthetic_fixture: bool = False
|
|
48
|
+
|
|
49
|
+
def __post_init__(self) -> None:
|
|
50
|
+
if any(type(value) is not bool for value in vars(self).values()):
|
|
51
|
+
fail("options", "capture and validation options must be boolean")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class AtifImportResult:
|
|
56
|
+
traces: tuple[AgentTrace, ...]
|
|
57
|
+
source_receipts: tuple[ImportReceipt, ...]
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def warnings(self) -> tuple[dict, ...]:
|
|
61
|
+
return tuple(
|
|
62
|
+
notice for receipt in self.source_receipts for notice in receipt.to_dict()["notices"]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def inventory(self) -> dict:
|
|
66
|
+
return {
|
|
67
|
+
"schema_version": "1.0",
|
|
68
|
+
"source_format": "atif",
|
|
69
|
+
**summarize_receipts(list(self.source_receipts)),
|
|
70
|
+
"invalid_documents": sum(
|
|
71
|
+
receipt.to_dict()["missing_trace_reason"] == "invalid_artifact"
|
|
72
|
+
for receipt in self.source_receipts
|
|
73
|
+
),
|
|
74
|
+
"timing_unavailable": sum(
|
|
75
|
+
receipt.to_dict()["completeness"]["timing"] != "complete"
|
|
76
|
+
for receipt in self.source_receipts
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
def write(self, out: str | Path) -> Path:
|
|
81
|
+
"""Write stable native files and receipts; conflicting bytes are an error."""
|
|
82
|
+
from agentloop.interoperability.coordination import write_coordination_report
|
|
83
|
+
|
|
84
|
+
inventory = write_import_bundle(self.traces, self.source_receipts, out, self.inventory())
|
|
85
|
+
write_coordination_report(self.traces, self.source_receipts, inventory.parent)
|
|
86
|
+
return inventory
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class _Importer:
|
|
90
|
+
def __init__(
|
|
91
|
+
self, root: Path, limits: ImportLimits, options: AtifOptions, identity: dict[str, str]
|
|
92
|
+
):
|
|
93
|
+
self.root, self.limits, self.options, self.identity = root, limits, options, identity
|
|
94
|
+
self.budget = ImportBudget(limits)
|
|
95
|
+
self.traces: list[AgentTrace] = []
|
|
96
|
+
self.receipts: list[ImportReceipt] = []
|
|
97
|
+
self.active_files: set[str] = set()
|
|
98
|
+
self.completed_files: dict[str, str] = {}
|
|
99
|
+
self.document_ids: set[str] = set()
|
|
100
|
+
self.partial_documents: set[str] = set()
|
|
101
|
+
|
|
102
|
+
def notice(
|
|
103
|
+
self,
|
|
104
|
+
notices: list[dict],
|
|
105
|
+
code: str,
|
|
106
|
+
artifact: JsonArtifact,
|
|
107
|
+
field: str | None = None,
|
|
108
|
+
severity: str = "warning",
|
|
109
|
+
) -> None:
|
|
110
|
+
notices.append(
|
|
111
|
+
{
|
|
112
|
+
"code": code,
|
|
113
|
+
"severity": severity,
|
|
114
|
+
"artifact": artifact.reference,
|
|
115
|
+
"field": field,
|
|
116
|
+
"record": None,
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def text(self, value: str, *, capture: bool) -> Any:
|
|
121
|
+
size = len(value.encode("utf-8"))
|
|
122
|
+
if capture and size <= self.limits.max_metadata_bytes:
|
|
123
|
+
return value
|
|
124
|
+
return {
|
|
125
|
+
"content_available": True,
|
|
126
|
+
"size_bytes": size,
|
|
127
|
+
"capture": "omitted",
|
|
128
|
+
"reason": "size_limit" if capture else "capture_disabled",
|
|
129
|
+
"provenance": "external_reported",
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
def sanitize(self, value: Any) -> Any:
|
|
133
|
+
if isinstance(value, str):
|
|
134
|
+
return self.text(value, capture=self.options.capture_content)
|
|
135
|
+
if isinstance(value, dict):
|
|
136
|
+
result = {}
|
|
137
|
+
safe_labels = {
|
|
138
|
+
"role": {"reviewer", "implementer", "responder"},
|
|
139
|
+
"usage_scope": {
|
|
140
|
+
"includes_subagents",
|
|
141
|
+
"self_only",
|
|
142
|
+
"agent",
|
|
143
|
+
"subagents",
|
|
144
|
+
"agent_and_subagents",
|
|
145
|
+
"model_calls",
|
|
146
|
+
"trial",
|
|
147
|
+
"unknown",
|
|
148
|
+
},
|
|
149
|
+
"cost_scope": {
|
|
150
|
+
"includes_subagents",
|
|
151
|
+
"self_only",
|
|
152
|
+
"agent",
|
|
153
|
+
"subagents",
|
|
154
|
+
"agent_and_subagents",
|
|
155
|
+
"model_calls",
|
|
156
|
+
"trial",
|
|
157
|
+
"unknown",
|
|
158
|
+
},
|
|
159
|
+
"context_operation": {"summarize", "compact", "truncate", "prune"},
|
|
160
|
+
"accounting_source": {
|
|
161
|
+
"provider_reported",
|
|
162
|
+
"external_reported",
|
|
163
|
+
"calculated",
|
|
164
|
+
"unknown",
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
for key, item in value.items():
|
|
168
|
+
normalized = key.lower().replace("-", "_")
|
|
169
|
+
compact = normalized.replace("_", "")
|
|
170
|
+
token_ids = "token_ids" in normalized or normalized == "logprobs"
|
|
171
|
+
sensitive = (
|
|
172
|
+
any(
|
|
173
|
+
word in compact
|
|
174
|
+
for word in (
|
|
175
|
+
"password",
|
|
176
|
+
"passwd",
|
|
177
|
+
"authorization",
|
|
178
|
+
"apikey",
|
|
179
|
+
"secret",
|
|
180
|
+
"credential",
|
|
181
|
+
"bearer",
|
|
182
|
+
"cookie",
|
|
183
|
+
"privatekey",
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
or "token" in compact
|
|
187
|
+
and not token_ids
|
|
188
|
+
and normalized
|
|
189
|
+
not in {
|
|
190
|
+
"prompt_tokens",
|
|
191
|
+
"completion_tokens",
|
|
192
|
+
"input_tokens",
|
|
193
|
+
"output_tokens",
|
|
194
|
+
"cached_tokens",
|
|
195
|
+
"max_tokens",
|
|
196
|
+
"total_tokens",
|
|
197
|
+
}
|
|
198
|
+
)
|
|
199
|
+
reasoning = any(word in normalized for word in ("reasoning", "chain_of_thought"))
|
|
200
|
+
if (
|
|
201
|
+
sensitive
|
|
202
|
+
or reasoning
|
|
203
|
+
and not self.options.capture_reasoning
|
|
204
|
+
or token_ids
|
|
205
|
+
and not self.options.capture_token_ids
|
|
206
|
+
):
|
|
207
|
+
result[key] = {
|
|
208
|
+
"capture": "omitted",
|
|
209
|
+
"reason": "sensitive_field",
|
|
210
|
+
"size_bytes": len(canonical_json(item).encode()),
|
|
211
|
+
}
|
|
212
|
+
elif isinstance(item, str) and item in safe_labels.get(key, set()):
|
|
213
|
+
result[key] = item
|
|
214
|
+
else:
|
|
215
|
+
result[key] = self.sanitize(item)
|
|
216
|
+
return result
|
|
217
|
+
if isinstance(value, list):
|
|
218
|
+
return [self.sanitize(item) for item in value]
|
|
219
|
+
return value
|
|
220
|
+
|
|
221
|
+
def bounded(self, value: Any, notices: list[dict], artifact: JsonArtifact, field: str) -> Any:
|
|
222
|
+
sanitized = self.sanitize(value)
|
|
223
|
+
if len(canonical_json(sanitized).encode()) > self.limits.max_metadata_bytes:
|
|
224
|
+
self.notice(notices, "source_metadata_dropped", artifact, field)
|
|
225
|
+
return {"capture": "omitted", "reason": "metadata_size_limit"}
|
|
226
|
+
return sanitized
|
|
227
|
+
|
|
228
|
+
def arguments(
|
|
229
|
+
self, value: dict, notices: list[dict], artifact: JsonArtifact, field: str
|
|
230
|
+
) -> dict:
|
|
231
|
+
if self.options.capture_content:
|
|
232
|
+
return self.bounded(value, notices, artifact, field)
|
|
233
|
+
return {
|
|
234
|
+
"capture": "omitted",
|
|
235
|
+
"size_bytes": len(canonical_json(value).encode()),
|
|
236
|
+
"provenance": "external_reported",
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
def reference_metadata(
|
|
240
|
+
self, reference: dict, notices: list[dict], artifact: JsonArtifact, field: str
|
|
241
|
+
) -> dict:
|
|
242
|
+
result = {
|
|
243
|
+
"trajectory_id": reference.get("trajectory_id"),
|
|
244
|
+
"session_id": reference.get("session_id"),
|
|
245
|
+
"extra": self.bounded(reference.get("extra"), notices, artifact, field),
|
|
246
|
+
}
|
|
247
|
+
path = reference.get("trajectory_path")
|
|
248
|
+
if path is not None:
|
|
249
|
+
try:
|
|
250
|
+
result["trajectory_path"] = relative_reference(path, self.limits)
|
|
251
|
+
except ImportValidationError:
|
|
252
|
+
result["trajectory_path_sha256"] = sha256(path.encode()).hexdigest()
|
|
253
|
+
result["path_capture"] = "omitted"
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
def content(
|
|
257
|
+
self,
|
|
258
|
+
value: Any,
|
|
259
|
+
notices: list[dict],
|
|
260
|
+
artifact: JsonArtifact,
|
|
261
|
+
field: str,
|
|
262
|
+
*,
|
|
263
|
+
consume_reference: bool = True,
|
|
264
|
+
) -> Any:
|
|
265
|
+
if value is None:
|
|
266
|
+
return None
|
|
267
|
+
if isinstance(value, str):
|
|
268
|
+
return self.text(value, capture=self.options.capture_content)
|
|
269
|
+
parts = []
|
|
270
|
+
for part in value:
|
|
271
|
+
if part["type"] == "text":
|
|
272
|
+
parts.append(
|
|
273
|
+
{
|
|
274
|
+
"type": "text",
|
|
275
|
+
"text": self.text(part["text"], capture=self.options.capture_content),
|
|
276
|
+
}
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
if consume_reference:
|
|
280
|
+
self.budget.consume(references=1)
|
|
281
|
+
source = part["source"]
|
|
282
|
+
marker = {"type": part["type"], "media_type": source["media_type"], "loaded": False}
|
|
283
|
+
try:
|
|
284
|
+
marker["reference"] = relative_reference(source["path"], self.limits)
|
|
285
|
+
except ImportValidationError:
|
|
286
|
+
marker["reference_sha256"] = sha256(source["path"].encode()).hexdigest()
|
|
287
|
+
marker["reference_omitted"] = True
|
|
288
|
+
self.notice(notices, "external_media_reference_omitted", artifact, field)
|
|
289
|
+
if "duration_sec" in source:
|
|
290
|
+
marker["source_duration_sec"] = source["duration_sec"]
|
|
291
|
+
parts.append(marker)
|
|
292
|
+
return parts
|
|
293
|
+
|
|
294
|
+
def interval(
|
|
295
|
+
self, extra: dict | None, notices: list[dict], artifact: JsonArtifact, field: str
|
|
296
|
+
) -> tuple[str, str, float] | None:
|
|
297
|
+
declaration = (extra or {}).get("agentloop")
|
|
298
|
+
if declaration is None:
|
|
299
|
+
return None
|
|
300
|
+
if not isinstance(declaration, dict):
|
|
301
|
+
fail(field + ".agentloop", "measurement extension must be an object")
|
|
302
|
+
timing = declaration.get("timing")
|
|
303
|
+
if timing is None:
|
|
304
|
+
return None
|
|
305
|
+
if not isinstance(timing, dict) or set(timing) - {"started_at", "ended_at", "duration_ms"}:
|
|
306
|
+
fail(field + ".timing", "invalid timing extension")
|
|
307
|
+
if timing.get("started_at") is None or timing.get("ended_at") is None:
|
|
308
|
+
self.notice(notices, "incomplete_source_interval", artifact, field)
|
|
309
|
+
return None
|
|
310
|
+
start = timestamp(timing["started_at"], field + ".timing.started_at")
|
|
311
|
+
end = timestamp(timing["ended_at"], field + ".timing.ended_at")
|
|
312
|
+
if start.tzinfo is None or end.tzinfo is None:
|
|
313
|
+
self.notice(notices, "timestamp_timezone_unknown", artifact, field)
|
|
314
|
+
return None
|
|
315
|
+
duration = (end - start).total_seconds() * 1000
|
|
316
|
+
if duration < 0:
|
|
317
|
+
fail(field + ".timing", "source interval ends before it begins")
|
|
318
|
+
given = timing.get("duration_ms")
|
|
319
|
+
if given is not None and (type(given) not in {int, float} or abs(given - duration) > 0.001):
|
|
320
|
+
fail(field + ".timing", "duration contradicts source interval")
|
|
321
|
+
return timing["started_at"], timing["ended_at"], duration
|
|
322
|
+
|
|
323
|
+
def status(self, extra: dict | None) -> str:
|
|
324
|
+
declaration = (extra or {}).get("agentloop") or {}
|
|
325
|
+
if not isinstance(declaration, dict):
|
|
326
|
+
fail("extra.agentloop", "measurement extension must be an object")
|
|
327
|
+
value = declaration.get("execution_status", "unknown")
|
|
328
|
+
if not isinstance(value, str) or value not in {
|
|
329
|
+
"completed",
|
|
330
|
+
"failed",
|
|
331
|
+
"timed_out",
|
|
332
|
+
"cancelled",
|
|
333
|
+
"partial",
|
|
334
|
+
"missing",
|
|
335
|
+
"unknown",
|
|
336
|
+
}:
|
|
337
|
+
fail("extra.agentloop.execution_status", "unsupported source execution status")
|
|
338
|
+
return value
|
|
339
|
+
|
|
340
|
+
def identity_for(
|
|
341
|
+
self, payload: dict, artifact: JsonArtifact, locator: str, inherited_session: str | None
|
|
342
|
+
) -> tuple[dict, dict]:
|
|
343
|
+
identity = dict.fromkeys(sorted(IDENTITY_FIELDS))
|
|
344
|
+
identity.update(self.identity)
|
|
345
|
+
identity.update(
|
|
346
|
+
session_id=payload.get("session_id") or inherited_session,
|
|
347
|
+
trajectory_id=payload.get("trajectory_id"),
|
|
348
|
+
)
|
|
349
|
+
provenance = {
|
|
350
|
+
key: "external_reported" if value is not None else "unknown"
|
|
351
|
+
for key, value in identity.items()
|
|
352
|
+
}
|
|
353
|
+
if payload.get("session_id") is None and inherited_session is not None:
|
|
354
|
+
provenance["session_id"] = "inferred"
|
|
355
|
+
if identity["trajectory_id"] is None:
|
|
356
|
+
identity["trajectory_id"] = external_id(
|
|
357
|
+
"group",
|
|
358
|
+
"harbor",
|
|
359
|
+
{
|
|
360
|
+
"artifact_sha256": artifact.artifact_sha256,
|
|
361
|
+
"artifact_reference": artifact.reference,
|
|
362
|
+
"locator": locator,
|
|
363
|
+
},
|
|
364
|
+
)
|
|
365
|
+
provenance["trajectory_id"] = "calculated"
|
|
366
|
+
return identity, provenance
|
|
367
|
+
|
|
368
|
+
def base_receipt(
|
|
369
|
+
self, payload: dict, artifact: JsonArtifact, locator: str, inherited_session: str | None
|
|
370
|
+
) -> dict:
|
|
371
|
+
identity, provenance = self.identity_for(payload, artifact, locator, inherited_session)
|
|
372
|
+
version = payload.get("schema_version")
|
|
373
|
+
version = (
|
|
374
|
+
version
|
|
375
|
+
if isinstance(version, str) and version and len(version.encode()) <= 512
|
|
376
|
+
else None
|
|
377
|
+
)
|
|
378
|
+
return {
|
|
379
|
+
"schema_version": "1.0",
|
|
380
|
+
"receipt_id": "pending",
|
|
381
|
+
"source": {
|
|
382
|
+
"system": "harbor",
|
|
383
|
+
"producer_version": None,
|
|
384
|
+
"producer_revision": None,
|
|
385
|
+
"format": "atif",
|
|
386
|
+
"format_version": version,
|
|
387
|
+
"artifact_reference": artifact.reference,
|
|
388
|
+
"artifact_sha256": artifact.artifact_sha256,
|
|
389
|
+
"trust": "agent_writable",
|
|
390
|
+
},
|
|
391
|
+
"external_identity": identity,
|
|
392
|
+
"identity_provenance": provenance,
|
|
393
|
+
"traces": [],
|
|
394
|
+
"missing_trace_reason": "invalid_artifact",
|
|
395
|
+
"outcome": {
|
|
396
|
+
"execution_status": "unknown",
|
|
397
|
+
"verifier_status": "missing",
|
|
398
|
+
"verifier_dimensions": {},
|
|
399
|
+
**dict.fromkeys(("quality_pass",)),
|
|
400
|
+
"quality_basis": "unavailable",
|
|
401
|
+
"scoring_contract": None,
|
|
402
|
+
"verifier_isolation": "unknown",
|
|
403
|
+
},
|
|
404
|
+
"completeness": {
|
|
405
|
+
"trajectory": "unknown",
|
|
406
|
+
"usage": "unknown",
|
|
407
|
+
"quality": "missing",
|
|
408
|
+
"parentage": "unknown",
|
|
409
|
+
"timing": "unknown",
|
|
410
|
+
"cost": "unknown",
|
|
411
|
+
},
|
|
412
|
+
"relationships": [],
|
|
413
|
+
"notices": [],
|
|
414
|
+
"source_metadata": {"document_locator": locator},
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
def file(self, reference: str, depth: int = 0, session: str | None = None) -> str | None:
|
|
418
|
+
if reference in self.active_files:
|
|
419
|
+
fail("reference", "cyclic trajectory reference", "reference_cycle")
|
|
420
|
+
if reference in self.completed_files:
|
|
421
|
+
return self.completed_files[reference]
|
|
422
|
+
try:
|
|
423
|
+
artifact = load_json_artifact(
|
|
424
|
+
self.root, reference, limits=self.limits, budget=self.budget
|
|
425
|
+
)
|
|
426
|
+
except ImportValidationError as exc:
|
|
427
|
+
if (
|
|
428
|
+
exc.code == "limit_exceeded"
|
|
429
|
+
or depth == 0
|
|
430
|
+
and self.options.strict
|
|
431
|
+
or not hasattr(exc, "artifact_sha256")
|
|
432
|
+
):
|
|
433
|
+
raise
|
|
434
|
+
artifact = JsonArtifact(reference, exc.artifact_sha256, exc.byte_count, None)
|
|
435
|
+
self.failed({}, artifact, "root", session, exc)
|
|
436
|
+
return None
|
|
437
|
+
self.active_files.add(reference)
|
|
438
|
+
try:
|
|
439
|
+
identity = self.document(artifact.payload, artifact, "root", depth, session)
|
|
440
|
+
if identity is not None:
|
|
441
|
+
self.completed_files[reference] = identity
|
|
442
|
+
return identity
|
|
443
|
+
finally:
|
|
444
|
+
self.active_files.remove(reference)
|
|
445
|
+
|
|
446
|
+
def resolve(
|
|
447
|
+
self,
|
|
448
|
+
path: str,
|
|
449
|
+
artifact: JsonArtifact,
|
|
450
|
+
depth: int,
|
|
451
|
+
session: str | None,
|
|
452
|
+
notices: list[dict],
|
|
453
|
+
field: str,
|
|
454
|
+
) -> str | None:
|
|
455
|
+
self.budget.consume(references=1)
|
|
456
|
+
try:
|
|
457
|
+
relative_reference(path, self.limits)
|
|
458
|
+
if Path(path).suffix.lower() != ".json":
|
|
459
|
+
fail(
|
|
460
|
+
"reference", "trajectory references must be JSON files", "unsupported_reference"
|
|
461
|
+
)
|
|
462
|
+
reference = (Path(artifact.reference).parent / path).as_posix()
|
|
463
|
+
relative_reference(reference, self.limits)
|
|
464
|
+
return self.file(reference, depth + 1, session)
|
|
465
|
+
except ImportValidationError as exc:
|
|
466
|
+
if exc.code == "limit_exceeded":
|
|
467
|
+
raise
|
|
468
|
+
self.notice(notices, exc.code, artifact, field)
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
def failed(
|
|
472
|
+
self,
|
|
473
|
+
raw: Any,
|
|
474
|
+
artifact: JsonArtifact,
|
|
475
|
+
locator: str,
|
|
476
|
+
session: str | None,
|
|
477
|
+
error: ImportValidationError,
|
|
478
|
+
) -> None:
|
|
479
|
+
safe = {"schema_version": raw.get("schema_version")} if isinstance(raw, dict) else {}
|
|
480
|
+
data = self.base_receipt(safe, artifact, locator, session)
|
|
481
|
+
data["receipt_id"] = receipt_id(data)
|
|
482
|
+
data["completeness"]["trajectory"] = "missing"
|
|
483
|
+
self.notice(data["notices"], error.code, artifact, error.field, "error")
|
|
484
|
+
self.receipts.append(ImportReceipt.from_dict(data, limits=self.limits))
|
|
485
|
+
|
|
486
|
+
def document(
|
|
487
|
+
self,
|
|
488
|
+
raw: Any,
|
|
489
|
+
artifact: JsonArtifact,
|
|
490
|
+
locator: str,
|
|
491
|
+
depth: int,
|
|
492
|
+
inherited_session: str | None,
|
|
493
|
+
) -> str | None:
|
|
494
|
+
if depth >= self.limits.max_depth:
|
|
495
|
+
fail("trajectory.depth", "trajectory nesting limit exceeded", "limit_exceeded")
|
|
496
|
+
self.budget.consume(trajectories=1)
|
|
497
|
+
try:
|
|
498
|
+
payload = validate_trajectory(raw, self.limits)
|
|
499
|
+
except ImportValidationError as exc:
|
|
500
|
+
if exc.code == "limit_exceeded" or self.options.strict and depth == 0:
|
|
501
|
+
raise
|
|
502
|
+
self.failed(raw, artifact, locator, inherited_session, exc)
|
|
503
|
+
return None
|
|
504
|
+
document_id = payload.get("trajectory_id")
|
|
505
|
+
if document_id is not None:
|
|
506
|
+
if document_id in self.document_ids:
|
|
507
|
+
fail(
|
|
508
|
+
"trajectory_id", "duplicate source document identity", "duplicate_trajectory_id"
|
|
509
|
+
)
|
|
510
|
+
self.document_ids.add(document_id)
|
|
511
|
+
receipt = self.base_receipt(payload, artifact, locator, inherited_session)
|
|
512
|
+
receipt["receipt_id"] = receipt_id(receipt)
|
|
513
|
+
identity = receipt["external_identity"]
|
|
514
|
+
run_inputs = {
|
|
515
|
+
key: identity[key] for key in ("job_id", "trial_id", "step_id", "trajectory_id")
|
|
516
|
+
}
|
|
517
|
+
if identity["job_id"] is None or identity["trial_id"] is None:
|
|
518
|
+
run_inputs.update(
|
|
519
|
+
artifact_sha256=artifact.artifact_sha256, artifact_reference=artifact.reference
|
|
520
|
+
)
|
|
521
|
+
run_id = external_id("run", "harbor", run_inputs)
|
|
522
|
+
notices = receipt["notices"]
|
|
523
|
+
children = {}
|
|
524
|
+
for index, child in enumerate(payload.get("subagent_trajectories") or []):
|
|
525
|
+
children[child["trajectory_id"]] = self.document(
|
|
526
|
+
child,
|
|
527
|
+
artifact,
|
|
528
|
+
f"{locator}/subagent_trajectories/{index}",
|
|
529
|
+
depth + 1,
|
|
530
|
+
identity["session_id"],
|
|
531
|
+
)
|
|
532
|
+
root_interval = self.interval(payload.get("extra"), notices, artifact, "extra")
|
|
533
|
+
trace = AgentTrace(
|
|
534
|
+
name=payload["agent"]["name"],
|
|
535
|
+
run_id=run_id,
|
|
536
|
+
started_at=root_interval[0] if root_interval else "unknown",
|
|
537
|
+
ended_at=root_interval[1] if root_interval else None,
|
|
538
|
+
elapsed_ms=root_interval[2] if root_interval else None,
|
|
539
|
+
metadata={
|
|
540
|
+
"source": "harbor_atif",
|
|
541
|
+
"external_identity": identity,
|
|
542
|
+
"source_receipt_id": receipt["receipt_id"],
|
|
543
|
+
"external_evidence_schema": "1.0",
|
|
544
|
+
**({"synthetic": True} if self.options.synthetic_fixture else {}),
|
|
545
|
+
},
|
|
546
|
+
)
|
|
547
|
+
source_steps = []
|
|
548
|
+
reported_calls: int | None = 0
|
|
549
|
+
usage_complete = True
|
|
550
|
+
copied_count = 0
|
|
551
|
+
for step in payload["steps"]:
|
|
552
|
+
step_id = step["step_id"]
|
|
553
|
+
field = f"steps[{step_id - 1}]"
|
|
554
|
+
model_metrics = step.get("metrics") or {}
|
|
555
|
+
observations = (step.get("observation") or {}).get("results") or []
|
|
556
|
+
source_step = {
|
|
557
|
+
"step_id": step_id,
|
|
558
|
+
"source": step["source"],
|
|
559
|
+
"timestamp": step.get("timestamp"),
|
|
560
|
+
"is_copied_context": step.get("is_copied_context", False),
|
|
561
|
+
"llm_call_count": step.get("llm_call_count"),
|
|
562
|
+
"message": self.content(step["message"], notices, artifact, field),
|
|
563
|
+
"tool_calls": [
|
|
564
|
+
{
|
|
565
|
+
"tool_call_id": call["tool_call_id"],
|
|
566
|
+
"function_name": call["function_name"],
|
|
567
|
+
"extra": self.bounded(
|
|
568
|
+
call.get("extra"), notices, artifact, field + ".tool_calls.extra"
|
|
569
|
+
),
|
|
570
|
+
"arguments": self.arguments(
|
|
571
|
+
call["arguments"], notices, artifact, field + ".arguments"
|
|
572
|
+
),
|
|
573
|
+
}
|
|
574
|
+
for call in step.get("tool_calls") or []
|
|
575
|
+
],
|
|
576
|
+
"observations": [
|
|
577
|
+
{
|
|
578
|
+
"source_call_id": result.get("source_call_id"),
|
|
579
|
+
"subagent_trajectory_ref": [
|
|
580
|
+
self.reference_metadata(reference, notices, artifact, field)
|
|
581
|
+
for reference in result.get("subagent_trajectory_ref") or []
|
|
582
|
+
],
|
|
583
|
+
"content": self.content(
|
|
584
|
+
result.get("content"), notices, artifact, field + ".observation"
|
|
585
|
+
),
|
|
586
|
+
"extra": self.bounded(
|
|
587
|
+
result.get("extra"), notices, artifact, field + ".observation.extra"
|
|
588
|
+
),
|
|
589
|
+
}
|
|
590
|
+
for result in observations
|
|
591
|
+
],
|
|
592
|
+
}
|
|
593
|
+
if step.get("reasoning_content") is not None:
|
|
594
|
+
source_step["reasoning_content"] = self.text(
|
|
595
|
+
step["reasoning_content"], capture=self.options.capture_reasoning
|
|
596
|
+
)
|
|
597
|
+
retained_metrics = {
|
|
598
|
+
key: value
|
|
599
|
+
for key, value in model_metrics.items()
|
|
600
|
+
if key not in {"prompt_token_ids", "completion_token_ids", "logprobs", "extra"}
|
|
601
|
+
}
|
|
602
|
+
for key in ("prompt_token_ids", "completion_token_ids", "logprobs"):
|
|
603
|
+
if model_metrics.get(key) is not None:
|
|
604
|
+
values = model_metrics[key]
|
|
605
|
+
retained_metrics[key] = (
|
|
606
|
+
values
|
|
607
|
+
if self.options.capture_token_ids
|
|
608
|
+
and len(canonical_json(values).encode()) <= self.limits.max_metadata_bytes
|
|
609
|
+
else {"count": len(values), "capture": "omitted"}
|
|
610
|
+
)
|
|
611
|
+
if "extra" in model_metrics:
|
|
612
|
+
retained_metrics["extra"] = self.bounded(
|
|
613
|
+
model_metrics["extra"], notices, artifact, field + ".metrics.extra"
|
|
614
|
+
)
|
|
615
|
+
source_step["metrics"] = retained_metrics
|
|
616
|
+
source_step["extra"] = self.bounded(
|
|
617
|
+
step.get("extra"), notices, artifact, field + ".extra"
|
|
618
|
+
)
|
|
619
|
+
source_steps.append(source_step)
|
|
620
|
+
if step.get("is_copied_context") is True:
|
|
621
|
+
copied_count += 1
|
|
622
|
+
continue
|
|
623
|
+
count = step.get("llm_call_count")
|
|
624
|
+
inferred_block = step["source"] == "agent" and any(
|
|
625
|
+
model_metrics.get(key) is not None
|
|
626
|
+
for key in (
|
|
627
|
+
"prompt_tokens",
|
|
628
|
+
"completion_tokens",
|
|
629
|
+
"cost_usd",
|
|
630
|
+
"prompt_token_ids",
|
|
631
|
+
"completion_token_ids",
|
|
632
|
+
"logprobs",
|
|
633
|
+
)
|
|
634
|
+
)
|
|
635
|
+
model_block = step["source"] == "agent" and (
|
|
636
|
+
count is not None and count > 0 or count is None and inferred_block
|
|
637
|
+
)
|
|
638
|
+
if step["source"] == "agent":
|
|
639
|
+
reported_calls = (
|
|
640
|
+
None if reported_calls is None or count is None else reported_calls + count
|
|
641
|
+
)
|
|
642
|
+
step_interval = self.interval(step.get("extra"), notices, artifact, field + ".extra")
|
|
643
|
+
|
|
644
|
+
def event(
|
|
645
|
+
kind: str,
|
|
646
|
+
name: str,
|
|
647
|
+
*,
|
|
648
|
+
call_id: str | None = None,
|
|
649
|
+
interval: tuple | None = step_interval,
|
|
650
|
+
status: str = "unknown",
|
|
651
|
+
metadata: dict | None = None,
|
|
652
|
+
) -> None:
|
|
653
|
+
if len(trace.events) >= self.limits.max_events_per_trace:
|
|
654
|
+
fail("events", "native event count limit exceeded", "limit_exceeded")
|
|
655
|
+
event_id = external_id(
|
|
656
|
+
"event",
|
|
657
|
+
"harbor",
|
|
658
|
+
{"run_id": run_id, "step_id": step_id, "kind": kind, "tool_call_id": call_id},
|
|
659
|
+
)
|
|
660
|
+
trace.add_event(
|
|
661
|
+
AgentEvent(
|
|
662
|
+
event_id=event_id,
|
|
663
|
+
run_id=run_id,
|
|
664
|
+
event_type="model_call"
|
|
665
|
+
if kind == "model"
|
|
666
|
+
else "tool_call"
|
|
667
|
+
if kind == "tool"
|
|
668
|
+
else "source_step",
|
|
669
|
+
name=name,
|
|
670
|
+
started_at=interval[0] if interval else step.get("timestamp") or "unknown",
|
|
671
|
+
ended_at=interval[1] if interval else "unknown",
|
|
672
|
+
duration_ms=interval[2] if interval else 0.0,
|
|
673
|
+
model=(step.get("model_name") or payload["agent"].get("model_name"))
|
|
674
|
+
if kind == "model"
|
|
675
|
+
else None,
|
|
676
|
+
input_tokens=(model_metrics.get("prompt_tokens") or 0)
|
|
677
|
+
if kind == "model"
|
|
678
|
+
else 0,
|
|
679
|
+
output_tokens=(model_metrics.get("completion_tokens") or 0)
|
|
680
|
+
if kind == "model"
|
|
681
|
+
else 0,
|
|
682
|
+
token_provenance="external_reported"
|
|
683
|
+
if kind == "model"
|
|
684
|
+
and (
|
|
685
|
+
model_metrics.get("prompt_tokens") is not None
|
|
686
|
+
or model_metrics.get("completion_tokens") is not None
|
|
687
|
+
)
|
|
688
|
+
else "unavailable",
|
|
689
|
+
status="error" if status in {"failed", "timed_out", "cancelled"} else "ok",
|
|
690
|
+
error="External operation did not complete"
|
|
691
|
+
if status in {"failed", "timed_out", "cancelled"}
|
|
692
|
+
else None,
|
|
693
|
+
metadata={
|
|
694
|
+
"operation_kind": kind,
|
|
695
|
+
"external_evidence_schema": "1.0",
|
|
696
|
+
"source_step_id": step_id,
|
|
697
|
+
"source_step_role": step["source"],
|
|
698
|
+
"source_receipt_id": receipt["receipt_id"],
|
|
699
|
+
"timing_available": interval is not None,
|
|
700
|
+
"timing_provenance": "external_reported" if interval else "unknown",
|
|
701
|
+
"source_status_available": status
|
|
702
|
+
in {"completed", "failed", "timed_out", "cancelled"},
|
|
703
|
+
"source_execution_status": status,
|
|
704
|
+
**(metadata or {}),
|
|
705
|
+
},
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
if model_block:
|
|
710
|
+
usage_complete &= (
|
|
711
|
+
model_metrics.get("prompt_tokens") is not None
|
|
712
|
+
and model_metrics.get("completion_tokens") is not None
|
|
713
|
+
)
|
|
714
|
+
event(
|
|
715
|
+
"model",
|
|
716
|
+
"model_block",
|
|
717
|
+
status=self.status(step.get("extra")),
|
|
718
|
+
metadata={
|
|
719
|
+
"llm_call_count": count,
|
|
720
|
+
"source_metrics": retained_metrics,
|
|
721
|
+
"usage_available": model_metrics.get("prompt_tokens") is not None
|
|
722
|
+
and model_metrics.get("completion_tokens") is not None,
|
|
723
|
+
"usage_provenance": "external_reported",
|
|
724
|
+
"input_tokens_available": model_metrics.get("prompt_tokens") is not None,
|
|
725
|
+
"output_tokens_available": model_metrics.get("completion_tokens")
|
|
726
|
+
is not None,
|
|
727
|
+
"cached_input_tokens": model_metrics.get("cached_tokens") or 0,
|
|
728
|
+
},
|
|
729
|
+
)
|
|
730
|
+
else:
|
|
731
|
+
if step["source"] == "agent" and count is None:
|
|
732
|
+
usage_complete = False
|
|
733
|
+
kind = (
|
|
734
|
+
"transform"
|
|
735
|
+
if step["source"] == "system"
|
|
736
|
+
and isinstance((step.get("extra") or {}).get("context_operation"), str)
|
|
737
|
+
else "agent"
|
|
738
|
+
if step["source"] == "agent"
|
|
739
|
+
else "unknown"
|
|
740
|
+
)
|
|
741
|
+
event(
|
|
742
|
+
kind,
|
|
743
|
+
step["source"] + "_step",
|
|
744
|
+
status=self.status(step.get("extra")),
|
|
745
|
+
metadata={"llm_call_count": count, "source_content": source_step["message"]},
|
|
746
|
+
)
|
|
747
|
+
for call in step.get("tool_calls") or []:
|
|
748
|
+
results = [
|
|
749
|
+
item
|
|
750
|
+
for item in observations
|
|
751
|
+
if item.get("source_call_id") == call["tool_call_id"]
|
|
752
|
+
]
|
|
753
|
+
if not results:
|
|
754
|
+
self.notice(
|
|
755
|
+
notices, "missing_tool_observation", artifact, field + ".tool_calls"
|
|
756
|
+
)
|
|
757
|
+
call_status = self.status(call.get("extra"))
|
|
758
|
+
result_statuses = [self.status(result.get("extra")) for result in results]
|
|
759
|
+
if call_status == "unknown" and result_statuses:
|
|
760
|
+
call_status = next(
|
|
761
|
+
(
|
|
762
|
+
status
|
|
763
|
+
for status in result_statuses
|
|
764
|
+
if status in {"failed", "timed_out", "cancelled"}
|
|
765
|
+
),
|
|
766
|
+
"completed"
|
|
767
|
+
if all(status == "completed" for status in result_statuses)
|
|
768
|
+
else "unknown",
|
|
769
|
+
)
|
|
770
|
+
event(
|
|
771
|
+
"tool",
|
|
772
|
+
call["function_name"],
|
|
773
|
+
call_id=call["tool_call_id"],
|
|
774
|
+
interval=self.interval(
|
|
775
|
+
call.get("extra"), notices, artifact, field + ".tool_calls.extra"
|
|
776
|
+
),
|
|
777
|
+
status=call_status,
|
|
778
|
+
metadata={
|
|
779
|
+
"tool_call_id": call["tool_call_id"],
|
|
780
|
+
"observation_available": bool(results),
|
|
781
|
+
"source_arguments": self.arguments(
|
|
782
|
+
call["arguments"], notices, artifact, field + ".tool_calls.arguments"
|
|
783
|
+
),
|
|
784
|
+
"source_results": [
|
|
785
|
+
self.content(
|
|
786
|
+
item.get("content"),
|
|
787
|
+
notices,
|
|
788
|
+
artifact,
|
|
789
|
+
field + ".observation",
|
|
790
|
+
consume_reference=False,
|
|
791
|
+
)
|
|
792
|
+
for item in results
|
|
793
|
+
],
|
|
794
|
+
},
|
|
795
|
+
)
|
|
796
|
+
for observation in observations:
|
|
797
|
+
for reference in observation.get("subagent_trajectory_ref") or []:
|
|
798
|
+
self.budget.consume(references=1)
|
|
799
|
+
target = reference.get("trajectory_id")
|
|
800
|
+
resolved = children.get(target) if target is not None else None
|
|
801
|
+
if resolved is None and reference.get("trajectory_path") is not None:
|
|
802
|
+
resolved = self.resolve(
|
|
803
|
+
reference["trajectory_path"],
|
|
804
|
+
artifact,
|
|
805
|
+
depth,
|
|
806
|
+
reference.get("session_id") or identity["session_id"],
|
|
807
|
+
notices,
|
|
808
|
+
field,
|
|
809
|
+
)
|
|
810
|
+
if resolved is None:
|
|
811
|
+
self.notice(notices, "unresolved_subagent", artifact, field)
|
|
812
|
+
elif target is not None and resolved != target:
|
|
813
|
+
self.notice(notices, "reference_identity_mismatch", artifact, field)
|
|
814
|
+
resolved = None
|
|
815
|
+
receipt["relationships"].append(
|
|
816
|
+
{
|
|
817
|
+
"kind": "delegation",
|
|
818
|
+
"target_id": target
|
|
819
|
+
or resolved
|
|
820
|
+
or external_id(
|
|
821
|
+
"group",
|
|
822
|
+
"harbor",
|
|
823
|
+
{
|
|
824
|
+
"reference_sha256": sha256(
|
|
825
|
+
reference["trajectory_path"].encode()
|
|
826
|
+
).hexdigest()
|
|
827
|
+
},
|
|
828
|
+
),
|
|
829
|
+
"basis": "external_reported",
|
|
830
|
+
"resolved": resolved is not None,
|
|
831
|
+
}
|
|
832
|
+
)
|
|
833
|
+
continuation = payload.get("continued_trajectory_ref")
|
|
834
|
+
if continuation is not None:
|
|
835
|
+
resolved = self.resolve(
|
|
836
|
+
continuation,
|
|
837
|
+
artifact,
|
|
838
|
+
depth,
|
|
839
|
+
identity["session_id"],
|
|
840
|
+
notices,
|
|
841
|
+
"continued_trajectory_ref",
|
|
842
|
+
)
|
|
843
|
+
receipt["relationships"].append(
|
|
844
|
+
{
|
|
845
|
+
"kind": "continuation",
|
|
846
|
+
"target_id": resolved
|
|
847
|
+
or external_id(
|
|
848
|
+
"group",
|
|
849
|
+
"harbor",
|
|
850
|
+
{"reference_sha256": sha256(continuation.encode()).hexdigest()},
|
|
851
|
+
),
|
|
852
|
+
"basis": "external_reported",
|
|
853
|
+
"resolved": resolved is not None,
|
|
854
|
+
}
|
|
855
|
+
)
|
|
856
|
+
timing_complete = bool(trace.events) and all(
|
|
857
|
+
event.metadata["timing_available"] for event in trace.events
|
|
858
|
+
)
|
|
859
|
+
execution_status = self.status(payload.get("extra"))
|
|
860
|
+
source = receipt["source"]
|
|
861
|
+
trace.metadata[EXTERNAL_KEY] = {
|
|
862
|
+
"schema_version": "1.0",
|
|
863
|
+
"source": source,
|
|
864
|
+
"receipt_id": receipt["receipt_id"],
|
|
865
|
+
"runtime_ms": root_interval[2] if root_interval else None,
|
|
866
|
+
"event_timing_complete": timing_complete,
|
|
867
|
+
"execution_status": execution_status,
|
|
868
|
+
"usage_complete": usage_complete,
|
|
869
|
+
"reported_model_call_count": reported_calls,
|
|
870
|
+
"comparison_eligible": False,
|
|
871
|
+
}
|
|
872
|
+
if not timing_complete or root_interval is None:
|
|
873
|
+
self.notice(notices, "timing_unavailable", artifact)
|
|
874
|
+
if execution_status == "unknown":
|
|
875
|
+
self.notice(notices, "execution_status_unknown", artifact)
|
|
876
|
+
receipt["outcome"]["execution_status"] = execution_status
|
|
877
|
+
unresolved = (
|
|
878
|
+
any(
|
|
879
|
+
not item["resolved"] or item["target_id"] in self.partial_documents
|
|
880
|
+
for item in receipt["relationships"]
|
|
881
|
+
)
|
|
882
|
+
or any(child is None or child in self.partial_documents for child in children.values())
|
|
883
|
+
or any(notice["code"] == "missing_tool_observation" for notice in notices)
|
|
884
|
+
)
|
|
885
|
+
if unresolved:
|
|
886
|
+
self.partial_documents.add(identity["trajectory_id"])
|
|
887
|
+
receipt["completeness"].update(
|
|
888
|
+
trajectory="partial" if unresolved else "complete",
|
|
889
|
+
usage="complete" if usage_complete else "partial",
|
|
890
|
+
timing="complete"
|
|
891
|
+
if timing_complete and root_interval
|
|
892
|
+
else "missing"
|
|
893
|
+
if not any(event.metadata["timing_available"] for event in trace.events)
|
|
894
|
+
else "partial",
|
|
895
|
+
parentage="partial" if unresolved else "unknown",
|
|
896
|
+
)
|
|
897
|
+
final_metrics = dict(payload.get("final_metrics") or {})
|
|
898
|
+
if "extra" in final_metrics:
|
|
899
|
+
final_metrics["extra"] = self.bounded(
|
|
900
|
+
final_metrics["extra"], notices, artifact, "final_metrics.extra"
|
|
901
|
+
)
|
|
902
|
+
metadata = {
|
|
903
|
+
"document_locator": locator,
|
|
904
|
+
"adapter_contract_revision": ATIF_CONTRACT_REVISION,
|
|
905
|
+
"import_options": vars(self.options),
|
|
906
|
+
"notes": self.text(payload["notes"], capture=self.options.capture_content)
|
|
907
|
+
if payload.get("notes") is not None
|
|
908
|
+
else None,
|
|
909
|
+
"agent": {
|
|
910
|
+
"name": payload["agent"]["name"],
|
|
911
|
+
"version": payload["agent"]["version"],
|
|
912
|
+
"model_name": payload["agent"].get("model_name"),
|
|
913
|
+
"tool_definitions": self.bounded(
|
|
914
|
+
payload["agent"].get("tool_definitions"),
|
|
915
|
+
notices,
|
|
916
|
+
artifact,
|
|
917
|
+
"agent.tool_definitions",
|
|
918
|
+
),
|
|
919
|
+
"extra": self.bounded(
|
|
920
|
+
payload["agent"].get("extra"), notices, artifact, "agent.extra"
|
|
921
|
+
),
|
|
922
|
+
},
|
|
923
|
+
"steps": source_steps,
|
|
924
|
+
"copied_context_steps": copied_count,
|
|
925
|
+
"source_final_metrics": final_metrics,
|
|
926
|
+
"extra": self.bounded(payload.get("extra"), notices, artifact, "extra"),
|
|
927
|
+
"scope": "one trajectory document; final aggregates may include children and are never added to step usage",
|
|
928
|
+
}
|
|
929
|
+
if len(canonical_json(metadata).encode()) > self.limits.max_metadata_bytes:
|
|
930
|
+
self.notice(notices, "source_metadata_dropped", artifact, "source_metadata.steps")
|
|
931
|
+
metadata["steps"] = {
|
|
932
|
+
"capture": "omitted",
|
|
933
|
+
"source_step_count": len(source_steps),
|
|
934
|
+
"reason": "metadata_size_limit",
|
|
935
|
+
}
|
|
936
|
+
receipt["source_metadata"] = metadata
|
|
937
|
+
trace_data = _native_bytes(trace)
|
|
938
|
+
if len(trace_data) > self.limits.max_json_bytes:
|
|
939
|
+
fail("native_trace", "native artifact exceeds configured byte limit", "limit_exceeded")
|
|
940
|
+
receipt["traces"] = [
|
|
941
|
+
{
|
|
942
|
+
"run_id": run_id,
|
|
943
|
+
"trace_file": f"traces/{run_id}.json",
|
|
944
|
+
"trace_sha256": sha256(trace_data).hexdigest(),
|
|
945
|
+
}
|
|
946
|
+
]
|
|
947
|
+
receipt["missing_trace_reason"] = None
|
|
948
|
+
self.receipts.append(ImportReceipt.from_dict(receipt, limits=self.limits))
|
|
949
|
+
self.traces.append(trace)
|
|
950
|
+
return identity["trajectory_id"]
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def import_atif(
|
|
954
|
+
path: str | Path,
|
|
955
|
+
*,
|
|
956
|
+
root: str | Path | None = None,
|
|
957
|
+
identity: dict[str, str] | None = None,
|
|
958
|
+
limits: ImportLimits = ImportLimits(),
|
|
959
|
+
options: AtifOptions = AtifOptions(),
|
|
960
|
+
) -> AtifImportResult:
|
|
961
|
+
"""Import one ATIF graph; no binary, remote, verifier or task code is loaded."""
|
|
962
|
+
input_path = Path(path).absolute()
|
|
963
|
+
if root is None:
|
|
964
|
+
source_root = input_path.parent.resolve()
|
|
965
|
+
reference = input_path.name
|
|
966
|
+
else:
|
|
967
|
+
root_path = Path(root).absolute()
|
|
968
|
+
source_root = root_path.resolve()
|
|
969
|
+
try:
|
|
970
|
+
reference = input_path.relative_to(root_path).as_posix()
|
|
971
|
+
except ValueError as exc:
|
|
972
|
+
raise ImportValidationError(
|
|
973
|
+
"unsafe_path", "path", "input lies outside the source root"
|
|
974
|
+
) from exc
|
|
975
|
+
context = dict(identity or {})
|
|
976
|
+
if set(context) - {"job_id", "trial_id", "task_id", "task_digest", "step_id"}:
|
|
977
|
+
fail("identity", "unsupported caller identity field")
|
|
978
|
+
for key, value in context.items():
|
|
979
|
+
label(value, "identity." + key)
|
|
980
|
+
importer = _Importer(source_root, limits, options, context)
|
|
981
|
+
importer.file(reference)
|
|
982
|
+
return AtifImportResult(tuple(importer.traces), tuple(importer.receipts))
|