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
@@ -12,6 +12,12 @@ module Smith
12
12
  super
13
13
  subclass.instance_variable_set(:@states, (@states || []).dup)
14
14
  subclass.instance_variable_set(:@transitions, (@transitions || {}).dup)
15
+ subclass.instance_variable_set(
16
+ :@transitions_by_state,
17
+ duplicate_transition_index(@transitions_by_state)
18
+ )
19
+ subclass.instance_variable_set(:@transition_order, (@transition_order || {}).dup)
20
+ subclass.instance_variable_set(:@transition_sequence, @transition_sequence)
15
21
  subclass.instance_variable_set(:@initial_state_name, @initial_state_name)
16
22
  subclass.instance_variable_set(:@budget_config, @budget_config&.dup)
17
23
  subclass.instance_variable_set(:@max_transitions_count, @max_transitions_count)
@@ -29,19 +35,24 @@ module Smith
29
35
  def initial_state(name = nil)
30
36
  return @initial_state_name if name.nil?
31
37
 
32
- @initial_state_name = name
33
- state(name)
38
+ @initial_state_name = own_identifier(name)
39
+ state(@initial_state_name)
34
40
  end
35
41
 
36
42
  def state(name)
43
+ name = own_identifier(name)
37
44
  @states ||= []
38
45
  @states << name unless @states.include?(name)
39
46
  generate_fail_transition if name == :failed
40
47
  end
41
48
 
42
49
  def transition(name, from:, to:, &)
50
+ declared = Transition.new(name, from: from, to: to, &)
51
+ name = declared.name
43
52
  @transitions ||= {}
44
- @transitions[name] = Transition.new(name, from: from, to: to, &)
53
+ remove_from_transition_index(@transitions[name]) if @transitions.key?(name)
54
+ @transitions[name] = declared
55
+ insert_into_transition_index(declared)
45
56
  end
46
57
 
47
58
  def budget(**opts)
@@ -196,13 +207,25 @@ module Smith
196
207
  end
197
208
 
198
209
  def transitions_from(state)
199
- (@transitions || {}).values.select { |t| t.from == state }
210
+ transitions_by_state.fetch(state, []).dup
211
+ end
212
+
213
+ def first_transition_from(state)
214
+ transitions_by_state.fetch(state, []).first
215
+ end
216
+
217
+ def transition_from?(state)
218
+ transitions_by_state.fetch(state, []).any?
200
219
  end
201
220
 
202
221
  def find_transition(name)
203
222
  (@transitions || {})[name]
204
223
  end
205
224
 
225
+ def transition_at(index)
226
+ (@transitions || {}).values.fetch(index)
227
+ end
228
+
206
229
  def from_state(hash)
207
230
  workflow = allocate
208
231
  workflow.send(:restore_state, hash)
@@ -211,11 +234,56 @@ module Smith
211
234
 
212
235
  private
213
236
 
237
+ def own_identifier(identifier)
238
+ Identifier.normalize(identifier, label: "workflow identifier")
239
+ end
240
+
241
+ def transitions_by_state
242
+ @transitions_by_state ||= Hash.new { |index, state| index[state] = [] }
243
+ end
244
+
245
+ def transition_order
246
+ @transition_order ||= {}
247
+ end
248
+
249
+ def transition_sequence
250
+ @transition_sequence ||= 0
251
+ end
252
+
253
+ def insert_into_transition_index(transition)
254
+ order = transition_order_for(transition.name)
255
+ indexed = transitions_by_state[transition.from]
256
+ insertion = indexed.bsearch_index { |candidate| transition_order.fetch(candidate.name) > order }
257
+ indexed.insert(insertion || indexed.length, transition)
258
+ end
259
+
260
+ def transition_order_for(name)
261
+ return transition_order.fetch(name) if transition_order.key?(name)
262
+
263
+ transition_order[name] = transition_sequence
264
+ @transition_sequence = transition_sequence + 1
265
+ transition_order.fetch(name)
266
+ end
267
+
268
+ def duplicate_transition_index(source)
269
+ source.to_h.each_with_object(Hash.new { |index, state| index[state] = [] }) do |(state, transitions), copy|
270
+ copy[state] = transitions.dup
271
+ end
272
+ end
273
+
274
+ def remove_from_transition_index(transition)
275
+ return unless transition
276
+
277
+ indexed = transitions_by_state[transition.from]
278
+ indexed.delete(transition)
279
+ transitions_by_state.delete(transition.from) if indexed.empty?
280
+ end
281
+
214
282
  def generate_fail_transition
215
283
  @transitions ||= {}
216
284
  return if @transitions.key?(:fail)
217
285
 
218
- @transitions[:fail] = Transition.new(:fail, from: nil, to: :failed)
286
+ transition(:fail, from: nil, to: :failed)
219
287
  end
220
288
  end
221
289
  end
@@ -164,7 +164,7 @@ module Smith
164
164
 
165
165
  klass = Object.const_get(class_name)
166
166
  state = state_name_for_payload(klass, state_name)
167
- klass.transitions_from(state).empty? && next_transition.nil?
167
+ !klass.transition_from?(state) && next_transition.nil?
168
168
  rescue JSON::ParserError, NameError, NoMethodError
169
169
  false
170
170
  end
@@ -12,13 +12,13 @@ module Smith
12
12
 
13
13
  def execute_optimization_step(transition, prepared_input: nil)
14
14
  state = OptimizationState.new(transition.optimization_config, prepared_input)
15
- state.generator_class = Agent::Registry.fetch!(
15
+ state.generator_class = resolve_registered_agent!(
16
16
  state.config[:generator],
17
17
  workflow_class: self.class,
18
18
  transition_name: transition.name,
19
19
  role: :generator
20
20
  )
21
- state.evaluator_class = Agent::Registry.fetch!(
21
+ state.evaluator_class = resolve_registered_agent!(
22
22
  state.config[:evaluator],
23
23
  workflow_class: self.class,
24
24
  transition_name: transition.name,
@@ -143,28 +143,24 @@ module Smith
143
143
  end
144
144
 
145
145
  def invoke_with_evaluator_schema(evaluator_class, schema, input)
146
- original_schema = evaluator_class.output_schema
147
- evaluator_class.output_schema(schema)
148
- invoke_agent_with_budget(evaluator_class, input)
149
- ensure
150
- evaluator_class.output_schema(original_schema)
146
+ invoke_agent_with_budget(evaluator_class, input, output_schema: schema)
151
147
  end
152
148
 
153
- def invoke_agent_with_budget(agent_class, prepared_input)
149
+ def invoke_agent_with_budget(agent_class, prepared_input, output_schema: agent_class.output_schema)
154
150
  Thread.current[:smith_last_agent_result] = nil
155
151
  clear_failed_billable_attempts
156
152
  with_agent_context(agent_class) do
157
- invoke_with_call_ledger(agent_class, prepared_input)
153
+ invoke_with_call_ledger(agent_class, prepared_input, output_schema:)
158
154
  end
159
155
  ensure
160
156
  clear_failed_billable_attempts
161
157
  end
162
158
 
163
- def invoke_with_call_ledger(agent_class, prepared_input)
159
+ def invoke_with_call_ledger(agent_class, prepared_input, output_schema:)
164
160
  ledger = effective_call_ledger
165
161
  reserved = reserve_serial_budget(ledger, agent_budget: agent_class&.budget)
166
162
  begin
167
- result = invoke_agent(agent_class, prepared_input)
163
+ result = invoke_agent(agent_class, prepared_input, output_schema:)
168
164
  agent_result = result.is_a?(AgentResult) ? result : nil
169
165
  reconcile_branch_budget(ledger, reserved, agent_result: agent_result)
170
166
  reserved = nil
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "agent_result"
4
+ require_relative "execution_binding_resolution"
5
+ require_relative "prepared_branch_execution"
6
+ require_relative "step_completion"
7
+ require_relative "step_context"
4
8
 
5
9
  module Smith
6
10
  class Workflow
7
11
  module Execution
12
+ include ExecutionBindingResolution
13
+ include PreparedBranchExecution
14
+ include StepCompletion
15
+ include StepContext
8
16
  include Agent::Lifecycle
9
17
  include NestedExecution
10
18
  include EvaluatorOptimizer
@@ -17,15 +25,12 @@ module Smith
17
25
  private
18
26
 
19
27
  def execute_step(transition)
20
- setup_step_context
28
+ with_step_context(transition) { execute_step_body(transition) }
29
+ end
30
+
31
+ def execute_step_body(transition)
21
32
  output = with_scoped_artifacts { run_with_retry_policy(transition) }
22
- complete_step(transition, output)
23
- rescue StandardError => e
24
- @outcome = nil
25
- handle_step_failure(transition, e)
26
- { transition: transition.name, from: transition.from, to: transition.to, error: e }
27
- ensure
28
- teardown_step_context
33
+ StepCompletion.instance_method(:complete_step).bind_call(self, transition, output)
29
34
  end
30
35
 
31
36
  def setup_step_context
@@ -34,15 +39,16 @@ module Smith
34
39
  Tool.current_tool_result_collector = tool_result_collector
35
40
  end
36
41
 
37
- def teardown_step_context
38
- Tool.current_guardrails = nil
39
- Tool.current_deadline = nil
40
- Tool.current_ledger = nil
41
- Tool.current_tool_result_collector = nil
42
- Smith.scoped_artifacts = nil
42
+ def run_guarded_step(transition)
43
+ return apply_composite_reduction!(transition) if @composite_reduction
44
+
45
+ @resolved_parallel_branch_count = preflight_branch_count(transition)
46
+ run_standard_guarded_step(transition)
47
+ ensure
48
+ @resolved_parallel_branch_count = nil
43
49
  end
44
50
 
45
- def run_guarded_step(transition)
51
+ def run_standard_guarded_step(transition)
46
52
  return dispatch_step(transition) if transition.deterministic?
47
53
  return run_guarded_fanout_step(transition) if transition.fanout?
48
54
 
@@ -60,20 +66,10 @@ module Smith
60
66
  resolve_router_output(transition, output)
61
67
  end
62
68
 
63
- def complete_step(transition, output)
64
- @state = transition.to
65
- @next_transition_name = @router_next_transition || transition.success_transition
66
- @router_next_transition = nil
67
- append_accepted_output(output)
68
- emit_step_completed(transition, output)
69
- { transition: transition.name, from: transition.from, to: transition.to, output: output }
70
- end
69
+ def preflight_branch_count(transition)
70
+ return Parallel.validate_branch_count!(transition.fanout_config.fetch(:branches).length) if transition.fanout?
71
71
 
72
- def append_accepted_output(output)
73
- return unless @session_messages
74
- return if output.nil?
75
-
76
- @session_messages << { role: :assistant, content: output }
72
+ Parallel.resolve_branch_count(transition, @context) if transition.parallel?
77
73
  end
78
74
 
79
75
  def resolve_router_output(transition, output)
@@ -120,9 +116,11 @@ module Smith
120
116
  end
121
117
 
122
118
  def resolve_agent_class(transition)
123
- return nil unless transition.agent_name
119
+ parallel_binding = Thread.current[:smith_parallel_agent_binding]
120
+ parallel_agent_class = parallel_binding&.resolve(workflow: self, transition:)
121
+ return parallel_agent_class if parallel_agent_class
124
122
 
125
- Agent::Registry.fetch!(
123
+ transition.agent_name && resolve_registered_agent!(
126
124
  transition.agent_name,
127
125
  workflow_class: self.class,
128
126
  transition_name: transition.name,
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ module ExecutionBindingResolution
6
+ private
7
+
8
+ def resolve_registered_agent!(name, workflow_class:, transition_name:, role:)
9
+ authorization = @split_step_active_execution_authorization
10
+ return authorization.fetch_agent!(name, workflow_class:, transition_name:, role:) if authorization
11
+
12
+ Agent::Registry.fetch!(name, workflow_class:, transition_name:, role:)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ require_relative "../errors"
6
+ require_relative "string_snapshot"
7
+
8
+ module Smith
9
+ class Workflow
10
+ class ExecutionResultSnapshot
11
+ MAX_DEPTH = 128
12
+ MAX_NODES = 100_000
13
+ MAX_BYTES = 4 * 1024 * 1024
14
+
15
+ extend Dry::Initializer
16
+
17
+ param :value
18
+ option :freeze_copy, default: proc { true }
19
+ option :preserved_error, default: proc {}
20
+
21
+ def call
22
+ reset
23
+ root = copy_value(value, depth: 0, preserve_error: true)
24
+ drain
25
+ @containers.each(&:freeze) if freeze_copy
26
+ root
27
+ end
28
+
29
+ private
30
+
31
+ def reset
32
+ @bytes = 0
33
+ @nodes = 0
34
+ @copies = {}.compare_by_identity
35
+ @containers = []
36
+ @pending = []
37
+ end
38
+
39
+ def drain
40
+ until @pending.empty?
41
+ source, copy, depth, preserve_error = @pending.pop
42
+ source.is_a?(Hash) ? copy_hash(source, copy, depth, preserve_error) : copy_array(source, copy, depth)
43
+ end
44
+ end
45
+
46
+ def copy_value(item, depth:, preserve_error: false)
47
+ visit!(depth)
48
+ return @copies.fetch(item) if @copies.key?(item)
49
+
50
+ case item
51
+ when Hash then prepare_container(item, {}, depth, preserve_error)
52
+ when Array then prepare_container(item, [], depth, false)
53
+ when String then copy_string(item)
54
+ when Float then copy_float(item)
55
+ when Symbol, Integer, true, false, nil then item
56
+ else
57
+ raise WorkflowError,
58
+ "prepared-step execution result contains unsupported mutable value #{item.class}"
59
+ end
60
+ end
61
+
62
+ def prepare_container(source, copy, depth, preserve_error)
63
+ @copies[source] = copy
64
+ @containers << copy
65
+ @pending << [source, copy, depth, preserve_error]
66
+ copy
67
+ end
68
+
69
+ def copy_hash(source, copy, depth, preserve_error)
70
+ source.each do |key, item|
71
+ copied_key = copy_hash_key(key, depth + 1)
72
+ copied_item = preserved_error?(copied_key, item, preserve_error) ? item : copy_value(item, depth: depth + 1)
73
+ copy[copied_key] = copied_item
74
+ end
75
+ end
76
+
77
+ def copy_hash_key(key, depth)
78
+ unless key.is_a?(String) || key.is_a?(Symbol)
79
+ raise WorkflowError,
80
+ "prepared-step execution result contains unsupported Hash key #{key.class}"
81
+ end
82
+
83
+ copy_value(key, depth: depth)
84
+ end
85
+
86
+ def copy_array(source, copy, depth)
87
+ source.each { |item| copy << copy_value(item, depth: depth + 1) }
88
+ end
89
+
90
+ def preserved_error?(key, item, permitted)
91
+ permitted && key == :error && item.equal?(preserved_error)
92
+ end
93
+
94
+ def copy_string(string)
95
+ @bytes += StringSnapshot.bytesize(string)
96
+ raise WorkflowError, "prepared-step execution result exceeds maximum bytes #{MAX_BYTES}" if
97
+ @bytes > MAX_BYTES
98
+
99
+ StringSnapshot.copy(string, freeze: freeze_copy)
100
+ end
101
+
102
+ def copy_float(float)
103
+ return float if float.finite?
104
+
105
+ raise WorkflowError, "prepared-step execution result contains non-finite Float"
106
+ end
107
+
108
+ def visit!(depth)
109
+ raise WorkflowError, "prepared-step execution result exceeds maximum depth #{MAX_DEPTH}" if depth > MAX_DEPTH
110
+
111
+ @nodes += 1
112
+ return if @nodes <= MAX_NODES
113
+
114
+ raise WorkflowError, "prepared-step execution result exceeds maximum size #{MAX_NODES}"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -35,29 +35,32 @@ module Smith
35
35
  )
36
36
 
37
37
  branch_calls = branches.map do |branch_key, agent_name|
38
- proc do |signal|
39
- run_fanout_branch(branch_key, agent_name, branch_agent_classes.fetch(branch_key), env, signal)
40
- end
38
+ PreparedBranchExecution.instance_method(:prepared_branch).bind_call(
39
+ self,
40
+ FanoutExecution.instance_method(:run_fanout_branch),
41
+ branch_key,
42
+ agent_name,
43
+ branch_agent_classes.fetch(branch_key),
44
+ env
45
+ )
41
46
  end
42
47
 
43
48
  Parallel.execute(branches: branch_calls)
44
49
  end
45
50
 
46
51
  def run_fanout_branch(branch_key, agent_name, agent_class, env, signal)
47
- setup_fanout_branch_context(env, @ledger, agent_class)
48
-
49
- with_agent_context(agent_class) do
50
- branch_ledger = effective_call_ledger
51
- reserved = reserve_fanout_branch_call(branch_ledger, env.branch_estimates[branch_key], agent_class)
52
- begin
53
- result = guarded_fanout_branch_call(agent_class, env, signal)
54
- finalize_named_branch(branch_key, agent_name, result, branch_ledger, reserved).tap { reserved = nil }
55
- ensure
56
- settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
52
+ with_branch_context(env, @ledger, agent_class:) do
53
+ with_agent_context(agent_class) do
54
+ branch_ledger = effective_call_ledger
55
+ reserved = reserve_fanout_branch_call(branch_ledger, env.branch_estimates[branch_key], agent_class)
56
+ begin
57
+ result = guarded_fanout_branch_call(agent_class, env, signal)
58
+ finalize_named_branch(branch_key, agent_name, result, branch_ledger, reserved).tap { reserved = nil }
59
+ ensure
60
+ settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
61
+ end
57
62
  end
58
63
  end
59
- ensure
60
- teardown_branch_context(env)
61
64
  end
62
65
 
63
66
  def guarded_fanout_branch_call(agent_class, env, signal)
@@ -89,9 +92,7 @@ module Smith
89
92
  end
90
93
 
91
94
  def run_fanout_agent_input_guardrails(branch_agent_classes)
92
- branch_agent_classes.each_value do |agent_class|
93
- run_agent_input_guardrails(agent_class)
94
- end
95
+ branch_agent_classes.each_value { |agent_class| run_agent_input_guardrails(agent_class) }
95
96
  end
96
97
 
97
98
  def fanout_branch_estimates(branches, branch_agent_classes)
@@ -109,7 +110,7 @@ module Smith
109
110
  end
110
111
 
111
112
  def resolve_fanout_agent_class(transition, agent_name)
112
- Agent::Registry.fetch!(
113
+ resolve_registered_agent!(
113
114
  agent_name,
114
115
  workflow_class: self.class,
115
116
  transition_name: transition&.name,
@@ -1,21 +1,47 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "diagnostic_path"
4
+
3
5
  module Smith
4
6
  class Workflow
5
7
  class Graph
6
8
  class Diagnostic
7
- attr_reader :severity, :code, :message, :state, :transition, :target, :suggestion
9
+ attr_reader :severity, :code, :state, :transition, :target, :suggestion
8
10
 
9
11
  def initialize(**attributes)
10
12
  @severity = attributes.fetch(:severity)
11
13
  @code = attributes.fetch(:code)
12
14
  @message = attributes.fetch(:message)
15
+ @path = attributes[:path]
13
16
  @state = attributes[:state]
14
17
  @transition = attributes[:transition]
15
18
  @target = attributes[:target]
16
19
  @suggestion = attributes[:suggestion]
17
20
  end
18
21
 
22
+ def message
23
+ return @message unless @path
24
+ return @rendered_message if defined?(@rendered_message)
25
+
26
+ @rendered_message = String.new.tap do |rendered|
27
+ @path.each_label { |label| rendered << "Nested workflow #{label}: " }
28
+ rendered << @message
29
+ end.freeze
30
+ end
31
+
32
+ def nested(label:, code:, transition:, target:)
33
+ self.class.new(
34
+ severity: severity,
35
+ code: code,
36
+ message: @message,
37
+ path: DiagnosticPath.new(label:, tail: @path),
38
+ state: state,
39
+ transition: transition,
40
+ target: target,
41
+ suggestion: suggestion
42
+ )
43
+ end
44
+
19
45
  def to_h
20
46
  {
21
47
  severity: severity,
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-initializer"
4
+
5
+ module Smith
6
+ class Workflow
7
+ class Graph
8
+ class DiagnosticPath
9
+ extend Dry::Initializer
10
+
11
+ option :label
12
+ option :tail, default: proc {}
13
+
14
+ def each_label
15
+ current = self
16
+ while current
17
+ yield current.label
18
+ current = current.tail
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../transition_actionability"
4
+
5
+ module Smith
6
+ class Workflow
7
+ class Graph
8
+ class ExecutionSuccessors
9
+ attr_reader :graph
10
+
11
+ def initialize(graph)
12
+ @graph = graph
13
+ end
14
+
15
+ def initial_transition
16
+ graph.first_transition_from(graph.initial_state)
17
+ end
18
+
19
+ def for(transition)
20
+ successful_successors(transition) + failure_successors(transition)
21
+ end
22
+
23
+ def failure_control_marker(transition)
24
+ target = executable_target(transition.failure_transition, expected_state: transition.from)
25
+ target if target && !TransitionActionability.call(target)
26
+ end
27
+
28
+ private
29
+
30
+ def successful_successors(transition)
31
+ successful_target_names(transition).filter_map do |name|
32
+ executable_target(name, expected_state: transition.to)
33
+ end
34
+ end
35
+
36
+ def failure_successors(transition)
37
+ target = executable_target(transition.failure_transition, expected_state: transition.from)
38
+ return [] unless target
39
+ return [target] if TransitionActionability.call(target)
40
+
41
+ successor = graph.first_transition_from(target.to)
42
+ successor ? [successor] : []
43
+ end
44
+
45
+ def successful_target_names(transition)
46
+ return Targets.router_for(transition) if transition.routed?
47
+
48
+ names = Array(transition.deterministic_routes).dup
49
+ name = transition.success_transition || graph.first_transition_from(transition.to)&.name
50
+ names << name if name
51
+ names.uniq
52
+ end
53
+
54
+ def executable_target(name, expected_state:)
55
+ return if name.nil?
56
+
57
+ target = graph.transitions[name]
58
+ return unless target
59
+
60
+ target if target.from.nil? || target.from == expected_state
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class Graph
6
+ class IdentifierProjection
7
+ def call(value)
8
+ Identifier.normalize(value, label: "graph identifier", allow_nil: true)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -23,9 +23,7 @@ module Smith
23
23
  private
24
24
 
25
25
  def terminal_states
26
- graph.states.select do |state|
27
- graph.transitions.values.none? { |transition| transition.from == state }
28
- end
26
+ graph.states.select { |state| graph.transitions_from(state).empty? }
29
27
  end
30
28
  end
31
29
  end