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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +201 -0
- data/README.md +108 -4
- data/docs/CONFIGURATION.md +4 -1
- data/docs/PATTERNS.md +7 -0
- data/docs/PERSISTENCE.md +347 -25
- data/lib/smith/agent/lifecycle.rb +19 -10
- data/lib/smith/agent/registry/execution_binding_capture.rb +26 -0
- data/lib/smith/agent/registry/mutation_boundary.rb +29 -0
- data/lib/smith/agent/registry.rb +8 -0
- data/lib/smith/agent.rb +24 -2
- data/lib/smith/budget/amount_contract.rb +67 -0
- data/lib/smith/budget/amount_representation.rb +43 -0
- data/lib/smith/budget/decimal_context.rb +18 -0
- data/lib/smith/budget/ledger.rb +110 -18
- data/lib/smith/budget/ledger_publication.rb +60 -0
- data/lib/smith/budget/ledger_state.rb +21 -0
- data/lib/smith/budget/ledger_state_transition.rb +72 -0
- data/lib/smith/budget/public_state_validator.rb +25 -0
- data/lib/smith/budget/reservation.rb +20 -0
- data/lib/smith/budget/reservation_contract.rb +34 -0
- data/lib/smith/context/observation_masking.rb +3 -1
- data/lib/smith/context/state_injection.rb +1 -1
- data/lib/smith/doctor/checks/persistence_capabilities.rb +18 -4
- data/lib/smith/error.rb +5 -0
- data/lib/smith/errors.rb +6 -0
- data/lib/smith/exponential_backoff.rb +128 -0
- data/lib/smith/persistence_adapters/active_record_exact_predicate.rb +43 -0
- data/lib/smith/persistence_adapters/active_record_exact_store.rb +62 -0
- data/lib/smith/persistence_adapters/active_record_exact_write.rb +40 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +7 -4
- data/lib/smith/persistence_adapters/cache_store.rb +1 -0
- data/lib/smith/persistence_adapters/memory.rb +16 -1
- data/lib/smith/persistence_adapters/redis_client_access.rb +38 -0
- data/lib/smith/persistence_adapters/redis_exact_write.rb +44 -0
- data/lib/smith/persistence_adapters/redis_store.rb +28 -11
- data/lib/smith/persistence_adapters/retry.rb +22 -10
- data/lib/smith/persistence_adapters.rb +21 -18
- data/lib/smith/persistence_payload_conflict.rb +12 -0
- data/lib/smith/tool/budget_enforcement.rb +2 -2
- data/lib/smith/types.rb +1 -0
- data/lib/smith/version.rb +2 -1
- data/lib/smith/workflow/branch_env.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +21 -11
- data/lib/smith/workflow/definition_identity/class_methods.rb +48 -0
- data/lib/smith/workflow/definition_identity.rb +25 -0
- data/lib/smith/workflow/deterministic_step.rb +2 -2
- data/lib/smith/workflow/dsl.rb +73 -5
- data/lib/smith/workflow/durability.rb +1 -1
- data/lib/smith/workflow/evaluator_optimizer.rb +7 -11
- data/lib/smith/workflow/execution.rb +16 -18
- data/lib/smith/workflow/execution_binding_resolution.rb +16 -0
- data/lib/smith/workflow/execution_result_snapshot.rb +118 -0
- data/lib/smith/workflow/fanout_execution.rb +1 -1
- data/lib/smith/workflow/graph/diagnostic.rb +27 -1
- data/lib/smith/workflow/graph/diagnostic_path.rb +24 -0
- data/lib/smith/workflow/graph/execution_successors.rb +65 -0
- data/lib/smith/workflow/graph/identifier_projection.rb +13 -0
- data/lib/smith/workflow/graph/metrics.rb +1 -3
- data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +15 -16
- data/lib/smith/workflow/graph/optimization_contract.rb +2 -2
- data/lib/smith/workflow/graph/orchestration_contract.rb +3 -3
- data/lib/smith/workflow/graph/reachability.rb +19 -31
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +17 -1
- data/lib/smith/workflow/graph/retry_policy_diagnostic.rb +36 -0
- data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness.rb +3 -44
- data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +2 -2
- data/lib/smith/workflow/graph/runtime_readiness_report_builder.rb +47 -0
- data/lib/smith/workflow/graph/runtime_readiness_traversal.rb +108 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +2 -2
- data/lib/smith/workflow/graph/transition_contract.rb +47 -0
- data/lib/smith/workflow/graph/transition_contract_attributes.rb +73 -0
- data/lib/smith/workflow/graph/transition_contract_configurations.rb +100 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +54 -12
- data/lib/smith/workflow/graph/transition_optimization_configuration.rb +62 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +4 -1
- data/lib/smith/workflow/graph/validator.rb +1 -1
- data/lib/smith/workflow/graph.rb +62 -4
- data/lib/smith/workflow/guardrail_integration.rb +23 -9
- data/lib/smith/workflow/identifier.rb +24 -0
- data/lib/smith/workflow/message_admission.rb +40 -0
- data/lib/smith/workflow/message_admission_boundary.rb +22 -0
- data/lib/smith/workflow/message_batch.rb +60 -0
- data/lib/smith/workflow/message_value_normalizer.rb +133 -0
- data/lib/smith/workflow/nested_execution.rb +10 -0
- data/lib/smith/workflow/orchestrator_worker.rb +3 -7
- data/lib/smith/workflow/parallel/cancellation_signal.rb +14 -5
- data/lib/smith/workflow/parallel/execution_context.rb +113 -0
- data/lib/smith/workflow/parallel/nested_execution.rb +124 -0
- data/lib/smith/workflow/parallel/root_execution.rb +91 -0
- data/lib/smith/workflow/parallel.rb +19 -22
- data/lib/smith/workflow/parallel_agent_binding.rb +24 -0
- data/lib/smith/workflow/parallel_execution.rb +7 -3
- data/lib/smith/workflow/persistence.rb +37 -8
- data/lib/smith/workflow/prepared_step.rb +122 -0
- data/lib/smith/workflow/prepared_step_dispatch.rb +88 -0
- data/lib/smith/workflow/prepared_step_execution_authorization.rb +116 -0
- data/lib/smith/workflow/prepared_step_execution_result.rb +49 -0
- data/lib/smith/workflow/prepared_step_execution_scope.rb +45 -0
- data/lib/smith/workflow/prepared_step_recovery.rb +38 -0
- data/lib/smith/workflow/retry_execution.rb +20 -11
- data/lib/smith/workflow/split_step_persistence/boundary.rb +35 -22
- data/lib/smith/workflow/split_step_persistence/boundary_reset.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/canonical_payload_digest.rb +55 -0
- data/lib/smith/workflow/split_step_persistence/checkpoint.rb +3 -5
- data/lib/smith/workflow/split_step_persistence/definition_boundary.rb +19 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_boundary.rb +16 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_claim.rb +118 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_confirmation.rb +70 -0
- data/lib/smith/workflow/split_step_persistence/dispatch_verification.rb +35 -0
- data/lib/smith/workflow/split_step_persistence/execution.rb +49 -47
- data/lib/smith/workflow/split_step_persistence/execution_authorization.rb +89 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_collector.rb +90 -0
- data/lib/smith/workflow/split_step_persistence/execution_binding_snapshot.rb +95 -0
- data/lib/smith/workflow/split_step_persistence/execution_result_capture.rb +48 -0
- data/lib/smith/workflow/split_step_persistence/execution_verification.rb +65 -0
- data/lib/smith/workflow/split_step_persistence/execution_workflow_snapshot.rb +77 -0
- data/lib/smith/workflow/split_step_persistence/inheritance.rb +3 -1
- data/lib/smith/workflow/split_step_persistence/payloads.rb +7 -3
- data/lib/smith/workflow/split_step_persistence/preparation.rb +30 -2
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +38 -12
- data/lib/smith/workflow/split_step_persistence/preparation_payload.rb +25 -0
- data/lib/smith/workflow/split_step_persistence/preparation_recovery.rb +10 -2
- data/lib/smith/workflow/split_step_persistence/recovery.rb +125 -0
- data/lib/smith/workflow/split_step_persistence/recovery_boundary.rb +56 -0
- data/lib/smith/workflow/split_step_persistence/recovery_class_methods.rb +14 -0
- data/lib/smith/workflow/split_step_persistence/replace_exact_signature.rb +46 -0
- data/lib/smith/workflow/split_step_persistence/restart_safe_adapter.rb +42 -0
- data/lib/smith/workflow/split_step_persistence/subclass_boundary.rb +50 -0
- data/lib/smith/workflow/split_step_persistence/transaction_identity.rb +45 -0
- data/lib/smith/workflow/split_step_persistence.rb +36 -0
- data/lib/smith/workflow/step_completion.rb +31 -0
- data/lib/smith/workflow/string_snapshot.rb +25 -0
- data/lib/smith/workflow/transition.rb +122 -133
- data/lib/smith/workflow/transition_actionability.rb +22 -0
- data/lib/smith/workflow.rb +44 -18
- data/lib/smith.rb +41 -2
- data/sig/smith.rbs +1 -0
- 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
|
-
|
|
19
|
+
validate_execution_primitive_conflict!("execute")
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
generator: generator,
|
|
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
|
-
|
|
100
|
+
validate_execution_primitive_conflict!("orchestrate")
|
|
88
101
|
validate_orchestrate_controls!(opts)
|
|
89
|
-
|
|
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
|
-
|
|
112
|
+
validate_execution_primitive_conflict!("fan_out")
|
|
113
|
+
|
|
114
|
+
config = { branches: normalize_fanout_branches!(branches) }.freeze
|
|
94
115
|
|
|
95
|
-
@fanout_config =
|
|
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
|
-
|
|
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:
|
|
106
|
-
max_delay:
|
|
107
|
-
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
|
-
|
|
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 =
|
|
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
|
|
153
|
-
|
|
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
|
|
163
|
-
|
|
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
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
raise WorkflowError, "transition cannot declare both
|
|
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
|
|
251
|
-
|
|
252
|
-
|
|
224
|
+
def validate_fanout_size!(branches)
|
|
225
|
+
limit = Smith.config.parallel_branch_limit
|
|
226
|
+
return if branches.length <= limit
|
|
253
227
|
|
|
254
|
-
|
|
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
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
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
|
data/lib/smith/workflow.rb
CHANGED
|
@@ -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, :
|
|
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 =
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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"
|