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.
- agent_lifecycle/__init__.py +5 -0
- agent_lifecycle/__main__.py +5 -0
- agent_lifecycle/_version.py +3 -0
- agent_lifecycle/adapter_sessions/__init__.py +41 -0
- agent_lifecycle/adapter_sessions/contracts.py +133 -0
- agent_lifecycle/adapter_sessions/env.py +44 -0
- agent_lifecycle/adapter_sessions/launcher.py +101 -0
- agent_lifecycle/adapter_sessions/process.py +39 -0
- agent_lifecycle/adapter_sessions/redaction.py +30 -0
- agent_lifecycle/adapter_sessions/session_store.py +64 -0
- agent_lifecycle/adapter_sessions/task_intake.py +450 -0
- agent_lifecycle/adapter_sessions/workflow_bridge.py +154 -0
- agent_lifecycle/audit/__init__.py +67 -0
- agent_lifecycle/audit/bug_forensics.py +94 -0
- agent_lifecycle/audit/implementation.py +516 -0
- agent_lifecycle/audit/ownership.py +119 -0
- agent_lifecycle/audit/proof_integrity.py +814 -0
- agent_lifecycle/audit/review_verdict.py +116 -0
- agent_lifecycle/changesets/__init__.py +5 -0
- agent_lifecycle/changesets/git.py +35 -0
- agent_lifecycle/cli/__init__.py +5 -0
- agent_lifecycle/cli/adapter.py +360 -0
- agent_lifecycle/cli/dispatch.py +1220 -0
- agent_lifecycle/cli/followup.py +89 -0
- agent_lifecycle/cli/main.py +41 -0
- agent_lifecycle/cli/metrics_parser.py +49 -0
- agent_lifecycle/cli/parsers.py +618 -0
- agent_lifecycle/cli/policy.py +101 -0
- agent_lifecycle/cli/progress_hooks.py +117 -0
- agent_lifecycle/cli/worktree.py +74 -0
- agent_lifecycle/compiler/__init__.py +17 -0
- agent_lifecycle/compiler/small_model_packets.py +322 -0
- agent_lifecycle/compiler/task_packets.py +229 -0
- agent_lifecycle/context/__init__.py +20 -0
- agent_lifecycle/context/episode_retrieval.py +30 -0
- agent_lifecycle/context/external_memory.py +71 -0
- agent_lifecycle/context/profiles.py +104 -0
- agent_lifecycle/context/rendering.py +226 -0
- agent_lifecycle/contracts/__init__.py +25 -0
- agent_lifecycle/contracts/adapter_contract_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_event_schemas.py +118 -0
- agent_lifecycle/contracts/adapter_session_schemas.py +130 -0
- agent_lifecycle/contracts/adapter_task_schemas.py +90 -0
- agent_lifecycle/contracts/audit_schemas.py +133 -0
- agent_lifecycle/contracts/bug_forensics_schemas.py +400 -0
- agent_lifecycle/contracts/canonical.py +55 -0
- agent_lifecycle/contracts/compatibility.py +242 -0
- agent_lifecycle/contracts/context_model_schemas.py +265 -0
- agent_lifecycle/contracts/core_schemas.py +252 -0
- agent_lifecycle/contracts/cross_check_schemas.py +121 -0
- agent_lifecycle/contracts/errors.py +26 -0
- agent_lifecycle/contracts/evidence_import_schemas.py +152 -0
- agent_lifecycle/contracts/host_capability_schemas.py +64 -0
- agent_lifecycle/contracts/import_dialect_schemas.py +166 -0
- agent_lifecycle/contracts/metric_schemas.py +315 -0
- agent_lifecycle/contracts/paths.py +31 -0
- agent_lifecycle/contracts/plan_contract_schemas.py +148 -0
- agent_lifecycle/contracts/policy_schemas.py +350 -0
- agent_lifecycle/contracts/progress_bridge_schemas.py +114 -0
- agent_lifecycle/contracts/progress_hook_schemas.py +119 -0
- agent_lifecycle/contracts/proof_integrity_schemas.py +240 -0
- agent_lifecycle/contracts/release_contract_schemas.py +476 -0
- agent_lifecycle/contracts/review_mesh_recommendation_schemas.py +64 -0
- agent_lifecycle/contracts/review_mesh_schemas.py +222 -0
- agent_lifecycle/contracts/review_quality_schemas.py +109 -0
- agent_lifecycle/contracts/runner_schemas.py +91 -0
- agent_lifecycle/contracts/runner_worktree_schemas.py +389 -0
- agent_lifecycle/contracts/sandbox_schemas.py +159 -0
- agent_lifecycle/contracts/schema_builders.py +23 -0
- agent_lifecycle/contracts/schemas.py +93 -0
- agent_lifecycle/contracts/status_goal_schemas.py +469 -0
- agent_lifecycle/contracts/task_template_schemas.py +96 -0
- agent_lifecycle/contracts/usage_export_schemas.py +68 -0
- agent_lifecycle/diagnostics/__init__.py +17 -0
- agent_lifecycle/diagnostics/bundles.py +124 -0
- agent_lifecycle/diagnostics/readiness.py +733 -0
- agent_lifecycle/evidence_index/__init__.py +39 -0
- agent_lifecycle/evidence_index/core.py +272 -0
- agent_lifecycle/evidence_index/episode_index.py +329 -0
- agent_lifecycle/evidence_index/external_context.py +276 -0
- agent_lifecycle/followup/__init__.py +23 -0
- agent_lifecycle/followup/records.py +404 -0
- agent_lifecycle/freeze/__init__.py +5 -0
- agent_lifecycle/freeze/locks.py +23 -0
- agent_lifecycle/goal/__init__.py +15 -0
- agent_lifecycle/goal/records.py +244 -0
- agent_lifecycle/goal/view.py +109 -0
- agent_lifecycle/host_protocol/__init__.py +65 -0
- agent_lifecycle/host_protocol/acp_capability.py +195 -0
- agent_lifecycle/host_protocol/capabilities.py +268 -0
- agent_lifecycle/host_protocol/contracts.py +214 -0
- agent_lifecycle/host_protocol/event_capture.py +390 -0
- agent_lifecycle/host_protocol/events.py +196 -0
- agent_lifecycle/host_protocol/inspection.py +194 -0
- agent_lifecycle/host_protocol/inspection_common.py +310 -0
- agent_lifecycle/host_protocol/inspection_cursor.py +164 -0
- agent_lifecycle/host_protocol/inspection_gemini_cli.py +163 -0
- agent_lifecycle/host_protocol/inspection_hermes.py +161 -0
- agent_lifecycle/host_protocol/inspection_kimi_code.py +169 -0
- agent_lifecycle/host_protocol/inspection_opencode.py +147 -0
- agent_lifecycle/host_protocol/inspection_qwen_code.py +143 -0
- agent_lifecycle/host_protocol/receipts.py +58 -0
- agent_lifecycle/host_protocol/scaffold.py +329 -0
- agent_lifecycle/host_protocol/validation.py +229 -0
- agent_lifecycle/imports/__init__.py +76 -0
- agent_lifecycle/imports/agentskills_profile.py +54 -0
- agent_lifecycle/imports/bmad_profile.py +49 -0
- agent_lifecycle/imports/constitution_adr.py +91 -0
- agent_lifecycle/imports/dialect_profiles.py +121 -0
- agent_lifecycle/imports/external_agent.py +28 -0
- agent_lifecycle/imports/external_dialects.py +486 -0
- agent_lifecycle/imports/external_workflow.py +28 -0
- agent_lifecycle/imports/multi_markdown.py +111 -0
- agent_lifecycle/imports/openspec_profile.py +49 -0
- agent_lifecycle/imports/planning.py +536 -0
- agent_lifecycle/imports/spec_kit_profile.py +49 -0
- agent_lifecycle/imports/spec_kitty_profile.py +49 -0
- agent_lifecycle/metrics/__init__.py +60 -0
- agent_lifecycle/metrics/cost_collection.py +315 -0
- agent_lifecycle/metrics/costs.py +203 -0
- agent_lifecycle/metrics/outcome_index.py +330 -0
- agent_lifecycle/metrics/phase_resources.py +208 -0
- agent_lifecycle/metrics/recommendations.py +507 -0
- agent_lifecycle/metrics/regression_signals.py +64 -0
- agent_lifecycle/metrics/usage_export.py +390 -0
- agent_lifecycle/model_routing/__init__.py +19 -0
- agent_lifecycle/model_routing/profiles.py +323 -0
- agent_lifecycle/model_routing/receipts.py +140 -0
- agent_lifecycle/model_routing/resolver.py +566 -0
- agent_lifecycle/neutrality/__init__.py +12 -0
- agent_lifecycle/neutrality/__main__.py +6 -0
- agent_lifecycle/neutrality/authority.py +265 -0
- agent_lifecycle/neutrality/canonical.py +28 -0
- agent_lifecycle/neutrality/cli.py +294 -0
- agent_lifecycle/neutrality/ed25519.py +124 -0
- agent_lifecycle/neutrality/errors.py +89 -0
- agent_lifecycle/neutrality/gate.py +269 -0
- agent_lifecycle/neutrality/paths.py +92 -0
- agent_lifecycle/neutrality/policy.py +76 -0
- agent_lifecycle/neutrality/receipt.py +135 -0
- agent_lifecycle/neutrality/scanner.py +310 -0
- agent_lifecycle/neutrality/test_runner.py +116 -0
- agent_lifecycle/planning/__init__.py +47 -0
- agent_lifecycle/planning/acceptance_markdown.py +159 -0
- agent_lifecycle/planning/completeness.py +362 -0
- agent_lifecycle/planning/continuity.py +255 -0
- agent_lifecycle/planning/templates.py +294 -0
- agent_lifecycle/planning/tiering.py +87 -0
- agent_lifecycle/planning/validation.py +72 -0
- agent_lifecycle/policy/__init__.py +37 -0
- agent_lifecycle/policy/adaptive_lifecycle.py +416 -0
- agent_lifecycle/policy/apply.py +44 -0
- agent_lifecycle/policy/proposals.py +189 -0
- agent_lifecycle/policy/quality_floor.py +144 -0
- agent_lifecycle/policy/runtime_receipts.py +160 -0
- agent_lifecycle/py.typed +1 -0
- agent_lifecycle/quality/__init__.py +89 -0
- agent_lifecycle/quality/bug_forensics.py +727 -0
- agent_lifecycle/quality/bug_forensics_advisor.py +88 -0
- agent_lifecycle/quality/bug_forensics_recipes.py +239 -0
- agent_lifecycle/quality/cross_check.py +437 -0
- agent_lifecycle/quality/failure_classification.py +318 -0
- agent_lifecycle/quality/packs.py +265 -0
- agent_lifecycle/reporting/__init__.py +37 -0
- agent_lifecycle/reporting/change_summary.py +156 -0
- agent_lifecycle/reporting/event_feed.py +101 -0
- agent_lifecycle/reporting/progress_bridge.py +153 -0
- agent_lifecycle/reporting/progress_hooks.py +161 -0
- agent_lifecycle/reporting/progress_terminal.py +95 -0
- agent_lifecycle/reporting/progress_view.py +284 -0
- agent_lifecycle/reporting/status_view.py +125 -0
- agent_lifecycle/reporting/usage_export.py +59 -0
- agent_lifecycle/review/__init__.py +5 -0
- agent_lifecycle/review/validation.py +29 -0
- agent_lifecycle/review_mesh/__init__.py +83 -0
- agent_lifecycle/review_mesh/assignments.py +160 -0
- agent_lifecycle/review_mesh/contracts.py +685 -0
- agent_lifecycle/review_mesh/operator_templates.py +320 -0
- agent_lifecycle/review_mesh/quorum.py +38 -0
- agent_lifecycle/review_mesh/recommendation.py +487 -0
- agent_lifecycle/review_mesh/results.py +98 -0
- agent_lifecycle/review_mesh/synthesis.py +102 -0
- agent_lifecycle/runner/__init__.py +47 -0
- agent_lifecycle/runner/attempt_snapshots.py +244 -0
- agent_lifecycle/runner/core.py +653 -0
- agent_lifecycle/runner/sandbox_receipts.py +628 -0
- agent_lifecycle/specification/__init__.py +23 -0
- agent_lifecycle/specification/completion_check.py +135 -0
- agent_lifecycle/specification/completion_gate.py +501 -0
- agent_lifecycle/specification/completion_signal.py +75 -0
- agent_lifecycle/specification/validation.py +48 -0
- agent_lifecycle/workflow/__init__.py +79 -0
- agent_lifecycle/workflow/artifacts.py +74 -0
- agent_lifecycle/workflow/budget_decisions.py +370 -0
- agent_lifecycle/workflow/budget_policy.py +140 -0
- agent_lifecycle/workflow/budget_receipts.py +70 -0
- agent_lifecycle/workflow/bug_forensics_gates.py +284 -0
- agent_lifecycle/workflow/controller.py +45 -0
- agent_lifecycle/workflow/events.py +50 -0
- agent_lifecycle/workflow/final_proof_integrity.py +74 -0
- agent_lifecycle/workflow/finalization.py +422 -0
- agent_lifecycle/workflow/gates.py +185 -0
- agent_lifecycle/workflow/implementation_audit_gate.py +126 -0
- agent_lifecycle/workflow/leases.py +220 -0
- agent_lifecycle/workflow/lineage.py +108 -0
- agent_lifecycle/workflow/managed_runner.py +167 -0
- agent_lifecycle/workflow/model_usage.py +106 -0
- agent_lifecycle/workflow/next_action.py +86 -0
- agent_lifecycle/workflow/operation_kernel.py +98 -0
- agent_lifecycle/workflow/plan_adoption.py +439 -0
- agent_lifecycle/workflow/query.py +52 -0
- agent_lifecycle/workflow/review_mesh_gate.py +93 -0
- agent_lifecycle/workflow/reviews.py +168 -0
- agent_lifecycle/workflow/run_transitions.py +163 -0
- agent_lifecycle/workflow/sandbox_policy.py +214 -0
- agent_lifecycle/workflow/selectors.py +57 -0
- agent_lifecycle/workflow/state.py +130 -0
- agent_lifecycle/workflow/task_transitions.py +421 -0
- agent_lifecycle/worktree/__init__.py +17 -0
- agent_lifecycle/worktree/isolation.py +407 -0
- agent_lifecycle_kit-1.50.1.dist-info/METADATA +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/RECORD +227 -0
- agent_lifecycle_kit-1.50.1.dist-info/WHEEL +5 -0
- agent_lifecycle_kit-1.50.1.dist-info/entry_points.txt +3 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/LICENSE +201 -0
- agent_lifecycle_kit-1.50.1.dist-info/licenses/NOTICE +5 -0
- 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)
|