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,366 @@
1
+ """Narrow synchronous recovery through LangGraph's existing root checkpointer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from threading import RLock
7
+
8
+ from agentloop.budget_types import DispatchOptions
9
+ from agentloop.checkpoint_state import (
10
+ CheckpointError,
11
+ _require,
12
+ _text,
13
+ restore_run,
14
+ seal_run,
15
+ validate_states,
16
+ )
17
+ from agentloop.context_types import fingerprint
18
+ from agentloop.events import new_run_id
19
+ from agentloop.harness import HarnessControlError, Hook
20
+ from agentloop.integrations.langgraph_harness import (
21
+ SUPPORTED_VERSION,
22
+ ControlledRunnable,
23
+ _check_version,
24
+ )
25
+ from agentloop.loop_types import StepInfo
26
+ from agentloop.recovery_evidence import record_recovery
27
+ from agentloop.tracer import current_trace
28
+
29
+
30
+ @dataclass(frozen=True)
31
+ class RecoveryOutcome:
32
+ run_id: str
33
+ status: str
34
+ value: object = field(repr=False, compare=False)
35
+
36
+
37
+ class LangGraphRecovery:
38
+ """Host-owned records and atomic claims; only declared retry-safe root nodes.
39
+
40
+ Bind one controller per owner/thread. Capture only after the original root
41
+ invocation has unwound. The journal must serialize all actors for that thread.
42
+ No asynchronous, streamed, subgraph or dynamic-interrupt recovery is claimed.
43
+ """
44
+
45
+ def __init__(
46
+ self, controlled, store, *, owner, thread_id, graph_version, clock_domain, retry_safe_nodes
47
+ ):
48
+ _require(type(controlled) is ControlledRunnable, "unsupported_recovery_adapter")
49
+ _check_version()
50
+ self._adapter, self._app = controlled._adapter, controlled.app
51
+ self._controlled = controlled
52
+ _require(
53
+ {Hook("iteration"), Hook("iteration", "after")} <= self._adapter.capabilities.hooks,
54
+ "unsupported_recovery_adapter",
55
+ )
56
+ _require(
57
+ callable(getattr(self._app, "get_state", None))
58
+ and getattr(self._app, "checkpointer", None) is not None,
59
+ "checkpoint_provider_required",
60
+ )
61
+ _require(
62
+ getattr(store, "atomic_claim", None) is True
63
+ and type(getattr(store, "durable", None)) is bool
64
+ and getattr(store, "schema_version", None) == "1.0"
65
+ and all(callable(getattr(store, name, None)) for name in ("save", "load", "claim")),
66
+ "unsupported_recovery_store",
67
+ )
68
+ _require(
69
+ all(_text(value) for value in (owner, thread_id, graph_version, clock_domain)),
70
+ "invalid_owner",
71
+ )
72
+ _require(
73
+ isinstance(retry_safe_nodes, (list, tuple, set, frozenset))
74
+ and len(retry_safe_nodes) <= 256
75
+ and all(_text(node) for node in retry_safe_nodes),
76
+ "invalid_retry_safety",
77
+ )
78
+ validate_states(self._adapter._harness, {})
79
+ self._store, self._owner, self._thread, self._clock = store, owner, thread_id, clock_domain
80
+ self._safe = frozenset(retry_safe_nodes)
81
+ self._recovery_config_hash = fingerprint(
82
+ {"graph_version": graph_version, "retry_safe_nodes": sorted(self._safe)}
83
+ )
84
+ self._stream = fingerprint({"thread_id": thread_id, "namespace": ""})
85
+ self._lock, self._running = RLock(), False
86
+ self._claim, self._root_run_id = None, None
87
+ self._last_run = None
88
+
89
+ @property
90
+ def last_run(self):
91
+ return self._last_run
92
+
93
+ def _snapshot(self):
94
+ snapshot = self._app.get_state(
95
+ {"configurable": {"thread_id": self._thread, "checkpoint_ns": ""}}
96
+ )
97
+ config = getattr(snapshot, "config", None)
98
+ _require(
99
+ type(config) is dict and type(config.get("configurable")) is dict, "missing_checkpoint"
100
+ )
101
+ selected = config["configurable"]
102
+ _require(
103
+ selected.get("thread_id") == self._thread and selected.get("checkpoint_ns", "") == "",
104
+ "checkpoint_owner_mismatch",
105
+ )
106
+ _require(_text(selected.get("checkpoint_id")), "missing_checkpoint")
107
+ pending, tasks = getattr(snapshot, "next", None), getattr(snapshot, "tasks", None)
108
+ _require(
109
+ isinstance(pending, (tuple, list))
110
+ and len(pending) <= 256
111
+ and all(_text(name) for name in pending),
112
+ "invalid_checkpoint",
113
+ )
114
+ _require(isinstance(tasks, (tuple, list)) and len(tasks) <= 256, "invalid_checkpoint")
115
+ declarations = []
116
+ for task in tasks:
117
+ _require(_text(getattr(task, "name", None)), "invalid_checkpoint")
118
+ _require(getattr(task, "state", None) is None, "subgraph_recovery_unsupported")
119
+ declarations.append(
120
+ {
121
+ "name": task.name,
122
+ "failed": getattr(task, "error", None) is not None,
123
+ "interrupted": bool(getattr(task, "interrupts", ())),
124
+ }
125
+ )
126
+ return {
127
+ "checkpoint_id": selected["checkpoint_id"],
128
+ "next": list(pending),
129
+ "tasks": declarations,
130
+ }
131
+
132
+ def _enter(self):
133
+ with self._lock:
134
+ _require(not self._running, "recovery_already_running")
135
+ self._running = True
136
+
137
+ def capture(self):
138
+ """Retire a quiescent run and save its ledger beside the host checkpoint.
139
+
140
+ Failed persistence leaves the old run retired. Do not retry work; reconcile
141
+ the journal and framework state with the host before another attempt.
142
+ """
143
+ self._enter()
144
+ run = self._adapter.last_run
145
+ reference = None
146
+ try:
147
+ _require(run is not None, "missing_run")
148
+ if self._last_run is not None:
149
+ _require(run is self._last_run, "run_lineage_changed")
150
+ source_trace_id = self._last_trace_id
151
+ else:
152
+ source = self._controlled._last_invocation.get()
153
+ _require(
154
+ isinstance(source, dict)
155
+ and source["finished"]
156
+ and source["run_id"] == run.run_id
157
+ and source["thread_id"] == self._thread
158
+ and source["checkpoint_ns"] == "",
159
+ "run_checkpoint_mismatch",
160
+ )
161
+ _require(source["durability"] == "sync", "synchronous_durability_required")
162
+ source_trace_id = source["trace_id"]
163
+ snapshot = self._snapshot()
164
+ _require(bool(snapshot["next"]), "completed_checkpoint")
165
+ state = seal_run(run, clock_domain=self._clock)
166
+ self._controlled._recovery_bound = True
167
+ record = {
168
+ "schema_version": "1.0",
169
+ "adapter_version": SUPPORTED_VERSION,
170
+ "recovery_config_hash": self._recovery_config_hash,
171
+ "owner_hash": fingerprint(self._owner),
172
+ "stream_hash": self._stream,
173
+ "root_run_id": self._root_run_id or run.run_id,
174
+ "parent_trace_id": source_trace_id,
175
+ "host": snapshot,
176
+ "policy_state": state,
177
+ }
178
+ reference = self._store.save(
179
+ self._owner,
180
+ self._stream,
181
+ record,
182
+ previous_ref=None if self._claim is None else self._claim[0],
183
+ request_id=None if self._claim is None else self._claim[1],
184
+ )
185
+ _require(_text(reference), "invalid_checkpoint_reference")
186
+ record_recovery(
187
+ "continue",
188
+ "checkpoint_saved",
189
+ reference=reference,
190
+ parent_run_id=run.run_id,
191
+ status="incomplete",
192
+ store_durable=self._store.durable,
193
+ )
194
+ return reference
195
+ except BaseException as exc:
196
+ try:
197
+ record_recovery(
198
+ "escalate",
199
+ exc.reason_code
200
+ if isinstance(exc, CheckpointError)
201
+ else "checkpoint_save_failed",
202
+ reference=reference,
203
+ parent_run_id=None if run is None else run.run_id,
204
+ store_durable=self._store.durable,
205
+ )
206
+ except BaseException:
207
+ pass
208
+ raise
209
+ finally:
210
+ with self._lock:
211
+ self._running = False
212
+
213
+ def resume(self, reference, *, request_id):
214
+ self._enter()
215
+ run = None
216
+ parent = None
217
+ try:
218
+ _require(_text(reference) and _text(request_id), "invalid_checkpoint_reference")
219
+ record = self._store.load(self._owner, self._stream, reference)
220
+ _require(
221
+ type(record) is dict
222
+ and set(record)
223
+ == {
224
+ "schema_version",
225
+ "adapter_version",
226
+ "recovery_config_hash",
227
+ "owner_hash",
228
+ "stream_hash",
229
+ "root_run_id",
230
+ "parent_trace_id",
231
+ "host",
232
+ "policy_state",
233
+ },
234
+ "invalid_checkpoint",
235
+ )
236
+ _require(
237
+ record["schema_version"] == "1.0"
238
+ and record["adapter_version"] == SUPPORTED_VERSION,
239
+ "incompatible_schema",
240
+ )
241
+ _require(
242
+ record["recovery_config_hash"] == self._recovery_config_hash,
243
+ "incompatible_configuration",
244
+ )
245
+ _require(
246
+ record["owner_hash"] == fingerprint(self._owner)
247
+ and record["stream_hash"] == self._stream,
248
+ "checkpoint_owner_mismatch",
249
+ )
250
+ _require(
251
+ _text(record["root_run_id"]) and type(record["policy_state"]) is dict,
252
+ "invalid_run_lineage",
253
+ )
254
+ _require(
255
+ record["parent_trace_id"] is None or _text(record["parent_trace_id"]),
256
+ "invalid_run_lineage",
257
+ )
258
+ parent = record["policy_state"].get("run_id")
259
+ snapshot = self._snapshot()
260
+ _require(snapshot == record["host"], "stale_checkpoint")
261
+ _require(bool(snapshot["next"]), "completed_checkpoint")
262
+ _require(
263
+ not any(task["interrupted"] for task in snapshot["tasks"]),
264
+ "dynamic_interrupt_requires_host",
265
+ )
266
+ _require(
267
+ set(snapshot["next"]) <= self._safe
268
+ and all(task["name"] in self._safe for task in snapshot["tasks"]),
269
+ "side_effect_reconciliation_required",
270
+ )
271
+ trace = current_trace()
272
+ _require(
273
+ trace is None or trace.run_id != record["parent_trace_id"], "new_trace_required"
274
+ )
275
+ run = restore_run(
276
+ self._adapter._harness,
277
+ record["policy_state"],
278
+ clock_domain=self._clock,
279
+ run_id=new_run_id(),
280
+ )
281
+ _require(
282
+ self._store.claim(
283
+ self._owner, self._stream, reference, request_id, fingerprint(record)
284
+ )
285
+ is True,
286
+ "duplicate_or_stale_resume",
287
+ )
288
+ self._claim, self._root_run_id = (reference, request_id), record["root_run_id"]
289
+ self._last_run = run
290
+ self._last_trace_id = None if trace is None else trace.run_id
291
+ self._controlled._recovery_bound = True
292
+ record_recovery(
293
+ "continue",
294
+ "checkpoint_resume_admitted",
295
+ reference=reference,
296
+ parent_run_id=parent,
297
+ attempt_run_id=run.run_id,
298
+ store_durable=self._store.durable,
299
+ )
300
+ session = self._adapter._start(run)
301
+ with self._adapter._scope(session, "main", root=True):
302
+ # A resumed root is an explicit harness retry. Its children keep
303
+ # their own model/tool accounting and any SDK retry identities.
304
+ invoke = run.wrap(
305
+ self._app.invoke,
306
+ boundary="iteration",
307
+ branch_id="checkpoint.resume",
308
+ dispatch=DispatchOptions(
309
+ retry_source="harness", step=StepInfo("checkpoint.resume", retry_safe=True)
310
+ ),
311
+ )
312
+ value = invoke(
313
+ None,
314
+ {
315
+ "configurable": {
316
+ "thread_id": self._thread,
317
+ "checkpoint_ns": "",
318
+ "checkpoint_id": snapshot["checkpoint_id"],
319
+ }
320
+ },
321
+ durability="sync",
322
+ )
323
+ completed = not self._snapshot()["next"]
324
+ if completed:
325
+ value = self._adapter._complete(session, value)
326
+ status = "completed" if completed else "incomplete"
327
+ record_recovery(
328
+ "continue",
329
+ "checkpoint_attempt_returned",
330
+ reference=reference,
331
+ parent_run_id=parent,
332
+ attempt_run_id=run.run_id,
333
+ status=status,
334
+ store_durable=self._store.durable,
335
+ )
336
+ return RecoveryOutcome(run.run_id, status, value)
337
+ except BaseException as exc:
338
+ status = (
339
+ "stopped"
340
+ if isinstance(exc, HarnessControlError)
341
+ else "cancelled"
342
+ if not isinstance(exc, Exception)
343
+ else "failed"
344
+ if run is not None
345
+ else "incomplete"
346
+ )
347
+ reason = (
348
+ exc.reason_code if isinstance(exc, CheckpointError) else "recovery_execution_failed"
349
+ )
350
+ try:
351
+ record_recovery(
352
+ "escalate",
353
+ reason,
354
+ reference=reference,
355
+ parent_run_id=parent,
356
+ attempt_run_id=None if run is None else run.run_id,
357
+ status=status,
358
+ store_durable=self._store.durable,
359
+ )
360
+ except BaseException:
361
+ # Preserve host failures/cancellation even if its trace is broken.
362
+ pass
363
+ raise
364
+ finally:
365
+ with self._lock:
366
+ self._running = False
@@ -0,0 +1 @@
1
+ """Read-only Omnigent telemetry adaptation; no Omnigent runtime dependency."""
@@ -0,0 +1,78 @@
1
+ """Read-only Omnigent file import and exported-session inspection commands."""
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
+ omnigent_app = typer.Typer(help="Import Omnigent OTLP files and inspect session evidence offline.")
13
+
14
+
15
+ @omnigent_app.command("import-otel")
16
+ def import_otel_command(
17
+ path: Path,
18
+ out: Path = typer.Option(...),
19
+ strict: bool = typer.Option(False),
20
+ synthetic: bool = typer.Option(False),
21
+ ) -> None:
22
+ from agentloop.integrations.omnigent.telemetry import import_omnigent
23
+ from agentloop.interoperability.otlp_jsonl import OtlpOptions
24
+
25
+ try:
26
+ result = import_omnigent(
27
+ path,
28
+ options=OtlpOptions(
29
+ system="omnigent", continue_on_error=not strict, synthetic_fixture=synthetic
30
+ ),
31
+ )
32
+ except (ImportValidationError, OSError) as exc:
33
+ message = (
34
+ str(exc)
35
+ if isinstance(exc, ImportValidationError)
36
+ else "Omnigent artifacts could not be read"
37
+ )
38
+ raise typer.BadParameter(message, param_hint="path") from None
39
+ try:
40
+ inventory = result.write(out)
41
+ except (ImportValidationError, OSError) as exc:
42
+ message = (
43
+ str(exc) if isinstance(exc, ImportValidationError) else "Output could not be written"
44
+ )
45
+ raise typer.BadParameter(message, param_hint="--out") from None
46
+ stats = result.inventory()
47
+ typer.echo(
48
+ f"{stats['native_traces']} trace projections; {stats['session_groups']} session groups; {stats['invalid_records']} invalid records."
49
+ )
50
+ typer.echo(
51
+ "Policy decisions are observations; dispatch prevention and task correctness remain unverified."
52
+ )
53
+ typer.echo(f"Inventory: {inventory}")
54
+
55
+
56
+ @omnigent_app.command("inspect")
57
+ def inspect_command(path: Path, json_out: Path | None = typer.Option(None)) -> None:
58
+ from agentloop.integrations.omnigent.telemetry import inspect_omnigent_bundle
59
+
60
+ try:
61
+ result = inspect_omnigent_bundle(path)
62
+ except (ImportValidationError, OSError) as exc:
63
+ message = (
64
+ str(exc)
65
+ if isinstance(exc, ImportValidationError)
66
+ else "Import bundle could not be read"
67
+ )
68
+ raise typer.BadParameter(message, param_hint="path") from None
69
+ content = json.dumps(result, indent=2)
70
+ if json_out is not None:
71
+ try:
72
+ json_out.parent.mkdir(parents=True, exist_ok=True)
73
+ json_out.write_text(content + "\n", encoding="utf-8")
74
+ except OSError:
75
+ raise typer.BadParameter(
76
+ "Inspection output could not be written", param_hint="--json-out"
77
+ ) from None
78
+ typer.echo(content)
@@ -0,0 +1,93 @@
1
+ """Snapshot-bound Omnigent observations, separate from enforcement and quality."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from hashlib import sha256
7
+ from typing import Any
8
+
9
+ from agentloop.interoperability.validation import ImportValidationError, validate_json_tree
10
+ from agentloop.interventions import canonical_json
11
+
12
+ OMNIGENT_KEY = "agentloop.omnigent"
13
+
14
+
15
+ def _binding(trace: Any) -> str:
16
+ value = trace.to_dict()
17
+ value["metadata"] = {
18
+ key: item for key, item in value["metadata"].items() if key != OMNIGENT_KEY
19
+ }
20
+ return sha256(canonical_json(value).encode()).hexdigest()
21
+
22
+
23
+ def attach_observations(trace: Any, observations: dict) -> None:
24
+ """Bind one conservative source summary to the captured native projection."""
25
+ if OMNIGENT_KEY in trace.metadata:
26
+ raise ImportValidationError(
27
+ "invalid_omnigent_evidence",
28
+ OMNIGENT_KEY,
29
+ "observations cannot replace an existing snapshot",
30
+ )
31
+ record = {
32
+ "schema_version": "1.0",
33
+ "run_id": trace.run_id,
34
+ "trace_binding_sha256": _binding(trace),
35
+ "observations": observations,
36
+ }
37
+ record["sha256"] = sha256(canonical_json(record).encode()).hexdigest()
38
+ trace.metadata[OMNIGENT_KEY] = json.loads(canonical_json(record))
39
+
40
+
41
+ def read_observations(trace: Any) -> dict | None:
42
+ record = trace.metadata.get(OMNIGENT_KEY)
43
+ if record is None:
44
+ return None
45
+ validate_json_tree(record)
46
+ if (
47
+ not isinstance(record, dict)
48
+ or set(record)
49
+ != {"schema_version", "run_id", "trace_binding_sha256", "observations", "sha256"}
50
+ or record["schema_version"] != "1.0"
51
+ ):
52
+ raise ImportValidationError(
53
+ "invalid_omnigent_evidence", OMNIGENT_KEY, "unsupported observation snapshot"
54
+ )
55
+ observations = record["observations"]
56
+ if (
57
+ not isinstance(observations, dict)
58
+ or observations.get("schema_version") != "1.0"
59
+ or observations.get("enforcement") != "unverified"
60
+ or observations.get("task_correctness") != "unavailable"
61
+ or observations.get("source_execution_outcome") != "unknown"
62
+ or observations.get("model_coverage") not in {"unknown", "observed_partial"}
63
+ ):
64
+ raise ImportValidationError(
65
+ "invalid_omnigent_evidence", OMNIGENT_KEY, "unsupported observation claims"
66
+ )
67
+ if not isinstance(observations.get("policy_decisions"), list) or any(
68
+ not isinstance(item, dict)
69
+ or item.get("enforcement") != "unverified"
70
+ or item.get("dispatch_prevention") != "unknown"
71
+ for item in observations["policy_decisions"]
72
+ ):
73
+ raise ImportValidationError(
74
+ "invalid_omnigent_evidence", OMNIGENT_KEY, "imported decisions cannot prove enforcement"
75
+ )
76
+ body = {key: value for key, value in record.items() if key != "sha256"}
77
+ if (
78
+ record["run_id"] != trace.run_id
79
+ or record["trace_binding_sha256"] != _binding(trace)
80
+ or record["sha256"] != sha256(canonical_json(body).encode()).hexdigest()
81
+ ):
82
+ raise ImportValidationError(
83
+ "invalid_omnigent_evidence",
84
+ OMNIGENT_KEY,
85
+ "observations no longer match the captured projection",
86
+ )
87
+ return json.loads(canonical_json(record["observations"]))
88
+
89
+
90
+ def qualify_report(trace: Any, report: dict) -> None:
91
+ observations = read_observations(trace)
92
+ if observations is not None:
93
+ report["omnigent_observations"] = observations