agentloop-profiler 0.4.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.

Potentially problematic release.


This version of agentloop-profiler might be problematic. Click here for more details.

Files changed (50) hide show
  1. agentloop/__init__.py +68 -0
  2. agentloop/audit.py +34 -0
  3. agentloop/autoinstrument.py +102 -0
  4. agentloop/ci.py +135 -0
  5. agentloop/cli.py +876 -0
  6. agentloop/client.py +170 -0
  7. agentloop/config.py +50 -0
  8. agentloop/costs.py +15 -0
  9. agentloop/decorators.py +160 -0
  10. agentloop/demo.py +283 -0
  11. agentloop/doctor.py +344 -0
  12. agentloop/events.py +49 -0
  13. agentloop/exporters.py +60 -0
  14. agentloop/findings.py +215 -0
  15. agentloop/graph.py +312 -0
  16. agentloop/integrations/__init__.py +1 -0
  17. agentloop/integrations/crewai.py +144 -0
  18. agentloop/integrations/langgraph.py +140 -0
  19. agentloop/integrations/openai.py +369 -0
  20. agentloop/integrations/openai_agents.py +212 -0
  21. agentloop/integrations/sdk.py +82 -0
  22. agentloop/integrations/vercel_ai.py +83 -0
  23. agentloop/issues.py +88 -0
  24. agentloop/markdown.py +102 -0
  25. agentloop/metrics.py +123 -0
  26. agentloop/optimizer.py +323 -0
  27. agentloop/otel.py +302 -0
  28. agentloop/otel_ids.py +81 -0
  29. agentloop/patches.py +583 -0
  30. agentloop/plan_export.py +72 -0
  31. agentloop/quality.py +222 -0
  32. agentloop/replay.py +424 -0
  33. agentloop/runtime.py +244 -0
  34. agentloop/savings.py +135 -0
  35. agentloop/server.py +292 -0
  36. agentloop/store.py +840 -0
  37. agentloop/timing.py +124 -0
  38. agentloop/tracer.py +356 -0
  39. agentloop/value.py +243 -0
  40. agentloop/version.py +3 -0
  41. agentloop_profiler-0.4.0.dist-info/METADATA +598 -0
  42. agentloop_profiler-0.4.0.dist-info/RECORD +50 -0
  43. agentloop_profiler-0.4.0.dist-info/WHEEL +5 -0
  44. agentloop_profiler-0.4.0.dist-info/entry_points.txt +2 -0
  45. agentloop_profiler-0.4.0.dist-info/licenses/LICENSE +201 -0
  46. agentloop_profiler-0.4.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +54 -0
  47. agentloop_profiler-0.4.0.dist-info/top_level.txt +2 -0
  48. dashboard/__init__.py +1 -0
  49. dashboard/app.py +698 -0
  50. dashboard/value_view.py +101 -0
agentloop/doctor.py ADDED
@@ -0,0 +1,344 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import os
5
+ import platform
6
+ import socket
7
+ import sys
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.error import URLError
12
+ from urllib.parse import urlsplit
13
+ from urllib.request import Request, urlopen
14
+
15
+ from agentloop.config import postgres_connection_source
16
+ from agentloop.runtime import get_runtime_config
17
+ from agentloop.store import get_store
18
+
19
+
20
+ @dataclass
21
+ class DoctorCheck:
22
+ name: str
23
+ status: str
24
+ detail: str
25
+ fix: str | None = None
26
+
27
+ def to_dict(self) -> dict[str, Any]:
28
+ payload = {"name": self.name, "status": self.status, "detail": self.detail}
29
+ if self.fix:
30
+ payload["fix"] = self.fix
31
+ return payload
32
+
33
+
34
+ def _ok(name: str, detail: str) -> DoctorCheck:
35
+ return DoctorCheck(name=name, status="ok", detail=detail)
36
+
37
+
38
+ def _warn(name: str, detail: str, fix: str | None = None) -> DoctorCheck:
39
+ return DoctorCheck(name=name, status="warn", detail=detail, fix=fix)
40
+
41
+
42
+ def _fail(name: str, detail: str, fix: str | None = None) -> DoctorCheck:
43
+ return DoctorCheck(name=name, status="fail", detail=detail, fix=fix)
44
+
45
+
46
+ def _can_connect(host: str, port: int, timeout: float = 1.0) -> bool:
47
+ try:
48
+ with socket.create_connection((host, port), timeout=timeout):
49
+ return True
50
+ except OSError:
51
+ return False
52
+
53
+
54
+ def _health_check(api_url: str, timeout: float = 2.0) -> DoctorCheck:
55
+ parsed = urlsplit(api_url)
56
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
57
+ return _warn(
58
+ "api_health",
59
+ "AGENTLOOP_API_URL must use http:// or https://",
60
+ "Set AGENTLOOP_API_URL to the AgentLoop HTTP API origin.",
61
+ )
62
+ url = api_url.rstrip("/") + "/health"
63
+ try:
64
+ request = Request(url, headers={"User-Agent": "agentloop-doctor"})
65
+ with urlopen( # nosec B310 - API URL scheme is validated above.
66
+ request, timeout=timeout
67
+ ) as response:
68
+ if response.status == 200:
69
+ return _ok("api_health", f"{url} returned HTTP 200")
70
+ return _warn("api_health", f"{url} returned HTTP {response.status}")
71
+ except URLError as exc:
72
+ return _warn(
73
+ "api_health",
74
+ f"Could not reach {url}: {exc.reason}",
75
+ "Start the API with: agentloop server --host 127.0.0.1 --port 8000",
76
+ )
77
+ except Exception as exc: # pragma: no cover - defensive for platform SSL/socket edge cases
78
+ return _warn("api_health", f"Could not reach {url}: {exc}")
79
+
80
+
81
+ def _ready_check(api_url: str, timeout: float = 2.0) -> DoctorCheck:
82
+ parsed = urlsplit(api_url)
83
+ if parsed.scheme not in {"http", "https"} or not parsed.netloc:
84
+ return _fail(
85
+ "api_ready",
86
+ "AGENTLOOP_API_URL must use http:// or https://",
87
+ "Set AGENTLOOP_API_URL to the deployed AgentLoop HTTP API origin.",
88
+ )
89
+ url = api_url.rstrip("/") + "/readyz"
90
+ try:
91
+ request = Request(url, headers={"User-Agent": "agentloop-production-check"})
92
+ with urlopen( # nosec B310 - API URL scheme is validated above.
93
+ request, timeout=timeout
94
+ ) as response:
95
+ if response.status == 200:
96
+ return _ok("api_ready", f"{url} returned HTTP 200")
97
+ return _fail("api_ready", f"{url} returned HTTP {response.status}")
98
+ except URLError as exc:
99
+ return _fail(
100
+ "api_ready",
101
+ f"Could not reach {url}: {exc.reason}",
102
+ "Deploy the API and confirm /readyz is reachable.",
103
+ )
104
+ except Exception as exc: # pragma: no cover - defensive for platform SSL/socket edge cases
105
+ return _fail("api_ready", f"Could not reach {url}: {exc}")
106
+
107
+
108
+ def _env_enabled(name: str) -> bool:
109
+ return os.getenv(name, "").lower() in {"1", "true", "yes", "on"}
110
+
111
+
112
+ def run_doctor(*, check_api: bool = True, runs_dir: Path | str = "runs") -> dict[str, Any]:
113
+ """Run local setup checks for SDK, CLI, store, and hosted API wiring."""
114
+
115
+ checks: list[DoctorCheck] = []
116
+
117
+ py_version = sys.version_info
118
+ if py_version >= (3, 10):
119
+ checks.append(_ok("python", f"Python {platform.python_version()}"))
120
+ else:
121
+ checks.append(
122
+ _fail(
123
+ "python",
124
+ f"Python {platform.python_version()} is too old",
125
+ "Use Python 3.10 or newer.",
126
+ )
127
+ )
128
+
129
+ for module_name in ["typer", "rich", "pydantic"]:
130
+ try:
131
+ importlib.import_module(module_name)
132
+ checks.append(_ok(f"dependency:{module_name}", "importable"))
133
+ except ImportError:
134
+ checks.append(
135
+ _fail(
136
+ f"dependency:{module_name}",
137
+ "not installed",
138
+ "Run: uv sync --locked --all-extras --dev",
139
+ )
140
+ )
141
+
142
+ out_dir = Path(runs_dir)
143
+ try:
144
+ out_dir.mkdir(parents=True, exist_ok=True)
145
+ probe = out_dir / ".agentloop_write_probe"
146
+ probe.write_text("ok", encoding="utf-8")
147
+ probe.unlink(missing_ok=True)
148
+ checks.append(_ok("runs_dir", f"{out_dir} is writable"))
149
+ except Exception as exc:
150
+ checks.append(
151
+ _fail(
152
+ "runs_dir",
153
+ f"{out_dir} is not writable: {exc}",
154
+ "Choose a writable path or set AGENTLOOP_EXPORT_DIR.",
155
+ )
156
+ )
157
+
158
+ try:
159
+ db = get_store()
160
+ db.init()
161
+ checks.append(_ok("store", f"{db.__class__.__name__} initialized"))
162
+ except Exception as exc:
163
+ checks.append(
164
+ _fail(
165
+ "store",
166
+ f"store initialization failed: {exc}",
167
+ "Check AGENTLOOP_STORE_BACKEND and database credentials.",
168
+ )
169
+ )
170
+
171
+ cfg = get_runtime_config()
172
+ if (
173
+ cfg.auto_upload
174
+ and not cfg.api_key
175
+ and os.getenv("AGENTLOOP_REQUIRE_API_KEY", "").lower() in {"1", "true", "yes", "on"}
176
+ ):
177
+ checks.append(
178
+ _warn(
179
+ "api_key",
180
+ "auto upload is enabled but AGENTLOOP_API_KEY is empty",
181
+ "Create a key with: agentloop create-api-key --project-id demo",
182
+ )
183
+ )
184
+ elif cfg.api_key:
185
+ checks.append(_ok("api_key", "AGENTLOOP_API_KEY is configured"))
186
+ else:
187
+ checks.append(
188
+ _warn("api_key", "no API key configured; OK for local API when auth is disabled")
189
+ )
190
+
191
+ if cfg.auto_upload:
192
+ checks.append(_ok("auto_upload", f"enabled -> {cfg.api_url}"))
193
+ else:
194
+ checks.append(
195
+ _warn(
196
+ "auto_upload",
197
+ "disabled",
198
+ "Set AGENTLOOP_AUTO_UPLOAD=true to upload traces automatically.",
199
+ )
200
+ )
201
+
202
+ if check_api:
203
+ checks.append(_health_check(cfg.api_url))
204
+
205
+ failed = sum(1 for check in checks if check.status == "fail")
206
+ warned = sum(1 for check in checks if check.status == "warn")
207
+ return {
208
+ "ok": failed == 0,
209
+ "failed": failed,
210
+ "warnings": warned,
211
+ "checks": [check.to_dict() for check in checks],
212
+ }
213
+
214
+
215
+ def run_production_check(
216
+ *,
217
+ check_api: bool = True,
218
+ check_store: bool = True,
219
+ allow_http: bool = False,
220
+ ) -> dict[str, Any]:
221
+ """Run strict checks for an internet-facing AgentLoop API/dashboard deployment."""
222
+
223
+ checks: list[DoctorCheck] = []
224
+
225
+ py_version = sys.version_info
226
+ if py_version >= (3, 10):
227
+ checks.append(_ok("python", f"Python {platform.python_version()}"))
228
+ else:
229
+ checks.append(
230
+ _fail(
231
+ "python",
232
+ f"Python {platform.python_version()} is too old",
233
+ "Use Python 3.10 or newer.",
234
+ )
235
+ )
236
+
237
+ backend = os.getenv("AGENTLOOP_STORE_BACKEND", "sqlite").lower()
238
+ if backend == "postgres":
239
+ checks.append(_ok("store_backend", "postgres"))
240
+ else:
241
+ checks.append(
242
+ _fail(
243
+ "store_backend",
244
+ f"{backend} is not production-ready",
245
+ "Set AGENTLOOP_STORE_BACKEND=postgres.",
246
+ )
247
+ )
248
+
249
+ database_source = postgres_connection_source()
250
+ if database_source:
251
+ checks.append(_ok("database_url", f"configured via {database_source}"))
252
+ else:
253
+ checks.append(
254
+ _fail(
255
+ "database_url",
256
+ "missing",
257
+ "Set AGENTLOOP_DATABASE_URL/DATABASE_URL or libpq PGHOST/PGSERVICE variables.",
258
+ )
259
+ )
260
+
261
+ if check_store:
262
+ try:
263
+ db = get_store()
264
+ db.init()
265
+ checks.append(_ok("store", f"{db.__class__.__name__} initialized"))
266
+ except Exception as exc:
267
+ checks.append(
268
+ _fail(
269
+ "store",
270
+ f"store initialization failed: {exc}",
271
+ "Check database credentials and network access.",
272
+ )
273
+ )
274
+
275
+ if _env_enabled("AGENTLOOP_REQUIRE_API_KEY"):
276
+ checks.append(_ok("api_auth", "AGENTLOOP_REQUIRE_API_KEY=true"))
277
+ else:
278
+ checks.append(
279
+ _fail("api_auth", "API key auth is disabled", "Set AGENTLOOP_REQUIRE_API_KEY=true.")
280
+ )
281
+
282
+ admin_key = os.getenv("AGENTLOOP_ADMIN_API_KEY", "")
283
+ weak_admin_values = {
284
+ "",
285
+ "change-me-admin-key",
286
+ "replace-with-a-long-random-admin-secret",
287
+ "dev-secret",
288
+ "admin-secret",
289
+ }
290
+ if admin_key in weak_admin_values or len(admin_key) < 32:
291
+ checks.append(
292
+ _fail(
293
+ "admin_api_key",
294
+ "missing or too weak",
295
+ "Set AGENTLOOP_ADMIN_API_KEY to a generated secret with at least 32 characters.",
296
+ )
297
+ )
298
+ else:
299
+ checks.append(_ok("admin_api_key", "configured"))
300
+
301
+ cors_origins = [
302
+ origin.strip()
303
+ for origin in os.getenv("AGENTLOOP_CORS_ORIGINS", "").split(",")
304
+ if origin.strip()
305
+ ]
306
+ if not cors_origins:
307
+ checks.append(
308
+ _fail("cors_origins", "missing", "Set AGENTLOOP_CORS_ORIGINS to the dashboard origin.")
309
+ )
310
+ elif "*" in cors_origins:
311
+ checks.append(
312
+ _fail(
313
+ "cors_origins",
314
+ "wildcard origin is not allowed for production",
315
+ "Use exact HTTPS origins.",
316
+ )
317
+ )
318
+ else:
319
+ checks.append(_ok("cors_origins", ", ".join(cors_origins)))
320
+
321
+ api_url = os.getenv("AGENTLOOP_API_URL", "")
322
+ if not api_url:
323
+ checks.append(_fail("api_url", "missing", "Set AGENTLOOP_API_URL to the public API URL."))
324
+ elif api_url.startswith("https://"):
325
+ checks.append(_ok("api_url", api_url))
326
+ elif allow_http:
327
+ checks.append(
328
+ _warn("api_url", f"{api_url} is not HTTPS", "Use HTTPS for production traffic.")
329
+ )
330
+ else:
331
+ checks.append(_fail("api_url", f"{api_url} is not HTTPS", "Use an HTTPS public API URL."))
332
+
333
+ if check_api and api_url:
334
+ checks.append(_health_check(api_url))
335
+ checks.append(_ready_check(api_url))
336
+
337
+ failed = sum(1 for check in checks if check.status == "fail")
338
+ warned = sum(1 for check in checks if check.status == "warn")
339
+ return {
340
+ "ok": failed == 0,
341
+ "failed": failed,
342
+ "warnings": warned,
343
+ "checks": [check.to_dict() for check in checks],
344
+ }
agentloop/events.py ADDED
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime, timezone
5
+ from typing import Any
6
+ from uuid import uuid4
7
+
8
+
9
+ def utc_now_iso() -> str:
10
+ return datetime.now(timezone.utc).isoformat()
11
+
12
+
13
+ def new_event_id() -> str:
14
+ return "evt_" + uuid4().hex[:16]
15
+
16
+
17
+ def new_run_id() -> str:
18
+ return "run_" + uuid4().hex[:16]
19
+
20
+
21
+ @dataclass
22
+ class AgentEvent:
23
+ event_id: str
24
+ run_id: str
25
+ event_type: str
26
+ name: str
27
+ started_at: str
28
+ ended_at: str
29
+ duration_ms: float
30
+ parent_id: str | None = None
31
+ model: str | None = None
32
+ input_tokens: int = 0
33
+ output_tokens: int = 0
34
+ input_text: str | None = None
35
+ output_text: str | None = None
36
+ status: str = "ok"
37
+ error: str | None = None
38
+ metadata: dict[str, Any] = field(default_factory=dict)
39
+
40
+ @property
41
+ def total_tokens(self) -> int:
42
+ return self.input_tokens + self.output_tokens
43
+
44
+ def to_dict(self) -> dict[str, Any]:
45
+ return self.__dict__.copy()
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict[str, Any]) -> "AgentEvent":
49
+ return cls(**data)
agentloop/exporters.py ADDED
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from agentloop.markdown import (
8
+ markdown_code_span,
9
+ markdown_heading,
10
+ markdown_table_cell,
11
+ markdown_text,
12
+ )
13
+
14
+
15
+ def export_report_json(report: dict[str, Any], path: str | Path) -> Path:
16
+ out = Path(path)
17
+ out.parent.mkdir(parents=True, exist_ok=True)
18
+ out.write_text(json.dumps(report, indent=2), encoding="utf-8")
19
+ return out
20
+
21
+
22
+ def export_report_markdown(report: dict[str, Any], path: str | Path) -> Path:
23
+ out = Path(path)
24
+ out.parent.mkdir(parents=True, exist_ok=True)
25
+ lines = [
26
+ f"# AgentLoop Report: {markdown_heading(report['name'])}",
27
+ "",
28
+ f"- Run ID: {markdown_code_span(report['run_id'])}",
29
+ f"- Runtime: {report['total_runtime_ms'] / 1000:.2f}s",
30
+ f"- Estimated cost: ${report['estimated_cost_usd']:.4f}",
31
+ f"- Model calls: {report['model_call_count']}",
32
+ f"- Tool calls: {report['tool_call_count']}",
33
+ f"- Retries: {report['retry_count']}",
34
+ f"- Input tokens: {report['input_tokens']}",
35
+ f"- Output tokens: {report['output_tokens']}",
36
+ f"- Repeated context: {report['repeated_context_ratio']:.1%}",
37
+ "",
38
+ "## Recommendations",
39
+ ]
40
+ for rec in report["recommendations"]:
41
+ lines.append(f"- **{markdown_text(rec['title'])}** — {markdown_text(rec['description'])}")
42
+ lines.extend(
43
+ [
44
+ "",
45
+ "## Events",
46
+ "",
47
+ "| Type | Name | Duration ms | Model | Input | Output | Status |",
48
+ "|---|---|---:|---|---:|---:|---|",
49
+ ]
50
+ )
51
+ for event in report["events"]:
52
+ lines.append(
53
+ f"| {markdown_table_cell(event['event_type'])} | "
54
+ f"{markdown_table_cell(event['name'])} | {event['duration_ms']:.2f} | "
55
+ f"{markdown_table_cell(event.get('model') or '')} | "
56
+ f"{event.get('input_tokens', 0)} | {event.get('output_tokens', 0)} | "
57
+ f"{markdown_table_cell(event.get('status', 'ok'))} |"
58
+ )
59
+ out.write_text("\n".join(lines) + "\n", encoding="utf-8")
60
+ return out
agentloop/findings.py ADDED
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from agentloop.markdown import markdown_code_span, markdown_heading, markdown_text
7
+ from agentloop.optimizer import build_optimization_plan
8
+
9
+
10
+ @dataclass
11
+ class OptimizationFinding:
12
+ finding_id: str
13
+ severity: str
14
+ type: str
15
+ title: str
16
+ confidence: str
17
+ affected_spans: list[str]
18
+ evidence: list[dict[str, Any]]
19
+ savings: dict[str, Any]
20
+ rewrite: dict[str, Any]
21
+ validation: dict[str, Any]
22
+ metadata: dict[str, Any] = field(default_factory=dict)
23
+
24
+ def to_dict(self) -> dict[str, Any]:
25
+ return {
26
+ "finding_id": self.finding_id,
27
+ "severity": self.severity,
28
+ "type": self.type,
29
+ "title": self.title,
30
+ "confidence": self.confidence,
31
+ "affected_spans": self.affected_spans,
32
+ "evidence": self.evidence,
33
+ "savings": self.savings,
34
+ "rewrite": self.rewrite,
35
+ "validation": self.validation,
36
+ "metadata": self.metadata,
37
+ }
38
+
39
+
40
+ def build_diagnosis(trace: Any) -> dict[str, Any]:
41
+ plan = build_optimization_plan(trace)
42
+ findings = [
43
+ _finding_from_card(index, card, plan)
44
+ for index, card in enumerate(plan.get("optimization_cards", []), start=1)
45
+ ]
46
+ findings = [finding for finding in findings if finding is not None]
47
+ return {
48
+ "run_id": plan["run_id"],
49
+ "name": plan["name"],
50
+ "current": plan["current"],
51
+ "estimated_after": plan["estimated_after"],
52
+ "summary": _summary(plan, findings),
53
+ "findings": [finding.to_dict() for finding in findings],
54
+ "graph": plan["graph"],
55
+ }
56
+
57
+
58
+ def diagnosis_to_markdown(diagnosis: dict[str, Any]) -> str:
59
+ current = diagnosis["current"]
60
+ after = diagnosis["estimated_after"]
61
+ lines = [
62
+ f"# AgentLoop Diagnosis: {markdown_heading(diagnosis['name'])}",
63
+ "",
64
+ f"- Run ID: {markdown_code_span(diagnosis['run_id'])}",
65
+ f"- Current runtime: {current['runtime_ms'] / 1000:.2f}s",
66
+ f"- Estimated runtime after fixes: {after['runtime_ms'] / 1000:.2f}s",
67
+ f"- Estimated latency reduction: {after['latency_reduction_pct']:.2f}%",
68
+ f"- Current cost: ${current['estimated_cost_usd']:.4f}",
69
+ f"- Estimated cost after fixes: ${after['estimated_cost_usd']:.4f}",
70
+ f"- Estimated cost reduction: {after['cost_reduction_pct']:.2f}%",
71
+ "",
72
+ "## Findings",
73
+ "",
74
+ ]
75
+ findings = diagnosis.get("findings", [])
76
+ if not findings:
77
+ lines.append("No machine-actionable optimization findings detected.")
78
+ for finding in findings:
79
+ savings = finding["savings"]
80
+ rewrite = finding["rewrite"]
81
+ validation = finding["validation"]
82
+ lines.extend(
83
+ [
84
+ f"### {markdown_heading(finding['finding_id'])}: "
85
+ f"{markdown_heading(finding['title'])}",
86
+ "",
87
+ f"- Severity: {markdown_text(finding['severity'])}",
88
+ f"- Type: {markdown_code_span(finding['type'])}",
89
+ f"- Confidence: {markdown_text(finding['confidence'])}",
90
+ "- Affected spans: "
91
+ f"{markdown_text(', '.join(map(str, finding['affected_spans'])) or 'none')}",
92
+ f"- Estimated latency savings: {savings['estimated_latency_savings_ms'] / 1000:.2f}s",
93
+ f"- Estimated cost savings: ${savings['estimated_cost_savings_usd']:.4f}",
94
+ f"- Rewrite: {markdown_text(rewrite['hint'])}",
95
+ f"- Validation: {markdown_text(validation['acceptance_criteria'])}",
96
+ "",
97
+ ]
98
+ )
99
+ return "\n".join(lines).rstrip() + "\n"
100
+
101
+
102
+ def _finding_from_card(
103
+ index: int, card: dict[str, Any], plan: dict[str, Any]
104
+ ) -> OptimizationFinding | None:
105
+ affected_spans = list(card.get("affected_nodes", []))
106
+ finding_type = str(card.get("type", "optimization"))
107
+ latency_savings = float(card.get("estimated_latency_savings_ms", 0.0) or 0.0)
108
+ cost_savings = float(card.get("estimated_cost_savings_usd", 0.0) or 0.0)
109
+ severity = _severity(latency_savings, cost_savings, str(card.get("confidence", "low")), plan)
110
+ node_lookup = {node["node_id"]: node for node in plan.get("graph", {}).get("nodes", [])}
111
+ evidence = [
112
+ _evidence_row(node_lookup[node_id]) for node_id in affected_spans if node_id in node_lookup
113
+ ]
114
+
115
+ return OptimizationFinding(
116
+ finding_id=f"al_{finding_type}_{index:03d}",
117
+ severity=severity,
118
+ type=finding_type,
119
+ title=str(card.get("title", "Optimization opportunity")),
120
+ confidence=str(card.get("confidence", "unknown")),
121
+ affected_spans=affected_spans,
122
+ evidence=evidence,
123
+ savings={
124
+ "estimated_latency_savings_ms": round(latency_savings, 3),
125
+ "estimated_cost_savings_usd": round(cost_savings, 6),
126
+ "formula": _savings_formula(finding_type),
127
+ },
128
+ rewrite={
129
+ "kind": finding_type,
130
+ "hint": str(card.get("rewrite_hint", "")),
131
+ "patchable": finding_type
132
+ in {
133
+ "parallelize_tools",
134
+ "cache_context",
135
+ "add_schema_validation",
136
+ "batch_model_calls",
137
+ "route_to_smaller_model",
138
+ "split_large_step",
139
+ "runaway_loop",
140
+ "tool_oscillation",
141
+ },
142
+ },
143
+ validation={
144
+ "command": "agentloop replay",
145
+ "acceptance_criteria": _acceptance_criteria(finding_type),
146
+ },
147
+ metadata={"why": card.get("why", "")},
148
+ )
149
+
150
+
151
+ def _summary(plan: dict[str, Any], findings: list[OptimizationFinding]) -> dict[str, Any]:
152
+ return {
153
+ "finding_count": len(findings),
154
+ "high_severity_count": sum(1 for finding in findings if finding.severity == "high"),
155
+ "patchable_count": sum(1 for finding in findings if finding.rewrite.get("patchable")),
156
+ "estimated_latency_reduction_pct": plan["estimated_after"]["latency_reduction_pct"],
157
+ "estimated_cost_reduction_pct": plan["estimated_after"]["cost_reduction_pct"],
158
+ }
159
+
160
+
161
+ def _severity(
162
+ latency_savings: float, cost_savings: float, confidence: str, plan: dict[str, Any]
163
+ ) -> str:
164
+ runtime = float(plan.get("current", {}).get("runtime_ms", 0.0) or 0.0)
165
+ cost = float(plan.get("current", {}).get("estimated_cost_usd", 0.0) or 0.0)
166
+ latency_share = latency_savings / runtime if runtime else 0.0
167
+ cost_share = cost_savings / cost if cost else 0.0
168
+ if confidence == "high" or latency_share >= 0.25 or cost_share >= 0.20:
169
+ return "high"
170
+ if confidence == "medium" or latency_share >= 0.10 or cost_share >= 0.10:
171
+ return "medium"
172
+ return "low"
173
+
174
+
175
+ def _evidence_row(node: dict[str, Any]) -> dict[str, Any]:
176
+ return {
177
+ "span_id": node["node_id"],
178
+ "name": node["name"],
179
+ "event_type": node["event_type"],
180
+ "duration_ms": node["duration_ms"],
181
+ "input_tokens": node.get("input_tokens", 0),
182
+ "output_tokens": node.get("output_tokens", 0),
183
+ "model": node.get("model"),
184
+ "status": node.get("status", "ok"),
185
+ }
186
+
187
+
188
+ def _savings_formula(finding_type: str) -> str:
189
+ formulas = {
190
+ "parallelize_tools": "sum(serial tool durations) - max(tool duration)",
191
+ "cache_context": "current cost * repeated context ratio, capped at 50%",
192
+ "add_schema_validation": "retry duration * 80%",
193
+ "batch_model_calls": "sum(repeated model durations) * 35%",
194
+ "route_to_smaller_model": "model step duration * 25%",
195
+ "split_large_step": "large step duration * 20%",
196
+ "runaway_loop": "repeated loop duration * 30%",
197
+ "tool_oscillation": "alternating tool duration * 40%",
198
+ }
199
+ return formulas.get(finding_type, "optimizer estimate")
200
+
201
+
202
+ def _acceptance_criteria(finding_type: str) -> str:
203
+ criteria = {
204
+ "parallelize_tools": "runtime decreases while tool outputs remain equivalent",
205
+ "cache_context": "input tokens and cost decrease without output quality regression",
206
+ "add_schema_validation": "retry count decreases and output schema validity stays at 100%",
207
+ "batch_model_calls": "runtime decreases and item-level outputs remain equivalent",
208
+ "route_to_smaller_model": "cost decreases while configured quality scorer passes",
209
+ "split_large_step": "context size decreases while final answer quality scorer passes",
210
+ "runaway_loop": "iteration count is bounded and replay passes quality and budget gates",
211
+ "tool_oscillation": "tool-call count decreases without losing required state transitions",
212
+ }
213
+ return criteria.get(
214
+ finding_type, "before/after replay passes configured quality and budget gates"
215
+ )