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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../composite/branch_outcome"
4
+ require_relative "../composite/error_evidence"
5
+
6
+ module Smith
7
+ class Workflow
8
+ module SplitStepPersistence
9
+ module CompositeBranchOutcome
10
+ private
11
+
12
+ def composite_branch_outcome(execution, output, error, effects)
13
+ return failed_composite_branch_outcome(execution, error, effects) if error
14
+
15
+ Composite::BranchOutcome.succeeded(
16
+ plan_digest: execution.plan_digest,
17
+ branch: execution.branch,
18
+ output: output.fetch(:output),
19
+ effects:
20
+ )
21
+ rescue WorkflowError, ArgumentError => e
22
+ failed_composite_branch_outcome(execution, e, effects)
23
+ end
24
+
25
+ def failed_composite_branch_outcome(execution, error, effects)
26
+ Composite::BranchOutcome.failed(
27
+ plan_digest: execution.plan_digest,
28
+ branch: execution.branch,
29
+ error: Composite::ErrorEvidence.call(error),
30
+ effects:
31
+ )
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module CompositeExecution
7
+ def prepare_composite_step!
8
+ ExecutionAuthorizationIssuance
9
+ .instance_method(:with_prepared_step_execution_authorization)
10
+ .bind_call(self) do |authorization|
11
+ CompositePreparation
12
+ .instance_method(:prepare_authorized_composite_step!)
13
+ .bind_call(self, authorization)
14
+ end
15
+ end
16
+
17
+ def execute_prepared_composite_branch!(execution:, input:)
18
+ CompositeBranchAuthorization
19
+ .instance_method(:with_composite_branch_authorization)
20
+ .bind_call(self, execution, input) do |authorization|
21
+ CompositeBranchExecution
22
+ .instance_method(:execute_authorized_composite_branch!)
23
+ .bind_call(self, authorization, execution:, input:)
24
+ end
25
+ end
26
+
27
+ def reduce_prepared_composite_step!(plan:, input:, outcomes:, primary_failure: nil)
28
+ ExecutionAuthorizationIssuance
29
+ .instance_method(:with_prepared_step_execution_authorization)
30
+ .bind_call(self) do |authorization|
31
+ CompositeReductionExecution
32
+ .instance_method(:reduce_authorized_composite_step!)
33
+ .bind_call(self, authorization, plan:, input:, outcomes:, primary_failure:)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../composite/contract"
4
+ require_relative "../composite/input"
5
+ require_relative "../composite/planner"
6
+ require_relative "../composite/preparation"
7
+
8
+ module Smith
9
+ class Workflow
10
+ module SplitStepPersistence
11
+ module CompositePreparation
12
+ include Composite::Contract
13
+
14
+ private
15
+
16
+ def prepare_authorized_composite_step!(authorization)
17
+ authorization = validate_composite_authorization!(authorization)
18
+ transition = @split_step_transition
19
+ specs = composite_branch_specs(authorization, transition)
20
+ input = prepare_composite_input(authorization, transition, specs)
21
+ plan = Composite::Planner.new(
22
+ dispatch: authorization.dispatch_claim,
23
+ kind: composite_kind(transition),
24
+ transition: transition.name,
25
+ from: transition.from,
26
+ execution_namespace: execution_namespace,
27
+ branch_specs: specs,
28
+ input_digest: input.digest,
29
+ budget_state_digest: composite_budget_state_digest
30
+ ).call
31
+ Composite::Preparation.new(plan:, input:)
32
+ end
33
+
34
+ def prepare_composite_input(authorization, transition, specs)
35
+ ThreadContextSnapshot.new.around do
36
+ run_composite_input_guardrails(authorization, transition, specs)
37
+ agent_messages = build_session&.prepare!
38
+ Composite::Input.build(agent_messages:, session_messages: snapshot_session_messages)
39
+ end
40
+ end
41
+
42
+ def run_composite_input_guardrails(authorization, transition, specs)
43
+ if transition.parallel?
44
+ agent = specs.first.fetch(:agent)
45
+ run_input_guardrails(captured_agent(authorization, transition, agent, :agent))
46
+ else
47
+ run_workflow_input_guardrails
48
+ specs.each do |spec|
49
+ agent = spec.fetch(:agent)
50
+ run_agent_input_guardrails(captured_agent(authorization, transition, agent, :fanout_agent))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../composite/branch_failure"
4
+ require_relative "../composite/contract"
5
+ require_relative "../composite/effects_baseline"
6
+ require_relative "../composite/effects_preflight"
7
+ require_relative "../composite/reducer"
8
+
9
+ module Smith
10
+ class Workflow
11
+ module SplitStepPersistence
12
+ module CompositeReductionExecution
13
+ include Composite::Contract
14
+
15
+ private
16
+
17
+ def reduce_authorized_composite_step!(authorization, plan:, input:, outcomes:, primary_failure: nil)
18
+ authorization = validate_composite_authorization!(authorization)
19
+ validate_composite_reduction_plan!(authorization, plan, input)
20
+ reduction = Composite::Reducer.new(plan:, outcomes:, primary_failure:).call
21
+ effects_application = prepare_composite_effects(reduction.effects)
22
+ @composite_plan = plan
23
+ @composite_input = input
24
+ @composite_reduction = reduction
25
+ @composite_effects_application = effects_application
26
+ @execution_namespace = plan.execution_namespace
27
+ Execution.instance_method(:execute_authorized_prepared_step!).bind_call(self, authorization)
28
+ ensure
29
+ @composite_plan = nil
30
+ @composite_input = nil
31
+ @composite_reduction = nil
32
+ @composite_effects_application = nil
33
+ end
34
+
35
+ def apply_composite_reduction!(transition)
36
+ reduction = @composite_reduction
37
+ apply_composite_effects!(@composite_effects_application)
38
+ restore_composite_session!
39
+ if reduction.failed?
40
+ raise Composite::BranchFailure.new(
41
+ branch_key: reduction.failed_branch_key,
42
+ error: reduction.error
43
+ )
44
+ end
45
+
46
+ validate_composite_output!(transition, reduction.output)
47
+ reduction.output
48
+ end
49
+
50
+ def restore_composite_session!
51
+ @session_messages = snapshot_value(@composite_input.session_messages)
52
+ @last_prepared_input = snapshot_value(@composite_input.agent_messages)
53
+ end
54
+
55
+ def prepare_composite_effects(effects)
56
+ Composite::EffectsPreflight.new(
57
+ effects:,
58
+ baseline: composite_effect_baseline,
59
+ snapshotter: method(:snapshot_value)
60
+ ).call
61
+ end
62
+
63
+ def apply_composite_effects!(application)
64
+ Thread.handle_interrupt(Object => :never) do
65
+ @usage_mutex.synchronize do
66
+ @tool_results_mutex.synchronize do
67
+ validate_composite_effect_baseline!(application.baseline)
68
+ @usage_entries = application.usage_entries.dup
69
+ @total_tokens = application.total_tokens
70
+ @total_cost = application.total_cost
71
+ @tool_results = application.tool_results.dup
72
+ @ledger = application.ledger
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ def composite_effect_baseline
79
+ @usage_mutex.synchronize do
80
+ @tool_results_mutex.synchronize do
81
+ build_composite_effect_baseline
82
+ end
83
+ end
84
+ end
85
+
86
+ def build_composite_effect_baseline
87
+ ledger = @ledger
88
+ Composite::EffectsBaseline.new(
89
+ usage_entries: @usage_entries.dup,
90
+ tool_results: snapshot_value(@tool_results),
91
+ total_tokens: @total_tokens,
92
+ total_cost: @total_cost,
93
+ ledger:,
94
+ budget_consumed: ledger ? ledger.consumed : {}
95
+ )
96
+ end
97
+
98
+ def validate_composite_effect_baseline!(expected)
99
+ current = build_composite_effect_baseline
100
+ return if current == expected
101
+
102
+ raise WorkflowError, "composite effect baseline changed before application"
103
+ end
104
+
105
+ def validate_composite_output!(transition, output)
106
+ if @composite_plan.kind == :parallel
107
+ agent = captured_agent(
108
+ @split_step_active_execution_authorization,
109
+ transition,
110
+ @composite_plan.branches.first.agent,
111
+ :agent
112
+ )
113
+ validate_data_volume!(output, agent)
114
+ run_output_guardrails(output, agent)
115
+ else
116
+ run_workflow_output_guardrails(output)
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module DefinitionBoundary
7
+ private
8
+
9
+ def restart_safe_split_step? = !effective_definition_digest.nil?
10
+
11
+ def ensure_split_step_definition_current!
12
+ return if effective_definition_digest == self.class.definition_digest
13
+
14
+ raise WorkflowError, "the prepared workflow definition has changed"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module DispatchBoundary
7
+ private
8
+
9
+ def dispatch_store!(...)
10
+ @split_step_dispatch_started = true if @split_step_phase == :preparing
11
+ super
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module DispatchClaim
7
+ def claim_prepared_step_dispatch!
8
+ ensure_restart_safe_dispatch_claim!
9
+ ensure_split_step_definition_current!
10
+ transaction_identity = TransactionIdentity.capture(@split_step_adapter)
11
+ claim_dispatch_intent!
12
+ payload, descriptor = build_split_step_dispatch_claim
13
+ dispatch_started = true
14
+ replace_exact_dispatch!(payload)
15
+ accept_split_step_dispatch!(payload, descriptor, transaction_identity)
16
+ descriptor
17
+ rescue PersistencePayloadConflict
18
+ reject_split_step_dispatch!
19
+ raise
20
+ rescue StandardError
21
+ if dispatch_started
22
+ mark_split_step_dispatch_unknown!(payload, descriptor)
23
+ else
24
+ restore_unclaimed_dispatch!
25
+ end
26
+ raise
27
+ end
28
+
29
+ private
30
+
31
+ def ensure_restart_safe_dispatch_claim!
32
+ return if restart_safe_split_step?
33
+
34
+ raise WorkflowError, "restart-safe dispatch claiming requires a definition_digest"
35
+ end
36
+
37
+ def split_step_dispatch_payload(token)
38
+ document = JSON.parse(@split_step_preparation_payload)
39
+ document["split_step_phase"] = "dispatching"
40
+ document["split_step_dispatch_token"] = token
41
+ JSON.generate(document)
42
+ end
43
+
44
+ def build_split_step_dispatch_claim
45
+ token = SecureRandom.uuid.freeze
46
+ payload = split_step_dispatch_payload(token)
47
+ [payload, build_split_step_dispatch(token, payload)]
48
+ end
49
+
50
+ def replace_exact_dispatch!(payload)
51
+ @split_step_adapter.replace_exact(
52
+ @split_step_persistence_key,
53
+ payload,
54
+ expected_payload: @split_step_preparation_payload,
55
+ ttl: nil
56
+ )
57
+ end
58
+
59
+ def build_split_step_dispatch(token, payload)
60
+ PreparedStepDispatch.new(
61
+ prepared_step: @split_step_prepared_descriptor,
62
+ token:,
63
+ dispatch_digest: CanonicalPayloadDigest.call(payload)
64
+ )
65
+ end
66
+
67
+ def accept_split_step_dispatch!(payload, descriptor, transaction_identity)
68
+ @split_step_mutex.synchronize do
69
+ unless active_split_step_dispatch_claim?
70
+ raise WorkflowError, "the prepared dispatch claim is no longer active"
71
+ end
72
+
73
+ @split_step_pre_dispatch_payload = @split_step_preparation_payload
74
+ @split_step_preparation_payload = payload.freeze
75
+ @split_step_dispatch_token = descriptor.token
76
+ @split_step_dispatch_descriptor = descriptor
77
+ @split_step_dispatch_thread = nil
78
+ @split_step_phase = transaction_identity ? :dispatch_claimed_uncommitted : :dispatch_claimed
79
+ end
80
+ end
81
+
82
+ def reject_split_step_dispatch!
83
+ @split_step_mutex.synchronize do
84
+ next unless active_split_step_dispatch_claim?
85
+
86
+ @split_step_dispatch_thread = nil
87
+ @split_step_phase = :dispatch_rejected
88
+ end
89
+ end
90
+
91
+ def restore_unclaimed_dispatch!
92
+ @split_step_mutex.synchronize do
93
+ next unless active_split_step_dispatch_claim?
94
+
95
+ @split_step_dispatch_thread = nil
96
+ @split_step_phase = :prepared
97
+ end
98
+ end
99
+
100
+ def mark_split_step_dispatch_unknown!(payload, descriptor)
101
+ @split_step_mutex.synchronize do
102
+ next unless active_split_step_dispatch_claim?
103
+
104
+ @split_step_attempted_dispatch_payload = payload&.freeze
105
+ @split_step_dispatch_token = descriptor&.token
106
+ @split_step_attempted_dispatch_descriptor = descriptor
107
+ @split_step_dispatch_thread = nil
108
+ @split_step_phase = :dispatch_unknown
109
+ end
110
+ end
111
+
112
+ def active_split_step_dispatch_claim?
113
+ @split_step_phase == :claiming_dispatch && @split_step_dispatch_thread.equal?(Thread.current)
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module DispatchConfirmation
7
+ def confirm_prepared_step_dispatch!
8
+ confirmation_claimed = false
9
+ rolled_back = false
10
+ claim_dispatch_confirmation!
11
+ confirmation_claimed = true
12
+ payload = @split_step_adapter.fetch(@split_step_persistence_key)
13
+ return confirm_committed_dispatch! if persisted_split_step_payload?(payload, @split_step_preparation_payload)
14
+
15
+ rolled_back = recover_rolled_back_dispatch!(payload)
16
+ raise WorkflowError, "the persisted split-step dispatch claim is not committed"
17
+ rescue StandardError
18
+ mark_unconfirmed_dispatch_unknown! if confirmation_claimed && !rolled_back
19
+ raise
20
+ end
21
+
22
+ private
23
+
24
+ def claim_dispatch_confirmation!
25
+ @split_step_mutex.synchronize do
26
+ unless @split_step_phase == :dispatch_claimed_uncommitted
27
+ raise WorkflowError, "no uncommitted split-step dispatch claim is awaiting confirmation"
28
+ end
29
+ if @split_step_adapter.transaction_open?
30
+ raise WorkflowError, "the split-step dispatch transaction is still open"
31
+ end
32
+
33
+ @split_step_phase = :confirming_dispatch
34
+ end
35
+ end
36
+
37
+ def confirm_committed_dispatch!
38
+ @split_step_mutex.synchronize do
39
+ @split_step_phase = :dispatch_claimed
40
+ clear_split_step_dispatch_transaction!
41
+ end
42
+ self
43
+ end
44
+
45
+ def recover_rolled_back_dispatch!(payload)
46
+ return false unless persisted_split_step_payload?(payload, @split_step_pre_dispatch_payload)
47
+
48
+ @split_step_mutex.synchronize do
49
+ return false unless @split_step_phase == :confirming_dispatch
50
+
51
+ @split_step_preparation_payload = @split_step_pre_dispatch_payload
52
+ @split_step_phase = :prepared
53
+ clear_split_step_dispatch_transaction!
54
+ true
55
+ end
56
+ end
57
+
58
+ def mark_unconfirmed_dispatch_unknown!
59
+ @split_step_mutex.synchronize do
60
+ @split_step_phase = :dispatch_unknown if @split_step_phase == :confirming_dispatch
61
+ end
62
+ end
63
+
64
+ def clear_split_step_dispatch_transaction!
65
+ @split_step_pre_dispatch_payload = nil
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module DispatchVerification
7
+ private
8
+
9
+ def claim_dispatch_intent!
10
+ @split_step_mutex.synchronize do
11
+ raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
12
+
13
+ ensure_prepared_split_step_transition_matches!
14
+ @split_step_phase = :claiming_dispatch
15
+ @split_step_dispatch_thread = Thread.current
16
+ end
17
+ end
18
+
19
+ def verify_split_step_dispatch_available!(verification_token)
20
+ payload = @split_step_adapter.fetch(@split_step_persistence_key)
21
+ return if persisted_split_step_payload?(payload, @split_step_preparation_payload)
22
+
23
+ @split_step_mutex.synchronize do
24
+ if active_split_step_execution_verification?(verification_token)
25
+ @split_step_phase = :dispatch_unknown
26
+ @split_step_execution_previous_phase = nil
27
+ clear_split_step_execution_verification!
28
+ end
29
+ end
30
+ raise PersistencePayloadConflict.new(key: @split_step_persistence_key)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -5,74 +5,38 @@ module Smith
5
5
  module SplitStepPersistence
6
6
  module Execution
7
7
  def execute_prepared_step!
8
- execution_started = false
9
- step = nil
10
- claim_split_step_execution_verification!
11
- begin
12
- verify_split_step_preparation_available!
13
- activate_split_step_execution!
14
- execution_started = true
15
- step = execute_claimed_split_step_transition!
16
- ensure
17
- execution_started ? finish_split_step_execution!(step) : restore_unverified_execution!
18
- end
19
- step
20
- end
21
-
22
- def prepared_persisted_step? = @split_step_mutex.synchronize { @split_step_phase == :prepared }
23
-
24
- private
25
-
26
- def claim_split_step_execution_verification!
27
- @split_step_mutex.synchronize do
28
- raise WorkflowError, "no persisted step is prepared" unless @split_step_phase == :prepared
29
- raise WorkflowError, "the prepared transition no longer matches the workflow" unless
30
- prepared_split_step_transition_matches?
31
-
32
- @split_step_phase = :verifying_execution
8
+ result = ExecutionAuthorizationIssuance
9
+ .instance_method(:with_prepared_step_execution_authorization)
10
+ .bind_call(self) do |authorization|
11
+ Execution.instance_method(:execute_authorized_prepared_step!).bind_call(self, authorization)
33
12
  end
13
+ result.step_snapshot
34
14
  end
35
15
 
36
- def prepared_split_step_transition_matches?
37
- transition = pending_split_step_transition
38
- transition_name = @next_transition_name || transition&.name
39
- transition_name == @split_step_transition_name &&
40
- transition.equal?(@split_step_transition) &&
41
- split_step_transition_signature(transition) == @split_step_transition_signature
42
- end
43
-
44
- def activate_split_step_execution!
45
- @split_step_mutex.synchronize do
46
- unless @split_step_phase == :verifying_execution
47
- raise WorkflowError, "the prepared execution claim is no longer active"
48
- end
49
-
50
- @split_step_phase = :executing
51
- @split_step_execution_thread = Thread.current
52
- @split_step_advance_permit = true
16
+ def execute_authorized_prepared_step!(authorization)
17
+ perform_authorized_prepared_step_execution!(authorization) do |execution_thread|
18
+ step = execute_claimed_split_step_transition!
19
+ [step, consume_split_step_execution_result!(execution_thread)]
53
20
  end
54
21
  end
55
22
 
56
- def restore_unverified_execution!
23
+ def prepared_persisted_step?
57
24
  @split_step_mutex.synchronize do
58
- @split_step_phase = :prepared if @split_step_phase == :verifying_execution
25
+ expected = restart_safe_split_step? ? :dispatch_claimed : :prepared
26
+ [expected, :execution_authorized].include?(@split_step_phase)
59
27
  end
60
28
  end
61
29
 
62
- def verify_split_step_preparation_available!
63
- payload = @split_step_adapter.fetch(@split_step_persistence_key)
64
- durable = persisted_split_step_payload?(payload, @split_step_preparation_payload)
65
- live = persisted_split_step_payload?(current_split_step_preparation_payload, @split_step_preparation_payload)
66
- stable = split_step_transition_signature(@split_step_transition) == @split_step_transition_signature
67
- return if durable && live && stable
30
+ private
68
31
 
69
- raise WorkflowError, "the persisted split-step preparation is no longer available"
70
- end
32
+ def restore_unverified_execution!(verification_token)
33
+ @split_step_mutex.synchronize do
34
+ next unless active_split_step_execution_verification?(verification_token)
71
35
 
72
- def current_split_step_preparation_payload
73
- split_step_preparation_payload(
74
- JSON.generate(to_state.merge(persistence_version: @persistence_version))
75
- )
36
+ @split_step_phase = @split_step_execution_previous_phase
37
+ @split_step_execution_previous_phase = nil
38
+ clear_split_step_execution_verification!
39
+ end
76
40
  end
77
41
 
78
42
  def execute_claimed_split_step_transition!
@@ -82,14 +46,6 @@ module Smith
82
46
  raise WorkflowError, "prepared execution did not return the claimed transition"
83
47
  end
84
48
 
85
- def finish_split_step_execution!(step)
86
- @split_step_mutex.synchronize do
87
- @split_step_execution_thread = nil
88
- @split_step_advance_permit = false
89
- @split_step_phase = step ? :executed : :attempted
90
- end
91
- end
92
-
93
49
  def resolve_split_step_advance_transition
94
50
  @split_step_mutex.synchronize do
95
51
  unless @split_step_phase == :executing && @split_step_execution_thread.equal?(Thread.current)