agentloop-profiler 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,422 @@
1
+ """Omnigent source aliases, session correlation and qualified policy evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from collections import Counter
7
+ from dataclasses import dataclass
8
+ from hashlib import sha256
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from agentloop.integrations.omnigent.evidence import attach_observations
13
+ from agentloop.interoperability.artifacts import native_bytes, write_artifact, write_import_bundle
14
+ from agentloop.interoperability.contracts import ImportReceipt, external_id
15
+ from agentloop.interoperability.evidence import EXTERNAL_KEY
16
+ from agentloop.interoperability.otlp_jsonl import OtlpOptions, import_otlp
17
+ from agentloop.interoperability.validation import (
18
+ ImportLimits,
19
+ ImportValidationError,
20
+ load_json_artifact,
21
+ )
22
+ from agentloop.interventions import canonical_json
23
+ from agentloop.tracer import AgentTrace
24
+
25
+ CONTRACT_REVISION = "a2956be0e97bb175a60b274053d836f07d494c6c"
26
+ _ALIASES = {
27
+ "agent_name": ("gen_ai.agent.name", "agent.name"),
28
+ "agent_id": ("omnigent.agent.id", "agent.id"),
29
+ "agent_role": ("omnigent.agent.role", "agent.role"),
30
+ "parent_agent_id": ("omnigent.parent_agent.id", "parent.agent.id"),
31
+ "parent_agent_name": ("parent.agent.name",),
32
+ "session_id": ("session.id", "gen_ai.conversation.id"),
33
+ "parent_session_id": ("omnigent.parent_session.id", "parent.session.id"),
34
+ "request_id": ("omnigent.request.id", "request.id"),
35
+ "turn_id": ("omnigent.turn.id", "turn.id"),
36
+ "harness": ("omnigent.harness", "harness.name", "harness.type", "runtime.harness"),
37
+ "harness_version": ("omnigent.harness.version", "harness.version", "runtime.version"),
38
+ "transport": ("omnigent.integration_mode",),
39
+ "skill_name": ("omnigent.skill.name",),
40
+ }
41
+
42
+
43
+ def _label(value: Any) -> str | None:
44
+ return (
45
+ value if isinstance(value, str) and value.strip() and len(value.encode()) <= 512 else None
46
+ )
47
+
48
+
49
+ def _attributes(event: Any) -> dict:
50
+ return {**event.metadata.get("otel_resource_attributes", {}), **event.metadata}
51
+
52
+
53
+ def _aliases(event: Any) -> dict:
54
+ attrs = _attributes(event)
55
+ result = {}
56
+ for key, names in _ALIASES.items():
57
+ labels = {value for name in names if (value := _label(attrs.get(name))) is not None}
58
+ result[key] = next(iter(labels)) if len(labels) == 1 else None
59
+ return result
60
+
61
+
62
+ def _timings(events: list[Any], *, conflicting: bool = False) -> dict:
63
+ result = {}
64
+ for role in ("agent", "model", "tool", "guardrail", "approval", "host", "unknown"):
65
+ selected = [event for event in events if _role(event) == role]
66
+ intervals = sorted(
67
+ (
68
+ int(event.metadata["source_interval"]["start_unix_nano"]),
69
+ int(event.metadata["source_interval"]["end_unix_nano"]),
70
+ )
71
+ for event in selected
72
+ if event.metadata.get("timing_available") is True
73
+ )
74
+ union = 0
75
+ end = None
76
+ for a, b in intervals:
77
+ union += b - a if end is None or a >= end else max(0, b - end)
78
+ end = b if end is None else max(end, b)
79
+ complete = bool(selected) and len(intervals) == len(selected) and not conflicting
80
+ result[role] = {
81
+ "spans": len(selected),
82
+ "timed_spans": len(intervals),
83
+ "interval_union_ms": union / 1_000_000 if complete else None,
84
+ "known_interval_union_ms": union / 1_000_000 if intervals else None,
85
+ "span_cumulative_ms": sum(b - a for a, b in intervals) / 1_000_000
86
+ if complete
87
+ else None,
88
+ "coverage": "complete_for_recorded_spans"
89
+ if complete
90
+ else "partial"
91
+ if intervals
92
+ else "unknown",
93
+ }
94
+ return result
95
+
96
+
97
+ def _role(event: Any) -> str:
98
+ attrs = _attributes(event)
99
+ declared = attrs.get("omnigent.span.role")
100
+ if isinstance(declared, str) and declared in {"approval", "host"}:
101
+ return declared
102
+ return (
103
+ event.operation_kind
104
+ if event.operation_kind in {"agent", "model", "tool", "guardrail"}
105
+ else "unknown"
106
+ )
107
+
108
+
109
+ def _policy(event: Any, tools: list[Any]) -> dict:
110
+ attrs = _attributes(event)
111
+ action = _label(attrs.get("policy.action"))
112
+ canonical = (
113
+ action.upper()
114
+ if action is not None and action.upper() in {"ALLOW", "DENY", "ASK"}
115
+ else "UNKNOWN"
116
+ )
117
+ call_id = _label(attrs.get("policy.tool_call_id")) or _label(attrs.get("tool_call_id"))
118
+ matched = [
119
+ tool
120
+ for tool in tools
121
+ if call_id is not None and _label(_attributes(tool).get("tool_call_id")) == call_id
122
+ ]
123
+ phase = _label(attrs.get("policy.phase"))
124
+ return {
125
+ "event_id": event.event_id,
126
+ "source_span_id": event.metadata.get("source_span_id"),
127
+ "name": _label(attrs.get("policy.name")),
128
+ "phase": phase,
129
+ "action": canonical,
130
+ "source_action": action,
131
+ "tool_call_id": call_id,
132
+ "observed_tool_event_ids": [tool.event_id for tool in matched],
133
+ "dispatch_observation": "recorded_tool_span" if matched else "unknown",
134
+ "dispatch_prevention": "unknown",
135
+ "enforcement": "unverified",
136
+ "source_enforcement_claim": attrs.get("policy.enforced")
137
+ if type(attrs.get("policy.enforced")) is bool
138
+ else None,
139
+ "approval_id": _label(attrs.get("approval.id")),
140
+ "approval_resolution": _label(attrs.get("approval.resolution")),
141
+ "latency_ms": event.duration_ms if event.metadata.get("timing_available") is True else None,
142
+ "basis": "external_reported_policy_decision",
143
+ }
144
+
145
+
146
+ @dataclass(frozen=True)
147
+ class OmnigentImportResult:
148
+ traces: tuple[AgentTrace, ...]
149
+ source_receipts: tuple[ImportReceipt, ...]
150
+ _inventory_json: str
151
+ _sessions_json: str
152
+
153
+ def inventory(self) -> dict:
154
+ return json.loads(self._inventory_json)
155
+
156
+ def sessions(self) -> dict:
157
+ return json.loads(self._sessions_json)
158
+
159
+ def write(self, out: str | Path) -> Path:
160
+ inventory = write_import_bundle(self.traces, self.source_receipts, out, self.inventory())
161
+ write_artifact(
162
+ inventory.parent, "omnigent-sessions.json", (self._sessions_json + "\n").encode()
163
+ )
164
+ from agentloop.interoperability.coordination import write_coordination_report
165
+
166
+ write_coordination_report(self.traces, self.source_receipts, inventory.parent)
167
+ return inventory
168
+
169
+
170
+ def import_omnigent(
171
+ path: str | Path,
172
+ *,
173
+ options: OtlpOptions | None = None,
174
+ limits: ImportLimits = ImportLimits(),
175
+ jsonl: bool | None = None,
176
+ ) -> OmnigentImportResult:
177
+ """Read source files only; no runtime, vendor tool or policy executes."""
178
+ options = options or OtlpOptions(system="omnigent")
179
+ if options.system != "omnigent":
180
+ raise ImportValidationError(
181
+ "invalid_source", "system", "Omnigent adaptation requires the omnigent source system"
182
+ )
183
+ imported = import_otlp(path, options=options, limits=limits, jsonl=jsonl)
184
+ receipt_by_run = {
185
+ item["run_id"]: receipt.to_dict()
186
+ for receipt in imported.source_receipts
187
+ for item in receipt.to_dict()["traces"]
188
+ }
189
+ span_index: dict[tuple[str, str], list[dict]] = {}
190
+ for trace in imported.traces:
191
+ for event in trace.events:
192
+ if (
193
+ event.metadata.get("source_trace_id") is not None
194
+ and event.metadata.get("source_span_id") is not None
195
+ ):
196
+ key = (event.metadata["source_trace_id"], event.metadata["source_span_id"])
197
+ span_index.setdefault(key, []).append(
198
+ {"run_id": trace.run_id, "event_id": event.event_id}
199
+ )
200
+ groups: dict[str, dict] = {}
201
+ frozen = []
202
+ for trace in imported.traces:
203
+ receipt = receipt_by_run[trace.run_id]
204
+ tools = [event for event in trace.events if _role(event) == "tool"]
205
+ observations = []
206
+ event_sessions = {}
207
+ policies = []
208
+ relations = []
209
+ for event in trace.events:
210
+ aliases = _aliases(event)
211
+ ambiguous_aliases = [
212
+ key
213
+ for key, names in _ALIASES.items()
214
+ if len(
215
+ {
216
+ _label(_attributes(event).get(name))
217
+ for name in names
218
+ if _label(_attributes(event).get(name)) is not None
219
+ }
220
+ )
221
+ > 1
222
+ ]
223
+ event.metadata["source.omnigent"] = {
224
+ "schema_version": "1.0",
225
+ **aliases,
226
+ "observation_role": _role(event),
227
+ "ambiguous_aliases": ambiguous_aliases,
228
+ "capture_flags": {
229
+ key: value
230
+ for key, value in _attributes(event).items()
231
+ if key
232
+ in {
233
+ "content_capture_enabled",
234
+ "omnigent.capture_content",
235
+ "omnigent.tracing.content_capture",
236
+ }
237
+ and type(value) is bool
238
+ },
239
+ }
240
+ observations.append(
241
+ {
242
+ "event_id": event.event_id,
243
+ "source_span_id": event.metadata.get("source_span_id"),
244
+ **aliases,
245
+ "role": _role(event),
246
+ }
247
+ )
248
+ event_sessions[event.event_id] = aliases["session_id"]
249
+ if _role(event) == "guardrail" or "policy.action" in event.metadata:
250
+ policies.append(_policy(event, tools))
251
+ for link in event.metadata.get("otel_links", []):
252
+ key = (link["traceId"].lower(), link["spanId"].lower())
253
+ targets = span_index.get(key, [])
254
+ relations.append(
255
+ {
256
+ "kind": "span_link",
257
+ "source_event_id": event.event_id,
258
+ "target_trace_id": key[0],
259
+ "target_span_id": key[1],
260
+ "targets": targets,
261
+ "resolved": len(targets) == 1,
262
+ "causal_edge": False,
263
+ "basis": "external_reported_link",
264
+ }
265
+ )
266
+ parent_reference = _label(event.metadata.get("parent_span_id"))
267
+ if parent_reference is not None:
268
+ relations.append(
269
+ {
270
+ "kind": "ended_parent_reference",
271
+ "source_event_id": event.event_id,
272
+ "target_trace_id": None,
273
+ "target_span_id": parent_reference,
274
+ "targets": [],
275
+ "resolved": False,
276
+ "causal_edge": False,
277
+ "basis": "external_reported_attribute_without_trace_scope",
278
+ }
279
+ )
280
+ model_count = sum(_role(event) == "model" for event in trace.events)
281
+ summary = {
282
+ "schema_version": "1.0",
283
+ "adapter_contract_revision": CONTRACT_REVISION,
284
+ "source_receipt_id": receipt["receipt_id"],
285
+ "source_identity": receipt["external_identity"],
286
+ "observation_count": len(observations),
287
+ "agent_names": sorted(
288
+ {item["agent_name"] for item in observations if item["agent_name"] is not None}
289
+ ),
290
+ "policy_decisions": policies,
291
+ "relationships": relations,
292
+ "timing_by_role": _timings(
293
+ trace.events,
294
+ conflicting=receipt["source_metadata"].get("conflicting_segments", 0) > 0,
295
+ ),
296
+ "model_coverage": "observed_partial" if model_count else "unknown",
297
+ "task_correctness": "unavailable",
298
+ "enforcement": "unverified",
299
+ "source_execution_outcome": "unknown",
300
+ "gaps": sorted(
301
+ {
302
+ "complete_executor_model_coverage_unestablished",
303
+ "external_task_verifier_absent",
304
+ *(
305
+ ["unresolved_policy_ask"]
306
+ if any(
307
+ item["action"] == "ASK" and item["approval_resolution"] is None
308
+ for item in policies
309
+ )
310
+ else []
311
+ ),
312
+ *(
313
+ ["cross_process_parentage_incomplete"]
314
+ if any(not item["resolved"] for item in relations)
315
+ or any(
316
+ "unresolved_parent_span_id" in event.metadata for event in trace.events
317
+ )
318
+ else []
319
+ ),
320
+ }
321
+ ),
322
+ }
323
+ # Policy DENY has ERROR status in Omnigent. That is a recorded policy
324
+ # decision, not an independently established failed task execution.
325
+ trace.metadata[EXTERNAL_KEY]["execution_status"] = "unknown"
326
+ attach_observations(trace, summary)
327
+ receipt["outcome"]["execution_status"] = "unknown"
328
+ receipt["source_metadata"]["omnigent"] = {
329
+ key: summary[key]
330
+ for key in (
331
+ "schema_version",
332
+ "adapter_contract_revision",
333
+ "observation_count",
334
+ "model_coverage",
335
+ "task_correctness",
336
+ "enforcement",
337
+ "source_execution_outcome",
338
+ "gaps",
339
+ )
340
+ }
341
+ for item in receipt["traces"]:
342
+ item["trace_sha256"] = sha256(native_bytes(trace)).hexdigest()
343
+ frozen.append(ImportReceipt.from_dict(receipt, limits=limits))
344
+ session_ids = {
345
+ item["session_id"] for item in observations if item["session_id"] is not None
346
+ }
347
+ for session_id in sorted(session_ids):
348
+ group_id = external_id(
349
+ "group", "omnigent", {**options.identity, "session_id": session_id}
350
+ )
351
+ group = groups.setdefault(
352
+ group_id,
353
+ {
354
+ "group_id": group_id,
355
+ "session_id": session_id,
356
+ "run_ids": [],
357
+ "source_trace_ids": [],
358
+ "relationships": [],
359
+ "parentage": "partial",
360
+ "interpretation": "session grouping preserves separate trace identities and establishes no causal tree",
361
+ },
362
+ )
363
+ group["run_ids"].append(trace.run_id)
364
+ group["source_trace_ids"].append(receipt["external_identity"]["trace_id"])
365
+ group["relationships"].extend(
366
+ item
367
+ for item in relations
368
+ if event_sessions.get(item["source_event_id"]) == session_id
369
+ )
370
+ frozen.extend(
371
+ receipt for receipt in imported.source_receipts if not receipt.to_dict()["traces"]
372
+ )
373
+ actions = Counter(
374
+ item["action"]
375
+ for trace in imported.traces
376
+ for item in trace.metadata["agentloop.omnigent"]["observations"]["policy_decisions"]
377
+ )
378
+ inventory = {
379
+ **imported.inventory(),
380
+ "adapter": "omnigent-otel",
381
+ "adapter_contract_revision": CONTRACT_REVISION,
382
+ "session_groups": len(groups),
383
+ "policy_actions": dict(sorted(actions.items())),
384
+ "enforcement": "unverified",
385
+ "model_coverage": "unknown_or_observed_partial",
386
+ "session_sidecar": "omnigent-sessions.json",
387
+ }
388
+ assigned_run_ids = {run_id for group in groups.values() for run_id in group["run_ids"]}
389
+ sessions = {
390
+ "schema_version": "1.0",
391
+ "source": "omnigent",
392
+ "groups": list(groups.values()),
393
+ "unassigned_run_ids": [
394
+ trace.run_id for trace in imported.traces if trace.run_id not in assigned_run_ids
395
+ ],
396
+ }
397
+ inventory["session_sidecar_sha256"] = sha256(
398
+ (canonical_json(sessions) + "\n").encode()
399
+ ).hexdigest()
400
+ return OmnigentImportResult(
401
+ imported.traces, tuple(frozen), canonical_json(inventory), canonical_json(sessions)
402
+ )
403
+
404
+
405
+ def inspect_omnigent_bundle(path: str | Path, limits: ImportLimits = ImportLimits()) -> dict:
406
+ """Inspect existing exported evidence without executing any source records."""
407
+ root = Path(path)
408
+ inventory = load_json_artifact(root, "inventory.json", limits=limits).payload
409
+ sessions_artifact = load_json_artifact(root, "omnigent-sessions.json", limits=limits)
410
+ sessions = sessions_artifact.payload
411
+ if (
412
+ not isinstance(inventory, dict)
413
+ or inventory.get("adapter") != "omnigent-otel"
414
+ or not isinstance(sessions, dict)
415
+ or sessions.get("schema_version") != "1.0"
416
+ or sessions.get("source") != "omnigent"
417
+ or inventory.get("session_sidecar_sha256") != sessions_artifact.artifact_sha256
418
+ ):
419
+ raise ImportValidationError(
420
+ "invalid_bundle", "inventory", "not a supported Omnigent import bundle"
421
+ )
422
+ return {"inventory": inventory, "sessions": sessions}