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,585 @@
|
|
|
1
|
+
"""Versioned, payload-free harness evidence carried by existing trace artifacts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from hashlib import sha256
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from agentloop.budget_types import BudgetSnapshot, BudgetValidationError
|
|
14
|
+
|
|
15
|
+
EVIDENCE_VERSION = "1.1"
|
|
16
|
+
_IDENTITY_VERSION = "1.0"
|
|
17
|
+
_COMPARISON_VERSION = "1.0"
|
|
18
|
+
METADATA_KEY = "agentloop.harness"
|
|
19
|
+
_ACTIONS = {"continue", "deny", "stop", "escalate"}
|
|
20
|
+
_OUTCOMES = {"proposed", "applied", "rejected", "failed", "no_op"}
|
|
21
|
+
_HASH = re.compile(r"[a-f0-9]{64}\Z")
|
|
22
|
+
_DECISION_ID = re.compile(r"hdec_[a-f0-9]{64}\Z")
|
|
23
|
+
_LABEL = re.compile(r"[A-Za-z0-9_.:-]{1,96}\Z")
|
|
24
|
+
_FIELDS = {
|
|
25
|
+
"schema_version",
|
|
26
|
+
"decision_id",
|
|
27
|
+
"hook_id",
|
|
28
|
+
"run_id",
|
|
29
|
+
"call_id",
|
|
30
|
+
"branch_id",
|
|
31
|
+
"trace_id",
|
|
32
|
+
"span_id",
|
|
33
|
+
"policy_id",
|
|
34
|
+
"policy_version",
|
|
35
|
+
"policy_config_hash",
|
|
36
|
+
"harness_config_hash",
|
|
37
|
+
"boundary",
|
|
38
|
+
"phase",
|
|
39
|
+
"mode",
|
|
40
|
+
"requested_action",
|
|
41
|
+
"resolved_action",
|
|
42
|
+
"outcome",
|
|
43
|
+
"reason_code",
|
|
44
|
+
"dispatched",
|
|
45
|
+
"execution_status",
|
|
46
|
+
"evidence_refs",
|
|
47
|
+
"conflicting_decision_ids",
|
|
48
|
+
"retry_of",
|
|
49
|
+
"timing",
|
|
50
|
+
"budget_snapshot",
|
|
51
|
+
"evaluation_status",
|
|
52
|
+
"origin",
|
|
53
|
+
"hook_sequence",
|
|
54
|
+
"policy_order",
|
|
55
|
+
}
|
|
56
|
+
_CURRENT_FIELDS = _FIELDS | {"feedback"}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class HarnessEvidenceError(ValueError):
|
|
60
|
+
"""Harness evidence is malformed, unsupported, or conflicts with an earlier record."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def canonical_json(value: Any) -> str:
|
|
64
|
+
"""Own a finite JSON snapshot without retaining caller-owned containers."""
|
|
65
|
+
try:
|
|
66
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False)
|
|
67
|
+
except (TypeError, ValueError, RecursionError):
|
|
68
|
+
raise HarnessEvidenceError("harness evidence must contain finite JSON values") from None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def hook_id(run_id: str, call_id: str, boundary: str, phase: str) -> str:
|
|
72
|
+
identity = [_IDENTITY_VERSION, run_id, call_id, boundary, phase]
|
|
73
|
+
return "hook_" + sha256(canonical_json(identity).encode()).hexdigest()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def decision_id(run_id: str, call_id: str, boundary: str, phase: str, policy_id: str) -> str:
|
|
77
|
+
"""Return one stable identity for a policy at a particular call boundary."""
|
|
78
|
+
identity = [_IDENTITY_VERSION, run_id, call_id, boundary, phase, policy_id]
|
|
79
|
+
return "hdec_" + sha256(canonical_json(identity).encode()).hexdigest()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def valid_decision_id(value: Any) -> bool:
|
|
83
|
+
return isinstance(value, str) and _DECISION_ID.fullmatch(value) is not None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def empty_evidence() -> dict[str, Any]:
|
|
87
|
+
return {
|
|
88
|
+
"schema_version": EVIDENCE_VERSION,
|
|
89
|
+
"policies": {},
|
|
90
|
+
"decisions": {},
|
|
91
|
+
"capture_errors": [],
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _envelope(value: Any) -> dict[str, Any]:
|
|
96
|
+
if (
|
|
97
|
+
not isinstance(value, dict)
|
|
98
|
+
or set(value) != {"schema_version", "policies", "decisions", "capture_errors"}
|
|
99
|
+
or not isinstance(value.get("schema_version"), str)
|
|
100
|
+
or value.get("schema_version") not in {"1.0", EVIDENCE_VERSION}
|
|
101
|
+
or not isinstance(value.get("policies"), dict)
|
|
102
|
+
or not isinstance(value.get("decisions"), dict)
|
|
103
|
+
or not isinstance(value.get("capture_errors"), list)
|
|
104
|
+
):
|
|
105
|
+
raise HarnessEvidenceError("invalid or unsupported harness evidence envelope")
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class HarnessDecisionRecord:
|
|
111
|
+
"""Immutable decision evidence; exports are independent JSON copies."""
|
|
112
|
+
|
|
113
|
+
_json: str
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> dict[str, Any]:
|
|
116
|
+
return json.loads(self._json)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, value: dict[str, Any]) -> HarnessDecisionRecord:
|
|
120
|
+
if not isinstance(value, dict):
|
|
121
|
+
raise HarnessEvidenceError("invalid harness decision fields")
|
|
122
|
+
version = value.get("schema_version")
|
|
123
|
+
if not isinstance(version, str) or version not in {"1.0", EVIDENCE_VERSION}:
|
|
124
|
+
raise HarnessEvidenceError("unsupported harness decision version")
|
|
125
|
+
if set(value) != (_FIELDS if version == "1.0" else _CURRENT_FIELDS):
|
|
126
|
+
raise HarnessEvidenceError("invalid harness decision fields")
|
|
127
|
+
feedback = value.get("feedback")
|
|
128
|
+
if feedback is not None and (
|
|
129
|
+
not isinstance(feedback, str) or not feedback or len(feedback) > 256
|
|
130
|
+
):
|
|
131
|
+
raise HarnessEvidenceError("invalid bounded decision feedback")
|
|
132
|
+
for key in ("run_id", "call_id", "branch_id", "policy_id", "policy_version", "reason_code"):
|
|
133
|
+
if not isinstance(value[key], str) or not value[key]:
|
|
134
|
+
raise HarnessEvidenceError(
|
|
135
|
+
"decision identity and reason fields must be nonempty strings"
|
|
136
|
+
)
|
|
137
|
+
for key in ("policy_id", "policy_version", "reason_code"):
|
|
138
|
+
if not _LABEL.fullmatch(value[key]):
|
|
139
|
+
raise HarnessEvidenceError(
|
|
140
|
+
"policy identity and reasons must be bounded identifiers"
|
|
141
|
+
)
|
|
142
|
+
for key in ("trace_id", "span_id"):
|
|
143
|
+
if value[key] is not None and (not isinstance(value[key], str) or not value[key]):
|
|
144
|
+
raise HarnessEvidenceError("trace/span references must be strings or null")
|
|
145
|
+
for key in ("policy_config_hash", "harness_config_hash"):
|
|
146
|
+
if not isinstance(value[key], str) or not _HASH.fullmatch(value[key]):
|
|
147
|
+
raise HarnessEvidenceError("configuration hashes must be SHA-256 hex digests")
|
|
148
|
+
for key in (
|
|
149
|
+
"boundary",
|
|
150
|
+
"phase",
|
|
151
|
+
"mode",
|
|
152
|
+
"origin",
|
|
153
|
+
"requested_action",
|
|
154
|
+
"resolved_action",
|
|
155
|
+
"outcome",
|
|
156
|
+
"execution_status",
|
|
157
|
+
"evaluation_status",
|
|
158
|
+
):
|
|
159
|
+
if not isinstance(value[key], str):
|
|
160
|
+
raise HarnessEvidenceError("decision enum fields must be strings")
|
|
161
|
+
if value["boundary"] not in {"model", "tool", "iteration", "completion"} or value[
|
|
162
|
+
"phase"
|
|
163
|
+
] not in {"before", "after"}:
|
|
164
|
+
raise HarnessEvidenceError("unsupported decision boundary")
|
|
165
|
+
if value["mode"] not in {"shadow", "enforce"} or value["origin"] not in {
|
|
166
|
+
"policy",
|
|
167
|
+
"harness",
|
|
168
|
+
}:
|
|
169
|
+
raise HarnessEvidenceError("unsupported decision mode or origin")
|
|
170
|
+
if value["requested_action"] not in _ACTIONS or value["resolved_action"] not in _ACTIONS:
|
|
171
|
+
raise HarnessEvidenceError("unsupported decision action")
|
|
172
|
+
if value["outcome"] not in _OUTCOMES:
|
|
173
|
+
raise HarnessEvidenceError("unsupported decision outcome")
|
|
174
|
+
if value["mode"] == "shadow" and value["outcome"] not in {"proposed", "failed", "no_op"}:
|
|
175
|
+
raise HarnessEvidenceError("shadow decisions cannot be applied or rejected controls")
|
|
176
|
+
if value["outcome"] == "applied" and (
|
|
177
|
+
value["requested_action"] == "continue"
|
|
178
|
+
or value["requested_action"] != value["resolved_action"]
|
|
179
|
+
):
|
|
180
|
+
raise HarnessEvidenceError("applied decisions must match the resolved control action")
|
|
181
|
+
if type(value["dispatched"]) is not bool or (
|
|
182
|
+
value["phase"] == "before" and value["dispatched"]
|
|
183
|
+
):
|
|
184
|
+
raise HarnessEvidenceError("invalid dispatch evidence")
|
|
185
|
+
if value["execution_status"] not in {
|
|
186
|
+
"pending",
|
|
187
|
+
"ok",
|
|
188
|
+
"error",
|
|
189
|
+
"cancelled",
|
|
190
|
+
"closed",
|
|
191
|
+
"denied",
|
|
192
|
+
}:
|
|
193
|
+
raise HarnessEvidenceError("unsupported execution status")
|
|
194
|
+
if value["evaluation_status"] != "unverified":
|
|
195
|
+
raise HarnessEvidenceError("a decision alone is not a measured evaluation")
|
|
196
|
+
if value["hook_sequence"] is not None and (
|
|
197
|
+
type(value["hook_sequence"]) is not int or value["hook_sequence"] < 0
|
|
198
|
+
):
|
|
199
|
+
raise HarnessEvidenceError("hook sequence must be nonnegative or unavailable")
|
|
200
|
+
if type(value["policy_order"]) is not int or value["policy_order"] < 0:
|
|
201
|
+
raise HarnessEvidenceError("policy order must be nonnegative")
|
|
202
|
+
if value["budget_snapshot"] is not None:
|
|
203
|
+
try:
|
|
204
|
+
budget = BudgetSnapshot.from_dict(value["budget_snapshot"]).to_dict()
|
|
205
|
+
if value["mode"] == "shadow" and any(
|
|
206
|
+
budget[key] == "best_effort"
|
|
207
|
+
for key in ("spend_enforcement", "token_enforcement")
|
|
208
|
+
):
|
|
209
|
+
raise BudgetValidationError("shadow budgets cannot claim enforcement")
|
|
210
|
+
except (BudgetValidationError, TypeError):
|
|
211
|
+
raise HarnessEvidenceError("invalid or unsupported budget snapshot") from None
|
|
212
|
+
for key in ("evidence_refs", "conflicting_decision_ids"):
|
|
213
|
+
items = value[key]
|
|
214
|
+
if (
|
|
215
|
+
not isinstance(items, list)
|
|
216
|
+
or any(not isinstance(item, str) or not item for item in items)
|
|
217
|
+
or items != sorted(set(items))
|
|
218
|
+
):
|
|
219
|
+
raise HarnessEvidenceError("decision references must be sorted unique IDs")
|
|
220
|
+
refs = value["conflicting_decision_ids"] + (
|
|
221
|
+
[value["retry_of"]] if value["retry_of"] is not None else []
|
|
222
|
+
)
|
|
223
|
+
if any(not valid_decision_id(item) or item == value["decision_id"] for item in refs):
|
|
224
|
+
raise HarnessEvidenceError("invalid related decision identity")
|
|
225
|
+
identity_args = (value["run_id"], value["call_id"], value["boundary"], value["phase"])
|
|
226
|
+
if value["decision_id"] != decision_id(*identity_args, value["policy_id"]) or value[
|
|
227
|
+
"hook_id"
|
|
228
|
+
] != hook_id(*identity_args):
|
|
229
|
+
raise HarnessEvidenceError("decision identity does not match its source")
|
|
230
|
+
timing = value["timing"]
|
|
231
|
+
if not isinstance(timing, dict) or set(timing) != {
|
|
232
|
+
"started_at",
|
|
233
|
+
"duration_ms",
|
|
234
|
+
"hook_duration_ms",
|
|
235
|
+
}:
|
|
236
|
+
raise HarnessEvidenceError("invalid decision timing")
|
|
237
|
+
if timing["started_at"] is not None:
|
|
238
|
+
try:
|
|
239
|
+
parsed = datetime.fromisoformat(timing["started_at"].replace("Z", "+00:00"))
|
|
240
|
+
if parsed.tzinfo is None:
|
|
241
|
+
raise ValueError
|
|
242
|
+
except (AttributeError, TypeError, ValueError):
|
|
243
|
+
raise HarnessEvidenceError("decision timestamp must include a timezone") from None
|
|
244
|
+
for key in ("duration_ms", "hook_duration_ms"):
|
|
245
|
+
item = timing[key]
|
|
246
|
+
if item is not None:
|
|
247
|
+
try:
|
|
248
|
+
valid = type(item) in {int, float} and math.isfinite(float(item)) and item >= 0
|
|
249
|
+
except (OverflowError, ValueError):
|
|
250
|
+
valid = False
|
|
251
|
+
if not valid:
|
|
252
|
+
raise HarnessEvidenceError("decision durations must be finite and nonnegative")
|
|
253
|
+
return cls(canonical_json(value))
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def policy_snapshot(policy: Any, *, include_configuration: bool) -> dict[str, Any]:
|
|
257
|
+
"""Capture declarations; raw configuration requires explicit caller opt-in."""
|
|
258
|
+
|
|
259
|
+
# Policy's configuration is already deeply frozen and JSON-validated. Convert
|
|
260
|
+
# its mapping/tuple containers only when the caller requests raw capture.
|
|
261
|
+
def native(value):
|
|
262
|
+
from collections.abc import Mapping
|
|
263
|
+
|
|
264
|
+
if isinstance(value, Mapping):
|
|
265
|
+
return {key: native(item) for key, item in value.items()}
|
|
266
|
+
if isinstance(value, tuple):
|
|
267
|
+
return [native(item) for item in value]
|
|
268
|
+
return value
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
"policy_id": policy.policy_id,
|
|
272
|
+
"version": policy.version,
|
|
273
|
+
"config_hash": policy.config_hash,
|
|
274
|
+
"priority": policy.priority,
|
|
275
|
+
"hooks": [
|
|
276
|
+
{"boundary": hook.boundary, "phase": hook.phase} for hook in sorted(policy.hooks)
|
|
277
|
+
],
|
|
278
|
+
"actions": sorted(policy.actions),
|
|
279
|
+
"configuration_capture": "explicit" if include_configuration else "redacted",
|
|
280
|
+
"configuration": native(policy.configuration) if include_configuration else None,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def records_for_hook(
|
|
285
|
+
result: Any, config_hash: str, system_snapshot: dict
|
|
286
|
+
) -> tuple[HarnessDecisionRecord, ...]:
|
|
287
|
+
"""Resolve each proposal separately while sharing one hook/effect identity."""
|
|
288
|
+
identities = {
|
|
289
|
+
item.policy_id: decision_id(
|
|
290
|
+
result.run_id, result.call_id, result.hook.boundary, result.hook.phase, item.policy_id
|
|
291
|
+
)
|
|
292
|
+
for item in result.proposals
|
|
293
|
+
}
|
|
294
|
+
rows = result.proposals or (None,)
|
|
295
|
+
records = []
|
|
296
|
+
for policy_order, proposal in enumerate(rows):
|
|
297
|
+
action = proposal.decision.action if proposal is not None else result.action
|
|
298
|
+
if proposal is not None and proposal.failed:
|
|
299
|
+
outcome = "failed"
|
|
300
|
+
elif action == "continue":
|
|
301
|
+
outcome = "no_op"
|
|
302
|
+
elif result.mode == "shadow":
|
|
303
|
+
outcome = "proposed"
|
|
304
|
+
elif result.applied and action == result.action:
|
|
305
|
+
outcome = "applied"
|
|
306
|
+
else:
|
|
307
|
+
outcome = "rejected"
|
|
308
|
+
policy_id = proposal.policy_id if proposal is not None else system_snapshot["policy_id"]
|
|
309
|
+
record_id = decision_id(
|
|
310
|
+
result.run_id, result.call_id, result.hook.boundary, result.hook.phase, policy_id
|
|
311
|
+
)
|
|
312
|
+
records.append(
|
|
313
|
+
HarnessDecisionRecord.from_dict(
|
|
314
|
+
{
|
|
315
|
+
"schema_version": EVIDENCE_VERSION,
|
|
316
|
+
"decision_id": record_id,
|
|
317
|
+
"hook_id": hook_id(
|
|
318
|
+
result.run_id, result.call_id, result.hook.boundary, result.hook.phase
|
|
319
|
+
),
|
|
320
|
+
"run_id": result.run_id,
|
|
321
|
+
"call_id": result.call_id,
|
|
322
|
+
"branch_id": result.branch_id,
|
|
323
|
+
"trace_id": result.trace_id,
|
|
324
|
+
"span_id": result.parent_span_id,
|
|
325
|
+
"policy_id": policy_id,
|
|
326
|
+
"policy_version": proposal.policy_version
|
|
327
|
+
if proposal is not None
|
|
328
|
+
else system_snapshot["version"],
|
|
329
|
+
"policy_config_hash": proposal.config_hash
|
|
330
|
+
if proposal is not None
|
|
331
|
+
else system_snapshot["config_hash"],
|
|
332
|
+
"harness_config_hash": config_hash,
|
|
333
|
+
"boundary": result.hook.boundary,
|
|
334
|
+
"phase": result.hook.phase,
|
|
335
|
+
"mode": result.mode,
|
|
336
|
+
"requested_action": action,
|
|
337
|
+
"resolved_action": result.action,
|
|
338
|
+
"outcome": outcome,
|
|
339
|
+
"reason_code": proposal.decision.reason_code
|
|
340
|
+
if proposal is not None
|
|
341
|
+
else "run_stopped"
|
|
342
|
+
if result.action == "stop"
|
|
343
|
+
else "no_policy_change",
|
|
344
|
+
"dispatched": result.dispatched,
|
|
345
|
+
"execution_status": result.status,
|
|
346
|
+
"evidence_refs": sorted(set(proposal.decision.evidence_refs))
|
|
347
|
+
if proposal is not None
|
|
348
|
+
else [],
|
|
349
|
+
"conflicting_decision_ids": sorted(
|
|
350
|
+
identities[item.policy_id]
|
|
351
|
+
for item in result.proposals
|
|
352
|
+
if item.policy_id != policy_id
|
|
353
|
+
and item.decision.action != action
|
|
354
|
+
and item.decision.action != "continue"
|
|
355
|
+
and action != "continue"
|
|
356
|
+
),
|
|
357
|
+
"retry_of": proposal.decision.retry_of if proposal is not None else None,
|
|
358
|
+
"timing": {
|
|
359
|
+
"started_at": proposal.started_at
|
|
360
|
+
if proposal is not None
|
|
361
|
+
else result.started_at,
|
|
362
|
+
"duration_ms": proposal.duration_ms if proposal is not None else 0.0,
|
|
363
|
+
"hook_duration_ms": result.duration_ms,
|
|
364
|
+
},
|
|
365
|
+
"budget_snapshot": (
|
|
366
|
+
proposal.decision.budget_snapshot.to_dict()
|
|
367
|
+
if proposal is not None and proposal.decision.budget_snapshot is not None
|
|
368
|
+
else None
|
|
369
|
+
),
|
|
370
|
+
"evaluation_status": "unverified",
|
|
371
|
+
"origin": "policy" if proposal is not None else "harness",
|
|
372
|
+
"feedback": proposal.decision.feedback if proposal is not None else None,
|
|
373
|
+
"hook_sequence": result.sequence,
|
|
374
|
+
"policy_order": policy_order,
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
return tuple(records)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def append_records(
|
|
382
|
+
envelope: dict, records: tuple[HarnessDecisionRecord, ...], snapshots: dict[str, dict]
|
|
383
|
+
) -> None:
|
|
384
|
+
"""Append only new identities in O(new records); exact retries are idempotent."""
|
|
385
|
+
target = _envelope(envelope)
|
|
386
|
+
updates = []
|
|
387
|
+
for record in records:
|
|
388
|
+
payload = record.to_dict()
|
|
389
|
+
identity, config_hash = payload["decision_id"], payload["policy_config_hash"]
|
|
390
|
+
snapshot = snapshots.get(config_hash)
|
|
391
|
+
if (
|
|
392
|
+
not isinstance(snapshot, dict)
|
|
393
|
+
or snapshot.get("config_hash") != config_hash
|
|
394
|
+
or snapshot.get("policy_id") != payload["policy_id"]
|
|
395
|
+
or snapshot.get("version") != payload["policy_version"]
|
|
396
|
+
):
|
|
397
|
+
raise HarnessEvidenceError("decision policy snapshot is missing or incompatible")
|
|
398
|
+
snapshot_json = canonical_json(snapshot)
|
|
399
|
+
existing = target["decisions"].get(identity)
|
|
400
|
+
if existing is not None and canonical_json(existing) != record._json:
|
|
401
|
+
raise HarnessEvidenceError("decision identity already has different evidence")
|
|
402
|
+
saved_policy = target["policies"].get(config_hash)
|
|
403
|
+
if saved_policy is not None and canonical_json(saved_policy) != snapshot_json:
|
|
404
|
+
raise HarnessEvidenceError("policy snapshot already has different evidence")
|
|
405
|
+
updates.append((identity, config_hash, payload, snapshot_json))
|
|
406
|
+
for identity, config_hash, payload, snapshot_json in updates:
|
|
407
|
+
if config_hash not in target["policies"]:
|
|
408
|
+
target["policies"].setdefault(config_hash, json.loads(snapshot_json))
|
|
409
|
+
target["decisions"].setdefault(identity, payload)
|
|
410
|
+
if payload["schema_version"] == EVIDENCE_VERSION:
|
|
411
|
+
target["schema_version"] = EVIDENCE_VERSION
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def validate_evidence(value: Any, *, trace_id: str | None = None) -> dict[str, Any]:
|
|
415
|
+
"""Validate a standalone artifact or evidence belonging to one native trace."""
|
|
416
|
+
envelope = _envelope(value)
|
|
417
|
+
snapshot_fields = {
|
|
418
|
+
"policy_id",
|
|
419
|
+
"version",
|
|
420
|
+
"config_hash",
|
|
421
|
+
"priority",
|
|
422
|
+
"hooks",
|
|
423
|
+
"actions",
|
|
424
|
+
"configuration_capture",
|
|
425
|
+
"configuration",
|
|
426
|
+
}
|
|
427
|
+
for config_hash, snapshot in envelope["policies"].items():
|
|
428
|
+
if (
|
|
429
|
+
not isinstance(config_hash, str)
|
|
430
|
+
or not _HASH.fullmatch(config_hash)
|
|
431
|
+
or not isinstance(snapshot, dict)
|
|
432
|
+
or set(snapshot) != snapshot_fields
|
|
433
|
+
or snapshot["config_hash"] != config_hash
|
|
434
|
+
):
|
|
435
|
+
raise HarnessEvidenceError("invalid policy snapshot")
|
|
436
|
+
if (
|
|
437
|
+
any(
|
|
438
|
+
not isinstance(snapshot[key], str) or not snapshot[key]
|
|
439
|
+
for key in ("policy_id", "version", "configuration_capture")
|
|
440
|
+
)
|
|
441
|
+
or type(snapshot["priority"]) is not int
|
|
442
|
+
):
|
|
443
|
+
raise HarnessEvidenceError("invalid policy declaration")
|
|
444
|
+
if any(not _LABEL.fullmatch(snapshot[key]) for key in ("policy_id", "version")):
|
|
445
|
+
raise HarnessEvidenceError("invalid policy identity")
|
|
446
|
+
if snapshot["configuration_capture"] == "redacted":
|
|
447
|
+
if snapshot["configuration"] is not None:
|
|
448
|
+
raise HarnessEvidenceError("redacted policy configuration must be omitted")
|
|
449
|
+
elif snapshot["configuration_capture"] != "explicit" or not isinstance(
|
|
450
|
+
snapshot["configuration"], dict
|
|
451
|
+
):
|
|
452
|
+
raise HarnessEvidenceError("invalid policy configuration capture")
|
|
453
|
+
actions = snapshot["actions"]
|
|
454
|
+
if (
|
|
455
|
+
not isinstance(actions, list)
|
|
456
|
+
or any(not isinstance(action, str) or action not in _ACTIONS for action in actions)
|
|
457
|
+
or actions != sorted(set(actions))
|
|
458
|
+
):
|
|
459
|
+
raise HarnessEvidenceError("invalid policy action declaration")
|
|
460
|
+
if not isinstance(snapshot["hooks"], list):
|
|
461
|
+
raise HarnessEvidenceError("invalid policy hook declaration")
|
|
462
|
+
for hook in snapshot["hooks"]:
|
|
463
|
+
if (
|
|
464
|
+
not isinstance(hook, dict)
|
|
465
|
+
or set(hook) != {"boundary", "phase"}
|
|
466
|
+
or not isinstance(hook["boundary"], str)
|
|
467
|
+
or not isinstance(hook["phase"], str)
|
|
468
|
+
or hook["boundary"] not in {"model", "tool", "iteration", "completion"}
|
|
469
|
+
or hook["phase"] not in {"before", "after"}
|
|
470
|
+
):
|
|
471
|
+
raise HarnessEvidenceError("invalid policy hook declaration")
|
|
472
|
+
hooks = {}
|
|
473
|
+
orders: dict[str, set[int]] = {}
|
|
474
|
+
for identity, payload in envelope["decisions"].items():
|
|
475
|
+
record = HarnessDecisionRecord.from_dict(payload).to_dict()
|
|
476
|
+
if envelope["schema_version"] == "1.0" and record["schema_version"] != "1.0":
|
|
477
|
+
raise HarnessEvidenceError("decision version exceeds its envelope version")
|
|
478
|
+
if identity != record["decision_id"] or (
|
|
479
|
+
trace_id is not None and record["trace_id"] != trace_id
|
|
480
|
+
):
|
|
481
|
+
raise HarnessEvidenceError("decision does not belong to this trace")
|
|
482
|
+
snapshot = envelope["policies"].get(record["policy_config_hash"])
|
|
483
|
+
if (
|
|
484
|
+
not isinstance(snapshot, dict)
|
|
485
|
+
or snapshot.get("config_hash") != record["policy_config_hash"]
|
|
486
|
+
or snapshot.get("policy_id") != record["policy_id"]
|
|
487
|
+
or snapshot.get("version") != record["policy_version"]
|
|
488
|
+
):
|
|
489
|
+
raise HarnessEvidenceError("decision policy snapshot is missing or incompatible")
|
|
490
|
+
if record["outcome"] != "failed" and (
|
|
491
|
+
record["requested_action"] not in snapshot["actions"]
|
|
492
|
+
or {"boundary": record["boundary"], "phase": record["phase"]} not in snapshot["hooks"]
|
|
493
|
+
):
|
|
494
|
+
raise HarnessEvidenceError("decision contradicts its policy declaration")
|
|
495
|
+
hook_fields = {
|
|
496
|
+
key: record[key]
|
|
497
|
+
for key in (
|
|
498
|
+
"run_id",
|
|
499
|
+
"call_id",
|
|
500
|
+
"branch_id",
|
|
501
|
+
"trace_id",
|
|
502
|
+
"span_id",
|
|
503
|
+
"harness_config_hash",
|
|
504
|
+
"boundary",
|
|
505
|
+
"phase",
|
|
506
|
+
"mode",
|
|
507
|
+
"resolved_action",
|
|
508
|
+
"dispatched",
|
|
509
|
+
"execution_status",
|
|
510
|
+
"hook_sequence",
|
|
511
|
+
)
|
|
512
|
+
}
|
|
513
|
+
hook_fields["duration_ms"] = record["timing"]["hook_duration_ms"]
|
|
514
|
+
if hooks.setdefault(record["hook_id"], hook_fields) != hook_fields:
|
|
515
|
+
raise HarnessEvidenceError("decisions disagree about their shared hook")
|
|
516
|
+
seen_orders = orders.setdefault(record["hook_id"], set())
|
|
517
|
+
if record["policy_order"] in seen_orders:
|
|
518
|
+
raise HarnessEvidenceError("duplicate policy order within a hook")
|
|
519
|
+
seen_orders.add(record["policy_order"])
|
|
520
|
+
for related in record["conflicting_decision_ids"]:
|
|
521
|
+
other = envelope["decisions"].get(related)
|
|
522
|
+
if isinstance(other, dict) and other.get("hook_id") != record["hook_id"]:
|
|
523
|
+
raise HarnessEvidenceError("conflict references must belong to the same hook")
|
|
524
|
+
for error in envelope["capture_errors"]:
|
|
525
|
+
if (
|
|
526
|
+
not isinstance(error, dict)
|
|
527
|
+
or set(error) != {"call_id", "phase", "category"}
|
|
528
|
+
or not isinstance(error["call_id"], str)
|
|
529
|
+
or not isinstance(error["phase"], str)
|
|
530
|
+
or error["phase"] not in {"before", "after"}
|
|
531
|
+
or error["category"] != "trace_evidence_error"
|
|
532
|
+
):
|
|
533
|
+
raise HarnessEvidenceError("invalid capture diagnostic")
|
|
534
|
+
return json.loads(canonical_json(envelope))
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
def read_evidence(trace: Any) -> dict[str, Any] | None:
|
|
538
|
+
"""Validate/copy the known envelope without rewriting unknown native metadata."""
|
|
539
|
+
if METADATA_KEY not in trace.metadata:
|
|
540
|
+
return None
|
|
541
|
+
return validate_evidence(trace.metadata[METADATA_KEY], trace_id=trace.run_id)
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def comparison_evidence(baseline: Any, candidate: Any) -> dict[str, Any] | None:
|
|
545
|
+
"""Snapshot decision evidence only inside a real baseline/candidate comparison."""
|
|
546
|
+
before, after = read_evidence(baseline), read_evidence(candidate)
|
|
547
|
+
if before is None and after is None:
|
|
548
|
+
return None
|
|
549
|
+
applied = (
|
|
550
|
+
[]
|
|
551
|
+
if after is None
|
|
552
|
+
else sorted(
|
|
553
|
+
identity
|
|
554
|
+
for identity, record in after["decisions"].items()
|
|
555
|
+
if record["mode"] == "enforce"
|
|
556
|
+
and record["outcome"] == "applied"
|
|
557
|
+
and record["origin"] == "policy"
|
|
558
|
+
)
|
|
559
|
+
)
|
|
560
|
+
unresolved = sorted(
|
|
561
|
+
{
|
|
562
|
+
reference
|
|
563
|
+
for envelope in (before, after)
|
|
564
|
+
if envelope is not None
|
|
565
|
+
for record in envelope["decisions"].values()
|
|
566
|
+
for reference in record["conflicting_decision_ids"]
|
|
567
|
+
+ ([record["retry_of"]] if record["retry_of"] else [])
|
|
568
|
+
if reference not in envelope["decisions"]
|
|
569
|
+
}
|
|
570
|
+
)
|
|
571
|
+
return {
|
|
572
|
+
"schema_version": _COMPARISON_VERSION,
|
|
573
|
+
"baseline_run_id": baseline.run_id,
|
|
574
|
+
"candidate_run_id": candidate.run_id,
|
|
575
|
+
"comparison_status": "observed_pair",
|
|
576
|
+
"capture_status": "known_incomplete"
|
|
577
|
+
if unresolved
|
|
578
|
+
or any(envelope["capture_errors"] for envelope in (before, after) if envelope is not None)
|
|
579
|
+
else "no_reported_gaps",
|
|
580
|
+
"individual_policy_effect": "unverified",
|
|
581
|
+
"unresolved_decision_ids": unresolved,
|
|
582
|
+
"applied_candidate_decision_ids": applied,
|
|
583
|
+
"baseline": before,
|
|
584
|
+
"candidate": after,
|
|
585
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Per-invocation usage observation that never retains a raw result or chunk."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from agentloop.budget_types import ResourceUsage
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UsageTracker:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
reader: Callable[[Any], ResourceUsage | None] | None,
|
|
16
|
+
invoke: Callable,
|
|
17
|
+
error_reader: Callable[[BaseException], ResourceUsage | None] | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
self.reader = reader
|
|
20
|
+
self.invoke = invoke
|
|
21
|
+
self.error_reader = error_reader
|
|
22
|
+
self.latest: ResourceUsage | None = None
|
|
23
|
+
self.error: str | None = None
|
|
24
|
+
|
|
25
|
+
def observe(self, value: Any) -> None:
|
|
26
|
+
self._observe(value, self.reader)
|
|
27
|
+
|
|
28
|
+
def observe_error(self, error: BaseException) -> None:
|
|
29
|
+
"""Retain available failure usage without replacing the original failure."""
|
|
30
|
+
try:
|
|
31
|
+
self._observe(error, self.error_reader)
|
|
32
|
+
except BaseException:
|
|
33
|
+
self.error = "collector_cancelled"
|
|
34
|
+
|
|
35
|
+
def _observe(self, value: Any, reader: Callable | None) -> None:
|
|
36
|
+
if reader is None:
|
|
37
|
+
return
|
|
38
|
+
try:
|
|
39
|
+
report = self.invoke(reader, value)
|
|
40
|
+
if report is None:
|
|
41
|
+
return
|
|
42
|
+
if type(report) is not ResourceUsage:
|
|
43
|
+
if inspect.iscoroutine(report):
|
|
44
|
+
report.close()
|
|
45
|
+
self.error = "invalid_usage"
|
|
46
|
+
return
|
|
47
|
+
except Exception:
|
|
48
|
+
self.error = "collector_error"
|
|
49
|
+
return
|
|
50
|
+
except BaseException:
|
|
51
|
+
self.error = "collector_cancelled"
|
|
52
|
+
raise
|
|
53
|
+
# Usage updates are snapshots, not increments. In particular, successive
|
|
54
|
+
# cumulative stream usage chunks must never be added to one another.
|
|
55
|
+
self.latest = report
|
|
56
|
+
self.error = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ObservedGenerator:
|
|
60
|
+
"""Delegate the complete generator protocol while observing returned chunks."""
|
|
61
|
+
|
|
62
|
+
def __init__(self, source: Any, tracker: UsageTracker) -> None:
|
|
63
|
+
self.source = source
|
|
64
|
+
self.tracker = tracker
|
|
65
|
+
|
|
66
|
+
def __iter__(self):
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
def _advance(self, operation: Callable, *args: Any):
|
|
70
|
+
try:
|
|
71
|
+
value = operation(*args)
|
|
72
|
+
except StopIteration as stopped:
|
|
73
|
+
if stopped.value is not None:
|
|
74
|
+
self.tracker.observe(stopped.value)
|
|
75
|
+
raise
|
|
76
|
+
self.tracker.observe(value)
|
|
77
|
+
return value
|
|
78
|
+
|
|
79
|
+
def __next__(self):
|
|
80
|
+
return self._advance(self.source.__next__)
|
|
81
|
+
|
|
82
|
+
def send(self, value):
|
|
83
|
+
return self._advance(self.source.send, value)
|
|
84
|
+
|
|
85
|
+
def throw(self, *args):
|
|
86
|
+
if len(args) == 3 and isinstance(args[1], BaseException):
|
|
87
|
+
return self._advance(self.source.throw, args[1].with_traceback(args[2]))
|
|
88
|
+
return self._advance(self.source.throw, *args)
|
|
89
|
+
|
|
90
|
+
def close(self):
|
|
91
|
+
return self.source.close()
|