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
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class ReachabilityDiagnostics
|
|
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_a
|
|
15
|
+
(graph.transitions.keys - reachable_transition_names).filter_map do |transition_name|
|
|
16
|
+
next if auto_fail_transition?(transition_name)
|
|
17
|
+
|
|
18
|
+
diagnostic_for(transition_name)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def auto_fail_transition?(transition_name)
|
|
25
|
+
transition_name == :fail && graph.transitions[transition_name]&.from.nil?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def diagnostic_for(transition_name)
|
|
29
|
+
transition = graph.transitions.fetch(transition_name)
|
|
30
|
+
Diagnostic.new(
|
|
31
|
+
severity: :warning,
|
|
32
|
+
code: :unreachable_transition,
|
|
33
|
+
transition: transition_name,
|
|
34
|
+
state: transition.from,
|
|
35
|
+
message: "Transition #{ref(transition_name)} is not reachable from " \
|
|
36
|
+
"initial_state #{ref(graph.initial_state)}.",
|
|
37
|
+
suggestion: "Connect transition #{ref(transition_name)} from a reachable state or remove it."
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ref(value)
|
|
42
|
+
Reference.format(value)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Report
|
|
7
|
+
attr_reader :status, :workflow_class, :initial_state, :states, :transitions, :diagnostics, :metrics
|
|
8
|
+
|
|
9
|
+
def initialize(**attributes)
|
|
10
|
+
@status = attributes.fetch(:status)
|
|
11
|
+
@workflow_class = attributes.fetch(:workflow_class)
|
|
12
|
+
@initial_state = attributes.fetch(:initial_state)
|
|
13
|
+
@states = attributes.fetch(:states)
|
|
14
|
+
@transitions = attributes.fetch(:transitions)
|
|
15
|
+
@diagnostics = attributes.fetch(:diagnostics)
|
|
16
|
+
@metrics = attributes.fetch(:metrics)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def valid?
|
|
20
|
+
errors.empty?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def errors
|
|
24
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :error }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def warnings
|
|
28
|
+
diagnostics.select { |diagnostic| diagnostic.severity == :warning }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def suggestions
|
|
32
|
+
diagnostics.map(&:suggestion).compact.uniq
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
status: status,
|
|
38
|
+
workflow_class: workflow_class,
|
|
39
|
+
initial_state: initial_state,
|
|
40
|
+
states: states,
|
|
41
|
+
transitions: transitions.map(&:to_h),
|
|
42
|
+
diagnostics: diagnostics.map(&:to_h),
|
|
43
|
+
suggestions: suggestions,
|
|
44
|
+
metrics: metrics
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class StateDiagnostics
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_a
|
|
14
|
+
[
|
|
15
|
+
*initial_state_diagnostics,
|
|
16
|
+
*state_reference_diagnostics
|
|
17
|
+
]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def initial_state_diagnostics
|
|
23
|
+
return [] if graph.initial_state && graph.states.include?(graph.initial_state)
|
|
24
|
+
|
|
25
|
+
[
|
|
26
|
+
Diagnostic.new(
|
|
27
|
+
severity: :error,
|
|
28
|
+
code: :missing_initial_state,
|
|
29
|
+
state: graph.initial_state,
|
|
30
|
+
message: "Workflow initial_state is not declared as a state.",
|
|
31
|
+
suggestion: "Declare an initial_state and ensure it is included in the workflow states."
|
|
32
|
+
)
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def state_reference_diagnostics
|
|
37
|
+
graph.transitions.values.flat_map do |transition|
|
|
38
|
+
[
|
|
39
|
+
undefined_state_diagnostic(transition, :from, transition.from),
|
|
40
|
+
undefined_state_diagnostic(transition, :to, transition.to)
|
|
41
|
+
].compact
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def undefined_state_diagnostic(transition, edge, state)
|
|
46
|
+
return if edge == :from && state.nil?
|
|
47
|
+
return if graph.states.include?(state)
|
|
48
|
+
|
|
49
|
+
Diagnostic.new(
|
|
50
|
+
severity: :error,
|
|
51
|
+
code: :"undefined_#{edge}_state",
|
|
52
|
+
state: state,
|
|
53
|
+
transition: transition.name,
|
|
54
|
+
message: "Transition #{ref(transition.name)} references undefined #{edge} state #{ref(state)}.",
|
|
55
|
+
suggestion: "Declare state #{ref(state)} or update transition #{ref(transition.name)}."
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def ref(value)
|
|
60
|
+
Reference.format(value)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Targets
|
|
7
|
+
attr_reader :transition
|
|
8
|
+
|
|
9
|
+
def self.for(transition)
|
|
10
|
+
new(transition).names
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.router_for(transition)
|
|
14
|
+
new(transition).router_names
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(transition)
|
|
18
|
+
@transition = transition
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def names
|
|
22
|
+
names = [transition.success_transition, transition.failure_transition]
|
|
23
|
+
names.concat(router_names)
|
|
24
|
+
names.compact.uniq
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def router_names
|
|
28
|
+
return [] unless transition.router_config
|
|
29
|
+
|
|
30
|
+
[
|
|
31
|
+
*transition.router_config.fetch(:routes).values,
|
|
32
|
+
transition.router_config.fetch(:fallback)
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class TransitionDiagnostics
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_a
|
|
14
|
+
graph.transitions.values.flat_map do |transition|
|
|
15
|
+
[
|
|
16
|
+
transition_target_diagnostic(transition, :success_transition, transition.success_transition),
|
|
17
|
+
transition_target_diagnostic(transition, :failure_transition, transition.failure_transition),
|
|
18
|
+
*router_target_diagnostics(transition),
|
|
19
|
+
*target_state_mismatch_diagnostics(transition)
|
|
20
|
+
].compact
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def transition_target_diagnostic(transition, code, target)
|
|
27
|
+
return if target.nil?
|
|
28
|
+
return if graph.transitions.key?(target)
|
|
29
|
+
|
|
30
|
+
Diagnostic.new(
|
|
31
|
+
severity: :error,
|
|
32
|
+
code: :"unresolved_#{code}",
|
|
33
|
+
transition: transition.name,
|
|
34
|
+
target: target,
|
|
35
|
+
message: "Transition #{ref(transition.name)} references missing transition #{ref(target)}.",
|
|
36
|
+
suggestion: "Declare transition #{ref(target)} or update transition #{ref(transition.name)}."
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def router_target_diagnostics(transition)
|
|
41
|
+
return [] unless transition.router_config
|
|
42
|
+
|
|
43
|
+
Targets.router_for(transition).filter_map do |target|
|
|
44
|
+
transition_target_diagnostic(transition, :router_target, target)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def target_state_mismatch_diagnostics(transition)
|
|
49
|
+
Targets.for(transition).filter_map do |target_name|
|
|
50
|
+
target = graph.transitions[target_name]
|
|
51
|
+
next unless target
|
|
52
|
+
next if target.from.nil? || target.from == transition.to
|
|
53
|
+
|
|
54
|
+
mismatch_diagnostic(transition, target_name, target)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def mismatch_diagnostic(transition, target_name, target)
|
|
59
|
+
Diagnostic.new(
|
|
60
|
+
severity: :warning,
|
|
61
|
+
code: :target_from_state_mismatch,
|
|
62
|
+
transition: transition.name,
|
|
63
|
+
target: target_name,
|
|
64
|
+
state: transition.to,
|
|
65
|
+
message: "Transition #{ref(transition.name)} can route to #{ref(target_name)}, " \
|
|
66
|
+
"but #{ref(target_name)} starts from #{ref(target.from)} instead of #{ref(transition.to)}.",
|
|
67
|
+
suggestion: "Align #{ref(target_name)}'s from state with #{ref(transition.to)}, " \
|
|
68
|
+
"or remove the named route."
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def ref(value)
|
|
73
|
+
Reference.format(value)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class TransitionSnapshot
|
|
7
|
+
KINDS = [
|
|
8
|
+
%i[deterministic deterministic?],
|
|
9
|
+
%i[router routed?],
|
|
10
|
+
%i[nested_workflow nested?],
|
|
11
|
+
%i[optimizer optimized?],
|
|
12
|
+
%i[orchestrator orchestrated?],
|
|
13
|
+
%i[fanout fanout?],
|
|
14
|
+
%i[parallel parallel?]
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
attr_reader :name, :from, :to, :kind, :success_transition, :failure_transition, :routes, :fallback,
|
|
18
|
+
:fanout_branches, :retry_policy
|
|
19
|
+
|
|
20
|
+
def self.from_transition(transition)
|
|
21
|
+
new(
|
|
22
|
+
name: transition.name,
|
|
23
|
+
from: transition.from,
|
|
24
|
+
to: transition.to,
|
|
25
|
+
kind: kind_for(transition),
|
|
26
|
+
success_transition: transition.success_transition,
|
|
27
|
+
failure_transition: transition.failure_transition,
|
|
28
|
+
routes: transition.router_config&.fetch(:routes, 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)
|
|
32
|
+
)
|
|
33
|
+
end
|
|
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
|
+
|
|
48
|
+
def self.kind_for(transition)
|
|
49
|
+
kind = KINDS.find { |_name, predicate| transition.public_send(predicate) }
|
|
50
|
+
return kind.first if kind
|
|
51
|
+
return :agent if transition.agent_name
|
|
52
|
+
|
|
53
|
+
:noop
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def initialize(**attributes)
|
|
57
|
+
@name = attributes.fetch(:name)
|
|
58
|
+
@from = attributes.fetch(:from)
|
|
59
|
+
@to = attributes.fetch(:to)
|
|
60
|
+
@kind = attributes.fetch(:kind)
|
|
61
|
+
@success_transition = attributes[:success_transition]
|
|
62
|
+
@failure_transition = attributes[:failure_transition]
|
|
63
|
+
@routes = attributes[:routes]
|
|
64
|
+
@fallback = attributes[:fallback]
|
|
65
|
+
@fanout_branches = attributes[:fanout_branches]
|
|
66
|
+
@retry_policy = attributes[:retry_policy]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_h
|
|
70
|
+
{
|
|
71
|
+
name: name,
|
|
72
|
+
from: from,
|
|
73
|
+
to: to,
|
|
74
|
+
kind: kind,
|
|
75
|
+
success_transition: success_transition,
|
|
76
|
+
failure_transition: failure_transition,
|
|
77
|
+
routes: routes,
|
|
78
|
+
fallback: fallback,
|
|
79
|
+
fanout_branches: fanout_branches,
|
|
80
|
+
retry_policy: retry_policy
|
|
81
|
+
}.compact
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
class Validator
|
|
7
|
+
attr_reader :graph
|
|
8
|
+
|
|
9
|
+
def initialize(graph)
|
|
10
|
+
@graph = graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def report
|
|
14
|
+
diagnostics = all_diagnostics
|
|
15
|
+
|
|
16
|
+
Report.new(
|
|
17
|
+
status: status_for(diagnostics),
|
|
18
|
+
workflow_class: graph.workflow_class.name,
|
|
19
|
+
initial_state: graph.initial_state,
|
|
20
|
+
states: graph.states,
|
|
21
|
+
transitions: graph.transition_snapshots,
|
|
22
|
+
diagnostics: diagnostics,
|
|
23
|
+
metrics: Metrics.new(graph, reachable_transition_names).to_h
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def all_diagnostics
|
|
30
|
+
[
|
|
31
|
+
*StateDiagnostics.new(graph).to_a,
|
|
32
|
+
*TransitionDiagnostics.new(graph).to_a,
|
|
33
|
+
*ReachabilityDiagnostics.new(graph, reachable_transition_names).to_a
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def reachable_transition_names
|
|
38
|
+
@reachable_transition_names ||= Reachability.new(graph).transition_names
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def status_for(diagnostics)
|
|
42
|
+
return :invalid if diagnostics.any? { |diagnostic| diagnostic.severity == :error }
|
|
43
|
+
return :warning if diagnostics.any?
|
|
44
|
+
|
|
45
|
+
:valid
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
class Graph
|
|
6
|
+
attr_reader :workflow_class, :initial_state, :states, :transitions
|
|
7
|
+
|
|
8
|
+
def initialize(workflow_class:, initial_state:, states:, transitions:)
|
|
9
|
+
@workflow_class = workflow_class
|
|
10
|
+
@initial_state = initial_state
|
|
11
|
+
@states = Array(states).uniq.freeze
|
|
12
|
+
@transitions = transitions.dup.freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def validate
|
|
16
|
+
Validator.new(self).report
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def transition_snapshots
|
|
20
|
+
transitions.values.map { |transition| TransitionSnapshot.from_transition(transition) }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require_relative "graph/reference"
|
|
27
|
+
require_relative "graph/diagnostic"
|
|
28
|
+
require_relative "graph/state_diagnostics"
|
|
29
|
+
require_relative "graph/reachability"
|
|
30
|
+
require_relative "graph/reachability_diagnostics"
|
|
31
|
+
require_relative "graph/metrics"
|
|
32
|
+
require_relative "graph/report"
|
|
33
|
+
require_relative "graph/targets"
|
|
34
|
+
require_relative "graph/transition_snapshot"
|
|
35
|
+
require_relative "graph/transition_diagnostics"
|
|
36
|
+
require_relative "graph/validator"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
def self.graph
|
|
6
|
+
Graph.new(
|
|
7
|
+
workflow_class: self,
|
|
8
|
+
initial_state: initial_state,
|
|
9
|
+
states: @states || [],
|
|
10
|
+
transitions: @transitions || {}
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.validate_graph
|
|
15
|
+
graph.validate
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
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
|