agentloop-profiler 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. agentloop/__init__.py +136 -0
  2. agentloop/ablation_protocol.py +229 -0
  3. agentloop/ablations.py +564 -0
  4. agentloop/aggregate_cli.py +88 -0
  5. agentloop/aggregate_stats.py +175 -0
  6. agentloop/aggregates.py +532 -0
  7. agentloop/audit.py +39 -0
  8. agentloop/autoinstrument.py +145 -0
  9. agentloop/batch_experiments.py +295 -0
  10. agentloop/batching.py +95 -0
  11. agentloop/budget_types.py +335 -0
  12. agentloop/budgets.py +539 -0
  13. agentloop/calibration.py +872 -0
  14. agentloop/checkpoint_state.py +306 -0
  15. agentloop/checkpoint_store.py +86 -0
  16. agentloop/ci.py +202 -0
  17. agentloop/cli.py +1314 -0
  18. agentloop/client.py +228 -0
  19. agentloop/completion.py +345 -0
  20. agentloop/completion_types.py +199 -0
  21. agentloop/config.py +50 -0
  22. agentloop/context_controls.py +443 -0
  23. agentloop/context_experiments.py +337 -0
  24. agentloop/context_types.py +216 -0
  25. agentloop/costs.py +667 -0
  26. agentloop/cursor_validation.py +48 -0
  27. agentloop/decorators.py +445 -0
  28. agentloop/demo.py +300 -0
  29. agentloop/doctor.py +341 -0
  30. agentloop/drift.py +361 -0
  31. agentloop/drift_cli.py +147 -0
  32. agentloop/drift_types.py +110 -0
  33. agentloop/drift_windows.py +340 -0
  34. agentloop/entrypoint.py +201 -0
  35. agentloop/estimates.py +133 -0
  36. agentloop/events.py +75 -0
  37. agentloop/experiment_artifacts.py +80 -0
  38. agentloop/experiment_observations.py +169 -0
  39. agentloop/experiment_reports.py +545 -0
  40. agentloop/experiment_types.py +355 -0
  41. agentloop/experiments.py +392 -0
  42. agentloop/exporters.py +117 -0
  43. agentloop/finding_benchmark_types.py +131 -0
  44. agentloop/finding_benchmarks.py +424 -0
  45. agentloop/findings.py +419 -0
  46. agentloop/graph.py +464 -0
  47. agentloop/harness.py +825 -0
  48. agentloop/harness_bench/__init__.py +1 -0
  49. agentloop/harness_bench/cli.py +77 -0
  50. agentloop/harness_bench/drivers.py +96 -0
  51. agentloop/harness_bench/offline.py +507 -0
  52. agentloop/harness_evidence.py +585 -0
  53. agentloop/harness_usage.py +91 -0
  54. agentloop/html_report.py +860 -0
  55. agentloop/integrations/__init__.py +1 -0
  56. agentloop/integrations/crewai.py +144 -0
  57. agentloop/integrations/harbor/__init__.py +1 -0
  58. agentloop/integrations/harbor/atif.py +982 -0
  59. agentloop/integrations/harbor/atif_validation.py +329 -0
  60. agentloop/integrations/harbor/cli.py +207 -0
  61. agentloop/integrations/harbor/manifest.py +86 -0
  62. agentloop/integrations/harbor/trial_evidence.py +196 -0
  63. agentloop/integrations/harbor/trials.py +1109 -0
  64. agentloop/integrations/harbor/verifier.py +118 -0
  65. agentloop/integrations/langgraph.py +150 -0
  66. agentloop/integrations/langgraph_harness.py +466 -0
  67. agentloop/integrations/langgraph_recovery.py +366 -0
  68. agentloop/integrations/omnigent/__init__.py +1 -0
  69. agentloop/integrations/omnigent/cli.py +78 -0
  70. agentloop/integrations/omnigent/evidence.py +93 -0
  71. agentloop/integrations/omnigent/telemetry.py +422 -0
  72. agentloop/integrations/openai.py +415 -0
  73. agentloop/integrations/openai_agents.py +291 -0
  74. agentloop/integrations/sdk.py +82 -0
  75. agentloop/integrations/vercel_ai.py +83 -0
  76. agentloop/interoperability/__init__.py +5 -0
  77. agentloop/interoperability/artifacts.py +112 -0
  78. agentloop/interoperability/cli.py +59 -0
  79. agentloop/interoperability/cohort_evidence.py +175 -0
  80. agentloop/interoperability/cohorts.py +681 -0
  81. agentloop/interoperability/contracts.py +427 -0
  82. agentloop/interoperability/coordination.py +554 -0
  83. agentloop/interoperability/evidence.py +215 -0
  84. agentloop/interoperability/otlp_jsonl.py +775 -0
  85. agentloop/interoperability/otlp_validation.py +496 -0
  86. agentloop/interoperability/registry.py +561 -0
  87. agentloop/interoperability/validation.py +333 -0
  88. agentloop/intervention_service.py +110 -0
  89. agentloop/intervention_store.py +118 -0
  90. agentloop/interventions.py +190 -0
  91. agentloop/issues.py +90 -0
  92. agentloop/judgment_adapters.py +125 -0
  93. agentloop/judgment_benchmark_types.py +98 -0
  94. agentloop/judgment_benchmarks.py +606 -0
  95. agentloop/judgment_types.py +241 -0
  96. agentloop/judgment_views.py +38 -0
  97. agentloop/judgments.py +342 -0
  98. agentloop/loop_guards.py +274 -0
  99. agentloop/loop_types.py +66 -0
  100. agentloop/markdown.py +102 -0
  101. agentloop/metrics.py +342 -0
  102. agentloop/migrations.py +303 -0
  103. agentloop/model_routing.py +470 -0
  104. agentloop/onboarding.py +315 -0
  105. agentloop/operations.py +44 -0
  106. agentloop/optimizer.py +199 -0
  107. agentloop/otel.py +715 -0
  108. agentloop/otel_ids.py +81 -0
  109. agentloop/otel_semantics.py +134 -0
  110. agentloop/parallelism.py +141 -0
  111. agentloop/patches.py +603 -0
  112. agentloop/plan_export.py +126 -0
  113. agentloop/policy_promotion.py +325 -0
  114. agentloop/promotion_evidence.py +304 -0
  115. agentloop/promotion_types.py +209 -0
  116. agentloop/quality.py +446 -0
  117. agentloop/ranking.py +529 -0
  118. agentloop/recovery_evidence.py +96 -0
  119. agentloop/replay.py +753 -0
  120. agentloop/retention.py +375 -0
  121. agentloop/retention_types.py +113 -0
  122. agentloop/reuse_experiments.py +247 -0
  123. agentloop/routing_checks.py +116 -0
  124. agentloop/routing_types.py +241 -0
  125. agentloop/rules.py +396 -0
  126. agentloop/runtime.py +286 -0
  127. agentloop/savings.py +158 -0
  128. agentloop/scheduling_plan.py +99 -0
  129. agentloop/scheduling_types.py +169 -0
  130. agentloop/schema.py +257 -0
  131. agentloop/semantic_rules.py +200 -0
  132. agentloop/semantic_waste.py +345 -0
  133. agentloop/semantic_waste_types.py +88 -0
  134. agentloop/server.py +494 -0
  135. agentloop/store.py +1429 -0
  136. agentloop/structural_experiments.py +431 -0
  137. agentloop/structural_types.py +113 -0
  138. agentloop/structured_quality.py +1020 -0
  139. agentloop/studies.py +629 -0
  140. agentloop/study_statistics.py +58 -0
  141. agentloop/substitution_evidence.py +199 -0
  142. agentloop/substitution_export.py +190 -0
  143. agentloop/substitution_reports.py +407 -0
  144. agentloop/substitution_types.py +284 -0
  145. agentloop/substitutions.py +267 -0
  146. agentloop/timing.py +142 -0
  147. agentloop/tokens.py +188 -0
  148. agentloop/tool_scheduling.py +410 -0
  149. agentloop/trace_sources.py +31 -0
  150. agentloop/tracer.py +494 -0
  151. agentloop/value.py +306 -0
  152. agentloop/version.py +3 -0
  153. agentloop/workflow_types.py +264 -0
  154. agentloop/workflows.py +284 -0
  155. agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
  156. agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
  157. agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
  158. agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
  159. agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
  160. agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
  161. agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
  162. dashboard/__init__.py +5 -0
  163. dashboard/app.py +922 -0
  164. dashboard/streamlit_compat.py +63 -0
  165. dashboard/trace_upload.py +57 -0
  166. dashboard/value_view.py +105 -0
@@ -0,0 +1,315 @@
1
+ """First-use validation of native or standards-imported execution evidence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from collections import Counter
8
+
9
+ from agentloop.metrics import build_report
10
+ from agentloop.operations import operation_counts, operation_kind
11
+ from agentloop.otel import traces_from_otel
12
+ from agentloop.retention import read_retention
13
+ from agentloop.schema import coerce_event_dict, validate_trace_dict
14
+ from agentloop.timing import event_interval_ms, timestamp_ms
15
+ from agentloop.tokens import provenance_grade
16
+ from agentloop.tracer import AgentTrace
17
+
18
+
19
+ def _hash(value):
20
+ return hashlib.sha256(json.dumps(value, sort_keys=True, allow_nan=False).encode()).hexdigest()
21
+
22
+
23
+ def validate_capture(trace: AgentTrace, *, expected_operations: tuple[str, ...] = ()) -> dict:
24
+ """Describe missing evidence without echoing payloads or claiming complete coverage."""
25
+ from agentloop.operations import OPERATION_KINDS
26
+
27
+ if not isinstance(trace, AgentTrace):
28
+ raise TypeError("trace must be AgentTrace")
29
+ if not isinstance(expected_operations, tuple) or any(
30
+ kind not in OPERATION_KINDS for kind in expected_operations
31
+ ):
32
+ raise ValueError("expected_operations must be a tuple of supported operation kinds")
33
+ checks = []
34
+
35
+ def add(code, severity, count, detail):
36
+ if count:
37
+ checks.append({"code": code, "severity": severity, "count": count, "detail": detail})
38
+
39
+ try:
40
+ validate_trace_dict(trace.to_dict())
41
+ for index, event in enumerate(trace.events):
42
+ coerce_event_dict(event.to_dict(), index=index)
43
+ retained = read_retention(trace)
44
+ except (ValueError, TypeError, OverflowError):
45
+ return {
46
+ "schema_version": "1.0",
47
+ "status": "invalid",
48
+ "analysis_allowed": False,
49
+ "checks": [
50
+ {
51
+ "code": "invalid_native_schema",
52
+ "severity": "error",
53
+ "count": 1,
54
+ "detail": "Trace schema, event fields or retained evidence are invalid.",
55
+ }
56
+ ],
57
+ "coverage": "unverified",
58
+ "operation_counts": {},
59
+ }
60
+ by_id = {event.event_id: event for event in trace.events}
61
+ transport_ids = [
62
+ event.metadata["otel_trace_id"]
63
+ for event in trace.events
64
+ if "otel_trace_id" in event.metadata
65
+ ]
66
+ add(
67
+ "transport_trace_id_missing",
68
+ "error",
69
+ sum(not isinstance(value, str) or not value.strip() for value in transport_ids),
70
+ "Imported spans lack a source trace ID; execution boundaries cannot be established.",
71
+ )
72
+ add(
73
+ "transport_trace_ids_mixed",
74
+ "error",
75
+ int(len({value for value in transport_ids if isinstance(value, str) and value}) > 1),
76
+ "One native trace contains spans with several source trace IDs; keep those executions separate.",
77
+ )
78
+ add(
79
+ "no_execution_spans",
80
+ "warning",
81
+ int(not by_id),
82
+ "No execution evidence was captured; evaluation metadata alone does not profile execution.",
83
+ )
84
+ missing = sum(
85
+ event.parent_id is not None and event.parent_id not in by_id for event in trace.events
86
+ )
87
+ add(
88
+ "missing_parent",
89
+ "error",
90
+ missing,
91
+ "A parent is outside this captured trace; export the complete execution boundary.",
92
+ )
93
+ states = {}
94
+ cycles = 0
95
+ for event in trace.events:
96
+ current = event.event_id
97
+ path = []
98
+ while current in by_id and current not in states:
99
+ states[current] = 1
100
+ path.append(current)
101
+ current = by_id[current].parent_id
102
+ if states.get(current) == 1:
103
+ cycles += 1
104
+ for key in path:
105
+ states[key] = 2
106
+ add(
107
+ "parent_cycle",
108
+ "error",
109
+ cycles,
110
+ "Parent relationships contain a cycle and cannot define a valid execution tree.",
111
+ )
112
+ kinds = operation_counts(trace.events)
113
+ add(
114
+ "unsupported_operation",
115
+ "warning",
116
+ kinds.get("unknown", 0),
117
+ "Unknown operation kinds remain unsupported; annotate their actual operation kind.",
118
+ )
119
+ missing_kinds = [kind for kind in expected_operations if not kinds.get(kind)]
120
+ add(
121
+ "expected_operation_missing",
122
+ "warning",
123
+ len(missing_kinds),
124
+ "At least one expected operation kind was not captured; configure its adapter or span hook.",
125
+ )
126
+ models = [event for event in trace.events if operation_kind(event) == "model"]
127
+ incomplete_usage = sum(provenance_grade(event.token_provenance) != "exact" for event in models)
128
+ add(
129
+ "usage_not_exact",
130
+ "warning",
131
+ incomplete_usage,
132
+ "Some model usage is missing, estimated or unspecified; request SDK usage and retain its provenance.",
133
+ )
134
+ missing_models = sum(not event.model for event in models)
135
+ add(
136
+ "model_identity_missing",
137
+ "warning",
138
+ missing_models,
139
+ "Some model calls have no model identity; model pricing and comparisons may be unavailable.",
140
+ )
141
+ start, end = timestamp_ms(trace.started_at), timestamp_ms(trace.ended_at)
142
+ boundary_missing = (
143
+ start is None or end is None or end < start or getattr(trace, "_timing_active", False)
144
+ )
145
+ add(
146
+ "trace_boundary_missing",
147
+ "warning",
148
+ int(boundary_missing),
149
+ "The completed trace boundary is missing or invalid; finish the host execution before analysis.",
150
+ )
151
+ missing_intervals = outside = 0
152
+ for event in trace.events:
153
+ interval = event_interval_ms(event)
154
+ missing_intervals += interval is None
155
+ if interval is not None and not boundary_missing:
156
+ outside += interval[0] < start - 1 or interval[1] > end + 1
157
+ add(
158
+ "span_timing_unavailable",
159
+ "warning",
160
+ missing_intervals,
161
+ "Some span timestamps do not support their recorded durations; timing analysis may use a fallback.",
162
+ )
163
+ add(
164
+ "span_outside_boundary",
165
+ "warning",
166
+ outside,
167
+ "Some spans extend outside the host trace boundary; finish streams and callbacks before finalizing the run.",
168
+ )
169
+ text_fields = sum(
170
+ getattr(event, key) is not None
171
+ for event in trace.events
172
+ for key in ("input_text", "output_text", "error")
173
+ )
174
+ metadata_events = sum(bool(event.metadata) for event in trace.events)
175
+ add(
176
+ "payload_capture_present",
177
+ "warning",
178
+ text_fields,
179
+ "Captured input/output/error text can contain private data; apply an explicit retention policy before sharing.",
180
+ )
181
+ add(
182
+ "metadata_requires_review",
183
+ "notice",
184
+ metadata_events + bool(trace.metadata),
185
+ "Metadata and labels can contain private data, including imported attributes/logs; privacy has not been certified.",
186
+ )
187
+ add(
188
+ "retained_evidence_incomplete",
189
+ "warning",
190
+ int(retained is not None and not retained["complete_evidence"]),
191
+ "Retention omitted analysis evidence; use the original trace for findings or replay.",
192
+ )
193
+ roots = sum(event.parent_id is None for event in trace.events)
194
+ add(
195
+ "multiple_roots",
196
+ "notice",
197
+ int(roots > 1),
198
+ "Several root spans are present; confirm that they belong to one host execution rather than unrelated tasks.",
199
+ )
200
+ invalid = any(item["severity"] == "error" for item in checks)
201
+ partial = any(item["severity"] == "warning" for item in checks)
202
+ return {
203
+ "schema_version": "1.0",
204
+ "status": "invalid" if invalid else "partial" if partial else "ready",
205
+ "analysis_allowed": not invalid
206
+ and bool(by_id)
207
+ and not (retained and not retained["complete_evidence"]),
208
+ "checks": checks,
209
+ "operation_counts": kinds,
210
+ "expected_operations": list(expected_operations),
211
+ "missing_expected_operations": missing_kinds,
212
+ "event_count": len(by_id),
213
+ "root_count": roots,
214
+ "usage_provenance_grades": dict(
215
+ Counter(provenance_grade(event.token_provenance) for event in models)
216
+ ),
217
+ "privacy": {
218
+ "captured_text_field_count": text_fields,
219
+ "metadata_event_count": metadata_events,
220
+ "certified_safe": False,
221
+ },
222
+ "coverage": "Only supplied spans are observable; absence of warnings does not prove complete instrumentation.",
223
+ }
224
+
225
+
226
+ def onboard(
227
+ payload, *, format="native", expected_operations: tuple[str, ...] = (), analyze=True
228
+ ) -> dict:
229
+ """Import without business-logic changes, validate, then produce a payload-free summary."""
230
+ if format not in {"native", "otlp"}:
231
+ raise ValueError(
232
+ "format must be native or otlp (including supported OpenInference attributes)"
233
+ )
234
+ if type(analyze) is not bool:
235
+ raise TypeError("analyze must be boolean")
236
+ if format == "otlp" and not isinstance(payload, (dict, list)):
237
+ raise ValueError("OTLP telemetry must be an object or a list of spans")
238
+ try:
239
+ traces = (
240
+ [AgentTrace.from_dict(payload)] if format == "native" else traces_from_otel(payload)
241
+ )
242
+ except (AttributeError, KeyError, IndexError):
243
+ raise ValueError(
244
+ "Malformed telemetry structure; expected supported trace/span fields"
245
+ ) from None
246
+ results = []
247
+ for trace in traces:
248
+ validation = validate_capture(trace, expected_operations=expected_operations)
249
+ report = None
250
+ findings = []
251
+ if analyze and validation["analysis_allowed"] and len(trace.events) <= 2000:
252
+ original = build_report(trace)
253
+ report = {
254
+ key: original[key]
255
+ for key in (
256
+ "event_count",
257
+ "operation_counts",
258
+ "total_runtime_ms",
259
+ "model_call_count",
260
+ "tool_call_count",
261
+ "input_tokens",
262
+ "output_tokens",
263
+ "token_status",
264
+ "estimated_cost_usd",
265
+ "cost_status",
266
+ "analysis_complete",
267
+ )
268
+ }
269
+ findings = [
270
+ {
271
+ key: item.get(key)
272
+ for key in (
273
+ "rule_id",
274
+ "rule_version",
275
+ "type",
276
+ "confidence",
277
+ "affected_nodes",
278
+ "evidence_level",
279
+ "estimated_latency_savings_ms",
280
+ "estimated_cost_savings_usd",
281
+ )
282
+ }
283
+ for item in original["finding_candidates"]
284
+ ]
285
+ results.append(
286
+ {
287
+ "run_id": trace.run_id,
288
+ "source_sha256": _hash(trace.to_dict()),
289
+ "validation": validation,
290
+ "analysis": report,
291
+ "findings": findings,
292
+ "analysis_status": "performed"
293
+ if report is not None
294
+ else "not_requested"
295
+ if not analyze
296
+ else "unavailable",
297
+ "analysis_limit_events": 2000,
298
+ }
299
+ )
300
+ return {
301
+ "schema_version": "1.0",
302
+ "format": format,
303
+ "source_sha256": _hash(payload),
304
+ "trace_count": len(traces),
305
+ "traces": results,
306
+ "status": "no_execution_data"
307
+ if not traces or all(not trace.events for trace in traces)
308
+ else "invalid"
309
+ if any(item["validation"]["status"] == "invalid" for item in results)
310
+ else "partial"
311
+ if any(item["validation"]["status"] == "partial" for item in results)
312
+ else "ready",
313
+ "privacy": "Raw input/output/error text and arbitrary metadata are excluded. Run/span IDs must be opaque host identifiers.",
314
+ "interpretation": "Findings are estimates requiring validation; supplied telemetry is not a coverage or task-quality guarantee.",
315
+ }
@@ -0,0 +1,44 @@
1
+ """Framework-neutral operation taxonomy layered over legacy event categories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import Counter
6
+ from collections.abc import Mapping
7
+ from typing import Any
8
+
9
+ OPERATION_KINDS = frozenset(
10
+ {
11
+ "agent",
12
+ "workflow",
13
+ "model",
14
+ "tool",
15
+ "retriever",
16
+ "memory",
17
+ "reranker",
18
+ "guardrail",
19
+ "evaluator",
20
+ "retry",
21
+ "rule",
22
+ "classifier",
23
+ "transform",
24
+ "external_service",
25
+ }
26
+ )
27
+ LEGACY_OPERATION_KINDS = {"model_call": "model", "tool_call": "tool", "retry": "retry"}
28
+
29
+
30
+ def normalize_operation_kind(value: Any) -> str:
31
+ """Unknown labels stay in source metadata and analyze as `unknown`."""
32
+ normalized = value.strip().lower() if isinstance(value, str) else ""
33
+ return normalized if normalized in OPERATION_KINDS else "unknown"
34
+
35
+
36
+ def operation_kind(event: Any) -> str:
37
+ metadata = getattr(event, "metadata", None)
38
+ if isinstance(metadata, Mapping) and "operation_kind" in metadata:
39
+ return normalize_operation_kind(metadata["operation_kind"])
40
+ return LEGACY_OPERATION_KINDS.get(getattr(event, "event_type", None), "unknown")
41
+
42
+
43
+ def operation_counts(events: list[Any]) -> dict[str, int]:
44
+ return dict(sorted(Counter(operation_kind(event) for event in events).items()))
agentloop/optimizer.py ADDED
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentloop.costs import is_cost_evaluable
6
+ from agentloop.graph import ExecutionGraph
7
+ from agentloop.rules import AnalysisContext, run_rules
8
+ from agentloop.rules import FindingCandidate as OptimizationCard
9
+ from agentloop.rules import RecommendationType as RecommendationType
10
+ from agentloop.savings import SavingsItem, select_compatible
11
+
12
+
13
+ def build_optimization_plan(trace: Any, report: dict[str, Any] | None = None) -> dict[str, Any]:
14
+ report = report or trace.report()
15
+ graph = ExecutionGraph.from_trace(trace)
16
+ if "finding_candidates" in report:
17
+ cards = [OptimizationCard.from_dict(item) for item in report["finding_candidates"]]
18
+ rule_errors = list(report.get("rule_errors", []))
19
+ else:
20
+ cards, rule_errors = run_rules(AnalysisContext(report=report, graph=graph))
21
+
22
+ current_runtime = report.get("total_runtime_ms", graph.total_runtime_ms())
23
+ current_cost = report.get("estimated_cost_usd", 0.0)
24
+ # `estimated_cost_usd` is only a complete measurement when cost_status is
25
+ # "complete"/"empty"; otherwise it's a lower bound (some model calls were
26
+ # unpriced). Surface that so a consumer doesn't read the cost/savings numbers
27
+ # below as exact for a trace that used unpriced models.
28
+ cost_status = report.get("cost_status", "complete")
29
+ cost_evaluable = is_cost_evaluable(cost_status)
30
+ if not cost_evaluable:
31
+ for card in cards:
32
+ card.estimated_cost_savings_usd = None
33
+ aggregate = _aggregate_savings(
34
+ cards, current_runtime, current_cost, cost_evaluable=cost_evaluable
35
+ )
36
+ total_latency_savings = aggregate["latency_savings_ms"]
37
+ total_cost_savings = aggregate["cost_savings_usd"]
38
+ external = report.get("external_evidence")
39
+ if external is not None and not external["event_timing_complete"]:
40
+ total_latency_savings = None
41
+ aggregate["explanation"].update(
42
+ latency_estimate_complete=False,
43
+ raw_latency_savings_ms=None,
44
+ effective_latency_savings_ms=None,
45
+ )
46
+
47
+ return {
48
+ **({"semantic_waste": report["semantic_waste"]} if "semantic_waste" in report else {}),
49
+ **(
50
+ {
51
+ "semantic_judgments": report["semantic_judgments"],
52
+ "evidence_categories": report["evidence_categories"],
53
+ }
54
+ if "semantic_judgments" in report
55
+ else {}
56
+ ),
57
+ "run_id": trace.run_id,
58
+ "name": trace.name,
59
+ "cost_status": cost_status,
60
+ "current": {
61
+ "runtime_ms": current_runtime,
62
+ "estimated_cost_usd": current_cost,
63
+ "cost_status": cost_status,
64
+ "input_tokens": report.get("input_tokens", 0),
65
+ "output_tokens": report.get("output_tokens", 0),
66
+ "retry_count": report.get("retry_count", 0),
67
+ "repeated_context_ratio": report.get("repeated_context_ratio", 0.0),
68
+ },
69
+ "estimated_after": {
70
+ "runtime_ms": round(max(0.0, current_runtime - total_latency_savings), 3)
71
+ if current_runtime is not None and total_latency_savings is not None
72
+ else None,
73
+ "estimated_cost_usd": (
74
+ round(max(0.0, current_cost - total_cost_savings), 6)
75
+ if total_cost_savings is not None
76
+ else None
77
+ ),
78
+ "cost_status": cost_status,
79
+ "latency_reduction_pct": None
80
+ if total_latency_savings is None
81
+ else (
82
+ round((total_latency_savings / current_runtime) * 100, 2)
83
+ if current_runtime
84
+ else 0.0
85
+ ),
86
+ "cost_reduction_pct": (
87
+ round((total_cost_savings / current_cost) * 100, 2)
88
+ if total_cost_savings is not None and current_cost
89
+ else (0.0 if total_cost_savings == 0 else None)
90
+ ),
91
+ },
92
+ "savings_aggregation": aggregate["explanation"],
93
+ "graph": graph.to_dict(),
94
+ "optimization_cards": [card.to_dict() for card in cards],
95
+ "rule_errors": rule_errors,
96
+ "analysis_complete": not rule_errors,
97
+ }
98
+
99
+
100
+ def _aggregate_savings(
101
+ cards: list[OptimizationCard],
102
+ current_runtime: float | None,
103
+ current_cost: float | None,
104
+ *,
105
+ cost_evaluable: bool = True,
106
+ ) -> dict[str, Any]:
107
+ """Combine per-card savings without double-counting overlapping spans.
108
+
109
+ Cards that share any affected span compete for the same work, so their
110
+ estimates are mutually exclusive alternatives, not additive. Small overlap
111
+ components are solved exactly. If an unusually large component crosses the
112
+ selector's exact limit, the same compatible subset still drives both
113
+ latency and cost totals, but the explanation marks it as an approximation
114
+ rather than claiming a proven maximum. Totals are capped at current runtime
115
+ and cost because a plan cannot save more than the run spent.
116
+ """
117
+ items = [
118
+ SavingsItem(
119
+ spans=frozenset(card.affected_nodes or []),
120
+ latency_ms=float(card.estimated_latency_savings_ms or 0.0),
121
+ cost_usd=float(card.estimated_cost_savings_usd or 0.0),
122
+ )
123
+ for card in cards
124
+ ]
125
+ selection = select_compatible(items)
126
+ capped_latency = (
127
+ None
128
+ if current_runtime is None
129
+ else min(selection.latency_ms, current_runtime)
130
+ if current_runtime
131
+ else 0.0
132
+ )
133
+ capped_cost = (
134
+ (min(selection.cost_usd, current_cost) if current_cost else 0.0) if cost_evaluable else None
135
+ )
136
+ raw_latency = sum(card.estimated_latency_savings_ms or 0.0 for card in cards)
137
+ raw_cost = (
138
+ sum(float(card.estimated_cost_savings_usd or 0.0) for card in cards)
139
+ if cost_evaluable
140
+ else None
141
+ )
142
+ if selection.optimal:
143
+ rule = (
144
+ "cards sharing affected spans are mutually exclusive alternatives; "
145
+ "totals come from the proven-optimal compatible (span-disjoint) subset "
146
+ "that maximizes latency savings, breaking ties by cost savings, capped "
147
+ "at current runtime/cost"
148
+ )
149
+ else:
150
+ rule = (
151
+ "cards sharing affected spans are mutually exclusive alternatives; "
152
+ "components above the exact selection limit use a deterministic greedy "
153
+ "approximation, so totals may understate achievable savings; latency and "
154
+ "cost still come from one compatible subset and are capped at current runtime/cost"
155
+ )
156
+ return {
157
+ "latency_savings_ms": capped_latency,
158
+ "cost_savings_usd": capped_cost,
159
+ "explanation": {
160
+ **(
161
+ {
162
+ "unmodeled_cost_findings": sum(
163
+ card.estimated_cost_savings_usd is None
164
+ and (card.estimate or {}).get("estimator_id") == "semantic_leaf_removal"
165
+ for card in cards
166
+ ),
167
+ "cost_estimate_complete": False,
168
+ }
169
+ if any(
170
+ card.estimated_cost_savings_usd is None
171
+ and (card.estimate or {}).get("estimator_id") == "semantic_leaf_removal"
172
+ for card in cards
173
+ )
174
+ else {}
175
+ ),
176
+ **(
177
+ {
178
+ "unmodeled_latency_findings": sum(
179
+ card.estimated_latency_savings_ms is None for card in cards
180
+ ),
181
+ "latency_estimate_complete": False,
182
+ }
183
+ if any(card.estimated_latency_savings_ms is None for card in cards)
184
+ else {}
185
+ ),
186
+ "rule": rule,
187
+ "card_count": len(cards),
188
+ "selected_card_indexes": list(selection.indices),
189
+ "selection_optimal": selection.optimal,
190
+ "selection_algorithm": selection.algorithm,
191
+ "exact_component_limit": selection.exact_component_limit,
192
+ "raw_latency_savings_ms": round(raw_latency, 3),
193
+ "effective_latency_savings_ms": None
194
+ if capped_latency is None
195
+ else round(capped_latency, 3),
196
+ "raw_cost_savings_usd": None if raw_cost is None else round(raw_cost, 6),
197
+ "effective_cost_savings_usd": (None if capped_cost is None else round(capped_cost, 6)),
198
+ },
199
+ }