smith-agents 0.4.3 → 0.4.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -0
- data/README.md +8 -2
- data/docs/PERSISTENCE.md +180 -1
- data/docs/workflow_claim.md +4 -1
- data/lib/smith/agent/lifecycle.rb +30 -10
- data/lib/smith/doctor/checks/persistence_capabilities.rb +1 -0
- data/lib/smith/persistence_adapters/active_record_connection_errors.rb +23 -0
- data/lib/smith/persistence_adapters/active_record_initial_write.rb +42 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +91 -51
- data/lib/smith/persistence_adapters/cache_store.rb +3 -1
- data/lib/smith/persistence_adapters/memory.rb +29 -13
- data/lib/smith/persistence_adapters/payload_version.rb +23 -0
- data/lib/smith/persistence_adapters/redis_store.rb +11 -33
- data/lib/smith/persistence_adapters/redis_versioned_write.rb +56 -0
- data/lib/smith/persistence_adapters/version_expectation.rb +19 -0
- data/lib/smith/persistence_adapters.rb +6 -1
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/durability.rb +1 -0
- data/lib/smith/workflow/persistence.rb +12 -2
- data/lib/smith/workflow/split_step_persistence/boundary.rb +115 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +121 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint_state.rb +51 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +126 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +20 -0
- data/lib/smith/workflow/split_step_persistence/payloads.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/preparation.rb +97 -0
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +91 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +84 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +52 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_freezer.rb +83 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_signature.rb +107 -0
- data/lib/smith/workflow/split_step_persistence/transition_contract_structured_values.rb +51 -0
- data/lib/smith/workflow/split_step_persistence.rb +43 -0
- data/lib/smith/workflow.rb +13 -1
- data/lib/smith.rb +1 -0
- metadata +22 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
module Smith
|
|
6
|
+
class Workflow
|
|
7
|
+
module SplitStepPersistence
|
|
8
|
+
class TransitionContractFreezer
|
|
9
|
+
extend Dry::Initializer
|
|
10
|
+
|
|
11
|
+
CORE_FREEZE = Object.instance_method(:freeze)
|
|
12
|
+
|
|
13
|
+
option :value
|
|
14
|
+
option :max_depth
|
|
15
|
+
option :max_nodes
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
stack = [[value, 0]]
|
|
19
|
+
seen = {}.compare_by_identity
|
|
20
|
+
nodes = 0
|
|
21
|
+
until stack.empty?
|
|
22
|
+
item, depth = stack.pop
|
|
23
|
+
nodes = visit!(nodes, depth)
|
|
24
|
+
freeze_item(item, depth, stack, seen)
|
|
25
|
+
end
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def visit!(nodes, depth)
|
|
32
|
+
raise WorkflowError, "split-step transition contract exceeds maximum depth #{max_depth}" if depth > max_depth
|
|
33
|
+
return nodes + 1 if nodes < max_nodes
|
|
34
|
+
|
|
35
|
+
raise WorkflowError, "split-step transition contract exceeds maximum size #{max_nodes}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def freeze_item(item, depth, stack, seen)
|
|
39
|
+
return if immutable?(item) || item.is_a?(Module) || seen.key?(item)
|
|
40
|
+
|
|
41
|
+
seen[item] = true
|
|
42
|
+
children(item).reverse_each { |child| stack << [child, depth + 1] }
|
|
43
|
+
CORE_FREEZE.bind_call(item)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def children(item)
|
|
47
|
+
return [] if leaf?(item)
|
|
48
|
+
return item.flat_map { |key, value| [key, value] } if item.is_a?(Hash)
|
|
49
|
+
return [item.begin, item.end] if item.is_a?(Range)
|
|
50
|
+
|
|
51
|
+
object_children(item)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def object_children(item)
|
|
55
|
+
return item.to_a if item.is_a?(Array) || item.is_a?(Set)
|
|
56
|
+
return member_children(item) if item.is_a?(Struct) || item.is_a?(Data)
|
|
57
|
+
|
|
58
|
+
transition_children(item)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def member_children(item)
|
|
62
|
+
item.members.map { |name| item.public_send(name) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def transition_children(item)
|
|
66
|
+
unless item.is_a?(Workflow::Transition)
|
|
67
|
+
raise WorkflowError, "split-step transition contract contains unsupported value #{item.class}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
item.instance_variables.map { |name| item.instance_variable_get(name) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def leaf?(item)
|
|
74
|
+
item.is_a?(String) || item.is_a?(Proc)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def immutable?(item)
|
|
78
|
+
item.nil? || item.is_a?(Symbol) || item.is_a?(Numeric) || item == true || item == false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
require "digest"
|
|
5
|
+
|
|
6
|
+
module Smith
|
|
7
|
+
class Workflow
|
|
8
|
+
module SplitStepPersistence
|
|
9
|
+
class TransitionContractSignature
|
|
10
|
+
extend Dry::Initializer
|
|
11
|
+
include TransitionContractStructuredValues
|
|
12
|
+
|
|
13
|
+
option :value
|
|
14
|
+
option :max_depth
|
|
15
|
+
option :max_nodes
|
|
16
|
+
option :max_bytes
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
@nodes = 0
|
|
20
|
+
@bytes = 0
|
|
21
|
+
@seen = {}.compare_by_identity
|
|
22
|
+
signature_value(value, 0)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def signature_value(item, depth)
|
|
28
|
+
visit!(depth)
|
|
29
|
+
reference = reference_for(item)
|
|
30
|
+
return reference if reference
|
|
31
|
+
return terminal_signature(item) if terminal?(item)
|
|
32
|
+
|
|
33
|
+
composite_signature(item, depth)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def composite_signature(item, depth)
|
|
37
|
+
case item
|
|
38
|
+
when Hash then hash_signature(item, depth)
|
|
39
|
+
when Array then collection_signature(:array, item, depth)
|
|
40
|
+
when Set then collection_signature(:set, item, depth)
|
|
41
|
+
else structured_signature(item, depth)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def terminal_signature(item)
|
|
46
|
+
case item
|
|
47
|
+
when String then string_signature(item)
|
|
48
|
+
when Module then [:module, item.name, item.object_id]
|
|
49
|
+
when Proc then proc_signature(item)
|
|
50
|
+
else item
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def hash_signature(hash, depth)
|
|
55
|
+
[:hash, reference_id(hash), hash.map do |key, item|
|
|
56
|
+
[signature_value(key, depth + 1), signature_value(item, depth + 1)]
|
|
57
|
+
end]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def collection_signature(kind, collection, depth)
|
|
61
|
+
[kind, reference_id(collection), collection.map { |item| signature_value(item, depth + 1) }]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def proc_signature(callable)
|
|
65
|
+
[:proc, callable.object_id, callable.source_location, callable.parameters, callable.lambda?]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def string_signature(string)
|
|
69
|
+
@bytes += string.bytesize
|
|
70
|
+
raise WorkflowError, "split-step transition contract exceeds maximum bytes #{max_bytes}" if @bytes > max_bytes
|
|
71
|
+
|
|
72
|
+
[:string, string.bytesize, Digest::SHA256.hexdigest(string)]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def reference_for(item)
|
|
76
|
+
return unless referenceable?(item)
|
|
77
|
+
return [:reference, reference_id(item)] if @seen.key?(item)
|
|
78
|
+
|
|
79
|
+
@seen[item] = @seen.size
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def reference_id(item) = @seen.fetch(item)
|
|
84
|
+
|
|
85
|
+
def referenceable?(item)
|
|
86
|
+
!terminal?(item)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def terminal?(item)
|
|
90
|
+
case item
|
|
91
|
+
when String, Symbol, Numeric, Module, Proc, true, false, nil then true
|
|
92
|
+
else false
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def visit!(depth)
|
|
97
|
+
raise WorkflowError, "split-step transition contract exceeds maximum depth #{max_depth}" if depth > max_depth
|
|
98
|
+
|
|
99
|
+
@nodes += 1
|
|
100
|
+
return if @nodes <= max_nodes
|
|
101
|
+
|
|
102
|
+
raise WorkflowError, "split-step transition contract exceeds maximum size #{max_nodes}"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module SplitStepPersistence
|
|
6
|
+
module TransitionContractStructuredValues
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def structured_signature(item, depth)
|
|
10
|
+
case item
|
|
11
|
+
when Struct then member_signature(:struct, item, depth)
|
|
12
|
+
when Data then member_signature(:data, item, depth)
|
|
13
|
+
when Range then range_signature(item, depth)
|
|
14
|
+
when Workflow::Transition then object_signature(item, depth)
|
|
15
|
+
else unsupported!(item)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def member_signature(kind, object, depth)
|
|
20
|
+
members = object.members.map do |name|
|
|
21
|
+
[name, signature_value(object.public_send(name), depth + 1)]
|
|
22
|
+
end
|
|
23
|
+
[kind, object.class.name, reference_id(object), members]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def object_signature(item, depth)
|
|
27
|
+
variables = item.instance_variables.sort.map do |name|
|
|
28
|
+
[name, signature_value(item.instance_variable_get(name), depth + 1)]
|
|
29
|
+
end
|
|
30
|
+
[:object, item.class.name, item.object_id, variables]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def range_signature(range, depth)
|
|
34
|
+
[
|
|
35
|
+
:range,
|
|
36
|
+
reference_id(range),
|
|
37
|
+
signature_value(range.begin, depth + 1),
|
|
38
|
+
signature_value(range.end, depth + 1),
|
|
39
|
+
range.exclude_end?
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def unsupported!(item)
|
|
44
|
+
raise WorkflowError,
|
|
45
|
+
"split-step transition contract contains unsupported value #{item.class}; " \
|
|
46
|
+
"use primitives, Hash, Array, Set, Struct, Data, Range, Module, or Proc"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
|
|
7
|
+
require_relative "split_step_persistence/subclass_boundary"
|
|
8
|
+
require_relative "split_step_persistence/inheritance"
|
|
9
|
+
require_relative "split_step_persistence/transition_contract_structured_values"
|
|
10
|
+
require_relative "split_step_persistence/transition_contract_signature"
|
|
11
|
+
require_relative "split_step_persistence/transition_contract_freezer"
|
|
12
|
+
require_relative "split_step_persistence/transition_contract"
|
|
13
|
+
require_relative "split_step_persistence/boundary"
|
|
14
|
+
require_relative "split_step_persistence/state_snapshot"
|
|
15
|
+
require_relative "split_step_persistence/preparation_claim"
|
|
16
|
+
require_relative "split_step_persistence/preparation_recovery"
|
|
17
|
+
require_relative "split_step_persistence/preparation"
|
|
18
|
+
require_relative "split_step_persistence/execution"
|
|
19
|
+
require_relative "split_step_persistence/checkpoint_state"
|
|
20
|
+
require_relative "split_step_persistence/checkpoint"
|
|
21
|
+
require_relative "split_step_persistence/payloads"
|
|
22
|
+
|
|
23
|
+
module Smith
|
|
24
|
+
class Workflow
|
|
25
|
+
module SplitStepPersistence
|
|
26
|
+
NO_SPLIT_TRANSITION = Object.new.freeze
|
|
27
|
+
|
|
28
|
+
def self.prepended(base)
|
|
29
|
+
base.singleton_class.prepend(Inheritance)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
include Boundary
|
|
33
|
+
include StateSnapshot
|
|
34
|
+
include PreparationClaim
|
|
35
|
+
include PreparationRecovery
|
|
36
|
+
include Preparation
|
|
37
|
+
include Execution
|
|
38
|
+
include CheckpointState
|
|
39
|
+
include Checkpoint
|
|
40
|
+
include Payloads
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/smith/workflow.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Smith
|
|
|
16
16
|
include DSL
|
|
17
17
|
include Persistence
|
|
18
18
|
include Durability
|
|
19
|
+
prepend SplitStepPersistence
|
|
19
20
|
include GuardrailIntegration
|
|
20
21
|
include BudgetIntegration
|
|
21
22
|
include EventIntegration
|
|
@@ -104,6 +105,12 @@ module Smith
|
|
|
104
105
|
# workflows with the marker set raise
|
|
105
106
|
# Smith::StepInProgressOnRestore. Lax mode leaves it false.
|
|
106
107
|
@step_in_progress = false
|
|
108
|
+
# Process-local phase for the split-step persistence API. This is not
|
|
109
|
+
# serialized: strict restore rejects the durable step_in_progress marker
|
|
110
|
+
# before an uncertain step can be resumed.
|
|
111
|
+
@split_step_phase = nil
|
|
112
|
+
@split_step_transition_name = nil
|
|
113
|
+
@split_step_mutex = Mutex.new
|
|
107
114
|
# Set of context keys recorded via deterministic step write_context
|
|
108
115
|
# writes. Used by persist :auto Context mode to compute the
|
|
109
116
|
# persisted-context slice. Seeded from the Context class's
|
|
@@ -119,10 +126,12 @@ module Smith
|
|
|
119
126
|
end
|
|
120
127
|
|
|
121
128
|
def advance!
|
|
129
|
+
advance_claim = SplitStepPersistence.instance_method(:claim_split_step_advance!).bind_call(self)
|
|
122
130
|
ensure_transition_budget!
|
|
123
131
|
@step_work_started = false
|
|
124
132
|
|
|
125
|
-
transition =
|
|
133
|
+
transition = SplitStepPersistence.instance_method(:resolve_split_step_advance_transition).bind_call(self)
|
|
134
|
+
transition = resolve_transition if transition.equal?(SplitStepPersistence::NO_SPLIT_TRANSITION)
|
|
126
135
|
return if transition.nil?
|
|
127
136
|
|
|
128
137
|
@step_work_started = true
|
|
@@ -139,9 +148,12 @@ module Smith
|
|
|
139
148
|
step_result = { transition: e.requested_name, from: origin_state, to: @state, error: e }
|
|
140
149
|
record_step_snapshot(step_result)
|
|
141
150
|
step_result
|
|
151
|
+
ensure
|
|
152
|
+
SplitStepPersistence.instance_method(:release_split_step_advance!).bind_call(self, advance_claim) if advance_claim
|
|
142
153
|
end
|
|
143
154
|
|
|
144
155
|
def run!
|
|
156
|
+
SplitStepPersistence.instance_method(:ensure_split_step_execution_allowed!).bind_call(self)
|
|
145
157
|
steps = []
|
|
146
158
|
until terminal?
|
|
147
159
|
step = advance!
|
data/lib/smith.rb
CHANGED
|
@@ -241,6 +241,7 @@ require_relative "smith/workflow/worker_execution"
|
|
|
241
241
|
require_relative "smith/workflow/dsl"
|
|
242
242
|
require_relative "smith/workflow/persistence"
|
|
243
243
|
require_relative "smith/workflow/durability"
|
|
244
|
+
require_relative "smith/workflow/split_step_persistence"
|
|
244
245
|
require_relative "smith/workflow/guardrail_integration"
|
|
245
246
|
require_relative "smith/workflow/budget_integration"
|
|
246
247
|
require_relative "smith/workflow/event_integration"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smith-agents
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Ralak
|
|
@@ -179,12 +179,17 @@ files:
|
|
|
179
179
|
- lib/smith/models/normalizer.rb
|
|
180
180
|
- lib/smith/models/profile.rb
|
|
181
181
|
- lib/smith/persistence_adapters.rb
|
|
182
|
+
- lib/smith/persistence_adapters/active_record_connection_errors.rb
|
|
183
|
+
- lib/smith/persistence_adapters/active_record_initial_write.rb
|
|
182
184
|
- lib/smith/persistence_adapters/active_record_store.rb
|
|
183
185
|
- lib/smith/persistence_adapters/cache_store.rb
|
|
184
186
|
- lib/smith/persistence_adapters/memory.rb
|
|
187
|
+
- lib/smith/persistence_adapters/payload_version.rb
|
|
185
188
|
- lib/smith/persistence_adapters/rails_cache.rb
|
|
186
189
|
- lib/smith/persistence_adapters/redis_store.rb
|
|
190
|
+
- lib/smith/persistence_adapters/redis_versioned_write.rb
|
|
187
191
|
- lib/smith/persistence_adapters/retry.rb
|
|
192
|
+
- lib/smith/persistence_adapters/version_expectation.rb
|
|
188
193
|
- lib/smith/pricing.rb
|
|
189
194
|
- lib/smith/providers/openai/responses.rb
|
|
190
195
|
- lib/smith/providers/openai/routing.rb
|
|
@@ -261,6 +266,22 @@ files:
|
|
|
261
266
|
- lib/smith/workflow/retry_execution.rb
|
|
262
267
|
- lib/smith/workflow/router.rb
|
|
263
268
|
- lib/smith/workflow/run_result.rb
|
|
269
|
+
- lib/smith/workflow/split_step_persistence.rb
|
|
270
|
+
- lib/smith/workflow/split_step_persistence/boundary.rb
|
|
271
|
+
- lib/smith/workflow/split_step_persistence/checkpoint.rb
|
|
272
|
+
- lib/smith/workflow/split_step_persistence/checkpoint_state.rb
|
|
273
|
+
- lib/smith/workflow/split_step_persistence/execution.rb
|
|
274
|
+
- lib/smith/workflow/split_step_persistence/inheritance.rb
|
|
275
|
+
- lib/smith/workflow/split_step_persistence/payloads.rb
|
|
276
|
+
- lib/smith/workflow/split_step_persistence/preparation.rb
|
|
277
|
+
- lib/smith/workflow/split_step_persistence/preparation_claim.rb
|
|
278
|
+
- lib/smith/workflow/split_step_persistence/preparation_recovery.rb
|
|
279
|
+
- lib/smith/workflow/split_step_persistence/state_snapshot.rb
|
|
280
|
+
- lib/smith/workflow/split_step_persistence/subclass_boundary.rb
|
|
281
|
+
- lib/smith/workflow/split_step_persistence/transition_contract.rb
|
|
282
|
+
- lib/smith/workflow/split_step_persistence/transition_contract_freezer.rb
|
|
283
|
+
- lib/smith/workflow/split_step_persistence/transition_contract_signature.rb
|
|
284
|
+
- lib/smith/workflow/split_step_persistence/transition_contract_structured_values.rb
|
|
264
285
|
- lib/smith/workflow/transition.rb
|
|
265
286
|
- lib/smith/workflow/usage_entry.rb
|
|
266
287
|
- lib/smith/workflow/worker_execution.rb
|