agentloop-profiler 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,470 @@
1
+ """Explicit model routes and bounded, independently metered provider attempts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ from contextvars import ContextVar
7
+ from copy import deepcopy
8
+ from dataclasses import dataclass, replace
9
+ from threading import RLock
10
+ from time import perf_counter
11
+ from types import MappingProxyType
12
+ from uuid import uuid4
13
+
14
+ from agentloop.budget_types import ResourceUsage
15
+ from agentloop.context_types import fingerprint
16
+ from agentloop.harness import (
17
+ Decision,
18
+ HarnessControlError,
19
+ HarnessRun,
20
+ Hook,
21
+ Policy,
22
+ _invoke_usage_reader,
23
+ )
24
+ from agentloop.routing_checks import check_route
25
+ from agentloop.routing_types import (
26
+ ROUTING_KEY,
27
+ ROUTING_VERSION,
28
+ ModelBackend,
29
+ RouteProviderError,
30
+ RouteResponseInfo,
31
+ RoutingConfig,
32
+ RoutingError,
33
+ RoutingRequest,
34
+ )
35
+ from agentloop.tracer import current_trace
36
+
37
+ _ACTIVE = ContextVar("agentloop_model_route", default=None)
38
+ _EVIDENCE_LOCK = RLock()
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class _RouteAdmission:
43
+ config: RoutingConfig
44
+
45
+ def __call__(self, context):
46
+ if context.branch_id != self.config.branch_id:
47
+ return Decision("continue", "route_not_selected")
48
+ active = _ACTIVE.get()
49
+ if (
50
+ active is None
51
+ or active["used"]
52
+ or active["config"] != self.config
53
+ or active["run_id"] != context.run_id
54
+ ):
55
+ return Decision("deny", "route_adapter_required")
56
+ active["used"] = True
57
+ active["attempt"]["harness_call_id"] = context.call_id
58
+ return Decision("continue", "explicit_route_admitted")
59
+
60
+
61
+ def routing_policy(config):
62
+ if type(config) is not RoutingConfig:
63
+ raise ValueError("routing policy requires RoutingConfig")
64
+ return Policy(
65
+ config.route_id,
66
+ config.version,
67
+ _RouteAdmission(config),
68
+ hooks=frozenset({Hook("model")}),
69
+ actions=frozenset({"continue", "deny"}),
70
+ priority=50,
71
+ configuration=config.to_dict(),
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class _Frame:
77
+ value: object = None
78
+ info: RouteResponseInfo | None = None
79
+
80
+
81
+ class ModelRouter:
82
+ """One supported synchronous or streaming model boundary per actual attempt.
83
+
84
+ Backends are trusted host-owned SDK adapters, not dynamically loaded code.
85
+ No router decision establishes task quality. Automatic SDK retries must be
86
+ disabled or fully accounted for by the host adapter's usage contract.
87
+ """
88
+
89
+ def __init__(self, run, *, config, backends, max_records=256):
90
+ if not isinstance(run, HarnessRun) or type(config) is not RoutingConfig:
91
+ raise ValueError("typed run and routing configuration required")
92
+ if (
93
+ not isinstance(backends, (list, tuple))
94
+ or not backends
95
+ or any(type(item) is not ModelBackend for item in backends)
96
+ ):
97
+ raise ValueError("explicit typed backends required")
98
+ registry = {item.identity: item for item in backends}
99
+ if (
100
+ len(registry) != len(backends)
101
+ or not {config.original, config.target, *config.fallbacks} <= registry.keys()
102
+ or not registry.keys() <= set(config.allowlist)
103
+ ):
104
+ raise ValueError("backends must be unique, complete and allowlisted")
105
+ if type(max_records) is not int or not 1 <= max_records <= 10000:
106
+ raise ValueError("max_records must be bounded")
107
+ expected = routing_policy(config)
108
+ if run.harness.config.mode != "disabled" and not any(
109
+ policy.config_hash == expected.config_hash and type(policy.evaluate) is _RouteAdmission
110
+ for policy in run.harness.config.policies
111
+ ):
112
+ raise ValueError("register the matching routing_policy before binding")
113
+ self._run, self._config, self._backends = run, config, MappingProxyType(registry)
114
+ self._max_records, self._policy_hash = max_records, expected.config_hash
115
+ self._records, self._lock, self._capture_failed = {}, RLock(), False
116
+
117
+ @property
118
+ def run(self):
119
+ return self._run
120
+
121
+ @property
122
+ def config(self):
123
+ return self._config
124
+
125
+ @property
126
+ def backends(self):
127
+ return self._backends
128
+
129
+ @property
130
+ def max_records(self):
131
+ return self._max_records
132
+
133
+ def export_evidence(self):
134
+ with self._lock:
135
+ return {"schema_version": ROUTING_VERSION, "records": deepcopy(self._records)}
136
+
137
+ def _info(self, backend, value, attempt):
138
+ try:
139
+ info = _invoke_usage_reader(backend.inspect_response, value)
140
+ if type(info) is not RouteResponseInfo:
141
+ if inspect.iscoroutine(info):
142
+ info.close()
143
+ raise ValueError()
144
+ except Exception:
145
+ attempt["inspection_error"] = "response_inspection_failed"
146
+ if attempt["usage"] is not None:
147
+ attempt["last_known_usage"] = attempt["usage"]
148
+ return self._record_info(backend, RouteResponseInfo(ResourceUsage()), attempt)
149
+ return self._record_info(backend, info, attempt)
150
+
151
+ def _record_info(self, backend, info, attempt):
152
+ usage = info.usage
153
+ if usage is not None and usage.usage_id is not None:
154
+ usage = replace(
155
+ usage, usage_id="route:" + fingerprint([backend.identity.provider, usage.usage_id])
156
+ )
157
+ info = RouteResponseInfo(usage, info.reported_model)
158
+ if info.reported_model is not None:
159
+ attempt["provider_reported_model"] = info.reported_model
160
+ if usage is not None:
161
+ attempt["usage"] = usage.to_dict()
162
+ return info
163
+
164
+ def _invoke(self, backend, payload, attempt):
165
+ attempt["provider_invoked"] = True
166
+ result = backend.invoke(deepcopy(payload))
167
+ if inspect.isawaitable(result) or hasattr(result, "__next__"):
168
+ close = getattr(result, "close", None)
169
+ if callable(close):
170
+ close()
171
+ raise RoutingError("invalid_response")
172
+ return _Frame(result, self._info(backend, result, attempt))
173
+
174
+ def _stream_frames(self, backend, payload, attempt, state):
175
+ source, failure = None, None
176
+ try:
177
+ attempt["provider_invoked"] = True
178
+ source = backend.stream(deepcopy(payload))
179
+ if inspect.isawaitable(source):
180
+ close = getattr(source, "close", None)
181
+ if callable(close):
182
+ close()
183
+ raise RoutingError("unsupported_stream")
184
+ for chunk in source:
185
+ info = self._info(backend, chunk, attempt)
186
+ attempt["chunks"] += 1
187
+ yield _Frame(chunk, info)
188
+ except BaseException as exc:
189
+ failure = exc
190
+ raise
191
+ finally:
192
+ close = getattr(source, "close", None)
193
+ if callable(close):
194
+ try:
195
+ close()
196
+ except BaseException as exc:
197
+ attempt["cleanup_error"] = "stream_close_failed"
198
+ if isinstance(exc, RouteProviderError):
199
+ state["close_info"] = exc.info
200
+ if failure is None:
201
+ raise
202
+
203
+ def _attempt(self, backend, payload, attempt, streaming, fallback):
204
+ dispatch = (
205
+ replace(backend.dispatch, retry_source="harness") if fallback else backend.dispatch
206
+ )
207
+ state = {}
208
+
209
+ def error_usage(error):
210
+ info = state.get("close_info")
211
+ if info is None and isinstance(error, RouteProviderError):
212
+ info = error.info
213
+ return self._record_info(backend, info, attempt).usage if info is not None else None
214
+
215
+ wrapped = self.run.wrap(
216
+ self._stream_frames if streaming else self._invoke,
217
+ boundary="model",
218
+ branch_id=self.config.branch_id,
219
+ dispatch=dispatch,
220
+ usage_reader=lambda frame: frame.info.usage if frame.info is not None else None,
221
+ error_usage_reader=error_usage,
222
+ )
223
+ binding = {
224
+ "config": self.config,
225
+ "run_id": self.run.run_id,
226
+ "attempt": attempt,
227
+ "used": False,
228
+ }
229
+ iterator = None
230
+ start = perf_counter()
231
+ try:
232
+ if not streaming:
233
+ token = _ACTIVE.set(binding)
234
+ try:
235
+ frame = wrapped(backend, payload, attempt)
236
+ finally:
237
+ _ACTIVE.reset(token)
238
+ attempt["status"] = "completed"
239
+ yield frame.value
240
+ return
241
+ iterator = wrapped(backend, payload, attempt, state)
242
+ while True:
243
+ token = _ACTIVE.set(binding)
244
+ try:
245
+ frame = next(iterator)
246
+ except StopIteration:
247
+ attempt["status"] = "completed"
248
+ return
249
+ finally:
250
+ _ACTIVE.reset(token)
251
+ yield frame.value
252
+ except BaseException as exc:
253
+ attempt["status"] = (
254
+ "cancelled"
255
+ if not isinstance(exc, Exception)
256
+ else "blocked"
257
+ if isinstance(exc, HarnessControlError)
258
+ else "failed"
259
+ )
260
+ attempt["reason"] = (
261
+ exc.category
262
+ if isinstance(exc, RouteProviderError)
263
+ else "harness_control"
264
+ if isinstance(exc, HarnessControlError)
265
+ else "cancelled"
266
+ if not isinstance(exc, Exception)
267
+ else "provider_error"
268
+ )
269
+ raise
270
+ finally:
271
+ try:
272
+ if iterator is not None:
273
+ iterator.close()
274
+ finally:
275
+ attempt["latency_ms"] = (perf_counter() - start) * 1000
276
+
277
+ def _drive(self, request, streaming):
278
+ if type(request) is not RoutingRequest:
279
+ raise TypeError("router requires RoutingRequest")
280
+ payload = request.payload
281
+ if payload["model"] != self.config.original.model:
282
+ raise RoutingError("request_model_mismatch")
283
+ if payload.get("stream", False) is not streaming:
284
+ raise RoutingError("unsupported_stream")
285
+ original = self.backends[self.config.original]
286
+ if self.run.harness.config.mode == "disabled":
287
+ if streaming:
288
+ if original.stream is None:
289
+ raise RoutingError("unsupported_stream")
290
+ source = original.stream(payload)
291
+ try:
292
+ for chunk in source:
293
+ yield chunk
294
+ finally:
295
+ close = getattr(source, "close", None)
296
+ if callable(close):
297
+ close()
298
+ else:
299
+ yield original.invoke(payload)
300
+ return
301
+ if _ACTIVE.get() is not None:
302
+ raise RoutingError("recursive_route")
303
+ # Both adapters otherwise count the same physical call. An explicit
304
+ # shared dispatch integration is required before composing them.
305
+ from agentloop.context_controls import _ACTIVE as _CONTEXT_ACTIVE
306
+
307
+ if _CONTEXT_ACTIVE.get() is not None:
308
+ raise RoutingError("context_adapter_nesting")
309
+ identity = "route_" + uuid4().hex
310
+ receipt = {
311
+ "record_id": identity,
312
+ "route_id": self.config.route_id,
313
+ "route_version": self.config.version,
314
+ "route_config_hash": self._policy_hash,
315
+ "quality_ref": self.config.quality_ref,
316
+ "mode": self.run.harness.config.mode,
317
+ "requested": self.config.original.to_dict(),
318
+ "configured_target": self.config.target.to_dict(),
319
+ "original_hash": fingerprint(payload),
320
+ "status": "pending",
321
+ "reason": None,
322
+ "capability_checks": [],
323
+ "attempts": [],
324
+ }
325
+ trace = current_trace()
326
+ with self._lock, _EVIDENCE_LOCK:
327
+ if self._capture_failed:
328
+ raise RoutingError("invalid_trace_evidence")
329
+ if len(self._records) >= self.max_records:
330
+ raise RoutingError("evidence_limit")
331
+ if trace is not None:
332
+ envelope = trace.metadata.setdefault(
333
+ ROUTING_KEY, {"schema_version": ROUTING_VERSION, "records": {}}
334
+ )
335
+ if (
336
+ not isinstance(envelope, dict)
337
+ or envelope.get("schema_version") != ROUTING_VERSION
338
+ or not isinstance(envelope.get("records"), dict)
339
+ or len(envelope["records"]) >= 10000
340
+ ):
341
+ raise RoutingError("invalid_trace_evidence")
342
+ envelope["records"][identity] = deepcopy(receipt)
343
+ self._records[identity] = deepcopy(receipt)
344
+ failure = None
345
+ started = perf_counter()
346
+ try:
347
+ kind = "generator" if streaming else "sync"
348
+ if (
349
+ kind not in self.run.harness.capabilities.execution_kinds
350
+ or not {Hook("model"), Hook("model", "after")}
351
+ <= self.run.harness.capabilities.hooks
352
+ ):
353
+ receipt["reason"] = "unsupported_harness_lifecycle"
354
+ raise RoutingError("unsupported_route")
355
+ target_payload = {**payload, "model": self.config.target.model}
356
+ checks = check_route(
357
+ self.backends[self.config.target], target_payload, streaming=streaming
358
+ )
359
+ receipt["capability_checks"].append({"model": self.config.target.to_dict(), **checks})
360
+ if self.run.harness.config.mode == "shadow":
361
+ choices, receipt["reason"] = (self.config.original,), "shadow_original"
362
+ elif not checks["supported"]:
363
+ if self.config.on_unsupported == "error":
364
+ receipt["reason"] = "unsupported_route"
365
+ raise RoutingError("unsupported_route")
366
+ choices, receipt["reason"] = (self.config.original,), "unsupported_original"
367
+ else:
368
+ choices, receipt["reason"] = (
369
+ (self.config.target, *self.config.fallbacks),
370
+ "explicit_configuration",
371
+ )
372
+ for index, selected in enumerate(choices):
373
+ backend = self.backends[selected]
374
+ candidate = {**payload, "model": selected.model}
375
+ if index:
376
+ checks = check_route(backend, candidate, streaming=streaming)
377
+ receipt["capability_checks"].append({"model": selected.to_dict(), **checks})
378
+ if not checks["supported"]:
379
+ receipt["reason"] = "fallback_unsupported"
380
+ raise RoutingError("unsupported_route")
381
+ if streaming and backend.stream is None:
382
+ raise RoutingError("unsupported_stream")
383
+ attempt = {
384
+ "index": index,
385
+ "selected": selected.to_dict(),
386
+ "capability_reference": backend.capabilities.reference,
387
+ "request_hash": fingerprint(candidate),
388
+ "provider_reported_model": None,
389
+ "provider_invoked": False,
390
+ "harness_call_id": None,
391
+ "status": "pending",
392
+ "reason": None,
393
+ "usage": None,
394
+ "chunks": 0,
395
+ "latency_ms": None,
396
+ }
397
+ receipt["attempts"].append(attempt)
398
+ emitted = False
399
+ iterator = self._attempt(backend, candidate, attempt, streaming, bool(index))
400
+ try:
401
+ for value in iterator:
402
+ emitted = True
403
+ yield value
404
+ receipt["status"] = "completed"
405
+ return
406
+ except RouteProviderError as exc:
407
+ if (
408
+ emitted
409
+ or exc.category not in self.config.fallback_on
410
+ or index + 1 == len(choices)
411
+ or self.run.stopped
412
+ ):
413
+ raise
414
+ receipt["reason"] = "configured_failure_fallback"
415
+ finally:
416
+ iterator.close()
417
+ except BaseException as exc:
418
+ failure = exc
419
+ receipt["status"] = (
420
+ "cancelled"
421
+ if not isinstance(exc, Exception)
422
+ else "blocked"
423
+ if isinstance(exc, HarnessControlError)
424
+ else "failed"
425
+ )
426
+ receipt["error_code"] = (
427
+ exc.reason_code
428
+ if isinstance(exc, RoutingError)
429
+ else exc.category
430
+ if isinstance(exc, RouteProviderError)
431
+ else "cancelled"
432
+ if not isinstance(exc, Exception)
433
+ else "harness_control"
434
+ if isinstance(exc, HarnessControlError)
435
+ else "provider_error"
436
+ )
437
+ raise
438
+ finally:
439
+ receipt["latency_ms"] = (perf_counter() - started) * 1000
440
+ with self._lock, _EVIDENCE_LOCK:
441
+ capture_error = False
442
+ if trace is not None:
443
+ try:
444
+ current = trace.metadata[ROUTING_KEY]
445
+ if (
446
+ current is not envelope
447
+ or current.get("schema_version") != ROUTING_VERSION
448
+ or not isinstance(current["records"], dict)
449
+ ):
450
+ raise ValueError()
451
+ current["records"][identity] = deepcopy(receipt)
452
+ except (KeyError, TypeError, ValueError):
453
+ capture_error = self._capture_failed = True
454
+ receipt["capture_error"] = "invalid_trace_evidence"
455
+ self._records[identity] = deepcopy(receipt)
456
+ if capture_error and failure is None:
457
+ raise RoutingError("invalid_trace_evidence")
458
+
459
+ def __call__(self, request):
460
+ values = self._drive(request, False)
461
+ result = None
462
+ try:
463
+ for result in values:
464
+ pass
465
+ finally:
466
+ values.close()
467
+ return result
468
+
469
+ def stream(self, request):
470
+ yield from self._drive(request, True)