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,244 @@
1
+ """Goal record validation and compact continuation snapshots."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from typing import Any
7
+
8
+ from agent_lifecycle.context.profiles import resolve_window, validate_context_profile
9
+ from agent_lifecycle.context.rendering import estimate_tokens
10
+ from agent_lifecycle.contracts import LifecycleError, canonical_digest
11
+
12
+ GOAL_STATUSES = {"ACTIVE", "BLOCKED", "READY_FOR_FINALIZATION", "COMPLETE"}
13
+ LINEAGE_KEYS = ("runId", "packageId", "planRevision", "planDigest", "sourceRevision")
14
+
15
+
16
+ def validate_goal_record(
17
+ record: dict[str, Any],
18
+ *,
19
+ state: dict[str, Any] | None = None,
20
+ require_current: bool = False,
21
+ ) -> dict[str, Any]:
22
+ """Validate a goal record and optionally bind it to current workflow state."""
23
+
24
+ if not isinstance(record, dict):
25
+ raise LifecycleError("invalid-goal-record", "goal record must be an object")
26
+ if record.get("schemaVersion") != "agent-goal-record.v1":
27
+ raise LifecycleError("invalid-goal-record", "goal record schemaVersion is unsupported")
28
+ goal_id = _required_string(record.get("goalId"), label="goalId")
29
+ _required_string(record.get("userIntent"), label="userIntent")
30
+ _required_string(record.get("ownerOutcome"), label="ownerOutcome")
31
+ status = record.get("status")
32
+ if status not in GOAL_STATUSES:
33
+ raise LifecycleError("invalid-goal-record", "goal status is unsupported")
34
+ constraints = _string_list(record.get("constraints"), label="constraints")
35
+ evidence_ids = _string_list(record.get("evidenceIds"), label="evidenceIds", allow_empty=True)
36
+ lineage = _lineage(record)
37
+ completion_check = _completion_check(record.get("completionCheck"))
38
+ if state is not None:
39
+ _validate_state_binding(lineage, completion_check, state, require_current=require_current)
40
+ return {
41
+ "schemaVersion": "agent-goal-record-validation.v1",
42
+ "status": "PASS",
43
+ "goalId": goal_id,
44
+ "goalStatus": status,
45
+ "lineage": lineage,
46
+ "stateRevision": lineage.get("stateRevision"),
47
+ "constraintCount": len(constraints),
48
+ "evidenceIds": evidence_ids,
49
+ "completionCheck": completion_check,
50
+ "goalDigest": canonical_digest(record),
51
+ }
52
+
53
+
54
+ def build_objective_snapshot(
55
+ record: dict[str, Any],
56
+ state: dict[str, Any],
57
+ *,
58
+ profile: dict[str, Any] | None = None,
59
+ window: str | None = None,
60
+ ) -> dict[str, Any]:
61
+ """Build a compact continuation summary from a current goal record."""
62
+
63
+ from agent_lifecycle.workflow.query import next_action
64
+
65
+ validation = validate_goal_record(record, state=state, require_current=True)
66
+ body: dict[str, Any] = {
67
+ "schemaVersion": "agent-objective-snapshot.v1",
68
+ "status": "PASS",
69
+ "goal": {
70
+ "goalId": validation["goalId"],
71
+ "goalStatus": validation["goalStatus"],
72
+ "goalDigest": validation["goalDigest"],
73
+ "userIntent": record.get("userIntent"),
74
+ "userIntentDigest": canonical_digest({"userIntent": record.get("userIntent")}),
75
+ "ownerOutcome": record.get("ownerOutcome"),
76
+ "constraints": record.get("constraints", []),
77
+ "constraintsDigest": canonical_digest({"constraints": record.get("constraints", [])}),
78
+ "evidenceIds": validation["evidenceIds"],
79
+ },
80
+ "lifecycle": {
81
+ "runId": state.get("runId"),
82
+ "packageId": state.get("packageId"),
83
+ "planRevision": state.get("planRevision"),
84
+ "planDigest": state.get("planDigest"),
85
+ "sourceRevision": state.get("sourceRevision"),
86
+ "phase": state.get("phase"),
87
+ "stateRevision": state.get("stateRevision"),
88
+ "nextAction": next_action(state),
89
+ },
90
+ "completionCheck": validation["completionCheck"],
91
+ }
92
+ token_estimate = estimate_tokens(body)
93
+ target: dict[str, Any] | None = None
94
+ if profile is not None:
95
+ validate_context_profile(profile)
96
+ selected = resolve_window(profile, window)
97
+ limit = selected["limits"]["maxStateSummaryTokens"]
98
+ target = {"window": selected["name"], "limit": limit}
99
+ if token_estimate > limit:
100
+ raise LifecycleError(
101
+ "goal-summary-overflow",
102
+ "objective snapshot exceeds compact context state-summary limit",
103
+ {"estimatedTokens": token_estimate, "limit": limit, "window": selected["name"]},
104
+ )
105
+ body["estimatedTokens"] = token_estimate
106
+ if target is not None:
107
+ body["target"] = target
108
+ return {**body, "snapshotDigest": canonical_digest(body)}
109
+
110
+
111
+ def update_goal_record(
112
+ record: dict[str, Any],
113
+ state: dict[str, Any],
114
+ *,
115
+ status: str | None = None,
116
+ reason: str,
117
+ evidence_ids: list[str] | None = None,
118
+ ) -> dict[str, Any]:
119
+ """Return a new goal record bound to the current workflow state."""
120
+
121
+ previous = validate_goal_record(record, state=state, require_current=False)
122
+ new_status = status or str(record.get("status"))
123
+ if new_status not in GOAL_STATUSES:
124
+ raise LifecycleError("invalid-goal-record", "goal status is unsupported")
125
+ added_evidence = _string_list(evidence_ids or [], label="evidenceIds", allow_empty=True)
126
+ merged_evidence = sorted(set(previous["evidenceIds"]) | set(added_evidence))
127
+ updated = {
128
+ **record,
129
+ "status": new_status,
130
+ "lineage": _lineage_from_state(state),
131
+ "completionCheck": _state_completion_check(state),
132
+ "evidenceIds": merged_evidence,
133
+ "updatedAt": _now_iso(),
134
+ }
135
+ if updated["completionCheck"] is None:
136
+ updated.pop("completionCheck", None)
137
+ history = list(record.get("history", [])) if isinstance(record.get("history"), list) else []
138
+ history.append({
139
+ "previousGoalDigest": previous["goalDigest"],
140
+ "previousStateRevision": previous["stateRevision"],
141
+ "reason": reason,
142
+ "updatedAt": updated["updatedAt"],
143
+ })
144
+ updated["history"] = history
145
+ validate_goal_record(updated, state=state, require_current=True)
146
+ return updated
147
+
148
+
149
+ def _validate_state_binding(
150
+ lineage: dict[str, Any],
151
+ completion_check: dict[str, Any] | None,
152
+ state: dict[str, Any],
153
+ *,
154
+ require_current: bool,
155
+ ) -> None:
156
+ for key in LINEAGE_KEYS:
157
+ if lineage.get(key) != state.get(key):
158
+ raise LifecycleError("goal-lineage-mismatch", f"goal record {key} mismatch")
159
+ state_revision = lineage.get("stateRevision")
160
+ if not isinstance(state_revision, int) or isinstance(state_revision, bool) or state_revision < 1:
161
+ raise LifecycleError("invalid-goal-record", "goal lineage.stateRevision must be a positive integer")
162
+ if require_current and state_revision != state.get("stateRevision"):
163
+ raise LifecycleError(
164
+ "goal-state-stale",
165
+ "goal record is not bound to the current workflow state revision",
166
+ {"goalStateRevision": state_revision, "stateRevision": state.get("stateRevision")},
167
+ )
168
+ expected_check = _state_completion_check(state)
169
+ if expected_check != completion_check:
170
+ raise LifecycleError("goal-completion-check-mismatch", "goal record completionCheck does not match workflow state")
171
+
172
+
173
+ def _lineage(record: dict[str, Any]) -> dict[str, Any]:
174
+ lineage = record.get("lineage")
175
+ if not isinstance(lineage, dict):
176
+ raise LifecycleError("invalid-goal-record", "goal lineage is required")
177
+ result = {key: lineage.get(key) for key in LINEAGE_KEYS}
178
+ for key, value in result.items():
179
+ if key == "planRevision":
180
+ if not isinstance(value, int) or isinstance(value, bool) or value < 1:
181
+ raise LifecycleError("invalid-goal-record", "goal lineage.planRevision must be a positive integer")
182
+ elif not isinstance(value, str) or not value:
183
+ raise LifecycleError("invalid-goal-record", f"goal lineage.{key} is required")
184
+ state_revision = lineage.get("stateRevision")
185
+ if not isinstance(state_revision, int) or isinstance(state_revision, bool) or state_revision < 1:
186
+ raise LifecycleError("invalid-goal-record", "goal lineage.stateRevision must be a positive integer")
187
+ result["stateRevision"] = state_revision
188
+ return result
189
+
190
+
191
+ def _lineage_from_state(state: dict[str, Any]) -> dict[str, Any]:
192
+ return {
193
+ "runId": state.get("runId"),
194
+ "packageId": state.get("packageId"),
195
+ "planRevision": state.get("planRevision"),
196
+ "planDigest": state.get("planDigest"),
197
+ "sourceRevision": state.get("sourceRevision"),
198
+ "stateRevision": state.get("stateRevision"),
199
+ }
200
+
201
+
202
+ def _state_completion_check(state: dict[str, Any]) -> dict[str, Any] | None:
203
+ validation = state.get("completionCheckValidation")
204
+ if not isinstance(validation, dict):
205
+ return None
206
+ check_id = validation.get("checkId")
207
+ check_digest = validation.get("checkDigest")
208
+ if not isinstance(check_id, str) or not isinstance(check_digest, str):
209
+ return None
210
+ return {"checkId": check_id, "checkDigest": check_digest}
211
+
212
+
213
+ def _completion_check(value: Any) -> dict[str, Any] | None:
214
+ if value is None:
215
+ return None
216
+ if not isinstance(value, dict):
217
+ raise LifecycleError("invalid-goal-record", "goal completionCheck must be an object")
218
+ check_id = _required_string(value.get("checkId"), label="completionCheck.checkId")
219
+ check_digest = _digest(value.get("checkDigest"), label="completionCheck.checkDigest")
220
+ return {"checkId": check_id, "checkDigest": check_digest}
221
+
222
+
223
+ def _required_string(value: Any, *, label: str) -> str:
224
+ if not isinstance(value, str) or not value:
225
+ raise LifecycleError("invalid-goal-record", f"{label} is required")
226
+ return value
227
+
228
+
229
+ def _digest(value: Any, *, label: str) -> str:
230
+ if not isinstance(value, str) or len(value) != 64:
231
+ raise LifecycleError("invalid-goal-record", f"{label} must be a 64-character digest")
232
+ return value
233
+
234
+
235
+ def _string_list(value: Any, *, label: str, allow_empty: bool = False) -> list[str]:
236
+ if not isinstance(value, list) or not all(isinstance(item, str) and item for item in value):
237
+ raise LifecycleError("invalid-goal-record", f"{label} must be a list of non-empty strings")
238
+ if not value and not allow_empty:
239
+ raise LifecycleError("invalid-goal-record", f"{label} must not be empty")
240
+ return list(value)
241
+
242
+
243
+ def _now_iso() -> str:
244
+ return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
@@ -0,0 +1,109 @@
1
+ """Read-only goal and lifecycle progress view."""
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_digest, read_json_object
9
+ from agent_lifecycle.goal.records import validate_goal_record
10
+ from agent_lifecycle.reporting.progress_view import build_lifecycle_progress_view
11
+ from agent_lifecycle.workflow.query import next_action
12
+ from agent_lifecycle.workflow.state import load_state
13
+
14
+ GOAL_PROGRESS_VIEW_SCHEMA = "agent-goal-progress-view.v1"
15
+
16
+
17
+ def build_goal_progress_view(
18
+ *,
19
+ record_path: Path,
20
+ state_path: Path,
21
+ usage_receipt_paths: list[Path] | None = None,
22
+ change_summary_path: Path | None = None,
23
+ require_current: bool = True,
24
+ ) -> dict[str, Any]:
25
+ """Build a compact read-only view from goal and progress artifacts."""
26
+
27
+ record = read_json_object(record_path, label="goal record")
28
+ state = load_state(state_path)
29
+ validation = validate_goal_record(record, state=state, require_current=require_current)
30
+ progress = build_lifecycle_progress_view(
31
+ state_path=state_path,
32
+ usage_receipt_paths=usage_receipt_paths or [],
33
+ change_summary_path=change_summary_path,
34
+ )
35
+ task_summary = _task_summary(state)
36
+ blockers = _blockers(state, task_summary)
37
+ body = {
38
+ "schemaVersion": GOAL_PROGRESS_VIEW_SCHEMA,
39
+ "status": "PASS",
40
+ "sourceOfTruth": False,
41
+ "readOnly": True,
42
+ "modelCallsStarted": False,
43
+ "hostCallsStarted": False,
44
+ "stateMutated": False,
45
+ "goalRecordMutated": False,
46
+ "stateWritten": False,
47
+ "goalRecordWritten": False,
48
+ "tokenSpendForView": False,
49
+ "goal": {
50
+ "goalId": validation["goalId"],
51
+ "goalStatus": validation["goalStatus"],
52
+ "goalDigest": validation["goalDigest"],
53
+ "userIntentDigest": canonical_digest({"userIntent": record.get("userIntent")}),
54
+ "ownerOutcomeDigest": canonical_digest({"ownerOutcome": record.get("ownerOutcome")}),
55
+ "constraintCount": validation["constraintCount"],
56
+ "evidenceIds": validation["evidenceIds"],
57
+ "stateRevision": validation["stateRevision"],
58
+ },
59
+ "lifecycle": {
60
+ "runId": state.get("runId"),
61
+ "packageId": state.get("packageId"),
62
+ "planRevision": state.get("planRevision"),
63
+ "planDigest": state.get("planDigest"),
64
+ "sourceRevision": state.get("sourceRevision"),
65
+ "phase": state.get("phase"),
66
+ "stateRevision": state.get("stateRevision"),
67
+ "nextAction": next_action(state),
68
+ "taskSummary": task_summary,
69
+ },
70
+ "progress": {
71
+ "progressDigest": progress["progressDigest"],
72
+ "rowCount": progress["rowCount"],
73
+ "lines": progress["lines"],
74
+ "terminalSummary": progress["terminalSummary"],
75
+ },
76
+ "metrics": {
77
+ "duration": progress["terminalSummary"]["duration"],
78
+ "tokens": progress["terminalSummary"]["tokens"],
79
+ "changeSummary": progress["terminalSummary"]["changeSummary"],
80
+ },
81
+ "blockers": blockers,
82
+ "productionPromotionClaimed": False,
83
+ }
84
+ return {**body, "viewDigest": canonical_digest(body)}
85
+
86
+
87
+ def _task_summary(state: dict[str, Any]) -> dict[str, int]:
88
+ counts: dict[str, int] = {}
89
+ tasks = state.get("tasks")
90
+ for task in tasks if isinstance(tasks, list) else []:
91
+ if not isinstance(task, dict):
92
+ continue
93
+ status = str(task.get("status") or "UNKNOWN")
94
+ counts[status] = counts.get(status, 0) + 1
95
+ return counts
96
+
97
+
98
+ def _blockers(state: dict[str, Any], task_summary: dict[str, int]) -> list[dict[str, Any]]:
99
+ blockers: list[dict[str, Any]] = []
100
+ blocker = state.get("blocker")
101
+ if isinstance(blocker, dict):
102
+ blockers.append({
103
+ "code": str(blocker.get("code") or "workflow-blocked"),
104
+ "message": str(blocker.get("message") or blocker.get("reason") or "workflow is blocked"),
105
+ })
106
+ blocked_count = task_summary.get("BLOCKED", 0)
107
+ if blocked_count:
108
+ blockers.append({"code": "blocked-tasks-present", "count": blocked_count})
109
+ return blockers
@@ -0,0 +1,65 @@
1
+ """Provider-neutral host operation protocol."""
2
+
3
+ from agent_lifecycle.host_protocol.acp_capability import (
4
+ build_acp_capability,
5
+ build_acp_probe_receipt,
6
+ require_host_capabilities_pass,
7
+ validate_host_capabilities,
8
+ validate_no_acp_evidence_for_hosts,
9
+ )
10
+ from agent_lifecycle.host_protocol.capabilities import (
11
+ build_capability_manifest,
12
+ validate_capability_manifest,
13
+ )
14
+ from agent_lifecycle.host_protocol.contracts import HostAdapterEvent, HostOperationReceipt, HostOperationRequest
15
+ from agent_lifecycle.host_protocol.event_capture import (
16
+ adapter_declares_event_capture,
17
+ build_adapter_event_stream,
18
+ build_event_stream_receipt,
19
+ event_capture_declaration,
20
+ require_event_capture_pass,
21
+ validate_event_capture_conformance,
22
+ validate_event_capture_receipt,
23
+ )
24
+ from agent_lifecycle.host_protocol.events import (
25
+ require_adapter_event_stream_pass,
26
+ validate_adapter_event_stream,
27
+ )
28
+ from agent_lifecycle.host_protocol.inspection import (
29
+ inspect_adapter_descriptor,
30
+ require_adapter_inspection_pass,
31
+ )
32
+ from agent_lifecycle.host_protocol.receipts import normalize_host_operation_receipt
33
+ from agent_lifecycle.host_protocol.scaffold import scaffold_adapter
34
+ from agent_lifecycle.host_protocol.validation import (
35
+ require_adapter_validation_pass,
36
+ validate_adapter_descriptor,
37
+ )
38
+
39
+ __all__ = [
40
+ "HostAdapterEvent",
41
+ "HostOperationReceipt",
42
+ "HostOperationRequest",
43
+ "adapter_declares_event_capture",
44
+ "build_acp_capability",
45
+ "build_acp_probe_receipt",
46
+ "build_capability_manifest",
47
+ "build_adapter_event_stream",
48
+ "build_event_stream_receipt",
49
+ "event_capture_declaration",
50
+ "normalize_host_operation_receipt",
51
+ "require_event_capture_pass",
52
+ "require_adapter_event_stream_pass",
53
+ "require_adapter_inspection_pass",
54
+ "require_adapter_validation_pass",
55
+ "require_host_capabilities_pass",
56
+ "inspect_adapter_descriptor",
57
+ "scaffold_adapter",
58
+ "validate_capability_manifest",
59
+ "validate_event_capture_conformance",
60
+ "validate_event_capture_receipt",
61
+ "validate_adapter_event_stream",
62
+ "validate_adapter_descriptor",
63
+ "validate_host_capabilities",
64
+ "validate_no_acp_evidence_for_hosts",
65
+ ]
@@ -0,0 +1,195 @@
1
+ """Neutral ACP host capability declarations and fail-closed probe receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts.errors import LifecycleError
8
+
9
+ ACP_CAPABILITY_ID = "acp"
10
+ HOST_CAPABILITY_SCHEMA_VERSION = "agent-host-capability.v1"
11
+ HOST_CAPABILITY_VALIDATION_SCHEMA_VERSION = "agent-host-capability-validation.v1"
12
+ ACP_PROBE_RECEIPT_SCHEMA_VERSION = "agent-acp-probe-receipt.v1"
13
+ _SUPPORTED_VALUES = {"supported", "unsupported", "unknown"}
14
+
15
+
16
+ def build_acp_capability(
17
+ *,
18
+ adapter_id: str,
19
+ host: str,
20
+ support: str,
21
+ probe_command: list[str] | None = None,
22
+ ) -> dict[str, Any]:
23
+ """Build a schema-backed ACP capability declaration without provider identity."""
24
+
25
+ capability: dict[str, Any] = {
26
+ "schemaVersion": HOST_CAPABILITY_SCHEMA_VERSION,
27
+ "capabilityId": ACP_CAPABILITY_ID,
28
+ "adapterId": adapter_id,
29
+ "host": host,
30
+ "support": support,
31
+ "transport": "acp" if support == "supported" else "unknown",
32
+ "evidencePolicy": "probe-required" if support == "supported" else "not-claimed",
33
+ "providerIdentityUsed": False,
34
+ "probe": None,
35
+ "invocationContract": None,
36
+ }
37
+ if support == "supported":
38
+ capability["probe"] = {
39
+ "required": True,
40
+ "command": list(probe_command or [host, "--help"]),
41
+ "liveCallsStarted": False,
42
+ }
43
+ capability["invocationContract"] = {
44
+ "requestSchema": "agent-host-operation-request.v1",
45
+ "receiptSchema": "agent-host-operation-receipt.v1",
46
+ "unsupportedOperationPolicy": "fail-closed",
47
+ }
48
+ return capability
49
+
50
+
51
+ def validate_host_capabilities(
52
+ capabilities: Any,
53
+ *,
54
+ adapter_id: str | None = None,
55
+ host: str | None = None,
56
+ ) -> dict[str, Any]:
57
+ """Validate optional host capability declarations attached to a descriptor."""
58
+
59
+ blockers: list[dict[str, Any]] = []
60
+ if not isinstance(capabilities, list):
61
+ blockers.append({"code": "invalid-host-capabilities", "message": "hostCapabilities must be an array"})
62
+ return _validation_result(0, blockers)
63
+ for index, capability in enumerate(capabilities):
64
+ if not isinstance(capability, dict):
65
+ blockers.append({"code": "invalid-host-capability", "index": index, "message": "host capability must be an object"})
66
+ continue
67
+ _validate_capability_shape(capability, blockers, index=index, adapter_id=adapter_id, host=host)
68
+ return _validation_result(len(capabilities), blockers)
69
+
70
+
71
+ def validate_no_acp_evidence_for_hosts(capabilities: list[dict[str, Any]], *, excluded_hosts: set[str]) -> dict[str, Any]:
72
+ """Validate that excluded hosts have no positive ACP evidence declarations."""
73
+
74
+ blockers: list[dict[str, Any]] = []
75
+ for capability in capabilities:
76
+ if capability.get("capabilityId") != ACP_CAPABILITY_ID:
77
+ continue
78
+ host = capability.get("host")
79
+ if isinstance(host, str) and host in excluded_hosts and capability.get("support") == "supported":
80
+ blockers.append({"code": "excluded-host-acp-evidence", "host": host})
81
+ return _validation_result(len(capabilities), blockers)
82
+
83
+
84
+ def build_acp_probe_receipt(
85
+ capability: dict[str, Any],
86
+ *,
87
+ executable_found: bool,
88
+ probe_passed: bool,
89
+ invocation_contract_valid: bool,
90
+ ) -> dict[str, Any]:
91
+ """Return a fail-closed ACP probe receipt without starting live model calls."""
92
+
93
+ validation = validate_host_capabilities(
94
+ [capability],
95
+ adapter_id=capability.get("adapterId") if isinstance(capability.get("adapterId"), str) else None,
96
+ host=capability.get("host") if isinstance(capability.get("host"), str) else None,
97
+ )
98
+ blockers = list(validation["blockers"])
99
+ support = capability.get("support")
100
+ checks = [
101
+ {"name": "capability-declaration", "status": validation["status"]},
102
+ {"name": "executable-present", "status": "PASS" if executable_found else "FAIL"},
103
+ {"name": "acp-probe", "status": "PASS" if probe_passed else "FAIL"},
104
+ {"name": "invocation-contract", "status": "PASS" if invocation_contract_valid else "FAIL"},
105
+ ]
106
+ if support != "supported":
107
+ return {
108
+ "schemaVersion": ACP_PROBE_RECEIPT_SCHEMA_VERSION,
109
+ "status": "SKIPPED",
110
+ "adapterId": capability.get("adapterId"),
111
+ "host": capability.get("host"),
112
+ "capabilityId": ACP_CAPABILITY_ID,
113
+ "liveCallsStarted": False,
114
+ "checks": checks[:1],
115
+ "blockers": blockers,
116
+ }
117
+ if not executable_found:
118
+ blockers.append({"code": "acp-executable-missing"})
119
+ if not probe_passed:
120
+ blockers.append({"code": "acp-probe-failed"})
121
+ if not invocation_contract_valid:
122
+ blockers.append({"code": "acp-invocation-contract-invalid"})
123
+ return {
124
+ "schemaVersion": ACP_PROBE_RECEIPT_SCHEMA_VERSION,
125
+ "status": "PASS" if not blockers else "FAIL",
126
+ "adapterId": capability.get("adapterId"),
127
+ "host": capability.get("host"),
128
+ "capabilityId": ACP_CAPABILITY_ID,
129
+ "liveCallsStarted": False,
130
+ "checks": checks,
131
+ "blockers": blockers,
132
+ }
133
+
134
+
135
+ def require_host_capabilities_pass(payload: dict[str, Any]) -> dict[str, Any]:
136
+ if payload.get("status") == "FAIL":
137
+ raise LifecycleError("host-capability-validation-failed", "host capability validation failed", {"validation": payload})
138
+ return payload
139
+
140
+
141
+ def _validate_capability_shape(
142
+ capability: dict[str, Any],
143
+ blockers: list[dict[str, Any]],
144
+ *,
145
+ index: int,
146
+ adapter_id: str | None,
147
+ host: str | None,
148
+ ) -> None:
149
+ if capability.get("schemaVersion") != HOST_CAPABILITY_SCHEMA_VERSION:
150
+ blockers.append({"code": "invalid-host-capability-schema", "index": index})
151
+ if capability.get("capabilityId") != ACP_CAPABILITY_ID:
152
+ blockers.append({"code": "unsupported-host-capability", "index": index, "capabilityId": capability.get("capabilityId")})
153
+ if capability.get("support") not in _SUPPORTED_VALUES:
154
+ blockers.append({"code": "invalid-host-capability-support", "index": index})
155
+ if capability.get("providerIdentityUsed") is not False:
156
+ blockers.append({"code": "host-capability-provider-identity", "index": index})
157
+ if adapter_id is not None and capability.get("adapterId") != adapter_id:
158
+ blockers.append({"code": "host-capability-adapter-mismatch", "index": index})
159
+ if host is not None and capability.get("host") != host:
160
+ blockers.append({"code": "host-capability-host-mismatch", "index": index})
161
+ if capability.get("support") == "supported":
162
+ _validate_supported_acp(capability, blockers, index=index)
163
+
164
+
165
+ def _validate_supported_acp(capability: dict[str, Any], blockers: list[dict[str, Any]], *, index: int) -> None:
166
+ if capability.get("transport") != "acp":
167
+ blockers.append({"code": "supported-acp-transport-mismatch", "index": index})
168
+ if capability.get("evidencePolicy") != "probe-required":
169
+ blockers.append({"code": "supported-acp-probe-policy-missing", "index": index})
170
+ probe = capability.get("probe")
171
+ if not isinstance(probe, dict) or probe.get("required") is not True:
172
+ blockers.append({"code": "supported-acp-probe-required", "index": index})
173
+ elif not isinstance(probe.get("command"), list) or not all(isinstance(item, str) and item for item in probe["command"]):
174
+ blockers.append({"code": "supported-acp-probe-command-invalid", "index": index})
175
+ elif probe.get("liveCallsStarted") is not False:
176
+ blockers.append({"code": "supported-acp-probe-live-call-overclaim", "index": index})
177
+ contract = capability.get("invocationContract")
178
+ if not isinstance(contract, dict):
179
+ blockers.append({"code": "supported-acp-invocation-contract-missing", "index": index})
180
+ return
181
+ if contract.get("requestSchema") != "agent-host-operation-request.v1":
182
+ blockers.append({"code": "supported-acp-request-schema", "index": index})
183
+ if contract.get("receiptSchema") != "agent-host-operation-receipt.v1":
184
+ blockers.append({"code": "supported-acp-receipt-schema", "index": index})
185
+ if contract.get("unsupportedOperationPolicy") != "fail-closed":
186
+ blockers.append({"code": "supported-acp-unsupported-policy", "index": index})
187
+
188
+
189
+ def _validation_result(capability_count: int, blockers: list[dict[str, Any]]) -> dict[str, Any]:
190
+ return {
191
+ "schemaVersion": HOST_CAPABILITY_VALIDATION_SCHEMA_VERSION,
192
+ "status": "PASS" if not blockers else "FAIL",
193
+ "capabilityCount": capability_count,
194
+ "blockers": blockers,
195
+ }