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
@@ -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,74 @@ 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")
113
+
114
+ config = { branches: normalize_fanout_branches!(branches) }.freeze
94
115
 
95
- @fanout_config = { branches: normalize_fanout_branches!(branches) }
116
+ @fanout_config = config
117
+ commit_execution_primitive!("fan_out")
96
118
  end
97
119
  alias fanout fan_out
98
120
 
99
121
  def retry_on(*error_classes, attempts:, backoff: 0, max_delay: nil, jitter: 0)
100
- validate_retry_controls!(error_classes, attempts:, backoff:, max_delay:, jitter:)
122
+ policy = normalize_retry_policy!(error_classes, attempts:, backoff:, max_delay:, jitter:)
101
123
 
102
124
  @retry_config = {
103
125
  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)
126
+ attempts: policy.attempts,
127
+ backoff: policy.base_delay,
128
+ max_delay: policy.max_delay,
129
+ jitter: policy.jitter
108
130
  }.freeze
109
131
  end
110
132
 
111
133
  %i[compute run].each do |method_name|
112
134
  define_method(method_name) do |routes: nil, &block|
113
- validate_deterministic_conflicts!
135
+ validate_execution_primitive_conflict!("compute/run", declaration: method_name.to_s)
114
136
  raise WorkflowError, "#{method_name} requires a block" unless block
115
137
 
138
+ normalized_routes = normalize_deterministic_routes!(routes)
139
+
116
140
  @deterministic_block = block
117
141
  @deterministic_kind = method_name
118
- @deterministic_routes = normalize_deterministic_routes!(routes)
142
+ @deterministic_routes = normalized_routes
143
+ commit_execution_primitive!("compute/run", declaration: method_name.to_s)
119
144
  end
120
145
  end
121
146
 
@@ -149,92 +174,41 @@ module Smith
149
174
 
150
175
  private
151
176
 
152
- def validate_execute_conflicts!
153
- validate_conflicts!(
154
- "execute",
155
- [
156
- ["compute/run", @deterministic_block],
157
- ["fan_out", @fanout_config]
158
- ]
159
- )
177
+ def own_identifier(identifier, allow_nil: false)
178
+ Identifier.normalize(identifier, label: "workflow identifier", allow_nil:)
160
179
  end
161
180
 
162
- def validate_route_conflicts!
163
- validate_conflicts!(
164
- "route",
165
- [
166
- ["compute/run", @deterministic_block],
167
- ["fan_out", @fanout_config]
168
- ]
169
- )
181
+ def normalize_agent_reference!(agent_name, label)
182
+ Identifier.normalize(agent_name, label: "#{label} agent")
170
183
  end
171
184
 
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
185
+ def validate_execution_primitive_conflict!(primitive, declaration: primitive)
186
+ return if @execution_primitive.nil?
199
187
 
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
188
+ return raise_duplicate_execution_primitive!(declaration) if @execution_primitive == primitive
213
189
 
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
- )
190
+ raise WorkflowError, "transition cannot declare both #{primitive} and #{@execution_primitive}"
226
191
  end
227
192
 
228
- def validate_conflicts!(primitive, conflicts)
229
- conflicts.each do |other, present|
230
- raise WorkflowError, "transition cannot declare both #{primitive} and #{other}" if present
193
+ def raise_duplicate_execution_primitive!(declaration)
194
+ if @execution_primitive == "compute/run" && @execution_declaration != declaration
195
+ raise WorkflowError, "transition cannot declare both compute and run"
231
196
  end
197
+
198
+ raise WorkflowError, "transition cannot declare #{declaration} more than once"
199
+ end
200
+
201
+ def commit_execution_primitive!(primitive, declaration: primitive)
202
+ @execution_primitive = primitive
203
+ @execution_declaration = declaration
232
204
  end
233
205
 
234
206
  def normalize_fanout_branches!(branches)
235
207
  raise WorkflowError, "fan_out branches must be a Hash" unless branches.is_a?(Hash)
236
208
  raise WorkflowError, "fan_out requires at least one branch" if branches.empty?
237
209
 
210
+ validate_fanout_size!(branches)
211
+
238
212
  normalized = branches.each_with_object({}) do |(branch_key, agent_name), map|
239
213
  key = normalize_fanout_branch_key!(branch_key)
240
214
  agent = normalize_fanout_agent_name!(agent_name, key)
@@ -247,11 +221,30 @@ module Smith
247
221
  normalized.freeze
248
222
  end
249
223
 
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?
224
+ def validate_fanout_size!(branches)
225
+ limit = Smith.config.parallel_branch_limit
226
+ return if branches.length <= limit
253
227
 
254
- value.to_sym
228
+ raise WorkflowError, "fan_out branch count exceeds configured limit #{limit}"
229
+ end
230
+
231
+ def validate_parallel_options!(options)
232
+ return unless options[:parallel] == true
233
+
234
+ count = options[:count]
235
+ return if count.nil? || count.respond_to?(:call)
236
+ unless count.is_a?(Integer) && count.positive?
237
+ raise WorkflowError, "parallel branch count must be a positive integer"
238
+ end
239
+
240
+ limit = Smith.config.parallel_branch_limit
241
+ return if count <= limit
242
+
243
+ raise WorkflowError, "parallel branch count exceeds configured limit #{limit}"
244
+ end
245
+
246
+ def normalize_agent_name!(agent_name, label)
247
+ normalize_agent_reference!(agent_name, label).to_sym
255
248
  end
256
249
 
257
250
  def normalize_router_routes!(routes)
@@ -275,7 +268,7 @@ module Smith
275
268
 
276
269
  def normalize_confidence_threshold!(threshold)
277
270
  numeric = Float(threshold)
278
- return numeric if numeric >= 0.0 && numeric <= 1.0
271
+ return numeric if numeric.between?(0.0, 1.0)
279
272
 
280
273
  raise WorkflowError, "router confidence_threshold must be a number in 0.0..1.0"
281
274
  rescue TypeError, ArgumentError
@@ -306,10 +299,11 @@ module Smith
306
299
  end
307
300
 
308
301
  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?
302
+ if [String, Symbol].any? { agent_name.is_a?(_1) } && agent_name.to_s.strip.empty?
303
+ raise WorkflowError, "fan_out branch #{branch_key.inspect} must declare an agent"
304
+ end
311
305
 
312
- value.to_sym
306
+ normalize_agent_reference!(agent_name, "fan_out branch #{branch_key.inspect}").to_sym
313
307
  end
314
308
 
315
309
  def validate_distinct_fanout_agents!(branches)
@@ -319,20 +313,22 @@ module Smith
319
313
  raise WorkflowError, "fan_out branch agents must be distinct: #{duplicates.map(&:inspect).join(", ")}"
320
314
  end
321
315
 
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
-
316
+ def normalize_retry_policy!(error_classes, attempts:, backoff:, max_delay:, jitter:)
327
317
  error_classes.each do |error_class|
328
318
  next if error_class.is_a?(Class) && error_class <= StandardError
329
319
 
330
320
  raise WorkflowError, "retry_on error classes must inherit from StandardError"
331
321
  end
332
322
 
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?
323
+ ExponentialBackoff.new(
324
+ attempts:,
325
+ base_delay: backoff,
326
+ max_delay:,
327
+ jitter:,
328
+ delay_label: "backoff"
329
+ )
330
+ rescue ArgumentError => e
331
+ raise WorkflowError, "retry_on #{e.message}"
336
332
  end
337
333
 
338
334
  def normalize_deterministic_routes!(routes)
@@ -340,10 +336,12 @@ module Smith
340
336
  raise WorkflowError, "deterministic routes must be an Array" unless routes.is_a?(Array)
341
337
  raise WorkflowError, "deterministic routes must not be empty" if routes.empty?
342
338
 
339
+ seen = {}
343
340
  routes.each_with_object([]) do |route, list|
344
341
  name = normalize_deterministic_route!(route)
345
- raise WorkflowError, "deterministic route #{name.inspect} is duplicated" if list.include?(name)
342
+ raise WorkflowError, "deterministic route #{name.inspect} is duplicated" if seen.key?(name)
346
343
 
344
+ seen[name] = true
347
345
  list << name
348
346
  end.freeze
349
347
  end
@@ -364,15 +362,6 @@ module Smith
364
362
  end
365
363
  end
366
364
 
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
365
  def validate_orchestrate_controls!(opts)
377
366
  validate_orchestrate_required_fields!(opts)
378
367
  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
@@ -66,7 +69,11 @@ module Smith
66
69
  RETRYABLE_BEARING_FAMILIES = %w[deterministic_step_failure tool_guardrail_failed].freeze
67
70
  private_constant :RETRYABLE_BEARING_FAMILIES
68
71
 
69
- attr_reader :state, :last_prepared_input, :session_messages, :ledger
72
+ attr_reader :state, :last_prepared_input, :ledger
73
+
74
+ def session_messages
75
+ snapshot_session_messages
76
+ end
70
77
 
71
78
  def initialize(context: {}, ledger: nil, created_at: nil)
72
79
  @state = self.class.initial_state
@@ -135,17 +142,19 @@ module Smith
135
142
  return if transition.nil?
136
143
 
137
144
  @step_work_started = true
138
- step_result = execute_step(transition)
145
+ step_result = if advance_claim == :split_step
146
+ Execution.instance_method(:execute_step).bind_call(self, transition)
147
+ else
148
+ execute_step(transition)
149
+ end
139
150
  @step_count += 1
140
151
  @updated_at = Time.now.utc.iso8601
141
152
  record_step_snapshot(step_result)
142
153
  step_result
143
154
  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 }
155
+ step_result = GuardrailIntegration
156
+ .instance_method(:handle_unresolved_transition_failure)
157
+ .bind_call(self, e)
149
158
  record_step_snapshot(step_result)
150
159
  step_result
151
160
  ensure
@@ -162,8 +171,12 @@ module Smith
162
171
  build_run_result(steps)
163
172
  end
164
173
 
174
+ def pending_transition_name
175
+ @next_transition_name || self.class.first_transition_from(@state)&.name
176
+ end
177
+
165
178
  def terminal?
166
- self.class.transitions_from(@state).empty? && @next_transition_name.nil?
179
+ !self.class.transition_from?(@state) && @next_transition_name.nil?
167
180
  end
168
181
 
169
182
  def done?
@@ -245,14 +258,6 @@ module Smith
245
258
  Budget::Ledger.new(limits: config)
246
259
  end
247
260
 
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
261
  def ensure_transition_budget!
257
262
  max = self.class.max_transitions || DEFAULT_MAX_TRANSITIONS
258
263
  raise MaxTransitionsExceeded if @step_count >= max
@@ -267,7 +272,7 @@ module Smith
267
272
  validate_transition_origin!(transition)
268
273
  transition
269
274
  else
270
- self.class.transitions_from(@state).first
275
+ self.class.first_transition_from(@state)
271
276
  end
272
277
  end
273
278
 
@@ -460,7 +465,7 @@ module Smith
460
465
  when Array
461
466
  value.map { |nested| snapshot_value(nested) }
462
467
  when String
463
- value.dup
468
+ StringSnapshot.copy(value)
464
469
  else
465
470
  value.dup
466
471
  end
@@ -469,3 +474,24 @@ module Smith
469
474
  end
470
475
  end
471
476
  end
477
+
478
+ Smith::Workflow::SplitStepPersistence::SubclassBoundary.protect_execution_path!(
479
+ Smith::Workflow,
480
+ Smith::Workflow::Execution,
481
+ Smith::Workflow::ExecutionBindingResolution,
482
+ Smith::Workflow::StepCompletion,
483
+ Smith::Agent::Lifecycle,
484
+ Smith::Workflow::NestedExecution,
485
+ Smith::Workflow::EvaluatorOptimizer,
486
+ Smith::Workflow::OrchestratorWorker,
487
+ Smith::Workflow::ParallelExecution,
488
+ Smith::Workflow::FanoutExecution,
489
+ Smith::Workflow::RetryExecution,
490
+ Smith::Workflow::DeterministicExecution,
491
+ Smith::Workflow::GuardrailIntegration,
492
+ Smith::Workflow::BudgetIntegration,
493
+ Smith::Workflow::EventIntegration,
494
+ Smith::Workflow::ArtifactIntegration,
495
+ Smith::Workflow::DataVolumePolicy,
496
+ Smith::Workflow::DeadlineEnforcement
497
+ )
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,7 +259,9 @@ 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"
@@ -239,6 +272,12 @@ require_relative "smith/workflow/optimization_state"
239
272
  require_relative "smith/workflow/orchestration_state"
240
273
  require_relative "smith/workflow/worker_execution"
241
274
  require_relative "smith/workflow/dsl"
275
+ require_relative "smith/workflow/definition_identity"
276
+ require_relative "smith/workflow/prepared_step_dispatch"
277
+ require_relative "smith/workflow/message_value_normalizer"
278
+ require_relative "smith/workflow/message_admission"
279
+ require_relative "smith/workflow/message_batch"
280
+ require_relative "smith/workflow/message_admission_boundary"
242
281
  require_relative "smith/workflow/persistence"
243
282
  require_relative "smith/workflow/durability"
244
283
  require_relative "smith/workflow/split_step_persistence"
data/sig/smith.rbs CHANGED
@@ -1,4 +1,5 @@
1
1
  module Smith
2
2
  VERSION: String
3
+ EXECUTION_SEMANTICS_VERSION: String
3
4
  # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
5
  end