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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- 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
|
+
)
|