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,106 @@
1
+ """Workflow enforcement for model-route usage receipts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import LifecycleError
8
+ from agent_lifecycle.model_routing import validate_usage_receipt
9
+
10
+ UNSAFE_CRITICAL_REVIEW_CLASSES = {"budget", "local-compact"}
11
+
12
+
13
+ def validate_attempt_model_route(task: dict[str, Any]) -> None:
14
+ """Validate that a task route is executable before an attempt starts."""
15
+
16
+ route = task.get("modelRoute")
17
+ if not isinstance(route, dict) or not route:
18
+ return
19
+ model_class = _required_route_string(route, "modelClass")
20
+ if model_class == "no-model":
21
+ return
22
+ _required_route_string(route, "operationId")
23
+ _required_route_string(route, "decisionDigest")
24
+ if route.get("requiresUsageReceipt") is False:
25
+ raise LifecycleError(
26
+ "model-usage-receipt-required",
27
+ "model-backed routes must require usage receipts",
28
+ )
29
+ if route.get("criticalReview") is True and model_class in UNSAFE_CRITICAL_REVIEW_CLASSES:
30
+ raise LifecycleError(
31
+ "model-route-critical-downgrade",
32
+ "critical review route cannot use a budget or compact model class",
33
+ {"modelClass": model_class},
34
+ )
35
+
36
+
37
+ def model_usage_receipt_required(task: dict[str, Any]) -> bool:
38
+ route = _attempt_route(task)
39
+ if route is None:
40
+ return False
41
+ model_class = route.get("modelClass")
42
+ if model_class == "no-model":
43
+ return False
44
+ return route.get("requiresUsageReceipt", True) is True
45
+
46
+
47
+ def validate_task_model_usage_receipt(
48
+ state: dict[str, Any],
49
+ task: dict[str, Any],
50
+ receipt: dict[str, Any],
51
+ *,
52
+ budget_targets: dict[str, Any] | None = None,
53
+ fail_on_invalid: bool = True,
54
+ ) -> dict[str, Any]:
55
+ route = _attempt_route(task)
56
+ if route is None or not model_usage_receipt_required(task):
57
+ raise LifecycleError("unexpected-model-usage-receipt", "task attempt does not require a model usage receipt")
58
+ _require_lineage(state, task, receipt)
59
+ result = validate_usage_receipt(receipt, budget_targets=budget_targets, route_decision=route)
60
+ if result["status"] == "FAIL" and fail_on_invalid:
61
+ raise LifecycleError(
62
+ "model-usage-validation-failed",
63
+ "model usage receipt validation failed",
64
+ {"validation": result},
65
+ )
66
+ return result
67
+
68
+
69
+ def _attempt_route(task: dict[str, Any]) -> dict[str, Any] | None:
70
+ route = task.get("attemptModelRoute")
71
+ if isinstance(route, dict) and route:
72
+ return route
73
+ route = task.get("modelRoute")
74
+ if isinstance(route, dict) and route:
75
+ return route
76
+ return None
77
+
78
+
79
+ def _require_lineage(state: dict[str, Any], task: dict[str, Any], receipt: dict[str, Any]) -> None:
80
+ expected = {
81
+ "runId": state.get("runId"),
82
+ "packageId": state.get("packageId"),
83
+ "taskId": task.get("id"),
84
+ "attempt": task.get("attempt"),
85
+ "planDigest": state.get("planDigest"),
86
+ "sourceRevision": state.get("sourceRevision"),
87
+ }
88
+ for key, value in expected.items():
89
+ if receipt.get(key) != value:
90
+ raise LifecycleError(
91
+ "model-usage-lineage-mismatch",
92
+ f"model usage receipt {key} mismatch",
93
+ )
94
+ route = _attempt_route(task) or {}
95
+ if receipt.get("routeDecisionDigest") != route.get("decisionDigest"):
96
+ raise LifecycleError(
97
+ "model-usage-lineage-mismatch",
98
+ "model usage receipt routeDecisionDigest mismatch",
99
+ )
100
+
101
+
102
+ def _required_route_string(route: dict[str, Any], key: str) -> str:
103
+ value = route.get(key)
104
+ if not isinstance(value, str) or not value:
105
+ raise LifecycleError("invalid-model-route", f"modelRoute.{key} is required")
106
+ return value
@@ -0,0 +1,86 @@
1
+ """Typed next-action projection for the managed lifecycle runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from agent_lifecycle.contracts import canonical_digest
8
+ from agent_lifecycle.workflow.query import next_action as workflow_next_action
9
+
10
+
11
+ MODEL_CALLS_STARTED = False
12
+
13
+
14
+ def build_managed_next_action(state: dict[str, Any]) -> dict[str, Any]:
15
+ """Return a host-owned next action without mutating workflow state."""
16
+
17
+ projected = workflow_next_action(state)
18
+ blockers = _blockers_for_projected_action(projected)
19
+ action = {
20
+ "schemaVersion": "agent-managed-lifecycle-next-action.v1",
21
+ "type": projected.get("type", "continue-phase"),
22
+ "status": "BLOCKED" if blockers else "READY",
23
+ "hostActionRequired": _host_action_required(projected),
24
+ "modelCallsStarted": MODEL_CALLS_STARTED,
25
+ "stateMutationRequired": _state_mutation_required(projected),
26
+ "projectedAction": projected,
27
+ "taskIds": list(projected.get("taskIds", [])) if isinstance(projected.get("taskIds"), list) else [],
28
+ "blockers": blockers,
29
+ }
30
+ return {**action, "actionDigest": canonical_digest(action)}
31
+
32
+
33
+ def _host_action_required(projected: dict[str, Any]) -> bool:
34
+ action_type = projected.get("type")
35
+ return action_type in {
36
+ "launch-tasks",
37
+ "request-human-decision",
38
+ "record-budget-decision",
39
+ "record-external-action-receipt",
40
+ "request-execution-authorization",
41
+ "start-execution",
42
+ "run-final-audit",
43
+ "finalize-run",
44
+ }
45
+
46
+
47
+ def _state_mutation_required(projected: dict[str, Any]) -> bool:
48
+ action_type = projected.get("type")
49
+ return action_type in {
50
+ "start-execution",
51
+ "record-budget-decision",
52
+ "record-external-action-receipt",
53
+ "request-execution-authorization",
54
+ "finalize-run",
55
+ }
56
+
57
+
58
+ def _blockers_for_projected_action(projected: dict[str, Any]) -> list[dict[str, Any]]:
59
+ action_type = projected.get("type")
60
+ if action_type == "request-human-decision":
61
+ blocker = projected.get("blocker")
62
+ if isinstance(blocker, dict):
63
+ return [{
64
+ "code": str(blocker.get("code") or "workflow-blocked"),
65
+ "message": str(blocker.get("reason") or "workflow is blocked"),
66
+ }]
67
+ return [{"code": "workflow-blocked", "message": "workflow is blocked"}]
68
+ if action_type == "record-budget-decision":
69
+ blocker = projected.get("blocker")
70
+ if isinstance(blocker, dict):
71
+ return [{
72
+ "code": str(blocker.get("code") or "budget-decision-required"),
73
+ "message": str(blocker.get("reason") or "budget decision is required"),
74
+ }]
75
+ return [{"code": "budget-decision-required", "message": "budget decision is required"}]
76
+ if action_type == "record-external-action-receipt":
77
+ external_action = projected.get("externalAction")
78
+ action_id = external_action.get("actionId") if isinstance(external_action, dict) else None
79
+ return [{
80
+ "code": "external-action-receipt-required",
81
+ "message": "external action receipt is required",
82
+ "actionId": action_id,
83
+ }]
84
+ if action_type == "request-execution-authorization":
85
+ return [{"code": "execution-authorization-required", "message": "execution authorization is required"}]
86
+ return []
@@ -0,0 +1,98 @@
1
+ """Shared workflow operation commit kernel."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from agent_lifecycle.contracts import LifecycleError
10
+ from agent_lifecycle.workflow.events import append_event, event_log_path
11
+ from agent_lifecycle.workflow.state import (
12
+ load_state,
13
+ now_iso,
14
+ record_operation,
15
+ require_expected_revision,
16
+ require_operation_unused,
17
+ write_state_replace,
18
+ )
19
+
20
+
21
+ def load_for_update(
22
+ state_path: Path,
23
+ *,
24
+ operation_id: str,
25
+ expected_revision: int,
26
+ ) -> dict[str, Any]:
27
+ state = load_state(state_path)
28
+ _require_no_split_brain(state_path, state)
29
+ require_expected_revision(state, expected_revision)
30
+ require_operation_unused(state, operation_id)
31
+ return state
32
+
33
+
34
+ def _require_no_split_brain(state_path: Path, state: dict[str, Any]) -> None:
35
+ path = event_log_path(state_path, state)
36
+ if not path.exists():
37
+ return
38
+ last_event = _last_event(path)
39
+ if last_event is None:
40
+ return
41
+ event_revision = last_event.get("stateRevision")
42
+ if not isinstance(event_revision, int):
43
+ raise LifecycleError("invalid-workflow-event-log", "workflow event stateRevision is invalid")
44
+ if event_revision > state["stateRevision"]:
45
+ raise LifecycleError(
46
+ "workflow-split-brain",
47
+ "workflow event log is ahead of state",
48
+ {
49
+ "stateRevision": state["stateRevision"],
50
+ "eventRevision": event_revision,
51
+ "operationId": last_event.get("operationId"),
52
+ "eventType": last_event.get("eventType"),
53
+ },
54
+ )
55
+
56
+
57
+ def _last_event(path: Path) -> dict[str, Any] | None:
58
+ last: dict[str, Any] | None = None
59
+ for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
60
+ if not line.strip():
61
+ continue
62
+ try:
63
+ event = json.loads(line)
64
+ except json.JSONDecodeError as exc:
65
+ raise LifecycleError(
66
+ "invalid-workflow-event-log",
67
+ "workflow event log contains malformed JSON",
68
+ {"path": path.as_posix(), "line": line_number},
69
+ ) from exc
70
+ if not isinstance(event, dict):
71
+ raise LifecycleError(
72
+ "invalid-workflow-event-log",
73
+ "workflow event log entry must be an object",
74
+ {"path": path.as_posix(), "line": line_number},
75
+ )
76
+ last = event
77
+ return last
78
+
79
+
80
+ def commit_state(
81
+ state_path: Path,
82
+ state: dict[str, Any],
83
+ *,
84
+ operation_id: str,
85
+ event_type: str,
86
+ payload: dict[str, Any],
87
+ ) -> None:
88
+ state["stateRevision"] += 1
89
+ state["updatedAt"] = now_iso()
90
+ record_operation(state, operation_id=operation_id, event_type=event_type)
91
+ append_event(
92
+ state_path=state_path,
93
+ state=state,
94
+ operation_id=operation_id,
95
+ event_type=event_type,
96
+ payload=payload,
97
+ )
98
+ write_state_replace(state_path, state)