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