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,775 @@
|
|
|
1
|
+
"""Bounded OTLP JSON/JSONL ingestion using AgentLoop's existing OTel parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from hashlib import sha256
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from agentloop.interoperability.artifacts import native_bytes, write_import_bundle
|
|
13
|
+
from agentloop.interoperability.contracts import (
|
|
14
|
+
IDENTITY_FIELDS,
|
|
15
|
+
ImportReceipt,
|
|
16
|
+
external_id,
|
|
17
|
+
receipt_id,
|
|
18
|
+
summarize_receipts,
|
|
19
|
+
)
|
|
20
|
+
from agentloop.interoperability.evidence import EXTERNAL_KEY
|
|
21
|
+
from agentloop.interoperability.otlp_validation import fail, flatten
|
|
22
|
+
from agentloop.interoperability.validation import (
|
|
23
|
+
ImportBudget,
|
|
24
|
+
ImportLimits,
|
|
25
|
+
ImportValidationError,
|
|
26
|
+
load_json_artifact,
|
|
27
|
+
parse_json_bytes,
|
|
28
|
+
safe_artifact_path,
|
|
29
|
+
)
|
|
30
|
+
from agentloop.interventions import canonical_json
|
|
31
|
+
from agentloop.otel import traces_from_otel
|
|
32
|
+
from agentloop.otel_semantics import INPUT_USAGE, OUTPUT_USAGE
|
|
33
|
+
from agentloop.schema import TraceValidationError
|
|
34
|
+
from agentloop.tracer import AgentTrace
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class OtlpOptions:
|
|
39
|
+
system: str = "otel"
|
|
40
|
+
producer_version: str | None = None
|
|
41
|
+
producer_revision: str | None = None
|
|
42
|
+
identity: dict[str, str] = field(default_factory=dict)
|
|
43
|
+
continue_on_error: bool = True
|
|
44
|
+
capture_content: bool = False
|
|
45
|
+
synthetic_fixture: bool = False
|
|
46
|
+
|
|
47
|
+
def __post_init__(self) -> None:
|
|
48
|
+
if not isinstance(self.system, str) or self.system not in {"otel", "harbor", "omnigent"}:
|
|
49
|
+
fail("system", "unsupported source system")
|
|
50
|
+
for key in ("continue_on_error", "capture_content", "synthetic_fixture"):
|
|
51
|
+
if type(getattr(self, key)) is not bool:
|
|
52
|
+
fail("options", "import flags must be boolean")
|
|
53
|
+
if not isinstance(self.identity, dict) or set(self.identity) - (
|
|
54
|
+
IDENTITY_FIELDS - {"trace_id", "trajectory_id", "session_id"}
|
|
55
|
+
):
|
|
56
|
+
fail("identity", "unsupported caller identity fields")
|
|
57
|
+
for value in (*self.identity.values(), self.producer_version):
|
|
58
|
+
if value is not None and (
|
|
59
|
+
not isinstance(value, str) or not value.strip() or len(value.encode()) > 512
|
|
60
|
+
):
|
|
61
|
+
fail("identity", "identities/versions must be bounded strings")
|
|
62
|
+
if self.producer_revision is not None and (
|
|
63
|
+
not isinstance(self.producer_revision, str)
|
|
64
|
+
or not re.fullmatch(r"[0-9a-f]{40}", self.producer_revision)
|
|
65
|
+
):
|
|
66
|
+
fail("producer_revision", "revision must be a full lowercase Git SHA")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass(frozen=True)
|
|
70
|
+
class OtlpImportResult:
|
|
71
|
+
traces: tuple[AgentTrace, ...]
|
|
72
|
+
source_receipts: tuple[ImportReceipt, ...]
|
|
73
|
+
_inventory_json: str
|
|
74
|
+
|
|
75
|
+
def inventory(self) -> dict:
|
|
76
|
+
return json.loads(self._inventory_json)
|
|
77
|
+
|
|
78
|
+
def write(self, out: str | Path) -> Path:
|
|
79
|
+
return write_import_bundle(self.traces, self.source_receipts, out, self.inventory())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class _BatchImporter:
|
|
83
|
+
def __init__(
|
|
84
|
+
self, root: Path, reference: str, options: OtlpOptions, limits: ImportLimits, jsonl: bool
|
|
85
|
+
):
|
|
86
|
+
self.root, self.reference, self.options, self.limits, self.jsonl = (
|
|
87
|
+
root,
|
|
88
|
+
reference,
|
|
89
|
+
options,
|
|
90
|
+
limits,
|
|
91
|
+
jsonl,
|
|
92
|
+
)
|
|
93
|
+
self.budget = ImportBudget(limits)
|
|
94
|
+
self.file_hash = sha256()
|
|
95
|
+
self.artifact_sha256 = ""
|
|
96
|
+
self.groups: dict[str, dict] = {}
|
|
97
|
+
self.errors: list[dict] = []
|
|
98
|
+
self.records: list[dict] = []
|
|
99
|
+
self.blank_lines = 0
|
|
100
|
+
self.raw_spans = self.raw_logs = self.duplicates = self.conflicts = 0
|
|
101
|
+
|
|
102
|
+
def notice(
|
|
103
|
+
self,
|
|
104
|
+
code: str,
|
|
105
|
+
line: int | None = None,
|
|
106
|
+
field: str | None = None,
|
|
107
|
+
severity: str = "warning",
|
|
108
|
+
) -> dict:
|
|
109
|
+
return {
|
|
110
|
+
"code": code,
|
|
111
|
+
"severity": severity,
|
|
112
|
+
"artifact": self.reference,
|
|
113
|
+
"field": field,
|
|
114
|
+
"record": line,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
def invalid(self, exc: ImportValidationError, line: int) -> None:
|
|
118
|
+
if not self.options.continue_on_error:
|
|
119
|
+
raise exc
|
|
120
|
+
self.errors.append(self.notice(exc.code, line, exc.field, "error"))
|
|
121
|
+
self.records.append({"record": line, "status": "invalid", "diagnostic": exc.to_dict()})
|
|
122
|
+
|
|
123
|
+
def read(self) -> None:
|
|
124
|
+
if not self.jsonl:
|
|
125
|
+
try:
|
|
126
|
+
artifact = load_json_artifact(
|
|
127
|
+
self.root, self.reference, limits=self.limits, budget=self.budget
|
|
128
|
+
)
|
|
129
|
+
self.artifact_sha256 = artifact.artifact_sha256
|
|
130
|
+
self.record(artifact.payload, 1, artifact.artifact_sha256)
|
|
131
|
+
except ImportValidationError as exc:
|
|
132
|
+
if exc.code == "limit_exceeded":
|
|
133
|
+
raise
|
|
134
|
+
self.artifact_sha256 = getattr(exc, "artifact_sha256", "")
|
|
135
|
+
if not self.artifact_sha256:
|
|
136
|
+
raise
|
|
137
|
+
self.invalid(exc, 1)
|
|
138
|
+
return
|
|
139
|
+
path = safe_artifact_path(self.root, self.reference, self.limits)
|
|
140
|
+
with path.open("rb") as stream:
|
|
141
|
+
line = 0
|
|
142
|
+
while True:
|
|
143
|
+
remaining = self.limits.max_total_bytes - self.budget.total_bytes
|
|
144
|
+
data = stream.readline(min(self.limits.max_line_bytes, remaining) + 1)
|
|
145
|
+
if not data:
|
|
146
|
+
break
|
|
147
|
+
line += 1
|
|
148
|
+
self.budget.consume(total_bytes=len(data), records=1)
|
|
149
|
+
self.file_hash.update(data)
|
|
150
|
+
line_hash = sha256(data)
|
|
151
|
+
oversized = len(data) > self.limits.max_line_bytes
|
|
152
|
+
# Drain an oversized record in bounded chunks. It remains one
|
|
153
|
+
# physical record; total byte overflow is always terminal.
|
|
154
|
+
while oversized and not data.endswith(b"\n"):
|
|
155
|
+
remaining = self.limits.max_total_bytes - self.budget.total_bytes
|
|
156
|
+
data = stream.readline(min(self.limits.max_line_bytes, remaining) + 1)
|
|
157
|
+
if not data:
|
|
158
|
+
break
|
|
159
|
+
self.budget.consume(total_bytes=len(data))
|
|
160
|
+
self.file_hash.update(data)
|
|
161
|
+
line_hash.update(data)
|
|
162
|
+
if oversized:
|
|
163
|
+
self.invalid(
|
|
164
|
+
ImportValidationError(
|
|
165
|
+
"record_limit_exceeded",
|
|
166
|
+
"line.bytes",
|
|
167
|
+
"JSONL line exceeds configured bounds",
|
|
168
|
+
),
|
|
169
|
+
line,
|
|
170
|
+
)
|
|
171
|
+
continue
|
|
172
|
+
if not data.strip():
|
|
173
|
+
self.blank_lines += 1
|
|
174
|
+
continue
|
|
175
|
+
try:
|
|
176
|
+
payload = parse_json_bytes(data, self.limits)
|
|
177
|
+
self.record(payload, line, line_hash.hexdigest())
|
|
178
|
+
except ImportValidationError as exc:
|
|
179
|
+
if exc.code == "limit_exceeded" and exc.field in {
|
|
180
|
+
"total_bytes",
|
|
181
|
+
"records",
|
|
182
|
+
"spans",
|
|
183
|
+
"trajectories",
|
|
184
|
+
"references",
|
|
185
|
+
"events",
|
|
186
|
+
}:
|
|
187
|
+
raise
|
|
188
|
+
self.invalid(exc, line)
|
|
189
|
+
if safe_artifact_path(self.root, self.reference, self.limits) != path:
|
|
190
|
+
fail("reference", "artifact path changed while reading", "unsafe_path")
|
|
191
|
+
self.artifact_sha256 = self.file_hash.hexdigest()
|
|
192
|
+
|
|
193
|
+
def record(self, payload: Any, line: int, record_hash: str) -> None:
|
|
194
|
+
try:
|
|
195
|
+
clean, spans, logs = flatten(
|
|
196
|
+
payload, self.limits, capture_content=self.options.capture_content
|
|
197
|
+
)
|
|
198
|
+
self.budget.consume(spans=len(spans) + len(logs))
|
|
199
|
+
self.budget.consume(
|
|
200
|
+
references=sum(len(item["span"].get("links", [])) for item in spans)
|
|
201
|
+
)
|
|
202
|
+
self.raw_spans += len(spans)
|
|
203
|
+
self.raw_logs += len(logs)
|
|
204
|
+
witnesses: dict[tuple[str, str], dict] = {}
|
|
205
|
+
by_group: dict[str, list[dict]] = {}
|
|
206
|
+
for index, (is_span, witness) in enumerate(
|
|
207
|
+
[(True, item) for item in spans] + [(False, item) for item in logs]
|
|
208
|
+
):
|
|
209
|
+
group = (
|
|
210
|
+
witness["trace_id"]
|
|
211
|
+
or sha256(
|
|
212
|
+
canonical_json({"record": line, "sha256": record_hash}).encode()
|
|
213
|
+
).hexdigest()[:32]
|
|
214
|
+
)
|
|
215
|
+
item = witness["span"]
|
|
216
|
+
item["traceId"] = group
|
|
217
|
+
if not witness["span_id"]:
|
|
218
|
+
item["spanId"] = sha256(
|
|
219
|
+
canonical_json(
|
|
220
|
+
{"record": line, "ordinal": index, "sha256": record_hash}
|
|
221
|
+
).encode()
|
|
222
|
+
).hexdigest()[:16]
|
|
223
|
+
else:
|
|
224
|
+
item["spanId"] = witness["span_id"]
|
|
225
|
+
witness.update(group=group, record=line, record_sha256=record_hash)
|
|
226
|
+
by_group.setdefault(group, []).append(witness)
|
|
227
|
+
if is_span:
|
|
228
|
+
key = (group, item["spanId"])
|
|
229
|
+
if (
|
|
230
|
+
key in witnesses
|
|
231
|
+
and witnesses[key]["source_sha256"] != witness["source_sha256"]
|
|
232
|
+
):
|
|
233
|
+
fail("spanId", "conflicting span identity within one record")
|
|
234
|
+
witnesses[key] = witness
|
|
235
|
+
# All span/log interpretation belongs to the existing parser. We
|
|
236
|
+
# qualify its native projection rather than building another parser.
|
|
237
|
+
projected = traces_from_otel(clean)
|
|
238
|
+
pending = []
|
|
239
|
+
for trace in projected:
|
|
240
|
+
group_ids = {event.metadata.get("otel_trace_id") for event in trace.events}
|
|
241
|
+
if not group_ids:
|
|
242
|
+
group_ids = {trace.run_id.removeprefix("run_")}
|
|
243
|
+
if len(group_ids) != 1:
|
|
244
|
+
fail("traceId", "native projection has ambiguous trace identity")
|
|
245
|
+
group = next(iter(group_ids))
|
|
246
|
+
source_witnesses = by_group.get(group, [])
|
|
247
|
+
if not source_witnesses:
|
|
248
|
+
fail("traceId", "native identity does not match source telemetry")
|
|
249
|
+
for event in trace.events:
|
|
250
|
+
witness = witnesses[(group, event.metadata["otel_span_id"])]
|
|
251
|
+
self.qualify_event(event, witness)
|
|
252
|
+
if (
|
|
253
|
+
len(canonical_json(event.metadata).encode())
|
|
254
|
+
> self.limits.max_metadata_bytes
|
|
255
|
+
):
|
|
256
|
+
fail(
|
|
257
|
+
"span.metadata",
|
|
258
|
+
"projected metadata exceeds configured bounds",
|
|
259
|
+
"record_limit_exceeded",
|
|
260
|
+
)
|
|
261
|
+
if len(trace.events) > self.limits.max_events_per_trace:
|
|
262
|
+
fail("events", "event count exceeds configured bounds", "record_limit_exceeded")
|
|
263
|
+
pending.append((group, trace, source_witnesses, witnesses))
|
|
264
|
+
new_groups = {group for group, *_ in pending} - set(self.groups)
|
|
265
|
+
self.budget.consume(trajectories=len(new_groups))
|
|
266
|
+
for group, trace, source_witnesses, witnesses in pending:
|
|
267
|
+
self.merge(group, trace, source_witnesses, witnesses)
|
|
268
|
+
self.records.append(
|
|
269
|
+
{
|
|
270
|
+
"record": line,
|
|
271
|
+
"status": "imported",
|
|
272
|
+
"sha256": record_hash,
|
|
273
|
+
"trace_groups": [item[0] for item in pending],
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
except TraceValidationError:
|
|
277
|
+
fail("record", "OTLP semantic validation failed")
|
|
278
|
+
except (OverflowError, TypeError, KeyError, AttributeError, ValueError) as exc:
|
|
279
|
+
if isinstance(exc, ImportValidationError):
|
|
280
|
+
raise
|
|
281
|
+
fail("record", "OTLP structural or semantic validation failed")
|
|
282
|
+
|
|
283
|
+
def qualify_event(self, event: Any, witness: dict) -> None:
|
|
284
|
+
attrs, span = witness["attributes"], witness["span"]
|
|
285
|
+
converter = (
|
|
286
|
+
witness["resource"].get("telemetry.sdk.name") == "harbor-atif2otel"
|
|
287
|
+
or witness["scope"].get("name") == "harbor-atif2otel"
|
|
288
|
+
)
|
|
289
|
+
event.metadata["otel_parent_span_id"] = (span.get("parentSpanId") or "").lower() or None
|
|
290
|
+
event.metadata["source_interval"] = {
|
|
291
|
+
"start_unix_nano": span.get("startTimeUnixNano"),
|
|
292
|
+
"end_unix_nano": span.get("endTimeUnixNano"),
|
|
293
|
+
"duration_ms": event.duration_ms,
|
|
294
|
+
}
|
|
295
|
+
event.metadata["unrepresented_structural_fields"] = witness["unrepresented_fields"]
|
|
296
|
+
native_identity = {
|
|
297
|
+
"event_id": attrs.get("agentloop.native_event_id"),
|
|
298
|
+
"parent_id": attrs.get("agentloop.native_parent_id"),
|
|
299
|
+
"run_id": witness["resource"].get("agentloop.run_id") or attrs.get("agentloop.run_id"),
|
|
300
|
+
}
|
|
301
|
+
if any(
|
|
302
|
+
value is not None and (not isinstance(value, str) or len(value.encode()) > 512)
|
|
303
|
+
for value in native_identity.values()
|
|
304
|
+
):
|
|
305
|
+
fail(
|
|
306
|
+
"agentloop.native_identity",
|
|
307
|
+
"native identity assertions must be bounded strings or null",
|
|
308
|
+
)
|
|
309
|
+
if any(value is not None for value in native_identity.values()):
|
|
310
|
+
event.metadata["source_native_identity"] = native_identity
|
|
311
|
+
start, end = span.get("startTimeUnixNano"), span.get("endTimeUnixNano")
|
|
312
|
+
if start is None:
|
|
313
|
+
event.started_at = "unknown"
|
|
314
|
+
if end is None:
|
|
315
|
+
event.ended_at = "unknown"
|
|
316
|
+
timing = start is not None and end is not None and not converter
|
|
317
|
+
if timing:
|
|
318
|
+
try:
|
|
319
|
+
timing = (
|
|
320
|
+
type(start) in {int, str}
|
|
321
|
+
and type(end) in {int, str}
|
|
322
|
+
and int(end) >= int(start) >= 0
|
|
323
|
+
)
|
|
324
|
+
except (ValueError, OverflowError):
|
|
325
|
+
timing = False
|
|
326
|
+
status = span.get("status", {}).get("code")
|
|
327
|
+
status_available = status in {1, 2, "STATUS_CODE_OK", "STATUS_CODE_ERROR"} and not converter
|
|
328
|
+
if event.operation_kind == "unknown" or not any(
|
|
329
|
+
isinstance(attrs.get(key), str) and bool(attrs[key].strip())
|
|
330
|
+
for key in (
|
|
331
|
+
"gen_ai.operation.name",
|
|
332
|
+
"openinference.span.kind",
|
|
333
|
+
"mcp.method.name",
|
|
334
|
+
"agentloop.event_type",
|
|
335
|
+
)
|
|
336
|
+
):
|
|
337
|
+
event.event_type = "source_span"
|
|
338
|
+
event.metadata["operation_kind"] = "unknown"
|
|
339
|
+
if not timing:
|
|
340
|
+
event.duration_ms = 0.0
|
|
341
|
+
input_available = any(key in attrs for key in ("agentloop.input_tokens", *INPUT_USAGE))
|
|
342
|
+
output_available = any(key in attrs for key in ("agentloop.output_tokens", *OUTPUT_USAGE))
|
|
343
|
+
event.metadata.update(
|
|
344
|
+
external_evidence_schema="1.0",
|
|
345
|
+
timing_available=bool(timing),
|
|
346
|
+
timing_provenance="inferred"
|
|
347
|
+
if converter
|
|
348
|
+
else "external_reported"
|
|
349
|
+
if timing
|
|
350
|
+
else "unknown",
|
|
351
|
+
source_status_available=status_available,
|
|
352
|
+
source_execution_status="failed"
|
|
353
|
+
if status in {2, "STATUS_CODE_ERROR"} and not converter
|
|
354
|
+
else "completed"
|
|
355
|
+
if status_available
|
|
356
|
+
else "unknown",
|
|
357
|
+
input_tokens_available=input_available,
|
|
358
|
+
output_tokens_available=output_available,
|
|
359
|
+
source_token_provenance=event.token_provenance,
|
|
360
|
+
source_span_sha256=witness["source_sha256"],
|
|
361
|
+
source_span_id=witness["span_id"],
|
|
362
|
+
source_trace_id=witness["trace_id"],
|
|
363
|
+
source_record=witness["record"],
|
|
364
|
+
converter_generated=converter,
|
|
365
|
+
)
|
|
366
|
+
if event.event_type == "model_call":
|
|
367
|
+
multiplicity = attrs.get("metadata.aggregated_llm_calls", 1)
|
|
368
|
+
if type(multiplicity) is not int or multiplicity < 1:
|
|
369
|
+
fail(
|
|
370
|
+
"metadata.aggregated_llm_calls",
|
|
371
|
+
"model block multiplicity must be a positive integer",
|
|
372
|
+
)
|
|
373
|
+
event.metadata["source_model_call_count"] = multiplicity
|
|
374
|
+
event.token_provenance = (
|
|
375
|
+
"external_reported" if input_available and output_available else "unavailable"
|
|
376
|
+
)
|
|
377
|
+
cache = event.metadata.get("cached_input_tokens")
|
|
378
|
+
if cache is not None and input_available and cache > event.input_tokens:
|
|
379
|
+
fail("usage", "cache usage must be a prompt subset")
|
|
380
|
+
|
|
381
|
+
def merge(
|
|
382
|
+
self, group: str, trace: AgentTrace, witnesses: list[dict], span_witnesses: dict
|
|
383
|
+
) -> None:
|
|
384
|
+
state = self.groups.setdefault(
|
|
385
|
+
group,
|
|
386
|
+
{
|
|
387
|
+
"events": {},
|
|
388
|
+
"trace_id": witnesses[0]["trace_id"],
|
|
389
|
+
"records": [],
|
|
390
|
+
"notices": [],
|
|
391
|
+
"logs": [],
|
|
392
|
+
"sessions": set(),
|
|
393
|
+
"bounds": [],
|
|
394
|
+
"conflicts": 0,
|
|
395
|
+
"source_aggregates": [],
|
|
396
|
+
},
|
|
397
|
+
)
|
|
398
|
+
state["records"].append(witnesses[0]["record"])
|
|
399
|
+
for event in trace.events:
|
|
400
|
+
state["logs"].extend(event.metadata.get("otel_log_records", []))
|
|
401
|
+
for witness in witnesses:
|
|
402
|
+
for source_attrs in (witness["attributes"], witness["resource"]):
|
|
403
|
+
session = source_attrs.get("session.id") or source_attrs.get(
|
|
404
|
+
"gen_ai.conversation.id"
|
|
405
|
+
)
|
|
406
|
+
if isinstance(session, str) and session.strip() and len(session.encode()) <= 512:
|
|
407
|
+
state["sessions"].add(session)
|
|
408
|
+
for event in trace.events:
|
|
409
|
+
witness = span_witnesses[(group, event.metadata["otel_span_id"])]
|
|
410
|
+
span_id = event.metadata["otel_span_id"]
|
|
411
|
+
existing = state["events"].get(span_id)
|
|
412
|
+
if existing is not None:
|
|
413
|
+
if existing.metadata["source_span_sha256"] == event.metadata["source_span_sha256"]:
|
|
414
|
+
self.duplicates += 1
|
|
415
|
+
state["notices"].append(
|
|
416
|
+
self.notice("identical_span_duplicate", witness["record"], "spanId", "info")
|
|
417
|
+
)
|
|
418
|
+
else:
|
|
419
|
+
self.conflicts += 1
|
|
420
|
+
state["conflicts"] += 1
|
|
421
|
+
state["notices"].append(
|
|
422
|
+
self.notice(
|
|
423
|
+
"conflicting_span_segment", witness["record"], "spanId", "error"
|
|
424
|
+
)
|
|
425
|
+
)
|
|
426
|
+
continue
|
|
427
|
+
if len(state["events"]) >= self.limits.max_events_per_trace:
|
|
428
|
+
fail("events", "merged trace exceeds configured bounds", "limit_exceeded")
|
|
429
|
+
event.metadata.pop("agentloop.external", None)
|
|
430
|
+
state["events"][span_id] = event
|
|
431
|
+
if event.metadata["unrepresented_structural_fields"]:
|
|
432
|
+
state["notices"].append(
|
|
433
|
+
self.notice("unrepresented_structural_fields", witness["record"], "record")
|
|
434
|
+
)
|
|
435
|
+
if event.operation_kind != "model" and any(
|
|
436
|
+
key in witness["attributes"]
|
|
437
|
+
for key in (*INPUT_USAGE, *OUTPUT_USAGE, "llm.cost.total")
|
|
438
|
+
):
|
|
439
|
+
state["source_aggregates"].append(
|
|
440
|
+
{
|
|
441
|
+
"span_id": span_id,
|
|
442
|
+
"input_tokens": event.input_tokens
|
|
443
|
+
if event.metadata["input_tokens_available"]
|
|
444
|
+
else None,
|
|
445
|
+
"output_tokens": event.output_tokens
|
|
446
|
+
if event.metadata["output_tokens_available"]
|
|
447
|
+
else None,
|
|
448
|
+
"cost_usd": event.metadata.get("provider_reported_cost_usd"),
|
|
449
|
+
"basis": "external_reported_nonmodel_aggregate",
|
|
450
|
+
}
|
|
451
|
+
)
|
|
452
|
+
if event.metadata["timing_available"]:
|
|
453
|
+
state["bounds"].append(
|
|
454
|
+
(
|
|
455
|
+
int(witness["span"]["startTimeUnixNano"]),
|
|
456
|
+
int(witness["span"]["endTimeUnixNano"]),
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
if event.metadata["converter_generated"]:
|
|
460
|
+
state["notices"].append(
|
|
461
|
+
self.notice("converter_inferred_timing_status", witness["record"], "timing")
|
|
462
|
+
)
|
|
463
|
+
log_records = trace.metadata.get("otel_log_records", [])
|
|
464
|
+
state["logs"].extend(log_records)
|
|
465
|
+
|
|
466
|
+
def receipt(
|
|
467
|
+
self,
|
|
468
|
+
identity: dict,
|
|
469
|
+
*,
|
|
470
|
+
reason: str | None,
|
|
471
|
+
notices: list[dict],
|
|
472
|
+
metadata: dict,
|
|
473
|
+
traces: list[dict] | None = None,
|
|
474
|
+
completeness: dict | None = None,
|
|
475
|
+
execution: str = "unknown",
|
|
476
|
+
) -> dict:
|
|
477
|
+
return {
|
|
478
|
+
"schema_version": "1.0",
|
|
479
|
+
"receipt_id": "pending",
|
|
480
|
+
"source": {
|
|
481
|
+
"system": self.options.system,
|
|
482
|
+
"producer_version": self.options.producer_version,
|
|
483
|
+
"producer_revision": self.options.producer_revision,
|
|
484
|
+
"format": "otlp_jsonl" if self.jsonl else "otlp_json",
|
|
485
|
+
"format_version": None,
|
|
486
|
+
"artifact_reference": self.reference,
|
|
487
|
+
"artifact_sha256": self.artifact_sha256,
|
|
488
|
+
"trust": "external_reported",
|
|
489
|
+
},
|
|
490
|
+
"external_identity": identity,
|
|
491
|
+
"identity_provenance": {
|
|
492
|
+
key: "calculated"
|
|
493
|
+
if key == "trajectory_id" and value is not None
|
|
494
|
+
else "external_reported"
|
|
495
|
+
if value is not None
|
|
496
|
+
else "unknown"
|
|
497
|
+
for key, value in identity.items()
|
|
498
|
+
},
|
|
499
|
+
"traces": traces or [],
|
|
500
|
+
"missing_trace_reason": reason,
|
|
501
|
+
"outcome": {
|
|
502
|
+
"execution_status": execution,
|
|
503
|
+
"verifier_status": "unknown",
|
|
504
|
+
"verifier_dimensions": {},
|
|
505
|
+
"quality_pass": None, # nosec B105
|
|
506
|
+
"quality_basis": "unknown",
|
|
507
|
+
"scoring_contract": None,
|
|
508
|
+
"verifier_isolation": "unknown",
|
|
509
|
+
},
|
|
510
|
+
"completeness": completeness
|
|
511
|
+
or dict.fromkeys(
|
|
512
|
+
("trajectory", "usage", "quality", "parentage", "timing", "cost"), "unknown"
|
|
513
|
+
),
|
|
514
|
+
"relationships": [],
|
|
515
|
+
"notices": notices,
|
|
516
|
+
"source_metadata": metadata,
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
def finish(self) -> OtlpImportResult:
|
|
520
|
+
traces, receipts = [], []
|
|
521
|
+
for group, state in self.groups.items():
|
|
522
|
+
identity = {
|
|
523
|
+
**dict.fromkeys(sorted(IDENTITY_FIELDS)),
|
|
524
|
+
**self.options.identity,
|
|
525
|
+
"trace_id": state["trace_id"],
|
|
526
|
+
}
|
|
527
|
+
if state["trace_id"] is None:
|
|
528
|
+
identity["trajectory_id"] = external_id(
|
|
529
|
+
"group",
|
|
530
|
+
self.options.system,
|
|
531
|
+
{
|
|
532
|
+
"artifact_reference": self.reference,
|
|
533
|
+
"artifact_sha256": self.artifact_sha256,
|
|
534
|
+
"source_record": state["records"][0],
|
|
535
|
+
},
|
|
536
|
+
)
|
|
537
|
+
state["notices"].append(
|
|
538
|
+
self.notice("missing_trace_identity", state["records"][0], "traceId")
|
|
539
|
+
)
|
|
540
|
+
if len(state["sessions"]) == 1:
|
|
541
|
+
identity["session_id"] = next(iter(state["sessions"]))
|
|
542
|
+
elif len(state["sessions"]) > 1:
|
|
543
|
+
state["notices"].append(self.notice("multiple_trace_sessions", field="session.id"))
|
|
544
|
+
events = list(state["events"].values())
|
|
545
|
+
cyclic = _cyclic_parents(state["events"])
|
|
546
|
+
models = [event for event in events if event.event_type == "model_call"]
|
|
547
|
+
timing = (
|
|
548
|
+
bool(events)
|
|
549
|
+
and state["trace_id"] is not None
|
|
550
|
+
and all(event.metadata["timing_available"] for event in events)
|
|
551
|
+
and not state["conflicts"]
|
|
552
|
+
and not cyclic
|
|
553
|
+
)
|
|
554
|
+
usage = (
|
|
555
|
+
bool(models)
|
|
556
|
+
and all(
|
|
557
|
+
event.metadata["input_tokens_available"]
|
|
558
|
+
and event.metadata["output_tokens_available"]
|
|
559
|
+
for event in models
|
|
560
|
+
)
|
|
561
|
+
and not state["conflicts"]
|
|
562
|
+
)
|
|
563
|
+
runtime = (
|
|
564
|
+
(
|
|
565
|
+
max(end for _, end in state["bounds"])
|
|
566
|
+
- min(start for start, _ in state["bounds"])
|
|
567
|
+
)
|
|
568
|
+
/ 1_000_000
|
|
569
|
+
if timing
|
|
570
|
+
else None
|
|
571
|
+
)
|
|
572
|
+
execution = (
|
|
573
|
+
"failed"
|
|
574
|
+
if any(
|
|
575
|
+
event.status == "error" and event.metadata["source_status_available"]
|
|
576
|
+
for event in events
|
|
577
|
+
)
|
|
578
|
+
else "unknown"
|
|
579
|
+
)
|
|
580
|
+
metadata = {
|
|
581
|
+
"records": sorted(set(state["records"])),
|
|
582
|
+
"raw_unique_spans": len(events),
|
|
583
|
+
"conflicting_segments": state["conflicts"],
|
|
584
|
+
"source_aggregates": state["source_aggregates"],
|
|
585
|
+
"source_logs": state["logs"],
|
|
586
|
+
"coverage_basis": "exported telemetry; source execution/usage completeness not established",
|
|
587
|
+
"atif_losses": [
|
|
588
|
+
"original interaction/context history",
|
|
589
|
+
"source timestamps vs generated intervals",
|
|
590
|
+
"source execution outcome",
|
|
591
|
+
"unconverted or unresolved documents",
|
|
592
|
+
]
|
|
593
|
+
if self.options.system == "harbor"
|
|
594
|
+
or any(event.metadata["converter_generated"] for event in events)
|
|
595
|
+
else [],
|
|
596
|
+
}
|
|
597
|
+
receipt = self.receipt(
|
|
598
|
+
identity,
|
|
599
|
+
reason="no_execution_spans" if not events else None,
|
|
600
|
+
notices=state["notices"],
|
|
601
|
+
metadata=metadata,
|
|
602
|
+
completeness={
|
|
603
|
+
"trajectory": "partial" if events else "missing",
|
|
604
|
+
"usage": "complete" if usage else "partial" if models else "unknown",
|
|
605
|
+
"quality": "unknown",
|
|
606
|
+
"parentage": "unknown",
|
|
607
|
+
"timing": "complete" if timing else "missing",
|
|
608
|
+
"cost": "unknown",
|
|
609
|
+
},
|
|
610
|
+
execution=execution,
|
|
611
|
+
)
|
|
612
|
+
receipt["receipt_id"] = receipt_id(receipt)
|
|
613
|
+
if events:
|
|
614
|
+
run_id = external_id(
|
|
615
|
+
"run", self.options.system, {**identity, "source_group": group}
|
|
616
|
+
)
|
|
617
|
+
event_ids = {
|
|
618
|
+
span_id: external_id(
|
|
619
|
+
"event", self.options.system, {"run_id": run_id, "span_id": span_id}
|
|
620
|
+
)
|
|
621
|
+
for span_id in state["events"]
|
|
622
|
+
}
|
|
623
|
+
trace = AgentTrace(
|
|
624
|
+
name="external_otel_trace",
|
|
625
|
+
run_id=run_id,
|
|
626
|
+
started_at=events[0].started_at,
|
|
627
|
+
elapsed_ms=runtime,
|
|
628
|
+
metadata={
|
|
629
|
+
"source": self.options.system,
|
|
630
|
+
**({"synthetic": True} if self.options.synthetic_fixture else {}),
|
|
631
|
+
},
|
|
632
|
+
)
|
|
633
|
+
for event in events:
|
|
634
|
+
parent_span = event.metadata.get("otel_parent_span_id")
|
|
635
|
+
event.run_id, event.event_id = run_id, event_ids[event.metadata["otel_span_id"]]
|
|
636
|
+
source_parent = state["events"].get(parent_span)
|
|
637
|
+
event.parent_id = (
|
|
638
|
+
event_ids.get(parent_span)
|
|
639
|
+
if state["trace_id"] is not None
|
|
640
|
+
and source_parent is not None
|
|
641
|
+
and source_parent.metadata["source_span_id"] is not None
|
|
642
|
+
else None
|
|
643
|
+
)
|
|
644
|
+
if event.metadata["source_span_id"] is None:
|
|
645
|
+
receipt["notices"].append(
|
|
646
|
+
self.notice(
|
|
647
|
+
"missing_span_identity", event.metadata["source_record"], "spanId"
|
|
648
|
+
)
|
|
649
|
+
)
|
|
650
|
+
if event.metadata["otel_span_id"] in cyclic:
|
|
651
|
+
event.parent_id = None
|
|
652
|
+
event.metadata["unresolved_parent_span_id"] = parent_span
|
|
653
|
+
receipt["notices"].append(
|
|
654
|
+
self.notice(
|
|
655
|
+
"cyclic_parentage",
|
|
656
|
+
event.metadata["source_record"],
|
|
657
|
+
"parentSpanId",
|
|
658
|
+
"error",
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
if parent_span is not None and event.parent_id is None:
|
|
662
|
+
event.metadata["unresolved_parent_span_id"] = parent_span
|
|
663
|
+
receipt["notices"].append(
|
|
664
|
+
self.notice(
|
|
665
|
+
"unresolved_parent_span",
|
|
666
|
+
event.metadata["source_record"],
|
|
667
|
+
"parentSpanId",
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
event.metadata["source_receipt_id"] = receipt["receipt_id"]
|
|
671
|
+
trace.events = events
|
|
672
|
+
trace.metadata[EXTERNAL_KEY] = {
|
|
673
|
+
"schema_version": "1.0",
|
|
674
|
+
"source": {
|
|
675
|
+
"system": self.options.system,
|
|
676
|
+
"format": receipt["source"]["format"],
|
|
677
|
+
},
|
|
678
|
+
"receipt_id": receipt["receipt_id"],
|
|
679
|
+
"runtime_ms": runtime,
|
|
680
|
+
"event_timing_complete": timing,
|
|
681
|
+
"execution_status": execution,
|
|
682
|
+
"usage_complete": usage,
|
|
683
|
+
"reported_model_call_count": sum(
|
|
684
|
+
event.metadata["source_model_call_count"] for event in models
|
|
685
|
+
)
|
|
686
|
+
if models
|
|
687
|
+
else None,
|
|
688
|
+
"comparison_eligible": False,
|
|
689
|
+
}
|
|
690
|
+
receipt["traces"] = [
|
|
691
|
+
{
|
|
692
|
+
"run_id": run_id,
|
|
693
|
+
"trace_file": f"traces/{run_id}.json",
|
|
694
|
+
"trace_sha256": sha256(native_bytes(trace)).hexdigest(),
|
|
695
|
+
}
|
|
696
|
+
]
|
|
697
|
+
traces.append(trace)
|
|
698
|
+
receipts.append(ImportReceipt.from_dict(receipt, limits=self.limits))
|
|
699
|
+
if self.errors or not self.groups:
|
|
700
|
+
identity = {**dict.fromkeys(sorted(IDENTITY_FIELDS)), **self.options.identity}
|
|
701
|
+
receipt = self.receipt(
|
|
702
|
+
identity,
|
|
703
|
+
reason="invalid_artifact" if self.errors else "no_execution_spans",
|
|
704
|
+
notices=self.errors,
|
|
705
|
+
metadata={
|
|
706
|
+
"invalid_record_count": len(self.errors),
|
|
707
|
+
"blank_line_count": self.blank_lines,
|
|
708
|
+
},
|
|
709
|
+
)
|
|
710
|
+
receipt["receipt_id"] = receipt_id(receipt)
|
|
711
|
+
receipts.append(ImportReceipt.from_dict(receipt, limits=self.limits))
|
|
712
|
+
inventory = {
|
|
713
|
+
"schema_version": "1.0",
|
|
714
|
+
"source_system": self.options.system,
|
|
715
|
+
"artifact_reference": self.reference,
|
|
716
|
+
"artifact_sha256": self.artifact_sha256,
|
|
717
|
+
"source_format": "otlp_jsonl" if self.jsonl else "otlp_json",
|
|
718
|
+
"physical_records": self.budget.records,
|
|
719
|
+
"blank_lines": self.blank_lines,
|
|
720
|
+
"valid_records": sum(row["status"] == "imported" for row in self.records),
|
|
721
|
+
"invalid_records": len(self.errors),
|
|
722
|
+
"raw_spans": self.raw_spans,
|
|
723
|
+
"raw_log_records": self.raw_logs,
|
|
724
|
+
"identical_duplicate_spans": self.duplicates,
|
|
725
|
+
"conflicting_span_segments": self.conflicts,
|
|
726
|
+
"trace_groups": len(self.groups),
|
|
727
|
+
"record_rows": self.records,
|
|
728
|
+
**summarize_receipts(receipts),
|
|
729
|
+
}
|
|
730
|
+
return OtlpImportResult(tuple(traces), tuple(receipts), canonical_json(inventory))
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def _cyclic_parents(events: dict[str, Any]) -> set[str]:
|
|
734
|
+
"""Find cycles in a source parent forest without recursion or invented edges."""
|
|
735
|
+
parents = {key: event.metadata.get("otel_parent_span_id") for key, event in events.items()}
|
|
736
|
+
done, cyclic = set(), set()
|
|
737
|
+
for start in parents:
|
|
738
|
+
current, path, positions = start, [], {}
|
|
739
|
+
while current in parents and current not in done:
|
|
740
|
+
if current in positions:
|
|
741
|
+
cyclic.update(path[positions[current] :])
|
|
742
|
+
break
|
|
743
|
+
positions[current] = len(path)
|
|
744
|
+
path.append(current)
|
|
745
|
+
current = parents[current]
|
|
746
|
+
done.update(path)
|
|
747
|
+
return cyclic
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def import_otlp(
|
|
751
|
+
path: str | Path,
|
|
752
|
+
*,
|
|
753
|
+
root: str | Path | None = None,
|
|
754
|
+
options: OtlpOptions = OtlpOptions(),
|
|
755
|
+
limits: ImportLimits = ImportLimits(),
|
|
756
|
+
jsonl: bool | None = None,
|
|
757
|
+
) -> OtlpImportResult:
|
|
758
|
+
"""Import a bounded file offline; malformed independent records stay visible."""
|
|
759
|
+
if jsonl is not None and type(jsonl) is not bool:
|
|
760
|
+
fail("jsonl", "format selection must be boolean or null")
|
|
761
|
+
selected = Path(path).absolute()
|
|
762
|
+
source_root = Path(root).absolute() if root is not None else selected.parent
|
|
763
|
+
try:
|
|
764
|
+
reference = selected.relative_to(source_root).as_posix()
|
|
765
|
+
except ValueError:
|
|
766
|
+
fail("path", "input lies outside the selected source root", "unsafe_path")
|
|
767
|
+
importer = _BatchImporter(
|
|
768
|
+
source_root.resolve(),
|
|
769
|
+
reference,
|
|
770
|
+
options,
|
|
771
|
+
limits,
|
|
772
|
+
selected.suffix.lower() == ".jsonl" if jsonl is None else jsonl,
|
|
773
|
+
)
|
|
774
|
+
importer.read()
|
|
775
|
+
return importer.finish()
|