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
@@ -9,53 +9,61 @@ module Smith
9
9
  :deterministic_routes
10
10
 
11
11
  def initialize(name, from:, to:, &)
12
- @name = name
13
- @from = from
14
- @to = to
12
+ @name = own_identifier(name)
13
+ @from = own_identifier(from, allow_nil: true)
14
+ @to = own_identifier(to)
15
15
  instance_eval(&) if block_given?
16
16
  end
17
17
 
18
18
  def execute(agent_name, **opts)
19
- validate_execute_conflicts!
19
+ validate_execution_primitive_conflict!("execute")
20
20
 
21
- @agent_name = agent_name
22
- @agent_opts = opts
21
+ normalized_agent = normalize_agent_reference!(agent_name, "agent")
22
+ validate_parallel_options!(opts)
23
+
24
+ @agent_name = normalized_agent
25
+ @agent_opts = opts.freeze
26
+ commit_execution_primitive!("execute")
23
27
  end
24
28
 
25
29
  def on_success(transition_name)
26
- @success_transition = transition_name
30
+ if routed?
31
+ raise WorkflowError, "routed transitions cannot declare on_success; router routes select the next transition"
32
+ end
33
+
34
+ @success_transition = own_identifier(transition_name)
27
35
  end
28
36
 
29
37
  def on_failure(transition_name)
30
- @failure_transition = transition_name
38
+ @failure_transition = own_identifier(transition_name)
31
39
  end
32
40
 
33
41
  def route(agent_name, routes:, confidence_threshold:, fallback:)
34
- validate_route_conflicts!
42
+ validate_execution_primitive_conflict!("route")
43
+ if @success_transition
44
+ raise WorkflowError, "routed transitions cannot declare on_success; router routes select the next transition"
45
+ end
35
46
 
36
- @agent_name = normalize_agent_name!(agent_name, "router")
37
- @router_config = {
47
+ normalized_agent = normalize_agent_name!(agent_name, "router")
48
+ normalized_config = {
38
49
  routes: normalize_router_routes!(routes),
39
50
  confidence_threshold: normalize_confidence_threshold!(confidence_threshold),
40
51
  fallback: normalize_transition_reference!(fallback, "router fallback")
41
- }
52
+ }.freeze
53
+
54
+ @agent_name = normalized_agent
55
+ @router_config = normalized_config
56
+ commit_execution_primitive!("route")
42
57
  end
43
58
 
44
59
  def workflow(klass)
45
60
  raise WorkflowError, "workflow binding must be a Class" unless klass.is_a?(Class)
46
61
  raise WorkflowError, "workflow binding must be a Smith::Workflow subclass" unless klass < Workflow
47
62
 
48
- validate_conflicts!(
49
- "workflow",
50
- [
51
- ["execute", @agent_name && !@router_config],
52
- ["route", @router_config],
53
- ["compute/run", @deterministic_block],
54
- ["fan_out", @fanout_config]
55
- ]
56
- )
63
+ validate_execution_primitive_conflict!("workflow")
57
64
 
58
65
  @workflow_class = klass
66
+ commit_execution_primitive!("workflow")
59
67
  end
60
68
 
61
69
  def optimize(generator:, evaluator:, max_rounds:, evaluator_schema:,
@@ -65,57 +73,78 @@ module Smith
65
73
  on_exhaustion: :raise,
66
74
  on_converged: :raise,
67
75
  on_threshold: :raise)
68
- validate_optimize_conflicts!
76
+ validate_execution_primitive_conflict!("optimize")
69
77
  validate_optimize_controls!(generator, evaluator, max_rounds, evaluator_schema)
70
78
  validate_optimize_exit_modes!(on_exhaustion: on_exhaustion, on_converged: on_converged,
71
79
  on_threshold: on_threshold)
72
80
  validate_optimize_evaluator_context!(evaluator_context)
73
81
  validate_optimize_before_eval!(before_eval)
74
82
 
75
- @optimization_config = {
76
- generator: generator, evaluator: evaluator, max_rounds: max_rounds,
83
+ config = {
84
+ generator: normalize_agent_reference!(generator, "optimizer generator"),
85
+ evaluator: normalize_agent_reference!(evaluator, "optimizer evaluator"),
86
+ max_rounds: max_rounds,
77
87
  evaluator_schema: evaluator_schema, improvement_threshold: improvement_threshold,
78
88
  evaluator_context: evaluator_context,
79
89
  before_eval: before_eval,
80
90
  on_exhaustion: on_exhaustion,
81
91
  on_converged: on_converged,
82
92
  on_threshold: on_threshold
83
- }
93
+ }.freeze
94
+
95
+ @optimization_config = config
96
+ commit_execution_primitive!("optimize")
84
97
  end
85
98
 
86
99
  def orchestrate(**opts)
87
- validate_orchestrate_conflicts!
100
+ validate_execution_primitive_conflict!("orchestrate")
88
101
  validate_orchestrate_controls!(opts)
89
- @orchestrator_config = opts
102
+ config = opts.merge(
103
+ orchestrator: normalize_agent_reference!(opts.fetch(:orchestrator), "orchestrator"),
104
+ worker: normalize_agent_reference!(opts.fetch(:worker), "orchestrator worker")
105
+ ).freeze
106
+
107
+ @orchestrator_config = config
108
+ commit_execution_primitive!("orchestrate")
90
109
  end
91
110
 
92
111
  def fan_out(branches:)
93
- validate_fanout_conflicts!
112
+ validate_execution_primitive_conflict!("fan_out")
94
113
 
95
- @fanout_config = { branches: normalize_fanout_branches!(branches) }
114
+ normalized = normalize_fanout_branches!(branches)
115
+ @fanout_branch_lookup = normalized.to_h do |key, agent|
116
+ [key.to_s.freeze, [key, agent].freeze]
117
+ end.freeze
118
+ config = { branches: normalized }.freeze
119
+
120
+ @fanout_config = config
121
+ commit_execution_primitive!("fan_out")
96
122
  end
97
123
  alias fanout fan_out
98
124
 
99
125
  def retry_on(*error_classes, attempts:, backoff: 0, max_delay: nil, jitter: 0)
100
- validate_retry_controls!(error_classes, attempts:, backoff:, max_delay:, jitter:)
126
+ policy = normalize_retry_policy!(error_classes, attempts:, backoff:, max_delay:, jitter:)
101
127
 
102
128
  @retry_config = {
103
129
  error_classes: error_classes.freeze,
104
- attempts: attempts,
105
- backoff: Float(backoff),
106
- max_delay: max_delay.nil? ? nil : Float(max_delay),
107
- jitter: Float(jitter)
130
+ attempts: policy.attempts,
131
+ backoff: policy.base_delay,
132
+ max_delay: policy.max_delay,
133
+ jitter: policy.jitter
108
134
  }.freeze
109
135
  end
110
136
 
111
137
  %i[compute run].each do |method_name|
112
138
  define_method(method_name) do |routes: nil, &block|
113
- validate_deterministic_conflicts!
139
+ validate_execution_primitive_conflict!("compute/run", declaration: method_name.to_s)
114
140
  raise WorkflowError, "#{method_name} requires a block" unless block
115
141
 
142
+ normalized_routes = normalize_deterministic_routes!(routes)
143
+
116
144
  @deterministic_block = block
117
145
  @deterministic_kind = method_name
118
- @deterministic_routes = normalize_deterministic_routes!(routes)
146
+ @deterministic_routes = normalized_routes
147
+ commit_execution_primitive!("compute/run", declaration: method_name.to_s)
119
148
  end
120
149
  end
121
150
 
@@ -131,6 +160,17 @@ module Smith
131
160
  !@fanout_config.nil?
132
161
  end
133
162
 
163
+ def fetch_fanout_agent!(branch_key)
164
+ fetch_fanout_branch!(branch_key).last
165
+ end
166
+
167
+ def fetch_fanout_branch!(branch_key)
168
+ @fanout_branch_lookup.fetch(branch_key.to_s) do
169
+ raise WorkflowError, "fan_out branch #{branch_key.inspect} is not declared"
170
+ end
171
+ end
172
+ private :fetch_fanout_agent!, :fetch_fanout_branch!
173
+
134
174
  def optimized?
135
175
  !@optimization_config.nil?
136
176
  end
@@ -149,92 +189,41 @@ module Smith
149
189
 
150
190
  private
151
191
 
152
- def validate_execute_conflicts!
153
- validate_conflicts!(
154
- "execute",
155
- [
156
- ["compute/run", @deterministic_block],
157
- ["fan_out", @fanout_config]
158
- ]
159
- )
192
+ def own_identifier(identifier, allow_nil: false)
193
+ Identifier.normalize(identifier, label: "workflow identifier", allow_nil:)
160
194
  end
161
195
 
162
- def validate_route_conflicts!
163
- validate_conflicts!(
164
- "route",
165
- [
166
- ["compute/run", @deterministic_block],
167
- ["fan_out", @fanout_config]
168
- ]
169
- )
196
+ def normalize_agent_reference!(agent_name, label)
197
+ Identifier.normalize(agent_name, label: "#{label} agent")
170
198
  end
171
199
 
172
- def validate_deterministic_conflicts!
173
- validate_conflicts!(
174
- "compute/run",
175
- [
176
- ["execute", @agent_name && !@router_config],
177
- ["route", @router_config],
178
- ["workflow", @workflow_class],
179
- ["optimize", @optimization_config],
180
- ["orchestrate", @orchestrator_config],
181
- ["fan_out", @fanout_config]
182
- ]
183
- )
184
- raise WorkflowError, "transition cannot declare both compute and run" if @deterministic_block
185
- end
186
-
187
- def validate_optimize_conflicts!
188
- validate_conflicts!(
189
- "optimize",
190
- [
191
- ["execute", @agent_name && !@router_config],
192
- ["route", @router_config],
193
- ["workflow", @workflow_class],
194
- ["compute/run", @deterministic_block],
195
- ["fan_out", @fanout_config]
196
- ]
197
- )
198
- end
200
+ def validate_execution_primitive_conflict!(primitive, declaration: primitive)
201
+ return if @execution_primitive.nil?
199
202
 
200
- def validate_orchestrate_conflicts!
201
- validate_conflicts!(
202
- "orchestrate",
203
- [
204
- ["execute", @agent_name && !@router_config],
205
- ["route", @router_config],
206
- ["workflow", @workflow_class],
207
- ["optimize", @optimization_config],
208
- ["compute/run", @deterministic_block],
209
- ["fan_out", @fanout_config]
210
- ]
211
- )
212
- end
203
+ return raise_duplicate_execution_primitive!(declaration) if @execution_primitive == primitive
213
204
 
214
- def validate_fanout_conflicts!
215
- validate_conflicts!(
216
- "fan_out",
217
- [
218
- ["execute", @agent_name && !@router_config],
219
- ["route", @router_config],
220
- ["workflow", @workflow_class],
221
- ["optimize", @optimization_config],
222
- ["orchestrate", @orchestrator_config],
223
- ["compute/run", @deterministic_block]
224
- ]
225
- )
205
+ raise WorkflowError, "transition cannot declare both #{primitive} and #{@execution_primitive}"
226
206
  end
227
207
 
228
- def validate_conflicts!(primitive, conflicts)
229
- conflicts.each do |other, present|
230
- raise WorkflowError, "transition cannot declare both #{primitive} and #{other}" if present
208
+ def raise_duplicate_execution_primitive!(declaration)
209
+ if @execution_primitive == "compute/run" && @execution_declaration != declaration
210
+ raise WorkflowError, "transition cannot declare both compute and run"
231
211
  end
212
+
213
+ raise WorkflowError, "transition cannot declare #{declaration} more than once"
214
+ end
215
+
216
+ def commit_execution_primitive!(primitive, declaration: primitive)
217
+ @execution_primitive = primitive
218
+ @execution_declaration = declaration
232
219
  end
233
220
 
234
221
  def normalize_fanout_branches!(branches)
235
222
  raise WorkflowError, "fan_out branches must be a Hash" unless branches.is_a?(Hash)
236
223
  raise WorkflowError, "fan_out requires at least one branch" if branches.empty?
237
224
 
225
+ validate_fanout_size!(branches)
226
+
238
227
  normalized = branches.each_with_object({}) do |(branch_key, agent_name), map|
239
228
  key = normalize_fanout_branch_key!(branch_key)
240
229
  agent = normalize_fanout_agent_name!(agent_name, key)
@@ -247,11 +236,30 @@ module Smith
247
236
  normalized.freeze
248
237
  end
249
238
 
250
- def normalize_agent_name!(agent_name, label)
251
- value = agent_name.to_s.strip
252
- raise WorkflowError, "#{label} agent must not be blank" if value.empty?
239
+ def validate_fanout_size!(branches)
240
+ limit = Smith.config.parallel_branch_limit
241
+ return if branches.length <= limit
253
242
 
254
- value.to_sym
243
+ raise WorkflowError, "fan_out branch count exceeds configured limit #{limit}"
244
+ end
245
+
246
+ def validate_parallel_options!(options)
247
+ return unless options[:parallel] == true
248
+
249
+ count = options[:count]
250
+ return if count.nil? || count.respond_to?(:call)
251
+ unless count.is_a?(Integer) && count.positive?
252
+ raise WorkflowError, "parallel branch count must be a positive integer"
253
+ end
254
+
255
+ limit = Smith.config.parallel_branch_limit
256
+ return if count <= limit
257
+
258
+ raise WorkflowError, "parallel branch count exceeds configured limit #{limit}"
259
+ end
260
+
261
+ def normalize_agent_name!(agent_name, label)
262
+ normalize_agent_reference!(agent_name, label).to_sym
255
263
  end
256
264
 
257
265
  def normalize_router_routes!(routes)
@@ -275,7 +283,7 @@ module Smith
275
283
 
276
284
  def normalize_confidence_threshold!(threshold)
277
285
  numeric = Float(threshold)
278
- return numeric if numeric >= 0.0 && numeric <= 1.0
286
+ return numeric if numeric.between?(0.0, 1.0)
279
287
 
280
288
  raise WorkflowError, "router confidence_threshold must be a number in 0.0..1.0"
281
289
  rescue TypeError, ArgumentError
@@ -306,10 +314,11 @@ module Smith
306
314
  end
307
315
 
308
316
  def normalize_fanout_agent_name!(agent_name, branch_key)
309
- value = agent_name.to_s.strip
310
- raise WorkflowError, "fan_out branch #{branch_key.inspect} must declare an agent" if value.empty?
317
+ if [String, Symbol].any? { agent_name.is_a?(_1) } && agent_name.to_s.strip.empty?
318
+ raise WorkflowError, "fan_out branch #{branch_key.inspect} must declare an agent"
319
+ end
311
320
 
312
- value.to_sym
321
+ normalize_agent_reference!(agent_name, "fan_out branch #{branch_key.inspect}").to_sym
313
322
  end
314
323
 
315
324
  def validate_distinct_fanout_agents!(branches)
@@ -319,20 +328,22 @@ module Smith
319
328
  raise WorkflowError, "fan_out branch agents must be distinct: #{duplicates.map(&:inspect).join(", ")}"
320
329
  end
321
330
 
322
- def validate_retry_controls!(error_classes, attempts:, backoff:, max_delay:, jitter:)
323
- unless attempts.is_a?(Integer) && attempts.positive?
324
- raise WorkflowError, "retry_on attempts must be a positive integer"
325
- end
326
-
331
+ def normalize_retry_policy!(error_classes, attempts:, backoff:, max_delay:, jitter:)
327
332
  error_classes.each do |error_class|
328
333
  next if error_class.is_a?(Class) && error_class <= StandardError
329
334
 
330
335
  raise WorkflowError, "retry_on error classes must inherit from StandardError"
331
336
  end
332
337
 
333
- validate_non_negative_numeric!(:backoff, backoff)
334
- validate_non_negative_numeric!(:jitter, jitter)
335
- validate_non_negative_numeric!(:max_delay, max_delay) unless max_delay.nil?
338
+ ExponentialBackoff.new(
339
+ attempts:,
340
+ base_delay: backoff,
341
+ max_delay:,
342
+ jitter:,
343
+ delay_label: "backoff"
344
+ )
345
+ rescue ArgumentError => e
346
+ raise WorkflowError, "retry_on #{e.message}"
336
347
  end
337
348
 
338
349
  def normalize_deterministic_routes!(routes)
@@ -340,10 +351,12 @@ module Smith
340
351
  raise WorkflowError, "deterministic routes must be an Array" unless routes.is_a?(Array)
341
352
  raise WorkflowError, "deterministic routes must not be empty" if routes.empty?
342
353
 
354
+ seen = {}
343
355
  routes.each_with_object([]) do |route, list|
344
356
  name = normalize_deterministic_route!(route)
345
- raise WorkflowError, "deterministic route #{name.inspect} is duplicated" if list.include?(name)
357
+ raise WorkflowError, "deterministic route #{name.inspect} is duplicated" if seen.key?(name)
346
358
 
359
+ seen[name] = true
347
360
  list << name
348
361
  end.freeze
349
362
  end
@@ -364,15 +377,6 @@ module Smith
364
377
  end
365
378
  end
366
379
 
367
- def validate_non_negative_numeric!(name, value)
368
- numeric = Float(value)
369
- return if numeric >= 0.0
370
-
371
- raise WorkflowError, "retry_on #{name} must be non-negative"
372
- rescue TypeError, ArgumentError
373
- raise WorkflowError, "retry_on #{name} must be numeric"
374
- end
375
-
376
380
  def validate_orchestrate_controls!(opts)
377
381
  validate_orchestrate_required_fields!(opts)
378
382
  validate_orchestrate_bounds!(opts)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module TransitionActionability
6
+ module_function
7
+
8
+ def call(transition)
9
+ [
10
+ transition.agent_name,
11
+ transition.deterministic?,
12
+ transition.routed?,
13
+ transition.fanout?,
14
+ transition.nested?,
15
+ transition.optimized?,
16
+ transition.orchestrated?,
17
+ transition.success_transition
18
+ ].any?
19
+ end
20
+ end
21
+ end
22
+ end
@@ -9,14 +9,17 @@ require "time"
9
9
  require_relative "workflow/agent_result"
10
10
  require_relative "workflow/branch_env"
11
11
  require_relative "workflow/run_result"
12
+ require_relative "workflow/string_snapshot"
12
13
  require_relative "workflow/usage_entry"
13
14
 
14
15
  module Smith
15
16
  class Workflow
16
17
  include DSL
18
+ include DefinitionIdentity
17
19
  include Persistence
18
20
  include Durability
19
21
  prepend SplitStepPersistence
22
+ include MessageAdmissionBoundary
20
23
  include GuardrailIntegration
21
24
  include BudgetIntegration
22
25
  include EventIntegration
@@ -49,6 +52,9 @@ module Smith
49
52
  "Smith::AgentError" => ->(s) { Smith::AgentError.new(s[:error_message]) },
50
53
  "Smith::DeadlineExceeded" => ->(s) { Smith::DeadlineExceeded.new(s[:error_message]) },
51
54
  "Smith::WorkflowError" => ->(s) { Smith::WorkflowError.new(s[:error_message]) },
55
+ "Smith::Workflow::Composite::BranchFailure" => lambda { |s|
56
+ Smith::Workflow::Composite::BranchFailure.from_details(s[:error_details])
57
+ },
52
58
  # Smith errors with non-message constructors map to compatible
53
59
  # superclass — message preserved, original metadata (agent_name,
54
60
  # model_used, requested_name, workflow_class, origin_state) lossy
@@ -66,7 +72,11 @@ module Smith
66
72
  RETRYABLE_BEARING_FAMILIES = %w[deterministic_step_failure tool_guardrail_failed].freeze
67
73
  private_constant :RETRYABLE_BEARING_FAMILIES
68
74
 
69
- attr_reader :state, :last_prepared_input, :session_messages, :ledger
75
+ attr_reader :state, :last_prepared_input, :ledger
76
+
77
+ def session_messages
78
+ snapshot_session_messages
79
+ end
70
80
 
71
81
  def initialize(context: {}, ledger: nil, created_at: nil)
72
82
  @state = self.class.initial_state
@@ -135,17 +145,19 @@ module Smith
135
145
  return if transition.nil?
136
146
 
137
147
  @step_work_started = true
138
- step_result = execute_step(transition)
148
+ step_result = if advance_claim == :split_step
149
+ Execution.instance_method(:execute_step).bind_call(self, transition)
150
+ else
151
+ execute_step(transition)
152
+ end
139
153
  @step_count += 1
140
154
  @updated_at = Time.now.utc.iso8601
141
155
  record_step_snapshot(step_result)
142
156
  step_result
143
157
  rescue UnresolvedTransitionError => e
144
- origin_state = @state
145
- @outcome = nil
146
- raise unless route_to_fail_state!
147
-
148
- step_result = { transition: e.requested_name, from: origin_state, to: @state, error: e }
158
+ step_result = GuardrailIntegration
159
+ .instance_method(:handle_unresolved_transition_failure)
160
+ .bind_call(self, e)
149
161
  record_step_snapshot(step_result)
150
162
  step_result
151
163
  ensure
@@ -162,8 +174,12 @@ module Smith
162
174
  build_run_result(steps)
163
175
  end
164
176
 
177
+ def pending_transition_name
178
+ @next_transition_name || self.class.first_transition_from(@state)&.name
179
+ end
180
+
165
181
  def terminal?
166
- self.class.transitions_from(@state).empty? && @next_transition_name.nil?
182
+ !self.class.transition_from?(@state) && @next_transition_name.nil?
167
183
  end
168
184
 
169
185
  def done?
@@ -245,14 +261,6 @@ module Smith
245
261
  Budget::Ledger.new(limits: config)
246
262
  end
247
263
 
248
- def route_to_fail_state!
249
- fail_transition = self.class.find_transition(:fail)
250
- return false unless fail_transition
251
-
252
- @state = fail_transition.to
253
- true
254
- end
255
-
256
264
  def ensure_transition_budget!
257
265
  max = self.class.max_transitions || DEFAULT_MAX_TRANSITIONS
258
266
  raise MaxTransitionsExceeded if @step_count >= max
@@ -267,7 +275,7 @@ module Smith
267
275
  validate_transition_origin!(transition)
268
276
  transition
269
277
  else
270
- self.class.transitions_from(@state).first
278
+ self.class.first_transition_from(@state)
271
279
  end
272
280
  end
273
281
 
@@ -460,7 +468,7 @@ module Smith
460
468
  when Array
461
469
  value.map { |nested| snapshot_value(nested) }
462
470
  when String
463
- value.dup
471
+ StringSnapshot.copy(value)
464
472
  else
465
473
  value.dup
466
474
  end
@@ -469,3 +477,41 @@ module Smith
469
477
  end
470
478
  end
471
479
  end
480
+
481
+ Smith::Workflow::SplitStepPersistence::SubclassBoundary.protect_execution_path!(
482
+ Smith::Workflow,
483
+ Smith::Workflow::Execution,
484
+ Smith::Workflow::ExecutionBindingResolution,
485
+ Smith::Workflow::StepCompletion,
486
+ Smith::Agent::Lifecycle,
487
+ Smith::Workflow::NestedExecution,
488
+ Smith::Workflow::EvaluatorOptimizer,
489
+ Smith::Workflow::OrchestratorWorker,
490
+ Smith::Workflow::ParallelExecution,
491
+ Smith::Workflow::FanoutExecution,
492
+ Smith::Workflow::RetryExecution,
493
+ Smith::Workflow::DeterministicExecution,
494
+ Smith::Workflow::GuardrailIntegration,
495
+ Smith::Workflow::BudgetIntegration,
496
+ Smith::Workflow::EventIntegration,
497
+ Smith::Workflow::ArtifactIntegration,
498
+ Smith::Workflow::DataVolumePolicy,
499
+ Smith::Workflow::DeadlineEnforcement,
500
+ Smith::Workflow::SplitStepPersistence::DefinitionBoundary,
501
+ Smith::Workflow::SplitStepPersistence::ExecutionVerification,
502
+ Smith::Workflow::SplitStepPersistence::ExecutionAuthorization,
503
+ Smith::Workflow::SplitStepPersistence::ExecutionAuthorizationIssuance,
504
+ Smith::Workflow::SplitStepPersistence::ExecutionLifecycle,
505
+ Smith::Workflow::Composite::Contract,
506
+ Smith::Workflow::Composite::BranchContract,
507
+ Smith::Workflow::Composite::FanoutBranchContract,
508
+ Smith::Workflow::Composite::BranchBudgetContract,
509
+ Smith::Workflow::Composite::ExecutionContract,
510
+ Smith::Workflow::SplitStepPersistence::CompositeBranchEffects,
511
+ Smith::Workflow::SplitStepPersistence::CompositeBranchOutcome,
512
+ Smith::Workflow::SplitStepPersistence::CompositePreparation,
513
+ Smith::Workflow::SplitStepPersistence::CompositeBranchAuthorization,
514
+ Smith::Workflow::SplitStepPersistence::CompositeBranchExecution,
515
+ Smith::Workflow::SplitStepPersistence::CompositeReductionExecution,
516
+ Smith::Workflow::SplitStepPersistence::CompositeExecution
517
+ )
data/lib/smith.rb CHANGED
@@ -3,12 +3,32 @@
3
3
  require "dry-configurable"
4
4
 
5
5
  require_relative "smith/version"
6
+ require_relative "smith/error"
6
7
 
7
8
  module Smith
8
- class Error < StandardError; end
9
-
10
9
  extend Dry::Configurable
11
10
 
11
+ positive_integer_setting = lambda do |name|
12
+ lambda do |value|
13
+ unless value.is_a?(Integer) && value.positive?
14
+ raise ArgumentError, "Smith.config.#{name} must be a positive integer, got #{value.inspect}"
15
+ end
16
+
17
+ value
18
+ end
19
+ end
20
+
21
+ bounded_positive_integer_setting = lambda do |name, maximum|
22
+ lambda do |value|
23
+ unless value.is_a?(Integer) && value.positive? && value <= maximum
24
+ message = "Smith.config.#{name} must be a positive integer no greater than #{maximum}"
25
+ raise ArgumentError, "#{message}, got #{value.inspect}"
26
+ end
27
+
28
+ value
29
+ end
30
+ end
31
+
12
32
  # Artifact store (§4.7)
13
33
  setting :artifact_store
14
34
  setting :artifact_retention
@@ -46,6 +66,15 @@ module Smith
46
66
  # max_delay: cap on per-retry sleep
47
67
  setting :persistence_retry_policy, default: { attempts: 3, base_delay: 0.1, max_delay: 1.0 }
48
68
 
69
+ # Generic runtime resource bounds. Branch concurrency is scoped to one
70
+ # parallel execution; hosts retain control over process-level concurrency.
71
+ setting :parallel_branch_limit, default: 1_000, constructor: positive_integer_setting.call(:parallel_branch_limit)
72
+ setting :parallel_concurrency, default: 8, constructor: positive_integer_setting.call(:parallel_concurrency)
73
+ setting :parallel_nesting_limit, default: 64, constructor: bounded_positive_integer_setting.call(
74
+ :parallel_nesting_limit, 256
75
+ )
76
+ setting :retry_attempt_limit, default: 100, constructor: positive_integer_setting.call(:retry_attempt_limit)
77
+
49
78
  # Test isolation: when true AND persistence_adapter is nil, Smith
50
79
  # auto-selects the in-process Memory adapter. Lets specs avoid wiring
51
80
  # Redis/Rails.cache in spec_helper.rb.
@@ -148,6 +177,8 @@ end
148
177
  # Leaf modules (no internal dependencies)
149
178
  require_relative "smith/types"
150
179
  require_relative "smith/errors"
180
+ require_relative "smith/persistence_payload_conflict"
181
+ require_relative "smith/exponential_backoff"
151
182
 
152
183
  # Event system (depends on Types)
153
184
  require_relative "smith/event"
@@ -228,17 +259,28 @@ require_relative "smith/agent/registry_binding"
228
259
  require_relative "smith/agent/registry"
229
260
 
230
261
  # Workflow (Transition, DSL, Persistence, and Execution must load before Workflow)
262
+ require_relative "smith/workflow/identifier"
231
263
  require_relative "smith/workflow/transition"
264
+ require_relative "smith/workflow/transition_actionability"
232
265
  require_relative "smith/workflow/graph"
233
266
  require_relative "smith/workflow/graph_dsl"
234
267
  require_relative "smith/workflow/run_result"
235
268
  require_relative "smith/workflow/agent_result"
236
269
  require_relative "smith/workflow/usage_entry"
270
+ require_relative "smith/workflow/process_local"
271
+ require_relative "smith/workflow/thread_context_snapshot"
237
272
  require_relative "smith/workflow/branch_env"
238
273
  require_relative "smith/workflow/optimization_state"
239
274
  require_relative "smith/workflow/orchestration_state"
240
275
  require_relative "smith/workflow/worker_execution"
241
276
  require_relative "smith/workflow/dsl"
277
+ require_relative "smith/workflow/definition_identity"
278
+ require_relative "smith/workflow/prepared_step_dispatch"
279
+ require_relative "smith/workflow/prepared_branch_execution"
280
+ require_relative "smith/workflow/message_value_normalizer"
281
+ require_relative "smith/workflow/message_admission"
282
+ require_relative "smith/workflow/message_batch"
283
+ require_relative "smith/workflow/message_admission_boundary"
242
284
  require_relative "smith/workflow/persistence"
243
285
  require_relative "smith/workflow/durability"
244
286
  require_relative "smith/workflow/split_step_persistence"