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 @@
|
|
|
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))
|