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/findings.py ADDED
@@ -0,0 +1,419 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+ from agentloop.costs import format_cost_usd
10
+ from agentloop.estimates import estimate_markdown
11
+ from agentloop.markdown import markdown_code_span, markdown_heading, markdown_text
12
+ from agentloop.optimizer import build_optimization_plan
13
+ from agentloop.ranking import RANKING_KEY, RANKING_VERSION, rank_findings
14
+ from agentloop.timing import format_duration_ms
15
+
16
+
17
+ @dataclass
18
+ class OptimizationFinding:
19
+ finding_id: str
20
+ severity: str
21
+ type: str
22
+ title: str
23
+ confidence: str
24
+ affected_spans: list[str]
25
+ evidence: list[dict[str, Any]]
26
+ savings: dict[str, Any]
27
+ rewrite: dict[str, Any]
28
+ validation: dict[str, Any]
29
+ metadata: dict[str, Any] = field(default_factory=dict)
30
+ evidence_level: str | None = None
31
+ assumptions: list[str] = field(default_factory=list)
32
+ observations: dict[str, Any] = field(default_factory=dict)
33
+ rule_id: str | None = None
34
+ rule_version: str | None = None
35
+ estimate: dict[str, Any] | None = None
36
+ ranking_requirements: tuple[str, ...] = ()
37
+
38
+ def to_dict(self) -> dict[str, Any]:
39
+ result = {
40
+ "finding_id": self.finding_id,
41
+ "severity": self.severity,
42
+ "type": self.type,
43
+ "title": self.title,
44
+ "confidence": self.confidence,
45
+ "affected_spans": self.affected_spans,
46
+ "evidence": self.evidence,
47
+ "savings": self.savings,
48
+ "rewrite": self.rewrite,
49
+ "validation": self.validation,
50
+ "metadata": self.metadata,
51
+ }
52
+ if self.evidence_level is not None:
53
+ result.update(
54
+ evidence_level=self.evidence_level,
55
+ assumptions=self.assumptions,
56
+ observations=self.observations,
57
+ )
58
+ if self.rule_id is not None:
59
+ result.update(rule_id=self.rule_id, rule_version=self.rule_version)
60
+ if self.estimate is not None:
61
+ result["estimate"] = deepcopy(self.estimate)
62
+ if self.ranking_requirements:
63
+ result["ranking_requirements"] = list(self.ranking_requirements)
64
+ return result
65
+
66
+
67
+ def build_diagnosis(trace: Any, *, sort_by="priority", ranking_inputs=None) -> dict[str, Any]:
68
+ plan = build_optimization_plan(trace)
69
+ node_lookup = {node["node_id"]: node for node in plan.get("graph", {}).get("nodes", [])}
70
+ findings = [
71
+ _finding_from_card(card, plan, node_lookup=node_lookup)
72
+ for card in plan.get("optimization_cards", [])
73
+ ]
74
+ findings = [finding for finding in findings if finding is not None]
75
+ annotations = trace.metadata.get(RANKING_KEY, {}) if ranking_inputs is None else ranking_inputs
76
+ ranked = rank_findings(
77
+ [finding.to_dict() for finding in findings], inputs=annotations, sort_by=sort_by
78
+ )
79
+ return {
80
+ **({"semantic_waste": plan["semantic_waste"]} if "semantic_waste" in plan else {}),
81
+ **(
82
+ {
83
+ "semantic_judgments": plan["semantic_judgments"],
84
+ "evidence_categories": plan["evidence_categories"],
85
+ }
86
+ if "semantic_judgments" in plan
87
+ else {}
88
+ ),
89
+ **({"execution": plan["graph"]["execution"]} if "execution" in plan["graph"] else {}),
90
+ "run_id": plan["run_id"],
91
+ "name": plan["name"],
92
+ "cost_status": plan.get("cost_status", "complete"),
93
+ "current": plan["current"],
94
+ "estimated_after": plan["estimated_after"],
95
+ "summary": _summary(plan, findings),
96
+ "findings": ranked,
97
+ "ranking": {
98
+ "schema_version": RANKING_VERSION,
99
+ "sort_by": sort_by,
100
+ "ready_to_test_count": sum(
101
+ finding["ranking"]["status"] == "ready_to_test" for finding in ranked
102
+ ),
103
+ "interpretation": "Investigation ordering only. Unknown inputs remain explicit; estimated savings are not measured outcomes.",
104
+ },
105
+ "graph": plan["graph"],
106
+ "rule_errors": plan.get("rule_errors", []),
107
+ "analysis_complete": plan.get("analysis_complete", True),
108
+ }
109
+
110
+
111
+ def diagnosis_to_markdown(diagnosis: dict[str, Any]) -> str:
112
+ current = diagnosis["current"]
113
+ after = diagnosis["estimated_after"]
114
+ lines = [
115
+ f"# AgentLoop Diagnosis: {markdown_heading(diagnosis['name'])}",
116
+ "",
117
+ f"- Run ID: {markdown_code_span(diagnosis['run_id'])}",
118
+ f"- Current runtime: {format_duration_ms(current['runtime_ms'])}",
119
+ f"- Estimated runtime after fixes: {format_duration_ms(after['runtime_ms'])}",
120
+ "- Estimated latency reduction: "
121
+ + (
122
+ "unavailable"
123
+ if after["latency_reduction_pct"] is None
124
+ else f"{after['latency_reduction_pct']:.2f}%"
125
+ ),
126
+ "- Current cost: "
127
+ + format_cost_usd(current.get("estimated_cost_usd"), current.get("cost_status")),
128
+ "- Estimated cost after fixes: "
129
+ + format_cost_usd(after.get("estimated_cost_usd"), after.get("cost_status")),
130
+ "- Estimated cost reduction: "
131
+ + (
132
+ "unavailable"
133
+ if after.get("cost_reduction_pct") is None
134
+ else f"{after['cost_reduction_pct']:.2f}%"
135
+ ),
136
+ "",
137
+ "## Findings",
138
+ "",
139
+ ]
140
+ findings = diagnosis.get("findings", [])
141
+ summary = diagnosis.get("summary")
142
+ if isinstance(summary, dict) and (
143
+ summary.get("unmodeled_latency_findings") or summary.get("unmodeled_cost_findings")
144
+ ):
145
+ lines.extend(["Some savings are unavailable; totals cover modeled candidates only.", ""])
146
+ if diagnosis.get("rule_errors"):
147
+ lines.extend(["Analysis incomplete; some finding rules failed:", ""])
148
+ for error in diagnosis["rule_errors"]:
149
+ lines.append(
150
+ f"- {markdown_code_span(error['rule_id'])}: {markdown_text(error['message'])}"
151
+ )
152
+ lines.append("")
153
+ if not findings:
154
+ lines.append("No machine-actionable optimization findings detected.")
155
+ for finding in findings:
156
+ savings = finding["savings"]
157
+ rewrite = finding["rewrite"]
158
+ validation = finding["validation"]
159
+ lines.extend(
160
+ [
161
+ f"### {markdown_heading(finding['finding_id'])}: "
162
+ f"{markdown_heading(finding['title'])}",
163
+ "",
164
+ f"- Severity: {markdown_text(finding['severity'])}",
165
+ f"- Type: {markdown_code_span(finding['type'])}",
166
+ f"- Confidence: {markdown_text(finding['confidence'])}",
167
+ "- Affected spans: "
168
+ f"{markdown_text(', '.join(map(str, finding['affected_spans'])) or 'none')}",
169
+ f"- Estimated latency savings: {format_duration_ms(savings['estimated_latency_savings_ms'])}",
170
+ "- Estimated cost savings: "
171
+ + format_cost_usd(savings.get("estimated_cost_savings_usd")),
172
+ f"- Rewrite: {markdown_text(rewrite['hint'])}",
173
+ f"- Validation: {markdown_text(validation['acceptance_criteria'])}",
174
+ "",
175
+ ]
176
+ )
177
+ if finding.get("rule_id"):
178
+ lines.append(
179
+ f"- Rule: {markdown_code_span(finding['rule_id'])} version {markdown_text(finding['rule_version'])}"
180
+ )
181
+ if finding.get("ranking"):
182
+ ranking = finding["ranking"]
183
+ lines.extend(
184
+ [
185
+ f"- Investigation priority: {ranking['priority_rank']} ({markdown_text(ranking['status'])})",
186
+ "- Ranking reasons: "
187
+ + markdown_text(
188
+ ", ".join(ranking["reasons"])
189
+ or "Required inputs are complete; use the declared component order."
190
+ ),
191
+ "- Ranking components and provenance: "
192
+ + markdown_code_span(json.dumps(ranking["components"], sort_keys=True)),
193
+ ]
194
+ )
195
+ if finding.get("evidence_level") and finding.get("estimate"):
196
+ lines.append(f"- Evidence level: {markdown_text(finding['evidence_level'])}")
197
+ lines.extend(estimate_markdown(finding.get("estimate")))
198
+ if finding.get("evidence_level") and not finding.get("estimate"):
199
+ lines.extend(
200
+ [
201
+ f"- Evidence level: {markdown_text(finding['evidence_level'])}",
202
+ "- Assumptions: "
203
+ + "; ".join(markdown_text(value) for value in finding.get("assumptions", [])),
204
+ f"- Savings formula: {markdown_text(savings.get('formula', ''))}",
205
+ "",
206
+ ]
207
+ )
208
+ if "semantic_judgments" in diagnosis:
209
+ from agentloop.judgment_views import judgment_markdown
210
+
211
+ lines.extend(judgment_markdown(diagnosis["semantic_judgments"]))
212
+ if "semantic_waste" in diagnosis:
213
+ from agentloop.semantic_waste import semantic_waste_markdown
214
+
215
+ lines.extend(semantic_waste_markdown(diagnosis["semantic_waste"]))
216
+ return "\n".join(lines).rstrip() + "\n"
217
+
218
+
219
+ def _stable_finding_id(
220
+ finding_type: str, title: str, affected_spans: list[str], *, context=None
221
+ ) -> str:
222
+ """Return an order-independent identity for one concrete detected finding.
223
+
224
+ The old IDs used a card enumeration index, so inserting or reordering an
225
+ unrelated card could make a human-reviewed status attach to different
226
+ evidence. The fingerprint is based on the finding kind, human-readable
227
+ title, and sorted affected span identities instead. A changed finding gets a
228
+ new ID; an unchanged finding keeps its ID regardless of card order.
229
+ """
230
+ identity = {
231
+ "type": finding_type,
232
+ "title": title,
233
+ "affected_spans": sorted(str(span) for span in affected_spans),
234
+ }
235
+ if context is not None:
236
+ identity["semantic_context"] = context
237
+ raw = json.dumps(identity, sort_keys=True, separators=(",", ":")).encode("utf-8")
238
+ digest = hashlib.sha256(raw).hexdigest()[:12]
239
+ return f"al_{finding_type}_{digest}"
240
+
241
+
242
+ def _finding_from_card(
243
+ card: dict[str, Any], plan: dict[str, Any], *, node_lookup=None
244
+ ) -> OptimizationFinding | None:
245
+ affected_spans = list(card.get("affected_nodes", []))
246
+ finding_type = str(card.get("type", "optimization"))
247
+ title = str(card.get("title", "Optimization opportunity"))
248
+ latency_savings = float(card.get("estimated_latency_savings_ms", 0.0) or 0.0)
249
+ raw_cost_savings = card.get("estimated_cost_savings_usd")
250
+ cost_savings = None if raw_cost_savings is None else float(raw_cost_savings)
251
+ severity = _severity(latency_savings, cost_savings, str(card.get("confidence", "low")), plan)
252
+ if node_lookup is None:
253
+ node_lookup = {node["node_id"]: node for node in plan.get("graph", {}).get("nodes", [])}
254
+ evidence = [
255
+ _evidence_row(node_lookup[node_id]) for node_id in affected_spans if node_id in node_lookup
256
+ ]
257
+ semantic = (card.get("estimate") or {}).get("estimator_id") == "semantic_leaf_removal"
258
+ observations = dict(card.get("observations", {}))
259
+ if semantic:
260
+ severity = "medium" if card.get("confidence") == "medium" else "low"
261
+ evidence.extend(
262
+ {"kind": "semantic_judgment", **item} for item in observations.get("judgments", [])
263
+ )
264
+
265
+ return OptimizationFinding(
266
+ finding_id=_stable_finding_id(
267
+ finding_type,
268
+ title,
269
+ affected_spans,
270
+ context={
271
+ "investigation_id": observations.get("investigation_id"),
272
+ "criteria_ref": observations.get("criteria_ref"),
273
+ }
274
+ if semantic
275
+ else None,
276
+ ),
277
+ severity=severity,
278
+ type=finding_type,
279
+ title=title,
280
+ confidence=str(card.get("confidence", "unknown")),
281
+ affected_spans=affected_spans,
282
+ evidence=evidence,
283
+ savings={
284
+ "estimated_latency_savings_ms": None
285
+ if card.get("estimated_latency_savings_ms") is None
286
+ else round(latency_savings, 3),
287
+ "estimated_cost_savings_usd": (
288
+ None if cost_savings is None else round(cost_savings, 6)
289
+ ),
290
+ "formula": (card.get("estimate") or {}).get("formula")
291
+ or card.get("estimate_formula")
292
+ or _savings_formula(finding_type),
293
+ **(
294
+ {
295
+ "estimated_input_tokens": card["estimate"]["predictions"]["input_tokens"],
296
+ "estimated_output_tokens": card["estimate"]["predictions"]["output_tokens"],
297
+ }
298
+ if semantic
299
+ else {}
300
+ ),
301
+ },
302
+ rewrite={
303
+ "kind": finding_type,
304
+ "hint": str(card.get("rewrite_hint", "")),
305
+ "patchable": finding_type
306
+ in {
307
+ "parallelize_tools",
308
+ "cache_context",
309
+ "add_schema_validation",
310
+ "batch_model_calls",
311
+ "route_to_smaller_model",
312
+ "split_large_step",
313
+ "runaway_loop",
314
+ "tool_oscillation",
315
+ },
316
+ },
317
+ validation={
318
+ "command": "agentloop replay",
319
+ "acceptance_criteria": observations["validation_criteria"]
320
+ if semantic
321
+ else _acceptance_criteria(finding_type),
322
+ **(
323
+ {"criteria_ref": observations["criteria_ref"], "requires_independent_quality": True}
324
+ if semantic
325
+ else {}
326
+ ),
327
+ },
328
+ metadata={
329
+ "why": card.get("why", ""),
330
+ "cost_status": "unknown"
331
+ if semantic and raw_cost_savings is None
332
+ else plan.get("cost_status"),
333
+ "identity": "semantic-context-v1" if semantic else "content-v1",
334
+ },
335
+ evidence_level=card.get("evidence_level"),
336
+ assumptions=list(card.get("assumptions", [])),
337
+ observations=observations,
338
+ rule_id=card.get("rule_id"),
339
+ rule_version=card.get("rule_version"),
340
+ estimate=deepcopy(card.get("estimate")),
341
+ ranking_requirements=tuple(card.get("ranking_requirements", ())),
342
+ )
343
+
344
+
345
+ def _summary(plan: dict[str, Any], findings: list[OptimizationFinding]) -> dict[str, Any]:
346
+ return {
347
+ **{
348
+ key: plan["savings_aggregation"][key]
349
+ for key in ("unmodeled_latency_findings", "unmodeled_cost_findings")
350
+ if key in plan.get("savings_aggregation", {})
351
+ },
352
+ "finding_count": len(findings),
353
+ "high_severity_count": sum(1 for finding in findings if finding.severity == "high"),
354
+ "patchable_count": sum(1 for finding in findings if finding.rewrite.get("patchable")),
355
+ "estimated_latency_reduction_pct": plan["estimated_after"]["latency_reduction_pct"],
356
+ "estimated_cost_reduction_pct": plan["estimated_after"]["cost_reduction_pct"],
357
+ }
358
+
359
+
360
+ def _severity(
361
+ latency_savings: float,
362
+ cost_savings: float | None,
363
+ confidence: str,
364
+ plan: dict[str, Any],
365
+ ) -> str:
366
+ runtime = float(plan.get("current", {}).get("runtime_ms", 0.0) or 0.0)
367
+ cost = float(plan.get("current", {}).get("estimated_cost_usd", 0.0) or 0.0)
368
+ latency_share = latency_savings / runtime if runtime else 0.0
369
+ cost_share = cost_savings / cost if cost and cost_savings is not None else 0.0
370
+ if confidence == "high" or latency_share >= 0.25 or cost_share >= 0.20:
371
+ return "high"
372
+ if confidence == "medium" or latency_share >= 0.10 or cost_share >= 0.10:
373
+ return "medium"
374
+ return "low"
375
+
376
+
377
+ def _evidence_row(node: dict[str, Any]) -> dict[str, Any]:
378
+ return {
379
+ **({"stage": deepcopy(node["stage"])} if "stage" in node else {}),
380
+ "span_id": node["node_id"],
381
+ "name": node["name"],
382
+ "event_type": node["event_type"],
383
+ "operation_kind": node.get("operation_kind", "unknown"),
384
+ "duration_ms": node["duration_ms"],
385
+ "input_tokens": node.get("input_tokens", 0),
386
+ "output_tokens": node.get("output_tokens", 0),
387
+ "model": node.get("model"),
388
+ "status": node.get("status", "ok"),
389
+ }
390
+
391
+
392
+ def _savings_formula(finding_type: str) -> str:
393
+ formulas = {
394
+ "parallelize_tools": "sum(serial tool durations) - max(tool duration)",
395
+ "cache_context": "current cost * repeated context ratio, capped at 50%",
396
+ "add_schema_validation": "retry duration * 80%",
397
+ "batch_model_calls": "sum(repeated model durations) * 35%",
398
+ "route_to_smaller_model": "model step duration * 25%",
399
+ "split_large_step": "large step duration * 20%",
400
+ "runaway_loop": "repeated loop duration * 30%",
401
+ "tool_oscillation": "alternating tool duration * 40%",
402
+ }
403
+ return formulas.get(finding_type, "optimizer estimate")
404
+
405
+
406
+ def _acceptance_criteria(finding_type: str) -> str:
407
+ criteria = {
408
+ "parallelize_tools": "runtime decreases while tool outputs remain equivalent",
409
+ "cache_context": "input tokens and cost decrease without output quality regression",
410
+ "add_schema_validation": "retry count decreases and output schema validity stays at 100%",
411
+ "batch_model_calls": "runtime decreases and item-level outputs remain equivalent",
412
+ "route_to_smaller_model": "cost decreases while configured quality scorer passes",
413
+ "split_large_step": "context size decreases while final answer quality scorer passes",
414
+ "runaway_loop": "iteration count is bounded and replay passes quality and budget gates",
415
+ "tool_oscillation": "tool-call count decreases without losing required state transitions",
416
+ }
417
+ return criteria.get(
418
+ finding_type, "before/after replay passes configured quality and budget gates"
419
+ )