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