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,60 @@
1
+ """Lifecycle resource and process-cost metrics."""
2
+
3
+ from agent_lifecycle.metrics.costs import (
4
+ COST_CATEGORIES,
5
+ DEFAULT_MODE_LIMITS,
6
+ require_lifecycle_cost_pass,
7
+ validate_lifecycle_cost_report,
8
+ )
9
+ from agent_lifecycle.metrics.cost_collection import build_lifecycle_cost_summary, generate_lifecycle_cost_report
10
+ from agent_lifecycle.metrics.phase_resources import (
11
+ build_phase_resource_measurement,
12
+ require_phase_resource_measurement_pass,
13
+ validate_phase_resource_measurement,
14
+ )
15
+ from agent_lifecycle.metrics.outcome_index import (
16
+ build_quality_cost_signal_summary,
17
+ build_quality_cost_signals,
18
+ build_task_outcome_index,
19
+ )
20
+ from agent_lifecycle.metrics.recommendations import (
21
+ build_lifecycle_recommendation_summary,
22
+ recommend_lifecycle_mode,
23
+ recommend_from_quality_cost_signals,
24
+ require_lifecycle_recommendation_pass,
25
+ summarize_lifecycle_overhead,
26
+ validate_lifecycle_baselines,
27
+ )
28
+ from agent_lifecycle.metrics.regression_signals import summarize_regression_signals
29
+ from agent_lifecycle.metrics.usage_export import (
30
+ build_usage_export,
31
+ require_usage_export_pass,
32
+ usage_export_totals,
33
+ validate_usage_export,
34
+ )
35
+
36
+ __all__ = [
37
+ "COST_CATEGORIES",
38
+ "DEFAULT_MODE_LIMITS",
39
+ "build_usage_export",
40
+ "build_phase_resource_measurement",
41
+ "build_lifecycle_cost_summary",
42
+ "build_lifecycle_recommendation_summary",
43
+ "build_quality_cost_signal_summary",
44
+ "build_quality_cost_signals",
45
+ "build_task_outcome_index",
46
+ "generate_lifecycle_cost_report",
47
+ "recommend_from_quality_cost_signals",
48
+ "recommend_lifecycle_mode",
49
+ "require_lifecycle_cost_pass",
50
+ "require_lifecycle_recommendation_pass",
51
+ "require_phase_resource_measurement_pass",
52
+ "require_usage_export_pass",
53
+ "summarize_lifecycle_overhead",
54
+ "summarize_regression_signals",
55
+ "usage_export_totals",
56
+ "validate_lifecycle_baselines",
57
+ "validate_lifecycle_cost_report",
58
+ "validate_phase_resource_measurement",
59
+ "validate_usage_export",
60
+ ]
@@ -0,0 +1,315 @@
1
+ """Generate lifecycle cost reports from explicit local artifacts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.contracts import LifecycleError, canonical_bytes, canonical_digest, load_json_object, sha256_hex
9
+ from agent_lifecycle.metrics.costs import (
10
+ COST_CATEGORIES,
11
+ COST_REPORT_SCHEMA,
12
+ COST_SUMMARY_SCHEMA,
13
+ cost_entry_totals,
14
+ cost_ratios,
15
+ summarize_usage_confidence,
16
+ )
17
+
18
+
19
+ def generate_lifecycle_cost_report(
20
+ *,
21
+ artifact_paths: list[Path],
22
+ mode: str = "standard",
23
+ root: Path | None = None,
24
+ ) -> dict[str, Any]:
25
+ """Generate a cost report from explicit JSON artifacts without host calls."""
26
+
27
+ if not artifact_paths:
28
+ raise LifecycleError("cost-report-artifacts-required", "at least one artifact path is required")
29
+ project_root = (root or Path.cwd()).resolve()
30
+ source_artifacts: list[dict[str, Any]] = []
31
+ entries: list[dict[str, Any]] = []
32
+ payloads: list[dict[str, Any]] = []
33
+ for index, raw_path in enumerate(artifact_paths):
34
+ payload, source = _load_source_artifact(raw_path, project_root)
35
+ source_artifacts.append(source)
36
+ entries.append(_entry_from_artifact(index, source, payload))
37
+ payloads.append(payload)
38
+
39
+ report = {
40
+ "schemaVersion": COST_REPORT_SCHEMA,
41
+ "mode": mode,
42
+ "generatedBy": "agent-lifecycle metrics cost-report",
43
+ "sourceArtifacts": source_artifacts,
44
+ "lineage": _lineage(source_artifacts, payloads),
45
+ "entries": entries,
46
+ "usageConfidence": summarize_usage_confidence(entries),
47
+ "productionPromotionClaimed": False,
48
+ }
49
+ report["compactSummary"] = build_lifecycle_cost_summary(report)
50
+ return report
51
+
52
+
53
+ def build_lifecycle_cost_summary(report: dict[str, Any]) -> dict[str, Any]:
54
+ """Build a small-context summary from a lifecycle cost report."""
55
+
56
+ entries = report.get("entries") if isinstance(report.get("entries"), list) else []
57
+ totals = cost_entry_totals(entries)
58
+ ratios = cost_ratios(totals)
59
+ usage_confidence = summarize_usage_confidence(entries)
60
+ source_paths = _source_paths(report)
61
+ missing_usage = usage_confidence["missingEntries"]
62
+ next_action = "review missing usage entries" if missing_usage else "continue with the selected lifecycle policy"
63
+ return {
64
+ "schemaVersion": COST_SUMMARY_SCHEMA,
65
+ "mode": report.get("mode"),
66
+ "latestUserIntent": "Keep lifecycle overhead visible without reducing task quality.",
67
+ "activeDecisions": [
68
+ f"pipelineTokenShare={ratios['pipelineTokenShare']}",
69
+ f"pipelineStepShare={ratios['pipelineStepShare']}",
70
+ f"missingUsageEntries={missing_usage}",
71
+ ],
72
+ "openBlockers": [],
73
+ "acceptedEvidence": [
74
+ {
75
+ "id": "lifecycle-cost-report",
76
+ "status": "generated",
77
+ "sourceArtifactCount": len(source_paths),
78
+ }
79
+ ],
80
+ "changedFiles": source_paths[:20],
81
+ "nextRequiredAction": next_action,
82
+ "doNotDo": [
83
+ "Do not treat missing usage as zero.",
84
+ "Do not reduce required validation solely to improve cost ratios.",
85
+ ],
86
+ "categoryTotals": totals,
87
+ "ratios": ratios,
88
+ "usageConfidence": usage_confidence,
89
+ "usefulWorkTokens": totals["implementation"]["tokens"] + totals["productValidation"]["tokens"],
90
+ "processOverheadTokens": totals["pipelineCompliance"]["tokens"] + totals["coordination"]["tokens"],
91
+ }
92
+
93
+
94
+ def _load_source_artifact(raw_path: Path, project_root: Path) -> tuple[dict[str, Any], dict[str, Any]]:
95
+ path = raw_path if raw_path.is_absolute() else project_root / raw_path
96
+ display_path = _display_path(path, project_root)
97
+ try:
98
+ data = path.read_bytes()
99
+ except OSError as exc:
100
+ raise LifecycleError(
101
+ "cost-report-artifact-unavailable",
102
+ "cost report artifact cannot be read",
103
+ {"path": display_path, "reason": type(exc).__name__},
104
+ ) from exc
105
+ payload = load_json_object(data, label=display_path)
106
+ source = {
107
+ "path": display_path,
108
+ "sha256": sha256_hex(data),
109
+ "bytes": len(data),
110
+ "schemaVersion": payload.get("schemaVersion"),
111
+ "payloadDigest": canonical_digest(payload),
112
+ }
113
+ return payload, source
114
+
115
+
116
+ def _source_paths(report: dict[str, Any]) -> list[str]:
117
+ return [
118
+ item.get("path")
119
+ for item in report.get("sourceArtifacts", [])
120
+ if isinstance(item, dict) and isinstance(item.get("path"), str)
121
+ ]
122
+
123
+
124
+ def _entry_from_artifact(index: int, source: dict[str, Any], payload: dict[str, Any]) -> dict[str, Any]:
125
+ usage = _usage_from_payload(payload)
126
+ entry = {
127
+ "id": f"artifact-{index + 1}",
128
+ "category": _classify_artifact(str(source.get("path")), payload),
129
+ "tokens": usage["tokens"],
130
+ "steps": usage["steps"],
131
+ "usageConfidence": usage["confidence"],
132
+ "sourcePath": source["path"],
133
+ "sourceSchemaVersion": source.get("schemaVersion"),
134
+ "sourceDigest": source["payloadDigest"],
135
+ }
136
+ for field in ("runId", "packageId", "taskId", "operationId", "planDigest", "sourceRevision"):
137
+ value = payload.get(field)
138
+ if isinstance(value, (str, int)):
139
+ entry[field] = value
140
+ return entry
141
+
142
+
143
+ def _usage_from_payload(payload: dict[str, Any]) -> dict[str, Any]:
144
+ usage = payload.get("usage")
145
+ if isinstance(usage, dict):
146
+ billable = _int_value(usage.get("billableTokens"))
147
+ steps = _int_value(usage.get("toolCalls")) or 1
148
+ if billable is not None:
149
+ confidence = "ATTESTED" if _attested(payload.get("attestation")) else "ESTIMATED"
150
+ return {"tokens": billable, "steps": max(1, steps), "confidence": confidence}
151
+ return {"tokens": 0, "steps": 1, "confidence": "MISSING"}
152
+ for key in ("usageTotals", "counters"):
153
+ totals = payload.get(key)
154
+ if not isinstance(totals, dict):
155
+ continue
156
+ tokens = _first_int(totals, ("reportedTokens", "billableTokens", "tokens"))
157
+ if tokens is not None and tokens > 0:
158
+ steps = _first_int(totals, ("toolCalls", "validationRuns", "iterations", "steps")) or 1
159
+ return {"tokens": tokens, "steps": max(1, steps), "confidence": "ESTIMATED"}
160
+ if payload.get("schemaVersion") == "agent-lifecycle-model-usage-receipt.v1":
161
+ return {"tokens": 0, "steps": 1, "confidence": "MISSING"}
162
+ if _contains_pair(payload, "usageReceiptRequired", True):
163
+ return {"tokens": 0, "steps": 1, "confidence": "MISSING"}
164
+ return {"tokens": _estimate_tokens(payload), "steps": 1, "confidence": "ESTIMATED"}
165
+
166
+
167
+ def _classify_artifact(path: str, payload: dict[str, Any]) -> str:
168
+ schema = str(payload.get("schemaVersion") or "")
169
+ if schema == "agent-lifecycle-model-usage-receipt.v1":
170
+ return _classify_usage_receipt(payload)
171
+ text = _classification_text(path, payload)
172
+ if schema in {"agent-workflow-state.v3", "agent-runner-state.v1"}:
173
+ return "coordination"
174
+ if schema == "agent-task-result.v2":
175
+ return "implementation"
176
+ if any(marker in text for marker in _PRODUCT_VALIDATION_MARKERS):
177
+ return "productValidation"
178
+ if any(marker in text for marker in _PIPELINE_MARKERS):
179
+ return "pipelineCompliance"
180
+ return "implementation"
181
+
182
+
183
+ def _classify_usage_receipt(payload: dict[str, Any]) -> str:
184
+ text = _classification_text("", payload)
185
+ if any(marker in text for marker in _PRODUCT_VALIDATION_MARKERS):
186
+ return "productValidation"
187
+ if any(marker in text for marker in _PIPELINE_MARKERS):
188
+ return "pipelineCompliance"
189
+ return "implementation"
190
+
191
+
192
+ def _classification_text(path: str, payload: dict[str, Any]) -> str:
193
+ return " ".join(
194
+ str(item)
195
+ for item in (
196
+ path,
197
+ payload.get("schemaVersion"),
198
+ payload.get("operationId"),
199
+ payload.get("modelClass"),
200
+ payload.get("stage"),
201
+ payload.get("role"),
202
+ payload.get("kind"),
203
+ )
204
+ if item is not None
205
+ ).lower()
206
+
207
+
208
+ _PRODUCT_VALIDATION_MARKERS = (
209
+ "task-review",
210
+ "quality",
211
+ "test",
212
+ "security-review",
213
+ "performance-review",
214
+ "specialist-review",
215
+ )
216
+
217
+ _PIPELINE_MARKERS = (
218
+ "plan",
219
+ "workflow",
220
+ "runner",
221
+ "gate",
222
+ "neutrality",
223
+ "diagnostic",
224
+ "contract",
225
+ "release",
226
+ "cost",
227
+ "final-proof",
228
+ "handoff",
229
+ "followup",
230
+ "worktree",
231
+ "completion-check",
232
+ )
233
+
234
+
235
+ def _lineage(source_artifacts: list[dict[str, Any]], payloads: list[dict[str, Any]]) -> dict[str, list[Any]]:
236
+ values = {
237
+ "runIds": set(),
238
+ "packageIds": set(),
239
+ "taskIds": set(),
240
+ "planDigests": set(),
241
+ "sourceRevisions": set(),
242
+ "evidenceDigests": set(),
243
+ "sourceArtifactDigests": {item["sha256"] for item in source_artifacts},
244
+ }
245
+ for payload in payloads:
246
+ _collect_lineage_values(payload, values)
247
+ return {key: sorted(value) for key, value in values.items()}
248
+
249
+
250
+ def _collect_lineage_values(value: Any, found: dict[str, set[Any]]) -> None:
251
+ if isinstance(value, dict):
252
+ for key, item in value.items():
253
+ if key == "runId" and isinstance(item, str):
254
+ found["runIds"].add(item)
255
+ elif key == "packageId" and isinstance(item, str):
256
+ found["packageIds"].add(item)
257
+ elif key == "taskId" and isinstance(item, str):
258
+ found["taskIds"].add(item)
259
+ elif key == "id" and isinstance(item, str) and item.startswith("WS-"):
260
+ found["taskIds"].add(item)
261
+ elif key == "planDigest" and isinstance(item, str):
262
+ found["planDigests"].add(item)
263
+ elif key == "sourceRevision" and isinstance(item, str):
264
+ found["sourceRevisions"].add(item)
265
+ elif _is_evidence_digest(key, item):
266
+ found["evidenceDigests"].add(item)
267
+ _collect_lineage_values(item, found)
268
+ elif isinstance(value, list):
269
+ for item in value:
270
+ _collect_lineage_values(item, found)
271
+
272
+
273
+ def _is_evidence_digest(key: str, value: Any) -> bool:
274
+ keys = {"evidenceDigest", "resultDigest", "reviewDigest", "validationDigest", "finalProofDigest", "sha256"}
275
+ return key in keys and isinstance(value, str)
276
+
277
+
278
+ def _contains_pair(value: Any, key: str, expected: Any) -> bool:
279
+ if isinstance(value, dict):
280
+ return any(
281
+ (item_key == key and item_value == expected) or _contains_pair(item_value, key, expected)
282
+ for item_key, item_value in value.items()
283
+ )
284
+ if isinstance(value, list):
285
+ return any(_contains_pair(item, key, expected) for item in value)
286
+ return False
287
+
288
+
289
+ def _attested(value: Any) -> bool:
290
+ return isinstance(value, dict) and value.get("status") == "ATTESTED"
291
+
292
+
293
+ def _first_int(payload: dict[str, Any], keys: tuple[str, ...]) -> int | None:
294
+ for key in keys:
295
+ value = _int_value(payload.get(key))
296
+ if value is not None:
297
+ return value
298
+ return None
299
+
300
+
301
+ def _int_value(value: Any) -> int | None:
302
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
303
+ return value
304
+ return None
305
+
306
+
307
+ def _estimate_tokens(value: Any) -> int:
308
+ return max(1, (len(canonical_bytes(value)) + 3) // 4)
309
+
310
+
311
+ def _display_path(path: Path, root: Path) -> str:
312
+ try:
313
+ return path.resolve().relative_to(root).as_posix()
314
+ except ValueError:
315
+ return path.name
@@ -0,0 +1,203 @@
1
+ """Validate lifecycle cost reports for resource-disciplined runs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
8
+
9
+ COST_REPORT_SCHEMA = "agent-lifecycle-cost-report.v1"
10
+ COST_VALIDATION_SCHEMA = "agent-lifecycle-cost-validation.v1"
11
+ COST_GENERATION_SCHEMA = "agent-lifecycle-cost-generation.v1"
12
+ COST_SUMMARY_SCHEMA = "agent-lifecycle-cost-summary.v1"
13
+
14
+ COST_CATEGORIES = ("implementation", "productValidation", "pipelineCompliance", "coordination")
15
+ USAGE_CONFIDENCE_STATES = ("ATTESTED", "ESTIMATED", "MISSING")
16
+
17
+ DEFAULT_MODE_LIMITS: dict[str, dict[str, float | int]] = {
18
+ "light": {"maxPipelineTokenShare": 0.20, "maxPipelineStepShare": 0.30, "maxPipelineTokens": 1500, "maxPipelineSteps": 3},
19
+ "standard": {"maxPipelineTokenShare": 0.30, "maxPipelineStepShare": 0.40, "maxPipelineTokens": 6000, "maxPipelineSteps": 6},
20
+ "strict": {"maxPipelineTokenShare": 0.45, "maxPipelineStepShare": 0.55, "maxPipelineTokens": 20000, "maxPipelineSteps": 12},
21
+ "release": {"maxPipelineTokenShare": 0.60, "maxPipelineStepShare": 0.70, "maxPipelineTokens": 50000, "maxPipelineSteps": 24},
22
+ }
23
+
24
+
25
+ def validate_lifecycle_cost_report(report: dict[str, Any]) -> dict[str, Any]:
26
+ """Validate a lifecycle cost report and return a compact receipt."""
27
+
28
+ blockers: list[dict[str, Any]] = []
29
+ if report.get("schemaVersion") != COST_REPORT_SCHEMA:
30
+ blockers.append({"code": "cost-report-schema", "message": "unsupported lifecycle cost report schemaVersion"})
31
+ if report.get("productionPromotionClaimed") is True:
32
+ blockers.append({"code": "cost-report-production-claim", "message": "cost reports must not claim production promotion"})
33
+ mode = report.get("mode")
34
+ if mode not in DEFAULT_MODE_LIMITS:
35
+ blockers.append({"code": "cost-report-mode", "mode": mode})
36
+ mode = None
37
+ entries = report.get("entries")
38
+ if not isinstance(entries, list) or not entries:
39
+ blockers.append({"code": "cost-report-entries", "message": "entries must be a non-empty array"})
40
+ entries = []
41
+ totals = _empty_totals()
42
+ for index, entry in enumerate(entries):
43
+ _add_entry(index, entry, totals, blockers)
44
+ totals["overall"] = {
45
+ "tokens": sum(totals[category]["tokens"] for category in COST_CATEGORIES),
46
+ "steps": sum(totals[category]["steps"] for category in COST_CATEGORIES),
47
+ }
48
+ usage_confidence = summarize_usage_confidence(entries)
49
+ limits = _limits(report.get("limits"), mode)
50
+ ratios = cost_ratios(totals)
51
+ if mode is not None:
52
+ _check_pipeline_limits(totals, ratios, limits, report.get("overLimitReason"), blockers)
53
+ body = {
54
+ "schemaVersion": COST_VALIDATION_SCHEMA,
55
+ "status": "PASS" if not blockers else "FAIL",
56
+ "mode": mode,
57
+ "totals": totals,
58
+ "ratios": ratios,
59
+ "usageConfidence": usage_confidence,
60
+ "limits": limits,
61
+ "blockers": blockers,
62
+ "reportDigest": canonical_digest(report),
63
+ }
64
+ return {**body, "validationDigest": canonical_digest(body)}
65
+
66
+
67
+ def require_lifecycle_cost_pass(validation: dict[str, Any]) -> dict[str, Any]:
68
+ if validation.get("status") == "FAIL":
69
+ raise LifecycleError("lifecycle-cost-validation-failed", "lifecycle cost validation failed", {"validation": validation})
70
+ return validation
71
+
72
+
73
+ def _empty_totals() -> dict[str, dict[str, int]]:
74
+ return {category: {"tokens": 0, "steps": 0} for category in COST_CATEGORIES}
75
+
76
+
77
+ def cost_entry_totals(entries: list[Any]) -> dict[str, dict[str, int]]:
78
+ totals = _empty_totals()
79
+ for entry in entries:
80
+ if not isinstance(entry, dict):
81
+ continue
82
+ category = entry.get("category")
83
+ tokens = entry.get("tokens")
84
+ steps = entry.get("steps")
85
+ if category in COST_CATEGORIES and isinstance(tokens, int) and isinstance(steps, int):
86
+ totals[category]["tokens"] += max(0, tokens)
87
+ totals[category]["steps"] += max(0, steps)
88
+ totals["overall"] = {
89
+ "tokens": sum(totals[category]["tokens"] for category in COST_CATEGORIES),
90
+ "steps": sum(totals[category]["steps"] for category in COST_CATEGORIES),
91
+ }
92
+ return totals
93
+
94
+
95
+ def _add_entry(index: int, entry: Any, totals: dict[str, dict[str, int]], blockers: list[dict[str, Any]]) -> None:
96
+ if not isinstance(entry, dict):
97
+ blockers.append({"code": "cost-entry-type", "index": index, "message": "entry must be an object"})
98
+ return
99
+ category = entry.get("category")
100
+ if category not in COST_CATEGORIES:
101
+ blockers.append({"code": "cost-entry-category", "index": index, "category": category})
102
+ return
103
+ tokens = _non_negative_int(entry.get("tokens"), field="tokens", index=index, blockers=blockers)
104
+ steps = _non_negative_int(entry.get("steps"), field="steps", index=index, blockers=blockers)
105
+ confidence = entry.get("usageConfidence")
106
+ if confidence is not None and confidence not in USAGE_CONFIDENCE_STATES:
107
+ blockers.append({"code": "cost-entry-usage-confidence", "index": index, "usageConfidence": confidence})
108
+ if tokens is None or steps is None:
109
+ return
110
+ if tokens == 0 and steps == 0:
111
+ blockers.append({"code": "cost-entry-empty", "index": index})
112
+ return
113
+ totals[category]["tokens"] += tokens
114
+ totals[category]["steps"] += steps
115
+
116
+
117
+ def _non_negative_int(value: Any, *, field: str, index: int, blockers: list[dict[str, Any]]) -> int | None:
118
+ if not isinstance(value, int) or isinstance(value, bool) or value < 0:
119
+ blockers.append({"code": "cost-entry-numeric", "index": index, "field": field, "value": value})
120
+ return None
121
+ return value
122
+
123
+
124
+ def summarize_usage_confidence(entries: list[Any]) -> dict[str, int]:
125
+ summary = {
126
+ "attestedEntries": 0,
127
+ "attestedTokens": 0,
128
+ "estimatedEntries": 0,
129
+ "estimatedTokens": 0,
130
+ "missingEntries": 0,
131
+ "missingTokens": 0,
132
+ "unspecifiedEntries": 0,
133
+ "unspecifiedTokens": 0,
134
+ }
135
+ for entry in entries:
136
+ if not isinstance(entry, dict):
137
+ continue
138
+ confidence = entry.get("usageConfidence")
139
+ tokens = entry.get("tokens") if isinstance(entry.get("tokens"), int) else 0
140
+ if confidence == "ATTESTED":
141
+ summary["attestedEntries"] += 1
142
+ summary["attestedTokens"] += max(0, tokens)
143
+ elif confidence == "ESTIMATED":
144
+ summary["estimatedEntries"] += 1
145
+ summary["estimatedTokens"] += max(0, tokens)
146
+ elif confidence == "MISSING":
147
+ summary["missingEntries"] += 1
148
+ summary["missingTokens"] += max(0, tokens)
149
+ else:
150
+ summary["unspecifiedEntries"] += 1
151
+ summary["unspecifiedTokens"] += max(0, tokens)
152
+ return summary
153
+
154
+
155
+ def _limits(raw_limits: Any, mode: str | None) -> dict[str, float | int]:
156
+ if mode is None:
157
+ return {}
158
+ defaults = dict(DEFAULT_MODE_LIMITS[mode])
159
+ if not isinstance(raw_limits, dict):
160
+ return defaults
161
+ override = raw_limits.get(mode)
162
+ if not isinstance(override, dict):
163
+ return defaults
164
+ for key, default in defaults.items():
165
+ value = override.get(key)
166
+ if isinstance(default, float) and isinstance(value, (int, float)) and not isinstance(value, bool):
167
+ defaults[key] = float(value)
168
+ elif isinstance(default, int) and isinstance(value, int) and not isinstance(value, bool):
169
+ defaults[key] = value
170
+ return defaults
171
+
172
+
173
+ def cost_ratios(totals: dict[str, dict[str, int]]) -> dict[str, float]:
174
+ overall_tokens = totals["overall"]["tokens"]
175
+ overall_steps = totals["overall"]["steps"]
176
+ pipeline_tokens = totals["pipelineCompliance"]["tokens"]
177
+ pipeline_steps = totals["pipelineCompliance"]["steps"]
178
+ return {
179
+ "pipelineTokenShare": round(pipeline_tokens / overall_tokens, 6) if overall_tokens else 0.0,
180
+ "pipelineStepShare": round(pipeline_steps / overall_steps, 6) if overall_steps else 0.0,
181
+ }
182
+
183
+
184
+ def _check_pipeline_limits(
185
+ totals: dict[str, dict[str, int]],
186
+ ratios: dict[str, float],
187
+ limits: dict[str, float | int],
188
+ over_limit_reason: Any,
189
+ blockers: list[dict[str, Any]],
190
+ ) -> None:
191
+ over = []
192
+ if ratios["pipelineTokenShare"] > float(limits["maxPipelineTokenShare"]):
193
+ over.append("maxPipelineTokenShare")
194
+ if ratios["pipelineStepShare"] > float(limits["maxPipelineStepShare"]):
195
+ over.append("maxPipelineStepShare")
196
+ if totals["pipelineCompliance"]["tokens"] > int(limits["maxPipelineTokens"]):
197
+ over.append("maxPipelineTokens")
198
+ if totals["pipelineCompliance"]["steps"] > int(limits["maxPipelineSteps"]):
199
+ over.append("maxPipelineSteps")
200
+ if not over:
201
+ return
202
+ if not isinstance(over_limit_reason, str) or not over_limit_reason.strip():
203
+ blockers.append({"code": "pipeline-compliance-over-limit", "limits": sorted(set(over))})