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
agentloop/client.py ADDED
@@ -0,0 +1,228 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Literal
11
+
12
+ from agentloop.tracer import AgentTrace
13
+
14
+ CredentialScope = Literal["project", "admin"]
15
+
16
+
17
+ class AgentLoopClientError(RuntimeError):
18
+ pass
19
+
20
+
21
+ @dataclass
22
+ class AgentLoopClient:
23
+ """Dependency-free API v1 client; base_url is the origin or mount path, without /v1."""
24
+
25
+ base_url: str = "http://127.0.0.1:8000"
26
+ api_key: str | None = None
27
+ admin_api_key: str | None = None
28
+ timeout_s: float = 10.0
29
+
30
+ @classmethod
31
+ def from_env(cls) -> "AgentLoopClient":
32
+ return cls(
33
+ base_url=os.getenv("AGENTLOOP_API_URL", "http://127.0.0.1:8000"),
34
+ api_key=os.getenv("AGENTLOOP_API_KEY"),
35
+ admin_api_key=os.getenv("AGENTLOOP_ADMIN_API_KEY"),
36
+ timeout_s=float(os.getenv("AGENTLOOP_TIMEOUT_S", "10")),
37
+ )
38
+
39
+ def health(self) -> dict[str, Any]:
40
+ return self._request("GET", "/health")
41
+
42
+ def create_api_key(self, project_id: str = "default", name: str = "default") -> dict[str, Any]:
43
+ return self._request(
44
+ "POST",
45
+ "/api-keys",
46
+ {"project_id": project_id, "name": name},
47
+ credential_scope="admin",
48
+ )
49
+
50
+ def upload_trace(self, trace: AgentTrace | str | Path | dict[str, Any]) -> dict[str, Any]:
51
+ if isinstance(trace, AgentTrace):
52
+ payload = trace.to_dict()
53
+ elif isinstance(trace, dict):
54
+ payload = trace
55
+ else:
56
+ payload = json.loads(Path(trace).read_text(encoding="utf-8"))
57
+ return self._request("POST", "/traces", payload)
58
+
59
+ def list_traces(
60
+ self,
61
+ project_id: str | None = None,
62
+ *,
63
+ page_size: int | None = None,
64
+ cursor: str | None = None,
65
+ ) -> dict[str, Any]:
66
+ params: dict[str, Any] = {}
67
+ if project_id:
68
+ params["project_id"] = project_id
69
+ if page_size is not None:
70
+ params["page_size"] = page_size
71
+ if cursor:
72
+ params["cursor"] = cursor
73
+ route = "/traces"
74
+ if params:
75
+ route += "?" + urllib.parse.urlencode(params)
76
+ return self._request("GET", route)
77
+
78
+ def usage_summary(self, project_id: str | None = None) -> dict[str, Any]:
79
+ route = "/usage"
80
+ if project_id:
81
+ route += "?" + urllib.parse.urlencode({"project_id": project_id})
82
+ return self._request("GET", route)
83
+
84
+ def get_report(self, run_id: str) -> dict[str, Any]:
85
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
86
+ return self._request("GET", f"/traces/{encoded_run_id}/report")
87
+
88
+ def get_optimization_plan(self, run_id: str) -> dict[str, Any]:
89
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
90
+ return self._request("GET", f"/traces/{encoded_run_id}/optimization")
91
+
92
+ def get_diagnosis(self, run_id: str) -> dict[str, Any]:
93
+ """Compute a diagnosis without changing the server's stored findings."""
94
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
95
+ return self._request("GET", f"/traces/{encoded_run_id}/diagnosis")
96
+
97
+ def save_diagnosis(self, run_id: str) -> dict[str, Any]:
98
+ """Recompute and persist a stored trace's diagnosis on the server."""
99
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
100
+ return self._request("POST", f"/traces/{encoded_run_id}/diagnosis")
101
+
102
+ def list_findings(
103
+ self,
104
+ project_id: str | None = None,
105
+ status: str | None = None,
106
+ *,
107
+ page_size: int | None = None,
108
+ cursor: str | None = None,
109
+ ) -> dict[str, Any]:
110
+ params: dict[str, Any] = {}
111
+ if project_id:
112
+ params["project_id"] = project_id
113
+ if status:
114
+ params["status"] = status
115
+ if page_size is not None:
116
+ params["page_size"] = page_size
117
+ if cursor:
118
+ params["cursor"] = cursor
119
+ route = "/findings"
120
+ if params:
121
+ route += "?" + urllib.parse.urlencode(params)
122
+ return self._request("GET", route)
123
+
124
+ def update_finding_status(self, run_id: str, finding_id: str, status: str) -> dict[str, Any]:
125
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
126
+ encoded_finding_id = urllib.parse.quote(finding_id, safe="")
127
+ return self._request(
128
+ "POST", f"/findings/{encoded_run_id}/{encoded_finding_id}/status", {"status": status}
129
+ )
130
+
131
+ def optimization_queue(self, project_id: str | None = None) -> dict[str, Any]:
132
+ route = "/optimization-queue"
133
+ if project_id:
134
+ route += "?" + urllib.parse.urlencode({"project_id": project_id})
135
+ return self._request("GET", route)
136
+
137
+ def github_issue_drafts(self, project_id: str | None = None, limit: int = 5) -> dict[str, Any]:
138
+ params: dict[str, Any] = {"limit": limit}
139
+ if project_id:
140
+ params["project_id"] = project_id
141
+ return self._request(
142
+ "GET", "/optimization-queue/github-issues?" + urllib.parse.urlencode(params)
143
+ )
144
+
145
+ def build_quality_report(
146
+ self,
147
+ fixtures: list[dict[str, Any]],
148
+ *,
149
+ baseline_run_id: str | None = None,
150
+ candidate_run_id: str | None = None,
151
+ min_score: float | None = None,
152
+ ) -> dict[str, Any]:
153
+ return self._request(
154
+ "POST",
155
+ "/quality-reports",
156
+ {
157
+ "fixtures": fixtures,
158
+ "baseline_run_id": baseline_run_id,
159
+ "candidate_run_id": candidate_run_id,
160
+ "min_score": min_score,
161
+ },
162
+ )
163
+
164
+ def get_value_report(
165
+ self,
166
+ run_id: str,
167
+ *,
168
+ runs_per_month: int | None = None,
169
+ engineer_hourly_rate_usd: float | None = None,
170
+ incident_cost_usd: float | None = None,
171
+ ) -> dict[str, Any]:
172
+ params: dict[str, Any] = {}
173
+ if runs_per_month is not None:
174
+ params["runs_per_month"] = runs_per_month
175
+ if engineer_hourly_rate_usd is not None:
176
+ params["engineer_hourly_rate_usd"] = engineer_hourly_rate_usd
177
+ if incident_cost_usd is not None:
178
+ params["incident_cost_usd"] = incident_cost_usd
179
+ encoded_run_id = urllib.parse.quote(run_id, safe="")
180
+ route = f"/traces/{encoded_run_id}/value"
181
+ if params:
182
+ route += "?" + urllib.parse.urlencode(params)
183
+ return self._request("GET", route)
184
+
185
+ def create_intervention(self, request: dict[str, Any]) -> dict[str, Any]:
186
+ return self._request("POST", "/interventions", request)
187
+
188
+ def get_intervention(self, intervention_id: str) -> dict[str, Any]:
189
+ encoded_id = urllib.parse.quote(intervention_id, safe="")
190
+ return self._request("GET", f"/interventions/{encoded_id}")
191
+
192
+ def _request(
193
+ self,
194
+ method: str,
195
+ path: str,
196
+ payload: dict[str, Any] | None = None,
197
+ *,
198
+ credential_scope: CredentialScope = "project",
199
+ ) -> dict[str, Any]:
200
+ parsed_base_url = urllib.parse.urlsplit(self.base_url)
201
+ if parsed_base_url.scheme not in {"http", "https"} or not parsed_base_url.netloc:
202
+ raise AgentLoopClientError("AgentLoop API URL must use http:// or https://")
203
+ url = self.base_url.rstrip("/") + "/v1" + path
204
+ body = None if payload is None else json.dumps(payload).encode("utf-8")
205
+ headers = {"Accept": "application/json"}
206
+ if payload is not None:
207
+ headers["Content-Type"] = "application/json"
208
+ if credential_scope == "project":
209
+ if self.api_key:
210
+ headers["X-AgentLoop-Key"] = self.api_key
211
+ elif credential_scope == "admin":
212
+ if self.admin_api_key:
213
+ headers["X-AgentLoop-Admin-Key"] = self.admin_api_key
214
+ else:
215
+ raise AgentLoopClientError(f"Unsupported credential scope: {credential_scope}")
216
+ req = urllib.request.Request(url=url, data=body, method=method, headers=headers)
217
+ try:
218
+ with urllib.request.urlopen( # nosec B310 - base URL scheme is validated above.
219
+ req, timeout=self.timeout_s
220
+ ) as response:
221
+ return json.loads(response.read().decode("utf-8"))
222
+ except urllib.error.HTTPError as exc:
223
+ detail = exc.read().decode("utf-8", errors="ignore")
224
+ raise AgentLoopClientError(f"AgentLoop API returned {exc.code}: {detail}") from exc
225
+ except urllib.error.URLError as exc:
226
+ raise AgentLoopClientError(
227
+ f"Could not reach AgentLoop API at {url}: {exc.reason}"
228
+ ) from exc
@@ -0,0 +1,345 @@
1
+ """Single-use, bounded before-finish verification on a shared harness run."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from asyncio import CancelledError
6
+ from contextvars import ContextVar
7
+ from copy import deepcopy
8
+ from dataclasses import dataclass, replace
9
+ from threading import Event, RLock
10
+ from time import monotonic
11
+ from uuid import uuid4
12
+
13
+ from agentloop.completion_types import (
14
+ COMPLETION_KEY,
15
+ COMPLETION_VERSION,
16
+ CheckResult,
17
+ CompletionCandidate,
18
+ CompletionConfig,
19
+ CompletionResult,
20
+ RepairBackend,
21
+ VerificationContext,
22
+ VerificationFeedback,
23
+ )
24
+ from agentloop.context_types import fingerprint
25
+ from agentloop.harness import (
26
+ Decision,
27
+ HarnessControlError,
28
+ HarnessDeniedError,
29
+ HarnessRun,
30
+ Hook,
31
+ Policy,
32
+ )
33
+ from agentloop.tracer import current_trace
34
+
35
+ _ACTIVE = ContextVar("agentloop_completion", default=None)
36
+ _EVIDENCE_LOCK = RLock()
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class _CompletionAdmission:
41
+ config: CompletionConfig
42
+
43
+ def __call__(self, context):
44
+ if context.branch_id != self.config.branch_id:
45
+ return Decision("continue", "completion_not_selected")
46
+ active = _ACTIVE.get()
47
+ if (
48
+ active is None
49
+ or active["used"]
50
+ or active["run"] != context.run_id
51
+ or active["config"] is not self.config
52
+ ):
53
+ return Decision("deny", "completion_adapter_required")
54
+ active["used"] = True
55
+ active["record"]["harness_call_id"] = context.call_id
56
+ active["record"]["decision_id"] = context.decision_id
57
+ return Decision(active["action"], active["reason"], (active["reference"],))
58
+
59
+
60
+ def completion_policy(config):
61
+ if type(config) is not CompletionConfig:
62
+ raise ValueError("completion policy requires CompletionConfig")
63
+ return Policy(
64
+ config.policy_id,
65
+ config.version,
66
+ _CompletionAdmission(config),
67
+ hooks=frozenset({Hook("completion")}),
68
+ actions=frozenset({"continue", "deny", "stop", "escalate"}),
69
+ priority=50,
70
+ configuration=config.to_dict(),
71
+ )
72
+
73
+
74
+ class CompletionGate:
75
+ """Verify once before the host releases a final marker; never retract a stream.
76
+
77
+ Deadlines are cooperative. Trusted callbacks must use the provided remaining
78
+ time for their I/O or subprocess timeout. Late results cannot pass.
79
+ """
80
+
81
+ def __init__(self, run, *, config, repair=None):
82
+ if not isinstance(run, HarnessRun) or type(config) is not CompletionConfig:
83
+ raise ValueError("typed run and completion configuration required")
84
+ if run.harness.config.mode != "disabled" and not any(
85
+ type(policy.evaluate) is _CompletionAdmission and policy.evaluate.config is config
86
+ for policy in run.harness.config.policies
87
+ ):
88
+ raise ValueError("register matching completion_policy before binding")
89
+ needed = {
90
+ Hook("completion"),
91
+ Hook("completion", "after"),
92
+ Hook("tool"),
93
+ Hook("tool", "after"),
94
+ }
95
+ if repair is not None:
96
+ if type(repair) is not RepairBackend:
97
+ raise ValueError("repair must be RepairBackend")
98
+ needed.update({Hook(repair.boundary), Hook(repair.boundary, "after")})
99
+ if (
100
+ "sync" not in run.harness.capabilities.execution_kinds
101
+ or not needed <= run.harness.capabilities.hooks
102
+ ):
103
+ raise ValueError("unsupported completion lifecycle")
104
+ if bool(config.max_repairs) != (repair is not None):
105
+ raise ValueError("bounded repair configuration and backend must be supplied together")
106
+ self._run, self._config, self._repair = run, config, repair
107
+ self._lock, self._used, self._records = RLock(), False, {}
108
+
109
+ @property
110
+ def run(self):
111
+ return self._run
112
+
113
+ @property
114
+ def config(self):
115
+ return self._config
116
+
117
+ def export_evidence(self):
118
+ with self._lock:
119
+ return {"schema_version": COMPLETION_VERSION, "records": deepcopy(self._records)}
120
+
121
+ def complete(self, candidate, *, cancel_event=None):
122
+ if type(candidate) is not CompletionCandidate:
123
+ raise ValueError("candidate must be CompletionCandidate")
124
+ if cancel_event is not None and not isinstance(cancel_event, Event):
125
+ raise ValueError("cancel_event must be threading.Event")
126
+ with self._lock:
127
+ if self._used:
128
+ raise ValueError("completion gate is single-use")
129
+ self._used = True
130
+ mode = self.run.harness.config.mode
131
+ if mode == "disabled":
132
+ return CompletionResult("unverified", candidate, False, 0)
133
+ identity = "completion_" + uuid4().hex
134
+ trace = current_trace()
135
+ receipt = {
136
+ "completion_id": identity,
137
+ "mode": mode,
138
+ "policy_config_hash": completion_policy(self.config).config_hash,
139
+ "attempts": [],
140
+ "repairs": [],
141
+ "status": "running",
142
+ "independent_task_evaluation": False,
143
+ "already_streamed": candidate.already_streamed,
144
+ }
145
+ envelope = None
146
+ if trace is not None:
147
+ with _EVIDENCE_LOCK:
148
+ envelope = trace.metadata.setdefault(
149
+ COMPLETION_KEY, {"schema_version": COMPLETION_VERSION, "records": {}}
150
+ )
151
+ if (
152
+ not isinstance(envelope, dict)
153
+ or envelope.get("schema_version") != COMPLETION_VERSION
154
+ or not isinstance(envelope.get("records"), dict)
155
+ or len(envelope["records"]) >= 10000
156
+ ):
157
+ raise ValueError("invalid completion trace evidence")
158
+ envelope["records"][identity] = deepcopy(receipt)
159
+ deadline = monotonic() + self.config.total_timeout_s
160
+ verification_remaining = self.config.verification_timeout_s
161
+ repairs = 0
162
+ failure = None
163
+ try:
164
+ while True:
165
+ self._cancel(cancel_event)
166
+ attempt = {
167
+ "index": len(receipt["attempts"]),
168
+ "candidate_hash": fingerprint(candidate.value),
169
+ "present": candidate.present,
170
+ "already_streamed": candidate.already_streamed,
171
+ "checks": [],
172
+ "harness_call_id": None,
173
+ "decision_id": None,
174
+ }
175
+ receipt["attempts"].append(attempt)
176
+ feedback = []
177
+ if not candidate.present:
178
+ reason = "completion_missing"
179
+ elif candidate.already_streamed:
180
+ reason = "completion_already_streamed"
181
+ elif monotonic() >= deadline or verification_remaining <= 0:
182
+ reason = "completion_timeout"
183
+ else:
184
+ started = monotonic()
185
+ context = VerificationContext(
186
+ min(deadline, started + verification_remaining), cancel_event
187
+ )
188
+ for check_index, check in enumerate(self.config.checks):
189
+ self._cancel(cancel_event)
190
+ branch = f"{identity}:check:{attempt['index']}:{check_index}"
191
+ item = {**check.declaration(), "status": "pending", "branch_id": branch}
192
+ attempt["checks"].append(item)
193
+ if context.remaining_s <= 0:
194
+ item["status"] = "timeout"
195
+ feedback.append(VerificationFeedback(check.check_id, "timeout"))
196
+ continue
197
+
198
+ def invoke_check(candidate, context, check=check):
199
+ self._cancel(cancel_event)
200
+ if context.remaining_s <= 0:
201
+ raise TimeoutError("verification admission deadline expired")
202
+ return check.invoke(candidate, context)
203
+
204
+ try:
205
+ result = self.run.wrap(
206
+ invoke_check,
207
+ boundary="tool",
208
+ branch_id=branch,
209
+ dispatch=check.dispatch,
210
+ usage_reader=check.usage_reader,
211
+ error_usage_reader=check.error_usage_reader,
212
+ )(candidate, context)
213
+ except HarnessControlError:
214
+ item["status"] = "blocked"
215
+ raise
216
+ except Exception:
217
+ item["status"] = "error"
218
+ result = None
219
+ except BaseException:
220
+ item["status"] = "cancelled"
221
+ raise
222
+ if cancel_event is not None and cancel_event.is_set():
223
+ item["status"] = "cancelled"
224
+ self._cancel(cancel_event)
225
+ detail = ""
226
+ if context.remaining_s <= 0:
227
+ item["status"] = "timeout"
228
+ elif item["status"] == "pending":
229
+ if type(result) is not CheckResult:
230
+ item["status"] = "invalid"
231
+ else:
232
+ item["status"] = "passed" if result.passed else "failed"
233
+ detail = result.feedback
234
+ item["feedback_hash"] = fingerprint(detail)
235
+ if item["status"] != "passed":
236
+ feedback.append(
237
+ VerificationFeedback(check.check_id, item["status"], detail)
238
+ )
239
+ verification_remaining -= monotonic() - started
240
+ reason = "completion_verified" if not feedback else "completion_checks_failed"
241
+ passed = reason == "completion_verified"
242
+ if not passed and not feedback:
243
+ feedback.append(VerificationFeedback("completion", reason))
244
+ can_repair = (
245
+ not passed
246
+ and reason != "completion_already_streamed"
247
+ and repairs < self.config.max_repairs
248
+ and monotonic() < deadline
249
+ and verification_remaining > 0
250
+ )
251
+ action = "continue" if passed else "deny" if can_repair else self.config.on_failure
252
+ attempt.update(action=action, reason=reason, repair_requested=can_repair)
253
+ binding = {
254
+ "used": False,
255
+ "run": self.run.run_id,
256
+ "config": self.config,
257
+ "record": attempt,
258
+ "action": action,
259
+ "reason": reason,
260
+ "reference": f"completion:{identity}:attempt:{attempt['index']}",
261
+ }
262
+ token = _ACTIVE.set(binding)
263
+ try:
264
+ self.run.wrap(
265
+ lambda: None, boundary="completion", branch_id=self.config.branch_id
266
+ )()
267
+ except HarnessDeniedError as exc:
268
+ if not can_repair or any(
269
+ proposal.decision.action != "continue"
270
+ and proposal.policy_id != self.config.policy_id
271
+ for proposal in exc.result.proposals
272
+ ):
273
+ raise
274
+ finally:
275
+ _ACTIVE.reset(token)
276
+ if mode == "shadow":
277
+ receipt["status"] = "shadow"
278
+ return CompletionResult("unverified", candidate, False, 0)
279
+ if passed:
280
+ self._cancel(cancel_event)
281
+ if monotonic() >= deadline:
282
+ raise TimeoutError("completion deadline expired before release")
283
+ receipt["status"] = "accepted"
284
+ return CompletionResult("accepted", candidate, True, repairs)
285
+ self._cancel(cancel_event)
286
+ if monotonic() >= deadline:
287
+ raise TimeoutError("completion deadline expired before repair")
288
+ branch = f"{identity}:repair:{repairs + 1}"
289
+ repair_record = {"index": repairs + 1, "status": "pending", "branch_id": branch}
290
+ receipt["repairs"].append(repair_record)
291
+ backend = self._repair
292
+ repairs += 1
293
+
294
+ def invoke_repair(candidate, feedback, context):
295
+ self._cancel(cancel_event)
296
+ if context.remaining_s <= 0:
297
+ raise TimeoutError("repair admission deadline expired")
298
+ return backend.invoke(candidate, feedback, context)
299
+
300
+ try:
301
+ candidate = self.run.wrap(
302
+ invoke_repair,
303
+ boundary=backend.boundary,
304
+ branch_id=branch,
305
+ dispatch=replace(backend.dispatch, retry_source="harness"),
306
+ usage_reader=backend.usage_reader,
307
+ error_usage_reader=backend.error_usage_reader,
308
+ )(candidate, tuple(feedback), VerificationContext(deadline, cancel_event))
309
+ if type(candidate) is not CompletionCandidate:
310
+ raise ValueError("repair must return CompletionCandidate")
311
+ repair_record["status"] = "returned"
312
+ except BaseException:
313
+ repair_record["status"] = "failed"
314
+ raise
315
+ except BaseException as exc:
316
+ failure = exc
317
+ receipt["status"] = (
318
+ "blocked"
319
+ if isinstance(exc, HarnessControlError)
320
+ else "cancelled"
321
+ if not isinstance(exc, Exception)
322
+ else "error"
323
+ )
324
+ raise
325
+ finally:
326
+ with self._lock, _EVIDENCE_LOCK:
327
+ self._records[identity] = deepcopy(receipt)
328
+ if trace is not None:
329
+ if (
330
+ trace.metadata.get(COMPLETION_KEY) is not envelope
331
+ or envelope.get("schema_version") != COMPLETION_VERSION
332
+ or not isinstance(envelope.get("records"), dict)
333
+ ):
334
+ receipt["capture_error"] = "invalid_trace_evidence"
335
+ receipt["status"] = "evidence_error"
336
+ self._records[identity] = deepcopy(receipt)
337
+ if failure is None:
338
+ raise ValueError("invalid completion trace evidence")
339
+ else:
340
+ envelope["records"][identity] = deepcopy(receipt)
341
+
342
+ @staticmethod
343
+ def _cancel(cancel_event):
344
+ if cancel_event is not None and cancel_event.is_set():
345
+ raise CancelledError("completion cancelled")