hexastack-flow 0.5.0__tar.gz

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.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: hexastack-flow
3
+ Version: 0.5.0
4
+ Summary: Hexagonal workflow engine adapter integrating hexaflow with CQRS, DB, and Events
5
+ Author: Richard West
6
+ Author-email: Richard West <dopplereffect.us@gmail.com>
7
+ License-Expression: Apache-2.0
8
+ Requires-Dist: hexaflow>=0.2.0
9
+ Requires-Dist: hexastack-core
10
+ Requires-Dist: hexastack-cqrs
11
+ Requires-Dist: hexastack-flow[db,events] ; extra == 'all'
12
+ Requires-Dist: hexastack-db ; extra == 'db'
13
+ Requires-Dist: sqlalchemy>=2.0.0 ; extra == 'db'
14
+ Requires-Dist: hexastack-events ; extra == 'events'
15
+ Requires-Python: >=3.13
16
+ Provides-Extra: all
17
+ Provides-Extra: db
18
+ Provides-Extra: events
19
+ Description-Content-Type: text/markdown
20
+
21
+ # 🌊 `hexastack-flow`
22
+
23
+ > Hexagonal workflow engine adapter integrating `hexaflow` with Hexastack CQRS, Relational Persistence, and Event Streaming.
24
+
25
+ ---
26
+
27
+ ## 🎯 Architectural Intent
28
+
29
+ `hexastack-flow` provides first-class hexagonal adapters connecting the lightweight [`hexaflow`](https://pypi.org/project/hexaflow/) workflow engine to Hexastack:
30
+
31
+ 1. **CQRS Bridge**: `CommandStep` and `QueryStep` allow workflow steps to dispatch typed commands and queries directly onto the `CommandBus` and `QueryBus`, inheriting all CQRS middleware (auth, tracing, logging, metrics, retries).
32
+ 2. **Durable Distributed Sagas**: Workflows natively subsume the Saga pattern by pairing forward command execution stages with compensating rollback stages, backed by durable checkpoints.
33
+ 3. **Enterprise Persistence**: `SqlAlchemyWorkflowStore` persists workflow execution records and step checkpoints directly to PostgreSQL/SQLite/MySQL within transactional boundaries.
34
+ 4. **Event Streaming**: Emits domain events (`WorkflowStartedEvent`, `WorkflowStepCompletedEvent`, `WorkflowSuspendedEvent`, `WorkflowCompletedEvent`, `WorkflowAbortedEvent`) wrapped in standard `CloudEventEnvelope`s through `hexastack-events`.
35
+ 5. **DevTools Visualizer**: Backing engine for NiceGUI and Textual DAG monitoring.
36
+
37
+ ---
38
+
39
+ ## 🔄 Implementing the Saga Pattern as a Workflow
40
+
41
+ Historically, the **Saga Pattern** structures a distributed business transaction across multiple microservices or bounded contexts as a sequence of local transactions:
42
+ - **Forward transactions**: $T_1, T_2, \dots, T_n$
43
+ - **Compensating transactions**: $C_{k-1}, \dots, C_1$ executed in reverse (LIFO) order if step $k$ fails.
44
+
45
+ In `hexastack-flow`, you do **not** need a separate, restricted saga engine. A Saga is simply a workflow DAG that dispatches CQRS commands and executes compensating commands on failure.
46
+
47
+ ### Example: Trip Booking Saga with CQRS & Hexaflow
48
+
49
+ ```python
50
+ from hexaflow import Workflow
51
+ from hexastack_cqrs.ports.buses import CommandBusPort
52
+ from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
53
+ from hexastack_flow.adapters.cqrs.steps import as_command_step
54
+
55
+ # 1. Define CQRS commands for forward actions & compensations
56
+ from my_domain.commands import (
57
+ BookFlightCommand,
58
+ CancelFlightCommand,
59
+ BookHotelCommand,
60
+ CancelHotelCommand,
61
+ ChargePaymentCommand,
62
+ )
63
+
64
+ def build_trip_booking_workflow(bus: CommandBusPort, customer_id: str) -> Workflow:
65
+ """Construct a durable distributed saga as a hexaflow Workflow."""
66
+ workflow = (
67
+ Workflow("trip-booking-saga")
68
+ # Step 1: Reserve Flight
69
+ .stage(
70
+ "flight",
71
+ as_command_step(
72
+ lambda ctx: BookFlightCommand(customer_id=customer_id, destination="JFK"),
73
+ bus,
74
+ name="book_flight",
75
+ ),
76
+ )
77
+ # Step 2: Reserve Hotel (runs after flight is confirmed)
78
+ .stage(
79
+ "hotel",
80
+ as_command_step(
81
+ lambda ctx: BookHotelCommand(customer_id=customer_id, room_type="deluxe"),
82
+ bus,
83
+ name="book_hotel",
84
+ ),
85
+ depends_on=["flight"],
86
+ )
87
+ # Step 3: Process Payment
88
+ .stage(
89
+ "payment",
90
+ as_command_step(
91
+ lambda ctx: ChargePaymentCommand(customer_id=customer_id, amount_cents=50000),
92
+ bus,
93
+ name="charge_payment",
94
+ ),
95
+ depends_on=["hotel"],
96
+ )
97
+ )
98
+ return workflow
99
+
100
+ # 2. Execute via CqrsWorkflowRunner (with durable DB checkpoints)
101
+ runner = CqrsWorkflowRunner(engine=engine)
102
+ result = await runner.run(workflow.build())
103
+ ```
104
+
105
+ ### Why Workflows are Superior to Linear Sagas
106
+
107
+ | Feature | Legacy In-Memory Saga | `hexastack-flow` Workflow |
108
+ |---|---|---|
109
+ | **Execution Topologies** | Strictly sequential ($T_1 \to T_2 \to T_3$) | **Arbitrary DAGs** (e.g. reserve Flight & Hotel in parallel) |
110
+ | **Crash Recovery** | In-memory only (lost if process restarts) | **Durable Checkpoints** (`SqlAlchemyWorkflowStore`) |
111
+ | **Step Retries** | Manual loop or fail immediately | **Configurable `RetryPolicy`** (exponential backoff, jitter) |
112
+ | **Observability** | Console logs | **CloudEvents** (`WorkflowStepCompletedEvent`) + OTel spans |
113
+ | **Human-in-the-Loop** | Not supported | **Pause / Resume** (`engine.suspend()`, `engine.resume()`) |
114
+
115
+ ---
116
+
117
+ ## 📦 Installation
118
+
119
+ ```bash
120
+ pip install hexastack[flow]
121
+ ```
122
+
123
+ Or standalone:
124
+
125
+ ```bash
126
+ pip install hexastack-flow
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 📄 License
132
+
133
+ Apache 2.0
@@ -0,0 +1,113 @@
1
+ # 🌊 `hexastack-flow`
2
+
3
+ > Hexagonal workflow engine adapter integrating `hexaflow` with Hexastack CQRS, Relational Persistence, and Event Streaming.
4
+
5
+ ---
6
+
7
+ ## 🎯 Architectural Intent
8
+
9
+ `hexastack-flow` provides first-class hexagonal adapters connecting the lightweight [`hexaflow`](https://pypi.org/project/hexaflow/) workflow engine to Hexastack:
10
+
11
+ 1. **CQRS Bridge**: `CommandStep` and `QueryStep` allow workflow steps to dispatch typed commands and queries directly onto the `CommandBus` and `QueryBus`, inheriting all CQRS middleware (auth, tracing, logging, metrics, retries).
12
+ 2. **Durable Distributed Sagas**: Workflows natively subsume the Saga pattern by pairing forward command execution stages with compensating rollback stages, backed by durable checkpoints.
13
+ 3. **Enterprise Persistence**: `SqlAlchemyWorkflowStore` persists workflow execution records and step checkpoints directly to PostgreSQL/SQLite/MySQL within transactional boundaries.
14
+ 4. **Event Streaming**: Emits domain events (`WorkflowStartedEvent`, `WorkflowStepCompletedEvent`, `WorkflowSuspendedEvent`, `WorkflowCompletedEvent`, `WorkflowAbortedEvent`) wrapped in standard `CloudEventEnvelope`s through `hexastack-events`.
15
+ 5. **DevTools Visualizer**: Backing engine for NiceGUI and Textual DAG monitoring.
16
+
17
+ ---
18
+
19
+ ## 🔄 Implementing the Saga Pattern as a Workflow
20
+
21
+ Historically, the **Saga Pattern** structures a distributed business transaction across multiple microservices or bounded contexts as a sequence of local transactions:
22
+ - **Forward transactions**: $T_1, T_2, \dots, T_n$
23
+ - **Compensating transactions**: $C_{k-1}, \dots, C_1$ executed in reverse (LIFO) order if step $k$ fails.
24
+
25
+ In `hexastack-flow`, you do **not** need a separate, restricted saga engine. A Saga is simply a workflow DAG that dispatches CQRS commands and executes compensating commands on failure.
26
+
27
+ ### Example: Trip Booking Saga with CQRS & Hexaflow
28
+
29
+ ```python
30
+ from hexaflow import Workflow
31
+ from hexastack_cqrs.ports.buses import CommandBusPort
32
+ from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
33
+ from hexastack_flow.adapters.cqrs.steps import as_command_step
34
+
35
+ # 1. Define CQRS commands for forward actions & compensations
36
+ from my_domain.commands import (
37
+ BookFlightCommand,
38
+ CancelFlightCommand,
39
+ BookHotelCommand,
40
+ CancelHotelCommand,
41
+ ChargePaymentCommand,
42
+ )
43
+
44
+ def build_trip_booking_workflow(bus: CommandBusPort, customer_id: str) -> Workflow:
45
+ """Construct a durable distributed saga as a hexaflow Workflow."""
46
+ workflow = (
47
+ Workflow("trip-booking-saga")
48
+ # Step 1: Reserve Flight
49
+ .stage(
50
+ "flight",
51
+ as_command_step(
52
+ lambda ctx: BookFlightCommand(customer_id=customer_id, destination="JFK"),
53
+ bus,
54
+ name="book_flight",
55
+ ),
56
+ )
57
+ # Step 2: Reserve Hotel (runs after flight is confirmed)
58
+ .stage(
59
+ "hotel",
60
+ as_command_step(
61
+ lambda ctx: BookHotelCommand(customer_id=customer_id, room_type="deluxe"),
62
+ bus,
63
+ name="book_hotel",
64
+ ),
65
+ depends_on=["flight"],
66
+ )
67
+ # Step 3: Process Payment
68
+ .stage(
69
+ "payment",
70
+ as_command_step(
71
+ lambda ctx: ChargePaymentCommand(customer_id=customer_id, amount_cents=50000),
72
+ bus,
73
+ name="charge_payment",
74
+ ),
75
+ depends_on=["hotel"],
76
+ )
77
+ )
78
+ return workflow
79
+
80
+ # 2. Execute via CqrsWorkflowRunner (with durable DB checkpoints)
81
+ runner = CqrsWorkflowRunner(engine=engine)
82
+ result = await runner.run(workflow.build())
83
+ ```
84
+
85
+ ### Why Workflows are Superior to Linear Sagas
86
+
87
+ | Feature | Legacy In-Memory Saga | `hexastack-flow` Workflow |
88
+ |---|---|---|
89
+ | **Execution Topologies** | Strictly sequential ($T_1 \to T_2 \to T_3$) | **Arbitrary DAGs** (e.g. reserve Flight & Hotel in parallel) |
90
+ | **Crash Recovery** | In-memory only (lost if process restarts) | **Durable Checkpoints** (`SqlAlchemyWorkflowStore`) |
91
+ | **Step Retries** | Manual loop or fail immediately | **Configurable `RetryPolicy`** (exponential backoff, jitter) |
92
+ | **Observability** | Console logs | **CloudEvents** (`WorkflowStepCompletedEvent`) + OTel spans |
93
+ | **Human-in-the-Loop** | Not supported | **Pause / Resume** (`engine.suspend()`, `engine.resume()`) |
94
+
95
+ ---
96
+
97
+ ## 📦 Installation
98
+
99
+ ```bash
100
+ pip install hexastack[flow]
101
+ ```
102
+
103
+ Or standalone:
104
+
105
+ ```bash
106
+ pip install hexastack-flow
107
+ ```
108
+
109
+ ---
110
+
111
+ ## 📄 License
112
+
113
+ Apache 2.0
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "hexastack-flow"
3
+ version = "0.5.0"
4
+ description = "Hexagonal workflow engine adapter integrating hexaflow with CQRS, DB, and Events"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "hexaflow>=0.2.0",
10
+ "hexastack-core",
11
+ "hexastack-cqrs",
12
+ ]
13
+
14
+ [[project.authors]]
15
+ name = "Richard West"
16
+ email = "dopplereffect.us@gmail.com"
17
+
18
+ [project.optional-dependencies]
19
+ db = [
20
+ "hexastack-db",
21
+ "sqlalchemy>=2.0.0",
22
+ ]
23
+ events = ["hexastack-events"]
24
+ all = ["hexastack-flow[db,events]"]
25
+
26
+ [project.entry-points."hexastack.bootstrappers"]
27
+ flow = "hexastack_flow.infra.bootstrap:FlowBootstrapper"
28
+
29
+ [build-system]
30
+ requires = ["uv_build>=0.12.3,<0.13.0"]
31
+ build-backend = "uv_build"
32
+
33
+ [tool.uv.sources.hexastack-core]
34
+ workspace = true
35
+
36
+ [tool.uv.sources.hexastack-cqrs]
37
+ workspace = true
38
+
39
+ [tool.uv.sources.hexastack-db]
40
+ workspace = true
41
+
42
+ [tool.uv.sources.hexastack-events]
43
+ workspace = true
44
+
45
+ [tool.importlinter]
46
+ root_packages = ["hexastack_flow"]
47
+
48
+ [[tool.importlinter.contracts]]
49
+ name = "Hexagonal architecture layer hierarchy"
50
+ type = "layers"
51
+ containers = ["hexastack_flow"]
52
+ layers = [
53
+ "infra",
54
+ "adapters",
55
+ "ports",
56
+ "domain",
57
+ ]
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "hexastack-flow"
3
+ version = "0.5.0"
4
+ description = "Hexagonal workflow engine adapter integrating hexaflow with CQRS, DB, and Events"
5
+ readme = "README.md"
6
+ license = "Apache-2.0"
7
+ authors = [
8
+ { name = "Richard West", email = "dopplereffect.us@gmail.com" }
9
+ ]
10
+ requires-python = ">=3.13"
11
+ dependencies = [
12
+ "hexaflow>=0.2.0",
13
+ "hexastack-core",
14
+ "hexastack-cqrs",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ db = [
19
+ "hexastack-db",
20
+ "sqlalchemy>=2.0.0",
21
+ ]
22
+ events = [
23
+ "hexastack-events",
24
+ ]
25
+ all = [
26
+ "hexastack-flow[db,events]",
27
+ ]
28
+
29
+ [project.entry-points."hexastack.bootstrappers"]
30
+ flow = "hexastack_flow.infra.bootstrap:FlowBootstrapper"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.12.3,<0.13.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [tool.uv.sources]
37
+ hexastack-core = { workspace = true }
38
+ hexastack-cqrs = { workspace = true }
39
+ hexastack-db = { workspace = true }
40
+ hexastack-events = { workspace = true }
41
+
42
+ [tool.importlinter]
43
+ root_packages = ["hexastack_flow"]
44
+
45
+ [[tool.importlinter.contracts]]
46
+ name = "Hexagonal architecture layer hierarchy"
47
+ type = "layers"
48
+ containers = ["hexastack_flow"]
49
+ layers = [
50
+ "infra",
51
+ "adapters",
52
+ "ports",
53
+ "domain",
54
+ ]
@@ -0,0 +1,59 @@
1
+ """Hexastack Flow - Hexagonal workflow orchestration framework.
2
+
3
+ Notes/Architectural Intent:
4
+ Integrates the zero-daemon hexaflow workflow execution engine into Hexastack,
5
+ providing CQRS command/query steps, saga migration, relational checkpoint persistence,
6
+ and lifecycle domain event broadcasting.
7
+ """
8
+
9
+ from hexaflow import RetryPolicy, StageExecutionMode, Workflow
10
+
11
+ from hexastack_flow import adapters, domain, infra, ports
12
+ from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
13
+ from hexastack_flow.adapters.cqrs.steps import (
14
+ CommandStep,
15
+ QueryStep,
16
+ as_command_step,
17
+ as_query_step,
18
+ )
19
+ from hexastack_flow.adapters.events.publisher import WorkflowEventPublisher
20
+ from hexastack_flow.adapters.storage.sqlalchemy import SqlAlchemyWorkflowStore
21
+ from hexastack_flow.domain.events import (
22
+ WorkflowAbortedEvent,
23
+ WorkflowCompletedEvent,
24
+ WorkflowStartedEvent,
25
+ WorkflowStepCompletedEvent,
26
+ WorkflowSuspendedEvent,
27
+ )
28
+ from hexastack_flow.domain.models import (
29
+ CqrsStepMetadata,
30
+ WorkflowExecutionResult,
31
+ )
32
+ from hexastack_flow.infra.bootstrap import FlowBootstrapper
33
+ from hexastack_flow.ports.orchestrator import CqrsWorkflowOrchestratorPort
34
+
35
+ __all__ = [
36
+ "adapters",
37
+ "as_command_step",
38
+ "as_query_step",
39
+ "CommandStep",
40
+ "CqrsStepMetadata",
41
+ "CqrsWorkflowOrchestratorPort",
42
+ "CqrsWorkflowRunner",
43
+ "domain",
44
+ "FlowBootstrapper",
45
+ "infra",
46
+ "ports",
47
+ "QueryStep",
48
+ "RetryPolicy",
49
+ "SqlAlchemyWorkflowStore",
50
+ "StageExecutionMode",
51
+ "Workflow",
52
+ "WorkflowAbortedEvent",
53
+ "WorkflowCompletedEvent",
54
+ "WorkflowEventPublisher",
55
+ "WorkflowExecutionResult",
56
+ "WorkflowStartedEvent",
57
+ "WorkflowStepCompletedEvent",
58
+ "WorkflowSuspendedEvent",
59
+ ]
@@ -0,0 +1,29 @@
1
+ """Adapters for hexastack-flow.
2
+
3
+ Notes/Architectural Intent:
4
+ Exports CQRS, Storage, and Event adapters bridging hexaflow into Hexastack.
5
+ """
6
+
7
+ from hexastack_flow.adapters import cqrs, events, storage
8
+ from hexastack_flow.adapters.cqrs import (
9
+ CommandStep,
10
+ CqrsWorkflowRunner,
11
+ QueryStep,
12
+ as_command_step,
13
+ as_query_step,
14
+ )
15
+ from hexastack_flow.adapters.events import WorkflowEventPublisher
16
+ from hexastack_flow.adapters.storage import SqlAlchemyWorkflowStore
17
+
18
+ __all__ = [
19
+ "as_command_step",
20
+ "as_query_step",
21
+ "CommandStep",
22
+ "cqrs",
23
+ "CqrsWorkflowRunner",
24
+ "events",
25
+ "QueryStep",
26
+ "SqlAlchemyWorkflowStore",
27
+ "storage",
28
+ "WorkflowEventPublisher",
29
+ ]
@@ -0,0 +1,21 @@
1
+ """CQRS adapters for hexaflow workflow execution.
2
+
3
+ Notes/Architectural Intent:
4
+ Adapters connecting Hexastack CQRS buses and legacy saga models to hexaflow.
5
+ """
6
+
7
+ from hexastack_flow.adapters.cqrs.runner import CqrsWorkflowRunner
8
+ from hexastack_flow.adapters.cqrs.steps import (
9
+ CommandStep,
10
+ QueryStep,
11
+ as_command_step,
12
+ as_query_step,
13
+ )
14
+
15
+ __all__ = [
16
+ "as_command_step",
17
+ "as_query_step",
18
+ "CommandStep",
19
+ "CqrsWorkflowRunner",
20
+ "QueryStep",
21
+ ]
@@ -0,0 +1,212 @@
1
+ """CQRS workflow runner adapter implementing CqrsWorkflowOrchestratorPort.
2
+
3
+ Notes/Architectural Intent:
4
+ Glues the hexaflow execution engine, state store, and event bus together.
5
+ Translates hexaflow state objects into Hexastack domain results and
6
+ publishes lifecycle domain events.
7
+ """
8
+
9
+ from datetime import UTC, datetime
10
+ from typing import Any
11
+
12
+ from hexaflow.adapters.engines.local_async import AsyncioWorkflowEngine
13
+ from hexaflow.adapters.storage.in_memory import InMemoryStateStore
14
+ from hexaflow.domain.models import WorkflowDefinition
15
+ from hexaflow.domain.state import StepStatus, WorkflowExecutionState, WorkflowStatus
16
+ from hexaflow.ports.engine import WorkflowEnginePort
17
+ from hexaflow.ports.storage import WorkflowStateStorePort
18
+
19
+ from hexastack_cqrs.ports.buses import EventBusPort
20
+ from hexastack_flow.domain.events import (
21
+ WorkflowAbortedEvent,
22
+ WorkflowCompletedEvent,
23
+ WorkflowStartedEvent,
24
+ WorkflowSuspendedEvent,
25
+ )
26
+ from hexastack_flow.domain.models import WorkflowExecutionResult
27
+ from hexastack_flow.ports.orchestrator import CqrsWorkflowOrchestratorPort
28
+
29
+ __all__ = [
30
+ "CqrsWorkflowRunner",
31
+ ]
32
+
33
+
34
+ class CqrsWorkflowRunner(CqrsWorkflowOrchestratorPort):
35
+ """Orchestrator runner coordinating workflow execution with CQRS buses.
36
+
37
+ Notes/Architectural Intent:
38
+ Implements CqrsWorkflowOrchestratorPort to provide a complete
39
+ in-process execution runtime with automatic event publishing.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ state_store: WorkflowStateStorePort | None = None,
45
+ engine: WorkflowEnginePort | None = None,
46
+ event_bus: EventBusPort | None = None,
47
+ ) -> None:
48
+ """Initialize runner with state store, execution engine, and event bus.
49
+
50
+ Args:
51
+ state_store: WorkflowStateStorePort for persistence. Defaults to InMemoryStateStore.
52
+ engine: WorkflowEnginePort for DAG execution. Defaults to AsyncioWorkflowEngine.
53
+ event_bus: Optional EventBusPort for publishing workflow domain events.
54
+ """
55
+ self._store = state_store or InMemoryStateStore()
56
+ self._engine = engine or AsyncioWorkflowEngine(state_store=self._store)
57
+ self._event_bus = event_bus
58
+
59
+ def execute(
60
+ self,
61
+ workflow: WorkflowDefinition,
62
+ initial_inputs: dict[str, Any] | None = None,
63
+ ) -> WorkflowExecutionResult:
64
+ """Execute workflow definition from the first stage.
65
+
66
+ Args:
67
+ workflow: The WorkflowDefinition to run.
68
+ initial_inputs: Optional dictionary of root arguments.
69
+
70
+ Returns:
71
+ WorkflowExecutionResult summarizing status, step outputs, and timing.
72
+ """
73
+ start_time = datetime.now(UTC)
74
+ state = self._engine.run(workflow, initial_inputs=initial_inputs)
75
+
76
+ if self._event_bus is not None:
77
+ self._event_bus.publish(
78
+ WorkflowStartedEvent(
79
+ run_id=state.run_id,
80
+ workflow_name=state.workflow_name,
81
+ started_at=start_time,
82
+ )
83
+ )
84
+
85
+ self._emit_terminal_events(state)
86
+ return self._to_result(state)
87
+
88
+ def resume(
89
+ self,
90
+ run_id: str,
91
+ workflow: WorkflowDefinition,
92
+ patch_inputs: dict[str, Any] | None = None,
93
+ ) -> WorkflowExecutionResult:
94
+ """Resume a suspended workflow run from its latest checkpoints.
95
+
96
+ Args:
97
+ run_id: Unique identifier of the suspended run.
98
+ workflow: WorkflowDefinition specification matching the run.
99
+ patch_inputs: Optional updated inputs for the suspended step.
100
+
101
+ Returns:
102
+ WorkflowExecutionResult reflecting post-resumption status.
103
+ """
104
+ state = self._engine.resume(run_id, workflow, patch_inputs=patch_inputs)
105
+ self._emit_terminal_events(state)
106
+ return self._to_result(state)
107
+
108
+ def abort(
109
+ self,
110
+ run_id: str,
111
+ workflow: WorkflowDefinition,
112
+ reason: str = "Operator aborted",
113
+ ) -> WorkflowExecutionResult:
114
+ """Abort a workflow and unwind completed steps via compensation hooks.
115
+
116
+ Args:
117
+ run_id: Unique identifier of the workflow run.
118
+ workflow: WorkflowDefinition specification containing compensation hooks.
119
+ reason: Operator rationale or explanation for cancellation.
120
+
121
+ Returns:
122
+ WorkflowExecutionResult reflecting the aborted state.
123
+ """
124
+ state = self._engine.abort(run_id, workflow)
125
+
126
+ if self._event_bus is not None:
127
+ self._event_bus.publish(
128
+ WorkflowAbortedEvent(
129
+ run_id=state.run_id,
130
+ workflow_name=state.workflow_name,
131
+ reason=reason,
132
+ aborted_at=datetime.now(UTC),
133
+ )
134
+ )
135
+
136
+ return self._to_result(state)
137
+
138
+ def _emit_terminal_events(self, state: WorkflowExecutionState) -> None:
139
+ """Publish lifecycle events based on execution state outcome.
140
+
141
+ Args:
142
+ state: WorkflowExecutionState to inspect.
143
+ """
144
+ if self._event_bus is None:
145
+ return
146
+
147
+ if state.status == WorkflowStatus.COMPLETED:
148
+ self._event_bus.publish(
149
+ WorkflowCompletedEvent(
150
+ run_id=state.run_id,
151
+ workflow_name=state.workflow_name,
152
+ completed_at=state.finished_at or datetime.now(UTC),
153
+ total_steps=len(state.step_checkpoints),
154
+ )
155
+ )
156
+ elif state.status == WorkflowStatus.SUSPENDED:
157
+ failed_step = next(
158
+ (
159
+ name
160
+ for name, chk in state.step_checkpoints.items()
161
+ if chk.status == StepStatus.FAILED
162
+ ),
163
+ "unknown",
164
+ )
165
+ self._event_bus.publish(
166
+ WorkflowSuspendedEvent(
167
+ run_id=state.run_id,
168
+ stage_name=state.current_stage or "unknown",
169
+ step_name=failed_step,
170
+ error_type="StepExecutionError",
171
+ error_message=state.error_summary
172
+ or "Step failed and exhausted retries.",
173
+ )
174
+ )
175
+
176
+ @staticmethod
177
+ def _to_result(state: WorkflowExecutionState) -> WorkflowExecutionResult:
178
+ """Map a hexaflow WorkflowExecutionState into a WorkflowExecutionResult.
179
+
180
+ Args:
181
+ state: Engine execution state.
182
+
183
+ Returns:
184
+ Pydantic WorkflowExecutionResult value object.
185
+ """
186
+ completed = tuple(
187
+ name
188
+ for name, chk in state.step_checkpoints.items()
189
+ if chk.status == StepStatus.COMPLETED
190
+ )
191
+ failed = tuple(
192
+ name
193
+ for name, chk in state.step_checkpoints.items()
194
+ if chk.status == StepStatus.FAILED
195
+ )
196
+ outputs = {
197
+ name: chk.output_payload
198
+ for name, chk in state.step_checkpoints.items()
199
+ if chk.output_payload is not None
200
+ }
201
+
202
+ return WorkflowExecutionResult(
203
+ run_id=state.run_id,
204
+ workflow_name=state.workflow_name,
205
+ status=state.status.value,
206
+ completed_steps=completed,
207
+ failed_steps=failed,
208
+ outputs=outputs,
209
+ started_at=state.started_at,
210
+ ended_at=state.finished_at or datetime.now(UTC),
211
+ error_summary=state.error_summary,
212
+ )