smith-agents 0.4.0 → 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 +52 -0
- data/README.md +40 -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/diagnostic.rb +33 -0
- data/lib/smith/workflow/graph/metrics.rb +33 -0
- data/lib/smith/workflow/graph/reachability.rb +59 -0
- data/lib/smith/workflow/graph/reachability_diagnostics.rb +47 -0
- data/lib/smith/workflow/graph/reference.rb +15 -0
- data/lib/smith/workflow/graph/report.rb +50 -0
- data/lib/smith/workflow/graph/state_diagnostics.rb +65 -0
- data/lib/smith/workflow/graph/targets.rb +38 -0
- data/lib/smith/workflow/graph/transition_diagnostics.rb +78 -0
- data/lib/smith/workflow/graph/transition_snapshot.rb +86 -0
- data/lib/smith/workflow/graph/validator.rb +50 -0
- data/lib/smith/workflow/graph.rb +36 -0
- data/lib/smith/workflow/graph_dsl.rb +18 -0
- 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 +6 -0
- metadata +17 -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,58 @@ 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
|
+
|
|
47
|
+
## [0.4.1] - 2026-06-28
|
|
48
|
+
|
|
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.
|
|
50
|
+
|
|
51
|
+
### Added
|
|
52
|
+
|
|
53
|
+
- `Smith::Workflow.graph` — returns a read-only inspection object for a workflow class.
|
|
54
|
+
- `Smith::Workflow.validate_graph` — returns a structured report with validity status, diagnostics, suggestions, transition snapshots, and graph metrics.
|
|
55
|
+
- Pre-runtime graph diagnostics for missing initial states, undefined transition states, unresolved `on_success` / `on_failure` targets, unresolved router route/fallback targets, target-state mismatch warnings, and unreachable-transition warnings.
|
|
56
|
+
- Transition snapshots that preserve declared names exactly and expose `name`, `from`, `to`, `kind`, success/failure targets, router routes, and router fallback.
|
|
57
|
+
|
|
58
|
+
### Test coverage
|
|
59
|
+
|
|
60
|
+
- Default suite: 862 examples, 0 failures.
|
|
61
|
+
- Touched Ruby files: 17 files inspected by RuboCop, 0 offenses.
|
|
62
|
+
|
|
11
63
|
## [0.4.0] - 2026-06-24
|
|
12
64
|
|
|
13
65
|
Two more host-ergonomic primitives that close the deferred-from-0.3.0 backlog: `Workflow.stuck_for?` for liveness probing and `Context.persist :auto` for write-tracked context persistence. Both are purely additive.
|
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. |
|
|
@@ -94,6 +103,21 @@ end
|
|
|
94
103
|
|
|
95
104
|
The full pattern guide with working examples for each lives in [`docs/PATTERNS.md`](docs/PATTERNS.md).
|
|
96
105
|
|
|
106
|
+
## Workflow Graph Inspection
|
|
107
|
+
|
|
108
|
+
Smith can inspect a workflow's declared graph without running agents or advancing state. This is useful for host apps that want to render, lint, or cache a workflow shape before execution.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
report = ReplyWorkflow.validate_graph
|
|
112
|
+
|
|
113
|
+
report.valid? # => true
|
|
114
|
+
report.transitions # => read-only transition snapshots
|
|
115
|
+
report.diagnostics # => errors and warnings for missing states or routes
|
|
116
|
+
report.metrics # => state, transition, reachability, and terminal-state counts
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Graph inspection is static and diagnostic-only. Runtime execution, persistence, progress projection, retries, and recovery remain host-owned concerns.
|
|
120
|
+
|
|
97
121
|
## Configuration
|
|
98
122
|
|
|
99
123
|
```ruby
|
|
@@ -215,6 +239,19 @@ Smith::Errors.retryable_classes
|
|
|
215
239
|
# => [Smith::AgentError, Smith::DeadlineExceeded] (for ActiveJob retry_on)
|
|
216
240
|
```
|
|
217
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
|
+
|
|
218
255
|
## Development
|
|
219
256
|
|
|
220
257
|
```bash
|
|
@@ -223,4 +260,5 @@ bundle exec rspec
|
|
|
223
260
|
bundle exec rubocop
|
|
224
261
|
```
|
|
225
262
|
|
|
226
|
-
|
|
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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Diagnostic
|
|
7
|
+
attr_reader :severity, :code, :message, :state, :transition, :target, :suggestion
|
|
8
|
+
|
|
9
|
+
def initialize(**attributes)
|
|
10
|
+
@severity = attributes.fetch(:severity)
|
|
11
|
+
@code = attributes.fetch(:code)
|
|
12
|
+
@message = attributes.fetch(:message)
|
|
13
|
+
@state = attributes[:state]
|
|
14
|
+
@transition = attributes[:transition]
|
|
15
|
+
@target = attributes[:target]
|
|
16
|
+
@suggestion = attributes[:suggestion]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_h
|
|
20
|
+
{
|
|
21
|
+
severity: severity,
|
|
22
|
+
code: code,
|
|
23
|
+
message: message,
|
|
24
|
+
state: state,
|
|
25
|
+
transition: transition,
|
|
26
|
+
target: target,
|
|
27
|
+
suggestion: suggestion
|
|
28
|
+
}.compact
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Metrics
|
|
7
|
+
attr_reader :graph, :reachable_transition_names
|
|
8
|
+
|
|
9
|
+
def initialize(graph, reachable_transition_names)
|
|
10
|
+
@graph = graph
|
|
11
|
+
@reachable_transition_names = reachable_transition_names
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{
|
|
16
|
+
states_count: graph.states.length,
|
|
17
|
+
transitions_count: graph.transitions.length,
|
|
18
|
+
reachable_transitions_count: reachable_transition_names.length,
|
|
19
|
+
terminal_states: terminal_states
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def terminal_states
|
|
26
|
+
graph.states.select do |state|
|
|
27
|
+
graph.transitions.values.none? { |transition| transition.from == state }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Reachability
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def transition_names
|
|
14
|
+
return [] unless graph.initial_state
|
|
15
|
+
|
|
16
|
+
reset_walk
|
|
17
|
+
drain_queue
|
|
18
|
+
@seen_transitions.keys
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def reset_walk
|
|
24
|
+
@seen_states = { graph.initial_state => true }
|
|
25
|
+
@seen_transitions = {}
|
|
26
|
+
@queue = [graph.initial_state]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def drain_queue
|
|
30
|
+
transitions_from(@queue.shift).each { |transition| visit_transition(transition) } until @queue.empty?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def visit_transition(transition)
|
|
34
|
+
return if @seen_transitions.key?(transition.name)
|
|
35
|
+
|
|
36
|
+
@seen_transitions[transition.name] = true
|
|
37
|
+
enqueue_state(transition.to)
|
|
38
|
+
Targets.for(transition).each { |target_name| visit_named_transition(target_name) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit_named_transition(target_name)
|
|
42
|
+
target = graph.transitions[target_name]
|
|
43
|
+
visit_transition(target) if target
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enqueue_state(state)
|
|
47
|
+
return if state.nil? || @seen_states.key?(state)
|
|
48
|
+
|
|
49
|
+
@seen_states[state] = true
|
|
50
|
+
@queue << state
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def transitions_from(state)
|
|
54
|
+
graph.transitions.values.select { |transition| transition.from == state }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|