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 @@
1
+ """Capability evidence and executable conformance for explicit adapters."""
@@ -0,0 +1,77 @@
1
+ """Capability inspection commands; enumeration never invokes an adapter."""
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
+ harness_app = typer.Typer(help="Inspect declared and behavior-verified harness capabilities.")
13
+
14
+
15
+ @harness_app.command("bench")
16
+ def bench_command(
17
+ adapter: str = typer.Option("python-wrapped"),
18
+ mode: str = typer.Option("offline"),
19
+ out: Path = typer.Option(Path("runs/capability-bench")),
20
+ json_out: Path | None = typer.Option(None),
21
+ test_timestamp: str | None = typer.Option(None),
22
+ ) -> None:
23
+ """Run trusted offline probes; optional/live features remain explicitly skipped."""
24
+ from agentloop.harness_bench.offline import run_offline_bench
25
+
26
+ if mode != "offline":
27
+ raise typer.BadParameter(
28
+ "Only the approved offline probe mode is available", param_hint="--mode"
29
+ )
30
+ try:
31
+ result = run_offline_bench(adapter, test_timestamp=test_timestamp)
32
+ path = result.write(out)
33
+ if json_out is not None:
34
+ json_out.parent.mkdir(parents=True, exist_ok=True)
35
+ json_out.write_text(json.dumps(result.report(), indent=2) + "\n", encoding="utf-8")
36
+ except (ImportValidationError, OSError) as exc:
37
+ message = (
38
+ str(exc)
39
+ if isinstance(exc, ImportValidationError)
40
+ else "Bench output could not be written"
41
+ )
42
+ raise typer.BadParameter(message, param_hint="--adapter/--out") from None
43
+ report = result.report()
44
+ typer.echo(
45
+ f"Offline probe report: {path}; {len(report['drift'])} declaration/behavior contradictions. Scope: explicitly wrapped calls only."
46
+ )
47
+ if report["drift"] or report["status"] == "failed":
48
+ raise typer.Exit(1)
49
+
50
+
51
+ @harness_app.command("list-adapters")
52
+ def list_adapters_command() -> None:
53
+ from agentloop.interoperability.registry import builtin_adapters
54
+
55
+ typer.echo(json.dumps([descriptor.to_dict() for descriptor in builtin_adapters()], indent=2))
56
+
57
+
58
+ @harness_app.command("capabilities")
59
+ def capabilities_command(
60
+ adapter: str = typer.Option(...), json_out: Path | None = typer.Option(None)
61
+ ) -> None:
62
+ from agentloop.interoperability.registry import capability_report, get_adapter
63
+
64
+ try:
65
+ result = capability_report(get_adapter(adapter))
66
+ except ImportValidationError as exc:
67
+ raise typer.BadParameter(str(exc), param_hint="--adapter") from None
68
+ content = json.dumps(result, indent=2)
69
+ if json_out is not None:
70
+ try:
71
+ json_out.parent.mkdir(parents=True, exist_ok=True)
72
+ json_out.write_text(content + "\n", encoding="utf-8")
73
+ except OSError:
74
+ raise typer.BadParameter(
75
+ "Capability output could not be written", param_hint="--json-out"
76
+ ) from None
77
+ typer.echo(content)
@@ -0,0 +1,96 @@
1
+ """Trusted offline drivers for existing explicit wrapped-call interfaces."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from typing import Any
7
+
8
+ from agentloop.harness import BOUNDARIES, Harness, HarnessConfig
9
+
10
+
11
+ class ProtocolGraph:
12
+ """The same four-entrypoint fake protocol used by enforcement conformance."""
13
+
14
+ def __init__(self, function):
15
+ self.function = function
16
+
17
+ def invoke(self, *args, **kwargs):
18
+ return self.function(*args, **kwargs)
19
+
20
+ async def ainvoke(self, *args, **kwargs):
21
+ return await self.function(*args, **kwargs)
22
+
23
+ def stream(self, *args, **kwargs):
24
+ return (yield from self.function(*args, **kwargs))
25
+
26
+ def astream(self, *args, **kwargs):
27
+ return self.function(*args, **kwargs)
28
+
29
+
30
+ class OfflineDriver:
31
+ """Invoke explicit adapters; never discover or run a vendor transport."""
32
+
33
+ def __init__(self, adapter_id: str):
34
+ self.adapter_id = adapter_id
35
+
36
+ def unavailable_reason(self) -> str | None:
37
+ if self.adapter_id == "python-wrapped":
38
+ return None
39
+ if self.adapter_id == "langgraph-1.2.11":
40
+ try:
41
+ installed = version("langgraph")
42
+ except PackageNotFoundError:
43
+ installed = None
44
+ return None if installed == "1.2.11" else "pinned_langgraph_runtime_unavailable"
45
+ return "observation_adapter_has_no_runtime_probe_driver"
46
+
47
+ def bind(
48
+ self, config: HarnessConfig, function: Any, kind: str, *, boundary="tool", repeated=False
49
+ ):
50
+ if self.adapter_id == "python-wrapped":
51
+ run = Harness(config).start_run("offline-bench")
52
+ wrapped = run.wrap(function, boundary=boundary)
53
+ if repeated:
54
+ repeated_wrapper = run.wrap(wrapped, boundary=boundary)
55
+ if repeated_wrapper is not wrapped:
56
+ raise ValueError("repeated wrapping changed the wrapper")
57
+ return wrapped, lambda: run
58
+ if self.unavailable_reason() is not None:
59
+ raise RuntimeError("offline adapter unavailable")
60
+ from agentloop.integrations.langgraph_harness import LangGraphHarness
61
+
62
+ adapter = LangGraphHarness(config, boundaries=BOUNDARIES)
63
+ # These probes exercise explicit model/tool callable boundaries. They do
64
+ # not pretend a protocol fake is a complete compiled graph scheduler.
65
+ wrapped = getattr(adapter, boundary)(function)
66
+ if repeated and getattr(adapter, boundary)(wrapped) is not wrapped:
67
+ raise ValueError("repeated wrapping changed the wrapper")
68
+ graph = adapter.runnable(ProtocolGraph(wrapped))
69
+ captured = []
70
+ method = {
71
+ "sync": "invoke",
72
+ "async": "ainvoke",
73
+ "generator": "stream",
74
+ "async_generator": "astream",
75
+ }[kind]
76
+ original = getattr(graph, method)
77
+ if kind == "async":
78
+
79
+ async def call(*args, **kwargs):
80
+ try:
81
+ return await original(*args, **kwargs)
82
+ finally:
83
+ captured.append(adapter.last_run)
84
+ elif kind == "async_generator":
85
+
86
+ async def call(*args, **kwargs):
87
+ stream = original(*args, **kwargs)
88
+ try:
89
+ async for item in stream:
90
+ yield item
91
+ finally:
92
+ await stream.aclose()
93
+ captured.append(adapter.last_run)
94
+ else:
95
+ call = original
96
+ return call, lambda: captured[-1] if captured else adapter.last_run
@@ -0,0 +1,507 @@
1
+ """Deterministic offline behavior probes, counters and source-scoped drift."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import platform
8
+ from dataclasses import dataclass
9
+ from datetime import datetime, timezone
10
+ from hashlib import sha256
11
+ from pathlib import Path
12
+ from unittest.mock import patch
13
+
14
+ from agentloop.harness import (
15
+ ACTIONS,
16
+ Decision,
17
+ HarnessConfig,
18
+ HarnessControlError,
19
+ HarnessDeniedError,
20
+ Hook,
21
+ Policy,
22
+ )
23
+ from agentloop.harness_bench.drivers import OfflineDriver
24
+ from agentloop.interoperability.artifacts import write_artifact
25
+ from agentloop.interoperability.registry import (
26
+ AdapterManifest,
27
+ CapabilityObservation,
28
+ _probe_observation,
29
+ capability_report,
30
+ get_adapter,
31
+ )
32
+ from agentloop.interoperability.validation import ImportValidationError
33
+ from agentloop.interventions import canonical_json
34
+ from agentloop.tracer import trace_agent, trace_tool_call
35
+
36
+ PROBE_VERSION = "1.0"
37
+ _PROBES = (
38
+ ("basic-sync", "lifecycle.sync"),
39
+ ("basic-async", "lifecycle.async"),
40
+ ("tool-observation", "observation.tool_calls"),
41
+ ("pretool-deny", "enforcement.pre_tool"),
42
+ ("premodel-deny", "enforcement.pre_model"),
43
+ ("sync-stream", "lifecycle.stream"),
44
+ ("async-stream", "lifecycle.async_stream"),
45
+ ("partial-close", "lifecycle.partial_close"),
46
+ ("cancel", "lifecycle.cancel"),
47
+ ("policy-failure", "enforcement.policy_failure"),
48
+ ("repeated-wrap", "observation.outcomes"),
49
+ ("model-override", "compatibility.model_override"),
50
+ ("approval", "compatibility.approval"),
51
+ ("resume", "lifecycle.resume"),
52
+ ("fork", "lifecycle.fork"),
53
+ )
54
+
55
+
56
+ class FakeClock:
57
+ def __init__(self):
58
+ self.tick = 0
59
+
60
+ def ns(self):
61
+ self.tick += 1_000_000
62
+ return self.tick
63
+
64
+ def utc(self):
65
+ return "2000-01-01T00:00:00+00:00"
66
+
67
+
68
+ def _config(action="continue", boundary="tool", *, broken=False):
69
+ def policy(_context):
70
+ if broken:
71
+ raise ValueError("synthetic policy failure")
72
+ return Decision(action, reason_code="offline_probe")
73
+
74
+ return HarnessConfig(
75
+ "enforce",
76
+ (
77
+ Policy(
78
+ "offline-probe",
79
+ PROBE_VERSION,
80
+ policy,
81
+ hooks=frozenset({Hook(boundary)}),
82
+ actions=ACTIONS,
83
+ ),
84
+ ),
85
+ )
86
+
87
+
88
+ def _hooks(run) -> list[dict]:
89
+ if run is None:
90
+ return []
91
+ return [
92
+ {
93
+ "boundary": item.hook.boundary,
94
+ "phase": item.hook.phase,
95
+ "action": item.action,
96
+ "applied": item.applied,
97
+ "dispatched": item.dispatched,
98
+ "status": item.status,
99
+ "policy_failed": any(value.failed for value in item.proposals),
100
+ }
101
+ for item in run.results
102
+ ]
103
+
104
+
105
+ def _errors_preserved(driver: OfflineDriver) -> bool:
106
+ """Check original failure identity at each supported callable lifecycle."""
107
+ for kind in ("sync", "async", "generator", "async_generator"):
108
+ original = ValueError("synthetic invocation failure")
109
+
110
+ def fail():
111
+ raise original
112
+
113
+ async def afail():
114
+ raise original
115
+
116
+ def stream():
117
+ raise original
118
+ yield
119
+
120
+ async def astream():
121
+ raise original
122
+ yield
123
+
124
+ wrapped, _ = driver.bind(
125
+ _config(),
126
+ {"sync": fail, "async": afail, "generator": stream, "async_generator": astream}[kind],
127
+ kind,
128
+ )
129
+ try:
130
+ if kind == "sync":
131
+ wrapped()
132
+ elif kind == "generator":
133
+ next(wrapped())
134
+ else:
135
+
136
+ async def consume():
137
+ return await wrapped() if kind == "async" else await wrapped().__anext__()
138
+
139
+ asyncio.run(consume())
140
+ except ValueError as exc:
141
+ if exc is not original:
142
+ return False
143
+ else:
144
+ return False
145
+ return True
146
+
147
+
148
+ def _probe(driver: OfflineDriver, probe_id: str) -> dict:
149
+ calls = []
150
+ if probe_id in {"basic-sync", "basic-async", "repeated-wrap", "tool-observation"}:
151
+ expected = {"typed_output": 7}
152
+
153
+ def work(value, *, marker):
154
+ calls.append((value, marker))
155
+ run = get_run()
156
+ call_id = run.results[-1].call_id if run is not None and run.results else None
157
+ with trace_tool_call(
158
+ "offline-probe-tool", metadata={"source_harness_call_id": call_id}
159
+ ):
160
+ return expected
161
+
162
+ async def awork(value, *, marker):
163
+ return work(value, marker=marker)
164
+
165
+ kind = "async" if probe_id == "basic-async" else "sync"
166
+ wrapped, get_run = driver.bind(
167
+ _config(),
168
+ awork if kind == "async" else work,
169
+ kind,
170
+ repeated=probe_id == "repeated-wrap",
171
+ )
172
+ with trace_agent("offline-capability-probe") as trace:
173
+ actual = (
174
+ asyncio.run(wrapped(3, marker="safe"))
175
+ if kind == "async"
176
+ else wrapped(3, marker="safe")
177
+ )
178
+ hooks = _hooks(get_run())
179
+ observed = [event for event in trace.events if event.operation_kind == "tool"]
180
+ call_ids = (
181
+ {item.call_id for item in get_run().results if item.dispatched}
182
+ if get_run() is not None
183
+ else set()
184
+ )
185
+ correlated = bool(observed) and all(
186
+ event.metadata.get("source_harness_call_id") in call_ids for event in observed
187
+ )
188
+ errors_preserved = _errors_preserved(driver) if probe_id == "repeated-wrap" else True
189
+ return {
190
+ "passed": actual is expected
191
+ and calls == [(3, "safe")]
192
+ and correlated
193
+ and any(item["dispatched"] for item in hooks)
194
+ and errors_preserved,
195
+ "protected_callable_dispatches": len(calls),
196
+ "return_identity_preserved": actual is expected,
197
+ "tool_spans": len(observed),
198
+ "tool_identity_correlated": correlated,
199
+ "original_errors_preserved": errors_preserved,
200
+ "hook_records": hooks,
201
+ }
202
+ if probe_id in {"pretool-deny", "premodel-deny", "policy-failure"}:
203
+ boundary = "model" if probe_id == "premodel-deny" else "tool"
204
+
205
+ def work(value):
206
+ calls.append(value)
207
+ return 7
208
+
209
+ async def awork(value):
210
+ return work(value)
211
+
212
+ def stream(value):
213
+ yield work(value)
214
+
215
+ async def astream(value):
216
+ yield work(value)
217
+
218
+ acknowledgements, hooks = [], []
219
+ for kind in ("sync", "async", "generator", "async_generator"):
220
+ wrapped, get_run = driver.bind(
221
+ _config("deny", boundary, broken=probe_id == "policy-failure"),
222
+ {"sync": work, "async": awork, "generator": stream, "async_generator": astream}[
223
+ kind
224
+ ],
225
+ kind,
226
+ boundary=boundary,
227
+ )
228
+ denied = False
229
+ try:
230
+ if kind == "sync":
231
+ wrapped(3)
232
+ elif kind == "generator":
233
+ next(wrapped(3))
234
+ else:
235
+
236
+ async def consume():
237
+ return await wrapped(3) if kind == "async" else await wrapped(3).__anext__()
238
+
239
+ asyncio.run(consume())
240
+ except HarnessControlError as exc:
241
+ denied = isinstance(exc, HarnessDeniedError) or probe_id == "policy-failure"
242
+ acknowledgements.append(denied)
243
+ hooks.extend(_hooks(get_run()))
244
+ denied = all(acknowledgements)
245
+ policy_failed = any(item["policy_failed"] for item in hooks)
246
+ return {
247
+ "passed": denied
248
+ and not calls
249
+ and any(
250
+ item["action"]
251
+ in ({"deny", "stop", "escalate"} if probe_id == "policy-failure" else {"deny"})
252
+ and item["applied"]
253
+ and not item["dispatched"]
254
+ for item in hooks
255
+ )
256
+ and (probe_id != "policy-failure" or policy_failed),
257
+ "protected_callable_dispatches": len(calls),
258
+ "denial_acknowledged": denied,
259
+ "tested_lifecycles": ["sync", "async", "generator", "async_generator"],
260
+ "policy_failure_recorded": policy_failed,
261
+ "hook_records": hooks,
262
+ }
263
+ if probe_id in {"sync-stream", "async-stream", "partial-close"}:
264
+ state = {"entered": False, "finished": False, "closed": False, "chunks": 0}
265
+
266
+ def stream():
267
+ state["entered"] = True
268
+ try:
269
+ state["chunks"] += 1
270
+ yield 1
271
+ state["chunks"] += 1
272
+ yield 2
273
+ state["finished"] = True
274
+ finally:
275
+ state["closed"] = True
276
+
277
+ async def astream():
278
+ for value in stream():
279
+ yield value
280
+
281
+ kind = "async_generator" if probe_id == "async-stream" else "generator"
282
+ wrapped, get_run = driver.bind(
283
+ _config(), astream if kind == "async_generator" else stream, kind
284
+ )
285
+
286
+ async def consume_async():
287
+ source = wrapped()
288
+ lazy = not state["entered"]
289
+ first = await source.__anext__()
290
+ before_finish = not state["finished"] and state["chunks"] == 1
291
+ second = await source.__anext__()
292
+ try:
293
+ await source.__anext__()
294
+ except StopAsyncIteration:
295
+ pass
296
+ await source.aclose()
297
+ return lazy, first, before_finish, second
298
+
299
+ if kind == "async_generator":
300
+ lazy, first, before_finish, second = asyncio.run(consume_async())
301
+ else:
302
+ source = wrapped()
303
+ lazy = not state["entered"]
304
+ first = next(source)
305
+ before_finish = not state["finished"] and state["chunks"] == 1
306
+ if probe_id == "partial-close":
307
+ source.close()
308
+ second = None
309
+ else:
310
+ second = next(source)
311
+ try:
312
+ next(source)
313
+ except StopIteration:
314
+ pass
315
+ source.close()
316
+ passed = (
317
+ lazy
318
+ and first == 1
319
+ and before_finish
320
+ and state["closed"]
321
+ and (
322
+ state["chunks"] == 1 and not state["finished"]
323
+ if probe_id == "partial-close"
324
+ else second == 2 and state["finished"]
325
+ )
326
+ )
327
+ return {
328
+ "passed": passed,
329
+ "lazy_entry": lazy,
330
+ "first_chunk_before_completion": before_finish,
331
+ **state,
332
+ "hook_records": _hooks(get_run()),
333
+ }
334
+ if probe_id == "cancel":
335
+ state = {"entered": False, "cancellation_acknowledged": False, "finally_executed": False}
336
+
337
+ async def scenario():
338
+ started, blocked = asyncio.Event(), asyncio.Event()
339
+
340
+ async def work():
341
+ state["entered"] = True
342
+ started.set()
343
+ try:
344
+ await blocked.wait()
345
+ finally:
346
+ state["finally_executed"] = True
347
+
348
+ wrapped, get_run = driver.bind(_config(), work, "async")
349
+ task = asyncio.create_task(wrapped())
350
+ starter = asyncio.create_task(started.wait())
351
+ done, _ = await asyncio.wait({task, starter}, return_when=asyncio.FIRST_COMPLETED)
352
+ if task in done and not started.is_set():
353
+ starter.cancel()
354
+ await asyncio.gather(starter, return_exceptions=True)
355
+ return []
356
+ task.cancel()
357
+ try:
358
+ await task
359
+ except asyncio.CancelledError:
360
+ state["cancellation_acknowledged"] = True
361
+ await starter
362
+ return _hooks(get_run())
363
+
364
+ hooks = asyncio.run(scenario())
365
+ return {
366
+ "passed": all(state.values()) and any(item["status"] == "cancelled" for item in hooks),
367
+ **state,
368
+ "hook_records": hooks,
369
+ }
370
+ raise ValueError("unsupported offline probe")
371
+
372
+
373
+ @dataclass(frozen=True)
374
+ class BenchResult:
375
+ observations: tuple[CapabilityObservation, ...]
376
+ _report_json: str
377
+ _proofs_json: str
378
+
379
+ def report(self) -> dict:
380
+ return json.loads(self._report_json)
381
+
382
+ def write(self, out: str | Path) -> Path:
383
+ root = Path(out)
384
+ root.mkdir(parents=True, exist_ok=True)
385
+ root = root.resolve()
386
+ for reference, proof in json.loads(self._proofs_json).items():
387
+ write_artifact(root, reference, (canonical_json(proof) + "\n").encode())
388
+ write_artifact(root, "capability-report.json", (self._report_json + "\n").encode())
389
+ rows = self.report()["capabilities"]
390
+ markdown = (
391
+ "# Offline capability bench\n\nExplicit instrumented boundaries only; hidden SDK work and vendor behavior remain unverified.\n\n| Capability | State |\n| --- | --- |\n"
392
+ + "".join(f"| {row['name']} | {row['state']} |\n" for row in rows)
393
+ )
394
+ write_artifact(root, "capability-report.md", markdown.encode())
395
+ return root / "capability-report.json"
396
+
397
+
398
+ def run_offline_bench(
399
+ adapter_id="python-wrapped",
400
+ *,
401
+ driver: OfflineDriver | None = None,
402
+ manifest: AdapterManifest | None = None,
403
+ test_timestamp: str | None = None,
404
+ ) -> BenchResult:
405
+ """Execute only trusted local fakes; optional SDK absence yields real skips."""
406
+ manifest = manifest or get_adapter(adapter_id)
407
+ driver = driver or OfflineDriver(adapter_id)
408
+ if driver.adapter_id != manifest.adapter_id:
409
+ raise ImportValidationError(
410
+ "invalid_probe_driver", "adapter", "driver identity differs from its manifest"
411
+ )
412
+ stamp = test_timestamp or datetime.now(timezone.utc).isoformat()
413
+ environment = {
414
+ "python": platform.python_version(),
415
+ "platform": platform.system(),
416
+ "machine": platform.machine(),
417
+ "adapter_code_fingerprint": manifest.code_fingerprint,
418
+ "upstream_version": manifest.upstream_version,
419
+ }
420
+ env_hash = sha256(canonical_json(environment).encode()).hexdigest()
421
+ observations, proofs = [], {}
422
+ unavailable = driver.unavailable_reason()
423
+ for probe_id, capability in _PROBES:
424
+ declaration = next(item for item in manifest.capabilities if item.name == capability)
425
+ reason = unavailable or ("live_probe_required" if declaration.requires_live else None)
426
+ reference = f"artifacts/{probe_id}.json"
427
+ if reason:
428
+ proof = {
429
+ "schema_version": "1.0",
430
+ "probe_id": probe_id,
431
+ "probe_version": PROBE_VERSION,
432
+ "status": "skipped",
433
+ "reason": reason,
434
+ }
435
+ verdict = "skipped"
436
+ else:
437
+ clock = FakeClock()
438
+ try:
439
+ with (
440
+ patch("agentloop.harness.perf_counter_ns", clock.ns),
441
+ patch("agentloop.harness.utc_now_iso", clock.utc),
442
+ ):
443
+ values = _probe(driver, probe_id)
444
+ verdict = "verified_supported" if values["passed"] else "verified_failed"
445
+ proof = {
446
+ "schema_version": "1.0",
447
+ "probe_id": probe_id,
448
+ "probe_version": PROBE_VERSION,
449
+ "status": verdict,
450
+ "clock_source": "synthetic_monotonic_clock",
451
+ **values,
452
+ }
453
+ except Exception as exc:
454
+ verdict, reason = "verified_failed", "driver_or_contract_failure"
455
+ proof = {
456
+ "schema_version": "1.0",
457
+ "probe_id": probe_id,
458
+ "probe_version": PROBE_VERSION,
459
+ "status": verdict,
460
+ "error_type": type(exc).__name__,
461
+ }
462
+ proofs[reference] = proof
463
+ observations.append(
464
+ _probe_observation(
465
+ capability=capability,
466
+ verdict=verdict,
467
+ probe_id=probe_id,
468
+ probe_version=PROBE_VERSION,
469
+ tested_adapter_version=manifest.adapter_version,
470
+ tested_transport=manifest.transport,
471
+ manifest_sha256=manifest.fingerprint,
472
+ evidence_ref=reference,
473
+ evidence_sha256=sha256((canonical_json(proof) + "\n").encode()).hexdigest(),
474
+ environment_fingerprint=env_hash,
475
+ test_timestamp=stamp,
476
+ boundaries=()
477
+ if verdict == "skipped"
478
+ else (
479
+ ("model", "wrapped_callable")
480
+ if probe_id == "premodel-deny"
481
+ else ("tool", "wrapped_callable")
482
+ ),
483
+ source="offline_behavioral",
484
+ reason=reason,
485
+ )
486
+ )
487
+ report = {
488
+ **capability_report(manifest, observations),
489
+ "mode": "offline",
490
+ "test_timestamp": stamp,
491
+ "environment": environment,
492
+ "environment_fingerprint": env_hash,
493
+ "probes": [item.to_dict() for item in observations],
494
+ "network_used": False if type(driver) is OfflineDriver else None,
495
+ "vendor_runtime_used": False if type(driver) is OfflineDriver else None,
496
+ "driver_trust": "builtin_local_protocol"
497
+ if type(driver) is OfflineDriver
498
+ else "explicit_trusted_python_driver; network/process side effects not independently attested",
499
+ "installed_sdk_probe_scope": "explicit adapter interface with protocol-compatible fakes",
500
+ "live_features": "skipped; not inferred from fake requests",
501
+ "status": "failed"
502
+ if any(item.verdict == "verified_failed" for item in observations)
503
+ else "passed"
504
+ if any(item.verdict == "verified_supported" for item in observations)
505
+ else "skipped",
506
+ }
507
+ return BenchResult(tuple(observations), canonical_json(report), canonical_json(proofs))