smith-agents 0.4.2 → 0.4.3

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -3
  3. data/README.md +45 -2
  4. data/UPSTREAM_PROPOSAL.md +19 -0
  5. data/docs/PATTERNS.md +41 -1
  6. data/lib/smith/agent/registry/introspection.rb +27 -0
  7. data/lib/smith/agent/registry.rb +22 -17
  8. data/lib/smith/agent/registry_binding.rb +43 -0
  9. data/lib/smith/agent.rb +3 -4
  10. data/lib/smith/doctor/checks/models_registry.rb +25 -14
  11. data/lib/smith/doctor/checks/persistence_capabilities.rb +37 -29
  12. data/lib/smith/version.rb +1 -1
  13. data/lib/smith/workflow/agent_result.rb +26 -0
  14. data/lib/smith/workflow/branch_env.rb +24 -0
  15. data/lib/smith/workflow/budget_integration.rb +2 -0
  16. data/lib/smith/workflow/deterministic_execution.rb +1 -1
  17. data/lib/smith/workflow/deterministic_step.rb +32 -4
  18. data/lib/smith/workflow/durability.rb +38 -12
  19. data/lib/smith/workflow/evaluator_optimizer.rb +5 -8
  20. data/lib/smith/workflow/event_integration.rb +3 -3
  21. data/lib/smith/workflow/execution.rb +2 -0
  22. data/lib/smith/workflow/fanout_execution.rb +2 -0
  23. data/lib/smith/workflow/graph/contract_helpers.rb +65 -0
  24. data/lib/smith/workflow/graph/fanout_contract.rb +140 -0
  25. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +98 -0
  26. data/lib/smith/workflow/graph/optimization_contract.rb +96 -0
  27. data/lib/smith/workflow/graph/orchestration_contract.rb +83 -0
  28. data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +124 -0
  29. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +114 -0
  30. data/lib/smith/workflow/graph/runtime_readiness.rb +63 -0
  31. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +87 -0
  32. data/lib/smith/workflow/graph/runtime_readiness_report.rb +65 -0
  33. data/lib/smith/workflow/graph/targets.rb +5 -0
  34. data/lib/smith/workflow/graph/transition_diagnostics.rb +7 -0
  35. data/lib/smith/workflow/graph/transition_snapshot.rb +61 -20
  36. data/lib/smith/workflow/graph.rb +16 -1
  37. data/lib/smith/workflow/graph_dsl.rb +4 -0
  38. data/lib/smith/workflow/guardrail_integration.rb +20 -3
  39. data/lib/smith/workflow/nested_execution.rb +5 -4
  40. data/lib/smith/workflow/optimization_state.rb +13 -0
  41. data/lib/smith/workflow/orchestration_state.rb +13 -0
  42. data/lib/smith/workflow/orchestrator_worker.rb +3 -15
  43. data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
  44. data/lib/smith/workflow/parallel.rb +6 -15
  45. data/lib/smith/workflow/parallel_execution.rb +2 -0
  46. data/lib/smith/workflow/persistence.rb +37 -6
  47. data/lib/smith/workflow/router.rb +15 -4
  48. data/lib/smith/workflow/run_result.rb +54 -0
  49. data/lib/smith/workflow/transition.rb +90 -4
  50. data/lib/smith/workflow/usage_entry.rb +30 -0
  51. data/lib/smith/workflow/worker_execution.rb +13 -0
  52. data/lib/smith/workflow.rb +40 -136
  53. data/lib/smith.rb +9 -0
  54. metadata +21 -1
@@ -16,8 +16,14 @@ module Smith
16
16
  Validator.new(self).report
17
17
  end
18
18
 
19
+ def runtime_readiness(visited: nil)
20
+ RuntimeReadiness.new(self, visited: visited).report
21
+ end
22
+
19
23
  def transition_snapshots
20
- transitions.values.map { |transition| TransitionSnapshot.from_transition(transition) }
24
+ transitions.values.map do |transition|
25
+ TransitionSnapshot.from_transition(transition, workflow_class: workflow_class)
26
+ end
21
27
  end
22
28
  end
23
29
  end
@@ -29,8 +35,17 @@ require_relative "graph/state_diagnostics"
29
35
  require_relative "graph/reachability"
30
36
  require_relative "graph/reachability_diagnostics"
31
37
  require_relative "graph/metrics"
38
+ require_relative "graph/nested_readiness_diagnostics"
32
39
  require_relative "graph/report"
40
+ require_relative "graph/runtime_binding_diagnostic_builder"
41
+ require_relative "graph/runtime_binding_diagnostics"
42
+ require_relative "graph/runtime_readiness_report"
43
+ require_relative "graph/runtime_readiness_metrics"
33
44
  require_relative "graph/targets"
45
+ require_relative "graph/fanout_contract"
46
+ require_relative "graph/optimization_contract"
47
+ require_relative "graph/orchestration_contract"
34
48
  require_relative "graph/transition_snapshot"
35
49
  require_relative "graph/transition_diagnostics"
36
50
  require_relative "graph/validator"
51
+ require_relative "graph/runtime_readiness"
@@ -14,5 +14,9 @@ module Smith
14
14
  def self.validate_graph
15
15
  graph.validate
16
16
  end
17
+
18
+ def self.runtime_readiness
19
+ graph.runtime_readiness
20
+ end
17
21
  end
18
22
  end
@@ -20,16 +20,33 @@ module Smith
20
20
  run_agent_output_guardrails(output, agent_class)
21
21
  end
22
22
 
23
- def handle_step_failure(transition, _error)
23
+ def handle_step_failure(transition, error)
24
24
  failure_name = transition.failure_transition
25
- return unless failure_name
25
+ raise error unless failure_name
26
26
 
27
27
  fail_transition = self.class.find_transition(failure_name)
28
- return unless fail_transition
28
+ raise error unless fail_transition
29
+ validate_transition_origin!(fail_transition)
30
+
31
+ if actionable_failure_transition?(fail_transition)
32
+ @next_transition_name = failure_name
33
+ return
34
+ end
29
35
 
30
36
  @state = fail_transition.to
31
37
  end
32
38
 
39
+ def actionable_failure_transition?(transition)
40
+ transition.agent_name ||
41
+ transition.deterministic? ||
42
+ transition.routed? ||
43
+ transition.fanout? ||
44
+ transition.nested? ||
45
+ transition.optimized? ||
46
+ transition.orchestrated? ||
47
+ transition.success_transition
48
+ end
49
+
33
50
  def run_workflow_input_guardrails
34
51
  wf_guardrails = self.class.guardrails
35
52
  Guardrails::Runner.run_inputs(wf_guardrails, @context) if wf_guardrails
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "usage_entry"
4
+
3
5
  module Smith
4
6
  class Workflow
5
7
  module NestedExecution
@@ -29,10 +31,9 @@ module Smith
29
31
  # Roll up child totals AND usage_entries BEFORE the failed-step
30
32
  # check raises. Previously the rollup only fired on child success
31
33
  # — billable agent work inside a failed child was silently
32
- # dropped from the parent's totals/entries. The drift guard at
33
- # the hadithi boundary wouldn't catch it (parent rollups + entries
34
- # consistently undercount the same way; sum invariant still holds
35
- # incorrectly). Roll up first, then re-raise, so the parent's
34
+ # dropped from the parent's totals/entries. Simple aggregate drift
35
+ # checks can miss this when parent rollups and entries undercount
36
+ # the same way. Roll up first, then re-raise, so the parent's
36
37
  # terminal state reflects the child's billable work even when the
37
38
  # child failed.
38
39
  def handle_child_result(child_result)
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ OptimizationState = Struct.new(
6
+ :config, :prepared_input, :candidate, :feedback, :last_score, :generator_class, :evaluator_class
7
+ ) do
8
+ def initialize(config, prepared_input)
9
+ super(config, prepared_input, nil, nil, nil, nil, nil)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ OrchestrationState = Struct.new(
6
+ :config, :prepared_input, :orchestrator_class, :worker_class, :worker_results
7
+ ) do
8
+ def initialize(config, prepared_input)
9
+ super(config, prepared_input, nil, nil, nil)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,25 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
- require "securerandom"
4
+
5
+ require_relative "orchestration_state"
6
+ require_relative "worker_execution"
5
7
 
6
8
  module Smith
7
9
  class Workflow
8
10
  module OrchestratorWorker
9
- OrchestrationState = Struct.new(
10
- :config, :prepared_input, :orchestrator_class, :worker_class, :worker_results
11
- ) do
12
- def initialize(config, prepared_input)
13
- super(config, prepared_input, nil, nil, nil)
14
- end
15
- end
16
-
17
- WorkerExecution = Struct.new(:execution_id, :task, :output) do
18
- def self.run(worker_class, task, schema, budget_runner)
19
- new(SecureRandom.uuid, task, budget_runner.call(worker_class, task, schema))
20
- end
21
- end
22
-
23
11
  private
24
12
 
25
13
  def dispatch_step(transition, prepared_input: nil)
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class Parallel
6
+ CancellationSignal = Struct.new(:cancelled, :mutex) do
7
+ def initialize
8
+ super(false, Mutex.new)
9
+ end
10
+
11
+ def cancel!
12
+ mutex.synchronize { self.cancelled = true }
13
+ end
14
+
15
+ def cancelled?
16
+ mutex.synchronize { cancelled }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -2,26 +2,17 @@
2
2
 
3
3
  require "concurrent"
4
4
 
5
+ require_relative "parallel/cancellation_signal"
6
+
5
7
  module Smith
6
8
  class Workflow
7
9
  class Parallel
8
- CancellationSignal = Struct.new(:cancelled, :mutex) do
9
- def initialize
10
- super(false, Mutex.new)
11
- end
12
-
13
- def cancel!
14
- mutex.synchronize { self.cancelled = true }
15
- end
16
-
17
- def cancelled?
18
- mutex.synchronize { cancelled }
19
- end
20
- end
21
-
22
10
  def self.resolve_branch_count(transition, context)
23
11
  count = transition.agent_opts[:count]
24
- count.respond_to?(:call) ? count.call(context) : (count || 1)
12
+ resolved = count.respond_to?(:call) ? count.call(context) : (count || 1)
13
+ return resolved if resolved.is_a?(Integer) && resolved.positive?
14
+
15
+ raise WorkflowError, "parallel branch count must be a positive integer"
25
16
  end
26
17
 
27
18
  def self.execute(branches:)
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "branch_env"
4
+
3
5
  module Smith
4
6
  class Workflow
5
7
  module ParallelExecution
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "usage_entry"
4
+
3
5
  module Smith
4
6
  class Workflow
5
7
  module Persistence
@@ -20,7 +22,7 @@ module Smith
20
22
  total_tokens: @total_tokens || 0,
21
23
  tool_results: @tool_results || [],
22
24
  outcome: snapshot_outcome,
23
- # New durable fields for hadithi billing. All wrapped in
25
+ # Durable usage fields. All wrapped in
24
26
  # snapshot_value so non-JSON-safe runtime values (e.g.
25
27
  # custom Hash details on DeterministicStepFailure) get the
26
28
  # same deep-copy treatment as context/session_messages/etc.
@@ -163,9 +165,9 @@ module Smith
163
165
 
164
166
  h = raw.transform_keys { |k| k.is_a?(String) ? k.to_sym : k }
165
167
  {
166
- transition: h[:transition]&.to_sym,
167
- from: h[:from]&.to_sym,
168
- to: h[:to]&.to_sym,
168
+ transition: normalize_transition_name(h[:transition]),
169
+ from: normalize_state_name(h[:from]),
170
+ to: normalize_state_name(h[:to]),
169
171
  error_class: h[:error_class],
170
172
  error_family: h[:error_family],
171
173
  error_message: h[:error_message],
@@ -255,7 +257,7 @@ module Smith
255
257
  end
256
258
 
257
259
  def normalize_symbol_fields!(normalized)
258
- normalized[:state] = normalized[:state]&.to_sym
260
+ normalized[:state] = normalize_state_name(normalized[:state])
259
261
  if normalized[:outcome].is_a?(Hash) && normalized[:outcome].key?(:kind)
260
262
  normalized[:outcome][:kind] = normalized[:outcome][:kind]&.to_sym
261
263
  elsif normalized[:outcome].is_a?(Hash) && normalized[:outcome].key?("kind")
@@ -263,7 +265,36 @@ module Smith
263
265
  end
264
266
  return unless normalized.key?(:next_transition_name)
265
267
 
266
- normalized[:next_transition_name] = normalized[:next_transition_name]&.to_sym
268
+ normalized[:next_transition_name] = normalize_transition_name(normalized[:next_transition_name])
269
+ end
270
+
271
+ def normalize_transition_name(value)
272
+ return if value.nil?
273
+ transition = self.class.find_transition(value)
274
+ return transition.name if transition
275
+ return value unless value.is_a?(String)
276
+
277
+ symbolized = value.to_sym
278
+ transition = self.class.find_transition(symbolized)
279
+ return transition.name if transition
280
+
281
+ symbolized
282
+ end
283
+
284
+ def normalize_state_name(value)
285
+ return if value.nil?
286
+ return value if declared_state?(value)
287
+ return value unless value.is_a?(String)
288
+
289
+ symbolized = value.to_sym
290
+ return symbolized if declared_state?(symbolized)
291
+
292
+ symbolized
293
+ end
294
+
295
+ def declared_state?(value)
296
+ states = self.class.instance_variable_get(:@states) || []
297
+ states.include?(value)
267
298
  end
268
299
 
269
300
  def normalize_nested_hashes!(normalized)
@@ -4,16 +4,23 @@ module Smith
4
4
  class Workflow
5
5
  class Router
6
6
  def self.resolve(classifier_output, config, workflow_class:)
7
- validate!(classifier_output, config)
8
- transition_name = select_transition(classifier_output, config)
7
+ output = normalize_output(classifier_output)
8
+ validate!(output, config)
9
+ transition_name = select_transition(output, config)
9
10
  validate_transition_exists!(transition_name, workflow_class)
10
11
  transition_name
11
12
  end
12
13
 
14
+ def self.normalize_output(output)
15
+ return output unless output.is_a?(Hash)
16
+
17
+ output.transform_keys { |key| key.is_a?(String) ? key.to_sym : key }
18
+ end
19
+
13
20
  def self.validate!(output, config)
14
21
  validate_structure!(output)
15
22
  validate_confidence!(output[:confidence])
16
- validate_route_key!(output[:route].to_sym, output[:confidence], config)
23
+ validate_route_key!(normalize_route_key(output[:route]), output[:confidence], config)
17
24
  end
18
25
 
19
26
  def self.validate_structure!(output)
@@ -37,12 +44,16 @@ module Smith
37
44
 
38
45
  def self.select_transition(output, config)
39
46
  if output[:confidence] >= config[:confidence_threshold]
40
- config[:routes][output[:route].to_sym]
47
+ config[:routes][normalize_route_key(output[:route])]
41
48
  else
42
49
  config[:fallback]
43
50
  end
44
51
  end
45
52
 
53
+ def self.normalize_route_key(route)
54
+ route.to_s.strip.to_sym
55
+ end
56
+
46
57
  def self.validate_transition_exists!(transition_name, workflow_class)
47
58
  return if workflow_class.find_transition(transition_name)
48
59
 
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ # rubocop:disable Style/RedundantStructKeywordInit
6
+ RunResult = Struct.new(:state, :output, :steps, :total_cost, :total_tokens, :context, :session_messages,
7
+ :tool_results, :outcome, :usage_entries, keyword_init: true) do
8
+ def done?
9
+ state_named?(:done)
10
+ end
11
+
12
+ def failed?
13
+ state_named?(:failed)
14
+ end
15
+
16
+ def state_named?(name)
17
+ state == name || state.to_s == name.to_s
18
+ end
19
+
20
+ def terminal_output
21
+ output
22
+ end
23
+
24
+ def outcome_kind
25
+ outcome&.dig(:kind)
26
+ end
27
+
28
+ def outcome_payload
29
+ outcome&.dig(:payload)
30
+ end
31
+
32
+ def last_error
33
+ steps.reverse.map { |step| step[:error] }.compact.first
34
+ end
35
+
36
+ def failed_transition
37
+ failure_detail&.fetch(:transition)
38
+ end
39
+
40
+ def failure_detail
41
+ failed_step = steps.reverse.find { |step| step[:error] }
42
+ return nil unless failed_step
43
+
44
+ {
45
+ transition: failed_step[:transition],
46
+ from: failed_step[:from],
47
+ to: failed_step[:to],
48
+ error: failed_step[:error]
49
+ }
50
+ end
51
+ end
52
+ # rubocop:enable Style/RedundantStructKeywordInit
53
+ end
54
+ end
@@ -5,7 +5,8 @@ module Smith
5
5
  class Transition
6
6
  attr_reader :name, :from, :to, :agent_name, :agent_opts, :success_transition, :failure_transition,
7
7
  :router_config, :workflow_class, :optimization_config, :orchestrator_config,
8
- :fanout_config, :retry_config, :deterministic_block, :deterministic_kind
8
+ :fanout_config, :retry_config, :deterministic_block, :deterministic_kind,
9
+ :deterministic_routes
9
10
 
10
11
  def initialize(name, from:, to:, &)
11
12
  @name = name
@@ -32,8 +33,12 @@ module Smith
32
33
  def route(agent_name, routes:, confidence_threshold:, fallback:)
33
34
  validate_route_conflicts!
34
35
 
35
- @agent_name = agent_name
36
- @router_config = { routes: routes, confidence_threshold: confidence_threshold, fallback: fallback }
36
+ @agent_name = normalize_agent_name!(agent_name, "router")
37
+ @router_config = {
38
+ routes: normalize_router_routes!(routes),
39
+ confidence_threshold: normalize_confidence_threshold!(confidence_threshold),
40
+ fallback: normalize_transition_reference!(fallback, "router fallback")
41
+ }
37
42
  end
38
43
 
39
44
  def workflow(klass)
@@ -104,12 +109,13 @@ module Smith
104
109
  end
105
110
 
106
111
  %i[compute run].each do |method_name|
107
- define_method(method_name) do |&block|
112
+ define_method(method_name) do |routes: nil, &block|
108
113
  validate_deterministic_conflicts!
109
114
  raise WorkflowError, "#{method_name} requires a block" unless block
110
115
 
111
116
  @deterministic_block = block
112
117
  @deterministic_kind = method_name
118
+ @deterministic_routes = normalize_deterministic_routes!(routes)
113
119
  end
114
120
  end
115
121
 
@@ -241,6 +247,57 @@ module Smith
241
247
  normalized.freeze
242
248
  end
243
249
 
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?
253
+
254
+ value.to_sym
255
+ end
256
+
257
+ def normalize_router_routes!(routes)
258
+ raise WorkflowError, "router routes must be a Hash" unless routes.is_a?(Hash)
259
+ raise WorkflowError, "router routes must not be empty" if routes.empty?
260
+
261
+ routes.each_with_object({}) do |(route_key, transition_name), map|
262
+ key = normalize_router_route_key!(route_key)
263
+ raise WorkflowError, "router route :#{key} is duplicated" if map.key?(key)
264
+
265
+ map[key] = normalize_transition_reference!(transition_name, "router route :#{key}")
266
+ end.freeze
267
+ end
268
+
269
+ def normalize_router_route_key!(route_key)
270
+ value = route_key.to_s.strip
271
+ raise WorkflowError, "router route keys must not be blank" if value.empty?
272
+
273
+ value.to_sym
274
+ end
275
+
276
+ def normalize_confidence_threshold!(threshold)
277
+ numeric = Float(threshold)
278
+ return numeric if numeric >= 0.0 && numeric <= 1.0
279
+
280
+ raise WorkflowError, "router confidence_threshold must be a number in 0.0..1.0"
281
+ rescue TypeError, ArgumentError
282
+ raise WorkflowError, "router confidence_threshold must be a number in 0.0..1.0"
283
+ end
284
+
285
+ def normalize_transition_reference!(transition_name, label)
286
+ case transition_name
287
+ when Symbol
288
+ raise WorkflowError, "#{label} transition must not be blank" if transition_name.to_s.empty?
289
+
290
+ transition_name
291
+ when String
292
+ value = transition_name.strip
293
+ raise WorkflowError, "#{label} transition must not be blank" if value.empty?
294
+
295
+ value.freeze
296
+ else
297
+ raise WorkflowError, "#{label} transition must be String or Symbol"
298
+ end
299
+ end
300
+
244
301
  def normalize_fanout_branch_key!(branch_key)
245
302
  key = branch_key.to_s.strip
246
303
  raise WorkflowError, "fan_out branch keys must not be blank" if key.empty?
@@ -278,6 +335,35 @@ module Smith
278
335
  validate_non_negative_numeric!(:max_delay, max_delay) unless max_delay.nil?
279
336
  end
280
337
 
338
+ def normalize_deterministic_routes!(routes)
339
+ return nil if routes.nil?
340
+ raise WorkflowError, "deterministic routes must be an Array" unless routes.is_a?(Array)
341
+ raise WorkflowError, "deterministic routes must not be empty" if routes.empty?
342
+
343
+ routes.each_with_object([]) do |route, list|
344
+ name = normalize_deterministic_route!(route)
345
+ raise WorkflowError, "deterministic route #{name.inspect} is duplicated" if list.include?(name)
346
+
347
+ list << name
348
+ end.freeze
349
+ end
350
+
351
+ def normalize_deterministic_route!(route)
352
+ case route
353
+ when Symbol
354
+ raise WorkflowError, "deterministic route names must not be blank" if route.to_s.empty?
355
+
356
+ route
357
+ when String
358
+ value = route.strip
359
+ raise WorkflowError, "deterministic route names must not be blank" if value.empty?
360
+
361
+ value.freeze
362
+ else
363
+ raise WorkflowError, "deterministic route names must be String or Symbol"
364
+ end
365
+ end
366
+
281
367
  def validate_non_negative_numeric!(name, value)
282
368
  numeric = Float(value)
283
369
  return if numeric >= 0.0
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ # One row per agent provider call. `usage_id` is a UUID generated at
6
+ # recording time and stable across persist/restore so hosts can use it as an
7
+ # idempotency anchor.
8
+ # rubocop:disable Style/RedundantStructKeywordInit
9
+ UsageEntry = Struct.new(
10
+ :usage_id,
11
+ :agent_name,
12
+ :model,
13
+ :input_tokens,
14
+ :output_tokens,
15
+ :cost,
16
+ :attempt_kind,
17
+ :recorded_at,
18
+ keyword_init: true
19
+ ) do
20
+ def self.from_h(hash)
21
+ sym = hash.transform_keys(&:to_sym)
22
+ filtered = sym.slice(*members)
23
+ filtered[:agent_name] = filtered[:agent_name].to_sym if filtered[:agent_name].is_a?(String)
24
+ filtered[:attempt_kind] = filtered[:attempt_kind].to_sym if filtered[:attempt_kind].is_a?(String)
25
+ new(**filtered)
26
+ end
27
+ end
28
+ # rubocop:enable Style/RedundantStructKeywordInit
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Smith
6
+ class Workflow
7
+ WorkerExecution = Struct.new(:execution_id, :task, :output) do
8
+ def self.run(worker_class, task, schema, budget_runner)
9
+ new(SecureRandom.uuid, task, budget_runner.call(worker_class, task, schema))
10
+ end
11
+ end
12
+ end
13
+ end