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/ranking.py ADDED
@@ -0,0 +1,529 @@
1
+ """Deterministic investigation ordering; scores never authorize a change."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+ from collections import Counter
8
+ from copy import deepcopy
9
+ from enum import Enum
10
+
11
+ from agentloop.savings import SavingsItem, select_compatible
12
+
13
+ RANKING_KEY = "agentloop.ranking_inputs"
14
+ RANKING_VERSION = "1.0"
15
+ SORT_FIELDS = (
16
+ "priority",
17
+ "impact",
18
+ "frequency",
19
+ "latency",
20
+ "cost",
21
+ "tokens",
22
+ "confidence",
23
+ "evidence",
24
+ "quality_risk",
25
+ "reversible",
26
+ "validation_effort",
27
+ "validation_cost",
28
+ )
29
+
30
+
31
+ class RankingSort(str, Enum):
32
+ PRIORITY = "priority"
33
+ IMPACT = "impact"
34
+ FREQUENCY = "frequency"
35
+ LATENCY = "latency"
36
+ COST = "cost"
37
+ TOKENS = "tokens"
38
+ CONFIDENCE = "confidence"
39
+ EVIDENCE = "evidence"
40
+ QUALITY_RISK = "quality_risk"
41
+ REVERSIBLE = "reversible"
42
+ VALIDATION_EFFORT = "validation_effort"
43
+ VALIDATION_COST = "validation_cost"
44
+
45
+
46
+ REQUIRED_INPUTS = ("quality_risk", "reversible", "validation_effort_minutes", "validation_cost_usd")
47
+ _CONFIDENCE = {"high": 3, "medium": 2, "low": 1}
48
+ _EVIDENCE = {
49
+ "observed": 3,
50
+ "declared": 3,
51
+ "deterministic_inference": 2,
52
+ "inferred": 1,
53
+ "semantic_judgment": 1,
54
+ }
55
+ _KEY = re.compile(r"[A-Za-z0-9_.:-]{1,96}\Z")
56
+ ORDER = (
57
+ "ready_to_test",
58
+ "confidence",
59
+ "evidence",
60
+ "quality_risk",
61
+ "frequency",
62
+ "impact",
63
+ "validation_effort",
64
+ "validation_cost",
65
+ "latency",
66
+ "cost",
67
+ "tokens",
68
+ "identity",
69
+ )
70
+
71
+
72
+ def requirements(values):
73
+ if (
74
+ not isinstance(values, (list, tuple))
75
+ or len(values) > 32
76
+ or any(not isinstance(value, str) or not _KEY.fullmatch(value) for value in values)
77
+ ):
78
+ raise ValueError("ranking requirements must be bounded input identifiers")
79
+ result = tuple(sorted(set(REQUIRED_INPUTS) | set(values)))
80
+ if len(result) > 32:
81
+ raise ValueError("ranking requirements are limited to 32 including standard inputs")
82
+ return result
83
+
84
+
85
+ def _number(value, *, integer=False):
86
+ try:
87
+ return (
88
+ (type(value) is int if integer else type(value) in {int, float})
89
+ and math.isfinite(value)
90
+ and value >= 0
91
+ )
92
+ except (OverflowError, TypeError):
93
+ return False
94
+
95
+
96
+ def _input(inputs, name, validate, kind=None):
97
+ item = inputs.get(name)
98
+ if not isinstance(item, dict) or set(item) != {"value", "kind", "source_ref"}:
99
+ return None
100
+ if (
101
+ not isinstance(item["kind"], str)
102
+ or item["kind"] not in {"observed", "declared", "estimated"}
103
+ or (kind is not None and item["kind"] != kind)
104
+ or not isinstance(item["source_ref"], str)
105
+ or not item["source_ref"].strip()
106
+ or len(item["source_ref"]) > 512
107
+ ):
108
+ return None
109
+ return deepcopy(item) if validate(item["value"]) else None
110
+
111
+
112
+ def _component(value, kind, source):
113
+ return {"value": value, "kind": kind, "source_ref": source}
114
+
115
+
116
+ def _mapping(value):
117
+ return value if isinstance(value, dict) else {}
118
+
119
+
120
+ def _sum(values):
121
+ try:
122
+ result = math.fsum(values)
123
+ return result if _number(result) else None
124
+ except (OverflowError, ValueError):
125
+ return None
126
+
127
+
128
+ def _units(components):
129
+ for name, unit in {
130
+ "impact": "ms_of_recorded_span_work",
131
+ "frequency": "included_runs",
132
+ "latency": "ms",
133
+ "cost": "USD",
134
+ "tokens": "tokens",
135
+ "validation_effort": "minutes",
136
+ "validation_cost": "USD",
137
+ }.items():
138
+ components[name]["unit"] = unit
139
+
140
+
141
+ def finding_ranking(finding, *, inputs=None, frequency=1):
142
+ """Use explicit annotations plus preserved canonical evidence/estimates.
143
+
144
+ Span work is cumulative observed duration, not elapsed savings. Confidence
145
+ categories and evidence tiers are ordinal labels, not probabilities.
146
+ """
147
+ inputs = inputs if isinstance(inputs, dict) else {}
148
+ retained, invalid = {}, []
149
+ raw_spans = finding.get("affected_spans", ())
150
+ spans = (
151
+ {span for span in raw_spans if isinstance(span, str)}
152
+ if isinstance(raw_spans, (list, tuple))
153
+ else set()
154
+ )
155
+ evidence = finding.get("evidence", ())
156
+ evidence = evidence if isinstance(evidence, (list, tuple)) else ()
157
+ observed = {}
158
+ conflicts = False
159
+ for row in evidence:
160
+ if (
161
+ not isinstance(row, dict)
162
+ or not isinstance(row.get("span_id"), str)
163
+ or row.get("span_id") not in spans
164
+ or not _number(row.get("duration_ms"))
165
+ ):
166
+ continue
167
+ identity = row["span_id"]
168
+ if identity in observed and observed[identity] != row["duration_ms"]:
169
+ conflicts = True
170
+ observed[identity] = row["duration_ms"]
171
+ complete = bool(spans) and set(observed) == spans and not conflicts
172
+ confidence = finding.get("confidence", "unknown")
173
+ confidence = confidence if isinstance(confidence, str) else "unknown"
174
+ level = finding.get("evidence_level", "unknown")
175
+ level = level if isinstance(level, str) else "unknown"
176
+ estimate = finding.get("estimate")
177
+ estimate = estimate if isinstance(estimate, dict) else {}
178
+ savings = finding.get("savings")
179
+ savings = savings if isinstance(savings, dict) else {}
180
+ unmodeled = estimate.get("unmodeled_metrics", ())
181
+ if not isinstance(unmodeled, (list, tuple)):
182
+ unmodeled = ("latency", "cost")
183
+ source = (
184
+ "estimator:"
185
+ + str(estimate.get("estimator_id", "unavailable"))
186
+ + ":"
187
+ + str(estimate.get("estimator_version", "unknown"))
188
+ )
189
+ latency = savings.get("estimated_latency_savings_ms")
190
+ cost = savings.get("estimated_cost_savings_usd")
191
+ cost_status = _mapping(estimate.get("inputs")).get(
192
+ "cost_status", _mapping(finding.get("metadata")).get("cost_status")
193
+ )
194
+ modeled = all(
195
+ isinstance(estimate.get(key), str) and estimate[key]
196
+ for key in ("estimator_id", "estimator_version")
197
+ )
198
+ latency = latency if modeled and "latency" not in unmodeled and _number(latency) else None
199
+ cost = (
200
+ cost
201
+ if modeled
202
+ and "cost" not in unmodeled
203
+ and isinstance(cost_status, str)
204
+ and cost_status in {"complete", "empty"}
205
+ and _number(cost)
206
+ else None
207
+ )
208
+ components = {
209
+ "impact": _component(
210
+ _sum(observed.values()) if complete else None,
211
+ "observed",
212
+ "recorded_span_durations_nonadditive",
213
+ ),
214
+ "frequency": _component(
215
+ frequency if _number(frequency, integer=True) else None,
216
+ "observed",
217
+ "included_distinct_runs",
218
+ ),
219
+ "confidence": _component(
220
+ confidence if confidence in _CONFIDENCE else None,
221
+ "declared",
222
+ "rule:"
223
+ + str(finding.get("rule_id", "unknown"))
224
+ + ":"
225
+ + str(finding.get("rule_version", "unknown")),
226
+ ),
227
+ "evidence": _component(
228
+ level if complete and level in _EVIDENCE else None,
229
+ "declared",
230
+ "rule_evidence_level_and_span_coverage",
231
+ ),
232
+ "latency": _component(latency, "estimated", source),
233
+ "cost": _component(cost, "estimated", source),
234
+ "tokens": _component(None, "estimated", "unavailable"),
235
+ }
236
+ for name, key, validate, kind in (
237
+ (
238
+ "quality_risk",
239
+ "quality_risk",
240
+ lambda value: value in {"low", "medium", "high"} if isinstance(value, str) else False,
241
+ "declared",
242
+ ),
243
+ ("reversible", "reversible", lambda value: type(value) is bool, "declared"),
244
+ (
245
+ "evidence_level",
246
+ "evidence",
247
+ lambda value: isinstance(value, str) and value in _EVIDENCE,
248
+ "declared",
249
+ ),
250
+ ("validation_effort_minutes", "validation_effort", _number, "declared"),
251
+ ("validation_cost_usd", "validation_cost", _number, "declared"),
252
+ ("estimated_cost_savings_usd", "cost", _number, "estimated"),
253
+ ("estimated_latency_savings_ms", "latency", _number, "estimated"),
254
+ (
255
+ "estimated_token_savings",
256
+ "tokens",
257
+ lambda value: _number(value, integer=True),
258
+ "estimated",
259
+ ),
260
+ ):
261
+ supplied = _input(inputs, name, validate, kind)
262
+ if supplied is not None:
263
+ components[key] = supplied
264
+ retained[name] = deepcopy(supplied)
265
+ elif key not in components:
266
+ components[key] = _component(None, kind, "unavailable")
267
+ if name in inputs and supplied is None:
268
+ invalid.append(name)
269
+ try:
270
+ needed = requirements(finding.get("ranking_requirements", ()))
271
+ except ValueError:
272
+ needed = requirements(())
273
+ invalid.append("ranking_requirements")
274
+ aliases = {
275
+ "validation_effort_minutes": "validation_effort",
276
+ "validation_cost_usd": "validation_cost",
277
+ }
278
+ missing = []
279
+ for name in needed:
280
+ component = components.get(aliases.get(name, name))
281
+ if component is None:
282
+ component = _input(
283
+ inputs,
284
+ name,
285
+ lambda value: (
286
+ type(value) is bool
287
+ or _number(value)
288
+ or (isinstance(value, str) and 0 < len(value.strip()) <= 256)
289
+ ),
290
+ )
291
+ components[name] = component or _component(None, "declared", "unavailable")
292
+ if component is not None:
293
+ retained[name] = deepcopy(component)
294
+ if component is None or component["value"] is None:
295
+ missing.append(name)
296
+ reasons = ["missing_input:" + name for name in sorted(missing)]
297
+ reasons.extend("invalid_input:" + name for name in sorted(invalid))
298
+ if cost is None and components["cost"]["value"] is None:
299
+ reasons.append("cost_opportunity_unavailable")
300
+ if not complete or components["impact"]["value"] is None:
301
+ reasons.append("span_evidence_incomplete")
302
+ if _CONFIDENCE.get(confidence, 0) < 2:
303
+ reasons.append("confidence_weak_or_unknown")
304
+ if not all(
305
+ isinstance(finding.get(key), str) and finding[key] for key in ("rule_id", "rule_version")
306
+ ):
307
+ reasons.append("confidence_provenance_missing")
308
+ if _EVIDENCE.get(components["evidence"]["value"], 0) < 2:
309
+ reasons.append("evidence_weak_or_unknown")
310
+ if components["quality_risk"]["value"] == "high":
311
+ reasons.append("high_quality_risk_requires_review")
312
+ if components["reversible"]["value"] is False:
313
+ reasons.append("irreversible_change_requires_review")
314
+ if all(components[key]["value"] in {None, 0} for key in ("latency", "cost", "tokens")):
315
+ reasons.append("positive_opportunity_unavailable")
316
+ _units(components)
317
+ return {
318
+ "schema_version": RANKING_VERSION,
319
+ "method": "explicit_lexicographic",
320
+ "status": "ready_to_test" if not reasons else "needs_evidence_or_review",
321
+ "components": components,
322
+ "required_inputs": list(needed),
323
+ "reasons": sorted(set(reasons)),
324
+ "order": list(ORDER),
325
+ "estimate_provenance": deepcopy(estimate) or None,
326
+ "source_inputs": retained,
327
+ "invalid_inputs": sorted(invalid),
328
+ "automatic_application_allowed": False,
329
+ }
330
+
331
+
332
+ def _identity(item):
333
+ return item.get("finding_id", item.get("queue_id", ""))
334
+
335
+
336
+ def _priority(item):
337
+ rank = item["ranking"]
338
+ value = {key: part["value"] for key, part in rank["components"].items()}
339
+ return (
340
+ rank["status"] != "ready_to_test",
341
+ -_CONFIDENCE.get(value["confidence"], 0),
342
+ -_EVIDENCE.get(value["evidence"], 0),
343
+ {"low": 0, "medium": 1, "high": 2}.get(value["quality_risk"], 3),
344
+ -(value["frequency"] or 0),
345
+ -(value["impact"] or 0),
346
+ math.inf if value["validation_effort"] is None else value["validation_effort"],
347
+ math.inf if value["validation_cost"] is None else value["validation_cost"],
348
+ -(value["latency"] or 0),
349
+ -(value["cost"] or 0),
350
+ -(value["tokens"] or 0),
351
+ _identity(item),
352
+ )
353
+
354
+
355
+ def sort_ranked(items, sort_by="priority"):
356
+ if sort_by not in SORT_FIELDS:
357
+ raise ValueError("unsupported finding ranking dimension")
358
+ if sort_by == "priority":
359
+ return sorted(items, key=_priority)
360
+
361
+ def key(item):
362
+ value = item["ranking"]["components"][sort_by]["value"]
363
+ if sort_by == "confidence":
364
+ value = _CONFIDENCE.get(value)
365
+ elif sort_by == "evidence":
366
+ value = _EVIDENCE.get(value)
367
+ elif sort_by == "quality_risk":
368
+ value = {"low": 0, "medium": 1, "high": 2}.get(value)
369
+ return (
370
+ value is None,
371
+ 0
372
+ if value is None
373
+ else value
374
+ if sort_by in {"validation_effort", "validation_cost", "quality_risk"}
375
+ else -value,
376
+ _identity(item),
377
+ )
378
+
379
+ return sorted(items, key=key)
380
+
381
+
382
+ def rank_findings(findings, *, inputs=None, sort_by="priority"):
383
+ inputs = inputs if isinstance(inputs, dict) else {}
384
+ result = deepcopy(findings)
385
+ for finding in result:
386
+ declared = _mapping(inputs.get(finding.get("rule_id")))
387
+ if finding["finding_id"] in inputs:
388
+ selected = inputs[finding["finding_id"]]
389
+ declared = {**declared, **selected} if isinstance(selected, dict) else {}
390
+ finding["ranking"] = finding_ranking(finding, inputs=declared)
391
+ return finalize_ranking(result, sort_by=sort_by)
392
+
393
+
394
+ def finalize_ranking(items, *, sort_by="priority"):
395
+ prioritized = sort_ranked(items)
396
+ ready_count = sum(item["ranking"]["status"] == "ready_to_test" for item in prioritized)
397
+ for index, item in enumerate(prioritized):
398
+ item["ranking"]["priority_rank"] = index + 1
399
+ # Compatibility numeric field: an ordinal among ready investigations,
400
+ # never dollars, saved time, utility, or an auto-application threshold.
401
+ item["priority_score"] = max(0, ready_count - index)
402
+ item["ranking"]["score_meaning"] = "ordinal_among_ready_findings_zero_when_ineligible"
403
+ return sort_ranked(prioritized, sort_by)
404
+
405
+
406
+ def cluster_ranking(members, *, selection_cache=None):
407
+ """Aggregate conservatively and reuse compatible-span opportunity selection."""
408
+ members = sorted(
409
+ members,
410
+ key=lambda item: (
411
+ str(item.get("project_id", "")),
412
+ item["run_id"],
413
+ item.get("finding_id", ""),
414
+ ),
415
+ )
416
+ ranks, per_run, observed = [], {}, {}
417
+ conflicting_spans = False
418
+ for member in members:
419
+ finding = member.get("finding")
420
+ finding = finding if isinstance(finding, dict) else {}
421
+ previous = _mapping(finding.get("ranking"))
422
+ rank = finding_ranking(finding, inputs=previous.get("source_inputs", {}))
423
+ old_invalid = previous.get("invalid_inputs", ())
424
+ if isinstance(old_invalid, (list, tuple)):
425
+ for name in old_invalid:
426
+ if isinstance(name, str) and _KEY.fullmatch(name):
427
+ rank["reasons"].append("invalid_input:" + name)
428
+ rank["status"] = "needs_evidence_or_review"
429
+ ranks.append(rank)
430
+ key = (member.get("project_id"), member["run_id"])
431
+ per_run.setdefault(key, []).append(
432
+ SavingsItem(
433
+ frozenset(finding.get("affected_spans") or ()),
434
+ rank["components"]["latency"]["value"] or 0,
435
+ rank["components"]["cost"]["value"] or 0,
436
+ )
437
+ )
438
+ for row in finding.get("evidence", ()):
439
+ if (
440
+ isinstance(row, dict)
441
+ and row.get("span_id") in (finding.get("affected_spans") or ())
442
+ and _number(row.get("duration_ms"))
443
+ ):
444
+ span_key = (key, row["span_id"])
445
+ if span_key in observed and observed[span_key] != row["duration_ms"]:
446
+ conflicting_spans = True
447
+ observed[span_key] = row["duration_ms"]
448
+ result = deepcopy(ranks[0])
449
+ components = result["components"]
450
+ reasons = set(reason for rank in ranks for reason in rank["reasons"])
451
+ if conflicting_spans:
452
+ reasons.add("conflicting_span_observations")
453
+ required = set(name for rank in ranks for name in rank["required_inputs"])
454
+ for key in (
455
+ "quality_risk",
456
+ "reversible",
457
+ "validation_effort",
458
+ "validation_cost",
459
+ "confidence",
460
+ "evidence",
461
+ ):
462
+ values = [rank["components"][key]["value"] for rank in ranks]
463
+ value = None
464
+ if all(item is not None for item in values):
465
+ if key == "quality_risk":
466
+ value = max(values, key={"low": 0, "medium": 1, "high": 2}.get)
467
+ elif key == "confidence":
468
+ value = min(values, key=_CONFIDENCE.get)
469
+ elif key == "evidence":
470
+ value = min(values, key=_EVIDENCE.get)
471
+ elif key == "reversible":
472
+ value = all(values)
473
+ else:
474
+ value = max(values)
475
+ components[key] = _component(value, "declared", "conservative_member_aggregation")
476
+ totals = []
477
+ for key, items in per_run.items():
478
+ cached = (selection_cache or {}).get(key)
479
+ totals.append(
480
+ cached[1]
481
+ if cached is not None and Counter(cached[0]) == Counter(items)
482
+ else select_compatible(items)
483
+ )
484
+ for key, attribute in (("latency", "latency_ms"), ("cost", "cost_usd")):
485
+ components[key] = _component(
486
+ _sum(getattr(selection, attribute) for selection in totals)
487
+ if all(rank["components"][key]["value"] is not None for rank in ranks)
488
+ else None,
489
+ "estimated",
490
+ "compatible_span_selection_per_run",
491
+ )
492
+ components["tokens"] = _component(None, "estimated", "see_member_estimates")
493
+ components["impact"] = _component(
494
+ _sum(observed.values())
495
+ if not conflicting_spans
496
+ and all(rank["components"]["impact"]["value"] is not None for rank in ranks)
497
+ else None,
498
+ "observed",
499
+ "distinct_recorded_spans_per_run_nonadditive",
500
+ )
501
+ components["frequency"] = _component(len(per_run), "observed", "included_distinct_project_runs")
502
+ if components["cost"]["value"] is None:
503
+ reasons.add("cost_opportunity_unavailable")
504
+ if components["impact"]["value"] is None:
505
+ reasons.add("span_evidence_incomplete")
506
+ if not all(selection.optimal for selection in totals):
507
+ reasons.add("opportunity_selection_approximate")
508
+ _units(components)
509
+ result.update(
510
+ status="ready_to_test" if not reasons else "needs_evidence_or_review",
511
+ reasons=sorted(reasons),
512
+ required_inputs=sorted(required),
513
+ members=[
514
+ {
515
+ "run_id": member["run_id"],
516
+ "project_id": member.get("project_id"),
517
+ "finding_id": member.get("finding_id"),
518
+ "ranking": rank,
519
+ }
520
+ for member, rank in zip(members, ranks)
521
+ ],
522
+ estimate_provenance=[rank["estimate_provenance"] for rank in ranks],
523
+ source_inputs={},
524
+ selection={
525
+ "optimal": all(selection.optimal for selection in totals),
526
+ "algorithms": sorted({selection.algorithm for selection in totals}),
527
+ },
528
+ )
529
+ return result
@@ -0,0 +1,96 @@
1
+ """Payload-free checkpoint decisions joined to native harness provenance."""
2
+
3
+ from copy import deepcopy
4
+ from threading import RLock
5
+ from uuid import uuid4
6
+
7
+ from agentloop.checkpoint_state import _require
8
+ from agentloop.context_types import fingerprint
9
+ from agentloop.harness import Decision, Harness, HarnessConfig, HarnessControlError, Hook, Policy
10
+ from agentloop.tracer import current_trace
11
+
12
+ RECOVERY_KEY = "agentloop.recovery"
13
+ _LOCK = RLock()
14
+
15
+
16
+ def record_recovery(
17
+ action,
18
+ reason,
19
+ *,
20
+ reference=None,
21
+ parent_run_id=None,
22
+ attempt_run_id=None,
23
+ status="incomplete",
24
+ store_durable=False,
25
+ ):
26
+ """Record a separate admission audit, never a fresh execution budget."""
27
+ identity = "recovery_" + uuid4().hex
28
+ entry = {
29
+ "recovery_id": identity,
30
+ "action": action,
31
+ "reason": reason,
32
+ "checkpoint_ref_hash": None if reference is None else fingerprint(reference),
33
+ "parent_run_id": parent_run_id,
34
+ "attempt_run_id": attempt_run_id,
35
+ "status": status,
36
+ "store_durable": store_durable,
37
+ "scope": "recovery_admission_only",
38
+ }
39
+ trace = current_trace()
40
+ envelope = None
41
+ if trace is not None:
42
+ with _LOCK:
43
+ envelope = trace.metadata.setdefault(
44
+ RECOVERY_KEY, {"schema_version": "1.0", "records": {}}
45
+ )
46
+ _require(
47
+ type(envelope) is dict
48
+ and envelope.get("schema_version") == "1.0"
49
+ and type(envelope.get("records")) is dict
50
+ and len(envelope["records"]) < 10000,
51
+ "invalid_recovery_evidence",
52
+ )
53
+ envelope["records"][identity] = deepcopy(entry)
54
+ policy = Policy(
55
+ "agentloop.recovery",
56
+ "1.0",
57
+ lambda context: Decision(action, reason, ("recovery:" + identity,)),
58
+ hooks=frozenset({Hook("completion")}),
59
+ actions=frozenset({"continue", "escalate"}),
60
+ )
61
+ audit = Harness(HarnessConfig("enforce", (policy,))).start_run("audit_" + uuid4().hex)
62
+ try:
63
+ audit.wrap(lambda: None, boundary="completion", branch_id="recovery")()
64
+ except HarnessControlError:
65
+ # This audit has no task work or task quota. Its escalation is re-raised
66
+ # by the caller as a typed CheckpointError before host invocation.
67
+ pass
68
+ entry["audit_run_id"] = audit.run_id
69
+ entry["audit_call_id"] = audit.results[0].call_id
70
+ if trace is not None:
71
+ with _LOCK:
72
+ _require(trace.metadata.get(RECOVERY_KEY) is envelope, "invalid_recovery_evidence")
73
+ envelope["records"][identity] = deepcopy(entry)
74
+ return entry
75
+
76
+
77
+ def recovery_status(trace):
78
+ """Return the latest declared attempt status for study adapters, if present."""
79
+ envelope = trace.metadata.get(RECOVERY_KEY)
80
+ if envelope is None:
81
+ return None
82
+ _require(
83
+ type(envelope) is dict
84
+ and envelope.get("schema_version") == "1.0"
85
+ and type(envelope.get("records")) is dict
86
+ and bool(envelope["records"]),
87
+ "invalid_recovery_evidence",
88
+ )
89
+ latest = list(envelope["records"].values())[-1]
90
+ _require(type(latest) is dict, "invalid_recovery_evidence")
91
+ status = latest.get("status")
92
+ _require(
93
+ status in {"completed", "incomplete", "failed", "cancelled", "stopped"},
94
+ "invalid_recovery_evidence",
95
+ )
96
+ return status