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/replay.py ADDED
@@ -0,0 +1,753 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from fractions import Fraction
5
+ from typing import Any
6
+
7
+ from agentloop.markdown import markdown_code_span, markdown_table_cell
8
+ from agentloop.tokens import is_token_basis_evaluable
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class ReplayGates:
13
+ max_cost_regression_pct: float = 0.0
14
+ max_latency_regression_pct: float = 0.0
15
+ min_latency_improvement_pct: float = 0.0
16
+ min_cost_improvement_pct: float = 0.0
17
+ require_retry_non_increase: bool = True
18
+ require_schema_valid: bool = False
19
+ min_quality_score: float | None = None
20
+
21
+
22
+ def build_replay_report(
23
+ baseline_trace: Any,
24
+ candidate_trace: Any,
25
+ *,
26
+ gates: ReplayGates | None = None,
27
+ quality_report: dict[str, Any] | None = None,
28
+ ) -> dict[str, Any]:
29
+ """Compare a baseline and candidate trace and evaluate the configured gates.
30
+
31
+ A cost gate is only evaluated when both its inputs are trustworthy: every
32
+ model call has a known rate, *and* the token counts those rates multiply were
33
+ counted rather than approximated. When either is missing the cost gates go
34
+ indeterminate instead of comparing numbers that cannot support the comparison
35
+ (issue #133); ``gates.pricing_known`` and ``gates.token_basis_evaluable`` say
36
+ which input was missing.
37
+ """
38
+
39
+ from agentloop.retention import require_complete_evidence
40
+
41
+ require_complete_evidence(baseline_trace, "replay")
42
+ require_complete_evidence(candidate_trace, "replay")
43
+ from agentloop.integrations.harbor.trial_evidence import require_trial_pair
44
+
45
+ require_trial_pair(baseline_trace, candidate_trace)
46
+ from agentloop.interoperability.cohort_evidence import require_cohort_pair
47
+
48
+ require_cohort_pair(baseline_trace, candidate_trace)
49
+ gates = gates or ReplayGates()
50
+ baseline_report = baseline_trace.report()
51
+ candidate_report = candidate_trace.report()
52
+ baseline = _trace_summary(baseline_trace, baseline_report)
53
+ candidate = _trace_summary(candidate_trace, candidate_report)
54
+ if quality_report is not None:
55
+ if quality_report.get("schema_version") == "2.0":
56
+ from agentloop.structured_quality import decision_span_count, validate_report
57
+
58
+ quality_report = validate_report(
59
+ quality_report, baseline_trace=baseline_trace, candidate_trace=candidate_trace
60
+ )
61
+
62
+ baseline["quality_score"] = _optional_float(quality_report.get("baseline_score"))
63
+ candidate["quality_score"] = _optional_float(quality_report.get("candidate_score"))
64
+ baseline["decision_count"] = decision_span_count(baseline_trace)
65
+ candidate["decision_count"] = decision_span_count(candidate_trace)
66
+ baseline["error_span_count"] = sum(
67
+ event.status == "error" for event in baseline_trace.events
68
+ )
69
+ candidate["error_span_count"] = sum(
70
+ event.status == "error" for event in candidate_trace.events
71
+ )
72
+ else:
73
+ baseline["quality_score"] = float(quality_report.get("baseline_score", 0.0))
74
+ candidate["quality_score"] = float(quality_report.get("candidate_score", 0.0))
75
+ pricing_known = not (
76
+ bool(baseline.get("has_unknown_cost")) or bool(candidate.get("has_unknown_cost"))
77
+ )
78
+ token_basis_evaluable = is_token_basis_evaluable(
79
+ baseline.get("token_status")
80
+ ) and is_token_basis_evaluable(candidate.get("token_status"))
81
+ cost_evaluable = pricing_known and token_basis_evaluable
82
+ deltas = _deltas(baseline, candidate)
83
+ if "external_trial" in baseline or "external_trial" in candidate:
84
+ nullable = {
85
+ "input_tokens": ("input_tokens_delta", "input_token_improvement_pct"),
86
+ "output_tokens": ("output_tokens_delta", "output_token_improvement_pct"),
87
+ "retry_count": ("retry_count_delta", "retry_improvement_pct"),
88
+ "tool_call_count": ("tool_call_count_delta", "tool_call_improvement_pct"),
89
+ "model_call_count": ("model_call_count_delta", "model_call_improvement_pct"),
90
+ }
91
+ for metric, fields in nullable.items():
92
+ if baseline.get(metric) is None or candidate.get(metric) is None:
93
+ for key in fields:
94
+ deltas[key] = None
95
+ versioned_quality = quality_report is not None and quality_report.get("schema_version") == "2.0"
96
+ if versioned_quality or "quality_evidence" in baseline or "quality_evidence" in candidate:
97
+ if baseline.get("quality_score") is None or candidate.get("quality_score") is None:
98
+ deltas["quality_score_delta"] = None
99
+ else:
100
+ deltas["quality_score_delta"] = float(
101
+ Fraction(str(candidate["quality_score"])) - Fraction(str(baseline["quality_score"]))
102
+ )
103
+ if "decision_count" in baseline and "decision_count" in candidate:
104
+ deltas["decision_count_delta"] = (
105
+ candidate["decision_count"] - baseline["decision_count"]
106
+ )
107
+ deltas["error_span_count_delta"] = (
108
+ candidate["error_span_count"] - baseline["error_span_count"]
109
+ )
110
+ if not cost_evaluable:
111
+ # The cost totals are lower bounds when a model is unpriced, and a rate
112
+ # times a word estimate when the token basis is approximate. Either way
113
+ # their delta and percentages are not a valid comparison — expose them as
114
+ # unavailable (None) rather than let a consumer read that arithmetic as a
115
+ # real number. The gates stay indeterminate below.
116
+ deltas["cost_usd_delta"] = None
117
+ deltas["cost_improvement_pct"] = None
118
+ deltas["cost_regression_pct"] = None
119
+ gate_results = _gate_results(
120
+ deltas,
121
+ baseline,
122
+ candidate,
123
+ gates,
124
+ quality_report=quality_report,
125
+ cost_evaluable=cost_evaluable,
126
+ pricing_known=pricing_known,
127
+ )
128
+ if "external_trial" in baseline:
129
+ outcomes = [summary["external_trial"]["outcome"] for summary in (baseline, candidate)]
130
+ available = all(outcome["quality_pass"] is not None for outcome in outcomes)
131
+ correct = available and all(
132
+ outcome["quality_pass"] is True and outcome["execution_status"] == "completed"
133
+ for outcome in outcomes
134
+ )
135
+ gate_results.append(
136
+ _gate(
137
+ "external_task_correctness",
138
+ correct,
139
+ "matching externally supplied verifier criteria passed"
140
+ if correct
141
+ else "external task correctness failed or is unavailable",
142
+ indeterminate=not available,
143
+ )
144
+ )
145
+ passed = all(item["passed"] for item in gate_results)
146
+ indeterminate_gates = [item["name"] for item in gate_results if item.get("indeterminate")]
147
+
148
+ return {
149
+ "baseline": baseline,
150
+ "candidate": candidate,
151
+ "deltas": deltas,
152
+ "gates": {
153
+ "passed": passed,
154
+ "cost_evaluable": cost_evaluable,
155
+ # Split out so a failing/indeterminate cost gate says *which* input
156
+ # was missing: an unpriced model, or token counts that were only
157
+ # approximated (issue #133).
158
+ "pricing_known": pricing_known,
159
+ "token_basis_evaluable": token_basis_evaluable,
160
+ "baseline_token_status": baseline.get("token_status"),
161
+ "candidate_token_status": candidate.get("token_status"),
162
+ "indeterminate": indeterminate_gates,
163
+ "config": {
164
+ "max_cost_regression_pct": gates.max_cost_regression_pct,
165
+ "max_latency_regression_pct": gates.max_latency_regression_pct,
166
+ "min_latency_improvement_pct": gates.min_latency_improvement_pct,
167
+ "min_cost_improvement_pct": gates.min_cost_improvement_pct,
168
+ "require_retry_non_increase": gates.require_retry_non_increase,
169
+ "require_schema_valid": gates.require_schema_valid,
170
+ "min_quality_score": gates.min_quality_score,
171
+ },
172
+ "results": gate_results,
173
+ },
174
+ "quality": quality_report,
175
+ "summary": _summary(
176
+ deltas, passed, cost_evaluable=cost_evaluable, pricing_known=pricing_known
177
+ ),
178
+ }
179
+
180
+
181
+ def replay_report_to_markdown(report: dict[str, Any]) -> str:
182
+ baseline = report["baseline"]
183
+ candidate = report["candidate"]
184
+ deltas = report["deltas"]
185
+ gates = report["gates"]
186
+ status = "passed" if gates["passed"] else "failed"
187
+
188
+ lines = [
189
+ "# AgentLoop Replay Report",
190
+ "",
191
+ f"- Status: {status}",
192
+ f"- Baseline: {markdown_code_span(baseline['name'])} "
193
+ f"{markdown_code_span(baseline['run_id'])}",
194
+ f"- Candidate: {markdown_code_span(candidate['name'])} "
195
+ f"{markdown_code_span(candidate['run_id'])}",
196
+ "",
197
+ "## Metric Deltas",
198
+ "",
199
+ "| Metric | Baseline | Candidate | Delta | Improvement |",
200
+ "|---|---:|---:|---:|---:|",
201
+ _metric_row(
202
+ "Runtime",
203
+ baseline["runtime_ms"],
204
+ candidate["runtime_ms"],
205
+ deltas["runtime_ms_delta"],
206
+ deltas["latency_improvement_pct"],
207
+ "ms",
208
+ ),
209
+ _cost_metric_row(baseline, candidate, deltas),
210
+ _metric_row(
211
+ "Input tokens",
212
+ baseline["input_tokens"],
213
+ candidate["input_tokens"],
214
+ deltas["input_tokens_delta"],
215
+ deltas["input_token_improvement_pct"],
216
+ "count",
217
+ ),
218
+ _metric_row(
219
+ "Output tokens",
220
+ baseline["output_tokens"],
221
+ candidate["output_tokens"],
222
+ deltas["output_tokens_delta"],
223
+ deltas["output_token_improvement_pct"],
224
+ "count",
225
+ ),
226
+ _metric_row(
227
+ "Retries",
228
+ baseline["retry_count"],
229
+ candidate["retry_count"],
230
+ deltas["retry_count_delta"],
231
+ deltas["retry_improvement_pct"],
232
+ "count",
233
+ ),
234
+ _metric_row(
235
+ "Tool calls",
236
+ baseline["tool_call_count"],
237
+ candidate["tool_call_count"],
238
+ deltas["tool_call_count_delta"],
239
+ deltas["tool_call_improvement_pct"],
240
+ "count",
241
+ ),
242
+ _metric_row(
243
+ "Model calls",
244
+ baseline["model_call_count"],
245
+ candidate["model_call_count"],
246
+ deltas["model_call_count_delta"],
247
+ deltas["model_call_improvement_pct"],
248
+ "count",
249
+ ),
250
+ ]
251
+ if (
252
+ baseline.get("schema_validity_pct") is not None
253
+ or candidate.get("schema_validity_pct") is not None
254
+ ):
255
+ lines.append(
256
+ _metric_row(
257
+ "Schema validity",
258
+ baseline.get("schema_validity_pct") or 0.0,
259
+ candidate.get("schema_validity_pct") or 0.0,
260
+ deltas["schema_validity_delta_pct"],
261
+ deltas["schema_validity_improvement_pct"],
262
+ "pct",
263
+ )
264
+ )
265
+ if "decision_count_delta" in deltas:
266
+ lines.append(
267
+ _metric_row(
268
+ "Recorded decision spans",
269
+ baseline["decision_count"],
270
+ candidate["decision_count"],
271
+ deltas["decision_count_delta"],
272
+ None,
273
+ "count",
274
+ )
275
+ )
276
+ lines.append(
277
+ _metric_row(
278
+ "Recorded error spans",
279
+ baseline["error_span_count"],
280
+ candidate["error_span_count"],
281
+ deltas["error_span_count_delta"],
282
+ None,
283
+ "count",
284
+ )
285
+ )
286
+ versioned_quality = (
287
+ report.get("quality", {}).get("schema_version") == "2.0"
288
+ if report.get("quality") is not None
289
+ else False
290
+ )
291
+ versioned_quality = (
292
+ versioned_quality or "quality_evidence" in baseline or "quality_evidence" in candidate
293
+ )
294
+ if (
295
+ baseline.get("quality_score") is not None
296
+ or candidate.get("quality_score") is not None
297
+ or versioned_quality
298
+ ):
299
+ lines.append(
300
+ _metric_row(
301
+ "Quality score",
302
+ baseline.get("quality_score")
303
+ if versioned_quality
304
+ else baseline.get("quality_score") or 0.0,
305
+ candidate.get("quality_score")
306
+ if versioned_quality
307
+ else candidate.get("quality_score") or 0.0,
308
+ deltas["quality_score_delta"],
309
+ deltas["quality_score_improvement_pct"],
310
+ "exact_score" if versioned_quality else "score",
311
+ )
312
+ )
313
+ if report.get("quality"):
314
+ quality = report["quality"]
315
+ lines.extend(
316
+ [
317
+ "",
318
+ "## Quality",
319
+ "",
320
+ f"- Cases: {quality['case_count']}",
321
+ f"- Candidate score: {_format_metric(quality['candidate_score'], 'exact_score' if quality.get('schema_version') == '2.0' else 'score')}",
322
+ f"- Quality delta: {_format_metric(quality['quality_delta'], 'exact_score' if quality.get('schema_version') == '2.0' else 'score')}",
323
+ f"- Failed cases: {quality['failed_case_count']}",
324
+ ]
325
+ )
326
+ lines.extend(
327
+ [
328
+ "",
329
+ "## Gates",
330
+ "",
331
+ "| Gate | Status | Detail |",
332
+ "|---|---|---|",
333
+ ]
334
+ )
335
+ for item in gates["results"]:
336
+ if item.get("indeterminate"):
337
+ gate_status = "indeterminate"
338
+ else:
339
+ gate_status = "pass" if item["passed"] else "fail"
340
+ lines.append(
341
+ f"| {markdown_table_cell(item['name'])} | {gate_status} | "
342
+ f"{markdown_table_cell(item['detail'])} |"
343
+ )
344
+ return "\n".join(lines).rstrip() + "\n"
345
+
346
+
347
+ def _trace_summary(trace: Any, report: dict[str, Any]) -> dict[str, Any]:
348
+ metadata = getattr(trace, "metadata", {}) or {}
349
+ cost = report.get("cost_breakdown") or {}
350
+ result = {
351
+ **(
352
+ {
353
+ "quality_evidence": report["quality_evidence"],
354
+ "decision_count": report["decision_count"],
355
+ "error_span_count": report["error_span_count"],
356
+ }
357
+ if "quality_evidence" in report
358
+ else {}
359
+ ),
360
+ **({"execution": report["execution"]} if "execution" in report else {}),
361
+ **({"stages": report["stages"]} if "stages" in report else {}),
362
+ "run_id": trace.run_id,
363
+ "name": trace.name,
364
+ "runtime_ms": float(report.get("total_runtime_ms", 0.0) or 0.0),
365
+ "estimated_cost_usd": float(report.get("estimated_cost_usd", 0.0) or 0.0),
366
+ # Carried so the cost gates can tell "no cost" apart from "cost unknown":
367
+ # estimated_cost_usd is the *known* portion, has_unknown_cost flags that
368
+ # some model calls had no available rate, and cost_status summarizes
369
+ # completeness (see costs.py / metrics.py).
370
+ "has_unknown_cost": bool(cost.get("has_unknown_cost", False)),
371
+ "cost_status": report.get("cost_status", cost.get("cost_status", "complete")),
372
+ # How exact the token totals below are, and therefore how exact any cost
373
+ # calculated from them is. "unspecified" means the trace predates token
374
+ # provenance; agentloop.tokens.is_token_basis_evaluable() explains why
375
+ # that still gates.
376
+ "token_status": report.get("token_status", cost.get("token_status", "unspecified")),
377
+ "operation_counts": report.get("operation_counts", {}),
378
+ "input_tokens": int(report.get("input_tokens", 0) or 0),
379
+ "output_tokens": int(report.get("output_tokens", 0) or 0),
380
+ "retry_count": int(report.get("retry_count", 0) or 0),
381
+ "tool_call_count": int(report.get("tool_call_count", 0) or 0),
382
+ "model_call_count": int(report.get("model_call_count", 0) or 0),
383
+ "schema_validity_pct": _optional_float(
384
+ report.get("schema_validity_pct")
385
+ if report.get("schema_validity_pct") is not None
386
+ else metadata.get("schema_validity_pct")
387
+ ),
388
+ "schema_valid": _optional_bool(
389
+ report.get("schema_valid")
390
+ if report.get("schema_valid") is not None
391
+ else metadata.get("schema_valid")
392
+ ),
393
+ "quality_score": _optional_float(
394
+ report.get("quality_score")
395
+ if "quality_evidence" in report or report.get("quality_score") is not None
396
+ else metadata.get("quality_score")
397
+ ),
398
+ }
399
+ from agentloop.integrations.harbor.trial_evidence import read_trial, trial_study_values
400
+
401
+ trial = read_trial(trace)
402
+ if trial is not None:
403
+ measurement = trial["measurement"]
404
+ result.update(
405
+ external_trial=trial,
406
+ runtime_ms=measurement["runtime_ms"],
407
+ estimated_cost_usd=measurement["cost_usd"],
408
+ has_unknown_cost=measurement["cost_usd"] is None,
409
+ input_tokens=measurement["input_tokens"],
410
+ output_tokens=measurement["output_tokens"],
411
+ quality_score=trial_study_values(trial)[0],
412
+ retry_count=None,
413
+ tool_call_count=None,
414
+ model_call_count=None,
415
+ quality_basis="configured_binary_acceptance",
416
+ cost_basis="external_reported_agent_context",
417
+ )
418
+ return result
419
+
420
+
421
+ def _deltas(baseline: dict[str, Any], candidate: dict[str, Any]) -> dict[str, Any]:
422
+ base_runtime = float(baseline.get("runtime_ms", baseline.get("total_runtime_ms", 0.0)) or 0.0)
423
+ cand_runtime = float(candidate.get("runtime_ms", candidate.get("total_runtime_ms", 0.0)) or 0.0)
424
+ base_cost = float(baseline.get("estimated_cost_usd", 0.0) or 0.0)
425
+ cand_cost = float(candidate.get("estimated_cost_usd", 0.0) or 0.0)
426
+ base_input = int(baseline.get("input_tokens", 0) or 0)
427
+ cand_input = int(candidate.get("input_tokens", 0) or 0)
428
+ base_output = int(baseline.get("output_tokens", 0) or 0)
429
+ cand_output = int(candidate.get("output_tokens", 0) or 0)
430
+ base_retries = int(baseline.get("retry_count", 0) or 0)
431
+ cand_retries = int(candidate.get("retry_count", 0) or 0)
432
+ base_tools = int(baseline.get("tool_call_count", 0) or 0)
433
+ cand_tools = int(candidate.get("tool_call_count", 0) or 0)
434
+ base_models = int(baseline.get("model_call_count", 0) or 0)
435
+ cand_models = int(candidate.get("model_call_count", 0) or 0)
436
+ base_schema = _optional_float(baseline.get("schema_validity_pct"))
437
+ if base_schema is None:
438
+ base_schema = _schema_validity_from_bool(baseline.get("schema_valid"))
439
+ cand_schema = _optional_float(candidate.get("schema_validity_pct"))
440
+ if cand_schema is None:
441
+ cand_schema = _schema_validity_from_bool(candidate.get("schema_valid"))
442
+ base_quality = _optional_float(baseline.get("quality_score"))
443
+ cand_quality = _optional_float(candidate.get("quality_score"))
444
+
445
+ return {
446
+ "runtime_ms_delta": round(cand_runtime - base_runtime, 3),
447
+ "latency_improvement_pct": _improvement_pct(base_runtime, cand_runtime),
448
+ "latency_regression_pct": _regression_pct(base_runtime, cand_runtime),
449
+ "cost_usd_delta": round(cand_cost - base_cost, 6),
450
+ "cost_improvement_pct": _improvement_pct(base_cost, cand_cost),
451
+ "cost_regression_pct": _regression_pct(base_cost, cand_cost),
452
+ "input_tokens_delta": cand_input - base_input,
453
+ "input_token_improvement_pct": _improvement_pct(base_input, cand_input),
454
+ "output_tokens_delta": cand_output - base_output,
455
+ "output_token_improvement_pct": _improvement_pct(base_output, cand_output),
456
+ "retry_count_delta": cand_retries - base_retries,
457
+ "retry_improvement_pct": _improvement_pct(base_retries, cand_retries),
458
+ "tool_call_count_delta": cand_tools - base_tools,
459
+ "tool_call_improvement_pct": _improvement_pct(base_tools, cand_tools),
460
+ "model_call_count_delta": cand_models - base_models,
461
+ "model_call_improvement_pct": _improvement_pct(base_models, cand_models),
462
+ "schema_validity_delta_pct": round((cand_schema or 0.0) - (base_schema or 0.0), 3),
463
+ "schema_validity_improvement_pct": _relative_gain_pct(base_schema, cand_schema),
464
+ "quality_score_delta": round((cand_quality or 0.0) - (base_quality or 0.0), 4),
465
+ "quality_score_improvement_pct": _relative_gain_pct(base_quality, cand_quality),
466
+ }
467
+
468
+
469
+ def _gate_results(
470
+ deltas: dict[str, Any],
471
+ baseline: dict[str, Any],
472
+ candidate: dict[str, Any],
473
+ gates: ReplayGates,
474
+ *,
475
+ quality_report: dict[str, Any] | None = None,
476
+ cost_evaluable: bool = True,
477
+ pricing_known: bool = True,
478
+ ) -> list[dict[str, Any]]:
479
+ if cost_evaluable:
480
+ cost_regression_gate = _gate(
481
+ "cost_regression",
482
+ deltas["cost_regression_pct"] <= gates.max_cost_regression_pct,
483
+ f"{deltas['cost_regression_pct']:.2f}% regression "
484
+ f"<= {gates.max_cost_regression_pct:.2f}% allowed",
485
+ )
486
+ cost_improvement_gate = _gate(
487
+ "cost_improvement",
488
+ deltas["cost_improvement_pct"] >= gates.min_cost_improvement_pct,
489
+ f"{deltas['cost_improvement_pct']:.2f}% improvement "
490
+ f">= {gates.min_cost_improvement_pct:.2f}% required",
491
+ )
492
+ else:
493
+ # A missing rate — or a token count that was only approximated — is not
494
+ # itself evidence of a regression, so the regression gate stays
495
+ # non-failing rather than blocking every latency-only optimization that
496
+ # uses an unpriced model. But a *required* cost improvement can't be
497
+ # verified, so it must fail — we won't claim an improvement we can't
498
+ # compute.
499
+ reason = _cost_indeterminate_reason(pricing_known)
500
+ cost_regression_gate = _indeterminate_cost_gate(
501
+ "cost_regression", fail=False, reason=reason
502
+ )
503
+ cost_improvement_gate = _indeterminate_cost_gate(
504
+ "cost_improvement", fail=gates.min_cost_improvement_pct > 0, reason=reason
505
+ )
506
+ results = [
507
+ _gate(
508
+ "latency_regression",
509
+ deltas["latency_regression_pct"] <= gates.max_latency_regression_pct,
510
+ f"{deltas['latency_regression_pct']:.2f}% regression <= {gates.max_latency_regression_pct:.2f}% allowed",
511
+ ),
512
+ cost_regression_gate,
513
+ _gate(
514
+ "latency_improvement",
515
+ deltas["latency_improvement_pct"] >= gates.min_latency_improvement_pct,
516
+ f"{deltas['latency_improvement_pct']:.2f}% improvement >= {gates.min_latency_improvement_pct:.2f}% required",
517
+ ),
518
+ cost_improvement_gate,
519
+ ]
520
+ if gates.require_retry_non_increase:
521
+ if "external_trial" in baseline and (
522
+ baseline.get("retry_count") is None or candidate.get("retry_count") is None
523
+ ):
524
+ results.append(
525
+ _gate(
526
+ "retry_non_increase",
527
+ False,
528
+ "source retry counts are unavailable",
529
+ indeterminate=True,
530
+ )
531
+ )
532
+ else:
533
+ base_retries = int(baseline.get("retry_count", 0) or 0)
534
+ cand_retries = int(candidate.get("retry_count", 0) or 0)
535
+ results.append(
536
+ _gate(
537
+ "retry_non_increase",
538
+ cand_retries <= base_retries,
539
+ f"{cand_retries} candidate retries <= {base_retries} baseline retries",
540
+ )
541
+ )
542
+ if gates.require_schema_valid:
543
+ schema_valid = _candidate_schema_valid(candidate)
544
+ results.append(
545
+ _gate(
546
+ "schema_valid",
547
+ schema_valid,
548
+ "candidate schema validity passed"
549
+ if schema_valid
550
+ else "candidate schema validity missing or failed",
551
+ )
552
+ )
553
+ if quality_report is not None:
554
+ fixture_passed = bool(quality_report.get("passed"))
555
+ failed_count = int(quality_report.get("failed_case_count", 0) or 0)
556
+ versioned = quality_report.get("schema_version") == "2.0"
557
+ unavailable = quality_report.get("indeterminate_case_count", 0) if versioned else 0
558
+ results.append(
559
+ _gate(
560
+ "quality_fixtures",
561
+ fixture_passed,
562
+ "supplied quality fixtures passed"
563
+ if fixture_passed
564
+ else (
565
+ f"structured quality criteria failed; {failed_count} nonpassing cases, {unavailable} indeterminate cases; configured aggregate threshold retained"
566
+ if versioned
567
+ else f"{failed_count} supplied quality fixture case(s) failed"
568
+ ),
569
+ indeterminate=bool(unavailable),
570
+ )
571
+ )
572
+ if quality_report is None and "quality_evidence" in candidate:
573
+ evidence = candidate["quality_evidence"]
574
+ results.append(
575
+ _gate(
576
+ "quality_evidence",
577
+ evidence.get("status") == "complete" and evidence.get("passed") is True,
578
+ "attached structured quality evidence passed"
579
+ if evidence.get("passed")
580
+ else "attached structured quality evidence is nonpassing or unavailable",
581
+ indeterminate=evidence.get("status") != "complete",
582
+ )
583
+ )
584
+ if gates.min_quality_score is not None:
585
+ quality_score = _optional_float(candidate.get("quality_score"))
586
+ passed = quality_score is not None and quality_score >= gates.min_quality_score
587
+ detail = (
588
+ f"{quality_score:.4f} candidate quality >= {gates.min_quality_score:.4f} required"
589
+ if quality_score is not None
590
+ else f"candidate quality score missing; {gates.min_quality_score:.4f} required"
591
+ )
592
+ versioned = (
593
+ quality_report is not None
594
+ and quality_report.get("schema_version") == "2.0"
595
+ or "quality_evidence" in candidate
596
+ )
597
+ results.append(
598
+ _gate(
599
+ "quality_score", passed, detail, indeterminate=versioned and quality_score is None
600
+ )
601
+ )
602
+ return results
603
+
604
+
605
+ def _gate(name: str, passed: bool, detail: str, *, indeterminate: bool = False) -> dict[str, Any]:
606
+ return {"name": name, "passed": passed, "detail": detail, "indeterminate": indeterminate}
607
+
608
+
609
+ def _cost_indeterminate_reason(pricing_known: bool) -> str:
610
+ """Explain which missing input made a cost comparison indeterminate."""
611
+
612
+ if not pricing_known:
613
+ return "a model call has no known pricing (see cost_breakdown.unknown_models)"
614
+ return (
615
+ "token counts were approximated from word counts, so any cost derived "
616
+ "from them is not a measurement (see token_status)"
617
+ )
618
+
619
+
620
+ def _indeterminate_cost_gate(
621
+ name: str, *, fail: bool, reason: str = "a model call has no known pricing"
622
+ ) -> dict[str, Any]:
623
+ """A cost gate that couldn't be evaluated from the evidence available.
624
+
625
+ Two things can make a cost comparison meaningless: an unpriced model (the
626
+ totals are lower bounds) or token counts that were only approximated (the
627
+ totals are a rate times a guess). Rather than silently comparing those
628
+ numbers, the gate is marked ``indeterminate``. It fails only when the caller
629
+ explicitly required a cost outcome (``fail``) that therefore can't be
630
+ verified; otherwise it is a non-failing informational gate.
631
+ """
632
+ detail = f"cost could not be evaluated: {reason}. " + (
633
+ "failing because a cost outcome was required." if fail else "not gating."
634
+ )
635
+ return _gate(name, passed=not fail, detail=detail, indeterminate=True)
636
+
637
+
638
+ def _summary(
639
+ deltas: dict[str, Any],
640
+ passed: bool,
641
+ *,
642
+ cost_evaluable: bool = True,
643
+ pricing_known: bool = True,
644
+ ) -> str:
645
+ status = "passed" if passed else "failed"
646
+ if cost_evaluable:
647
+ cost_phrase = f"cost improvement {deltas['cost_improvement_pct']:.2f}%"
648
+ elif not pricing_known:
649
+ cost_phrase = "cost improvement unavailable (unknown model pricing)"
650
+ else:
651
+ cost_phrase = "cost improvement unavailable (token counts are estimates)"
652
+ return (
653
+ f"Replay {status}: latency improvement {deltas['latency_improvement_pct']:.2f}%, "
654
+ f"{cost_phrase}, "
655
+ f"retry delta {deltas['retry_count_delta']}."
656
+ )
657
+
658
+
659
+ def _improvement_pct(baseline: float, candidate: float) -> float:
660
+ if baseline == 0:
661
+ return 0.0 if candidate == 0 else -100.0
662
+ return round(((baseline - candidate) / baseline) * 100, 2)
663
+
664
+
665
+ def _relative_gain_pct(baseline: float | None, candidate: float | None) -> float:
666
+ if baseline is None:
667
+ return 0.0 if candidate is None else 100.0
668
+ if baseline == 0:
669
+ return 0.0 if not candidate else 100.0
670
+ return round((((candidate or 0.0) - baseline) / baseline) * 100, 2)
671
+
672
+
673
+ def _regression_pct(baseline: float, candidate: float) -> float:
674
+ if baseline == 0:
675
+ return 0.0 if candidate == 0 else 100.0
676
+ return round(max(0.0, ((candidate - baseline) / baseline) * 100), 2)
677
+
678
+
679
+ def _metric_row(
680
+ metric: str,
681
+ baseline: float,
682
+ candidate: float,
683
+ delta: float | None,
684
+ improvement: float | None,
685
+ kind: str,
686
+ ) -> str:
687
+ improvement_cell = "unavailable" if improvement is None else f"{improvement:.2f}%"
688
+ return (
689
+ f"| {metric} | {_format_metric(baseline, kind)} | {_format_metric(candidate, kind)} | "
690
+ f"{_format_metric(delta, kind)} | {improvement_cell} |"
691
+ )
692
+
693
+
694
+ def _cost_metric_row(
695
+ baseline: dict[str, Any], candidate: dict[str, Any], deltas: dict[str, Any]
696
+ ) -> str:
697
+ from agentloop.costs import format_cost_usd
698
+
699
+ improvement = deltas["cost_improvement_pct"]
700
+ improvement_cell = "unavailable" if improvement is None else f"{improvement:.2f}%"
701
+ return (
702
+ "| Cost | "
703
+ f"{format_cost_usd(baseline.get('estimated_cost_usd'), baseline.get('cost_status'), decimals=6)} | "
704
+ f"{format_cost_usd(candidate.get('estimated_cost_usd'), candidate.get('cost_status'), decimals=6)} | "
705
+ f"{_format_metric(deltas['cost_usd_delta'], 'usd')} | {improvement_cell} |"
706
+ )
707
+
708
+
709
+ def _format_metric(value: float | None, kind: str) -> str:
710
+ if value is None:
711
+ return "unavailable"
712
+ if kind == "usd":
713
+ return f"${value:.6f}"
714
+ if kind == "ms":
715
+ return f"{value:.3f}ms"
716
+ if kind == "pct":
717
+ return f"{value:.3f}%"
718
+ if kind == "score":
719
+ return f"{value:.4f}"
720
+ if kind == "exact_score":
721
+ return str(value)
722
+ return str(int(value))
723
+
724
+
725
+ def _optional_float(value: Any) -> float | None:
726
+ if value is None or value == "":
727
+ return None
728
+ return float(value)
729
+
730
+
731
+ def _optional_bool(value: Any) -> bool | None:
732
+ if value is None or value == "":
733
+ return None
734
+ if isinstance(value, bool):
735
+ return value
736
+ if isinstance(value, str):
737
+ return value.strip().lower() in {"1", "true", "yes", "pass", "passed", "valid"}
738
+ return bool(value)
739
+
740
+
741
+ def _schema_validity_from_bool(value: Any) -> float | None:
742
+ parsed = _optional_bool(value)
743
+ if parsed is None:
744
+ return None
745
+ return 100.0 if parsed else 0.0
746
+
747
+
748
+ def _candidate_schema_valid(candidate: dict[str, Any]) -> bool:
749
+ schema_valid = _optional_bool(candidate.get("schema_valid"))
750
+ if schema_valid is not None:
751
+ return schema_valid
752
+ validity_pct = _optional_float(candidate.get("schema_validity_pct"))
753
+ return validity_pct is not None and validity_pct >= 100.0