smith-agents 0.7.0 → 0.8.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 +226 -0
- data/README.md +71 -1
- data/docs/CONFIGURATION.md +22 -1
- data/docs/PERSISTENCE.md +27 -1
- data/docs/TOOLS_AND_GUARDRAILS.md +233 -2
- data/lib/smith/agent/chat_construction.rb +112 -0
- data/lib/smith/agent/completion.rb +46 -0
- data/lib/smith/agent/completion_usage_recording.rb +25 -0
- data/lib/smith/agent/dynamic_configuration.rb +69 -0
- data/lib/smith/agent/fallback_configuration.rb +26 -0
- data/lib/smith/agent/invocation_preparation.rb +104 -0
- data/lib/smith/agent/lifecycle.rb +7 -276
- data/lib/smith/agent/model_reference.rb +76 -0
- data/lib/smith/agent/provider_attempt.rb +28 -0
- data/lib/smith/agent/provider_candidate_sequence.rb +55 -0
- data/lib/smith/agent/provider_completion.rb +130 -0
- data/lib/smith/agent/provider_failure_handling.rb +93 -0
- data/lib/smith/agent/provider_usage.rb +28 -0
- data/lib/smith/agent/reserved_input_bridge.rb +40 -0
- data/lib/smith/agent/usage_entry_recording.rb +43 -0
- data/lib/smith/agent/usage_tracking.rb +113 -0
- data/lib/smith/agent.rb +29 -197
- data/lib/smith/context/observation_masking.rb +28 -4
- data/lib/smith/context/session.rb +7 -2
- data/lib/smith/diagnostic_text.rb +67 -0
- data/lib/smith/doctor/checks/configuration.rb +16 -3
- data/lib/smith/doctor/checks/models_registry.rb +75 -17
- data/lib/smith/errors.rb +34 -1
- data/lib/smith/models/ambiguous_profile_error.rb +9 -0
- data/lib/smith/models/collision_error.rb +9 -0
- data/lib/smith/models/inference.rb +14 -0
- data/lib/smith/models/normalizer.rb +11 -67
- data/lib/smith/models/provider_qualified_registry.rb +101 -0
- data/lib/smith/models/tool_routing.rb +140 -0
- data/lib/smith/models.rb +24 -78
- data/lib/smith/persisted_failure_invalid.rb +7 -0
- data/lib/smith/pricing.rb +138 -17
- data/lib/smith/pricing_configuration_error.rb +7 -0
- data/lib/smith/provider_permanent_failure.rb +16 -0
- data/lib/smith/tool/argument_container_reader.rb +97 -0
- data/lib/smith/tool/argument_scalar_snapshot.rb +99 -0
- data/lib/smith/tool/argument_snapshot.rb +19 -0
- data/lib/smith/tool/argument_snapshot_accounting.rb +69 -0
- data/lib/smith/tool/argument_snapshot_result.rb +15 -0
- data/lib/smith/tool/argument_snapshot_traversal.rb +119 -0
- data/lib/smith/tool/bounded_completion_context.rb +131 -0
- data/lib/smith/tool/bounded_completion_controls.rb +113 -0
- data/lib/smith/tool/bounded_completion_guard.rb +101 -0
- data/lib/smith/tool/bounded_completion_installation.rb +20 -0
- data/lib/smith/tool/bounded_completion_state.rb +57 -0
- data/lib/smith/tool/budget_enforcement.rb +15 -7
- data/lib/smith/tool/call_admission.rb +44 -0
- data/lib/smith/tool/call_allowance.rb +101 -28
- data/lib/smith/tool/call_allowance_counter.rb +43 -0
- data/lib/smith/tool/call_batch.rb +47 -0
- data/lib/smith/tool/call_budget.rb +51 -0
- data/lib/smith/tool/call_reservation.rb +42 -0
- data/lib/smith/tool/chat_execution_callbacks.rb +58 -0
- data/lib/smith/tool/chat_execution_context.rb +7 -88
- data/lib/smith/tool/execution_authority.rb +42 -0
- data/lib/smith/tool/execution_authorization.rb +17 -0
- data/lib/smith/tool/execution_batch.rb +100 -0
- data/lib/smith/tool/execution_batch_admission.rb +56 -0
- data/lib/smith/tool/execution_batch_builder.rb +122 -0
- data/lib/smith/tool/execution_batch_collection.rb +70 -0
- data/lib/smith/tool/execution_batch_invocations.rb +28 -0
- data/lib/smith/tool/execution_batch_lifecycle.rb +91 -0
- data/lib/smith/tool/execution_batch_registry.rb +99 -0
- data/lib/smith/tool/execution_batch_requests.rb +59 -0
- data/lib/smith/tool/execution_batch_source_call.rb +36 -0
- data/lib/smith/tool/execution_batch_source_metadata.rb +107 -0
- data/lib/smith/tool/execution_batch_sources.rb +32 -0
- data/lib/smith/tool/execution_batch_state.rb +110 -0
- data/lib/smith/tool/execution_dispatch.rb +78 -0
- data/lib/smith/tool/execution_failure_handling.rb +101 -0
- data/lib/smith/tool/execution_lifecycle.rb +28 -0
- data/lib/smith/tool/execution_tracker.rb +20 -0
- data/lib/smith/tool/fail_fast_completion.rb +24 -0
- data/lib/smith/tool/graceful_completion.rb +20 -0
- data/lib/smith/tool/invocation.rb +27 -0
- data/lib/smith/tool/invocation_request.rb +37 -0
- data/lib/smith/tool/invocation_sequence.rb +26 -0
- data/lib/smith/tool/legacy_call_allowance.rb +57 -0
- data/lib/smith/tool/scoped_context.rb +60 -8
- data/lib/smith/tool.rb +71 -12
- data/lib/smith/tool_execution_not_admitted.rb +7 -0
- data/lib/smith/tool_failure_notification_failed.rb +94 -0
- data/lib/smith/version.rb +2 -2
- data/lib/smith/workflow/agent_result.rb +4 -3
- data/lib/smith/workflow/artifact_integration.rb +2 -1
- data/lib/smith/workflow/composite/effects.rb +3 -1
- data/lib/smith/workflow/deadline_enforcement.rb +20 -2
- data/lib/smith/workflow/execution.rb +2 -9
- data/lib/smith/workflow/failure_detail_snapshot.rb +33 -0
- data/lib/smith/workflow/failure_reconstructor.rb +79 -0
- data/lib/smith/workflow/failure_record.rb +139 -0
- data/lib/smith/workflow/failure_record_restore.rb +145 -0
- data/lib/smith/workflow/failure_record_text.rb +83 -0
- data/lib/smith/workflow/failure_record_validator.rb +113 -0
- data/lib/smith/workflow/guarded_step_execution.rb +39 -0
- data/lib/smith/workflow/parallel.rb +14 -3
- data/lib/smith/workflow/persistence.rb +22 -19
- data/lib/smith/workflow/retry_execution.rb +5 -1
- data/lib/smith/workflow/thread_context_snapshot.rb +5 -2
- data/lib/smith/workflow/transition.rb +9 -4
- data/lib/smith/workflow/usage_entry.rb +12 -2
- data/lib/smith/workflow.rb +17 -130
- data/lib/smith.rb +3 -0
- metadata +78 -11
data/lib/smith/tool.rb
CHANGED
|
@@ -4,23 +4,68 @@ require "ruby_llm"
|
|
|
4
4
|
|
|
5
5
|
require_relative "tool/capability_builder"
|
|
6
6
|
require_relative "tool/policy"
|
|
7
|
+
require_relative "tool/call_budget"
|
|
8
|
+
require_relative "tool/call_batch"
|
|
9
|
+
require_relative "tool/legacy_call_allowance"
|
|
10
|
+
require_relative "tool/call_allowance_counter"
|
|
7
11
|
require_relative "tool/call_allowance"
|
|
12
|
+
require_relative "tool/call_reservation"
|
|
13
|
+
require_relative "tool/call_admission"
|
|
14
|
+
require_relative "tool/execution_authority"
|
|
15
|
+
require_relative "tool/execution_authorization"
|
|
16
|
+
require_relative "tool/execution_tracker"
|
|
17
|
+
require_relative "tool/execution_lifecycle"
|
|
18
|
+
require_relative "tool/invocation"
|
|
19
|
+
require_relative "tool/argument_snapshot_result"
|
|
20
|
+
require_relative "tool/argument_scalar_snapshot"
|
|
21
|
+
require_relative "tool/argument_container_reader"
|
|
22
|
+
require_relative "tool/argument_snapshot_accounting"
|
|
23
|
+
require_relative "tool/argument_snapshot_traversal"
|
|
24
|
+
require_relative "tool/argument_snapshot"
|
|
25
|
+
require_relative "tool/invocation_request"
|
|
26
|
+
require_relative "tool/invocation_sequence"
|
|
27
|
+
require_relative "tool/execution_batch_collection"
|
|
28
|
+
require_relative "tool/execution_batch_invocations"
|
|
29
|
+
require_relative "tool/execution_batch_source_metadata"
|
|
30
|
+
require_relative "tool/execution_batch_source_call"
|
|
31
|
+
require_relative "tool/execution_batch_sources"
|
|
32
|
+
require_relative "tool/execution_batch_state"
|
|
33
|
+
require_relative "tool/execution_batch"
|
|
34
|
+
require_relative "tool/execution_batch_admission"
|
|
35
|
+
require_relative "tool/execution_batch_requests"
|
|
36
|
+
require_relative "tool/execution_batch_builder"
|
|
37
|
+
require_relative "tool/execution_batch_registry"
|
|
8
38
|
require_relative "tool/budget_enforcement"
|
|
9
39
|
require_relative "tool_capture_failed"
|
|
10
40
|
require_relative "tool/capture"
|
|
11
41
|
require_relative "tool/capture_configuration"
|
|
12
42
|
require_relative "tool/compatibility"
|
|
13
43
|
require_relative "tool/scoped_context"
|
|
44
|
+
require_relative "tool/bounded_completion_state"
|
|
45
|
+
require_relative "tool/bounded_completion_guard"
|
|
46
|
+
require_relative "tool/bounded_completion_controls"
|
|
47
|
+
require_relative "tool/fail_fast_completion"
|
|
48
|
+
require_relative "tool/graceful_completion"
|
|
49
|
+
require_relative "tool/bounded_completion_context"
|
|
50
|
+
require_relative "tool/bounded_completion_installation"
|
|
51
|
+
require_relative "tool/execution_failure_handling"
|
|
52
|
+
require_relative "tool/execution_dispatch"
|
|
53
|
+
require_relative "tool/chat_execution_callbacks"
|
|
54
|
+
require_relative "tool/execution_batch_lifecycle"
|
|
14
55
|
require_relative "tool/chat_execution_context"
|
|
15
56
|
|
|
16
57
|
module Smith
|
|
17
58
|
class Tool < RubyLLM::Tool
|
|
18
59
|
include Policy
|
|
60
|
+
include ExecutionAuthorization
|
|
61
|
+
include ExecutionLifecycle
|
|
19
62
|
include BudgetEnforcement
|
|
20
63
|
include Capture
|
|
21
64
|
extend CaptureConfiguration
|
|
22
65
|
extend ScopedContext
|
|
23
66
|
|
|
67
|
+
private_constant :ExecutionAuthority
|
|
68
|
+
|
|
24
69
|
class << self
|
|
25
70
|
# Tool subclasses inherit the parent's compatible_with spec by
|
|
26
71
|
# reference (the spec is a frozen Hash; immutability makes shared
|
|
@@ -75,24 +120,32 @@ module Smith
|
|
|
75
120
|
end
|
|
76
121
|
|
|
77
122
|
def execute(**kwargs)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
check_authorization!(kwargs)
|
|
83
|
-
run_tool_guardrails!(kwargs)
|
|
84
|
-
check_tool_deadline!
|
|
85
|
-
charge_tool_call!
|
|
86
|
-
|
|
87
|
-
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
88
|
-
result = perform(**kwargs)
|
|
89
|
-
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
123
|
+
authorize_tool_execution!
|
|
124
|
+
kwargs.freeze
|
|
125
|
+
prepare_tool_execution!(kwargs)
|
|
126
|
+
result, duration = perform_with_duration(kwargs)
|
|
90
127
|
|
|
91
128
|
emit_tool_trace(kwargs, result, duration)
|
|
92
129
|
capture_result_if_configured(kwargs, result)
|
|
93
130
|
result
|
|
94
131
|
end
|
|
95
132
|
|
|
133
|
+
protected
|
|
134
|
+
|
|
135
|
+
def invocation_argument_error(arguments) = validate_keyword_arguments(normalize_args(arguments))
|
|
136
|
+
|
|
137
|
+
def execute_keyword_signature
|
|
138
|
+
parameters = method(:perform).parameters
|
|
139
|
+
required_keywords = parameters.filter_map { |kind, name| name if kind == :keyreq }
|
|
140
|
+
optional_keywords = parameters.filter_map { |kind, name| name if kind == :key }
|
|
141
|
+
accepts_extra_keywords = parameters.any? { |kind, _| kind == :keyrest }
|
|
142
|
+
accepts_positional_arguments = parameters.any? do |kind, _|
|
|
143
|
+
RubyLLM::Tool::POSITIONAL_PARAMETER_KINDS.include?(kind)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
[required_keywords, optional_keywords, accepts_extra_keywords, accepts_positional_arguments]
|
|
147
|
+
end
|
|
148
|
+
|
|
96
149
|
private
|
|
97
150
|
|
|
98
151
|
def run_before_execute_hook!(kwargs)
|
|
@@ -126,6 +179,12 @@ module Smith
|
|
|
126
179
|
raise DeadlineExceeded, "wall_clock deadline exceeded during tool execution" if Time.now.utc >= deadline
|
|
127
180
|
end
|
|
128
181
|
|
|
182
|
+
def check_dispatch_deadline!
|
|
183
|
+
check_tool_deadline!
|
|
184
|
+
rescue DeadlineExceeded
|
|
185
|
+
raise ToolDispatchRejected, "tool deadline expired before dispatch"
|
|
186
|
+
end
|
|
187
|
+
|
|
129
188
|
def perform(**kwargs)
|
|
130
189
|
raise NotImplementedError, "#{self.class} must implement #perform"
|
|
131
190
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "error"
|
|
4
|
+
require_relative "diagnostic_text"
|
|
5
|
+
|
|
6
|
+
module Smith
|
|
7
|
+
class ToolFailureNotificationFailed < Error
|
|
8
|
+
DETAIL_NAMES = %i[
|
|
9
|
+
dispatch_error_class dispatch_error_message notification_error_class notification_error_message
|
|
10
|
+
].freeze
|
|
11
|
+
DETAIL_KEYS = DETAIL_NAMES.to_h { |name| [name.to_s.freeze, name] }.freeze
|
|
12
|
+
EXCEPTION_MESSAGE = Exception.instance_method(:message)
|
|
13
|
+
MODULE_NAME = Module.instance_method(:name)
|
|
14
|
+
OBJECT_CLASS = Object.instance_method(:class)
|
|
15
|
+
private_constant :DETAIL_NAMES, :DETAIL_KEYS, :EXCEPTION_MESSAGE, :MODULE_NAME, :OBJECT_CLASS
|
|
16
|
+
|
|
17
|
+
attr_reader :details, :dispatch_error, :notification_error
|
|
18
|
+
|
|
19
|
+
def initialize(dispatch_error:, notification_error:)
|
|
20
|
+
@dispatch_error = dispatch_error
|
|
21
|
+
@notification_error = notification_error
|
|
22
|
+
@details = build_details
|
|
23
|
+
super(
|
|
24
|
+
"host failed to record a terminal tool outcome: " \
|
|
25
|
+
"#{@details.fetch(:notification_error_class)}: #{@details.fetch(:notification_error_message)}"
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_details
|
|
30
|
+
{
|
|
31
|
+
dispatch_error_class: DiagnosticText.capture(error_class_name(dispatch_error), max_bytes: 512),
|
|
32
|
+
dispatch_error_message: DiagnosticText.capture(error_message(dispatch_error)),
|
|
33
|
+
notification_error_class: DiagnosticText.capture(error_class_name(notification_error), max_bytes: 512),
|
|
34
|
+
notification_error_message: DiagnosticText.capture(error_message(notification_error))
|
|
35
|
+
}.freeze
|
|
36
|
+
end
|
|
37
|
+
private :build_details
|
|
38
|
+
|
|
39
|
+
def self.from_details(details)
|
|
40
|
+
values = normalize_details(details)
|
|
41
|
+
new(
|
|
42
|
+
dispatch_error: restored_error(values, :dispatch_error),
|
|
43
|
+
notification_error: restored_error(values, :notification_error)
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.normalize_details(details)
|
|
48
|
+
raise ArgumentError, "tool failure notification details must be a Hash" unless details.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
values = details.each_with_object({}) do |(key, value), normalized|
|
|
51
|
+
append_detail!(normalized, key, value)
|
|
52
|
+
end
|
|
53
|
+
validate_complete!(values)
|
|
54
|
+
values.freeze
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.append_detail!(normalized, key, value)
|
|
58
|
+
name = normalize_detail_name(key)
|
|
59
|
+
raise ArgumentError, "tool failure notification details contain an unknown attribute" unless name
|
|
60
|
+
raise ArgumentError, "tool failure notification details contain a duplicate attribute" if normalized.key?(name)
|
|
61
|
+
raise ArgumentError, "tool failure notification detail values must be strings" unless value.is_a?(String)
|
|
62
|
+
|
|
63
|
+
normalized[name] = value
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.validate_complete!(values)
|
|
67
|
+
missing = DETAIL_NAMES - values.keys
|
|
68
|
+
raise ArgumentError, "tool failure notification details are missing required attributes" if missing.any?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.normalize_detail_name(key)
|
|
72
|
+
return key if key.is_a?(Symbol) && DETAIL_NAMES.include?(key)
|
|
73
|
+
|
|
74
|
+
DETAIL_KEYS[key] if key.is_a?(String)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.restored_error(values, prefix)
|
|
78
|
+
RuntimeError.new(
|
|
79
|
+
"#{values.fetch(:"#{prefix}_class")}: #{values.fetch(:"#{prefix}_message")}"
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def error_class_name(error)
|
|
84
|
+
error_class = OBJECT_CLASS.bind_call(error)
|
|
85
|
+
MODULE_NAME.bind_call(error_class) || MODULE_NAME.bind_call(error_class.superclass) || "StandardError"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def error_message(error) = EXCEPTION_MESSAGE.bind_call(error)
|
|
89
|
+
private :error_class_name, :error_message
|
|
90
|
+
|
|
91
|
+
private_class_method :normalize_details, :append_detail!, :validate_complete!, :normalize_detail_name,
|
|
92
|
+
:restored_error
|
|
93
|
+
end
|
|
94
|
+
end
|
data/lib/smith/version.rb
CHANGED
|
@@ -4,16 +4,17 @@ module Smith
|
|
|
4
4
|
class Workflow
|
|
5
5
|
# rubocop:disable Style/RedundantStructKeywordInit
|
|
6
6
|
AgentResult = Struct.new(
|
|
7
|
-
:content, :input_tokens, :output_tokens, :cost, :model_used,
|
|
7
|
+
:content, :input_tokens, :output_tokens, :cost, :model_used, :provider_used,
|
|
8
8
|
keyword_init: true
|
|
9
9
|
) do
|
|
10
|
-
def self.from_response(response, content, model_used: nil)
|
|
10
|
+
def self.from_response(response, content, model_used: nil, provider_used: nil)
|
|
11
11
|
new(
|
|
12
12
|
content: content,
|
|
13
13
|
input_tokens: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
|
|
14
14
|
output_tokens: response.respond_to?(:output_tokens) ? response.output_tokens : nil,
|
|
15
15
|
cost: nil,
|
|
16
|
-
model_used: model_used
|
|
16
|
+
model_used: model_used,
|
|
17
|
+
provider_used: provider_used
|
|
17
18
|
)
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -13,7 +13,7 @@ module Smith
|
|
|
13
13
|
attr_reader :total_tokens, :total_cost
|
|
14
14
|
|
|
15
15
|
USAGE_ATTRIBUTES = %w[
|
|
16
|
-
usage_id agent_name model input_tokens output_tokens cost attempt_kind recorded_at
|
|
16
|
+
usage_id agent_name model provider input_tokens output_tokens cost attempt_kind recorded_at
|
|
17
17
|
].freeze
|
|
18
18
|
TOOL_ATTRIBUTES = %w[tool captured].freeze
|
|
19
19
|
private_constant :USAGE_ATTRIBUTES, :TOOL_ATTRIBUTES
|
|
@@ -70,6 +70,8 @@ module Smith
|
|
|
70
70
|
def validate_usage_identity!(entry)
|
|
71
71
|
validate_usage_id!(entry.fetch("usage_id"))
|
|
72
72
|
validate_agent_name!(entry.fetch("agent_name"))
|
|
73
|
+
provider = entry.fetch("provider")
|
|
74
|
+
validate_nonempty_string!(provider, "composite usage entry provider") if provider
|
|
73
75
|
%w[model attempt_kind recorded_at].each do |key|
|
|
74
76
|
validate_nonempty_string!(entry.fetch(key), "composite usage entry #{key}")
|
|
75
77
|
end
|
|
@@ -25,7 +25,7 @@ module Smith
|
|
|
25
25
|
saved_deadline = Tool.current_deadline
|
|
26
26
|
saved_call_ledger = Thread.current[:smith_call_ledger]
|
|
27
27
|
snapshot = ThreadContextSnapshot.new(
|
|
28
|
-
tool_attributes: %i[current_deadline current_tool_call_allowance],
|
|
28
|
+
tool_attributes: %i[current_deadline current_tool_call_allowance current_tool_execution_tracker],
|
|
29
29
|
thread_keys: %i[smith_call_deadline smith_call_ledger],
|
|
30
30
|
scoped_artifacts: false
|
|
31
31
|
)
|
|
@@ -41,6 +41,7 @@ module Smith
|
|
|
41
41
|
apply_agent_deadline(agent_class)
|
|
42
42
|
narrow_tool_deadline!
|
|
43
43
|
apply_agent_tool_calls(agent_class)
|
|
44
|
+
Tool.current_tool_execution_tracker ||= Tool::ExecutionTracker.new
|
|
44
45
|
apply_agent_call_ledger(agent_class)
|
|
45
46
|
end
|
|
46
47
|
|
|
@@ -49,6 +50,7 @@ module Smith
|
|
|
49
50
|
Thread.current[:smith_call_ledger] = call_ledger
|
|
50
51
|
clear_agent_deadline
|
|
51
52
|
clear_agent_tool_calls
|
|
53
|
+
Tool.current_tool_execution_tracker = nil
|
|
52
54
|
end
|
|
53
55
|
|
|
54
56
|
def effective_call_ledger
|
|
@@ -74,7 +76,23 @@ module Smith
|
|
|
74
76
|
|
|
75
77
|
def apply_agent_tool_calls(agent_class)
|
|
76
78
|
agent_tc = agent_class&.budget&.dig(:tool_calls)
|
|
77
|
-
|
|
79
|
+
if agent_class&.tool_budget_exhaustion == :complete && agent_tc.nil?
|
|
80
|
+
raise AgentError, "tool_budget_exhaustion :complete requires a finite tool_calls budget"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Tool.current_tool_call_allowance = build_agent_tool_call_allowance(agent_class, agent_tc)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_agent_tool_call_allowance(agent_class, agent_tool_calls)
|
|
87
|
+
return unless agent_tool_calls
|
|
88
|
+
|
|
89
|
+
parent = Tool.current_tool_call_allowance
|
|
90
|
+
if parent && !parent.is_a?(Tool::CallAllowance)
|
|
91
|
+
raise AgentError, "agent tool_calls budgets cannot scope a legacy Hash tool call allowance"
|
|
92
|
+
end
|
|
93
|
+
return parent.scope(agent_tool_calls, on_exhaustion: agent_class.tool_budget_exhaustion) if parent
|
|
94
|
+
|
|
95
|
+
Tool::CallAllowance.new(agent_tool_calls, on_exhaustion: agent_class.tool_budget_exhaustion)
|
|
78
96
|
end
|
|
79
97
|
|
|
80
98
|
def clear_agent_tool_calls
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "agent_result"
|
|
4
4
|
require_relative "execution_binding_resolution"
|
|
5
|
+
require_relative "guarded_step_execution"
|
|
5
6
|
require_relative "prepared_branch_execution"
|
|
6
7
|
require_relative "step_completion"
|
|
7
8
|
require_relative "step_context"
|
|
@@ -10,6 +11,7 @@ module Smith
|
|
|
10
11
|
class Workflow
|
|
11
12
|
module Execution
|
|
12
13
|
include ExecutionBindingResolution
|
|
14
|
+
include GuardedStepExecution
|
|
13
15
|
include PreparedBranchExecution
|
|
14
16
|
include StepCompletion
|
|
15
17
|
include StepContext
|
|
@@ -39,15 +41,6 @@ module Smith
|
|
|
39
41
|
Tool.current_tool_result_collector = tool_result_collector
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
def run_guarded_step(transition)
|
|
43
|
-
return apply_composite_reduction!(transition) if @composite_reduction
|
|
44
|
-
|
|
45
|
-
@resolved_parallel_branch_count = preflight_branch_count(transition)
|
|
46
|
-
run_standard_guarded_step(transition)
|
|
47
|
-
ensure
|
|
48
|
-
@resolved_parallel_branch_count = nil
|
|
49
|
-
end
|
|
50
|
-
|
|
51
44
|
def run_standard_guarded_step(transition)
|
|
52
45
|
return dispatch_step(transition) if transition.deterministic?
|
|
53
46
|
return run_guarded_fanout_step(transition) if transition.fanout?
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
require_relative "../diagnostic_text"
|
|
7
|
+
require_relative "message_value_normalizer"
|
|
8
|
+
|
|
9
|
+
module Smith
|
|
10
|
+
class Workflow
|
|
11
|
+
class FailureDetailSnapshot
|
|
12
|
+
OMITTED_KEY = "smith_failure_details_omitted"
|
|
13
|
+
OMITTED_REASON_BYTES = 1_024
|
|
14
|
+
private_constant :OMITTED_KEY, :OMITTED_REASON_BYTES
|
|
15
|
+
|
|
16
|
+
extend Dry::Initializer
|
|
17
|
+
|
|
18
|
+
param :value
|
|
19
|
+
|
|
20
|
+
def call
|
|
21
|
+
return if value.nil?
|
|
22
|
+
|
|
23
|
+
snapshot = MessageValueNormalizer.new(value, label: "workflow failure details").call
|
|
24
|
+
JSON.generate(snapshot)
|
|
25
|
+
snapshot
|
|
26
|
+
rescue StandardError => e
|
|
27
|
+
{
|
|
28
|
+
OMITTED_KEY => DiagnosticText.capture(e.message, max_bytes: OMITTED_REASON_BYTES)
|
|
29
|
+
}.freeze
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "failure_record_validator"
|
|
7
|
+
|
|
8
|
+
module Smith
|
|
9
|
+
class Workflow
|
|
10
|
+
class FailureReconstructor
|
|
11
|
+
FAMILY_BUILDERS = {
|
|
12
|
+
"deterministic_step_failure" => ->(record) { deterministic_step_failure(record) },
|
|
13
|
+
"tool_guardrail_failed" => ->(record) { tool_guardrail_failure(record) },
|
|
14
|
+
"tool_failure_notification_failed" => ->(record) { tool_failure_notification_failure(record) },
|
|
15
|
+
"tool_capture_failed" => ->(record) { Smith::ToolCaptureFailed.from_details(record.fetch(:error_details)) },
|
|
16
|
+
"tool_outcome_uncertain" => ->(record) { Smith::ToolOutcomeUncertain.new(record[:error_message]) },
|
|
17
|
+
"tool_execution_not_admitted" => ->(record) { Smith::ToolExecutionNotAdmitted.new(record[:error_message]) },
|
|
18
|
+
"bounded_completion_error" => ->(record) { Smith::BoundedCompletionError.new(record[:error_message]) },
|
|
19
|
+
"persisted_failure_invalid" => ->(record) { Smith::PersistedFailureInvalid.new(record[:error_message]) },
|
|
20
|
+
"deadline_exceeded" => ->(record) { Smith::DeadlineExceeded.new(record[:error_message]) },
|
|
21
|
+
"agent_error" => ->(record) { Smith::AgentError.new(record[:error_message]) },
|
|
22
|
+
"workflow_error" => ->(record) { Smith::WorkflowError.new(record[:error_message]) },
|
|
23
|
+
"other" => ->(record) { RuntimeError.new(record[:error_message]) }
|
|
24
|
+
}.freeze
|
|
25
|
+
SPECIAL_CLASS_BUILDERS = {
|
|
26
|
+
"Smith::Workflow::Composite::BranchFailure" => lambda { |record|
|
|
27
|
+
Smith::Workflow::Composite::BranchFailure.from_details(record[:error_details])
|
|
28
|
+
}
|
|
29
|
+
}.freeze
|
|
30
|
+
private_constant :FAMILY_BUILDERS, :SPECIAL_CLASS_BUILDERS
|
|
31
|
+
|
|
32
|
+
extend Dry::Initializer
|
|
33
|
+
|
|
34
|
+
option :snapshot
|
|
35
|
+
option :transition_normalizer
|
|
36
|
+
option :state_normalizer
|
|
37
|
+
|
|
38
|
+
def call
|
|
39
|
+
{
|
|
40
|
+
transition: transition_normalizer.call(snapshot[:transition]),
|
|
41
|
+
from: state_normalizer.call(snapshot[:from]),
|
|
42
|
+
to: state_normalizer.call(snapshot[:to]),
|
|
43
|
+
error: reconstruct_error
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def reconstruct_error
|
|
50
|
+
FailureRecordValidator.new(snapshot).call
|
|
51
|
+
builder = SPECIAL_CLASS_BUILDERS[snapshot[:error_class]] || FAMILY_BUILDERS.fetch(snapshot[:error_family])
|
|
52
|
+
builder.call(snapshot)
|
|
53
|
+
rescue Smith::PersistedFailureInvalid
|
|
54
|
+
raise
|
|
55
|
+
rescue ArgumentError, KeyError, TypeError
|
|
56
|
+
raise Smith::PersistedFailureInvalid, "persisted workflow failure details are invalid"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.deterministic_step_failure(record)
|
|
60
|
+
Smith::DeterministicStepFailure.new(
|
|
61
|
+
record[:error_message],
|
|
62
|
+
retryable: record[:error_retryable],
|
|
63
|
+
kind: record[:error_kind],
|
|
64
|
+
details: record[:error_details]
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def self.tool_guardrail_failure(record)
|
|
69
|
+
Smith::ToolGuardrailFailed.new(record[:error_message], retryable: record[:error_retryable])
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.tool_failure_notification_failure(record)
|
|
73
|
+
Smith::ToolFailureNotificationFailed.from_details(record.fetch(:error_details))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private_class_method :deterministic_step_failure, :tool_guardrail_failure, :tool_failure_notification_failure
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "../diagnostic_text"
|
|
6
|
+
require_relative "../errors"
|
|
7
|
+
require_relative "failure_detail_snapshot"
|
|
8
|
+
require_relative "failure_record_text"
|
|
9
|
+
|
|
10
|
+
module Smith
|
|
11
|
+
class Workflow
|
|
12
|
+
class FailureRecord
|
|
13
|
+
ERROR_FAMILIES = [
|
|
14
|
+
[Smith::DeterministicStepFailure, "deterministic_step_failure"],
|
|
15
|
+
[Smith::ToolFailureNotificationFailed, "tool_failure_notification_failed"],
|
|
16
|
+
[Smith::ToolCaptureFailed, "tool_capture_failed"],
|
|
17
|
+
[Smith::ToolOutcomeUncertain, "tool_outcome_uncertain"],
|
|
18
|
+
[Smith::ToolExecutionNotAdmitted, "tool_execution_not_admitted"],
|
|
19
|
+
[Smith::BoundedCompletionError, "bounded_completion_error"],
|
|
20
|
+
[Smith::PersistedFailureInvalid, "persisted_failure_invalid"],
|
|
21
|
+
[Smith::ToolGuardrailFailed, "tool_guardrail_failed"],
|
|
22
|
+
[Smith::DeadlineExceeded, "deadline_exceeded"],
|
|
23
|
+
[Smith::AgentError, "agent_error"],
|
|
24
|
+
[Smith::WorkflowError, "workflow_error"]
|
|
25
|
+
].freeze
|
|
26
|
+
BOOLEAN_VALUES = [true, false].freeze
|
|
27
|
+
EXCEPTION_CAUSE = Exception.instance_method(:cause)
|
|
28
|
+
EXCEPTION_MESSAGE = Exception.instance_method(:message)
|
|
29
|
+
MODULE_MATCH = Module.instance_method(:===)
|
|
30
|
+
MODULE_NAME = Module.instance_method(:name)
|
|
31
|
+
OBJECT_CLASS = Object.instance_method(:class)
|
|
32
|
+
private_constant :ERROR_FAMILIES, :BOOLEAN_VALUES, :EXCEPTION_CAUSE, :EXCEPTION_MESSAGE, :MODULE_MATCH,
|
|
33
|
+
:MODULE_NAME, :OBJECT_CLASS
|
|
34
|
+
|
|
35
|
+
extend Dry::Initializer
|
|
36
|
+
|
|
37
|
+
option :step_result
|
|
38
|
+
|
|
39
|
+
def self.capture(step_result) = new(step_result:).call
|
|
40
|
+
|
|
41
|
+
def call
|
|
42
|
+
error = step_result.fetch(:error)
|
|
43
|
+
{
|
|
44
|
+
transition: step_result[:transition],
|
|
45
|
+
from: step_result[:from],
|
|
46
|
+
to: step_result[:to],
|
|
47
|
+
**error_attributes(error)
|
|
48
|
+
}.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def error_attributes(error)
|
|
54
|
+
{
|
|
55
|
+
error_class: DiagnosticText.capture(error_class_name(error), max_bytes: 512),
|
|
56
|
+
error_family: error_family(error),
|
|
57
|
+
error_message: error_message(error),
|
|
58
|
+
error_retryable: retryable_value(error),
|
|
59
|
+
error_retry_forbidden: Smith::Errors.retry_forbidden?(error),
|
|
60
|
+
error_kind: error_kind(error),
|
|
61
|
+
error_details: error_details(error),
|
|
62
|
+
**cause_attributes(error)
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Uncertainty wrappers replace the causal failure at the transition
|
|
67
|
+
# boundary; without this classification a restored host cannot
|
|
68
|
+
# distinguish a deadline, cancellation, or defect behind an
|
|
69
|
+
# uncertain tool outcome.
|
|
70
|
+
def cause_attributes(error)
|
|
71
|
+
cause = uncertainty_cause(error)
|
|
72
|
+
{
|
|
73
|
+
error_cause_class: cause && DiagnosticText.capture(error_class_name(cause), max_bytes: 512),
|
|
74
|
+
error_cause_family: cause && error_family(cause),
|
|
75
|
+
error_cause_message: cause && error_message(cause)
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def uncertainty_cause(error)
|
|
80
|
+
return unless MODULE_MATCH.bind_call(Smith::ToolOutcomeUncertain, error)
|
|
81
|
+
|
|
82
|
+
cause = EXCEPTION_CAUSE.bind_call(error)
|
|
83
|
+
cause if MODULE_MATCH.bind_call(Exception, cause)
|
|
84
|
+
rescue StandardError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def error_family(error)
|
|
89
|
+
ERROR_FAMILIES.find { |error_class, _| MODULE_MATCH.bind_call(error_class, error) }&.last || "other"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def error_class_name(error)
|
|
93
|
+
error_class = OBJECT_CLASS.bind_call(error)
|
|
94
|
+
MODULE_NAME.bind_call(error_class) || "anonymous_error"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Never captures blank text: restore requires non-empty messages, so
|
|
98
|
+
# a blank live message takes the shared deterministic placeholder.
|
|
99
|
+
def error_message(error)
|
|
100
|
+
captured = DiagnosticText.capture(EXCEPTION_MESSAGE.bind_call(error))
|
|
101
|
+
captured.empty? ? FailureRecordText::MISSING_TEXT : captured
|
|
102
|
+
rescue StandardError
|
|
103
|
+
FailureRecordText::MISSING_TEXT
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def retryable_value(error)
|
|
107
|
+
return unless error.respond_to?(:retryable)
|
|
108
|
+
|
|
109
|
+
value = error.retryable
|
|
110
|
+
value if BOOLEAN_VALUES.include?(value)
|
|
111
|
+
rescue StandardError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def error_kind(error)
|
|
116
|
+
return unless error.respond_to?(:kind)
|
|
117
|
+
|
|
118
|
+
value = error.kind
|
|
119
|
+
return unless value.is_a?(String) || value.is_a?(Symbol)
|
|
120
|
+
|
|
121
|
+
captured = DiagnosticText.capture(value.to_s, max_bytes: 256)
|
|
122
|
+
# Restore rejects blank kinds; an empty kind carries no signal.
|
|
123
|
+
captured unless captured.empty?
|
|
124
|
+
rescue StandardError
|
|
125
|
+
nil
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def error_details(error)
|
|
129
|
+
return unless error.respond_to?(:details)
|
|
130
|
+
|
|
131
|
+
FailureDetailSnapshot.new(error.details).call
|
|
132
|
+
rescue StandardError => e
|
|
133
|
+
FailureDetailSnapshot.new(
|
|
134
|
+
smith_failure_details_omitted: DiagnosticText.capture(e.message, max_bytes: 1_024)
|
|
135
|
+
).call
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|