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,329 @@
1
+ """Narrow strict validation against the pinned ATIF v1.6/v1.7/v1.8 contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from datetime import datetime
7
+ from typing import Any
8
+
9
+ from agentloop.interoperability.validation import ImportLimits, ImportValidationError
10
+
11
+ SUPPORTED_ATIF = frozenset({"ATIF-v1.6", "ATIF-v1.7", "ATIF-v1.8"})
12
+
13
+
14
+ def fail(field: str, reason: str, code: str = "invalid_atif") -> None:
15
+ raise ImportValidationError(code, field, reason)
16
+
17
+
18
+ def obj(value: Any, field: str, allowed: set[str], required: set[str] = frozenset()) -> dict:
19
+ if not isinstance(value, dict) or set(value) - allowed or required - set(value):
20
+ fail(field, "invalid object fields")
21
+ return value
22
+
23
+
24
+ def label(value: Any, field: str) -> str:
25
+ if not isinstance(value, str) or not value.strip() or len(value.encode("utf-8")) > 512:
26
+ fail(field, "identifier must be a nonempty bounded string")
27
+ return value
28
+
29
+
30
+ def sequence(value: Any, field: str) -> list:
31
+ if not isinstance(value, list):
32
+ fail(field, "must be an array")
33
+ return value
34
+
35
+
36
+ def integer(value: Any, field: str, minimum: int = 0) -> None:
37
+ if type(value) is not int or value < minimum:
38
+ fail(field, "must be a nonnegative integer")
39
+
40
+
41
+ def number(value: Any, field: str, *, nonnegative: bool = True) -> None:
42
+ if type(value) not in {int, float} or not math.isfinite(value) or nonnegative and value < 0:
43
+ fail(field, "must be a finite number in the supported range")
44
+
45
+
46
+ def timestamp(value: Any, field: str) -> datetime:
47
+ if not isinstance(value, str) or not value:
48
+ fail(field, "timestamp must be an ISO-8601 string")
49
+ try:
50
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
51
+ except ValueError:
52
+ raise ImportValidationError("invalid_atif", field, "invalid ISO-8601 timestamp") from None
53
+
54
+
55
+ def validate_extra(value: Any, field: str) -> None:
56
+ if value is not None and not isinstance(value, dict):
57
+ fail(field, "extra must be an object or null")
58
+
59
+
60
+ def metrics(value: Any, field: str, *, final: bool = False) -> None:
61
+ prefix = "total_" if final else ""
62
+ token_fields = {prefix + key for key in ("prompt_tokens", "completion_tokens", "cached_tokens")}
63
+ allowed = {*token_fields, prefix + "cost_usd", "extra"}
64
+ allowed |= (
65
+ {"total_steps"} if final else {"prompt_token_ids", "completion_token_ids", "logprobs"}
66
+ )
67
+ obj(value, field, allowed)
68
+ for key in token_fields | ({"total_steps"} if final else set()):
69
+ if value.get(key) is not None:
70
+ integer(value[key], field + "." + key)
71
+ if value.get(prefix + "cost_usd") is not None:
72
+ number(value[prefix + "cost_usd"], field + "." + prefix + "cost_usd")
73
+ prompt, cache = value.get(prefix + "prompt_tokens"), value.get(prefix + "cached_tokens")
74
+ if prompt is not None and cache is not None and cache > prompt:
75
+ fail(field, "cached tokens must be a subset of prompt tokens")
76
+ if not final:
77
+ for key in ("prompt_token_ids", "completion_token_ids"):
78
+ if value.get(key) is not None:
79
+ for item in sequence(value[key], field + "." + key):
80
+ integer(item, field + "." + key)
81
+ if value.get("logprobs") is not None:
82
+ for item in sequence(value["logprobs"], field + ".logprobs"):
83
+ number(item, field + ".logprobs", nonnegative=False)
84
+ validate_extra(value.get("extra"), field + ".extra")
85
+
86
+
87
+ def content(value: Any, field: str, version: str, *, nullable: bool = False) -> None:
88
+ if value is None and nullable or isinstance(value, str):
89
+ return
90
+ for part in sequence(value, field):
91
+ obj(part, field, {"type", "text", "source"}, {"type"})
92
+ kind = part["type"]
93
+ if not isinstance(kind, str) or kind not in {"text", "image", "audio"}:
94
+ fail(field, "unsupported content part")
95
+ if kind == "text":
96
+ if not isinstance(part.get("text"), str) or part.get("source") is not None:
97
+ fail(field, "invalid text part")
98
+ continue
99
+ if kind == "audio" and version != "ATIF-v1.8":
100
+ fail(field, "audio requires ATIF v1.8", "unsupported_atif_semantics")
101
+ source = obj(
102
+ part.get("source"),
103
+ field + ".source",
104
+ {"media_type", "path", "duration_sec"} if kind == "audio" else {"media_type", "path"},
105
+ {"media_type", "path"},
106
+ )
107
+ if (
108
+ part.get("text") is not None
109
+ or not isinstance(source["path"], str)
110
+ or not source["path"]
111
+ ):
112
+ fail(field, "invalid media part")
113
+ media = source["media_type"]
114
+ if not isinstance(media, str):
115
+ fail(field, "media type must be a string")
116
+ allowed = (
117
+ {"image/jpeg", "image/png", "image/gif", "image/webp"}
118
+ if kind == "image"
119
+ else {
120
+ "audio/wav",
121
+ "audio/mpeg",
122
+ "audio/mp4",
123
+ "audio/aac",
124
+ "audio/ogg",
125
+ "audio/flac",
126
+ "audio/webm",
127
+ "audio/aiff",
128
+ "audio/mp3",
129
+ "audio/mpga",
130
+ "audio/x-mpeg",
131
+ "audio/x-wav",
132
+ "audio/wave",
133
+ "audio/vnd.wave",
134
+ "audio/x-m4a",
135
+ "audio/m4a",
136
+ "audio/x-aac",
137
+ "audio/x-flac",
138
+ "audio/x-aiff",
139
+ }
140
+ )
141
+ if media.strip().lower() not in allowed:
142
+ fail(field, "unsupported media type")
143
+ if source.get("duration_sec") is not None:
144
+ number(source["duration_sec"], field + ".source.duration_sec")
145
+
146
+
147
+ def validate_trajectory(payload: Any, limits: ImportLimits) -> dict:
148
+ obj(
149
+ payload,
150
+ "trajectory",
151
+ {
152
+ "schema_version",
153
+ "session_id",
154
+ "trajectory_id",
155
+ "agent",
156
+ "steps",
157
+ "notes",
158
+ "final_metrics",
159
+ "continued_trajectory_ref",
160
+ "extra",
161
+ "subagent_trajectories",
162
+ },
163
+ {"schema_version", "agent", "steps"},
164
+ )
165
+ version = payload["schema_version"]
166
+ if not isinstance(version, str) or version not in SUPPORTED_ATIF:
167
+ fail("schema_version", "unsupported ATIF version", "unsupported_atif_version")
168
+ for key in ("session_id", "trajectory_id", "continued_trajectory_ref"):
169
+ if payload.get(key) is not None:
170
+ label(payload[key], key)
171
+ if payload.get("notes") is not None and not isinstance(payload["notes"], str):
172
+ fail("notes", "notes must be text")
173
+ agent = obj(
174
+ payload["agent"],
175
+ "agent",
176
+ {"name", "version", "model_name", "tool_definitions", "extra"},
177
+ {"name", "version"},
178
+ )
179
+ for key in ("name", "version"):
180
+ label(agent[key], "agent." + key)
181
+ if agent.get("model_name") is not None:
182
+ label(agent["model_name"], "agent.model_name")
183
+ if agent.get("tool_definitions") is not None:
184
+ if any(
185
+ not isinstance(item, dict)
186
+ for item in sequence(agent["tool_definitions"], "agent.tool_definitions")
187
+ ):
188
+ fail("agent.tool_definitions", "tool definitions must be objects")
189
+ validate_extra(agent.get("extra"), "agent.extra")
190
+ validate_extra(payload.get("extra"), "extra")
191
+ if payload.get("final_metrics") is not None:
192
+ metrics(payload["final_metrics"], "final_metrics", final=True)
193
+ steps = sequence(payload["steps"], "steps")
194
+ if not steps or len(steps) > limits.max_events_per_trace:
195
+ fail("steps", "step count outside configured bounds", "limit_exceeded")
196
+ for index, step in enumerate(steps, 1):
197
+ field = f"steps[{index - 1}]"
198
+ obj(
199
+ step,
200
+ field,
201
+ {
202
+ "step_id",
203
+ "timestamp",
204
+ "source",
205
+ "model_name",
206
+ "reasoning_effort",
207
+ "message",
208
+ "reasoning_content",
209
+ "tool_calls",
210
+ "observation",
211
+ "metrics",
212
+ "is_copied_context",
213
+ "llm_call_count",
214
+ "extra",
215
+ },
216
+ {"step_id", "source", "message"},
217
+ )
218
+ if type(step["step_id"]) is not int or step["step_id"] != index:
219
+ fail(field + ".step_id", "step IDs must be sequential starting at one")
220
+ source = step["source"]
221
+ if not isinstance(source, str) or source not in {"agent", "user", "system"}:
222
+ fail(field + ".source", "unsupported step source")
223
+ if step.get("timestamp") is not None:
224
+ timestamp(step["timestamp"], field + ".timestamp")
225
+ content(step["message"], field + ".message", version)
226
+ validate_extra(step.get("extra"), field + ".extra")
227
+ if (
228
+ step.get("is_copied_context") is not None
229
+ and type(step["is_copied_context"]) is not bool
230
+ ):
231
+ fail(field + ".is_copied_context", "copied context flag must be boolean")
232
+ count = step.get("llm_call_count")
233
+ if count is not None:
234
+ integer(count, field + ".llm_call_count")
235
+ agent_fields = (
236
+ "model_name",
237
+ "reasoning_effort",
238
+ "reasoning_content",
239
+ "tool_calls",
240
+ "metrics",
241
+ )
242
+ if source != "agent" and any(step.get(key) is not None for key in agent_fields):
243
+ fail(field, "agent-only fields are present on a non-agent step")
244
+ if (
245
+ count == 0
246
+ and source == "agent"
247
+ and (step.get("metrics") is not None or step.get("reasoning_content") is not None)
248
+ ):
249
+ fail(field, "deterministic dispatch cannot carry model metrics or reasoning")
250
+ if step.get("model_name") is not None:
251
+ label(step["model_name"], field + ".model_name")
252
+ if step.get("reasoning_content") is not None and not isinstance(
253
+ step["reasoning_content"], str
254
+ ):
255
+ fail(field, "reasoning must be text")
256
+ if step.get("reasoning_effort") is not None and type(step["reasoning_effort"]) not in {
257
+ str,
258
+ int,
259
+ float,
260
+ }:
261
+ fail(field, "invalid reasoning effort")
262
+ if step.get("metrics") is not None:
263
+ metrics(step["metrics"], field + ".metrics")
264
+ calls = step.get("tool_calls") or []
265
+ if step.get("tool_calls") is not None:
266
+ sequence(step["tool_calls"], field + ".tool_calls")
267
+ seen = set()
268
+ for call in calls:
269
+ obj(
270
+ call,
271
+ field + ".tool_calls",
272
+ {"tool_call_id", "function_name", "arguments", "extra"},
273
+ {"tool_call_id", "function_name", "arguments"},
274
+ )
275
+ call_id = label(call["tool_call_id"], field + ".tool_call_id")
276
+ label(call["function_name"], field + ".function_name")
277
+ if call_id in seen or not isinstance(call["arguments"], dict):
278
+ fail(field + ".tool_calls", "duplicate tool call identity or invalid arguments")
279
+ seen.add(call_id)
280
+ validate_extra(call.get("extra"), field + ".tool_calls.extra")
281
+ if step.get("observation") is not None:
282
+ observation = obj(step["observation"], field + ".observation", {"results"}, {"results"})
283
+ for result in sequence(observation["results"], field + ".observation.results"):
284
+ obj(
285
+ result,
286
+ field + ".observation.results",
287
+ {"source_call_id", "content", "subagent_trajectory_ref", "extra"},
288
+ )
289
+ if (
290
+ result.get("source_call_id") is not None
291
+ and label(result["source_call_id"], field + ".source_call_id") not in seen
292
+ ):
293
+ fail(field + ".source_call_id", "observation references an unknown tool call")
294
+ content(
295
+ result.get("content"), field + ".observation.content", version, nullable=True
296
+ )
297
+ validate_extra(result.get("extra"), field + ".observation.extra")
298
+ if result.get("subagent_trajectory_ref") is not None:
299
+ for reference in sequence(
300
+ result["subagent_trajectory_ref"], field + ".subagent_trajectory_ref"
301
+ ):
302
+ obj(
303
+ reference,
304
+ field + ".subagent_trajectory_ref",
305
+ {"trajectory_id", "session_id", "trajectory_path", "extra"},
306
+ )
307
+ if (
308
+ reference.get("trajectory_id") is None
309
+ and reference.get("trajectory_path") is None
310
+ ):
311
+ fail(field, "subagent references require a document ID or path")
312
+ for key in ("trajectory_id", "session_id", "trajectory_path"):
313
+ if reference.get(key) is not None:
314
+ label(reference[key], field + ".subagent_trajectory_ref." + key)
315
+ validate_extra(
316
+ reference.get("extra"), field + ".subagent_trajectory_ref.extra"
317
+ )
318
+ children = payload.get("subagent_trajectories") or []
319
+ if payload.get("subagent_trajectories") is not None:
320
+ sequence(payload["subagent_trajectories"], "subagent_trajectories")
321
+ ids = set()
322
+ for child in children:
323
+ if not isinstance(child, dict) or child.get("trajectory_id") is None:
324
+ fail("subagent_trajectories", "embedded children require document IDs")
325
+ identity = label(child["trajectory_id"], "subagent_trajectories.trajectory_id")
326
+ if identity in ids:
327
+ fail("subagent_trajectories", "duplicate embedded document identity")
328
+ ids.add(identity)
329
+ return payload
@@ -0,0 +1,207 @@
1
+ """Offline Harbor commands; importing artifacts never runs a Harbor job."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from agentloop.interoperability.validation import ImportValidationError
11
+
12
+ harbor_app = typer.Typer(help="Import completed Harbor artifacts offline.")
13
+
14
+
15
+ @harbor_app.command("study")
16
+ def study_command(path: Path, out: Path = typer.Option(...)) -> None:
17
+ """Materialize completed source cohorts without running a benchmark job."""
18
+ from agentloop.interoperability.cohorts import materialize_source_study
19
+ from agentloop.studies import StudyValidationError
20
+
21
+ try:
22
+ result = materialize_source_study(path, out)
23
+ except (ImportValidationError, StudyValidationError, OSError) as exc:
24
+ message = (
25
+ str(exc)
26
+ if isinstance(exc, ImportValidationError)
27
+ else "Study sources/output could not be read or written"
28
+ )
29
+ raise typer.BadParameter(message, param_hint="path/--out") from None
30
+ report = result.report()
31
+ typer.echo(
32
+ f"Cohort report: {out / 'cohort-report.json'}; {sum(value['denominator'] for value in report['conditions'].values())} source attempts retained."
33
+ )
34
+ typer.echo(
35
+ "Externally incorrect, unpaired or incomplete attempts cannot count as verified improvements. Native manifest: "
36
+ + (
37
+ str(result.manifest_path)
38
+ if result.manifest_path
39
+ else "unavailable for an empty observed condition"
40
+ )
41
+ )
42
+
43
+
44
+ def _job(
45
+ path: Path,
46
+ *,
47
+ scoring: Path | None,
48
+ condition: str | None,
49
+ protocol_id: str | None,
50
+ context: Path | None,
51
+ synthetic: bool,
52
+ ):
53
+ from agentloop.integrations.harbor.trials import HarborOptions, import_harbor
54
+ from agentloop.integrations.harbor.verifier import ScoringContract
55
+ from agentloop.interoperability.validation import load_json_artifact
56
+
57
+ try:
58
+ criteria = (
59
+ ScoringContract.from_dict(load_json_artifact(scoring.parent, scoring.name).payload)
60
+ if scoring
61
+ else None
62
+ )
63
+ declared = load_json_artifact(context.parent, context.name).payload if context else {}
64
+ return import_harbor(
65
+ path,
66
+ options=HarborOptions(
67
+ scoring=criteria,
68
+ condition=condition,
69
+ protocol_id=protocol_id,
70
+ trial_context=declared,
71
+ synthetic_fixture=synthetic,
72
+ ),
73
+ )
74
+ except (ImportValidationError, OSError) as exc:
75
+ message = (
76
+ str(exc)
77
+ if isinstance(exc, ImportValidationError)
78
+ else "Job artifacts could not be read"
79
+ )
80
+ raise typer.BadParameter(message, param_hint="path") from None
81
+
82
+
83
+ @harbor_app.command("import")
84
+ def import_job_command(
85
+ path: Path,
86
+ out: Path = typer.Option(...),
87
+ scoring: Path | None = typer.Option(
88
+ None, help="Versioned explicit reward thresholds; no implicit positive-reward rule."
89
+ ),
90
+ condition: str | None = typer.Option(None),
91
+ protocol_id: str | None = typer.Option(None),
92
+ context: Path | None = typer.Option(
93
+ None, help="Explicit trial-directory condition/protocol/repetition mapping."
94
+ ),
95
+ synthetic: bool = typer.Option(False),
96
+ ) -> None:
97
+ result = _job(
98
+ path,
99
+ scoring=scoring,
100
+ condition=condition,
101
+ protocol_id=protocol_id,
102
+ context=context,
103
+ synthetic=synthetic,
104
+ )
105
+ try:
106
+ inventory = result.write(out)
107
+ except (ImportValidationError, OSError) as exc:
108
+ message = (
109
+ str(exc) if isinstance(exc, ImportValidationError) else "Output could not be written"
110
+ )
111
+ raise typer.BadParameter(message, param_hint="--out") from None
112
+ stats = result.inventory()
113
+ typer.echo(
114
+ f"{stats['trials_discovered']} trials retained; {stats['trials_imported']} imported; {stats['missing_trajectories']} missing trajectories; {stats['invalid_records']} invalid."
115
+ )
116
+ typer.echo(
117
+ f"External criterion outcomes: {stats['quality_pass']} pass, {stats['quality_fail']} fail, {stats['quality_indeterminate']} indeterminate. Source hashes are reproducibility evidence, not authenticity."
118
+ )
119
+ typer.echo(f"Inventory: {inventory}")
120
+
121
+
122
+ @harbor_app.command("inspect")
123
+ def inspect_job_command(
124
+ path: Path,
125
+ json_out: Path | None = typer.Option(None),
126
+ scoring: Path | None = typer.Option(None),
127
+ ) -> None:
128
+ result = _job(
129
+ path, scoring=scoring, condition=None, protocol_id=None, context=None, synthetic=False
130
+ )
131
+ content = json.dumps(result.inventory(), indent=2)
132
+ if json_out is not None:
133
+ try:
134
+ json_out.parent.mkdir(parents=True, exist_ok=True)
135
+ json_out.write_text(content + "\n", encoding="utf-8")
136
+ except OSError:
137
+ raise typer.BadParameter(
138
+ "Inspection output could not be written", param_hint="--json-out"
139
+ ) from None
140
+ typer.echo(content)
141
+
142
+
143
+ def _import(path: Path, *, strict: bool, root: Path | None, synthetic: bool = False):
144
+ from agentloop.integrations.harbor.atif import AtifOptions, import_atif
145
+
146
+ try:
147
+ return import_atif(
148
+ path, root=root, options=AtifOptions(strict=strict, synthetic_fixture=synthetic)
149
+ )
150
+ except (ImportValidationError, OSError) as exc:
151
+ message = (
152
+ str(exc) if isinstance(exc, ImportValidationError) else "Artifact could not be read"
153
+ )
154
+ raise typer.BadParameter(message, param_hint="path") from None
155
+
156
+
157
+ @harbor_app.command("import-atif")
158
+ def import_atif_command(
159
+ path: Path,
160
+ out: Path = typer.Option(
161
+ ..., help="Output directory for native traces, receipts and inventory."
162
+ ),
163
+ root: Path | None = typer.Option(
164
+ None, help="Allowed local reference root; defaults to the input directory."
165
+ ),
166
+ continue_on_error: bool = typer.Option(
167
+ False, help="Retain semantically invalid documents as error receipts."
168
+ ),
169
+ synthetic: bool = typer.Option(False, help="Explicitly tag owned synthetic fixtures."),
170
+ ) -> None:
171
+ """Import ATIF v1.7/v1.8 and the documented v1.6 compatibility subset."""
172
+ result = _import(path, strict=not continue_on_error, root=root, synthetic=synthetic)
173
+ try:
174
+ inventory = result.write(out)
175
+ except (ImportValidationError, OSError) as exc:
176
+ message = (
177
+ str(exc) if isinstance(exc, ImportValidationError) else "Output could not be written"
178
+ )
179
+ raise typer.BadParameter(message, param_hint="--out") from None
180
+ report = result.inventory()
181
+ typer.echo(
182
+ f"Imported {report['native_traces']} trajectories; {report['invalid_documents']} invalid; {report['timing_unavailable']} with unavailable timing."
183
+ )
184
+ typer.echo(
185
+ "Source usage/status are externally reported; ATIF alone supplies no independent task-quality gate."
186
+ )
187
+ typer.echo(f"Inventory: {inventory}")
188
+
189
+
190
+ @harbor_app.command("inspect-atif")
191
+ def inspect_atif_command(
192
+ path: Path,
193
+ json_out: Path | None = typer.Option(None),
194
+ root: Path | None = typer.Option(None),
195
+ ) -> None:
196
+ """Inspect a trajectory graph without creating native output artifacts."""
197
+ result = _import(path, strict=False, root=root)
198
+ payload = result.inventory()
199
+ if json_out is not None:
200
+ try:
201
+ json_out.parent.mkdir(parents=True, exist_ok=True)
202
+ json_out.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
203
+ except OSError:
204
+ raise typer.BadParameter(
205
+ "Inspection output could not be written", param_hint="--json-out"
206
+ ) from None
207
+ typer.echo(json.dumps(payload, indent=2))
@@ -0,0 +1,86 @@
1
+ """Native study manifests plus complete external source population receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ from agentloop.integrations.harbor.trial_evidence import PAIRING_KEYS
9
+ from agentloop.integrations.harbor.trials import HarborImportResult
10
+ from agentloop.interoperability.artifacts import output_artifact_path, write_artifact
11
+ from agentloop.interoperability.validation import ImportValidationError
12
+
13
+
14
+ def _write_stable(path: Path, value: dict) -> None:
15
+ content = (json.dumps(value, indent=2) + "\n").encode("utf-8")
16
+ try:
17
+ write_artifact(path.parent, path.name, content)
18
+ except ImportValidationError as exc:
19
+ if exc.code != "output_conflict":
20
+ raise
21
+ raise ImportValidationError(
22
+ "output_conflict", "study", "existing study artifact conflicts"
23
+ ) from None
24
+
25
+
26
+ def write_harbor_study(
27
+ conditions: dict[str, HarborImportResult],
28
+ out: str | Path,
29
+ *,
30
+ baseline: str,
31
+ name: str = "Harbor source study",
32
+ ) -> Path:
33
+ if (
34
+ not isinstance(conditions, dict)
35
+ or len(conditions) < 2
36
+ or baseline not in conditions
37
+ or any(not isinstance(key, str) or not key.strip() for key in conditions)
38
+ ):
39
+ raise ImportValidationError(
40
+ "invalid_study",
41
+ "conditions",
42
+ "at least two explicit conditions and a baseline are required",
43
+ )
44
+ root = Path(out)
45
+ root.mkdir(parents=True, exist_ok=True)
46
+ root = root.resolve()
47
+ folders = {
48
+ condition: output_artifact_path(root, f"condition-{index}")
49
+ for index, condition in enumerate(sorted(conditions))
50
+ }
51
+ manifests = {}
52
+ population = {}
53
+ for index, (condition, result) in enumerate(sorted(conditions.items())):
54
+ folder = f"condition-{index}"
55
+ result.write(folders[condition])
56
+ traces = [f"{folder}/traces/{trace.run_id}.json" for trace in result.trial_traces]
57
+ if not traces:
58
+ raise ImportValidationError(
59
+ "invalid_study",
60
+ "conditions",
61
+ "a condition has no measurable source projections; retain its inventory separately",
62
+ )
63
+ manifests[condition] = traces
64
+ population[condition] = {
65
+ "inventory": f"{folder}/harbor-inventory.json",
66
+ "trials_discovered": result.inventory()["trials_discovered"],
67
+ "planned_unidentified_trials": result.inventory()["planned_unidentified_trials"],
68
+ "native_study_trials": len(traces),
69
+ "excluded_trial_rows": [
70
+ row for row in result.inventory()["trial_rows"] if not row.get("trace_files")
71
+ ],
72
+ "interpretation": "native study statistics describe supplied native projections; complete population and missing trajectories remain in this inventory",
73
+ }
74
+ manifest = {
75
+ "schema_version": "1.0",
76
+ "name": name,
77
+ "baseline": baseline,
78
+ "conditions": manifests,
79
+ "pairing_keys": list(PAIRING_KEYS),
80
+ }
81
+ path = root / "study.json"
82
+ _write_stable(path, manifest)
83
+ _write_stable(
84
+ root / "cohort-inventory.json", {"schema_version": "1.0", "conditions": population}
85
+ )
86
+ return path