Module: Smith::Workflow::DSL::ClassMethods

Defined in:
lib/smith/workflow/dsl.rb,
lib/smith/workflow/pipeline.rb

Instance Method Summary collapse

Instance Method Details

#budget(**opts) ⇒ Object



64
65
66
67
68
# File 'lib/smith/workflow/dsl.rb', line 64

def budget(**opts)
  return @budget_config if opts.empty?

  @budget_config = opts
end

#context_manager(klass = nil) ⇒ Object



82
83
84
85
86
# File 'lib/smith/workflow/dsl.rb', line 82

def context_manager(klass = nil)
  return @context_manager_class if klass.nil?

  @context_manager_class = klass
end

#find_transition(name) ⇒ Object



227
228
229
# File 'lib/smith/workflow/dsl.rb', line 227

def find_transition(name)
  (@transitions || {})[name]
end

#first_transition_from(state) ⇒ Object



219
220
221
# File 'lib/smith/workflow/dsl.rb', line 219

def first_transition_from(state)
  transitions_by_state.fetch(state, []).first
end

#from_state(hash) ⇒ Object



235
236
237
238
239
# File 'lib/smith/workflow/dsl.rb', line 235

def from_state(hash)
  workflow = allocate
  workflow.send(:restore_state, hash)
  workflow
end

#guardrails(klass = nil) ⇒ Object



76
77
78
79
80
# File 'lib/smith/workflow/dsl.rb', line 76

def guardrails(klass = nil)
  return @guardrails_class if klass.nil?

  @guardrails_class = klass
end

#idempotency_mode(mode = nil) ⇒ Object

Controls whether run_persisted! / advance_persisted! stamp a step_in_progress marker before each advance and clear it afterward.

Modes:

:lax    (default) no marker stamping; restore never raises.
       Safe when agent calls and tools are idempotent, so
       re-running a step on restore is harmless.
:strict marker is persisted before each advance and cleared
       after. Restore raises
       Smith::StepInProgressOnRestore when the marker is
       set, indicating a previous worker crashed mid-step
       and re-running could double-execute non-idempotent
       agent calls or tools.


179
180
181
182
183
184
185
186
187
# File 'lib/smith/workflow/dsl.rb', line 179

def idempotency_mode(mode = nil)
  return @idempotency_mode || :lax if mode.nil?

  unless %i[strict lax].include?(mode)
    raise ArgumentError, "idempotency_mode must be :strict or :lax, got #{mode.inspect}"
  end

  @idempotency_mode = mode
end

#inherited(subclass) ⇒ Object



11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/smith/workflow/dsl.rb', line 11

def inherited(subclass)
  super
  subclass.instance_variable_set(:@states, (@states || []).dup)
  subclass.instance_variable_set(:@transitions, (@transitions || {}).dup)
  subclass.instance_variable_set(
    :@transitions_by_state,
    duplicate_transition_index(@transitions_by_state)
  )
  subclass.instance_variable_set(:@transition_order, (@transition_order || {}).dup)
  subclass.instance_variable_set(:@generated_transitions, (@generated_transitions || []).dup)
  subclass.instance_variable_set(:@transition_sequence, @transition_sequence)
  subclass.instance_variable_set(:@initial_state_name, @initial_state_name)
  subclass.instance_variable_set(:@budget_config, @budget_config&.dup)
  subclass.instance_variable_set(:@max_transitions_count, @max_transitions_count)
  subclass.instance_variable_set(:@guardrails_class, @guardrails_class)
  subclass.instance_variable_set(:@context_manager_class, @context_manager_class)
  subclass.instance_variable_set(:@seed_messages_builder, @seed_messages_builder)
  subclass.instance_variable_set(:@persistence_key_builder, @persistence_key_builder)
  subclass.instance_variable_set(:@persistence_schema_version, @persistence_schema_version)
  subclass.instance_variable_set(:@migrations, (@migrations || {}).dup)
  subclass.instance_variable_set(:@seed_validation_mode, @seed_validation_mode)
  subclass.instance_variable_set(:@idempotency_mode, @idempotency_mode)
  subclass.instance_variable_set(:@persistence_ttl, @persistence_ttl)
end

#initial_state(name = nil) ⇒ Object



36
37
38
39
40
41
# File 'lib/smith/workflow/dsl.rb', line 36

def initial_state(name = nil)
  return @initial_state_name if name.nil?

  @initial_state_name = own_identifier(name)
  state(@initial_state_name)
end

#max_transitions(count = nil) ⇒ Object



70
71
72
73
74
# File 'lib/smith/workflow/dsl.rb', line 70

def max_transitions(count = nil)
  return @max_transitions_count if count.nil?

  @max_transitions_count = count
end

#migrate_from(version, &block) ⇒ Object

Register a one-step migration from stored version N to N+1. The block receives the persisted payload Hash (top-level keys already symbolized) and must return the migrated payload. Bumping the :schema_version key is the migration's responsibility but Smith advances defensively if the block omits it, so migrations stay loop-free.

Raises:

  • (ArgumentError)


124
125
126
127
128
129
130
131
132
133
# File 'lib/smith/workflow/dsl.rb', line 124

def migrate_from(version, &block)
  raise ArgumentError, "migrate_from requires a block" unless block

  unless version.is_a?(Integer) && version >= 1
    raise ArgumentError, "migrate_from version must be a positive Integer, got #{version.inspect}"
  end

  @migrations ||= {}
  @migrations[version] = block
end

#migrationsObject



135
136
137
# File 'lib/smith/workflow/dsl.rb', line 135

def migrations
  @migrations || {}
end

#persistence_key(&block) ⇒ Object



94
95
96
97
98
# File 'lib/smith/workflow/dsl.rb', line 94

def persistence_key(&block)
  return @persistence_key_builder unless block_given?

  @persistence_key_builder = block
end

#persistence_schema_version(version = nil) ⇒ Object

Schema version stamped into every persisted payload's :schema_version key. Restore compares the stored version with this value and either passes through (equal), applies registered migrate_from blocks one step at a time (stored less than current), or raises Smith::PersistenceSchemaMismatch (stored greater than current, or unbridged gap). Pre-versioning payloads (no :schema_version key) are treated as v1 for backward compatibility.



108
109
110
111
112
113
114
115
116
# File 'lib/smith/workflow/dsl.rb', line 108

def persistence_schema_version(version = nil)
  return @persistence_schema_version || 1 if version.nil?

  unless version.is_a?(Integer) && version >= 1
    raise ArgumentError, "persistence_schema_version must be a positive Integer, got #{version.inspect}"
  end

  @persistence_schema_version = version
end

#persistence_ttl(seconds = nil) ⇒ Object

Per-workflow TTL override (in seconds). Takes precedence over Smith.config.persistence_ttl at persist! time. nil (default) means inherit the global config.

Hosts typically set this when different workflow classes have different durability horizons: e.g., short-lived UI sessions at 1.day.to_i, long-running research workflows at 30.days.to_i.

Wiring contract: when the resolved TTL is non-nil, Workflow#persist! forwards it to the adapter as a ttl: kwarg. Shipped adapters (Memory, RedisStore, CacheStore, ActiveRecordStore) accept this kwarg; external duck-typed adapters that implement only the bare REQUIRED_METHODS contract without a ttl: kwarg will only break when a host actually opts into TTL.



204
205
206
207
208
209
210
211
212
213
# File 'lib/smith/workflow/dsl.rb', line 204

def persistence_ttl(seconds = nil)
  return @persistence_ttl if seconds.nil?

  unless seconds.is_a?(Numeric) && seconds.positive?
    raise ArgumentError,
          "persistence_ttl must be a positive Numeric (seconds), got #{seconds.inspect}"
  end

  @persistence_ttl = seconds
end

#pipeline(name, from:, to:) ⇒ Object



111
112
113
# File 'lib/smith/workflow/pipeline.rb', line 111

def pipeline(name, from:, to:, &)
  Pipeline.new(name, from: from, to: to, &).compile!(self)
end

#seed_messages(&block) ⇒ Object



88
89
90
91
92
# File 'lib/smith/workflow/dsl.rb', line 88

def seed_messages(&block)
  return @seed_messages_builder unless block_given?

  @seed_messages_builder = block
end

#seed_validation(mode = nil) ⇒ Object

Controls whether restore validates that the seed_messages builder still produces the same digest as when this workflow was originally persisted.

Modes:

:off    (default) skip validation entirely. Recommended when
       the seed builder is non-deterministic (timestamps,
       UUIDs, request-scoped data) since drift would
       surface on every restore.
:warn   log a warning via Smith.config.logger on drift; do
       not raise. Suitable for soft monitoring.
:strict raise Smith::SeedMismatch on drift. Suitable when
       the seed builder is deterministic (system
       instructions, static templates) and divergence
       indicates a code change that would invalidate the
       persisted conversation context.


155
156
157
158
159
160
161
162
163
# File 'lib/smith/workflow/dsl.rb', line 155

def seed_validation(mode = nil)
  return @seed_validation_mode || :off if mode.nil?

  unless %i[strict warn off].include?(mode)
    raise ArgumentError, "seed_validation must be :strict, :warn, or :off, got #{mode.inspect}"
  end

  @seed_validation_mode = mode
end

#state(name) ⇒ Object



43
44
45
46
47
48
# File 'lib/smith/workflow/dsl.rb', line 43

def state(name)
  name = own_identifier(name)
  @states ||= []
  @states << name unless @states.include?(name)
  generate_fail_transition if name == :failed
end

#transition(name, from:, to:) ⇒ Object



50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/smith/workflow/dsl.rb', line 50

def transition(name, from:, to:, &)
  declared = Transition.new(name, from: from, to: to, &)
  name = declared.name
  @transitions ||= {}

  if @transitions.key?(name)
    remove_from_transition_index(@transitions[name])
    release_generated_transition_order(name)
  end

  @transitions[name] = declared
  insert_into_transition_index(declared)
end

#transition_at(index) ⇒ Object



231
232
233
# File 'lib/smith/workflow/dsl.rb', line 231

def transition_at(index)
  (@transitions || {}).values.fetch(index)
end

#transition_from?(state) ⇒ Boolean

Returns:

  • (Boolean)


223
224
225
# File 'lib/smith/workflow/dsl.rb', line 223

def transition_from?(state)
  transitions_by_state.fetch(state, []).any?
end

#transitions_from(state) ⇒ Object



215
216
217
# File 'lib/smith/workflow/dsl.rb', line 215

def transitions_from(state)
  transitions_by_state.fetch(state, []).dup
end