smith-agents 0.4.5 → 0.5.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +201 -0
  3. data/README.md +108 -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 +24 -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 +6 -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/definition_identity/class_methods.rb +48 -0
  46. data/lib/smith/workflow/definition_identity.rb +25 -0
  47. data/lib/smith/workflow/deterministic_step.rb +2 -2
  48. data/lib/smith/workflow/dsl.rb +73 -5
  49. data/lib/smith/workflow/durability.rb +1 -1
  50. data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
  51. data/lib/smith/workflow/execution.rb +16 -18
  52. data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
  53. data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
  54. data/lib/smith/workflow/fanout_execution.rb +1 -1
  55. data/lib/smith/workflow/graph/diagnostic.rb +27 -1
  56. data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
  57. data/lib/smith/workflow/graph/execution_successors.rb +65 -0
  58. data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
  59. data/lib/smith/workflow/graph/metrics.rb +1 -3
  60. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
  61. data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
  62. data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
  63. data/lib/smith/workflow/graph/reachability.rb +19 -31
  64. data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
  65. data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
  66. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
  67. data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
  68. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
  69. data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
  70. data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
  71. data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
  72. data/lib/smith/workflow/graph/transition_contract.rb +47 -0
  73. data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
  74. data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
  75. data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
  76. data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
  77. data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
  78. data/lib/smith/workflow/graph/validator.rb +1 -1
  79. data/lib/smith/workflow/graph.rb +62 -4
  80. data/lib/smith/workflow/guardrail_integration.rb +23 -9
  81. data/lib/smith/workflow/identifier.rb +24 -0
  82. data/lib/smith/workflow/message_admission.rb +40 -0
  83. data/lib/smith/workflow/message_admission_boundary.rb +22 -0
  84. data/lib/smith/workflow/message_batch.rb +60 -0
  85. data/lib/smith/workflow/message_value_normalizer.rb +133 -0
  86. data/lib/smith/workflow/nested_execution.rb +10 -0
  87. data/lib/smith/workflow/orchestrator_worker.rb +3 -7
  88. data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
  89. data/lib/smith/workflow/parallel/execution_context.rb +113 -0
  90. data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
  91. data/lib/smith/workflow/parallel/root_execution.rb +91 -0
  92. data/lib/smith/workflow/parallel.rb +19 -22
  93. data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
  94. data/lib/smith/workflow/parallel_execution.rb +7 -3
  95. data/lib/smith/workflow/persistence.rb +37 -8
  96. data/lib/smith/workflow/prepared_step.rb +122 -0
  97. data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
  98. data/lib/smith/workflow/prepared_step_execution_authorization.rb +116 -0
  99. data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
  100. data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
  101. data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
  102. data/lib/smith/workflow/retry_execution.rb +20 -11
  103. data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
  104. data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
  105. data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
  106. data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
  107. data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
  108. data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
  109. data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
  110. data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
  111. data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
  112. data/lib/smith/workflow/split_step_persistence/execution.rb +49 -47
  113. data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
  114. data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
  115. data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
  116. data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
  117. data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -0
  118. data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
  119. data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
  120. data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
  121. data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
  122. data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
  123. data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
  124. data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
  125. data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
  126. data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
  127. data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
  128. data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
  129. data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
  130. data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +50 -0
  131. data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
  132. data/lib/smith/workflow/split_step_persistence.rb +36 -0
  133. data/lib/smith/workflow/step_completion.rb +31 -0
  134. data/lib/smith/workflow/string_snapshot.rb +25 -0
  135. data/lib/smith/workflow/transition.rb +122 -133
  136. data/lib/smith/workflow/transition_actionability.rb +22 -0
  137. data/lib/smith/workflow.rb +44 -18
  138. data/lib/smith.rb +41 -2
  139. data/sig/smith.rbs +1 -0
  140. metadata +110 -19
@@ -13,7 +13,11 @@ module Smith
13
13
  handle_split_step_preparation_conflict!
14
14
  raise
15
15
  rescue StandardError
16
- mark_split_step_preparation_unknown!
16
+ if @split_step_dispatch_started
17
+ mark_split_step_preparation_unknown!
18
+ else
19
+ reset_failed_split_step_preparation!
20
+ end
17
21
  raise
18
22
  end
19
23
 
@@ -37,7 +41,11 @@ module Smith
37
41
 
38
42
  def mark_split_step_preparation_unknown!
39
43
  @split_step_mutex.synchronize do
40
- @split_step_phase = :preparation_unknown if @split_step_phase == :preparing
44
+ if @split_step_phase == :preparing
45
+ @split_step_phase = :preparation_unknown
46
+ @split_step_prepared_descriptor = nil
47
+ @split_step_transaction_identity = nil
48
+ end
41
49
  end
42
50
  end
43
51
  end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module Recovery
7
+ private
8
+
9
+ def recover_prepared_step(recovery, adapter)
10
+ decision = validate_prepared_step_recovery!(recovery)
11
+ descriptor = decision.prepared_step
12
+ store = persistence_adapter!(adapter)
13
+ ensure_strict_split_step_persistence!
14
+ validate_split_step_adapter!(store)
15
+ ensure_recovery_outside_transaction!(store)
16
+ payload = fetch_recovery_payload!(store, descriptor.persistence_key)
17
+ document = validate_recovery_payload!(payload, decision, store)
18
+ restore_state(document, allow_step_in_progress: true)
19
+ attach_recovered_split_step!(descriptor, payload, store, dispatch_claim: decision.dispatch_claim)
20
+ self
21
+ end
22
+
23
+ def validate_prepared_step_recovery!(recovery)
24
+ return recovery if recovery.is_a?(PreparedStepRecovery)
25
+
26
+ raise ArgumentError, "recovery must be a Smith::Workflow::PreparedStepRecovery"
27
+ end
28
+
29
+ def ensure_recovery_outside_transaction!(adapter)
30
+ return unless Smith::PersistenceAdapters.supports?(adapter, :transaction_open?)
31
+ return unless adapter.transaction_open?
32
+
33
+ raise WorkflowError, "prepared-step recovery requires committed host authority outside a transaction"
34
+ end
35
+
36
+ def fetch_recovery_payload!(adapter, key)
37
+ payload = adapter.fetch(key)
38
+ return payload if payload.is_a?(String)
39
+
40
+ raise WorkflowError, "the persisted split-step preparation is not available"
41
+ end
42
+
43
+ def validate_recovery_payload!(payload, decision, adapter)
44
+ if payload.bytesize > CanonicalPayloadDigest::MAX_BYTES
45
+ raise WorkflowError,
46
+ "persisted split-step preparation exceeds maximum bytes #{CanonicalPayloadDigest::MAX_BYTES}"
47
+ end
48
+
49
+ document = JSON.parse(payload)
50
+ raise WorkflowError, "the persisted split-step preparation must be an object" unless document.is_a?(Hash)
51
+
52
+ validate_recovery_definition!(document, decision.prepared_step)
53
+ validate_recovery_identity!(document, decision, adapter)
54
+ validate_recovery_digest!(payload, decision)
55
+ document
56
+ rescue JSON::ParserError, TypeError
57
+ raise WorkflowError, "the persisted split-step preparation is invalid"
58
+ end
59
+
60
+ def validate_recovery_definition!(document, descriptor)
61
+ current_digest = self.class.definition_digest
62
+ return if recovery_digests_match?(document, descriptor, current_digest) &&
63
+ recovery_class_matches?(document) && recovery_schema_matches?(document)
64
+
65
+ raise WorkflowError, "the prepared step does not match the current workflow definition"
66
+ end
67
+
68
+ def recovery_digests_match?(document, descriptor, current_digest)
69
+ current_digest &&
70
+ descriptor.definition_digest == current_digest &&
71
+ document["definition_digest"] == current_digest
72
+ end
73
+
74
+ def recovery_class_matches?(document)
75
+ !self.class.name.to_s.empty? && document["class"] == self.class.name
76
+ end
77
+
78
+ def recovery_schema_matches?(document)
79
+ document["schema_version"] == self.class.persistence_schema_version
80
+ end
81
+
82
+ def validate_recovery_identity!(document, decision, adapter)
83
+ descriptor = decision.prepared_step
84
+ return if recovery_marker_matches?(document, decision, adapter) &&
85
+ recovery_position_matches?(document, descriptor)
86
+
87
+ raise WorkflowError, "the persisted split-step preparation does not match the recovery descriptor"
88
+ end
89
+
90
+ def recovery_marker_matches?(document, decision, adapter)
91
+ descriptor = decision.prepared_step
92
+ document["step_in_progress"] == true &&
93
+ recovery_phase_matches?(document, decision.dispatch_claim) &&
94
+ document["split_step_persistence_identity"] == adapter.persistence_identity &&
95
+ document["split_step_token"] == descriptor.token &&
96
+ document["persistence_key"] == descriptor.persistence_key &&
97
+ document["persistence_version"] == descriptor.persistence_version
98
+ end
99
+
100
+ def recovery_phase_matches?(document, dispatch_claim)
101
+ if dispatch_claim
102
+ document["split_step_phase"] == "dispatching" &&
103
+ document["split_step_dispatch_token"] == dispatch_claim.token
104
+ else
105
+ document["split_step_phase"] == "prepared" && !document.key?("split_step_dispatch_token")
106
+ end
107
+ end
108
+
109
+ def recovery_position_matches?(document, descriptor)
110
+ step_count = document["step_count"]
111
+ step_count.is_a?(Integer) &&
112
+ step_count + 1 == descriptor.step_number &&
113
+ document["state"].to_s == descriptor.from
114
+ end
115
+
116
+ def validate_recovery_digest!(payload, decision)
117
+ expected = decision.dispatch_claim&.dispatch_digest || decision.prepared_step.preparation_digest
118
+ return if CanonicalPayloadDigest.call(payload) == expected
119
+
120
+ raise WorkflowError, "the persisted split-step recovery payload digest does not match"
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module RecoveryBoundary
7
+ private
8
+
9
+ def attach_recovered_split_step!(descriptor, payload, adapter, dispatch_claim:)
10
+ transition = pending_split_step_transition
11
+ transition_name = @next_transition_name || transition&.name
12
+ unless transition && transition_name.to_s == descriptor.transition && @state.to_s == descriptor.from
13
+ raise WorkflowError, "the recovered transition does not match the current workflow position"
14
+ end
15
+
16
+ detach_split_step_execution_state!
17
+ install_recovered_split_step!(descriptor, payload, adapter, transition, dispatch_claim)
18
+ end
19
+
20
+ def install_recovered_split_step!(descriptor, payload, adapter, transition, dispatch_claim)
21
+ signature = TransitionContract.capture(transition)
22
+ @split_step_mutex.synchronize do
23
+ install_recovered_transition!(transition, signature)
24
+ install_recovered_persistence!(descriptor, payload, adapter)
25
+ install_recovered_dispatch!(dispatch_claim)
26
+ end
27
+ end
28
+
29
+ def install_recovered_transition!(transition, signature)
30
+ @split_step_transition_name = transition.name
31
+ @split_step_transition = transition
32
+ @split_step_transition_signature = signature
33
+ @split_step_origin_state = @state
34
+ end
35
+
36
+ def install_recovered_persistence!(descriptor, payload, adapter)
37
+ @split_step_token = descriptor.token
38
+ @split_step_prepared_descriptor = descriptor
39
+ @split_step_persistence_key = descriptor.persistence_key
40
+ @split_step_adapter = adapter
41
+ @split_step_persistence_ttl = nil
42
+ @split_step_preparation_payload = payload.dup.freeze
43
+ @split_step_preparation_digest = descriptor.preparation_digest
44
+ @split_step_definition_digest = descriptor.definition_digest
45
+ end
46
+
47
+ def install_recovered_dispatch!(dispatch_claim)
48
+ @split_step_dispatch_started = false
49
+ @split_step_dispatch_token = dispatch_claim&.token
50
+ @split_step_dispatch_descriptor = dispatch_claim
51
+ @split_step_phase = dispatch_claim ? :dispatch_claimed : :prepared
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module RecoveryClassMethods
7
+ def recover_prepared_step(recovery, adapter: Smith.persistence_adapter)
8
+ seal_definition_identity!
9
+ allocate.send(:recover_prepared_step, recovery, adapter)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Workflow
7
+ module SplitStepPersistence
8
+ class ReplaceExactSignature
9
+ extend Dry::Initializer
10
+
11
+ CONTRACT_KEYWORDS = %i[expected_payload ttl].freeze
12
+
13
+ param :callable
14
+
15
+ def valid? = accepts_two_values? && accepts_keywords?
16
+
17
+ private
18
+
19
+ def parameters = callable.parameters
20
+
21
+ def accepts_two_values?
22
+ required = parameters.count { |kind, _| kind == :req }
23
+ positional = parameters.count { |kind, _| %i[req opt].include?(kind) }
24
+ rest = parameters.any? { |kind, _| kind == :rest }
25
+ required <= 2 && (positional >= 2 || rest)
26
+ end
27
+
28
+ def accepts_keywords?
29
+ required_keywords_supported? && contract_keywords_accepted?
30
+ end
31
+
32
+ def required_keywords_supported?
33
+ required = parameters.filter_map { |kind, name| name if kind == :keyreq }
34
+ (required - CONTRACT_KEYWORDS).empty?
35
+ end
36
+
37
+ def contract_keywords_accepted?
38
+ return true if parameters.any? { |kind, _| kind == :keyrest }
39
+
40
+ keywords = parameters.filter_map { |kind, name| name if %i[key keyreq].include?(kind) }
41
+ CONTRACT_KEYWORDS.all? { |name| keywords.include?(name) }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "replace_exact_signature"
4
+
5
+ module Smith
6
+ class Workflow
7
+ module SplitStepPersistence
8
+ module RestartSafeAdapter
9
+ private
10
+
11
+ def validate_restart_safe_adapter!(adapter)
12
+ return unless restart_safe_split_step?
13
+
14
+ ensure_replace_exact_capability!(adapter)
15
+ ensure_replace_exact_signature!(adapter)
16
+ ensure_persistence_identity!(adapter)
17
+ end
18
+
19
+ def ensure_replace_exact_capability!(adapter)
20
+ return if Smith::PersistenceAdapters.supports?(adapter, :replace_exact)
21
+
22
+ raise WorkflowError, "restart-safe split-step persistence requires replace_exact"
23
+ end
24
+
25
+ def ensure_replace_exact_signature!(adapter)
26
+ return if ReplaceExactSignature.new(adapter.method(:replace_exact)).valid?
27
+
28
+ raise WorkflowError,
29
+ "restart-safe split-step persistence requires replace_exact(key, payload, expected_payload:, ttl:)"
30
+ end
31
+
32
+ def ensure_persistence_identity!(adapter)
33
+ identity = adapter.persistence_identity if
34
+ Smith::PersistenceAdapters.supports?(adapter, :persistence_identity)
35
+ return if identity.is_a?(String) && !identity.empty? && identity.bytesize <= 256
36
+
37
+ raise WorkflowError, "restart-safe split-step persistence requires a bounded persistence_identity"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -4,6 +4,43 @@ module Smith
4
4
  class Workflow
5
5
  module SplitStepPersistence
6
6
  module SubclassBoundary
7
+ class << self
8
+ def protect_execution_path!(workflow_class, *owners)
9
+ protected_method_names(owners).each do |method_name|
10
+ protect_method!(workflow_class, method_name)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def protected_method_names(owners)
17
+ owners.flat_map do |owner|
18
+ owner.private_instance_methods(false) + owner.protected_instance_methods(false)
19
+ end.uniq
20
+ end
21
+
22
+ def protect_method!(workflow_class, method_name)
23
+ return if boundary_method_names.include?(method_name)
24
+
25
+ implementation = workflow_class.instance_method(method_name)
26
+ define_method(method_name) do |*arguments, **keywords, &block|
27
+ active = SubclassBoundary
28
+ .instance_method(:smith_prepared_execution_active?)
29
+ .bind_call(self)
30
+ if active
31
+ implementation.bind_call(self, *arguments, **keywords, &block)
32
+ else
33
+ super(*arguments, **keywords, &block)
34
+ end
35
+ end
36
+ private method_name
37
+ end
38
+
39
+ def boundary_method_names
40
+ instance_methods(false) + private_instance_methods(false) + protected_instance_methods(false)
41
+ end
42
+ end
43
+
7
44
  def self.guard
8
45
  Module.new.tap do |guard|
9
46
  instance_methods(false).each do |method_name|
@@ -33,6 +70,15 @@ module Smith
33
70
 
34
71
  private
35
72
 
73
+ def smith_prepared_execution_active?
74
+ root_execution = @split_step_phase == :executing &&
75
+ @split_step_execution_thread.equal?(Thread.current)
76
+ return true if root_execution
77
+
78
+ authorization = @split_step_active_execution_authorization
79
+ authorization&.active_in_current_execution? == true
80
+ end
81
+
36
82
  def effective_persistence_ttl
37
83
  return super unless split_step_boundary_active?
38
84
  return super unless instance_variable_defined?(:@split_step_persistence_ttl)
@@ -46,6 +92,10 @@ module Smith
46
92
 
47
93
  { ttl: ttl }
48
94
  end
95
+
96
+ def dispatch_store!(...)
97
+ DispatchBoundary.instance_method(:dispatch_store!).bind_call(self, ...)
98
+ end
49
99
  end
50
100
  end
51
101
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Workflow
7
+ module SplitStepPersistence
8
+ class TransactionIdentity
9
+ extend Dry::Initializer
10
+
11
+ param :adapter
12
+
13
+ def self.capture(adapter) = new(adapter).capture
14
+ def self.matches?(adapter, expected) = new(adapter).matches?(expected)
15
+
16
+ def capture
17
+ return unless PersistenceAdapters.supports?(adapter, :transaction_open?)
18
+ return unless adapter.transaction_open?
19
+
20
+ ensure_identity_capability!
21
+ identity = adapter.transaction_identity
22
+ if !identity || identity.to_s.empty?
23
+ raise WorkflowError, "transactional split-step persistence requires an active transaction identity"
24
+ end
25
+
26
+ identity.to_s.dup.freeze
27
+ end
28
+
29
+ def matches?(expected)
30
+ capture == expected
31
+ rescue StandardError
32
+ false
33
+ end
34
+
35
+ private
36
+
37
+ def ensure_identity_capability!
38
+ return if PersistenceAdapters.supports?(adapter, :transaction_identity)
39
+
40
+ raise WorkflowError, "transactional split-step persistence requires transaction_identity"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -4,17 +4,39 @@ require "digest"
4
4
  require "json"
5
5
  require "securerandom"
6
6
 
7
+ require_relative "prepared_step"
8
+ require_relative "prepared_step_dispatch"
9
+ require_relative "prepared_step_execution_authorization"
10
+ require_relative "prepared_step_execution_result"
11
+ require_relative "prepared_step_recovery"
7
12
  require_relative "split_step_persistence/subclass_boundary"
8
13
  require_relative "split_step_persistence/inheritance"
14
+ require_relative "split_step_persistence/recovery_class_methods"
9
15
  require_relative "split_step_persistence/transition_contract_structured_values"
10
16
  require_relative "split_step_persistence/transition_contract_signature"
11
17
  require_relative "split_step_persistence/transition_contract_freezer"
12
18
  require_relative "split_step_persistence/transition_contract"
19
+ require_relative "split_step_persistence/transaction_identity"
20
+ require_relative "split_step_persistence/canonical_payload_digest"
21
+ require_relative "split_step_persistence/definition_boundary"
13
22
  require_relative "split_step_persistence/boundary"
23
+ require_relative "split_step_persistence/boundary_reset"
14
24
  require_relative "split_step_persistence/state_snapshot"
15
25
  require_relative "split_step_persistence/preparation_claim"
16
26
  require_relative "split_step_persistence/preparation_recovery"
17
27
  require_relative "split_step_persistence/preparation"
28
+ require_relative "split_step_persistence/restart_safe_adapter"
29
+ require_relative "split_step_persistence/recovery"
30
+ require_relative "split_step_persistence/recovery_boundary"
31
+ require_relative "split_step_persistence/preparation_payload"
32
+ require_relative "split_step_persistence/dispatch_boundary"
33
+ require_relative "split_step_persistence/dispatch_claim"
34
+ require_relative "split_step_persistence/dispatch_verification"
35
+ require_relative "split_step_persistence/dispatch_confirmation"
36
+ require_relative "split_step_persistence/execution_verification"
37
+ require_relative "split_step_persistence/execution_binding_snapshot"
38
+ require_relative "split_step_persistence/execution_authorization"
39
+ require_relative "split_step_persistence/execution_result_capture"
18
40
  require_relative "split_step_persistence/execution"
19
41
  require_relative "split_step_persistence/checkpoint_state"
20
42
  require_relative "split_step_persistence/checkpoint"
@@ -27,13 +49,27 @@ module Smith
27
49
 
28
50
  def self.prepended(base)
29
51
  base.singleton_class.prepend(Inheritance)
52
+ base.extend(RecoveryClassMethods)
30
53
  end
31
54
 
32
55
  include Boundary
56
+ include BoundaryReset
57
+ include DefinitionBoundary
33
58
  include StateSnapshot
34
59
  include PreparationClaim
35
60
  include PreparationRecovery
36
61
  include Preparation
62
+ include RestartSafeAdapter
63
+ include Recovery
64
+ include RecoveryBoundary
65
+ include PreparationPayload
66
+ include DispatchBoundary
67
+ include DispatchClaim
68
+ include DispatchVerification
69
+ include DispatchConfirmation
70
+ include ExecutionVerification
71
+ include ExecutionAuthorization
72
+ include ExecutionResultCapture
37
73
  include Execution
38
74
  include CheckpointState
39
75
  include Checkpoint
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module StepCompletion
6
+ private
7
+
8
+ def complete_step(transition, output)
9
+ step = { transition: transition.name, from: transition.from, to: transition.to, output: output }
10
+ result = SplitStepPersistence
11
+ .instance_method(:prepare_split_step_execution_result)
12
+ .bind_call(self, step)
13
+ StepCompletion.instance_method(:append_accepted_output).bind_call(self, output)
14
+ SplitStepPersistence
15
+ .instance_method(:commit_split_step_execution_result!)
16
+ .bind_call(self, result)
17
+ @state = transition.to
18
+ @next_transition_name = @router_next_transition || transition.success_transition
19
+ @router_next_transition = nil
20
+ emit_step_completed(transition, output)
21
+ step
22
+ end
23
+
24
+ def append_accepted_output(output)
25
+ return unless @session_messages && !output.nil?
26
+
27
+ @session_messages << { role: :assistant, content: output }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class StringSnapshot
6
+ BYTESIZE = String.instance_method(:bytesize)
7
+ FREEZE = String.instance_method(:freeze)
8
+ INITIALIZE_COPY = String.instance_method(:initialize_copy)
9
+ private_constant :BYTESIZE, :FREEZE, :INITIALIZE_COPY
10
+
11
+ class << self
12
+ def bytesize(value)
13
+ BYTESIZE.bind_call(value)
14
+ end
15
+
16
+ def copy(value, freeze: false)
17
+ String.allocate.tap do |copy|
18
+ INITIALIZE_COPY.bind_call(copy, value)
19
+ FREEZE.bind_call(copy) if freeze
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end