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/value.py ADDED
@@ -0,0 +1,306 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from agentloop.costs import is_cost_evaluable
6
+ from agentloop.optimizer import build_optimization_plan
7
+
8
+
9
+ def build_value_report(
10
+ trace: Any,
11
+ *,
12
+ runs_per_month: int = 1000,
13
+ engineer_hourly_rate_usd: float = 150.0,
14
+ incident_cost_usd: float = 500.0,
15
+ ) -> dict[str, Any]:
16
+ """Translate an optimization plan into operational value metrics.
17
+
18
+ The report connects a trace to modeled monthly cost, engineering time, latency,
19
+ and reliability impact. Its assumptions remain explicit so teams can use it for
20
+ capacity planning and optimization prioritization without treating estimates as
21
+ observed financial results.
22
+ """
23
+
24
+ from agentloop.interoperability.evidence import require_external_comparison
25
+
26
+ require_external_comparison(trace, "value estimates")
27
+ if runs_per_month < 0:
28
+ raise ValueError("runs_per_month must be non-negative")
29
+ if engineer_hourly_rate_usd < 0:
30
+ raise ValueError("engineer_hourly_rate_usd must be non-negative")
31
+ if incident_cost_usd < 0:
32
+ raise ValueError("incident_cost_usd must be non-negative")
33
+
34
+ plan = build_optimization_plan(trace)
35
+ current = plan["current"]
36
+ estimated_after = plan["estimated_after"]
37
+ all_cards = plan.get("optimization_cards", [])
38
+ cards = [
39
+ card
40
+ for card in all_cards
41
+ if (card.get("estimate") or {}).get("estimator_id") != "semantic_leaf_removal"
42
+ ]
43
+
44
+ cost_status = plan.get("cost_status", current.get("cost_status", "complete"))
45
+ cost_evaluable = is_cost_evaluable(cost_status)
46
+ current_cost_per_run = float(current.get("estimated_cost_usd", 0.0))
47
+ optimized_cost_raw = estimated_after.get("estimated_cost_usd")
48
+ optimized_cost_per_run = (
49
+ float(optimized_cost_raw) if cost_evaluable and optimized_cost_raw is not None else None
50
+ )
51
+ cost_savings_per_run = (
52
+ max(0.0, current_cost_per_run - optimized_cost_per_run)
53
+ if optimized_cost_per_run is not None
54
+ else None
55
+ )
56
+
57
+ current_runtime_ms = float(current.get("runtime_ms", 0.0))
58
+ optimized_runtime_ms = float(estimated_after.get("runtime_ms", current_runtime_ms))
59
+ latency_savings_ms_per_run = max(0.0, current_runtime_ms - optimized_runtime_ms)
60
+
61
+ retry_count = int(current.get("retry_count", 0) or 0)
62
+ reliability_risk_score = _risk_score(current=current, cards=cards)
63
+ avoided_incident_value = _avoided_incident_value(
64
+ retry_count=retry_count,
65
+ reliability_risk_score=reliability_risk_score,
66
+ incident_cost_usd=incident_cost_usd,
67
+ runs_per_month=runs_per_month,
68
+ )
69
+
70
+ engineering_hours_saved_monthly = _engineering_hours_saved(cards, runs_per_month)
71
+ engineering_value_monthly = engineering_hours_saved_monthly * engineer_hourly_rate_usd
72
+ direct_model_cost_savings_monthly = (
73
+ cost_savings_per_run * runs_per_month if cost_savings_per_run is not None else None
74
+ )
75
+ latency_hours_saved_monthly = (latency_savings_ms_per_run * runs_per_month) / 3_600_000
76
+ non_cost_value_monthly = engineering_value_monthly + avoided_incident_value
77
+ total_value_monthly = (
78
+ direct_model_cost_savings_monthly + non_cost_value_monthly
79
+ if direct_model_cost_savings_monthly is not None
80
+ else None
81
+ )
82
+ pricing = (
83
+ _pricing_scenario(
84
+ monthly_value=total_value_monthly,
85
+ runs_per_month=runs_per_month,
86
+ reliability_risk_score=reliability_risk_score,
87
+ cards=cards,
88
+ )
89
+ if total_value_monthly is not None
90
+ else _unavailable_pricing_scenario(non_cost_value_monthly)
91
+ )
92
+
93
+ return {
94
+ **(
95
+ {"unmodeled_semantic_non_cost_findings": len(all_cards) - len(cards)}
96
+ if len(all_cards) != len(cards)
97
+ else {}
98
+ ),
99
+ "run_id": plan.get("run_id"),
100
+ "name": plan.get("name"),
101
+ "cost_status": cost_status,
102
+ "assumptions": {
103
+ "runs_per_month": runs_per_month,
104
+ "engineer_hourly_rate_usd": engineer_hourly_rate_usd,
105
+ "incident_cost_usd": incident_cost_usd,
106
+ },
107
+ "current": current,
108
+ "estimated_after": estimated_after,
109
+ "per_run": {
110
+ "cost_savings_usd": (
111
+ None if cost_savings_per_run is None else round(cost_savings_per_run, 6)
112
+ ),
113
+ "latency_savings_ms": round(latency_savings_ms_per_run, 3),
114
+ "latency_reduction_pct": estimated_after.get("latency_reduction_pct", 0.0),
115
+ "cost_reduction_pct": estimated_after.get("cost_reduction_pct", 0.0),
116
+ },
117
+ "monthly_value": {
118
+ "direct_model_cost_savings_usd": (
119
+ None
120
+ if direct_model_cost_savings_monthly is None
121
+ else round(direct_model_cost_savings_monthly, 2)
122
+ ),
123
+ "engineering_hours_saved": round(engineering_hours_saved_monthly, 2),
124
+ "engineering_value_usd": round(engineering_value_monthly, 2),
125
+ "latency_hours_saved": round(latency_hours_saved_monthly, 2),
126
+ "avoided_incident_value_usd": round(avoided_incident_value, 2),
127
+ "non_cost_operational_value_usd": round(non_cost_value_monthly, 2),
128
+ "total_value_usd": (
129
+ None if total_value_monthly is None else round(total_value_monthly, 2)
130
+ ),
131
+ },
132
+ "pricing": pricing,
133
+ "reliability": {
134
+ "risk_score": reliability_risk_score,
135
+ "retry_count": retry_count,
136
+ "high_confidence_fixes": sum(1 for card in cards if card.get("confidence") == "high"),
137
+ "top_risks": _top_risks(cards),
138
+ },
139
+ "value_summary": _value_summary(
140
+ monthly_value=total_value_monthly,
141
+ latency_savings_ms_per_run=latency_savings_ms_per_run,
142
+ cost_savings_per_run=cost_savings_per_run,
143
+ cards=cards,
144
+ pricing=pricing,
145
+ ),
146
+ "optimization_cards": all_cards,
147
+ }
148
+
149
+
150
+ def _risk_score(*, current: dict[str, Any], cards: list[dict[str, Any]]) -> int:
151
+ score = 0
152
+ retry_count = int(current.get("retry_count", 0) or 0)
153
+ repeated_context_ratio = float(current.get("repeated_context_ratio", 0.0) or 0.0)
154
+ input_tokens = int(current.get("input_tokens", 0) or 0)
155
+
156
+ score += min(35, retry_count * 12)
157
+ score += min(25, int(repeated_context_ratio * 100))
158
+ score += min(20, input_tokens // 5000)
159
+ score += min(20, len(cards) * 4)
160
+ return max(0, min(100, score))
161
+
162
+
163
+ def _engineering_hours_saved(cards: list[dict[str, Any]], runs_per_month: int) -> float:
164
+ if not cards or runs_per_month == 0:
165
+ return 0.0
166
+
167
+ # Conservative proxy: repeated production inefficiencies create triage and refactor
168
+ # work. Cap it to avoid turning this into a fake precision calculator.
169
+ base_hours = min(12.0, len(cards) * 1.25)
170
+ scale = min(3.0, max(0.25, runs_per_month / 1000))
171
+ high_confidence_bonus = sum(0.5 for card in cards if card.get("confidence") == "high")
172
+ return min(40.0, (base_hours + high_confidence_bonus) * scale)
173
+
174
+
175
+ def _avoided_incident_value(
176
+ *,
177
+ retry_count: int,
178
+ reliability_risk_score: int,
179
+ incident_cost_usd: float,
180
+ runs_per_month: int,
181
+ ) -> float:
182
+ if runs_per_month == 0 or incident_cost_usd == 0:
183
+ return 0.0
184
+ monthly_failure_probability = min(0.25, (retry_count * 0.01) + (reliability_risk_score / 1000))
185
+ volume_multiplier = min(2.0, max(0.25, runs_per_month / 1000))
186
+ return incident_cost_usd * monthly_failure_probability * volume_multiplier
187
+
188
+
189
+ def _pricing_scenario(
190
+ *,
191
+ monthly_value: float,
192
+ runs_per_month: int,
193
+ reliability_risk_score: int,
194
+ cards: list[dict[str, Any]],
195
+ ) -> dict[str, Any]:
196
+ """Build a conservative pricing scenario from modeled operational value.
197
+
198
+ This is not a billing system or price quote. It provides a configurable tier,
199
+ monthly amount, and value-to-price ratio for planning comparisons.
200
+ """
201
+
202
+ if runs_per_month == 0 or monthly_value <= 0 or not cards:
203
+ suggested_plan = "free"
204
+ suggested_monthly_price_usd = 0
205
+ rationale = "No meaningful optimization value was detected for this trace."
206
+ elif monthly_value < 750 and runs_per_month < 1000:
207
+ suggested_plan = "pro"
208
+ suggested_monthly_price_usd = 49
209
+ rationale = "Low-volume workload with measurable optimization value."
210
+ elif monthly_value < 5000 and reliability_risk_score < 60:
211
+ suggested_plan = "team"
212
+ suggested_monthly_price_usd = 499
213
+ rationale = "Mid-volume workload with shared tracing and optimization needs."
214
+ elif monthly_value < 25000:
215
+ suggested_plan = "growth"
216
+ suggested_monthly_price_usd = 1500
217
+ rationale = "Higher-volume workload with material reliability and latency impact."
218
+ else:
219
+ suggested_plan = "enterprise"
220
+ suggested_monthly_price_usd = 5000
221
+ rationale = (
222
+ "Large modeled value; review deployment, retention, support, and usage requirements."
223
+ )
224
+
225
+ value_to_price_ratio = None
226
+ if suggested_monthly_price_usd > 0:
227
+ value_to_price_ratio = round(monthly_value / suggested_monthly_price_usd, 2)
228
+
229
+ return {
230
+ "suggested_plan": suggested_plan,
231
+ "suggested_monthly_price_usd": suggested_monthly_price_usd,
232
+ "estimated_monthly_value_usd": round(monthly_value, 2),
233
+ "value_to_price_ratio": value_to_price_ratio,
234
+ "rationale": rationale,
235
+ "scenario_notes": _scenario_notes(suggested_plan),
236
+ }
237
+
238
+
239
+ def _unavailable_pricing_scenario(non_cost_value: float) -> dict[str, Any]:
240
+ return {
241
+ "suggested_plan": None,
242
+ "suggested_monthly_price_usd": None,
243
+ "estimated_monthly_value_usd": None,
244
+ "estimated_non_cost_monthly_value_usd": round(non_cost_value, 2),
245
+ "value_to_price_ratio": None,
246
+ "rationale": "Pricing is unavailable because one or more model-call costs are unknown.",
247
+ "scenario_notes": [],
248
+ }
249
+
250
+
251
+ def _scenario_notes(plan: str) -> list[str]:
252
+ if plan == "free":
253
+ return ["Local traces", "CLI reports", "Manual optimization plan export"]
254
+ if plan == "pro":
255
+ return ["Local + API trace upload", "Value reports", "Single project workspace"]
256
+ if plan == "team":
257
+ return ["Shared dashboard", "API keys", "Shared projects", "Optimization reports"]
258
+ if plan == "growth":
259
+ return [
260
+ "Higher usage limits",
261
+ "Postgres backend",
262
+ "Extended retention",
263
+ "Operational value review",
264
+ ]
265
+ return ["Private deployment", "Custom retention", "Security review", "Custom integrations"]
266
+
267
+
268
+ def _top_risks(cards: list[dict[str, Any]]) -> list[str]:
269
+ risks = []
270
+ for card in cards[:3]:
271
+ title = str(card.get("title", "Optimization opportunity"))
272
+ confidence = str(card.get("confidence", "unknown"))
273
+ risks.append(f"{title} ({confidence} confidence)")
274
+ return risks
275
+
276
+
277
+ def _value_summary(
278
+ *,
279
+ monthly_value: float | None,
280
+ latency_savings_ms_per_run: float,
281
+ cost_savings_per_run: float | None,
282
+ cards: list[dict[str, Any]],
283
+ pricing: dict[str, Any],
284
+ ) -> str:
285
+ strongest_fix = cards[0]["title"] if cards else "No major bottleneck detected"
286
+ if monthly_value is None or cost_savings_per_run is None:
287
+ return (
288
+ f"AgentLoop found {len(cards)} optimization opportunities. "
289
+ f"The strongest immediate fix is: {strongest_fix}. "
290
+ f"Estimated latency savings are {latency_savings_ms_per_run / 1000:.2f}s per run. "
291
+ "Model-cost savings, total monthly value, and pricing are unavailable because "
292
+ "one or more model-call costs could not be priced."
293
+ )
294
+
295
+ price = pricing.get("suggested_monthly_price_usd", 0)
296
+ plan = pricing.get("suggested_plan", "free")
297
+ price_line = "free local usage" if price == 0 else f"the {plan} plan at ${price:,.0f}/month"
298
+ return (
299
+ f"AgentLoop found {len(cards)} optimization opportunities. "
300
+ f"The strongest immediate fix is: {strongest_fix}. "
301
+ f"At the provided usage assumptions, this trace represents about "
302
+ f"${monthly_value:,.0f}/month in modeled value, with "
303
+ f"{latency_savings_ms_per_run / 1000:.2f}s latency saved and "
304
+ f"${cost_savings_per_run:.4f} model cost saved per run. "
305
+ f"A modeled pricing scenario is {price_line}."
306
+ )
agentloop/version.py ADDED
@@ -0,0 +1,3 @@
1
+ from __future__ import annotations
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,264 @@
1
+ """Portable workflow/stage metadata without runtime or framework dependencies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import math
7
+ import re
8
+ from dataclasses import dataclass, fields
9
+
10
+ from agentloop.operations import normalize_operation_kind, operation_kind
11
+
12
+ WORKFLOW_KEY = "agentloop.workflow"
13
+ STAGE_KEY = "agentloop.stage"
14
+ METADATA_VERSION = "1.0"
15
+ _OUTCOME = re.compile(r"[A-Za-z0-9_.:-]{1,128}\Z")
16
+
17
+
18
+ def _text(value, name, *, optional=False):
19
+ if value is None and optional:
20
+ return
21
+ if not isinstance(value, str) or not value.strip():
22
+ raise ValueError(f"{name} must be nonempty text")
23
+
24
+
25
+ def _copy_metadata(value):
26
+ if value is None:
27
+ return {}
28
+ if value is not None and not isinstance(value, dict):
29
+ raise ValueError("metadata must be a JSON object")
30
+ try:
31
+ return json.loads(json.dumps(value or {}, allow_nan=False))
32
+ except (TypeError, ValueError, RecursionError):
33
+ raise ValueError("metadata must contain finite JSON values") from None
34
+
35
+
36
+ def _values(value):
37
+ # Declarations contain only validated immutable strings/None; no deep copy.
38
+ return {item.name: getattr(value, item.name) for item in fields(value)}
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class WorkflowInfo:
43
+ workflow_id: str
44
+ version: str
45
+ input_schema_ref: str | None = None
46
+ output_schema_ref: str | None = None
47
+ input_ref: str | None = None
48
+ output_ref: str | None = None
49
+
50
+ def __post_init__(self):
51
+ for key, value in _values(self).items():
52
+ _text(value, key, optional=key not in {"workflow_id", "version"})
53
+
54
+ def to_dict(self):
55
+ return {"schema_version": METADATA_VERSION, **_values(self)}
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class StageInfo:
60
+ stage_id: str
61
+ version: str
62
+ kind: str | None = None
63
+ input_schema_ref: str | None = None
64
+ output_schema_ref: str | None = None
65
+ input_ref: str | None = None
66
+ output_ref: str | None = None
67
+
68
+ def __post_init__(self):
69
+ for key, value in _values(self).items():
70
+ _text(value, key, optional=key not in {"stage_id", "version"})
71
+
72
+
73
+ def workflow_metadata(
74
+ info: WorkflowInfo, *, task_id=None, example_id=None, status="unknown", metadata=None
75
+ ):
76
+ """Build portable workflow metadata without importing a host framework."""
77
+ if type(info) is not WorkflowInfo:
78
+ raise ValueError("workflow must be WorkflowInfo")
79
+ if status not in ("unknown", "running", "completed", "failed", "cancelled", "interrupted"):
80
+ raise ValueError("unsupported workflow execution status")
81
+ result = _copy_metadata(metadata)
82
+ if WORKFLOW_KEY in result:
83
+ raise ValueError("use WorkflowInfo instead of supplying the reserved workflow namespace")
84
+ for key, value in (("task_id", task_id), ("example_id", example_id)):
85
+ if value is not None:
86
+ if type(value) is not int and (not isinstance(value, str) or not value):
87
+ raise ValueError(f"{key} must be an integer or nonempty string")
88
+ if key in result and (type(result[key]) is not type(value) or result[key] != value):
89
+ raise ValueError(f"conflicting {key}")
90
+ result[key] = value
91
+ result[WORKFLOW_KEY] = {**info.to_dict(), "status": status, "outcome": None}
92
+ return result
93
+
94
+
95
+ def operation_metadata(kind, *, stage=None, depends_on=None, metadata=None):
96
+ """Annotate new operations or an existing model/tool integration's metadata.
97
+
98
+ ``depends_on=None`` means undeclared. An explicit empty list declares a
99
+ root operation, not general side-effect or parallel-execution safety.
100
+ """
101
+ _text(kind, "kind")
102
+ if stage is not None and type(stage) is not StageInfo:
103
+ raise ValueError("stage must be StageInfo")
104
+ result = _copy_metadata(metadata)
105
+ if STAGE_KEY in result:
106
+ raise ValueError("use StageInfo instead of supplying the reserved stage namespace")
107
+ if "operation_kind" in result and result["operation_kind"] != kind:
108
+ raise ValueError("conflicting operation_kind")
109
+ if depends_on is None and "depends_on" in result:
110
+ depends_on = result["depends_on"]
111
+ if depends_on is not None:
112
+ if not isinstance(depends_on, (list, tuple, set, frozenset)):
113
+ raise ValueError("depends_on must be a collection of span IDs")
114
+ for reference in depends_on:
115
+ _text(reference, "dependency reference")
116
+ references = sorted(set(depends_on))
117
+ if "depends_on" in result and (
118
+ not isinstance(result["depends_on"], list)
119
+ or any(not isinstance(item, str) for item in result["depends_on"])
120
+ or sorted(set(result["depends_on"])) != references
121
+ ):
122
+ raise ValueError("conflicting depends_on")
123
+ result["depends_on"] = references
124
+ else:
125
+ references = []
126
+ result["operation_kind"] = kind
127
+ result[STAGE_KEY] = {
128
+ "schema_version": METADATA_VERSION,
129
+ "stage": _values(stage) if stage is not None else None,
130
+ "dependencies_declared": depends_on is not None,
131
+ "depends_on": references,
132
+ "outcome": None,
133
+ "output_ref": None,
134
+ }
135
+ return result
136
+
137
+
138
+ def _set_outcome(value, outcome, output_ref):
139
+ if not isinstance(outcome, str) or not _OUTCOME.fullmatch(outcome):
140
+ raise ValueError("outcome must be a bounded label, not an output body")
141
+ _text(output_ref, "output_ref", optional=True)
142
+ value["outcome"] = outcome
143
+ if output_ref is not None:
144
+ value["output_ref"] = output_ref
145
+
146
+
147
+ def set_workflow_outcome(trace, outcome, *, output_ref=None):
148
+ """Record a caller-defined result label/reference, independently of task quality."""
149
+ info = trace.metadata.get(WORKFLOW_KEY)
150
+ summary = workflow_summary(trace.metadata)
151
+ if summary is None or summary.get("schema_status") != "supported":
152
+ raise ValueError("trace has no supported workflow declaration")
153
+ _set_outcome(info, outcome, output_ref)
154
+
155
+
156
+ def workflow_summary(metadata):
157
+ """Select declared profiling fields; unknown source metadata remains untouched."""
158
+ raw = metadata.get(WORKFLOW_KEY) if isinstance(metadata, dict) else None
159
+ if not isinstance(raw, dict):
160
+ return None
161
+ if raw.get("schema_version") != METADATA_VERSION:
162
+ return {
163
+ "schema_version": raw.get("schema_version")
164
+ if isinstance(raw.get("schema_version"), str)
165
+ else None,
166
+ "schema_status": "unsupported",
167
+ }
168
+ if any(not isinstance(raw.get(key), str) or not raw[key] for key in ("workflow_id", "version")):
169
+ return {"schema_version": METADATA_VERSION, "schema_status": "invalid"}
170
+ return {
171
+ "schema_version": METADATA_VERSION,
172
+ "schema_status": "supported",
173
+ "kind": "workflow",
174
+ **{
175
+ key: metadata[key]
176
+ for key in ("task_id", "example_id")
177
+ if key in metadata
178
+ and (
179
+ type(metadata[key]) in {str, int, bool}
180
+ or type(metadata[key]) is float
181
+ and math.isfinite(metadata[key])
182
+ )
183
+ },
184
+ **{
185
+ key: raw.get(key) if isinstance(raw.get(key), str) else None
186
+ for key in (
187
+ "workflow_id",
188
+ "version",
189
+ "input_schema_ref",
190
+ "output_schema_ref",
191
+ "input_ref",
192
+ "output_ref",
193
+ "status",
194
+ "outcome",
195
+ )
196
+ },
197
+ "cost_scope": "recorded_model_calls",
198
+ # Measurement scope for model input/output token counts.
199
+ "token_scope": "recorded_model_calls", # nosec B105
200
+ }
201
+
202
+
203
+ def stage_summary(event):
204
+ metadata = getattr(event, "metadata", None)
205
+ raw = metadata.get(STAGE_KEY) if isinstance(metadata, dict) else None
206
+ if not isinstance(raw, dict):
207
+ return None
208
+ if raw.get("schema_version") != METADATA_VERSION:
209
+ return {
210
+ "schema_version": raw.get("schema_version")
211
+ if isinstance(raw.get("schema_version"), str)
212
+ else None,
213
+ "schema_status": "unsupported",
214
+ "kind": "unknown",
215
+ }
216
+ stage = raw.get("stage")
217
+ stage = stage if isinstance(stage, dict) else {}
218
+ raw_kind = stage.get("kind")
219
+ kind = normalize_operation_kind(raw_kind) if raw_kind is not None else operation_kind(event)
220
+ dependencies = raw.get("depends_on")
221
+ dependencies_valid = isinstance(dependencies, list) and all(
222
+ isinstance(value, str) and value for value in dependencies
223
+ )
224
+ if "depends_on" in metadata:
225
+ parallel_dependencies = metadata["depends_on"]
226
+ dependencies_valid = (
227
+ dependencies_valid
228
+ and isinstance(parallel_dependencies, list)
229
+ and all(isinstance(value, str) and value for value in parallel_dependencies)
230
+ and set(parallel_dependencies) == set(dependencies)
231
+ )
232
+
233
+ def selected_text(value):
234
+ return value if isinstance(value, str) else None
235
+
236
+ return {
237
+ "schema_version": METADATA_VERSION,
238
+ "schema_status": "supported",
239
+ "kind": kind,
240
+ "raw_kind": raw_kind if isinstance(raw_kind, str) else None,
241
+ **{
242
+ key: stage.get(key) if isinstance(stage.get(key), str) else None
243
+ for key in (
244
+ "stage_id",
245
+ "version",
246
+ "input_schema_ref",
247
+ "output_schema_ref",
248
+ "input_ref",
249
+ "output_ref",
250
+ )
251
+ },
252
+ "output_ref": selected_text(raw.get("output_ref"))
253
+ or selected_text(stage.get("output_ref")),
254
+ "outcome": selected_text(raw.get("outcome")),
255
+ "execution_status": selected_text(raw.get("execution_status"))
256
+ or {"ok": "completed", "error": "failed"}.get(getattr(event, "status", None), "unknown"),
257
+ "dependency_status": "invalid"
258
+ if raw.get("dependencies_declared") is True and not dependencies_valid
259
+ else "declared"
260
+ if raw.get("dependencies_declared") is True
261
+ else "undeclared",
262
+ "dependencies_declared": raw.get("dependencies_declared") is True and dependencies_valid,
263
+ "depends_on": list(dependencies) if dependencies_valid else [],
264
+ }