smith-agents 0.4.1 → 0.4.2
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 +36 -0
- data/README.md +25 -2
- data/docs/PATTERNS.md +43 -4
- data/lib/smith/agent/lifecycle.rb +11 -7
- data/lib/smith/version.rb +1 -1
- data/lib/smith/workflow/budget_integration.rb +25 -5
- data/lib/smith/workflow/evaluator_optimizer.rb +13 -2
- data/lib/smith/workflow/execution.rb +6 -1
- data/lib/smith/workflow/fanout_execution.rb +119 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +24 -3
- data/lib/smith/workflow/guardrail_integration.rb +28 -10
- data/lib/smith/workflow/orchestrator_worker.rb +2 -1
- data/lib/smith/workflow/parallel/cancellation.rb +9 -0
- data/lib/smith/workflow/parallel.rb +6 -1
- data/lib/smith/workflow/parallel_execution.rb +3 -1
- data/lib/smith/workflow/retry_execution.rb +52 -0
- data/lib/smith/workflow/transition.rb +171 -21
- data/lib/smith.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c5a7f554819eb342aa2284fb010eba8126f22fba1348683a36fbc4fa9d5383f
|
|
4
|
+
data.tar.gz: b72e10a5415340c5b337751c0cc92f2f59ed0ddb62850e03746df217be374721
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75d149a61196f2c0cd22748ff6d5a6c4530bc9d91c61b163c027e44d555b9fd0f1ca5ef0a283c6025d6906b6d984923265fba05190fa3da37a60cfe864e5f6f8
|
|
7
|
+
data.tar.gz: 906bc56189df0f84ad62cb643ed8141a05780a328bf9a3770db55c575d71db86b94ad39e91517ba0e2d76f05942f84358dc234b2e685ad0701dba059bed65d36
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,42 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version
|
|
|
8
8
|
|
|
9
9
|
No unreleased changes.
|
|
10
10
|
|
|
11
|
+
## [0.4.2] - 2026-07-02
|
|
12
|
+
|
|
13
|
+
Patch release for bounded fan-out and retry workflow primitives. This remains
|
|
14
|
+
workflow-first and host-owned: Smith executes declared transitions and exposes
|
|
15
|
+
inspection metadata, while durable scheduling, long waits, tool adapter
|
|
16
|
+
contracts, and deployment packaging stay with the host application.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- `fan_out branches: {...}` transition DSL for bounded heterogeneous
|
|
21
|
+
multi-agent fan-out with stable branch keys and named aggregate results.
|
|
22
|
+
- `retry_on` transition DSL for bounded local retries using explicit error
|
|
23
|
+
classes or Smith's built-in retryability classifier.
|
|
24
|
+
- Graph inspection metadata for `:fanout` transitions and retry policy details.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- Fan-out branch execution preserves branch identity, branch-specific budgets,
|
|
29
|
+
agent guardrails, tool guardrails, deadlines, and usage accounting.
|
|
30
|
+
- Parallel/fan-out failure handling now prefers the initiating branch error over
|
|
31
|
+
cooperative cancellation errors.
|
|
32
|
+
- Failed-but-billable provider attempts are included in budget reconciliation
|
|
33
|
+
for retry, fallback, and fan-out settlement paths.
|
|
34
|
+
- Retry `max_delay` remains a hard cap even when jitter is configured.
|
|
35
|
+
|
|
36
|
+
### Test coverage
|
|
37
|
+
|
|
38
|
+
- Default suite: 880 examples, 0 failures.
|
|
39
|
+
- Practical gem-level execution probe covering heterogeneous `fan_out`,
|
|
40
|
+
same-agent parallel execution, `retry_on`, failed-but-billable budget
|
|
41
|
+
settlement, cancellation cause preservation, branch input guardrail ordering
|
|
42
|
+
before session preparation, graph metadata, and invalid declaration rejection.
|
|
43
|
+
- Added focused coverage for heterogeneous fan-out, retry policies,
|
|
44
|
+
failed-but-billable retry budget accounting, cancellation cause preservation,
|
|
45
|
+
and graph inspection metadata.
|
|
46
|
+
|
|
11
47
|
## [0.4.1] - 2026-06-28
|
|
12
48
|
|
|
13
49
|
Patch release for static workflow graph inspection. This is additive and diagnostic-only: Smith exposes declared workflow topology for hosts to render, lint, or cache without executing agents, advancing state, owning progress projection, or changing durability/recovery boundaries.
|
data/README.md
CHANGED
|
@@ -5,11 +5,19 @@ Workflow-first multi-agent orchestration for Ruby. Smith sits on top of `RubyLLM
|
|
|
5
5
|
> [!WARNING]
|
|
6
6
|
> Smith is pre-1.0. Expect contract tightening between minor versions. Pin to an exact version in production.
|
|
7
7
|
|
|
8
|
+
## Verification Discipline
|
|
9
|
+
|
|
10
|
+
Tests are required, but they are never enough for runtime primitive changes.
|
|
11
|
+
Every Smith workflow slice must also run practical gem-level execution probes.
|
|
12
|
+
When a host application consumes unreleased Smith changes, point that host app at
|
|
13
|
+
the local Smith repository and exercise the changed workflow paths in the host
|
|
14
|
+
environment before calling the slice complete.
|
|
15
|
+
|
|
8
16
|
## Installation
|
|
9
17
|
|
|
10
18
|
```ruby
|
|
11
19
|
# Gemfile
|
|
12
|
-
gem "smith-agents", "~> 0.2
|
|
20
|
+
gem "smith-agents", "~> 0.4.2", require: "smith"
|
|
13
21
|
```
|
|
14
22
|
|
|
15
23
|
```bash
|
|
@@ -87,6 +95,7 @@ end
|
|
|
87
95
|
| Pipeline | sequential transitions | Multi-step workflow with explicit success/failure routing. |
|
|
88
96
|
| Router | `route :classifier, routes: {...}` | Branch on a classifier agent's output. |
|
|
89
97
|
| Parallel fan-out | `execute :agent, parallel: true` | Concurrent agent calls under one ledger. |
|
|
98
|
+
| Heterogeneous fan-out | `fan_out branches: {...}` | Concurrent calls to different agents with named branch results. |
|
|
90
99
|
| Nested workflow | `workflow OtherWorkflow` | Reuse a subflow as one transition. |
|
|
91
100
|
| Evaluator-Optimizer | `optimize generator:, evaluator:, ...` | Generate-then-critique refinement loops. |
|
|
92
101
|
| Orchestrator-Worker | `orchestrate orchestrator:, worker:, ...` | Dynamic task fan-out with delegation rounds. |
|
|
@@ -230,6 +239,19 @@ Smith::Errors.retryable_classes
|
|
|
230
239
|
# => [Smith::AgentError, Smith::DeadlineExceeded] (for ActiveJob retry_on)
|
|
231
240
|
```
|
|
232
241
|
|
|
242
|
+
Workflow transitions can also declare a bounded local retry policy:
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
transition :draft, from: :idle, to: :done do
|
|
246
|
+
execute :writer
|
|
247
|
+
retry_on Smith::AgentError, attempts: 3, backoff: 0.1, max_delay: 1.0
|
|
248
|
+
end
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
When no classes are passed, `retry_on` uses `Smith::Errors.retryable?`.
|
|
252
|
+
This is a bounded local transition retry policy. Durable scheduling, long waits,
|
|
253
|
+
and external idempotency guarantees remain host-owned.
|
|
254
|
+
|
|
233
255
|
## Development
|
|
234
256
|
|
|
235
257
|
```bash
|
|
@@ -238,4 +260,5 @@ bundle exec rspec
|
|
|
238
260
|
bundle exec rubocop
|
|
239
261
|
```
|
|
240
262
|
|
|
241
|
-
|
|
263
|
+
880 examples, MIT licensed. See [`CHANGELOG.md`](CHANGELOG.md) for the current
|
|
264
|
+
release surface.
|
data/docs/PATTERNS.md
CHANGED
|
@@ -239,7 +239,47 @@ Why this is valuable:
|
|
|
239
239
|
- branch failures discard step output and route through normal failure handling
|
|
240
240
|
- prepared input is reused consistently across branches
|
|
241
241
|
|
|
242
|
-
## Example 6:
|
|
242
|
+
## Example 6: Heterogeneous Fan-Out
|
|
243
|
+
|
|
244
|
+
Use heterogeneous fan-out when different specialists should run concurrently and return named branch results under one workflow transition.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class StaticReviewAgent < Smith::Agent
|
|
248
|
+
register_as :static_review_agent
|
|
249
|
+
model "gpt-4.1-nano"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
class SecurityReviewAgent < Smith::Agent
|
|
253
|
+
register_as :security_review_agent
|
|
254
|
+
model "gpt-4.1-nano"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
class CodeReviewWorkflow < Smith::Workflow
|
|
258
|
+
initial_state :idle
|
|
259
|
+
state :reviewed
|
|
260
|
+
state :failed
|
|
261
|
+
|
|
262
|
+
transition :review, from: :idle, to: :reviewed do
|
|
263
|
+
fan_out branches: {
|
|
264
|
+
static: :static_review_agent,
|
|
265
|
+
security: :security_review_agent
|
|
266
|
+
}
|
|
267
|
+
on_failure :fail
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
What you get:
|
|
273
|
+
|
|
274
|
+
- stable branch identity in the step output
|
|
275
|
+
- branch-specific agent budgets, guardrails, tools, and model configuration
|
|
276
|
+
- one shared prepared input for the transition
|
|
277
|
+
- one shared transition result, so downstream joins remain explicit in the workflow
|
|
278
|
+
- branch failures discard partial output and route through normal failure handling
|
|
279
|
+
|
|
280
|
+
Use same-agent `parallel: true` for repeated homogeneous work. Use `fan_out` when branches are different agents with different responsibilities.
|
|
281
|
+
|
|
282
|
+
## Example 7: Nested Workflows
|
|
243
283
|
|
|
244
284
|
Use nested workflows when one part of the system deserves to be a reusable subflow with its own states and transitions.
|
|
245
285
|
|
|
@@ -281,7 +321,7 @@ What you get:
|
|
|
281
321
|
- nested best-known token/cost totals roll up into the parent result
|
|
282
322
|
- artifact scope is preserved across nesting
|
|
283
323
|
|
|
284
|
-
## Example
|
|
324
|
+
## Example 8: Evaluator-Optimizer
|
|
285
325
|
|
|
286
326
|
Use `optimize` when one agent generates candidates and another agent evaluates whether the result is acceptable.
|
|
287
327
|
|
|
@@ -335,7 +375,7 @@ Why this matters:
|
|
|
335
375
|
- exhaustion, malformed evaluator output, and convergence without acceptance fail normally
|
|
336
376
|
- costs and token usage from the full loop roll into the workflow totals
|
|
337
377
|
|
|
338
|
-
## Example
|
|
378
|
+
## Example 9: Orchestrator-Worker
|
|
339
379
|
|
|
340
380
|
Use `orchestrate` when you need an orchestrator that can emit structured tasks for workers and later decide when the system is done.
|
|
341
381
|
|
|
@@ -489,4 +529,3 @@ The yielded step object exposes a narrow, read-heavy surface:
|
|
|
489
529
|
- **Persistence**: Context writes and written outcomes survive `to_state`/`from_state`. The block itself (a Proc) lives on the class-level Transition and is never serialized.
|
|
490
530
|
- **Trace**: Emits `:deterministic_step` traces for start, success/routed, and failure. When a step writes an outcome, the trace includes `outcome_kind`.
|
|
491
531
|
- **Mutual exclusivity**: `compute` and `run` cannot be combined with `execute`, `route`, `workflow`, `optimize`, or `orchestrate`. A transition declares exactly one primary execution body.
|
|
492
|
-
|
|
@@ -46,10 +46,10 @@ module Smith
|
|
|
46
46
|
|
|
47
47
|
def build_model_chain(agent_class)
|
|
48
48
|
primary = if agent_class.respond_to?(:model_block) && agent_class.model_block
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
resolve_dynamic_model(agent_class)
|
|
50
|
+
else
|
|
51
|
+
agent_class.chat_kwargs[:model]
|
|
52
|
+
end
|
|
53
53
|
fallbacks = agent_class.fallback_models || []
|
|
54
54
|
[primary, *fallbacks].compact
|
|
55
55
|
end
|
|
@@ -103,8 +103,8 @@ module Smith
|
|
|
103
103
|
|
|
104
104
|
declared = agent_class.inputs || []
|
|
105
105
|
user_declared = declared - Smith::Agent::RESERVED_INPUT_NAMES
|
|
106
|
-
user_declared.
|
|
107
|
-
|
|
106
|
+
user_declared.to_h do |name|
|
|
107
|
+
[name, @context[name]]
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -131,7 +131,9 @@ module Smith
|
|
|
131
131
|
|
|
132
132
|
combined_contents = existing_system_contents + prepared_system_contents
|
|
133
133
|
return if combined_contents.empty?
|
|
134
|
-
|
|
134
|
+
unless combined_contents.all?(String)
|
|
135
|
+
return prepared_system_messages.each { |message| chat.add_message(message) }
|
|
136
|
+
end
|
|
135
137
|
|
|
136
138
|
if chat.respond_to?(:with_instructions)
|
|
137
139
|
chat.with_instructions(combined_contents.join("\n\n"))
|
|
@@ -178,6 +180,8 @@ module Smith
|
|
|
178
180
|
agent_result = Workflow::AgentResult.new(
|
|
179
181
|
content: nil, input_tokens: input, output_tokens: output, cost: cost, model_used: model_id
|
|
180
182
|
)
|
|
183
|
+
Thread.current[:smith_failed_agent_results] ||= []
|
|
184
|
+
Thread.current[:smith_failed_agent_results] << agent_result
|
|
181
185
|
record_usage(agent_class, agent_result, :failed_attempt, model_id)
|
|
182
186
|
end
|
|
183
187
|
|
data/lib/smith/version.rb
CHANGED
|
@@ -30,19 +30,33 @@ module Smith
|
|
|
30
30
|
def reconcile_branch_budget(ledger, estimates, agent_result: nil)
|
|
31
31
|
return unless ledger && estimates
|
|
32
32
|
|
|
33
|
-
actuals = extract_actuals(agent_result)
|
|
33
|
+
actuals = extract_actuals(agent_results_for_settlement(agent_result))
|
|
34
34
|
estimates.each do |dim, amt|
|
|
35
35
|
ledger.reconcile!(dim, amt, actual_for_dimension(dim, actuals[:tokens], actuals[:cost]))
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def extract_actuals(
|
|
39
|
+
def extract_actuals(agent_results)
|
|
40
|
+
results = Array(agent_results).compact
|
|
41
|
+
|
|
40
42
|
{
|
|
41
|
-
tokens: (
|
|
42
|
-
cost:
|
|
43
|
+
tokens: results.sum { |result| (result.input_tokens || 0) + (result.output_tokens || 0) },
|
|
44
|
+
cost: results.sum { |result| result.cost || 0 }
|
|
43
45
|
}
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
def agent_results_for_settlement(agent_result = nil)
|
|
49
|
+
[*failed_billable_attempts, agent_result].compact
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def failed_billable_attempts
|
|
53
|
+
Array(Thread.current[:smith_failed_agent_results])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def clear_failed_billable_attempts
|
|
57
|
+
Thread.current[:smith_failed_agent_results] = []
|
|
58
|
+
end
|
|
59
|
+
|
|
46
60
|
def actual_for_dimension(dim, actual_tokens, actual_cost = 0)
|
|
47
61
|
return actual_tokens if TOKEN_DIMENSIONS.include?(dim)
|
|
48
62
|
return actual_cost if COST_DIMENSIONS.include?(dim)
|
|
@@ -59,7 +73,7 @@ module Smith
|
|
|
59
73
|
def settle_budget_on_failure(ledger, estimates, agent_result)
|
|
60
74
|
return unless ledger && estimates
|
|
61
75
|
|
|
62
|
-
if agent_result
|
|
76
|
+
if agent_result || failed_billable_attempts.any?
|
|
63
77
|
reconcile_branch_budget(ledger, estimates, agent_result: agent_result)
|
|
64
78
|
else
|
|
65
79
|
release_branch_budget(ledger, estimates)
|
|
@@ -85,6 +99,12 @@ module Smith
|
|
|
85
99
|
{ branch: index, agent: transition.agent_name, output: agent_result ? agent_result.content : result }
|
|
86
100
|
end
|
|
87
101
|
|
|
102
|
+
def finalize_named_branch(branch_key, agent_name, result, ledger, reserved)
|
|
103
|
+
agent_result = result.is_a?(Workflow::AgentResult) ? result : nil
|
|
104
|
+
reconcile_branch_budget(ledger, reserved, agent_result: agent_result)
|
|
105
|
+
{ branch: branch_key, agent: agent_name, output: agent_result ? agent_result.content : result }
|
|
106
|
+
end
|
|
107
|
+
|
|
88
108
|
def estimate_for_dimension(dim, limit, branch_count)
|
|
89
109
|
return 0 unless BUDGET_DIMENSIONS.include?(dim)
|
|
90
110
|
|
|
@@ -91,7 +91,7 @@ module Smith
|
|
|
91
91
|
when Hash
|
|
92
92
|
deep_symbolize_evaluation(evaluation)
|
|
93
93
|
when String
|
|
94
|
-
parsed = (
|
|
94
|
+
parsed = parse_evaluation_json(evaluation)
|
|
95
95
|
parsed.is_a?(Hash) ? parsed : evaluation
|
|
96
96
|
else
|
|
97
97
|
evaluation
|
|
@@ -155,9 +155,12 @@ module Smith
|
|
|
155
155
|
|
|
156
156
|
def invoke_agent_with_budget(agent_class, prepared_input)
|
|
157
157
|
Thread.current[:smith_last_agent_result] = nil
|
|
158
|
+
clear_failed_billable_attempts
|
|
158
159
|
with_agent_context(agent_class) do
|
|
159
160
|
invoke_with_call_ledger(agent_class, prepared_input)
|
|
160
161
|
end
|
|
162
|
+
ensure
|
|
163
|
+
clear_failed_billable_attempts
|
|
161
164
|
end
|
|
162
165
|
|
|
163
166
|
def invoke_with_call_ledger(agent_class, prepared_input)
|
|
@@ -179,12 +182,20 @@ module Smith
|
|
|
179
182
|
# routes through on_threshold and returns the resulting value
|
|
180
183
|
# (non-nil terminates the loop with that as the step output).
|
|
181
184
|
def check_improvement_threshold!(evaluation, state, round)
|
|
182
|
-
|
|
185
|
+
unless stop_for_threshold?(evaluation[:score], state.last_score, state.config[:improvement_threshold])
|
|
186
|
+
return nil
|
|
187
|
+
end
|
|
183
188
|
|
|
184
189
|
handle_exit(state, :on_threshold,
|
|
185
190
|
"optimization improvement below threshold after round #{round + 1}")
|
|
186
191
|
end
|
|
187
192
|
|
|
193
|
+
def parse_evaluation_json(evaluation)
|
|
194
|
+
JSON.parse(evaluation, symbolize_names: true)
|
|
195
|
+
rescue JSON::ParserError
|
|
196
|
+
nil
|
|
197
|
+
end
|
|
198
|
+
|
|
188
199
|
def prepare_generator_input(prepared_input, round, prior_candidate, feedback)
|
|
189
200
|
return prepared_input if round.zero?
|
|
190
201
|
|
|
@@ -8,13 +8,15 @@ module Smith
|
|
|
8
8
|
include EvaluatorOptimizer
|
|
9
9
|
include OrchestratorWorker
|
|
10
10
|
include ParallelExecution
|
|
11
|
+
include FanoutExecution
|
|
12
|
+
include RetryExecution
|
|
11
13
|
include DeterministicExecution
|
|
12
14
|
|
|
13
15
|
private
|
|
14
16
|
|
|
15
17
|
def execute_step(transition)
|
|
16
18
|
setup_step_context
|
|
17
|
-
output = with_scoped_artifacts {
|
|
19
|
+
output = with_scoped_artifacts { run_with_retry_policy(transition) }
|
|
18
20
|
complete_step(transition, output)
|
|
19
21
|
rescue StandardError => e
|
|
20
22
|
@outcome = nil
|
|
@@ -40,6 +42,7 @@ module Smith
|
|
|
40
42
|
|
|
41
43
|
def run_guarded_step(transition)
|
|
42
44
|
return dispatch_step(transition) if transition.deterministic?
|
|
45
|
+
return run_guarded_fanout_step(transition) if transition.fanout?
|
|
43
46
|
|
|
44
47
|
agent_class = resolve_agent_class(transition)
|
|
45
48
|
run_input_guardrails(agent_class)
|
|
@@ -93,6 +96,7 @@ module Smith
|
|
|
93
96
|
|
|
94
97
|
def execute_serial_step(transition, prepared_input: nil)
|
|
95
98
|
Thread.current[:smith_last_agent_result] = nil
|
|
99
|
+
clear_failed_billable_attempts
|
|
96
100
|
ledger = effective_call_ledger
|
|
97
101
|
reserved = reserve_for_serial(transition, ledger)
|
|
98
102
|
begin
|
|
@@ -104,6 +108,7 @@ module Smith
|
|
|
104
108
|
ensure
|
|
105
109
|
settle_budget_on_failure(ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
|
|
106
110
|
Thread.current[:smith_last_agent_result] = nil
|
|
111
|
+
clear_failed_billable_attempts
|
|
107
112
|
end
|
|
108
113
|
end
|
|
109
114
|
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module FanoutExecution
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_guarded_fanout_step(transition)
|
|
9
|
+
branches = transition.fanout_config.fetch(:branches)
|
|
10
|
+
branch_agent_classes = fanout_agent_classes(transition, branches)
|
|
11
|
+
run_workflow_input_guardrails
|
|
12
|
+
run_fanout_agent_input_guardrails(branch_agent_classes)
|
|
13
|
+
prepared_input = build_session&.prepare!
|
|
14
|
+
output = execute_fanout_step(
|
|
15
|
+
transition,
|
|
16
|
+
branches: branches,
|
|
17
|
+
branch_agent_classes: branch_agent_classes,
|
|
18
|
+
prepared_input: prepared_input
|
|
19
|
+
)
|
|
20
|
+
run_workflow_output_guardrails(output)
|
|
21
|
+
output
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute_fanout_step(transition, branches: nil, branch_agent_classes: nil, prepared_input: nil)
|
|
25
|
+
branches ||= transition.fanout_config.fetch(:branches)
|
|
26
|
+
branch_agent_classes ||= fanout_agent_classes(transition, branches)
|
|
27
|
+
env = BranchEnv.new(
|
|
28
|
+
prepared_input: prepared_input,
|
|
29
|
+
guardrail_sources: nil,
|
|
30
|
+
scoped_store: propagate_scoped_artifacts,
|
|
31
|
+
branch_estimates: fanout_branch_estimates(branches, branch_agent_classes),
|
|
32
|
+
deadline: wall_clock_deadline
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
branch_calls = branches.map do |branch_key, agent_name|
|
|
36
|
+
proc do |signal|
|
|
37
|
+
run_fanout_branch(branch_key, agent_name, branch_agent_classes.fetch(branch_key), env, signal)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Parallel.execute(branches: branch_calls)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def run_fanout_branch(branch_key, agent_name, agent_class, env, signal)
|
|
45
|
+
setup_fanout_branch_context(env, @ledger, agent_class)
|
|
46
|
+
|
|
47
|
+
with_agent_context(agent_class) do
|
|
48
|
+
branch_ledger = effective_call_ledger
|
|
49
|
+
reserved = reserve_fanout_branch_call(branch_ledger, env.branch_estimates[branch_key], agent_class)
|
|
50
|
+
begin
|
|
51
|
+
result = guarded_fanout_branch_call(agent_class, env, signal)
|
|
52
|
+
finalize_named_branch(branch_key, agent_name, result, branch_ledger, reserved).tap { reserved = nil }
|
|
53
|
+
ensure
|
|
54
|
+
settle_budget_on_failure(branch_ledger, reserved, Thread.current[:smith_last_agent_result]) if reserved
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
ensure
|
|
58
|
+
teardown_branch_context(env)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def guarded_fanout_branch_call(agent_class, env, signal)
|
|
62
|
+
check_cancellation!(signal)
|
|
63
|
+
check_deadline!
|
|
64
|
+
result = agent_class.model_configured? ? invoke_agent(agent_class, env.prepared_input) : nil
|
|
65
|
+
output = result.is_a?(AgentResult) ? result.content : result
|
|
66
|
+
validate_data_volume!(output, agent_class)
|
|
67
|
+
run_agent_output_guardrails(output, agent_class)
|
|
68
|
+
check_cancellation!(signal)
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def setup_fanout_branch_context(env, ledger, agent_class)
|
|
73
|
+
setup_branch_context(env, ledger)
|
|
74
|
+
apply_tool_guardrails(agent_class)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def reserve_fanout_branch_call(branch_ledger, branch_estimates, agent_class)
|
|
78
|
+
return reserve_branch_budget(branch_ledger, branch_estimates: branch_estimates) if @ledger
|
|
79
|
+
|
|
80
|
+
reserve_serial_budget(branch_ledger, agent_budget: agent_class&.budget) if branch_ledger
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def fanout_agent_classes(transition, branches)
|
|
84
|
+
branches.to_h do |branch_key, agent_name|
|
|
85
|
+
[branch_key, resolve_fanout_agent_class(transition, agent_name)]
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def run_fanout_agent_input_guardrails(branch_agent_classes)
|
|
90
|
+
branch_agent_classes.each_value do |agent_class|
|
|
91
|
+
run_agent_input_guardrails(agent_class)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def fanout_branch_estimates(branches, branch_agent_classes)
|
|
96
|
+
return {} unless @ledger
|
|
97
|
+
|
|
98
|
+
branch_count = branches.length
|
|
99
|
+
branches.each_with_object({}) do |(branch_key, _agent_name), map|
|
|
100
|
+
agent_class = branch_agent_classes.fetch(branch_key)
|
|
101
|
+
map[branch_key] = compute_branch_estimates(
|
|
102
|
+
@ledger,
|
|
103
|
+
branch_count: branch_count,
|
|
104
|
+
agent_budget: agent_class&.budget
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def resolve_fanout_agent_class(transition, agent_name)
|
|
110
|
+
Agent::Registry.fetch!(
|
|
111
|
+
agent_name,
|
|
112
|
+
workflow_class: self.class,
|
|
113
|
+
transition_name: transition&.name,
|
|
114
|
+
role: :fanout_agent
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -10,10 +10,12 @@ module Smith
|
|
|
10
10
|
%i[nested_workflow nested?],
|
|
11
11
|
%i[optimizer optimized?],
|
|
12
12
|
%i[orchestrator orchestrated?],
|
|
13
|
+
%i[fanout fanout?],
|
|
13
14
|
%i[parallel parallel?]
|
|
14
15
|
].freeze
|
|
15
16
|
|
|
16
|
-
attr_reader :name, :from, :to, :kind, :success_transition, :failure_transition, :routes, :fallback
|
|
17
|
+
attr_reader :name, :from, :to, :kind, :success_transition, :failure_transition, :routes, :fallback,
|
|
18
|
+
:fanout_branches, :retry_policy
|
|
17
19
|
|
|
18
20
|
def self.from_transition(transition)
|
|
19
21
|
new(
|
|
@@ -24,10 +26,25 @@ module Smith
|
|
|
24
26
|
success_transition: transition.success_transition,
|
|
25
27
|
failure_transition: transition.failure_transition,
|
|
26
28
|
routes: transition.router_config&.fetch(:routes, nil),
|
|
27
|
-
fallback: transition.router_config&.fetch(:fallback, nil)
|
|
29
|
+
fallback: transition.router_config&.fetch(:fallback, nil),
|
|
30
|
+
fanout_branches: transition.fanout_config&.fetch(:branches, nil),
|
|
31
|
+
retry_policy: retry_policy_for(transition)
|
|
28
32
|
)
|
|
29
33
|
end
|
|
30
34
|
|
|
35
|
+
def self.retry_policy_for(transition)
|
|
36
|
+
config = transition.retry_config
|
|
37
|
+
return unless config
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
attempts: config.fetch(:attempts),
|
|
41
|
+
error_classes: config.fetch(:error_classes).map(&:name),
|
|
42
|
+
backoff: config.fetch(:backoff),
|
|
43
|
+
max_delay: config[:max_delay],
|
|
44
|
+
jitter: config.fetch(:jitter)
|
|
45
|
+
}.compact
|
|
46
|
+
end
|
|
47
|
+
|
|
31
48
|
def self.kind_for(transition)
|
|
32
49
|
kind = KINDS.find { |_name, predicate| transition.public_send(predicate) }
|
|
33
50
|
return kind.first if kind
|
|
@@ -45,6 +62,8 @@ module Smith
|
|
|
45
62
|
@failure_transition = attributes[:failure_transition]
|
|
46
63
|
@routes = attributes[:routes]
|
|
47
64
|
@fallback = attributes[:fallback]
|
|
65
|
+
@fanout_branches = attributes[:fanout_branches]
|
|
66
|
+
@retry_policy = attributes[:retry_policy]
|
|
48
67
|
end
|
|
49
68
|
|
|
50
69
|
def to_h
|
|
@@ -56,7 +75,9 @@ module Smith
|
|
|
56
75
|
success_transition: success_transition,
|
|
57
76
|
failure_transition: failure_transition,
|
|
58
77
|
routes: routes,
|
|
59
|
-
fallback: fallback
|
|
78
|
+
fallback: fallback,
|
|
79
|
+
fanout_branches: fanout_branches,
|
|
80
|
+
retry_policy: retry_policy
|
|
60
81
|
}.compact
|
|
61
82
|
end
|
|
62
83
|
end
|
|
@@ -6,34 +6,52 @@ module Smith
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
def apply_tool_guardrails(agent_class)
|
|
9
|
-
sources =
|
|
9
|
+
sources = tool_guardrail_sources(agent_class)
|
|
10
10
|
Tool.current_guardrails = sources.empty? ? nil : sources
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def run_input_guardrails(agent_class)
|
|
14
|
+
run_workflow_input_guardrails
|
|
15
|
+
run_agent_input_guardrails(agent_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run_output_guardrails(output, agent_class)
|
|
19
|
+
run_workflow_output_guardrails(output)
|
|
20
|
+
run_agent_output_guardrails(output, agent_class)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def handle_step_failure(transition, _error)
|
|
24
|
+
failure_name = transition.failure_transition
|
|
25
|
+
return unless failure_name
|
|
26
|
+
|
|
27
|
+
fail_transition = self.class.find_transition(failure_name)
|
|
28
|
+
return unless fail_transition
|
|
29
|
+
|
|
30
|
+
@state = fail_transition.to
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def run_workflow_input_guardrails
|
|
14
34
|
wf_guardrails = self.class.guardrails
|
|
15
35
|
Guardrails::Runner.run_inputs(wf_guardrails, @context) if wf_guardrails
|
|
36
|
+
end
|
|
16
37
|
|
|
38
|
+
def run_agent_input_guardrails(agent_class)
|
|
17
39
|
agent_guardrails = agent_class&.guardrails
|
|
18
40
|
Guardrails::Runner.run_inputs(agent_guardrails, @context) if agent_guardrails
|
|
19
41
|
end
|
|
20
42
|
|
|
21
|
-
def
|
|
43
|
+
def run_workflow_output_guardrails(output)
|
|
22
44
|
wf_guardrails = self.class.guardrails
|
|
23
45
|
Guardrails::Runner.run_outputs(wf_guardrails, output) if wf_guardrails
|
|
46
|
+
end
|
|
24
47
|
|
|
48
|
+
def run_agent_output_guardrails(output, agent_class)
|
|
25
49
|
agent_guardrails = agent_class&.guardrails
|
|
26
50
|
Guardrails::Runner.run_outputs(agent_guardrails, output) if agent_guardrails
|
|
27
51
|
end
|
|
28
52
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
return unless failure_name
|
|
32
|
-
|
|
33
|
-
fail_transition = self.class.find_transition(failure_name)
|
|
34
|
-
return unless fail_transition
|
|
35
|
-
|
|
36
|
-
@state = fail_transition.to
|
|
53
|
+
def tool_guardrail_sources(agent_class)
|
|
54
|
+
[self.class.guardrails, agent_class&.guardrails].compact
|
|
37
55
|
end
|
|
38
56
|
end
|
|
39
57
|
end
|
|
@@ -23,7 +23,8 @@ module Smith
|
|
|
23
23
|
private
|
|
24
24
|
|
|
25
25
|
def dispatch_step(transition, prepared_input: nil)
|
|
26
|
-
if transition.
|
|
26
|
+
if transition.fanout? then execute_fanout_step(transition, prepared_input: prepared_input)
|
|
27
|
+
elsif transition.parallel? then execute_parallel_step(transition, prepared_input: prepared_input)
|
|
27
28
|
elsif transition.nested? then execute_nested_workflow(transition)
|
|
28
29
|
elsif transition.optimized? then execute_optimization_step(transition, prepared_input: prepared_input)
|
|
29
30
|
elsif transition.orchestrated? then execute_orchestration_step(transition, prepared_input: prepared_input)
|
|
@@ -39,12 +39,17 @@ module Smith
|
|
|
39
39
|
fulfilled, values, reasons = Concurrent::Promises.zip(*futures).result
|
|
40
40
|
|
|
41
41
|
unless fulfilled
|
|
42
|
-
error = reasons
|
|
42
|
+
error = preferred_error(reasons)
|
|
43
43
|
raise error
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
values
|
|
47
47
|
end
|
|
48
|
+
|
|
49
|
+
def self.preferred_error(reasons)
|
|
50
|
+
errors = reasons.compact
|
|
51
|
+
errors.find { |error| !error.is_a?(Cancellation) } || errors.first
|
|
52
|
+
end
|
|
48
53
|
end
|
|
49
54
|
end
|
|
50
55
|
end
|
|
@@ -50,10 +50,12 @@ module Smith
|
|
|
50
50
|
Tool.current_ledger = ledger
|
|
51
51
|
Tool.current_tool_result_collector = tool_result_collector
|
|
52
52
|
Thread.current[:smith_last_agent_result] = nil
|
|
53
|
+
clear_failed_billable_attempts
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
def teardown_branch_context(env)
|
|
56
57
|
Thread.current[:smith_last_agent_result] = nil
|
|
58
|
+
clear_failed_billable_attempts
|
|
57
59
|
Tool.current_ledger = nil
|
|
58
60
|
Tool.current_tool_result_collector = nil
|
|
59
61
|
env.teardown_thread
|
|
@@ -68,7 +70,7 @@ module Smith
|
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
def check_cancellation!(signal)
|
|
71
|
-
raise
|
|
73
|
+
raise Parallel::Cancellation, "cancelled" if signal.cancelled?
|
|
72
74
|
end
|
|
73
75
|
end
|
|
74
76
|
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module RetryExecution
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_with_retry_policy(transition)
|
|
9
|
+
config = transition.retry_config
|
|
10
|
+
return run_guarded_step(transition) unless config
|
|
11
|
+
|
|
12
|
+
attempt = 0
|
|
13
|
+
begin
|
|
14
|
+
attempt += 1
|
|
15
|
+
run_guarded_step(transition)
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
raise unless retry_transition_error?(config, e, attempt)
|
|
18
|
+
|
|
19
|
+
sleep_for_retry(config, attempt)
|
|
20
|
+
retry
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def retry_transition_error?(config, error, attempt)
|
|
25
|
+
return false if attempt >= config.fetch(:attempts)
|
|
26
|
+
|
|
27
|
+
classes = config.fetch(:error_classes)
|
|
28
|
+
if classes.any?
|
|
29
|
+
classes.any? { |error_class| error.is_a?(error_class) }
|
|
30
|
+
else
|
|
31
|
+
Smith::Errors.retryable?(error)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sleep_for_retry(config, failed_attempt)
|
|
36
|
+
delay = retry_delay(config, failed_attempt)
|
|
37
|
+
sleep(delay) if delay.positive?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def retry_delay(config, failed_attempt)
|
|
41
|
+
delay = config.fetch(:backoff) * (2**[failed_attempt - 1, 0].max)
|
|
42
|
+
max_delay = config[:max_delay]
|
|
43
|
+
delay = [delay, max_delay].min if max_delay
|
|
44
|
+
|
|
45
|
+
jitter = config.fetch(:jitter)
|
|
46
|
+
delay += rand * jitter if jitter.positive?
|
|
47
|
+
delay = [delay, max_delay].min if max_delay
|
|
48
|
+
delay
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -5,7 +5,7 @@ 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
|
-
:deterministic_block, :deterministic_kind
|
|
8
|
+
:fanout_config, :retry_config, :deterministic_block, :deterministic_kind
|
|
9
9
|
|
|
10
10
|
def initialize(name, from:, to:, &)
|
|
11
11
|
@name = name
|
|
@@ -15,7 +15,7 @@ module Smith
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def execute(agent_name, **opts)
|
|
18
|
-
|
|
18
|
+
validate_execute_conflicts!
|
|
19
19
|
|
|
20
20
|
@agent_name = agent_name
|
|
21
21
|
@agent_opts = opts
|
|
@@ -30,7 +30,7 @@ module Smith
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
def route(agent_name, routes:, confidence_threshold:, fallback:)
|
|
33
|
-
|
|
33
|
+
validate_route_conflicts!
|
|
34
34
|
|
|
35
35
|
@agent_name = agent_name
|
|
36
36
|
@router_config = { routes: routes, confidence_threshold: confidence_threshold, fallback: fallback }
|
|
@@ -39,9 +39,16 @@ module Smith
|
|
|
39
39
|
def workflow(klass)
|
|
40
40
|
raise WorkflowError, "workflow binding must be a Class" unless klass.is_a?(Class)
|
|
41
41
|
raise WorkflowError, "workflow binding must be a Smith::Workflow subclass" unless klass < Workflow
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
|
|
43
|
+
validate_conflicts!(
|
|
44
|
+
"workflow",
|
|
45
|
+
[
|
|
46
|
+
["execute", @agent_name && !@router_config],
|
|
47
|
+
["route", @router_config],
|
|
48
|
+
["compute/run", @deterministic_block],
|
|
49
|
+
["fan_out", @fanout_config]
|
|
50
|
+
]
|
|
51
|
+
)
|
|
45
52
|
|
|
46
53
|
@workflow_class = klass
|
|
47
54
|
end
|
|
@@ -77,6 +84,25 @@ module Smith
|
|
|
77
84
|
@orchestrator_config = opts
|
|
78
85
|
end
|
|
79
86
|
|
|
87
|
+
def fan_out(branches:)
|
|
88
|
+
validate_fanout_conflicts!
|
|
89
|
+
|
|
90
|
+
@fanout_config = { branches: normalize_fanout_branches!(branches) }
|
|
91
|
+
end
|
|
92
|
+
alias fanout fan_out
|
|
93
|
+
|
|
94
|
+
def retry_on(*error_classes, attempts:, backoff: 0, max_delay: nil, jitter: 0)
|
|
95
|
+
validate_retry_controls!(error_classes, attempts:, backoff:, max_delay:, jitter:)
|
|
96
|
+
|
|
97
|
+
@retry_config = {
|
|
98
|
+
error_classes: error_classes.freeze,
|
|
99
|
+
attempts: attempts,
|
|
100
|
+
backoff: Float(backoff),
|
|
101
|
+
max_delay: max_delay.nil? ? nil : Float(max_delay),
|
|
102
|
+
jitter: Float(jitter)
|
|
103
|
+
}.freeze
|
|
104
|
+
end
|
|
105
|
+
|
|
80
106
|
%i[compute run].each do |method_name|
|
|
81
107
|
define_method(method_name) do |&block|
|
|
82
108
|
validate_deterministic_conflicts!
|
|
@@ -95,6 +121,10 @@ module Smith
|
|
|
95
121
|
!@orchestrator_config.nil?
|
|
96
122
|
end
|
|
97
123
|
|
|
124
|
+
def fanout?
|
|
125
|
+
!@fanout_config.nil?
|
|
126
|
+
end
|
|
127
|
+
|
|
98
128
|
def optimized?
|
|
99
129
|
!@optimization_config.nil?
|
|
100
130
|
end
|
|
@@ -113,28 +143,148 @@ module Smith
|
|
|
113
143
|
|
|
114
144
|
private
|
|
115
145
|
|
|
146
|
+
def validate_execute_conflicts!
|
|
147
|
+
validate_conflicts!(
|
|
148
|
+
"execute",
|
|
149
|
+
[
|
|
150
|
+
["compute/run", @deterministic_block],
|
|
151
|
+
["fan_out", @fanout_config]
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def validate_route_conflicts!
|
|
157
|
+
validate_conflicts!(
|
|
158
|
+
"route",
|
|
159
|
+
[
|
|
160
|
+
["compute/run", @deterministic_block],
|
|
161
|
+
["fan_out", @fanout_config]
|
|
162
|
+
]
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
116
166
|
def validate_deterministic_conflicts!
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
167
|
+
validate_conflicts!(
|
|
168
|
+
"compute/run",
|
|
169
|
+
[
|
|
170
|
+
["execute", @agent_name && !@router_config],
|
|
171
|
+
["route", @router_config],
|
|
172
|
+
["workflow", @workflow_class],
|
|
173
|
+
["optimize", @optimization_config],
|
|
174
|
+
["orchestrate", @orchestrator_config],
|
|
175
|
+
["fan_out", @fanout_config]
|
|
176
|
+
]
|
|
177
|
+
)
|
|
122
178
|
raise WorkflowError, "transition cannot declare both compute and run" if @deterministic_block
|
|
123
179
|
end
|
|
124
180
|
|
|
125
181
|
def validate_optimize_conflicts!
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
validate_conflicts!(
|
|
183
|
+
"optimize",
|
|
184
|
+
[
|
|
185
|
+
["execute", @agent_name && !@router_config],
|
|
186
|
+
["route", @router_config],
|
|
187
|
+
["workflow", @workflow_class],
|
|
188
|
+
["compute/run", @deterministic_block],
|
|
189
|
+
["fan_out", @fanout_config]
|
|
190
|
+
]
|
|
191
|
+
)
|
|
130
192
|
end
|
|
131
193
|
|
|
132
194
|
def validate_orchestrate_conflicts!
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
195
|
+
validate_conflicts!(
|
|
196
|
+
"orchestrate",
|
|
197
|
+
[
|
|
198
|
+
["execute", @agent_name && !@router_config],
|
|
199
|
+
["route", @router_config],
|
|
200
|
+
["workflow", @workflow_class],
|
|
201
|
+
["optimize", @optimization_config],
|
|
202
|
+
["compute/run", @deterministic_block],
|
|
203
|
+
["fan_out", @fanout_config]
|
|
204
|
+
]
|
|
205
|
+
)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def validate_fanout_conflicts!
|
|
209
|
+
validate_conflicts!(
|
|
210
|
+
"fan_out",
|
|
211
|
+
[
|
|
212
|
+
["execute", @agent_name && !@router_config],
|
|
213
|
+
["route", @router_config],
|
|
214
|
+
["workflow", @workflow_class],
|
|
215
|
+
["optimize", @optimization_config],
|
|
216
|
+
["orchestrate", @orchestrator_config],
|
|
217
|
+
["compute/run", @deterministic_block]
|
|
218
|
+
]
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def validate_conflicts!(primitive, conflicts)
|
|
223
|
+
conflicts.each do |other, present|
|
|
224
|
+
raise WorkflowError, "transition cannot declare both #{primitive} and #{other}" if present
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def normalize_fanout_branches!(branches)
|
|
229
|
+
raise WorkflowError, "fan_out branches must be a Hash" unless branches.is_a?(Hash)
|
|
230
|
+
raise WorkflowError, "fan_out requires at least one branch" if branches.empty?
|
|
231
|
+
|
|
232
|
+
normalized = branches.each_with_object({}) do |(branch_key, agent_name), map|
|
|
233
|
+
key = normalize_fanout_branch_key!(branch_key)
|
|
234
|
+
agent = normalize_fanout_agent_name!(agent_name, key)
|
|
235
|
+
raise WorkflowError, "fan_out branch #{key.inspect} is duplicated" if map.key?(key)
|
|
236
|
+
|
|
237
|
+
map[key] = agent
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
validate_distinct_fanout_agents!(normalized)
|
|
241
|
+
normalized.freeze
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def normalize_fanout_branch_key!(branch_key)
|
|
245
|
+
key = branch_key.to_s.strip
|
|
246
|
+
raise WorkflowError, "fan_out branch keys must not be blank" if key.empty?
|
|
247
|
+
|
|
248
|
+
key.to_sym
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def normalize_fanout_agent_name!(agent_name, branch_key)
|
|
252
|
+
value = agent_name.to_s.strip
|
|
253
|
+
raise WorkflowError, "fan_out branch #{branch_key.inspect} must declare an agent" if value.empty?
|
|
254
|
+
|
|
255
|
+
value.to_sym
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def validate_distinct_fanout_agents!(branches)
|
|
259
|
+
duplicates = branches.values.tally.select { |_agent, count| count > 1 }.keys
|
|
260
|
+
return if duplicates.empty?
|
|
261
|
+
|
|
262
|
+
raise WorkflowError, "fan_out branch agents must be distinct: #{duplicates.map(&:inspect).join(", ")}"
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def validate_retry_controls!(error_classes, attempts:, backoff:, max_delay:, jitter:)
|
|
266
|
+
unless attempts.is_a?(Integer) && attempts.positive?
|
|
267
|
+
raise WorkflowError, "retry_on attempts must be a positive integer"
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
error_classes.each do |error_class|
|
|
271
|
+
next if error_class.is_a?(Class) && error_class <= StandardError
|
|
272
|
+
|
|
273
|
+
raise WorkflowError, "retry_on error classes must inherit from StandardError"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
validate_non_negative_numeric!(:backoff, backoff)
|
|
277
|
+
validate_non_negative_numeric!(:jitter, jitter)
|
|
278
|
+
validate_non_negative_numeric!(:max_delay, max_delay) unless max_delay.nil?
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def validate_non_negative_numeric!(name, value)
|
|
282
|
+
numeric = Float(value)
|
|
283
|
+
return if numeric >= 0.0
|
|
284
|
+
|
|
285
|
+
raise WorkflowError, "retry_on #{name} must be non-negative"
|
|
286
|
+
rescue TypeError, ArgumentError
|
|
287
|
+
raise WorkflowError, "retry_on #{name} must be numeric"
|
|
138
288
|
end
|
|
139
289
|
|
|
140
290
|
def validate_orchestrate_controls!(opts)
|
|
@@ -177,7 +327,7 @@ module Smith
|
|
|
177
327
|
raise WorkflowError, "optimize max_rounds must be a positive integer"
|
|
178
328
|
end
|
|
179
329
|
|
|
180
|
-
VALID_EXIT_MODES = [
|
|
330
|
+
VALID_EXIT_MODES = %i[raise return_last].freeze
|
|
181
331
|
private_constant :VALID_EXIT_MODES
|
|
182
332
|
|
|
183
333
|
def validate_optimize_exit_modes!(on_exhaustion:, on_converged:, on_threshold:)
|
data/lib/smith.rb
CHANGED
|
@@ -243,6 +243,8 @@ require_relative "smith/workflow/nested_execution"
|
|
|
243
243
|
require_relative "smith/workflow/evaluator_optimizer"
|
|
244
244
|
require_relative "smith/workflow/orchestrator_worker"
|
|
245
245
|
require_relative "smith/workflow/parallel_execution"
|
|
246
|
+
require_relative "smith/workflow/fanout_execution"
|
|
247
|
+
require_relative "smith/workflow/retry_execution"
|
|
246
248
|
require_relative "smith/workflow/deterministic_step"
|
|
247
249
|
require_relative "smith/workflow/deterministic_execution"
|
|
248
250
|
require_relative "smith/workflow/execution"
|
|
@@ -252,6 +254,7 @@ require_relative "smith/workflow/execution_frame"
|
|
|
252
254
|
require_relative "smith/workflow/pipeline"
|
|
253
255
|
require_relative "smith/workflow/router"
|
|
254
256
|
require_relative "smith/workflow/parallel"
|
|
257
|
+
require_relative "smith/workflow/parallel/cancellation"
|
|
255
258
|
|
|
256
259
|
# Conditional Rails integration
|
|
257
260
|
require_relative "smith/railtie" if defined?(Rails::Railtie)
|
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.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Ralak
|
|
@@ -219,6 +219,7 @@ files:
|
|
|
219
219
|
- lib/smith/workflow/event_integration.rb
|
|
220
220
|
- lib/smith/workflow/execution.rb
|
|
221
221
|
- lib/smith/workflow/execution_frame.rb
|
|
222
|
+
- lib/smith/workflow/fanout_execution.rb
|
|
222
223
|
- lib/smith/workflow/graph.rb
|
|
223
224
|
- lib/smith/workflow/graph/diagnostic.rb
|
|
224
225
|
- lib/smith/workflow/graph/metrics.rb
|
|
@@ -236,9 +237,11 @@ files:
|
|
|
236
237
|
- lib/smith/workflow/nested_execution.rb
|
|
237
238
|
- lib/smith/workflow/orchestrator_worker.rb
|
|
238
239
|
- lib/smith/workflow/parallel.rb
|
|
240
|
+
- lib/smith/workflow/parallel/cancellation.rb
|
|
239
241
|
- lib/smith/workflow/parallel_execution.rb
|
|
240
242
|
- lib/smith/workflow/persistence.rb
|
|
241
243
|
- lib/smith/workflow/pipeline.rb
|
|
244
|
+
- lib/smith/workflow/retry_execution.rb
|
|
242
245
|
- lib/smith/workflow/router.rb
|
|
243
246
|
- lib/smith/workflow/transition.rb
|
|
244
247
|
- script/profile_tool_results.rb
|