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,82 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def instrument(obj: Any, *, kind: str | None = None, name: str | None = None) -> Any:
|
|
7
|
+
"""Best-effort one-line instrumentation for common agent objects.
|
|
8
|
+
|
|
9
|
+
Explicit `kind` values:
|
|
10
|
+
- `openai`
|
|
11
|
+
- `crewai_crew`
|
|
12
|
+
- `crewai_agent`
|
|
13
|
+
- `crewai_task`
|
|
14
|
+
- `langgraph_state_graph`
|
|
15
|
+
- `langgraph_runnable`
|
|
16
|
+
|
|
17
|
+
Without `kind`, AgentLoop uses duck-typing to choose the safest wrapper.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
normalized = kind.lower().replace("-", "_") if kind else None
|
|
21
|
+
|
|
22
|
+
if normalized == "openai" or _looks_like_openai_client(obj):
|
|
23
|
+
from agentloop.integrations.openai import instrument_openai_client
|
|
24
|
+
|
|
25
|
+
return instrument_openai_client(obj)
|
|
26
|
+
|
|
27
|
+
if normalized == "crewai_crew" or _looks_like_crewai_crew(obj):
|
|
28
|
+
from agentloop.integrations.crewai import instrument_crew
|
|
29
|
+
|
|
30
|
+
return instrument_crew(obj, name=name)
|
|
31
|
+
|
|
32
|
+
if normalized == "crewai_agent":
|
|
33
|
+
from agentloop.integrations.crewai import instrument_agent
|
|
34
|
+
|
|
35
|
+
return instrument_agent(obj, name=name)
|
|
36
|
+
|
|
37
|
+
if normalized == "crewai_task":
|
|
38
|
+
from agentloop.integrations.crewai import instrument_task
|
|
39
|
+
|
|
40
|
+
return instrument_task(obj, name=name)
|
|
41
|
+
|
|
42
|
+
if normalized == "langgraph_state_graph" or _looks_like_state_graph(obj):
|
|
43
|
+
from agentloop.integrations.langgraph import instrument_state_graph
|
|
44
|
+
|
|
45
|
+
return instrument_state_graph(obj)
|
|
46
|
+
|
|
47
|
+
if normalized == "langgraph_runnable" or _looks_like_runnable(obj):
|
|
48
|
+
from agentloop.integrations.langgraph import trace_runnable
|
|
49
|
+
|
|
50
|
+
return trace_runnable(
|
|
51
|
+
obj, name=name or getattr(obj, "name", None) or obj.__class__.__name__
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"Could not infer object type for AgentLoop instrumentation. "
|
|
56
|
+
"Pass kind='openai', 'crewai_crew', 'crewai_agent', 'crewai_task', "
|
|
57
|
+
"'langgraph_state_graph', or 'langgraph_runnable'."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _looks_like_openai_client(obj: Any) -> bool:
|
|
62
|
+
responses = getattr(obj, "responses", None)
|
|
63
|
+
chat = getattr(obj, "chat", None)
|
|
64
|
+
completions = getattr(chat, "completions", None) if chat is not None else None
|
|
65
|
+
return bool(
|
|
66
|
+
responses is not None
|
|
67
|
+
and callable(getattr(responses, "create", None))
|
|
68
|
+
or completions is not None
|
|
69
|
+
and callable(getattr(completions, "create", None))
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _looks_like_crewai_crew(obj: Any) -> bool:
|
|
74
|
+
return callable(getattr(obj, "kickoff", None)) or callable(getattr(obj, "kickoff_async", None))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _looks_like_state_graph(obj: Any) -> bool:
|
|
78
|
+
return callable(getattr(obj, "add_node", None)) and callable(getattr(obj, "compile", None))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _looks_like_runnable(obj: Any) -> bool:
|
|
82
|
+
return callable(getattr(obj, "invoke", None)) or callable(getattr(obj, "ainvoke", None))
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from agentloop.events import AgentEvent, new_event_id, utc_now_iso
|
|
6
|
+
from agentloop.tracer import AgentTrace
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _get(data: Any, key: str, default: Any = None) -> Any:
|
|
10
|
+
if isinstance(data, dict):
|
|
11
|
+
return data.get(key, default)
|
|
12
|
+
return getattr(data, key, default)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def trace_from_vercel_ai_events(
|
|
16
|
+
events: list[dict[str, Any]],
|
|
17
|
+
*,
|
|
18
|
+
name: str = "vercel_ai_run",
|
|
19
|
+
run_id: str | None = None,
|
|
20
|
+
metadata: dict[str, Any] | None = None,
|
|
21
|
+
) -> AgentTrace:
|
|
22
|
+
"""Convert Vercel AI SDK-style telemetry events into an AgentLoop trace.
|
|
23
|
+
|
|
24
|
+
The adapter intentionally accepts plain dictionaries so JS/TS apps can POST or
|
|
25
|
+
export telemetry without importing Python code. Expected fields are flexible:
|
|
26
|
+
`type`/`event_type`, `name`, `duration_ms`, `model`, `input_tokens`,
|
|
27
|
+
`output_tokens`, `status`, and `metadata`.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
trace = AgentTrace(
|
|
31
|
+
name=name, run_id=run_id, metadata={"integration": "vercel_ai", **(metadata or {})}
|
|
32
|
+
)
|
|
33
|
+
for item in events:
|
|
34
|
+
event_type = str(_get(item, "event_type", _get(item, "type", "model_call")))
|
|
35
|
+
if event_type in {"text", "generate", "stream", "model"}:
|
|
36
|
+
event_type = "model_call"
|
|
37
|
+
elif event_type in {"tool", "tool-call", "step"}:
|
|
38
|
+
event_type = "tool_call"
|
|
39
|
+
|
|
40
|
+
trace.add_event(
|
|
41
|
+
AgentEvent(
|
|
42
|
+
event_id=str(_get(item, "event_id", new_event_id())),
|
|
43
|
+
run_id=trace.run_id,
|
|
44
|
+
event_type=event_type,
|
|
45
|
+
name=str(_get(item, "name", event_type)),
|
|
46
|
+
started_at=str(_get(item, "started_at", utc_now_iso())),
|
|
47
|
+
ended_at=str(_get(item, "ended_at", utc_now_iso())),
|
|
48
|
+
duration_ms=float(_get(item, "duration_ms", 0.0) or 0.0),
|
|
49
|
+
parent_id=_get(item, "parent_id"),
|
|
50
|
+
model=_get(item, "model"),
|
|
51
|
+
input_tokens=int(_get(item, "input_tokens", _get(item, "prompt_tokens", 0)) or 0),
|
|
52
|
+
output_tokens=int(
|
|
53
|
+
_get(item, "output_tokens", _get(item, "completion_tokens", 0)) or 0
|
|
54
|
+
),
|
|
55
|
+
input_text=_get(item, "input_text"),
|
|
56
|
+
output_text=_get(item, "output_text"),
|
|
57
|
+
status=str(_get(item, "status", "ok")),
|
|
58
|
+
error=_get(item, "error"),
|
|
59
|
+
metadata={"integration": "vercel_ai", **(_get(item, "metadata", {}) or {})},
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return trace
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
TYPESCRIPT_SNIPPET = r"""
|
|
66
|
+
// Minimal Vercel AI SDK telemetry bridge for AgentLoop.
|
|
67
|
+
// Push these events to your backend, then convert them with
|
|
68
|
+
// agentloop.integrations.vercel_ai.trace_from_vercel_ai_events(...).
|
|
69
|
+
|
|
70
|
+
const agentloopEvents: any[] = [];
|
|
71
|
+
|
|
72
|
+
export function recordAgentLoopEvent(event: any) {
|
|
73
|
+
agentloopEvents.push({
|
|
74
|
+
...event,
|
|
75
|
+
started_at: event.started_at ?? new Date().toISOString(),
|
|
76
|
+
ended_at: event.ended_at ?? new Date().toISOString(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function getAgentLoopEvents() {
|
|
81
|
+
return agentloopEvents;
|
|
82
|
+
}
|
|
83
|
+
"""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Stable local native projections and receipts shared by external importers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Iterable
|
|
9
|
+
|
|
10
|
+
from agentloop.interoperability.contracts import ImportReceipt
|
|
11
|
+
from agentloop.interoperability.validation import (
|
|
12
|
+
ImportValidationError,
|
|
13
|
+
indirect_path,
|
|
14
|
+
relative_reference,
|
|
15
|
+
)
|
|
16
|
+
from agentloop.interventions import canonical_json
|
|
17
|
+
from agentloop.tracer import AgentTrace
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def native_bytes(trace: AgentTrace) -> bytes:
|
|
21
|
+
return (
|
|
22
|
+
json.dumps(trace.to_dict(), indent=2, ensure_ascii=False, allow_nan=False) + "\n"
|
|
23
|
+
).encode("utf-8")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def write_artifact(root: Path, reference: str, data: bytes) -> None:
|
|
27
|
+
"""Reject indirect paths and conflicting bytes under an explicit output root."""
|
|
28
|
+
write_artifact_chunks(root, reference, (data,))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def output_artifact_path(root: Path, reference: str) -> Path:
|
|
32
|
+
"""Check every generated child reference before creating files or directories."""
|
|
33
|
+
relative_reference(reference)
|
|
34
|
+
current = root
|
|
35
|
+
for part in reference.split("/"):
|
|
36
|
+
current = current / part
|
|
37
|
+
try:
|
|
38
|
+
indirect = indirect_path(current)
|
|
39
|
+
except FileNotFoundError:
|
|
40
|
+
indirect = False
|
|
41
|
+
if indirect:
|
|
42
|
+
raise ImportValidationError(
|
|
43
|
+
"unsafe_path", "output", "output references cannot be symlinks"
|
|
44
|
+
)
|
|
45
|
+
return current
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def write_artifact_chunks(root: Path, reference: str, chunks: Iterable[bytes]) -> None:
|
|
49
|
+
"""Write bounded chunks, comparing an existing artifact without loading it all."""
|
|
50
|
+
target = output_artifact_path(root, reference)
|
|
51
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
if target.exists():
|
|
53
|
+
with target.open("rb") as stream:
|
|
54
|
+
for chunk in chunks:
|
|
55
|
+
if stream.read(len(chunk)) != chunk:
|
|
56
|
+
raise ImportValidationError(
|
|
57
|
+
"output_conflict", "output", "existing artifact has conflicting content"
|
|
58
|
+
)
|
|
59
|
+
if stream.read(1):
|
|
60
|
+
raise ImportValidationError(
|
|
61
|
+
"output_conflict", "output", "existing artifact has conflicting content"
|
|
62
|
+
)
|
|
63
|
+
return
|
|
64
|
+
# Open exclusively before entering cleanup, so a failed competing open
|
|
65
|
+
# cannot remove an artifact owned by another writer.
|
|
66
|
+
stream = target.open("xb")
|
|
67
|
+
try:
|
|
68
|
+
with stream:
|
|
69
|
+
for chunk in chunks:
|
|
70
|
+
stream.write(chunk)
|
|
71
|
+
except BaseException:
|
|
72
|
+
# Close before unlinking on Windows. A producer/write error must not
|
|
73
|
+
# leave a partial immutable artifact that blocks a successful retry.
|
|
74
|
+
target.unlink(missing_ok=True)
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def write_import_bundle(
|
|
79
|
+
traces: Iterable[AgentTrace],
|
|
80
|
+
receipts: Iterable[ImportReceipt],
|
|
81
|
+
out: str | Path,
|
|
82
|
+
inventory: dict[str, Any],
|
|
83
|
+
) -> Path:
|
|
84
|
+
"""Verify captured byte hashes before exporting a reproducible import bundle."""
|
|
85
|
+
traces, receipts = tuple(traces), tuple(receipts)
|
|
86
|
+
expected = {
|
|
87
|
+
item["run_id"]: item["trace_sha256"]
|
|
88
|
+
for receipt in receipts
|
|
89
|
+
for item in receipt.to_dict()["traces"]
|
|
90
|
+
}
|
|
91
|
+
if len({trace.run_id for trace in traces}) != len(traces):
|
|
92
|
+
raise ImportValidationError(
|
|
93
|
+
"source_conflict", "native_trace", "duplicate native trace identity"
|
|
94
|
+
)
|
|
95
|
+
for trace in traces:
|
|
96
|
+
if sha256(native_bytes(trace)).hexdigest() != expected.get(trace.run_id):
|
|
97
|
+
raise ImportValidationError(
|
|
98
|
+
"source_conflict", "native_trace", "trace changed after receipt capture"
|
|
99
|
+
)
|
|
100
|
+
root = Path(out)
|
|
101
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
root = root.resolve()
|
|
103
|
+
for trace in traces:
|
|
104
|
+
write_artifact(root, f"traces/{trace.run_id}.json", native_bytes(trace))
|
|
105
|
+
for receipt in receipts:
|
|
106
|
+
write_artifact(
|
|
107
|
+
root,
|
|
108
|
+
f"receipts/{receipt.receipt_id}.json",
|
|
109
|
+
(canonical_json(receipt.to_dict()) + "\n").encode(),
|
|
110
|
+
)
|
|
111
|
+
write_artifact(root, "inventory.json", (canonical_json(inventory) + "\n").encode())
|
|
112
|
+
return root / "inventory.json"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Offline batch telemetry import; no collectors, SDKs or providers are started."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from agentloop.interoperability.validation import ImportValidationError
|
|
10
|
+
|
|
11
|
+
telemetry_app = typer.Typer(help="Import bounded OTLP JSON/JSONL evidence offline.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@telemetry_app.command("import-jsonl")
|
|
15
|
+
def import_jsonl_command(
|
|
16
|
+
path: Path,
|
|
17
|
+
out: Path = typer.Option(
|
|
18
|
+
..., help="Output directory for qualified traces, receipts and inventory."
|
|
19
|
+
),
|
|
20
|
+
source: str = typer.Option("otel", help="Source system: otel, harbor or omnigent."),
|
|
21
|
+
strict: bool = typer.Option(False, help="Fail on an independent malformed record."),
|
|
22
|
+
single_json: bool = typer.Option(
|
|
23
|
+
False, help="Read one bounded JSON document instead of JSONL."
|
|
24
|
+
),
|
|
25
|
+
synthetic: bool = typer.Option(False, help="Explicitly mark owned synthetic fixtures."),
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Stream a completed export and preserve every invalid/conflicting record."""
|
|
28
|
+
from agentloop.interoperability.otlp_jsonl import OtlpOptions, import_otlp
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
result = import_otlp(
|
|
32
|
+
path,
|
|
33
|
+
options=OtlpOptions(
|
|
34
|
+
system=source, continue_on_error=not strict, synthetic_fixture=synthetic
|
|
35
|
+
),
|
|
36
|
+
jsonl=not single_json,
|
|
37
|
+
)
|
|
38
|
+
except (ImportValidationError, OSError) as exc:
|
|
39
|
+
message = (
|
|
40
|
+
str(exc)
|
|
41
|
+
if isinstance(exc, ImportValidationError)
|
|
42
|
+
else "Telemetry artifacts could not be read"
|
|
43
|
+
)
|
|
44
|
+
raise typer.BadParameter(message, param_hint="path") from None
|
|
45
|
+
try:
|
|
46
|
+
inventory = result.write(out)
|
|
47
|
+
except (ImportValidationError, OSError) as exc:
|
|
48
|
+
message = (
|
|
49
|
+
str(exc) if isinstance(exc, ImportValidationError) else "Output could not be written"
|
|
50
|
+
)
|
|
51
|
+
raise typer.BadParameter(message, param_hint="--out") from None
|
|
52
|
+
stats = result.inventory()
|
|
53
|
+
typer.echo(
|
|
54
|
+
f"{stats['valid_records']} records imported; {stats['invalid_records']} invalid; {stats['native_traces']} trace projections; {stats['identical_duplicate_spans']} identical duplicate spans; {stats['conflicting_span_segments']} conflicting segments."
|
|
55
|
+
)
|
|
56
|
+
typer.echo(
|
|
57
|
+
"Source spans/evaluations are externally reported. Exported telemetry alone establishes no task-correctness gate."
|
|
58
|
+
)
|
|
59
|
+
typer.echo(f"Inventory: {inventory}")
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Receipt-only cohort observations with independent acceptance conditions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from hashlib import sha256
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agentloop.integrations.harbor.trial_evidence import PAIRING_KEYS, read_trial
|
|
10
|
+
from agentloop.interoperability.validation import ImportValidationError, validate_json_tree
|
|
11
|
+
from agentloop.interventions import canonical_json
|
|
12
|
+
|
|
13
|
+
COHORT_KEY = "agentloop.external_cohort"
|
|
14
|
+
COHORT_PAIRING_KEYS = (*PAIRING_KEYS, "task_id", "verifier_config_hash")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _fail(reason: str) -> None:
|
|
18
|
+
raise ImportValidationError("invalid_cohort_evidence", COHORT_KEY, reason)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def acceptance(
|
|
22
|
+
outcome: dict,
|
|
23
|
+
*,
|
|
24
|
+
import_status: str,
|
|
25
|
+
trajectory_available: bool,
|
|
26
|
+
usage_available: bool,
|
|
27
|
+
missingness_policy: str,
|
|
28
|
+
) -> bool | None:
|
|
29
|
+
"""Keep recorded correctness separate from usable completed-run evidence."""
|
|
30
|
+
if (
|
|
31
|
+
outcome["execution_status"] in {"failed", "timed_out", "cancelled"}
|
|
32
|
+
or outcome["quality_pass"] is False
|
|
33
|
+
):
|
|
34
|
+
return False
|
|
35
|
+
complete = (
|
|
36
|
+
import_status == "imported"
|
|
37
|
+
and trajectory_available
|
|
38
|
+
and usage_available
|
|
39
|
+
and outcome["execution_status"] == "completed"
|
|
40
|
+
and outcome["verifier_status"] == "available"
|
|
41
|
+
)
|
|
42
|
+
if not complete or outcome["quality_pass"] is None:
|
|
43
|
+
return False if missingness_policy == "reject" else None
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cohort_record(
|
|
48
|
+
*,
|
|
49
|
+
source_receipt: dict | None,
|
|
50
|
+
import_status: str,
|
|
51
|
+
trajectory_available: bool,
|
|
52
|
+
usage_available: bool,
|
|
53
|
+
missingness_policy: str,
|
|
54
|
+
condition: str,
|
|
55
|
+
task_id: str | None,
|
|
56
|
+
verifier_config_hash: str | None,
|
|
57
|
+
) -> dict:
|
|
58
|
+
value = {
|
|
59
|
+
"schema_version": "1.0",
|
|
60
|
+
"source_receipt_id": source_receipt["receipt_id"] if source_receipt else None,
|
|
61
|
+
"source_receipt_sha256": sha256(canonical_json(source_receipt).encode()).hexdigest()
|
|
62
|
+
if source_receipt
|
|
63
|
+
else None,
|
|
64
|
+
"import_status": import_status,
|
|
65
|
+
"trajectory_available": trajectory_available,
|
|
66
|
+
"usage_available": usage_available,
|
|
67
|
+
"missingness_policy": missingness_policy,
|
|
68
|
+
"condition": condition,
|
|
69
|
+
"task_id": task_id,
|
|
70
|
+
"verifier_config_hash": verifier_config_hash,
|
|
71
|
+
}
|
|
72
|
+
value["sha256"] = sha256(canonical_json(value).encode()).hexdigest()
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def read_cohort(trace: Any) -> dict | None:
|
|
77
|
+
if COHORT_KEY not in trace.metadata:
|
|
78
|
+
return None
|
|
79
|
+
value = trace.metadata[COHORT_KEY]
|
|
80
|
+
validate_json_tree(value)
|
|
81
|
+
if (
|
|
82
|
+
not isinstance(value, dict)
|
|
83
|
+
or set(value)
|
|
84
|
+
!= {
|
|
85
|
+
"schema_version",
|
|
86
|
+
"source_receipt_id",
|
|
87
|
+
"source_receipt_sha256",
|
|
88
|
+
"import_status",
|
|
89
|
+
"trajectory_available",
|
|
90
|
+
"usage_available",
|
|
91
|
+
"missingness_policy",
|
|
92
|
+
"condition",
|
|
93
|
+
"task_id",
|
|
94
|
+
"verifier_config_hash",
|
|
95
|
+
"sha256",
|
|
96
|
+
}
|
|
97
|
+
or value["schema_version"] != "1.0"
|
|
98
|
+
):
|
|
99
|
+
_fail("unsupported cohort observation schema")
|
|
100
|
+
if (
|
|
101
|
+
value["sha256"]
|
|
102
|
+
!= sha256(
|
|
103
|
+
canonical_json({key: item for key, item in value.items() if key != "sha256"}).encode()
|
|
104
|
+
).hexdigest()
|
|
105
|
+
):
|
|
106
|
+
_fail("cohort record changed after capture")
|
|
107
|
+
if (
|
|
108
|
+
type(value["trajectory_available"]) is not bool
|
|
109
|
+
or type(value["usage_available"]) is not bool
|
|
110
|
+
or not isinstance(value["missingness_policy"], str)
|
|
111
|
+
or value["missingness_policy"] not in {"indeterminate", "reject"}
|
|
112
|
+
):
|
|
113
|
+
_fail("invalid source coverage/missingness flags")
|
|
114
|
+
if not isinstance(value["import_status"], str) or value["import_status"] not in {
|
|
115
|
+
"imported",
|
|
116
|
+
"invalid",
|
|
117
|
+
"missing",
|
|
118
|
+
"missing_trajectory",
|
|
119
|
+
}:
|
|
120
|
+
_fail("unsupported import status")
|
|
121
|
+
for key in ("condition", "task_id", "source_receipt_id"):
|
|
122
|
+
item = value[key]
|
|
123
|
+
if item is None and key != "condition":
|
|
124
|
+
continue
|
|
125
|
+
if not isinstance(item, str) or not item.strip() or len(item.encode()) > 512:
|
|
126
|
+
_fail("invalid cohort identity label")
|
|
127
|
+
for key in ("source_receipt_sha256", "verifier_config_hash"):
|
|
128
|
+
item = value[key]
|
|
129
|
+
if item is not None and (
|
|
130
|
+
not isinstance(item, str)
|
|
131
|
+
or len(item) != 64
|
|
132
|
+
or any(char not in "0123456789abcdef" for char in item)
|
|
133
|
+
):
|
|
134
|
+
_fail("source/configuration hash must be SHA-256 or null")
|
|
135
|
+
if (value["source_receipt_id"] is None) != (value["source_receipt_sha256"] is None):
|
|
136
|
+
_fail("source receipt identity and digest must be present together")
|
|
137
|
+
if any(
|
|
138
|
+
trace.metadata.get(key) != value[key]
|
|
139
|
+
for key in ("task_id", "verifier_config_hash", "condition")
|
|
140
|
+
):
|
|
141
|
+
_fail("source pairing labels differ from their captured record")
|
|
142
|
+
trial = read_trial(trace)
|
|
143
|
+
if trial is None:
|
|
144
|
+
_fail("cohort acceptance requires captured external trial evidence")
|
|
145
|
+
if value["source_receipt_id"] is not None and value["source_receipt_id"] != trial["receipt_id"]:
|
|
146
|
+
_fail("cohort receipt reference differs from captured trial source")
|
|
147
|
+
result = json.loads(canonical_json(value))
|
|
148
|
+
result["accepted"] = acceptance(
|
|
149
|
+
trial["outcome"],
|
|
150
|
+
import_status=value["import_status"],
|
|
151
|
+
trajectory_available=value["trajectory_available"],
|
|
152
|
+
usage_available=value["usage_available"],
|
|
153
|
+
missingness_policy=value["missingness_policy"],
|
|
154
|
+
)
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def require_cohort_pair(baseline: Any, candidate: Any) -> None:
|
|
159
|
+
left, right = read_cohort(baseline), read_cohort(candidate)
|
|
160
|
+
if left is None and right is None:
|
|
161
|
+
return
|
|
162
|
+
if left is None or right is None:
|
|
163
|
+
_fail("a cohort comparison requires two cohort observations")
|
|
164
|
+
for key in ("task_id", "verifier_config_hash"):
|
|
165
|
+
if baseline.metadata.get(key) is None or baseline.metadata.get(
|
|
166
|
+
key
|
|
167
|
+
) != candidate.metadata.get(key):
|
|
168
|
+
_fail("task/verifier identity is missing or incompatible")
|
|
169
|
+
if any(
|
|
170
|
+
item["import_status"] != "imported"
|
|
171
|
+
or not item["trajectory_available"]
|
|
172
|
+
or not item["usage_available"]
|
|
173
|
+
for item in (left, right)
|
|
174
|
+
):
|
|
175
|
+
_fail("cohort source coverage is incomplete; use the full-population study report")
|