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,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+ require_relative "process_local"
5
+
6
+ module Smith
7
+ class Workflow
8
+ class PreparedStepExecutionScope
9
+ include ProcessLocal
10
+
11
+ def initialize
12
+ @mutex = Mutex.new
13
+ @phase = :issued
14
+ @thread = nil
15
+ @fiber = nil
16
+ @branch_fibers = {}.compare_by_identity
17
+ end
18
+
19
+ def activate!(thread, fiber = Fiber.current)
20
+ Thread.handle_interrupt(Object => :never) do
21
+ @mutex.synchronize do
22
+ raise WorkflowError, "prepared-step execution scope is no longer available" unless @phase == :issued
23
+
24
+ @phase = :active
25
+ @thread = thread
26
+ @fiber = fiber
27
+ end
28
+ end
29
+ end
30
+
31
+ def close!(thread = nil, fiber = Fiber.current)
32
+ Thread.handle_interrupt(Object => :never) do
33
+ @mutex.synchronize do
34
+ if @phase == :active && thread && !owner?(@thread, @fiber, thread, fiber)
35
+ raise WorkflowError, "prepared-step execution scope belongs to another thread or fiber"
36
+ end
37
+ raise WorkflowError, "prepared-step execution still has active branch fibers" unless @branch_fibers.empty?
38
+
39
+ @phase = :closed
40
+ @thread = nil
41
+ @fiber = nil
42
+ end
43
+ end
44
+ end
45
+
46
+ def within_branch(&block)
47
+ thread = Thread.current
48
+ fiber = Fiber.current
49
+ Thread.handle_interrupt(Object => :never) do
50
+ enter_branch!(thread, fiber)
51
+ begin
52
+ Thread.handle_interrupt(Object => :immediate, &block)
53
+ ensure
54
+ leave_branch!(thread, fiber)
55
+ end
56
+ end
57
+ end
58
+
59
+ def active_for?(thread, fiber = Fiber.current)
60
+ @mutex.synchronize do
61
+ @phase == :active && (owner?(@thread, @fiber, thread, fiber) || branch_owner?(thread, fiber))
62
+ end
63
+ end
64
+
65
+ def binding_accessible_for?(thread, fiber = Fiber.current)
66
+ @mutex.synchronize do
67
+ @phase == :issued ||
68
+ (@phase == :active && (owner?(@thread, @fiber, thread, fiber) || branch_owner?(thread, fiber)))
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def enter_branch!(thread, fiber)
75
+ @mutex.synchronize do
76
+ raise WorkflowError, "prepared-step execution scope is not active" unless @phase == :active
77
+
78
+ entry = @branch_fibers[fiber]
79
+ if entry && !entry.fetch(:thread).equal?(thread)
80
+ raise WorkflowError, "prepared-step branch fiber belongs to another thread"
81
+ end
82
+
83
+ @branch_fibers[fiber] = { thread:, count: entry ? entry.fetch(:count) + 1 : 1 }
84
+ end
85
+ end
86
+
87
+ def leave_branch!(thread, fiber)
88
+ @mutex.synchronize do
89
+ entry = @branch_fibers.fetch(fiber) do
90
+ raise WorkflowError, "prepared-step branch execution fiber is not active"
91
+ end
92
+ unless entry.fetch(:thread).equal?(thread)
93
+ raise WorkflowError, "prepared-step branch fiber belongs to another thread"
94
+ end
95
+
96
+ count = entry.fetch(:count)
97
+ count == 1 ? @branch_fibers.delete(fiber) : entry[:count] = count - 1
98
+ end
99
+ end
100
+
101
+ def branch_owner?(thread, fiber)
102
+ entry = @branch_fibers[fiber]
103
+ !!(entry && entry.fetch(:thread).equal?(thread))
104
+ end
105
+
106
+ def owner?(owner_thread, owner_fiber, thread, fiber)
107
+ owner_thread.equal?(thread) && owner_fiber.equal?(fiber)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ require_relative "../types"
6
+ require_relative "prepared_step"
7
+ require_relative "prepared_step_dispatch"
8
+
9
+ module Smith
10
+ class Workflow
11
+ class PreparedStepRecovery < Dry::Struct
12
+ attribute :prepared_step, Types.Instance(PreparedStep)
13
+ attribute? :dispatch_claim, Types.Instance(PreparedStepDispatch).optional
14
+ attribute :execution_status, Types::Symbol.enum(:not_started)
15
+
16
+ def self.not_started(witness)
17
+ case witness
18
+ when PreparedStep
19
+ new(prepared_step: witness, dispatch_claim: nil, execution_status: :not_started)
20
+ when PreparedStepDispatch
21
+ new(prepared_step: witness.prepared_step, dispatch_claim: witness, execution_status: :not_started)
22
+ else
23
+ raise ArgumentError, "recovery witness must be a PreparedStep or PreparedStepDispatch"
24
+ end
25
+ end
26
+
27
+ def initialize(attributes)
28
+ super
29
+ if dispatch_claim && dispatch_claim.prepared_step.to_h != prepared_step.to_h
30
+ raise ArgumentError, "dispatch claim must belong to the prepared step"
31
+ end
32
+
33
+ self.attributes.freeze
34
+ freeze
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module ProcessLocal
6
+ def initialize_copy(_source)
7
+ raise TypeError, "#{self.class} cannot be copied"
8
+ end
9
+
10
+ def _dump(_depth)
11
+ raise TypeError, "#{self.class} cannot be serialized"
12
+ end
13
+
14
+ def encode_with(_coder)
15
+ raise TypeError, "#{self.class} cannot be serialized"
16
+ end
17
+
18
+ def init_with(_coder)
19
+ raise TypeError, "#{self.class} cannot be deserialized"
20
+ end
21
+
22
+ def as_json(*)
23
+ raise TypeError, "#{self.class} cannot be serialized"
24
+ end
25
+
26
+ def to_json(*)
27
+ raise TypeError, "#{self.class} cannot be serialized"
28
+ end
29
+ end
30
+
31
+ private_constant :ProcessLocal
32
+ end
33
+ end
@@ -9,6 +9,7 @@ module Smith
9
9
  config = transition.retry_config
10
10
  return run_guarded_step(transition) unless config
11
11
 
12
+ schedule = retry_schedule(config)
12
13
  attempt = 0
13
14
  begin
14
15
  attempt += 1
@@ -16,7 +17,7 @@ module Smith
16
17
  rescue StandardError => e
17
18
  raise unless retry_transition_error?(config, e, attempt)
18
19
 
19
- sleep_for_retry(config, attempt)
20
+ sleep_for_retry(schedule, attempt)
20
21
  retry
21
22
  end
22
23
  end
@@ -32,20 +33,28 @@ module Smith
32
33
  end
33
34
  end
34
35
 
35
- def sleep_for_retry(config, failed_attempt)
36
- delay = retry_delay(config, failed_attempt)
36
+ def sleep_for_retry(schedule, failed_attempt)
37
+ delay = retry_delay(schedule, failed_attempt)
37
38
  sleep(delay) if delay.positive?
38
39
  end
39
40
 
40
- def retry_delay(config, failed_attempt)
41
- delay = config.fetch(:backoff) * (2**[failed_attempt - 1, 0].max)
42
- max_delay = config[:max_delay]
43
- delay = [delay, max_delay].min if max_delay
41
+ def retry_delay(config_or_schedule, failed_attempt)
42
+ schedule = if config_or_schedule.is_a?(ExponentialBackoff)
43
+ config_or_schedule
44
+ else
45
+ retry_schedule(config_or_schedule)
46
+ end
47
+ schedule.delay(failed_attempt, random: method(:rand))
48
+ end
44
49
 
45
- jitter = config.fetch(:jitter)
46
- delay += rand * jitter if jitter.positive?
47
- delay = [delay, max_delay].min if max_delay
48
- delay
50
+ def retry_schedule(config)
51
+ ExponentialBackoff.new(
52
+ attempts: config.fetch(:attempts),
53
+ base_delay: config.fetch(:backoff),
54
+ max_delay: config[:max_delay],
55
+ jitter: config.fetch(:jitter),
56
+ delay_label: "backoff"
57
+ )
49
58
  end
50
59
  end
51
60
  end
@@ -21,19 +21,50 @@ module Smith
21
21
 
22
22
  def initialize_copy(source)
23
23
  super
24
- @split_step_mutex = Mutex.new
25
- return unless source.instance_variable_get(:@split_step_phase)
24
+ copied_split_step_phase = @split_step_phase
25
+ detach_copied_runtime_state!(source)
26
+ return unless copied_split_step_phase
26
27
 
27
28
  @split_step_phase = :copied_boundary
29
+ reset_copied_execution_state!
30
+ reset_copied_dispatch_state!
31
+ end
32
+ private :initialize_copy
33
+
34
+ private
35
+
36
+ def reset_copied_execution_state!
28
37
  @split_step_token = nil
38
+ @split_step_prepared_descriptor = nil
39
+ @split_step_transaction_identity = nil
29
40
  @split_step_execution_thread = nil
41
+ @split_step_active_execution_authorization = nil
42
+ @split_step_execution_result = nil
30
43
  @split_step_advance_permit = false
44
+ clear_split_step_execution_verification!
45
+ clear_split_step_execution_authorization!
46
+ end
47
+
48
+ def reset_copied_dispatch_state!
49
+ @split_step_dispatch_token = nil
50
+ @split_step_dispatch_descriptor = nil
51
+ @split_step_attempted_dispatch_descriptor = nil
52
+ @split_step_dispatch_thread = nil
53
+ @split_step_pre_dispatch_payload = nil
54
+ @split_step_attempted_dispatch_payload = nil
31
55
  @split_step_preparation_thread = nil
32
56
  @split_step_persist_permit = false
33
57
  end
34
- private :initialize_copy
35
58
 
36
- private
59
+ def detach_copied_runtime_state!(source)
60
+ source_messages = source.instance_variable_get(:@session_messages)
61
+ @split_step_mutex = Mutex.new
62
+ @persisted_keys_mutex = Mutex.new
63
+ @tool_results_mutex = Mutex.new
64
+ @usage_mutex = Mutex.new
65
+ detach_split_step_execution_state!
66
+ @session_messages = snapshot_value(source_messages || [])
67
+ end
37
68
 
38
69
  def ensure_split_step_execution_allowed!
39
70
  @split_step_mutex.synchronize do
@@ -91,24 +122,6 @@ module Smith
91
122
  @split_step_execution_thread.equal?(Thread.current) &&
92
123
  @split_step_advance_permit
93
124
  end
94
-
95
- def clear_split_step_boundary!
96
- @split_step_phase = nil
97
- @split_step_transition_name = nil
98
- @split_step_transition = nil
99
- @split_step_transition_signature = nil
100
- @split_step_origin_state = nil
101
- @split_step_token = nil
102
- @split_step_persistence_key = nil
103
- @split_step_adapter = nil
104
- remove_instance_variable(:@split_step_persistence_ttl) if
105
- instance_variable_defined?(:@split_step_persistence_ttl)
106
- @split_step_preparation_payload = nil
107
- @split_step_checkpoint_digest = nil
108
- @split_step_checkpoint_version = nil
109
- @split_step_preparation_thread = nil
110
- @split_step_persist_permit = false
111
- end
112
125
  end
113
126
  end
114
127
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module SplitStepPersistence
6
+ module BoundaryReset
7
+ private
8
+
9
+ def clear_split_step_boundary!
10
+ @split_step_phase = nil
11
+ @split_step_transition_name = nil
12
+ @split_step_transition = nil
13
+ @split_step_transition_signature = nil
14
+ @split_step_origin_state = nil
15
+ @split_step_token = nil
16
+ @split_step_active_execution_authorization = nil
17
+ @split_step_execution_result = nil
18
+ clear_split_step_descriptor_state!
19
+ @split_step_persistence_key = nil
20
+ @split_step_adapter = nil
21
+ remove_instance_variable(:@split_step_persistence_ttl) if
22
+ instance_variable_defined?(:@split_step_persistence_ttl)
23
+ @split_step_preparation_payload = nil
24
+ @split_step_checkpoint_digest = nil
25
+ @split_step_checkpoint_version = nil
26
+ @split_step_preparation_thread = nil
27
+ @split_step_persist_permit = false
28
+ end
29
+
30
+ def clear_split_step_descriptor_state!
31
+ @split_step_prepared_descriptor = nil
32
+ @split_step_transaction_identity = nil
33
+ @split_step_preparation_digest = nil
34
+ @split_step_pending_descriptor = nil
35
+ @split_step_dispatch_started = false
36
+ @split_step_dispatch_token = nil
37
+ @split_step_dispatch_descriptor = nil
38
+ @split_step_attempted_dispatch_descriptor = nil
39
+ @split_step_dispatch_thread = nil
40
+ @split_step_pre_dispatch_payload = nil
41
+ @split_step_attempted_dispatch_payload = nil
42
+ clear_split_step_execution_verification!
43
+ clear_split_step_execution_authorization!
44
+ remove_instance_variable(:@split_step_definition_digest) if
45
+ instance_variable_defined?(:@split_step_definition_digest)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "dry-initializer"
5
+ require "json"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module SplitStepPersistence
10
+ class CanonicalPayloadDigest
11
+ extend Dry::Initializer
12
+
13
+ MAX_BYTES = 4 * 1024 * 1024
14
+ MAX_NODES = 100_000
15
+
16
+ param :payload
17
+
18
+ def self.call(payload) = new(payload).call
19
+
20
+ def call
21
+ if payload.bytesize > MAX_BYTES
22
+ raise WorkflowError, "split-step preparation payload exceeds maximum bytes #{MAX_BYTES}"
23
+ end
24
+
25
+ @nodes = 0
26
+ canonical = canonicalize(JSON.parse(payload))
27
+ Digest::SHA256.hexdigest(JSON.generate(canonical))
28
+ end
29
+
30
+ private
31
+
32
+ def canonicalize(value)
33
+ visit!
34
+ case value
35
+ when Hash then canonical_hash(value)
36
+ when Array then value.map { |item| canonicalize(item) }
37
+ when Float then value == value.to_i ? value.to_i : value
38
+ else value
39
+ end
40
+ end
41
+
42
+ def canonical_hash(value)
43
+ value.keys.sort.to_h { |key| [key, canonicalize(value.fetch(key))] }
44
+ end
45
+
46
+ def visit!
47
+ @nodes += 1
48
+ return if @nodes <= MAX_NODES
49
+
50
+ raise WorkflowError, "split-step preparation payload exceeds maximum size #{MAX_NODES}"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -91,11 +91,9 @@ module Smith
91
91
  end
92
92
 
93
93
  def capture_split_step_payload!(payload, next_version, checkpoint_claimed:)
94
- validate_split_step_marker!(payload, expected: true) if @split_step_phase == :preparing || checkpoint_claimed
95
- if @split_step_phase == :preparing
96
- payload = split_step_preparation_payload(payload)
97
- @split_step_preparation_payload = payload
98
- end
94
+ return capture_split_step_preparation_payload!(payload, next_version) if @split_step_phase == :preparing
95
+
96
+ validate_split_step_marker!(payload, expected: true) if checkpoint_claimed
99
97
  return payload unless checkpoint_claimed
100
98
 
101
99
  checkpoint_payload = split_step_checkpoint_payload(payload)
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../composite/contract"
4
+ require_relative "../composite_branch_execution_authorization"
5
+ require_relative "execution_binding_snapshot"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module SplitStepPersistence
10
+ module CompositeBranchAuthorization
11
+ include Composite::Contract
12
+
13
+ private
14
+
15
+ def authorize_prepared_composite_branch_execution!(execution:, input:)
16
+ authorization = nil
17
+ handed_off = false
18
+ authorization = CompositeBranchAuthorization
19
+ .instance_method(:interrupt_safe_composite_branch_authorization)
20
+ .bind_call(self, execution, input)
21
+ handed_off = true
22
+ authorization
23
+ ensure
24
+ unless handed_off
25
+ ExecutionLifecycle
26
+ .instance_method(:release_failed_execution_authorization!)
27
+ .bind_call(self, authorization)
28
+ end
29
+ end
30
+
31
+ def with_composite_branch_authorization(execution, input)
32
+ Thread.handle_interrupt(Object => :never) do
33
+ authorization = interrupt_safe_composite_branch_authorization(execution, input)
34
+ begin
35
+ Thread.handle_interrupt(Object => :immediate) { yield(authorization) }
36
+ ensure
37
+ ExecutionLifecycle
38
+ .instance_method(:release_failed_execution_authorization!)
39
+ .bind_call(self, authorization)
40
+ end
41
+ end
42
+ end
43
+
44
+ def interrupt_safe_composite_branch_authorization(execution, input)
45
+ Thread.handle_interrupt(Object => :never) do
46
+ Composite::Contract
47
+ .instance_method(:validate_composite_branch_payload_types!)
48
+ .bind_call(self, execution, input)
49
+ verification_token = ExecutionAuthorization
50
+ .instance_method(:claim_framework_execution_verification!)
51
+ .bind_call(self)
52
+ begin
53
+ CompositeBranchAuthorization
54
+ .instance_method(:authorize_claimed_composite_branch_execution!)
55
+ .bind_call(self, verification_token, execution, input)
56
+ ensure
57
+ ExecutionAuthorization
58
+ .instance_method(:restore_framework_execution_verification!)
59
+ .bind_call(self, verification_token)
60
+ end
61
+ end
62
+ end
63
+
64
+ def authorize_claimed_composite_branch_execution!(verification_token, execution, input)
65
+ ExecutionVerification
66
+ .instance_method(:verify_claimed_split_step_execution!)
67
+ .bind_call(self, verification_token)
68
+ authorization = CompositeBranchAuthorization
69
+ .instance_method(:build_composite_branch_execution_authorization)
70
+ .bind_call(self, execution, input)
71
+ ExecutionAuthorization
72
+ .instance_method(:activate_split_step_execution_authorization!)
73
+ .bind_call(self, authorization, verification_token)
74
+ Composite::Contract
75
+ .instance_method(:validate_composite_branch_execution!)
76
+ .bind_call(self, authorization, execution, input)
77
+ authorization
78
+ end
79
+
80
+ def build_composite_branch_execution_authorization(execution, input)
81
+ role = execution.kind == :parallel ? :agent : :fanout_agent
82
+ bindings = ExecutionBindingSnapshot.capture_agent(
83
+ @split_step_transition,
84
+ workflow_class: self.class,
85
+ name: execution.branch.agent,
86
+ role:
87
+ )
88
+ CompositeBranchExecutionAuthorization.new(
89
+ execution:,
90
+ input:,
91
+ prepared_step: @split_step_prepared_descriptor,
92
+ dispatch_claim: @split_step_dispatch_descriptor,
93
+ execution_bindings: bindings
94
+ )
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../composite/effects"
4
+
5
+ module Smith
6
+ class Workflow
7
+ module SplitStepPersistence
8
+ module CompositeBranchEffects
9
+ private
10
+
11
+ def composite_effect_offsets
12
+ usage = @usage_mutex.synchronize { @usage_entries.length }
13
+ tools = @tool_results_mutex.synchronize { @tool_results.length }
14
+ [usage, tools]
15
+ end
16
+
17
+ def capture_composite_branch_effects(usage_offset, tool_offset, branch)
18
+ usage = @usage_mutex.synchronize do
19
+ @usage_entries.drop(usage_offset).map { composite_usage_entry(_1, branch) }
20
+ end
21
+ tools = @tool_results_mutex.synchronize { @tool_results.drop(tool_offset) }
22
+ consumed = @ledger ? @ledger.consumed : {}
23
+ [Composite::Effects.new(usage_entries: usage, tool_results: tools, budget_consumed: consumed), nil]
24
+ rescue WorkflowError, ArgumentError => e
25
+ safe = Composite::Effects.new(usage_entries: usage || [], tool_results: [], budget_consumed: consumed || {})
26
+ [safe, e]
27
+ end
28
+
29
+ def composite_usage_entry(entry, branch)
30
+ attributes = entry.to_h
31
+ recorded_agent = attributes[:agent_name]
32
+ if recorded_agent && recorded_agent.to_s != branch.agent
33
+ raise WorkflowError, "composite usage entry does not match the executed branch agent"
34
+ end
35
+
36
+ attributes.merge(agent_name: branch.agent)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../composite/contract"
4
+ require_relative "composite_branch_effects"
5
+ require_relative "composite_branch_outcome"
6
+
7
+ module Smith
8
+ class Workflow
9
+ module SplitStepPersistence
10
+ module CompositeBranchExecution
11
+ include Composite::Contract
12
+ include CompositeBranchEffects
13
+ include CompositeBranchOutcome
14
+
15
+ private
16
+
17
+ def execute_authorized_composite_branch!(authorization, execution:, input:)
18
+ authorization = validate_composite_authorization!(authorization)
19
+ perform_authorized_prepared_step_execution!(authorization) do
20
+ validate_composite_branch_execution!(authorization, execution, input)
21
+ [nil, execute_composite_branch(authorization, execution, input)]
22
+ end
23
+ end
24
+
25
+ def execute_composite_branch(authorization, execution, input)
26
+ branch = execution.branch
27
+ previous_ledger = @ledger
28
+ previous_execution_namespace = @execution_namespace
29
+ @ledger = composite_branch_ledger(branch)
30
+ @execution_namespace = execution.execution_namespace
31
+ offsets = composite_effect_offsets
32
+ output, error = capture_composite_branch do
33
+ dispatch_composite_branch(authorization, execution, input)
34
+ end
35
+ effects, effect_error = capture_composite_branch_effects(*offsets, branch)
36
+ composite_branch_outcome(execution, output, error || effect_error, effects)
37
+ ensure
38
+ @ledger = previous_ledger
39
+ @execution_namespace = previous_execution_namespace
40
+ end
41
+
42
+ def capture_composite_branch(&block)
43
+ output = within_raw_step_context { with_scoped_artifacts(&block) }
44
+ [output, nil]
45
+ rescue StandardError => e
46
+ [nil, e]
47
+ end
48
+
49
+ def dispatch_composite_branch(authorization, execution, input)
50
+ branch = execution.branch
51
+ transition = @split_step_transition
52
+ agent_role = execution.kind == :parallel ? :agent : :fanout_agent
53
+ agent_class = captured_agent(authorization, transition, branch.agent, agent_role)
54
+ apply_tool_guardrails(agent_class) if execution.kind == :parallel
55
+ environment = composite_branch_environment(execution, input, agent_class, transition)
56
+ execute_composite_branch_kind(execution, transition, agent_class, environment)
57
+ end
58
+
59
+ def execute_composite_branch_kind(execution, transition, agent_class, environment)
60
+ branch = execution.branch
61
+ signal = Parallel::CancellationSignal.new
62
+ return run_branch(transition, branch.ordinal, environment, @ledger, signal) if execution.kind == :parallel
63
+
64
+ branch_key, agent = fetch_composite_fanout_branch(transition, branch.key)
65
+ run_fanout_branch(branch_key, agent, agent_class, environment, signal)
66
+ end
67
+
68
+ def composite_branch_environment(execution, input, agent_class, transition)
69
+ branch = execution.branch
70
+ budget = branch.budget.transform_keys(&:to_sym)
71
+ branch_key = fetch_composite_fanout_branch(transition, branch.key).first unless execution.kind == :parallel
72
+ estimates = execution.kind == :parallel ? budget : { branch_key => budget }
73
+ BranchEnv.new(
74
+ prepared_input: input.agent_messages,
75
+ guardrail_sources: Tool.current_guardrails,
76
+ scoped_store: propagate_scoped_artifacts,
77
+ branch_estimates: estimates,
78
+ deadline: wall_clock_deadline,
79
+ agent_class: execution.kind == :parallel ? agent_class : nil
80
+ )
81
+ end
82
+
83
+ def composite_branch_ledger(branch)
84
+ return if branch.budget.empty?
85
+
86
+ Budget::Ledger.new(limits: branch.budget.transform_keys(&:to_sym))
87
+ end
88
+
89
+ def fetch_composite_fanout_branch(transition, branch_key)
90
+ Transition.instance_method(:fetch_fanout_branch!).bind_call(transition, branch_key)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end