agentloop-profiler 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,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))