smith-agents 0.4.5 → 0.6.0

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 (186) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -0
  3. data/README.md +191 -4
  4. data/docs/CONFIGURATION.md +4 -1
  5. data/docs/PATTERNS.md +7 -0
  6. data/docs/PERSISTENCE.md +347 -25
  7. data/lib/smith/agent/lifecycle.rb +19 -10
  8. data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
  9. data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
  10. data/lib/smith/agent/registry.rb +8 -0
  11. data/lib/smith/agent.rb +37 -2
  12. data/lib/smith/budget/amount_contract.rb +67 -0
  13. data/lib/smith/budget/amount_representation.rb +43 -0
  14. data/lib/smith/budget/decimal_context.rb +18 -0
  15. data/lib/smith/budget/ledger.rb +110 -18
  16. data/lib/smith/budget/ledger_publication.rb +60 -0
  17. data/lib/smith/budget/ledger_state.rb +21 -0
  18. data/lib/smith/budget/ledger_state_transition.rb +72 -0
  19. data/lib/smith/budget/public_state_validator.rb +25 -0
  20. data/lib/smith/budget/reservation.rb +20 -0
  21. data/lib/smith/budget/reservation_contract.rb +34 -0
  22. data/lib/smith/context/observation_masking.rb +3 -1
  23. data/lib/smith/context/state_injection.rb +1 -1
  24. data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
  25. data/lib/smith/error.rb +5 -0
  26. data/lib/smith/errors.rb +10 -0
  27. data/lib/smith/exponential_backoff.rb +128 -0
  28. data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
  29. data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
  30. data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
  31. data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
  32. data/lib/smith/persistence_adapters/cache_store.rb +1 -0
  33. data/lib/smith/persistence_adapters/memory.rb +16 -1
  34. data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
  35. data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
  36. data/lib/smith/persistence_adapters/redis_store.rb +28 -11
  37. data/lib/smith/persistence_adapters/retry.rb +22 -10
  38. data/lib/smith/persistence_adapters.rb +21 -18
  39. data/lib/smith/persistence_payload_conflict.rb +12 -0
  40. data/lib/smith/tool/budget_enforcement.rb +2 -2
  41. data/lib/smith/types.rb +1 -0
  42. data/lib/smith/version.rb +2 -1
  43. data/lib/smith/workflow/branch_env.rb +1 -1
  44. data/lib/smith/workflow/budget_integration.rb +21 -11
  45. data/lib/smith/workflow/composite/branch.rb +93 -0
  46. data/lib/smith/workflow/composite/branch_budget_contract.rb +41 -0
  47. data/lib/smith/workflow/composite/branch_contract.rb +102 -0
  48. data/lib/smith/workflow/composite/branch_execution.rb +124 -0
  49. data/lib/smith/workflow/composite/branch_failure.rb +91 -0
  50. data/lib/smith/workflow/composite/branch_outcome.rb +108 -0
  51. data/lib/smith/workflow/composite/budget_allocator.rb +78 -0
  52. data/lib/smith/workflow/composite/budget_math.rb +51 -0
  53. data/lib/smith/workflow/composite/contract.rb +100 -0
  54. data/lib/smith/workflow/composite/effects.rb +140 -0
  55. data/lib/smith/workflow/composite/effects_application.rb +36 -0
  56. data/lib/smith/workflow/composite/effects_baseline.rb +36 -0
  57. data/lib/smith/workflow/composite/effects_preflight.rb +102 -0
  58. data/lib/smith/workflow/composite/encoded_value_budget.rb +36 -0
  59. data/lib/smith/workflow/composite/enums.rb +27 -0
  60. data/lib/smith/workflow/composite/error.rb +54 -0
  61. data/lib/smith/workflow/composite/error_evidence.rb +61 -0
  62. data/lib/smith/workflow/composite/execution_contract.rb +73 -0
  63. data/lib/smith/workflow/composite/fanout_branch_contract.rb +61 -0
  64. data/lib/smith/workflow/composite/input.rb +53 -0
  65. data/lib/smith/workflow/composite/outcome_accumulator.rb +131 -0
  66. data/lib/smith/workflow/composite/outcome_set.rb +23 -0
  67. data/lib/smith/workflow/composite/payload.rb +128 -0
  68. data/lib/smith/workflow/composite/payload_digest.rb +28 -0
  69. data/lib/smith/workflow/composite/plan.rb +130 -0
  70. data/lib/smith/workflow/composite/plan_integrity.rb +52 -0
  71. data/lib/smith/workflow/composite/planner.rb +53 -0
  72. data/lib/smith/workflow/composite/preparation.rb +27 -0
  73. data/lib/smith/workflow/composite/reducer.rb +133 -0
  74. data/lib/smith/workflow/composite/reduction.rb +31 -0
  75. data/lib/smith/workflow/composite/value_budget.rb +90 -0
  76. data/lib/smith/workflow/composite_branch_execution_authorization.rb +52 -0
  77. data/lib/smith/workflow/deadline_enforcement.rb +22 -5
  78. data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
  79. data/lib/smith/workflow/definition_identity.rb +25 -0
  80. data/lib/smith/workflow/deterministic_step.rb +2 -2
  81. data/lib/smith/workflow/dsl.rb +73 -5
  82. data/lib/smith/workflow/durability.rb +1 -1
  83. data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
  84. data/lib/smith/workflow/execution.rb +28 -30
  85. data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
  86. data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
  87. data/lib/smith/workflow/fanout_execution.rb +20 -19
  88. data/lib/smith/workflow/graph/diagnostic.rb +27 -1
  89. data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
  90. data/lib/smith/workflow/graph/execution_successors.rb +65 -0
  91. data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
  92. data/lib/smith/workflow/graph/metrics.rb +1 -3
  93. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
  94. data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
  95. data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
  96. data/lib/smith/workflow/graph/reachability.rb +19 -31
  97. data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
  98. data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
  99. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
  100. data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
  101. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
  102. data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
  103. data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
  104. data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
  105. data/lib/smith/workflow/graph/transition_contract.rb +47 -0
  106. data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
  107. data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
  108. data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
  109. data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
  110. data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
  111. data/lib/smith/workflow/graph/validator.rb +1 -1
  112. data/lib/smith/workflow/graph.rb +62 -4
  113. data/lib/smith/workflow/guardrail_integration.rb +23 -9
  114. data/lib/smith/workflow/identifier.rb +24 -0
  115. data/lib/smith/workflow/message_admission.rb +40 -0
  116. data/lib/smith/workflow/message_admission_boundary.rb +22 -0
  117. data/lib/smith/workflow/message_batch.rb +60 -0
  118. data/lib/smith/workflow/message_value_normalizer.rb +134 -0
  119. data/lib/smith/workflow/nested_execution.rb +10 -0
  120. data/lib/smith/workflow/orchestrator_worker.rb +3 -7
  121. data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
  122. data/lib/smith/workflow/parallel/execution_context.rb +113 -0
  123. data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
  124. data/lib/smith/workflow/parallel/root_execution.rb +91 -0
  125. data/lib/smith/workflow/parallel.rb +19 -22
  126. data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
  127. data/lib/smith/workflow/parallel_execution.rb +46 -14
  128. data/lib/smith/workflow/persistence.rb +37 -8
  129. data/lib/smith/workflow/prepared_branch_execution.rb +31 -0
  130. data/lib/smith/workflow/prepared_step.rb +122 -0
  131. data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
  132. data/lib/smith/workflow/prepared_step_execution_authorization.rb +101 -0
  133. data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
  134. data/lib/smith/workflow/prepared_step_execution_scope.rb +111 -0
  135. data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
  136. data/lib/smith/workflow/process_local.rb +33 -0
  137. data/lib/smith/workflow/retry_execution.rb +20 -11
  138. data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
  139. data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
  140. data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
  141. data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
  142. data/lib/smith/workflow/split_step_persistence/composite_branch_authorization.rb +99 -0
  143. data/lib/smith/workflow/split_step_persistence/composite_branch_effects.rb +41 -0
  144. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +95 -0
  145. data/lib/smith/workflow/split_step_persistence/composite_branch_outcome.rb +36 -0
  146. data/lib/smith/workflow/split_step_persistence/composite_execution.rb +39 -0
  147. data/lib/smith/workflow/split_step_persistence/composite_preparation.rb +57 -0
  148. data/lib/smith/workflow/split_step_persistence/composite_reduction_execution.rb +122 -0
  149. data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
  150. data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
  151. data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
  152. data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
  153. data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
  154. data/lib/smith/workflow/split_step_persistence/execution.rb +20 -64
  155. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +109 -0
  156. data/lib/smith/workflow/split_step_persistence/execution_authorization_issuance.rb +57 -0
  157. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +95 -0
  158. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +109 -0
  159. data/lib/smith/workflow/split_step_persistence/execution_lifecycle.rb +96 -0
  160. data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
  161. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +77 -0
  162. data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
  163. data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
  164. data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
  165. data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
  166. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
  167. data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
  168. data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
  169. data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
  170. data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
  171. data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
  172. data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
  173. data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
  174. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +66 -0
  175. data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
  176. data/lib/smith/workflow/split_step_persistence.rb +50 -0
  177. data/lib/smith/workflow/step_completion.rb +31 -0
  178. data/lib/smith/workflow/step_context.rb +46 -0
  179. data/lib/smith/workflow/string_snapshot.rb +25 -0
  180. data/lib/smith/workflow/thread_context_snapshot.rb +103 -0
  181. data/lib/smith/workflow/transition.rb +137 -133
  182. data/lib/smith/workflow/transition_actionability.rb +22 -0
  183. data/lib/smith/workflow.rb +64 -18
  184. data/lib/smith.rb +44 -2
  185. data/sig/smith.rbs +1 -0
  186. metadata +155 -19
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module ExecutionAuthorization
7
+ def release_prepared_step_execution!(authorization)
8
+ authorization = validate_split_step_execution_authorization!(authorization)
9
+ Thread.handle_interrupt(Object => :never) do
10
+ @split_step_mutex.synchronize do
11
+ unless active_split_step_execution_authorization?(authorization)
12
+ raise WorkflowError, "the prepared-step execution authorization is no longer active"
13
+ end
14
+
15
+ @split_step_phase = @split_step_execution_previous_phase
16
+ clear_split_step_execution_authorization!
17
+ end
18
+ PreparedStepExecutionAuthorization
19
+ .instance_method(:close_execution!)
20
+ .bind_call(authorization)
21
+ end
22
+ self
23
+ end
24
+
25
+ private
26
+
27
+ def authorize_claimed_prepared_step_execution!(verification_token)
28
+ ExecutionVerification
29
+ .instance_method(:verify_claimed_split_step_execution!)
30
+ .bind_call(self, verification_token)
31
+ authorization = ExecutionAuthorization
32
+ .instance_method(:build_split_step_execution_authorization)
33
+ .bind_call(self)
34
+ ExecutionAuthorization
35
+ .instance_method(:activate_split_step_execution_authorization!)
36
+ .bind_call(self, authorization, verification_token)
37
+ authorization
38
+ end
39
+
40
+ def claim_framework_execution_verification!
41
+ ExecutionVerification
42
+ .instance_method(:claim_split_step_execution_verification!)
43
+ .bind_call(self)
44
+ end
45
+
46
+ def restore_framework_execution_verification!(verification_token)
47
+ return unless verification_token
48
+
49
+ active = ExecutionAuthorization
50
+ .instance_method(:active_split_step_execution_verification?)
51
+ .bind_call(self, verification_token)
52
+ return unless active
53
+
54
+ Execution.instance_method(:restore_unverified_execution!).bind_call(self, verification_token)
55
+ end
56
+
57
+ def build_split_step_execution_authorization
58
+ PreparedStepExecutionAuthorization.new(
59
+ prepared_step: @split_step_prepared_descriptor,
60
+ dispatch_claim: @split_step_dispatch_descriptor,
61
+ execution_bindings: ExecutionBindingSnapshot.capture(
62
+ @split_step_transition,
63
+ workflow_class: self.class
64
+ )
65
+ )
66
+ end
67
+
68
+ def activate_split_step_execution_authorization!(authorization, verification_token)
69
+ @split_step_mutex.synchronize do
70
+ unless active_split_step_execution_verification?(verification_token)
71
+ raise WorkflowError, "the prepared execution verification is no longer active"
72
+ end
73
+
74
+ @split_step_execution_authorization = authorization
75
+ clear_split_step_execution_verification!
76
+ @split_step_phase = :execution_authorized
77
+ end
78
+ end
79
+
80
+ def validate_split_step_execution_authorization!(authorization)
81
+ return authorization if authorization.is_a?(PreparedStepExecutionAuthorization)
82
+
83
+ raise ArgumentError,
84
+ "authorization must be a Smith::Workflow::PreparedStepExecutionAuthorization"
85
+ end
86
+
87
+ def active_split_step_execution_authorization?(authorization)
88
+ @split_step_phase == :execution_authorized &&
89
+ @split_step_execution_authorization.equal?(authorization) &&
90
+ authorization.issued_in_current_process?
91
+ end
92
+
93
+ def active_split_step_execution_verification?(verification_token)
94
+ @split_step_phase == :verifying_execution &&
95
+ @split_step_execution_verification_token.equal?(verification_token)
96
+ end
97
+
98
+ def clear_split_step_execution_verification!
99
+ @split_step_execution_verification_token = nil
100
+ end
101
+
102
+ def clear_split_step_execution_authorization!
103
+ @split_step_execution_authorization = nil
104
+ @split_step_execution_previous_phase = nil
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module ExecutionAuthorizationIssuance
7
+ def authorize_prepared_step_execution!
8
+ authorization = nil
9
+ handed_off = false
10
+ authorization = ExecutionAuthorizationIssuance
11
+ .instance_method(:interrupt_safe_prepared_step_authorization)
12
+ .bind_call(self)
13
+ handed_off = true
14
+ authorization
15
+ ensure
16
+ unless handed_off
17
+ ExecutionLifecycle
18
+ .instance_method(:release_failed_execution_authorization!)
19
+ .bind_call(self, authorization)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def with_prepared_step_execution_authorization
26
+ Thread.handle_interrupt(Object => :never) do
27
+ authorization = interrupt_safe_prepared_step_authorization
28
+ begin
29
+ Thread.handle_interrupt(Object => :immediate) { yield(authorization) }
30
+ ensure
31
+ ExecutionLifecycle
32
+ .instance_method(:release_failed_execution_authorization!)
33
+ .bind_call(self, authorization)
34
+ end
35
+ end
36
+ end
37
+
38
+ def interrupt_safe_prepared_step_authorization
39
+ Thread.handle_interrupt(Object => :never) do
40
+ verification_token = ExecutionAuthorization
41
+ .instance_method(:claim_framework_execution_verification!)
42
+ .bind_call(self)
43
+ begin
44
+ ExecutionAuthorization
45
+ .instance_method(:authorize_claimed_prepared_step_execution!)
46
+ .bind_call(self, verification_token)
47
+ ensure
48
+ ExecutionAuthorization
49
+ .instance_method(:restore_framework_execution_verification!)
50
+ .bind_call(self, verification_token)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../agent"
4
+ require_relative "../../agent/registry"
5
+
6
+ module Smith
7
+ class Workflow
8
+ module SplitStepPersistence
9
+ class ExecutionBindingCollector
10
+ MAX_BINDINGS = 10_000
11
+
12
+ def initialize
13
+ @requests = {}
14
+ @bindings = {}
15
+ end
16
+
17
+ def capture(transition, workflow_class:)
18
+ capture_binding(transition.agent_name, workflow_class:, transition:, role: :agent)
19
+ capture_fanout(transition, workflow_class:)
20
+ capture_optimization(transition, workflow_class:)
21
+ capture_orchestration(transition, workflow_class:)
22
+ end
23
+
24
+ def fetch!(name, workflow_class:, transition_name:, role:)
25
+ @bindings.fetch(name.to_s) do
26
+ raise WorkflowError,
27
+ "execution authorization does not contain #{role} :#{name} " \
28
+ "for workflow #{workflow_class}, transition :#{transition_name}"
29
+ end
30
+ end
31
+
32
+ def capture_agent(name, workflow_class:, transition:, role:)
33
+ capture_binding(name, workflow_class:, transition:, role:)
34
+ self
35
+ end
36
+
37
+ def resolve!
38
+ @bindings = Agent::Registry.capture_bindings!(@requests.values)
39
+
40
+ @requests.freeze
41
+ self
42
+ end
43
+
44
+ def each(&)
45
+ @bindings.each(&)
46
+ end
47
+
48
+ def freeze
49
+ @requests.freeze
50
+ @bindings.freeze
51
+ super
52
+ end
53
+
54
+ private
55
+
56
+ def capture_fanout(transition, workflow_class:)
57
+ transition.fanout_config&.fetch(:branches, nil)&.each_value do |agent|
58
+ capture_binding(agent, workflow_class:, transition:, role: :fanout_agent)
59
+ end
60
+ end
61
+
62
+ def capture_optimization(transition, workflow_class:)
63
+ return unless (config = transition.optimization_config)
64
+
65
+ capture_binding(config.fetch(:generator), workflow_class:, transition:, role: :generator)
66
+ capture_binding(config.fetch(:evaluator), workflow_class:, transition:, role: :evaluator)
67
+ end
68
+
69
+ def capture_orchestration(transition, workflow_class:)
70
+ return unless (config = transition.orchestrator_config)
71
+
72
+ capture_binding(config.fetch(:orchestrator), workflow_class:, transition:, role: :orchestrator)
73
+ capture_binding(config.fetch(:worker), workflow_class:, transition:, role: :worker)
74
+ end
75
+
76
+ def capture_binding(name, workflow_class:, transition:, role:)
77
+ return unless name
78
+
79
+ key = name.to_s
80
+ return if @requests.key?(key)
81
+ if @requests.length >= MAX_BINDINGS
82
+ raise WorkflowError, "execution authorization exceeds maximum agent bindings #{MAX_BINDINGS}"
83
+ end
84
+
85
+ @requests[key] = {
86
+ name:,
87
+ workflow_class:,
88
+ transition_name: transition.name,
89
+ role:
90
+ }.freeze
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "execution_binding_collector"
4
+ require_relative "execution_workflow_snapshot"
5
+
6
+ module Smith
7
+ class Workflow
8
+ module SplitStepPersistence
9
+ class ExecutionBindingSnapshot
10
+ MAX_NESTED_WORKFLOWS = 128
11
+ MAX_TRANSITIONS = 10_000
12
+
13
+ def self.capture(transition, workflow_class:)
14
+ new(transition, workflow_class:).capture
15
+ end
16
+
17
+ def self.capture_agent(transition, workflow_class:, name:, role:)
18
+ new(transition, workflow_class:, selected_agent: { name:, role: }).capture
19
+ end
20
+
21
+ def initialize(transition, workflow_class:, selected_agent: nil)
22
+ @transition = transition
23
+ @workflow_class = workflow_class
24
+ @selected_agent = selected_agent
25
+ @bindings = ExecutionBindingCollector.new
26
+ @visited_workflows = {}.compare_by_identity
27
+ @visited_workflows[workflow_class] = true
28
+ @workflow_snapshots = {}.compare_by_identity
29
+ @workflow_queue = []
30
+ @workflow_queue_index = 0
31
+ @transition_count = 0
32
+ end
33
+
34
+ def capture
35
+ if @selected_agent
36
+ @bindings.capture_agent(
37
+ @selected_agent.fetch(:name),
38
+ workflow_class: @workflow_class,
39
+ transition: @transition,
40
+ role: @selected_agent.fetch(:role)
41
+ )
42
+ else
43
+ capture_transition(@transition, workflow_class: @workflow_class)
44
+ drain_workflow_queue
45
+ end
46
+ @bindings.resolve!
47
+ @bindings.freeze
48
+ @workflow_snapshots.freeze
49
+ freeze
50
+ end
51
+
52
+ def fetch!(name, workflow_class:, transition_name:, role:)
53
+ @bindings.fetch!(name, workflow_class:, transition_name:, role:)
54
+ end
55
+
56
+ def each_agent_binding(&)
57
+ @bindings.each(&)
58
+ end
59
+
60
+ def verify_workflow!(workflow_class)
61
+ snapshot = @workflow_snapshots.fetch(workflow_class) do
62
+ raise WorkflowError, "execution authorization does not contain nested workflow #{workflow_class}"
63
+ end
64
+ snapshot.verify!
65
+ end
66
+
67
+ private
68
+
69
+ def capture_transition(transition, workflow_class:)
70
+ return unless transition
71
+
72
+ visit_transition!
73
+ @bindings.capture(transition, workflow_class:)
74
+ enqueue_nested_workflow(transition.workflow_class)
75
+ end
76
+
77
+ def enqueue_nested_workflow(workflow_class)
78
+ return unless workflow_class
79
+ return if @visited_workflows.key?(workflow_class)
80
+
81
+ @visited_workflows[workflow_class] = true
82
+ if @visited_workflows.length > MAX_NESTED_WORKFLOWS
83
+ raise WorkflowError,
84
+ "execution authorization exceeds maximum nested workflows #{MAX_NESTED_WORKFLOWS}"
85
+ end
86
+
87
+ @workflow_queue << workflow_class
88
+ end
89
+
90
+ def drain_workflow_queue
91
+ while @workflow_queue_index < @workflow_queue.length
92
+ workflow_class = @workflow_queue.fetch(@workflow_queue_index)
93
+ @workflow_queue_index += 1
94
+ @workflow_snapshots[workflow_class] = ExecutionWorkflowSnapshot.capture(workflow_class) do |transition|
95
+ capture_transition(transition, workflow_class:)
96
+ end
97
+ end
98
+ end
99
+
100
+ def visit_transition!
101
+ @transition_count += 1
102
+ return if @transition_count <= MAX_TRANSITIONS
103
+
104
+ raise WorkflowError, "execution authorization exceeds maximum transitions #{MAX_TRANSITIONS}"
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module ExecutionLifecycle
7
+ private
8
+
9
+ def perform_authorized_prepared_step_execution!(authorization)
10
+ authorization = validate_split_step_execution_authorization!(authorization)
11
+ execution_thread = Thread.current
12
+ execute_prepared_step_lifecycle!(authorization, execution_thread) { yield(execution_thread) }
13
+ end
14
+
15
+ def execute_prepared_step_lifecycle!(authorization, execution_thread)
16
+ execution_active = false
17
+ step = nil
18
+ result = nil
19
+ Thread.handle_interrupt(Object => :never) do
20
+ activate_split_step_execution!(authorization, execution_thread)
21
+ execution_active = true
22
+ Thread.handle_interrupt(Object => :immediate) do
23
+ step, result = yield(execution_thread)
24
+ end
25
+ ensure
26
+ finish_split_step_execution!(step, authorization, execution_thread) if execution_active
27
+ end
28
+ result
29
+ end
30
+
31
+ def activate_split_step_execution!(authorization, execution_thread)
32
+ Thread.handle_interrupt(Object => :never) do
33
+ @split_step_mutex.synchronize do
34
+ ensure_active_split_step_execution_authorization!(authorization)
35
+ ensure_split_step_definition_current!
36
+ ensure_prepared_split_step_transition_matches!
37
+ activate_prepared_step_execution_scope!(authorization)
38
+ publish_active_split_step_execution!(authorization, execution_thread)
39
+ end
40
+ end
41
+ end
42
+
43
+ def finish_split_step_execution!(step, authorization, execution_thread)
44
+ Thread.handle_interrupt(Object => :never) do
45
+ @split_step_mutex.synchronize do
46
+ reset_active_split_step_execution!(step) if active_split_step_execution?(execution_thread)
47
+ end
48
+ ensure
49
+ PreparedStepExecutionAuthorization.instance_method(:close_execution!).bind_call(authorization)
50
+ end
51
+ end
52
+
53
+ def ensure_active_split_step_execution_authorization!(authorization)
54
+ return if active_split_step_execution_authorization?(authorization)
55
+
56
+ raise WorkflowError, "the prepared-step execution authorization is no longer active"
57
+ end
58
+
59
+ def activate_prepared_step_execution_scope!(authorization)
60
+ PreparedStepExecutionAuthorization.instance_method(:activate_execution!).bind_call(authorization)
61
+ end
62
+
63
+ def publish_active_split_step_execution!(authorization, execution_thread)
64
+ @split_step_active_execution_authorization = authorization
65
+ @split_step_execution_result = nil
66
+ @split_step_phase = :executing
67
+ clear_split_step_execution_authorization!
68
+ @split_step_execution_thread = execution_thread
69
+ @split_step_advance_permit = true
70
+ end
71
+
72
+ def active_split_step_execution?(execution_thread)
73
+ @split_step_phase == :executing && @split_step_execution_thread.equal?(execution_thread)
74
+ end
75
+
76
+ def reset_active_split_step_execution!(step)
77
+ @split_step_execution_thread = nil
78
+ @split_step_advance_permit = false
79
+ @split_step_execution_previous_phase = nil
80
+ @split_step_active_execution_authorization = nil
81
+ @split_step_execution_result = nil
82
+ @split_step_phase = step ? :executed : :attempted
83
+ end
84
+
85
+ def release_failed_execution_authorization!(authorization)
86
+ return unless authorization
87
+ return unless active_split_step_execution_authorization?(authorization)
88
+
89
+ ExecutionAuthorization
90
+ .instance_method(:release_prepared_step_execution!)
91
+ .bind_call(self, authorization)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module ExecutionResultCapture
7
+ private
8
+
9
+ def capture_split_step_execution_result!(step)
10
+ result = prepare_split_step_execution_result(step)
11
+ commit_split_step_execution_result!(result)
12
+ end
13
+
14
+ def prepare_split_step_execution_result(step)
15
+ return unless active_split_step_execution_thread?
16
+
17
+ PreparedStepExecutionResult.from_step(step)
18
+ end
19
+
20
+ def commit_split_step_execution_result!(result)
21
+ return unless result
22
+
23
+ @split_step_mutex.synchronize do
24
+ return unless active_split_step_execution_thread?
25
+ raise WorkflowError, "prepared-step execution produced more than one result" if @split_step_execution_result
26
+
27
+ @split_step_execution_result = result
28
+ end
29
+ end
30
+
31
+ def consume_split_step_execution_result!(execution_thread)
32
+ @split_step_mutex.synchronize do
33
+ unless @split_step_phase == :executing && @split_step_execution_thread.equal?(execution_thread)
34
+ raise WorkflowError, "prepared-step execution is no longer active"
35
+ end
36
+
37
+ @split_step_execution_result ||
38
+ raise(WorkflowError, "prepared-step execution did not produce a result")
39
+ end
40
+ end
41
+
42
+ def active_split_step_execution_thread?
43
+ @split_step_phase == :executing && @split_step_execution_thread.equal?(Thread.current)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module ExecutionVerification
7
+ private
8
+
9
+ def claim_split_step_execution_verification!
10
+ @split_step_mutex.synchronize do
11
+ DefinitionBoundary
12
+ .instance_method(:ensure_split_step_definition_current!)
13
+ .bind_call(self)
14
+ restart_safe = DefinitionBoundary
15
+ .instance_method(:restart_safe_split_step?)
16
+ .bind_call(self)
17
+ expected = restart_safe ? :dispatch_claimed : :prepared
18
+ raise WorkflowError, "no persisted step is prepared for execution" unless @split_step_phase == expected
19
+
20
+ ExecutionVerification
21
+ .instance_method(:ensure_prepared_split_step_transition_matches!)
22
+ .bind_call(self)
23
+
24
+ token = Object.new.freeze
25
+ @split_step_execution_previous_phase = expected
26
+ @split_step_execution_verification_token = token
27
+ @split_step_phase = :verifying_execution
28
+ token
29
+ end
30
+ end
31
+
32
+ def verify_claimed_split_step_execution!(verification_token)
33
+ restart_safe = DefinitionBoundary
34
+ .instance_method(:restart_safe_split_step?)
35
+ .bind_call(self)
36
+ if restart_safe
37
+ DispatchVerification
38
+ .instance_method(:verify_split_step_dispatch_available!)
39
+ .bind_call(self, verification_token)
40
+ else
41
+ verify_split_step_preparation_available!
42
+ end
43
+ end
44
+
45
+ def prepared_split_step_transition_matches?
46
+ transition = pending_split_step_transition
47
+ transition_name = @next_transition_name || transition&.name
48
+ transition_name == @split_step_transition_name &&
49
+ transition.equal?(@split_step_transition) &&
50
+ split_step_transition_signature(transition) == @split_step_transition_signature
51
+ end
52
+
53
+ def ensure_prepared_split_step_transition_matches!
54
+ return if prepared_split_step_transition_matches?
55
+
56
+ raise WorkflowError, "the prepared transition no longer matches the workflow"
57
+ end
58
+
59
+ def verify_split_step_preparation_available!
60
+ payload = @split_step_adapter.fetch(@split_step_persistence_key)
61
+ durable = persisted_split_step_payload?(payload, @split_step_preparation_payload)
62
+ live = persisted_split_step_payload?(current_split_step_preparation_payload, @split_step_preparation_payload)
63
+ stable = split_step_transition_signature(@split_step_transition) == @split_step_transition_signature
64
+ return if durable && live && stable
65
+
66
+ raise WorkflowError, "the persisted split-step preparation is no longer available"
67
+ end
68
+
69
+ def current_split_step_preparation_payload
70
+ split_step_preparation_payload(
71
+ JSON.generate(to_state.merge(persistence_version: @persistence_version))
72
+ )
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "../../errors"
6
+ require_relative "../graph"
7
+ require_relative "transition_contract"
8
+
9
+ module Smith
10
+ class Workflow
11
+ module SplitStepPersistence
12
+ class ExecutionWorkflowSnapshot
13
+ extend Dry::Initializer
14
+
15
+ param :workflow_class
16
+
17
+ def self.capture(workflow_class, &)
18
+ new(workflow_class).capture(&)
19
+ end
20
+
21
+ def capture
22
+ @initial_state_signature = initial_state_signature
23
+ @transitions = reachable_definition_transitions.map do |transition|
24
+ yield transition if block_given?
25
+ [transition.name, transition, TransitionContract.capture(transition)].freeze
26
+ end.freeze
27
+ freeze
28
+ end
29
+
30
+ def verify!
31
+ unless initial_state_signature == @initial_state_signature
32
+ raise WorkflowError, "authorized nested workflow definition changed before execution"
33
+ end
34
+
35
+ index = 0
36
+ reachable_definition_transitions.each do |transition|
37
+ expected = @transitions[index]
38
+ verify_transition!(transition, expected)
39
+ index += 1
40
+ end
41
+ return true if index == @transitions.length
42
+
43
+ raise WorkflowError, "authorized nested workflow definition changed before execution"
44
+ end
45
+
46
+ private
47
+
48
+ def reachable_definition_transitions
49
+ workflow_class.graph.reachable_transitions.map do |transition|
50
+ workflow_class.transition_at(transition.definition_index)
51
+ end
52
+ rescue IndexError
53
+ raise WorkflowError, "authorized nested workflow definition changed before execution"
54
+ end
55
+
56
+ def initial_state_signature
57
+ TransitionContractSignature.new(
58
+ value: workflow_class.initial_state,
59
+ max_depth: TransitionContract::MAX_DEPTH,
60
+ max_nodes: TransitionContract::MAX_NODES,
61
+ max_bytes: TransitionContract::MAX_BYTES
62
+ ).call
63
+ end
64
+
65
+ def verify_transition!(transition, expected)
66
+ matches = expected &&
67
+ transition.name == expected.fetch(0) &&
68
+ transition.equal?(expected.fetch(1)) &&
69
+ TransitionContract.signature(transition) == expected.fetch(2)
70
+ return if matches
71
+
72
+ raise WorkflowError, "authorized nested workflow definition changed before execution"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -11,7 +11,9 @@ module Smith
11
11
 
12
12
  def prepend(*features)
13
13
  result = super
14
- Module.instance_method(:prepend).bind_call(self, SubclassBoundary.guard)
14
+ guard = SubclassBoundary.guard
15
+ guard.freeze
16
+ Module.instance_method(:prepend).bind_call(self, guard)
15
17
  result
16
18
  end
17
19
  end