agent-lifecycle-kit 1.50.1__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 (227) hide show
  1. agent_lifecycle/__init__.py +5 -0
  2. agent_lifecycle/__main__.py +5 -0
  3. agent_lifecycle/_version.py +3 -0
  4. agent_lifecycle/adapter_sessions/__init__.py +41 -0
  5. agent_lifecycle/adapter_sessions/contracts.py +133 -0
  6. agent_lifecycle/adapter_sessions/env.py +44 -0
  7. agent_lifecycle/adapter_sessions/launcher.py +101 -0
  8. agent_lifecycle/adapter_sessions/process.py +39 -0
  9. agent_lifecycle/adapter_sessions/redaction.py +30 -0
  10. agent_lifecycle/adapter_sessions/session_store.py +64 -0
  11. agent_lifecycle/adapter_sessions/task_intake.py +450 -0
  12. agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
  13. agent_lifecycle/audit/__init__.py +67 -0
  14. agent_lifecycle/audit/bug_forensics.py +94 -0
  15. agent_lifecycle/audit/implementation.py +516 -0
  16. agent_lifecycle/audit/ownership.py +119 -0
  17. agent_lifecycle/audit/proof_integrity.py +814 -0
  18. agent_lifecycle/audit/review_verdict.py +116 -0
  19. agent_lifecycle/changesets/__init__.py +5 -0
  20. agent_lifecycle/changesets/git.py +35 -0
  21. agent_lifecycle/cli/__init__.py +5 -0
  22. agent_lifecycle/cli/adapter.py +360 -0
  23. agent_lifecycle/cli/dispatch.py +1220 -0
  24. agent_lifecycle/cli/followup.py +89 -0
  25. agent_lifecycle/cli/main.py +41 -0
  26. agent_lifecycle/cli/metrics_parser.py +49 -0
  27. agent_lifecycle/cli/parsers.py +618 -0
  28. agent_lifecycle/cli/policy.py +101 -0
  29. agent_lifecycle/cli/progress_hooks.py +117 -0
  30. agent_lifecycle/cli/worktree.py +74 -0
  31. agent_lifecycle/compiler/__init__.py +17 -0
  32. agent_lifecycle/compiler/small_model_packets.py +322 -0
  33. agent_lifecycle/compiler/task_packets.py +229 -0
  34. agent_lifecycle/context/__init__.py +20 -0
  35. agent_lifecycle/context/episode_retrieval.py +30 -0
  36. agent_lifecycle/context/external_memory.py +71 -0
  37. agent_lifecycle/context/profiles.py +104 -0
  38. agent_lifecycle/context/rendering.py +226 -0
  39. agent_lifecycle/contracts/__init__.py +25 -0
  40. agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
  41. agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
  42. agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
  43. agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
  44. agent_lifecycle/contracts/audit_schemas.py +133 -0
  45. agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
  46. agent_lifecycle/contracts/canonical.py +55 -0
  47. agent_lifecycle/contracts/compatibility.py +242 -0
  48. agent_lifecycle/contracts/context_model_schemas.py +265 -0
  49. agent_lifecycle/contracts/core_schemas.py +252 -0
  50. agent_lifecycle/contracts/cross_check_schemas.py +121 -0
  51. agent_lifecycle/contracts/errors.py +26 -0
  52. agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
  53. agent_lifecycle/contracts/host_capability_schemas.py +64 -0
  54. agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
  55. agent_lifecycle/contracts/metric_schemas.py +315 -0
  56. agent_lifecycle/contracts/paths.py +31 -0
  57. agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
  58. agent_lifecycle/contracts/policy_schemas.py +350 -0
  59. agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
  60. agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
  61. agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
  62. agent_lifecycle/contracts/release_contract_schemas.py +476 -0
  63. agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
  64. agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
  65. agent_lifecycle/contracts/review_quality_schemas.py +109 -0
  66. agent_lifecycle/contracts/runner_schemas.py +91 -0
  67. agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
  68. agent_lifecycle/contracts/sandbox_schemas.py +159 -0
  69. agent_lifecycle/contracts/schema_builders.py +23 -0
  70. agent_lifecycle/contracts/schemas.py +93 -0
  71. agent_lifecycle/contracts/status_goal_schemas.py +469 -0
  72. agent_lifecycle/contracts/task_template_schemas.py +96 -0
  73. agent_lifecycle/contracts/usage_export_schemas.py +68 -0
  74. agent_lifecycle/diagnostics/__init__.py +17 -0
  75. agent_lifecycle/diagnostics/bundles.py +124 -0
  76. agent_lifecycle/diagnostics/readiness.py +733 -0
  77. agent_lifecycle/evidence_index/__init__.py +39 -0
  78. agent_lifecycle/evidence_index/core.py +272 -0
  79. agent_lifecycle/evidence_index/episode_index.py +329 -0
  80. agent_lifecycle/evidence_index/external_context.py +276 -0
  81. agent_lifecycle/followup/__init__.py +23 -0
  82. agent_lifecycle/followup/records.py +404 -0
  83. agent_lifecycle/freeze/__init__.py +5 -0
  84. agent_lifecycle/freeze/locks.py +23 -0
  85. agent_lifecycle/goal/__init__.py +15 -0
  86. agent_lifecycle/goal/records.py +244 -0
  87. agent_lifecycle/goal/view.py +109 -0
  88. agent_lifecycle/host_protocol/__init__.py +65 -0
  89. agent_lifecycle/host_protocol/acp_capability.py +195 -0
  90. agent_lifecycle/host_protocol/capabilities.py +268 -0
  91. agent_lifecycle/host_protocol/contracts.py +214 -0
  92. agent_lifecycle/host_protocol/event_capture.py +390 -0
  93. agent_lifecycle/host_protocol/events.py +196 -0
  94. agent_lifecycle/host_protocol/inspection.py +194 -0
  95. agent_lifecycle/host_protocol/inspection_common.py +310 -0
  96. agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
  97. agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
  98. agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
  99. agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
  100. agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
  101. agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
  102. agent_lifecycle/host_protocol/receipts.py +58 -0
  103. agent_lifecycle/host_protocol/scaffold.py +329 -0
  104. agent_lifecycle/host_protocol/validation.py +229 -0
  105. agent_lifecycle/imports/__init__.py +76 -0
  106. agent_lifecycle/imports/agentskills_profile.py +54 -0
  107. agent_lifecycle/imports/bmad_profile.py +49 -0
  108. agent_lifecycle/imports/constitution_adr.py +91 -0
  109. agent_lifecycle/imports/dialect_profiles.py +121 -0
  110. agent_lifecycle/imports/external_agent.py +28 -0
  111. agent_lifecycle/imports/external_dialects.py +486 -0
  112. agent_lifecycle/imports/external_workflow.py +28 -0
  113. agent_lifecycle/imports/multi_markdown.py +111 -0
  114. agent_lifecycle/imports/openspec_profile.py +49 -0
  115. agent_lifecycle/imports/planning.py +536 -0
  116. agent_lifecycle/imports/spec_kit_profile.py +49 -0
  117. agent_lifecycle/imports/spec_kitty_profile.py +49 -0
  118. agent_lifecycle/metrics/__init__.py +60 -0
  119. agent_lifecycle/metrics/cost_collection.py +315 -0
  120. agent_lifecycle/metrics/costs.py +203 -0
  121. agent_lifecycle/metrics/outcome_index.py +330 -0
  122. agent_lifecycle/metrics/phase_resources.py +208 -0
  123. agent_lifecycle/metrics/recommendations.py +507 -0
  124. agent_lifecycle/metrics/regression_signals.py +64 -0
  125. agent_lifecycle/metrics/usage_export.py +390 -0
  126. agent_lifecycle/model_routing/__init__.py +19 -0
  127. agent_lifecycle/model_routing/profiles.py +323 -0
  128. agent_lifecycle/model_routing/receipts.py +140 -0
  129. agent_lifecycle/model_routing/resolver.py +566 -0
  130. agent_lifecycle/neutrality/__init__.py +12 -0
  131. agent_lifecycle/neutrality/__main__.py +6 -0
  132. agent_lifecycle/neutrality/authority.py +265 -0
  133. agent_lifecycle/neutrality/canonical.py +28 -0
  134. agent_lifecycle/neutrality/cli.py +294 -0
  135. agent_lifecycle/neutrality/ed25519.py +124 -0
  136. agent_lifecycle/neutrality/errors.py +89 -0
  137. agent_lifecycle/neutrality/gate.py +269 -0
  138. agent_lifecycle/neutrality/paths.py +92 -0
  139. agent_lifecycle/neutrality/policy.py +76 -0
  140. agent_lifecycle/neutrality/receipt.py +135 -0
  141. agent_lifecycle/neutrality/scanner.py +310 -0
  142. agent_lifecycle/neutrality/test_runner.py +116 -0
  143. agent_lifecycle/planning/__init__.py +47 -0
  144. agent_lifecycle/planning/acceptance_markdown.py +159 -0
  145. agent_lifecycle/planning/completeness.py +362 -0
  146. agent_lifecycle/planning/continuity.py +255 -0
  147. agent_lifecycle/planning/templates.py +294 -0
  148. agent_lifecycle/planning/tiering.py +87 -0
  149. agent_lifecycle/planning/validation.py +72 -0
  150. agent_lifecycle/policy/__init__.py +37 -0
  151. agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
  152. agent_lifecycle/policy/apply.py +44 -0
  153. agent_lifecycle/policy/proposals.py +189 -0
  154. agent_lifecycle/policy/quality_floor.py +144 -0
  155. agent_lifecycle/policy/runtime_receipts.py +160 -0
  156. agent_lifecycle/py.typed +1 -0
  157. agent_lifecycle/quality/__init__.py +89 -0
  158. agent_lifecycle/quality/bug_forensics.py +727 -0
  159. agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
  160. agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
  161. agent_lifecycle/quality/cross_check.py +437 -0
  162. agent_lifecycle/quality/failure_classification.py +318 -0
  163. agent_lifecycle/quality/packs.py +265 -0
  164. agent_lifecycle/reporting/__init__.py +37 -0
  165. agent_lifecycle/reporting/change_summary.py +156 -0
  166. agent_lifecycle/reporting/event_feed.py +101 -0
  167. agent_lifecycle/reporting/progress_bridge.py +153 -0
  168. agent_lifecycle/reporting/progress_hooks.py +161 -0
  169. agent_lifecycle/reporting/progress_terminal.py +95 -0
  170. agent_lifecycle/reporting/progress_view.py +284 -0
  171. agent_lifecycle/reporting/status_view.py +125 -0
  172. agent_lifecycle/reporting/usage_export.py +59 -0
  173. agent_lifecycle/review/__init__.py +5 -0
  174. agent_lifecycle/review/validation.py +29 -0
  175. agent_lifecycle/review_mesh/__init__.py +83 -0
  176. agent_lifecycle/review_mesh/assignments.py +160 -0
  177. agent_lifecycle/review_mesh/contracts.py +685 -0
  178. agent_lifecycle/review_mesh/operator_templates.py +320 -0
  179. agent_lifecycle/review_mesh/quorum.py +38 -0
  180. agent_lifecycle/review_mesh/recommendation.py +487 -0
  181. agent_lifecycle/review_mesh/results.py +98 -0
  182. agent_lifecycle/review_mesh/synthesis.py +102 -0
  183. agent_lifecycle/runner/__init__.py +47 -0
  184. agent_lifecycle/runner/attempt_snapshots.py +244 -0
  185. agent_lifecycle/runner/core.py +653 -0
  186. agent_lifecycle/runner/sandbox_receipts.py +628 -0
  187. agent_lifecycle/specification/__init__.py +23 -0
  188. agent_lifecycle/specification/completion_check.py +135 -0
  189. agent_lifecycle/specification/completion_gate.py +501 -0
  190. agent_lifecycle/specification/completion_signal.py +75 -0
  191. agent_lifecycle/specification/validation.py +48 -0
  192. agent_lifecycle/workflow/__init__.py +79 -0
  193. agent_lifecycle/workflow/artifacts.py +74 -0
  194. agent_lifecycle/workflow/budget_decisions.py +370 -0
  195. agent_lifecycle/workflow/budget_policy.py +140 -0
  196. agent_lifecycle/workflow/budget_receipts.py +70 -0
  197. agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
  198. agent_lifecycle/workflow/controller.py +45 -0
  199. agent_lifecycle/workflow/events.py +50 -0
  200. agent_lifecycle/workflow/final_proof_integrity.py +74 -0
  201. agent_lifecycle/workflow/finalization.py +422 -0
  202. agent_lifecycle/workflow/gates.py +185 -0
  203. agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
  204. agent_lifecycle/workflow/leases.py +220 -0
  205. agent_lifecycle/workflow/lineage.py +108 -0
  206. agent_lifecycle/workflow/managed_runner.py +167 -0
  207. agent_lifecycle/workflow/model_usage.py +106 -0
  208. agent_lifecycle/workflow/next_action.py +86 -0
  209. agent_lifecycle/workflow/operation_kernel.py +98 -0
  210. agent_lifecycle/workflow/plan_adoption.py +439 -0
  211. agent_lifecycle/workflow/query.py +52 -0
  212. agent_lifecycle/workflow/review_mesh_gate.py +93 -0
  213. agent_lifecycle/workflow/reviews.py +168 -0
  214. agent_lifecycle/workflow/run_transitions.py +163 -0
  215. agent_lifecycle/workflow/sandbox_policy.py +214 -0
  216. agent_lifecycle/workflow/selectors.py +57 -0
  217. agent_lifecycle/workflow/state.py +130 -0
  218. agent_lifecycle/workflow/task_transitions.py +421 -0
  219. agent_lifecycle/worktree/__init__.py +17 -0
  220. agent_lifecycle/worktree/isolation.py +407 -0
  221. agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
  222. agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
  223. agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
  224. agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
  225. agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
  226. agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
  227. agent_lifecycle_kit-1.50.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,390 @@
1
+ """Usage/session export aggregation from local lifecycle artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest, load_json_object, sha256_hex
10
+
11
+ USAGE_EXPORT_SCHEMA = "agent-usage-export.v1"
12
+ USAGE_EXPORT_VALIDATION_SCHEMA = "agent-usage-export-validation.v1"
13
+ USAGE_EXPORT_GENERATION_SCHEMA = "agent-usage-export-generation.v1"
14
+
15
+ _SECRET_MARKERS = (
16
+ "BEGIN " + "RSA PRIVATE KEY",
17
+ "BEGIN " + "OPENSSH PRIVATE KEY",
18
+ "BEGIN " + "PRIVATE KEY",
19
+ "github" + "_pat_",
20
+ "gh" + "p_",
21
+ "xo" + "xb-",
22
+ )
23
+ _LOCAL_PATH_PREFIXES = ("/Vol" "umes/", "/Us" "ers/")
24
+
25
+
26
+ def build_usage_export(*, artifact_paths: list[Path], project_root: Path | None = None) -> dict[str, Any]:
27
+ """Build a deterministic usage export from explicit local JSON artifacts."""
28
+
29
+ if not artifact_paths:
30
+ raise LifecycleError("usage-export-artifacts-required", "at least one artifact path is required")
31
+ root = (project_root or Path.cwd()).resolve()
32
+ source_artifacts: list[dict[str, Any]] = []
33
+ entries: list[dict[str, Any]] = []
34
+ blockers: list[dict[str, Any]] = []
35
+ for index, raw_path in enumerate(artifact_paths):
36
+ payload, source = _load_artifact(raw_path, root)
37
+ source_artifacts.append(source)
38
+ entry = _entry_from_payload(index, source, payload)
39
+ entries.append(entry)
40
+ blockers.extend(_redaction_blockers(entry, index))
41
+ totals = usage_export_totals(entries)
42
+ body = {
43
+ "schemaVersion": USAGE_EXPORT_SCHEMA,
44
+ "status": "PASS" if not blockers else "FAIL",
45
+ "generatedBy": "agent-lifecycle metrics usage-export",
46
+ "sourceArtifacts": source_artifacts,
47
+ "lineage": _lineage(entries),
48
+ "entries": entries,
49
+ "totals": totals,
50
+ "blockers": blockers,
51
+ "productionPromotionClaimed": False,
52
+ }
53
+ return {**body, "exportDigest": canonical_digest(body)}
54
+
55
+
56
+ def validate_usage_export(export: dict[str, Any]) -> dict[str, Any]:
57
+ """Validate an already-built usage export."""
58
+
59
+ blockers: list[dict[str, Any]] = []
60
+ if export.get("schemaVersion") != USAGE_EXPORT_SCHEMA:
61
+ blockers.append({"code": "usage-export-schema", "message": "unsupported usage export schemaVersion"})
62
+ if export.get("productionPromotionClaimed") is not False:
63
+ blockers.append({"code": "usage-export-production-claim", "message": "usage export must not claim production promotion"})
64
+ entries = export.get("entries")
65
+ if not isinstance(entries, list) or not entries:
66
+ blockers.append({"code": "usage-export-entries", "message": "entries must be a non-empty array"})
67
+ entries = []
68
+ for index, entry in enumerate(entries):
69
+ _validate_entry(index, entry, blockers)
70
+ valid_entries = [entry for entry in entries if isinstance(entry, dict)]
71
+ totals = usage_export_totals(valid_entries)
72
+ if isinstance(export.get("totals"), dict) and export["totals"] != totals:
73
+ blockers.append({"code": "usage-export-totals-mismatch", "message": "totals do not match entries"})
74
+ blockers.extend(_redaction_blockers(export, "export"))
75
+ body = {
76
+ "schemaVersion": USAGE_EXPORT_VALIDATION_SCHEMA,
77
+ "status": "PASS" if not blockers else "FAIL",
78
+ "entryCount": len(entries),
79
+ "totals": totals,
80
+ "blockers": blockers,
81
+ "exportDigest": canonical_digest(export),
82
+ }
83
+ return {**body, "validationDigest": canonical_digest(body)}
84
+
85
+
86
+ def require_usage_export_pass(validation: dict[str, Any]) -> dict[str, Any]:
87
+ if validation.get("status") == "FAIL":
88
+ raise LifecycleError("usage-export-validation-failed", "usage export validation failed", {"validation": validation})
89
+ return validation
90
+
91
+
92
+ def usage_export_totals(entries: list[dict[str, Any]]) -> dict[str, Any]:
93
+ tokens = {"input": 0, "output": 0, "total": 0}
94
+ steps = 0
95
+ duration_ms = 0
96
+ resource_totals: dict[str, int] = {}
97
+ metered_entries = 0
98
+ host_reported_cost_total = 0.0
99
+ host_reported_cost_count = 0
100
+ for entry in entries:
101
+ usage = entry.get("tokens") if isinstance(entry.get("tokens"), dict) else {}
102
+ tokens["input"] += _non_negative_int(usage.get("input"))
103
+ tokens["output"] += _non_negative_int(usage.get("output"))
104
+ tokens["total"] += _non_negative_int(usage.get("total"))
105
+ steps += _non_negative_int(entry.get("steps"))
106
+ duration_ms += _non_negative_int(entry.get("durationMs"))
107
+ resources = entry.get("resources") if isinstance(entry.get("resources"), dict) else {}
108
+ for key, value in resources.items():
109
+ if isinstance(key, str):
110
+ resource_totals[key] = resource_totals.get(key, 0) + _non_negative_int(value)
111
+ monetary = entry.get("monetary") if isinstance(entry.get("monetary"), dict) else None
112
+ if monetary is not None:
113
+ metered_entries += 1
114
+ amount = monetary.get("cost_usd")
115
+ if isinstance(amount, (int, float)) and not isinstance(amount, bool):
116
+ host_reported_cost_total += float(amount)
117
+ host_reported_cost_count += 1
118
+ return {
119
+ "tokens": tokens,
120
+ "steps": steps,
121
+ "durationMs": duration_ms,
122
+ "resources": dict(sorted(resource_totals.items())),
123
+ "entries": len(entries),
124
+ "meteredEntries": metered_entries,
125
+ "hostReportedCost": {
126
+ "currency": "USD",
127
+ "entryCount": host_reported_cost_count,
128
+ "total": round(host_reported_cost_total, 6),
129
+ "canonical": False,
130
+ },
131
+ }
132
+
133
+
134
+ def _load_artifact(raw_path: Path, root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
135
+ path = raw_path if raw_path.is_absolute() else root / raw_path
136
+ display_path = _display_path(path, root)
137
+ try:
138
+ data = path.read_bytes()
139
+ except OSError as exc:
140
+ raise LifecycleError("usage-export-artifact-unavailable", "usage export artifact cannot be read", {"path": display_path}) from exc
141
+ payload = load_json_object(data, label=display_path)
142
+ source = {
143
+ "path": display_path,
144
+ "sha256": sha256_hex(data),
145
+ "bytes": len(data),
146
+ "schemaVersion": payload.get("schemaVersion"),
147
+ "payloadDigest": canonical_digest(payload),
148
+ }
149
+ return payload, source
150
+
151
+
152
+ def _entry_from_payload(index: int, source: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
153
+ entry = {
154
+ "entryId": f"usage-{index + 1}",
155
+ "source": source,
156
+ "schemaVersion": payload.get("schemaVersion"),
157
+ "adapterId": _first_string(payload, ("adapterId", "host", "adapter")),
158
+ "sessionId": _first_string(payload, ("sessionId", "runId")),
159
+ "attemptId": _first_string(payload, ("attemptId", "attempt")),
160
+ "runId": _string_or_none(payload.get("runId")),
161
+ "packageId": _string_or_none(payload.get("packageId")),
162
+ "taskId": _first_string(payload, ("taskId", "task")),
163
+ "operationId": _string_or_none(payload.get("operationId")),
164
+ "receiptDigests": _receipt_digests(payload),
165
+ "tokens": _tokens(payload),
166
+ "steps": _steps(payload),
167
+ "resources": _resources(payload),
168
+ "durationMs": _duration_ms(payload),
169
+ "budgetDecision": _budget_decision(payload),
170
+ }
171
+ monetary = _monetary(payload)
172
+ if monetary is not None:
173
+ entry["monetary"] = monetary
174
+ return entry
175
+
176
+
177
+ def _tokens(payload: dict[str, Any]) -> dict[str, int]:
178
+ for candidate in (payload.get("usage"), payload.get("usageTotals"), payload.get("tokenUsage"), payload.get("tokens"), payload.get("counters")):
179
+ if not isinstance(candidate, dict):
180
+ continue
181
+ input_tokens = _first_int(candidate, ("inputTokens", "promptTokens", "input"))
182
+ output_tokens = _first_int(candidate, ("outputTokens", "completionTokens", "output"))
183
+ total_tokens = _first_int(candidate, ("totalTokens", "reportedTokens", "billableTokens", "tokens", "total"))
184
+ if total_tokens is None and (input_tokens is not None or output_tokens is not None):
185
+ total_tokens = _non_negative_int(input_tokens) + _non_negative_int(output_tokens)
186
+ if input_tokens is not None or output_tokens is not None or total_tokens is not None:
187
+ return {
188
+ "input": _non_negative_int(input_tokens),
189
+ "output": _non_negative_int(output_tokens),
190
+ "total": _non_negative_int(total_tokens),
191
+ }
192
+ return {"input": 0, "output": 0, "total": 0}
193
+
194
+
195
+ def _steps(payload: dict[str, Any]) -> int:
196
+ for candidate in (payload.get("usage"), payload.get("usageTotals"), payload.get("counters"), payload):
197
+ if isinstance(candidate, dict):
198
+ value = _first_int(candidate, ("steps", "toolCalls", "validationRuns", "iterations"))
199
+ if value is not None:
200
+ return max(0, value)
201
+ return 1
202
+
203
+
204
+ def _resources(payload: dict[str, Any]) -> dict[str, int]:
205
+ resources: dict[str, int] = {}
206
+ for candidate in (payload.get("resources"), payload.get("resourceUsage"), payload.get("usage"), payload.get("counters")):
207
+ if not isinstance(candidate, dict):
208
+ continue
209
+ for source_key, export_key in (
210
+ ("cumulativeContextBytes", "contextBytes"),
211
+ ("contextBytes", "contextBytes"),
212
+ ("filesChanged", "filesChanged"),
213
+ ("toolCalls", "toolCalls"),
214
+ ("validationRuns", "validationRuns"),
215
+ ("cpuMs", "cpuMs"),
216
+ ("memoryMb", "memoryMb"),
217
+ ):
218
+ value = candidate.get(source_key)
219
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
220
+ resources[export_key] = resources.get(export_key, 0) + value
221
+ return dict(sorted(resources.items()))
222
+
223
+
224
+ def _duration_ms(payload: dict[str, Any]) -> int:
225
+ for candidate in (payload.get("usage"), payload.get("duration"), payload.get("timing"), payload):
226
+ if not isinstance(candidate, dict):
227
+ continue
228
+ value = _first_int(candidate, ("durationMs", "elapsedMs", "wallMs"))
229
+ if value is not None:
230
+ return max(0, value)
231
+ seconds = _number(candidate.get("durationSeconds")) or _number(candidate.get("wallSeconds"))
232
+ if seconds is not None:
233
+ return max(0, int(round(seconds * 1000)))
234
+ return 0
235
+
236
+
237
+ def _budget_decision(payload: dict[str, Any]) -> dict[str, Any] | None:
238
+ for key in ("budgetDecision", "budget", "routeDecision"):
239
+ value = payload.get(key)
240
+ if isinstance(value, dict):
241
+ return {
242
+ item_key: _safe_scalar(item)
243
+ for item_key, item in sorted(value.items())
244
+ if item_key in {"action", "decision", "mode", "reason", "status", "limit", "remainingTokens"}
245
+ and isinstance(item, (str, int, float, bool))
246
+ }
247
+ return None
248
+
249
+
250
+ def _monetary(payload: dict[str, Any]) -> dict[str, Any] | None:
251
+ for candidate in (payload.get("hostReportedCost"), payload.get("monetary"), payload.get("usage")):
252
+ if not isinstance(candidate, dict) or "cost_usd" not in candidate:
253
+ continue
254
+ return {
255
+ "hostReported": True,
256
+ "currency": "USD",
257
+ "cost_usd": _cost_or_none(candidate.get("cost_usd")),
258
+ "canonical": False,
259
+ }
260
+ return None
261
+
262
+
263
+ def _receipt_digests(payload: dict[str, Any]) -> list[str]:
264
+ digests: set[str] = set()
265
+ _collect_digest_values(payload, digests)
266
+ return sorted(digests)
267
+
268
+
269
+ def _collect_digest_values(value: Any, digests: set[str]) -> None:
270
+ if isinstance(value, dict):
271
+ for key, item in value.items():
272
+ if key.endswith("Digest") and isinstance(item, str) and len(item) == 64:
273
+ digests.add(item)
274
+ _collect_digest_values(item, digests)
275
+ elif isinstance(value, list):
276
+ for item in value:
277
+ _collect_digest_values(item, digests)
278
+
279
+
280
+ def _lineage(entries: list[dict[str, Any]]) -> dict[str, list[str]]:
281
+ keys = ("adapterId", "sessionId", "runId", "packageId", "taskId", "operationId")
282
+ return {
283
+ f"{key}s": sorted({entry[key] for entry in entries if isinstance(entry.get(key), str) and entry[key]})
284
+ for key in keys
285
+ }
286
+
287
+
288
+ def _validate_entry(index: int, entry: Any, blockers: list[dict[str, Any]]) -> None:
289
+ if not isinstance(entry, dict):
290
+ blockers.append({"code": "usage-export-entry-type", "index": index})
291
+ return
292
+ tokens = entry.get("tokens")
293
+ if not isinstance(tokens, dict):
294
+ blockers.append({"code": "usage-export-entry-tokens", "index": index})
295
+ else:
296
+ for key in ("input", "output", "total"):
297
+ if not isinstance(tokens.get(key), int) or isinstance(tokens.get(key), bool) or tokens.get(key) < 0:
298
+ blockers.append({"code": "usage-export-entry-token-value", "index": index, "field": key})
299
+ if not isinstance(entry.get("steps"), int) or isinstance(entry.get("steps"), bool) or entry.get("steps") < 0:
300
+ blockers.append({"code": "usage-export-entry-steps", "index": index})
301
+ if not isinstance(entry.get("durationMs"), int) or isinstance(entry.get("durationMs"), bool) or entry.get("durationMs") < 0:
302
+ blockers.append({"code": "usage-export-entry-duration", "index": index})
303
+ monetary = entry.get("monetary")
304
+ if monetary is not None:
305
+ _validate_monetary(index, monetary, blockers)
306
+
307
+
308
+ def _validate_monetary(index: int, monetary: Any, blockers: list[dict[str, Any]]) -> None:
309
+ if not isinstance(monetary, dict):
310
+ blockers.append({"code": "usage-export-monetary-type", "index": index})
311
+ return
312
+ if monetary.get("hostReported") is not True or monetary.get("canonical") is not False:
313
+ blockers.append({"code": "usage-export-monetary-authority", "index": index})
314
+ if monetary.get("currency") != "USD":
315
+ blockers.append({"code": "usage-export-monetary-currency", "index": index})
316
+ amount = monetary.get("cost_usd")
317
+ if amount is not None and (not isinstance(amount, (int, float)) or isinstance(amount, bool) or amount < 0):
318
+ blockers.append({"code": "usage-export-monetary-amount", "index": index})
319
+
320
+
321
+ def _redaction_blockers(value: Any, index: int | str) -> list[dict[str, Any]]:
322
+ text = str(value)
323
+ blockers = []
324
+ if any(prefix in text for prefix in _LOCAL_PATH_PREFIXES):
325
+ blockers.append({"code": "usage-export-local-path-leak", "index": index})
326
+ for marker in _SECRET_MARKERS:
327
+ if marker in text:
328
+ blockers.append({"code": "usage-export-secret-marker-leak", "index": index})
329
+ return blockers
330
+
331
+
332
+ def _display_path(path: Path, root: Path) -> str:
333
+ try:
334
+ return path.resolve().relative_to(root).as_posix()
335
+ except ValueError:
336
+ return path.name
337
+
338
+
339
+ def _first_string(payload: dict[str, Any], keys: tuple[str, ...]) -> str | None:
340
+ for key in keys:
341
+ value = payload.get(key)
342
+ text = _string_or_none(value)
343
+ if text is not None:
344
+ return text
345
+ return None
346
+
347
+
348
+ def _string_or_none(value: Any) -> str | None:
349
+ if isinstance(value, str) and value:
350
+ return _safe_string(value)
351
+ if isinstance(value, int) and not isinstance(value, bool):
352
+ return str(value)
353
+ return None
354
+
355
+
356
+ def _first_int(payload: dict[str, Any], keys: tuple[str, ...]) -> int | None:
357
+ for key in keys:
358
+ value = payload.get(key)
359
+ if isinstance(value, int) and not isinstance(value, bool):
360
+ return value
361
+ return None
362
+
363
+
364
+ def _non_negative_int(value: Any) -> int:
365
+ return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else 0
366
+
367
+
368
+ def _number(value: Any) -> float | None:
369
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
370
+ return float(value)
371
+ return None
372
+
373
+
374
+ def _cost_or_none(value: Any) -> float | None:
375
+ amount = _number(value)
376
+ if amount is None or amount < 0:
377
+ return None
378
+ return round(amount, 6)
379
+
380
+
381
+ def _safe_scalar(value: str | int | float | bool) -> str | int | float | bool:
382
+ return _safe_string(value) if isinstance(value, str) else value
383
+
384
+
385
+ def _safe_string(value: str) -> str:
386
+ text = re.sub(r"/(?:Volumes|Users)/\S+", "<redacted-local-path>", value)
387
+ for marker in _SECRET_MARKERS:
388
+ if marker in text:
389
+ text = text.replace(marker, "<redacted-secret>")
390
+ return text
@@ -0,0 +1,19 @@
1
+ """Provider-neutral model routing contracts."""
2
+
3
+ from agent_lifecycle.model_routing.profiles import (
4
+ ALLOWED_MODEL_CLASSES,
5
+ ROUTING_POLICIES,
6
+ validate_host_model_profile,
7
+ validate_model_routing_profile,
8
+ )
9
+ from agent_lifecycle.model_routing.receipts import validate_usage_receipt
10
+ from agent_lifecycle.model_routing.resolver import resolve_model_route
11
+
12
+ __all__ = [
13
+ "ALLOWED_MODEL_CLASSES",
14
+ "ROUTING_POLICIES",
15
+ "resolve_model_route",
16
+ "validate_host_model_profile",
17
+ "validate_model_routing_profile",
18
+ "validate_usage_receipt",
19
+ ]