smith-agents 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -1
- data/docs/TOOLS_AND_GUARDRAILS.md +61 -1
- data/lib/smith/agent.rb +23 -1
- data/lib/smith/tool/budget_enforcement.rb +11 -13
- data/lib/smith/tool/call_allowance.rb +61 -0
- data/lib/smith/tool/capture.rb +60 -4
- data/lib/smith/tool/capture_configuration.rb +20 -0
- data/lib/smith/tool/chat_execution_context.rb +105 -0
- data/lib/smith/tool/scoped_context.rb +71 -0
- data/lib/smith/tool.rb +8 -46
- data/lib/smith/tool_capture_failed.rb +106 -0
- data/lib/smith/version.rb +2 -2
- data/lib/smith/workflow/composite/branch_failure.rb +34 -13
- data/lib/smith/workflow/composite/branch_outcome.rb +19 -2
- data/lib/smith/workflow/composite/error.rb +53 -5
- data/lib/smith/workflow/composite/error_evidence.rb +10 -2
- data/lib/smith/workflow/deadline_enforcement.rb +1 -1
- data/lib/smith/workflow/fanout_execution.rb +12 -11
- data/lib/smith/workflow/parallel/cancellation_signal.rb +1 -1
- data/lib/smith/workflow/parallel/root_execution.rb +1 -1
- data/lib/smith/workflow/parallel.rb +6 -1
- data/lib/smith/workflow/prepared_branch_execution.rb +9 -2
- data/lib/smith/workflow/retry_execution.rb +1 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +13 -4
- data/lib/smith/workflow/split_step_persistence/preparation_claim.rb +8 -0
- data/lib/smith/workflow/thread_context_snapshot.rb +1 -0
- data/lib/smith/workflow/transition.rb +6 -0
- data/lib/smith/workflow.rb +6 -0
- metadata +14 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
require_relative "error"
|
|
6
|
+
|
|
7
|
+
module Smith
|
|
8
|
+
class ToolCaptureFailed < Error
|
|
9
|
+
REASONS = %i[
|
|
10
|
+
collector_missing collector_invalid capture_empty capture_block_failed collector_failed
|
|
11
|
+
].freeze
|
|
12
|
+
REASONS_BY_NAME = REASONS.to_h { |reason| [reason.to_s.freeze, reason] }.freeze
|
|
13
|
+
DETAIL_NAMES = %i[tool_name reason].freeze
|
|
14
|
+
DETAIL_KEYS = DETAIL_NAMES.to_h { |name| [name.to_s.freeze, name] }.freeze
|
|
15
|
+
MAX_TOOL_NAME_BYTES = 256
|
|
16
|
+
private_constant :REASONS, :REASONS_BY_NAME, :DETAIL_NAMES, :DETAIL_KEYS, :MAX_TOOL_NAME_BYTES
|
|
17
|
+
|
|
18
|
+
attr_reader :tool_name, :reason
|
|
19
|
+
|
|
20
|
+
def initialize(tool_name:, reason:)
|
|
21
|
+
@tool_name = normalize_tool_name(tool_name)
|
|
22
|
+
@reason = normalize_reason(reason)
|
|
23
|
+
super("strict result capture failed for #{@tool_name}: #{@reason}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def details
|
|
27
|
+
{ tool_name:, reason: }.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.from_details(details)
|
|
31
|
+
values = normalize_details(details)
|
|
32
|
+
new(tool_name: values.fetch(:tool_name), reason: values.fetch(:reason))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.for_runtime(tool_name:, reason:)
|
|
36
|
+
new(tool_name:, reason:)
|
|
37
|
+
rescue ArgumentError
|
|
38
|
+
new(tool_name: diagnostic_tool_name(tool_name), reason:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.normalize_details(details)
|
|
42
|
+
raise ArgumentError, "tool capture failure details must be a Hash" unless details.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
values = {}
|
|
45
|
+
Hash.instance_method(:each_pair).bind_call(details) do |key, value|
|
|
46
|
+
name = normalize_detail_name(key)
|
|
47
|
+
unless DETAIL_NAMES.include?(name)
|
|
48
|
+
raise ArgumentError, "tool capture failure details contain an unknown attribute"
|
|
49
|
+
end
|
|
50
|
+
raise ArgumentError, "tool capture failure details contain a duplicate attribute" if values.key?(name)
|
|
51
|
+
|
|
52
|
+
values[name] = value
|
|
53
|
+
end
|
|
54
|
+
missing = DETAIL_NAMES - values.keys
|
|
55
|
+
raise ArgumentError, "tool capture failure details are missing required attributes" if missing.any?
|
|
56
|
+
|
|
57
|
+
values
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.normalize_detail_name(key)
|
|
61
|
+
return key if key.is_a?(Symbol)
|
|
62
|
+
|
|
63
|
+
DETAIL_KEYS[key] if key.is_a?(String)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.diagnostic_tool_name(value)
|
|
67
|
+
bytes = value.to_s.b
|
|
68
|
+
return "anonymous_tool" if bytes.empty?
|
|
69
|
+
|
|
70
|
+
"tool_#{Digest::SHA256.hexdigest(bytes)}"
|
|
71
|
+
end
|
|
72
|
+
private_class_method :normalize_details, :normalize_detail_name, :diagnostic_tool_name
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def normalize_tool_name(value)
|
|
77
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
78
|
+
raise ArgumentError, "tool capture failure tool name must be a String or Symbol"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
name = value.to_s
|
|
82
|
+
raise ArgumentError, "tool capture failure tool name must be valid UTF-8" unless name.valid_encoding?
|
|
83
|
+
|
|
84
|
+
name = name.encode(Encoding::UTF_8)
|
|
85
|
+
raise ArgumentError, "tool capture failure tool name must be valid UTF-8" unless name.valid_encoding?
|
|
86
|
+
|
|
87
|
+
unless name.bytesize.between?(1, MAX_TOOL_NAME_BYTES)
|
|
88
|
+
raise ArgumentError, "tool capture failure tool name must be a bounded non-empty value"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
name.dup.freeze
|
|
92
|
+
rescue EncodingError
|
|
93
|
+
raise ArgumentError, "tool capture failure tool name must be valid UTF-8"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def normalize_reason(value)
|
|
97
|
+
unless value.is_a?(String) || value.is_a?(Symbol)
|
|
98
|
+
raise ArgumentError, "tool capture failure reason must be a String or Symbol"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
REASONS_BY_NAME.fetch(value.to_s) do
|
|
102
|
+
raise ArgumentError, "tool capture failure reason is not recognized"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
data/lib/smith/version.rb
CHANGED
|
@@ -12,21 +12,29 @@ module Smith
|
|
|
12
12
|
"error_class" => :error_class,
|
|
13
13
|
"error_family" => :error_family,
|
|
14
14
|
"retryable" => :retryable,
|
|
15
|
-
"kind" => :kind
|
|
15
|
+
"kind" => :kind,
|
|
16
|
+
"tool_name" => :tool_name,
|
|
17
|
+
"reason" => :reason
|
|
16
18
|
}.freeze
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
REQUIRED_DETAIL_NAMES = %i[branch_key error_class error_family retryable kind].freeze
|
|
20
|
+
OPTIONAL_DETAIL_NAMES = %i[tool_name reason].freeze
|
|
21
|
+
DETAIL_NAMES = (REQUIRED_DETAIL_NAMES + OPTIONAL_DETAIL_NAMES).freeze
|
|
22
|
+
private_constant :DETAIL_KEYS, :REQUIRED_DETAIL_NAMES, :OPTIONAL_DETAIL_NAMES, :DETAIL_NAMES
|
|
19
23
|
|
|
20
|
-
attr_reader :branch_key, :error_class, :error_family, :retryable, :kind, :details
|
|
24
|
+
attr_reader :branch_key, :error_class, :error_family, :retryable, :kind, :tool_name, :reason, :details
|
|
21
25
|
|
|
22
26
|
def self.from_details(details)
|
|
23
27
|
values = normalize_details(details)
|
|
24
|
-
|
|
28
|
+
error_attributes = {
|
|
25
29
|
class_name: values.fetch(:error_class),
|
|
26
30
|
family: values.fetch(:error_family),
|
|
27
31
|
retryable: values.fetch(:retryable),
|
|
28
32
|
kind: values.fetch(:kind)
|
|
29
|
-
|
|
33
|
+
}
|
|
34
|
+
OPTIONAL_DETAIL_NAMES.each do |name|
|
|
35
|
+
error_attributes[name] = values[name] if values.key?(name)
|
|
36
|
+
end
|
|
37
|
+
error = Error.new(error_attributes)
|
|
30
38
|
new(branch_key: values.fetch(:branch_key), error:)
|
|
31
39
|
end
|
|
32
40
|
|
|
@@ -42,7 +50,7 @@ module Smith
|
|
|
42
50
|
|
|
43
51
|
normalized[name] = value
|
|
44
52
|
end
|
|
45
|
-
missing =
|
|
53
|
+
missing = REQUIRED_DETAIL_NAMES - normalized.keys
|
|
46
54
|
raise ArgumentError, "composite branch failure details are missing required attributes" if missing.any?
|
|
47
55
|
|
|
48
56
|
normalized
|
|
@@ -60,10 +68,7 @@ module Smith
|
|
|
60
68
|
def initialize(branch_key:, error:)
|
|
61
69
|
validate_arguments!(branch_key, error)
|
|
62
70
|
@branch_key = branch_key.dup.freeze
|
|
63
|
-
|
|
64
|
-
@error_family = error.family.dup.freeze
|
|
65
|
-
@retryable = error.retryable
|
|
66
|
-
@kind = error.kind&.dup&.freeze
|
|
71
|
+
copy_error_attributes(error)
|
|
67
72
|
@details = failure_details
|
|
68
73
|
super("composite branch #{branch_key.inspect} failed")
|
|
69
74
|
end
|
|
@@ -76,14 +81,30 @@ module Smith
|
|
|
76
81
|
raise ArgumentError, "composite branch failure requires typed error evidence" unless error.is_a?(Error)
|
|
77
82
|
end
|
|
78
83
|
|
|
84
|
+
def copy_error_attributes(error)
|
|
85
|
+
@error_class = owned_string(error.class_name)
|
|
86
|
+
@error_family = owned_string(error.family)
|
|
87
|
+
@retryable = error.retryable
|
|
88
|
+
@kind = owned_string(error.kind)
|
|
89
|
+
@tool_name = owned_string(error.tool_name)
|
|
90
|
+
@reason = owned_string(error.reason)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def owned_string(value)
|
|
94
|
+
value&.dup&.freeze
|
|
95
|
+
end
|
|
96
|
+
|
|
79
97
|
def failure_details
|
|
80
|
-
{
|
|
98
|
+
values = {
|
|
81
99
|
branch_key: @branch_key,
|
|
82
100
|
error_class: @error_class,
|
|
83
101
|
error_family: @error_family,
|
|
84
102
|
retryable: @retryable,
|
|
85
103
|
kind: @kind
|
|
86
|
-
}
|
|
104
|
+
}
|
|
105
|
+
values[:tool_name] = @tool_name if @tool_name
|
|
106
|
+
values[:reason] = @reason if @reason
|
|
107
|
+
values.freeze
|
|
87
108
|
end
|
|
88
109
|
end
|
|
89
110
|
end
|
|
@@ -82,6 +82,12 @@ module Smith
|
|
|
82
82
|
def serializable(values)
|
|
83
83
|
values.merge(error: values[:error]&.to_h, effects: values.fetch(:effects).to_h)
|
|
84
84
|
end
|
|
85
|
+
|
|
86
|
+
def legacy_serializable(values)
|
|
87
|
+
serializable(values).tap do |payload|
|
|
88
|
+
payload[:error] = payload[:error]&.except(:tool_name, :reason)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
85
91
|
end
|
|
86
92
|
|
|
87
93
|
def initialize(attributes)
|
|
@@ -89,8 +95,7 @@ module Smith
|
|
|
89
95
|
owned[:output] = self.class.send(:normalize_output, owned[:output])
|
|
90
96
|
super(owned)
|
|
91
97
|
validate_shape!
|
|
92
|
-
|
|
93
|
-
raise ArgumentError, "composite branch outcome digest does not match" unless digest == expected
|
|
98
|
+
validate_digest!
|
|
94
99
|
end
|
|
95
100
|
|
|
96
101
|
def succeeded? = status == :succeeded
|
|
@@ -98,6 +103,18 @@ module Smith
|
|
|
98
103
|
|
|
99
104
|
private
|
|
100
105
|
|
|
106
|
+
def validate_digest!
|
|
107
|
+
values = to_h.except(:digest)
|
|
108
|
+
payloads = [self.class.send(:serializable, values)]
|
|
109
|
+
payloads << self.class.send(:legacy_serializable, values) if legacy_digest_allowed?
|
|
110
|
+
expected = payloads.map { PayloadDigest.call(_1) }
|
|
111
|
+
raise ArgumentError, "composite branch outcome digest does not match" unless expected.include?(digest)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def legacy_digest_allowed?
|
|
115
|
+
error.nil? || (error.tool_name.nil? && error.reason.nil?)
|
|
116
|
+
end
|
|
117
|
+
|
|
101
118
|
def validate_shape!
|
|
102
119
|
valid = succeeded? ? error.nil? : error && output.nil?
|
|
103
120
|
raise ArgumentError, "composite branch outcome fields do not match status" unless valid
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../../types"
|
|
4
|
+
require_relative "../../tool_capture_failed"
|
|
4
5
|
require_relative "payload"
|
|
5
6
|
|
|
6
7
|
module Smith
|
|
@@ -8,12 +9,15 @@ module Smith
|
|
|
8
9
|
module Composite
|
|
9
10
|
class Error < Payload
|
|
10
11
|
FAMILIES = %w[
|
|
11
|
-
tool_guardrail_failed deterministic_step_failure
|
|
12
|
-
agent_error workflow_error other
|
|
12
|
+
tool_capture_failed tool_guardrail_failed deterministic_step_failure
|
|
13
|
+
deadline_exceeded agent_error workflow_error other
|
|
13
14
|
].freeze
|
|
14
15
|
ALWAYS_RETRYABLE_FAMILIES = %w[deadline_exceeded agent_error].freeze
|
|
15
16
|
CONDITIONALLY_RETRYABLE_FAMILIES = %w[tool_guardrail_failed deterministic_step_failure].freeze
|
|
17
|
+
OPTIONAL_METADATA = %i[tool_name reason].freeze
|
|
18
|
+
STRING_METADATA = %i[kind tool_name reason].freeze
|
|
16
19
|
private_constant :ALWAYS_RETRYABLE_FAMILIES, :CONDITIONALLY_RETRYABLE_FAMILIES
|
|
20
|
+
private_constant :OPTIONAL_METADATA, :STRING_METADATA
|
|
17
21
|
OwnedString = Types::String.constructor { |value| value.is_a?(String) ? value.dup.freeze : value }
|
|
18
22
|
private_constant :OwnedString
|
|
19
23
|
|
|
@@ -21,17 +25,43 @@ module Smith
|
|
|
21
25
|
attribute :family, OwnedString.enum(*FAMILIES)
|
|
22
26
|
attribute :retryable, Types::Bool
|
|
23
27
|
attribute? :kind, OwnedString.constrained(min_size: 1, max_size: 128).optional
|
|
28
|
+
attribute? :tool_name, OwnedString.constrained(min_size: 1, max_size: 256).optional
|
|
29
|
+
attribute? :reason, OwnedString.constrained(min_size: 1, max_size: 128).optional
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def normalize_attributes(attributes)
|
|
33
|
+
return super unless attributes.is_a?(Hash)
|
|
34
|
+
|
|
35
|
+
super(attributes_with_optional_metadata(attributes))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def attributes_with_optional_metadata(attributes)
|
|
41
|
+
values = {}
|
|
42
|
+
Hash.instance_method(:each_pair).bind_call(attributes) { |key, value| values[key] = value }
|
|
43
|
+
OPTIONAL_METADATA.each do |name|
|
|
44
|
+
values[name] = nil unless values.key?(name) || values.key?(name.to_s)
|
|
45
|
+
end
|
|
46
|
+
values
|
|
47
|
+
end
|
|
48
|
+
end
|
|
24
49
|
|
|
25
50
|
def initialize(attributes)
|
|
26
|
-
|
|
27
|
-
owned[:kind] = owned[:kind].to_s if owned[:kind]
|
|
28
|
-
super(owned)
|
|
51
|
+
super(normalize_error_attributes(attributes))
|
|
29
52
|
validate_retryability!
|
|
30
53
|
validate_kind!
|
|
54
|
+
validate_tool_capture_metadata!
|
|
31
55
|
end
|
|
32
56
|
|
|
33
57
|
private
|
|
34
58
|
|
|
59
|
+
def normalize_error_attributes(attributes)
|
|
60
|
+
self.class.normalize_attributes(attributes).tap do |normalized|
|
|
61
|
+
STRING_METADATA.each { |name| normalized[name] = normalized[name].to_s if normalized[name] }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
35
65
|
def validate_retryability!
|
|
36
66
|
valid = if ALWAYS_RETRYABLE_FAMILIES.include?(family)
|
|
37
67
|
retryable
|
|
@@ -48,6 +78,24 @@ module Smith
|
|
|
48
78
|
|
|
49
79
|
raise ArgumentError, "composite error kind is not valid for its family"
|
|
50
80
|
end
|
|
81
|
+
|
|
82
|
+
def validate_tool_capture_metadata!
|
|
83
|
+
valid = if family == "tool_capture_failed"
|
|
84
|
+
valid_tool_capture_metadata?
|
|
85
|
+
else
|
|
86
|
+
tool_name.nil? && reason.nil?
|
|
87
|
+
end
|
|
88
|
+
return if valid
|
|
89
|
+
|
|
90
|
+
raise ArgumentError, "composite error tool metadata does not match its family"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def valid_tool_capture_metadata?
|
|
94
|
+
ToolCaptureFailed.from_details(tool_name:, reason:)
|
|
95
|
+
true
|
|
96
|
+
rescue ArgumentError
|
|
97
|
+
false
|
|
98
|
+
end
|
|
51
99
|
end
|
|
52
100
|
end
|
|
53
101
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "dry-initializer"
|
|
4
4
|
|
|
5
5
|
require_relative "../../errors"
|
|
6
|
+
require_relative "../../tool_capture_failed"
|
|
6
7
|
require_relative "error"
|
|
7
8
|
|
|
8
9
|
module Smith
|
|
@@ -16,12 +17,14 @@ module Smith
|
|
|
16
17
|
def self.call(error) = new(error).call
|
|
17
18
|
|
|
18
19
|
def call
|
|
19
|
-
|
|
20
|
+
attributes = {
|
|
20
21
|
class_name: error_class_name,
|
|
21
22
|
family: error_family,
|
|
22
23
|
retryable: retryable_error?,
|
|
23
24
|
kind: error_kind
|
|
24
|
-
|
|
25
|
+
}
|
|
26
|
+
attributes.merge!(tool_capture_metadata) if error.is_a?(ToolCaptureFailed)
|
|
27
|
+
Error.new(attributes)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
private
|
|
@@ -47,6 +50,7 @@ module Smith
|
|
|
47
50
|
|
|
48
51
|
def error_family
|
|
49
52
|
case error
|
|
53
|
+
when ToolCaptureFailed then "tool_capture_failed"
|
|
50
54
|
when ToolGuardrailFailed then "tool_guardrail_failed"
|
|
51
55
|
when DeterministicStepFailure then "deterministic_step_failure"
|
|
52
56
|
when DeadlineExceeded then "deadline_exceeded"
|
|
@@ -55,6 +59,10 @@ module Smith
|
|
|
55
59
|
else "other"
|
|
56
60
|
end
|
|
57
61
|
end
|
|
62
|
+
|
|
63
|
+
def tool_capture_metadata
|
|
64
|
+
{ tool_name: error.tool_name, reason: error.reason.to_s }
|
|
65
|
+
end
|
|
58
66
|
end
|
|
59
67
|
end
|
|
60
68
|
end
|
|
@@ -74,7 +74,7 @@ module Smith
|
|
|
74
74
|
|
|
75
75
|
def apply_agent_tool_calls(agent_class)
|
|
76
76
|
agent_tc = agent_class&.budget&.dig(:tool_calls)
|
|
77
|
-
Tool.current_tool_call_allowance = agent_tc ?
|
|
77
|
+
Tool.current_tool_call_allowance = agent_tc ? Tool::CallAllowance.new(agent_tc) : nil
|
|
78
78
|
end
|
|
79
79
|
|
|
80
80
|
def clear_agent_tool_calls
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "branch_env"
|
|
4
|
-
|
|
5
3
|
module Smith
|
|
6
4
|
class Workflow
|
|
7
5
|
module FanoutExecution
|
|
8
6
|
private
|
|
9
7
|
|
|
8
|
+
def fanout_branch_environment(branches, branch_agent_classes, prepared_input)
|
|
9
|
+
BranchEnv.new(
|
|
10
|
+
prepared_input:,
|
|
11
|
+
guardrail_sources: nil,
|
|
12
|
+
scoped_store: propagate_scoped_artifacts,
|
|
13
|
+
branch_estimates: fanout_branch_estimates(branches, branch_agent_classes),
|
|
14
|
+
deadline: wall_clock_deadline
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
def run_guarded_fanout_step(transition)
|
|
11
19
|
branches = transition.fanout_config.fetch(:branches)
|
|
12
20
|
branch_agent_classes = fanout_agent_classes(transition, branches)
|
|
@@ -26,13 +34,7 @@ module Smith
|
|
|
26
34
|
def execute_fanout_step(transition, branches: nil, branch_agent_classes: nil, prepared_input: nil)
|
|
27
35
|
branches ||= transition.fanout_config.fetch(:branches)
|
|
28
36
|
branch_agent_classes ||= fanout_agent_classes(transition, branches)
|
|
29
|
-
env =
|
|
30
|
-
prepared_input: prepared_input,
|
|
31
|
-
guardrail_sources: nil,
|
|
32
|
-
scoped_store: propagate_scoped_artifacts,
|
|
33
|
-
branch_estimates: fanout_branch_estimates(branches, branch_agent_classes),
|
|
34
|
-
deadline: wall_clock_deadline
|
|
35
|
-
)
|
|
37
|
+
env = fanout_branch_environment(branches, branch_agent_classes, prepared_input)
|
|
36
38
|
|
|
37
39
|
branch_calls = branches.map do |branch_key, agent_name|
|
|
38
40
|
PreparedBranchExecution.instance_method(:prepared_branch).bind_call(
|
|
@@ -98,12 +100,11 @@ module Smith
|
|
|
98
100
|
def fanout_branch_estimates(branches, branch_agent_classes)
|
|
99
101
|
return {} unless @ledger
|
|
100
102
|
|
|
101
|
-
branch_count = branches.length
|
|
102
103
|
branches.each_with_object({}) do |(branch_key, _agent_name), map|
|
|
103
104
|
agent_class = branch_agent_classes.fetch(branch_key)
|
|
104
105
|
map[branch_key] = compute_branch_estimates(
|
|
105
106
|
@ledger,
|
|
106
|
-
branch_count:
|
|
107
|
+
branch_count: branches.length,
|
|
107
108
|
agent_budget: agent_class&.budget
|
|
108
109
|
)
|
|
109
110
|
end
|
|
@@ -73,7 +73,7 @@ module Smith
|
|
|
73
73
|
|
|
74
74
|
def resolve(futures)
|
|
75
75
|
fulfilled, values, reasons = Concurrent::Promises.zip(*futures).result
|
|
76
|
-
raise(@signal.reason
|
|
76
|
+
raise(Parallel.preferred_error([@signal.reason, *reasons])) unless fulfilled
|
|
77
77
|
|
|
78
78
|
values
|
|
79
79
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../errors"
|
|
4
|
+
require_relative "../tool_capture_failed"
|
|
3
5
|
require_relative "parallel/cancellation"
|
|
4
6
|
require_relative "parallel/cancellation_signal"
|
|
5
7
|
require_relative "parallel/execution_context"
|
|
@@ -36,7 +38,10 @@ module Smith
|
|
|
36
38
|
|
|
37
39
|
def self.preferred_error(reasons)
|
|
38
40
|
errors = Array(reasons).compact
|
|
39
|
-
errors.find { |error| !error.is_a?(
|
|
41
|
+
errors.find { |error| !error.is_a?(StandardError) } ||
|
|
42
|
+
errors.find { |error| error.is_a?(ToolCaptureFailed) } ||
|
|
43
|
+
errors.find { |error| !error.is_a?(Cancellation) } ||
|
|
44
|
+
errors.first
|
|
40
45
|
end
|
|
41
46
|
end
|
|
42
47
|
end
|
|
@@ -6,13 +6,20 @@ module Smith
|
|
|
6
6
|
private
|
|
7
7
|
|
|
8
8
|
def prepared_branch(implementation, *arguments)
|
|
9
|
+
tool_context = Tool::ScopedContext.capture
|
|
9
10
|
unless @split_step_active_execution_authorization
|
|
10
|
-
return proc
|
|
11
|
+
return proc do |signal|
|
|
12
|
+
Tool::ScopedContext.around(tool_context) do
|
|
13
|
+
__send__(implementation.name, *arguments, signal)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
proc do |signal|
|
|
14
19
|
run = proc { implementation.bind_call(self, *arguments, signal) }
|
|
15
|
-
|
|
20
|
+
Tool::ScopedContext.around(tool_context) do
|
|
21
|
+
PreparedBranchExecution.instance_method(:within_prepared_branch_execution).bind_call(self, &run)
|
|
22
|
+
end
|
|
16
23
|
end
|
|
17
24
|
end
|
|
18
25
|
|
|
@@ -68,18 +68,27 @@ module Smith
|
|
|
68
68
|
def composite_branch_environment(execution, input, agent_class, transition)
|
|
69
69
|
branch = execution.branch
|
|
70
70
|
budget = branch.budget.transform_keys(&:to_sym)
|
|
71
|
-
branch_key = fetch_composite_fanout_branch(transition, branch.key).first unless execution.kind == :parallel
|
|
72
|
-
estimates = execution.kind == :parallel ? budget : { branch_key => budget }
|
|
73
71
|
BranchEnv.new(
|
|
74
72
|
prepared_input: input.agent_messages,
|
|
75
73
|
guardrail_sources: Tool.current_guardrails,
|
|
76
74
|
scoped_store: propagate_scoped_artifacts,
|
|
77
|
-
branch_estimates:
|
|
75
|
+
branch_estimates: composite_branch_estimates(execution, transition, budget),
|
|
78
76
|
deadline: wall_clock_deadline,
|
|
79
|
-
agent_class: execution
|
|
77
|
+
agent_class: composite_parallel_agent(execution, agent_class)
|
|
80
78
|
)
|
|
81
79
|
end
|
|
82
80
|
|
|
81
|
+
def composite_branch_estimates(execution, transition, budget)
|
|
82
|
+
return budget if execution.kind == :parallel
|
|
83
|
+
|
|
84
|
+
branch_key = fetch_composite_fanout_branch(transition, execution.branch.key).first
|
|
85
|
+
{ branch_key => budget }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def composite_parallel_agent(execution, agent_class)
|
|
89
|
+
agent_class if execution.kind == :parallel
|
|
90
|
+
end
|
|
91
|
+
|
|
83
92
|
def composite_branch_ledger(branch)
|
|
84
93
|
return if branch.budget.empty?
|
|
85
94
|
|
|
@@ -33,6 +33,8 @@ module Smith
|
|
|
33
33
|
def finalize_split_step_preparation!(transition, transition_name, key, adapter, persistence_ttl)
|
|
34
34
|
transaction_identity = TransactionIdentity.capture(adapter)
|
|
35
35
|
transition_signature = TransitionContract.capture(transition)
|
|
36
|
+
namespace_preparer = PreparationClaim.instance_method(:prepare_composite_execution_namespace!)
|
|
37
|
+
namespace_preparer.bind_call(self, transition)
|
|
36
38
|
@split_step_mutex.synchronize do
|
|
37
39
|
unless active_split_step_preparation_claim?
|
|
38
40
|
raise WorkflowError, "the split-step preparation claim is no longer active"
|
|
@@ -65,6 +67,12 @@ module Smith
|
|
|
65
67
|
@split_step_preparation_thread.equal?(Thread.current)
|
|
66
68
|
end
|
|
67
69
|
|
|
70
|
+
def prepare_composite_execution_namespace!(transition)
|
|
71
|
+
return unless transition&.parallel? || transition&.fanout?
|
|
72
|
+
|
|
73
|
+
ArtifactIntegration.instance_method(:execution_namespace).bind_call(self)
|
|
74
|
+
end
|
|
75
|
+
|
|
68
76
|
def mark_split_step_prepared!(_adapter)
|
|
69
77
|
@split_step_mutex.synchronize do
|
|
70
78
|
phase = @split_step_transaction_identity ? :prepared_uncommitted : :prepared
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../tool_capture_failed"
|
|
4
|
+
|
|
3
5
|
module Smith
|
|
4
6
|
class Workflow
|
|
5
7
|
class Transition
|
|
@@ -334,6 +336,10 @@ module Smith
|
|
|
334
336
|
|
|
335
337
|
raise WorkflowError, "retry_on error classes must inherit from StandardError"
|
|
336
338
|
end
|
|
339
|
+
if error_classes.any? { |error_class| error_class <= ToolCaptureFailed }
|
|
340
|
+
message = "retry_on cannot retry Smith::ToolCaptureFailed because the tool outcome may be uncertain"
|
|
341
|
+
raise WorkflowError, message
|
|
342
|
+
end
|
|
337
343
|
|
|
338
344
|
ExponentialBackoff.new(
|
|
339
345
|
attempts:,
|
data/lib/smith/workflow.rb
CHANGED
|
@@ -38,6 +38,9 @@ module Smith
|
|
|
38
38
|
# expose `attr_reader :retryable` only, with no setter, so kwargs
|
|
39
39
|
# MUST flow through `initialize`.
|
|
40
40
|
KNOWN_RECONSTRUCTORS = {
|
|
41
|
+
"Smith::ToolCaptureFailed" => ->(s) {
|
|
42
|
+
Smith::ToolCaptureFailed.from_details(s.fetch(:error_details))
|
|
43
|
+
},
|
|
41
44
|
"Smith::ToolGuardrailFailed" => ->(s) {
|
|
42
45
|
Smith::ToolGuardrailFailed.new(s[:error_message], retryable: s[:error_retryable])
|
|
43
46
|
},
|
|
@@ -227,6 +230,7 @@ module Smith
|
|
|
227
230
|
# so a real DSF doesn't get classified as workflow_error.
|
|
228
231
|
error_family = case err
|
|
229
232
|
when Smith::DeterministicStepFailure then "deterministic_step_failure"
|
|
233
|
+
when Smith::ToolCaptureFailed then "tool_capture_failed"
|
|
230
234
|
when Smith::ToolGuardrailFailed then "tool_guardrail_failed"
|
|
231
235
|
when Smith::DeadlineExceeded then "deadline_exceeded"
|
|
232
236
|
when Smith::AgentError then "agent_error"
|
|
@@ -378,6 +382,8 @@ module Smith
|
|
|
378
382
|
)
|
|
379
383
|
when "tool_guardrail_failed"
|
|
380
384
|
Smith::ToolGuardrailFailed.new(snap[:error_message], retryable: snap[:error_retryable])
|
|
385
|
+
when "tool_capture_failed"
|
|
386
|
+
Smith::ToolCaptureFailed.from_details(snap.fetch(:error_details))
|
|
381
387
|
when "deadline_exceeded" then Smith::DeadlineExceeded.new(snap[:error_message])
|
|
382
388
|
when "agent_error" then Smith::AgentError.new(snap[:error_message])
|
|
383
389
|
when "workflow_error" then Smith::WorkflowError.new(snap[:error_message])
|