smith-agents 0.6.1 → 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 +264 -1
- data/README.md +71 -1
- data/docs/CONFIGURATION.md +22 -1
- data/docs/PERSISTENCE.md +27 -1
- data/docs/TOOLS_AND_GUARDRAILS.md +293 -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 +31 -177
- 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 +17 -11
- data/lib/smith/tool/call_admission.rb +44 -0
- data/lib/smith/tool/call_allowance.rb +134 -0
- 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/capture.rb +60 -4
- data/lib/smith/tool/capture_configuration.rb +20 -0
- data/lib/smith/tool/chat_execution_callbacks.rb +58 -0
- data/lib/smith/tool/chat_execution_context.rb +24 -0
- 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 +123 -0
- data/lib/smith/tool.rb +78 -57
- data/lib/smith/tool_capture_failed.rb +106 -0
- 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/branch_failure.rb +34 -13
- data/lib/smith/workflow/composite/branch_outcome.rb +19 -2
- data/lib/smith/workflow/composite/effects.rb +3 -1
- 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 +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/fanout_execution.rb +12 -11
- data/lib/smith/workflow/guarded_step_execution.rb +39 -0
- 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 +17 -1
- data/lib/smith/workflow/persistence.rb +22 -19
- data/lib/smith/workflow/prepared_branch_execution.rb +9 -2
- data/lib/smith/workflow/retry_execution.rb +5 -0
- data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +13 -4
- data/lib/smith/workflow/thread_context_snapshot.rb +6 -2
- data/lib/smith/workflow/transition.rb +11 -0
- data/lib/smith/workflow/usage_entry.rb +12 -2
- data/lib/smith/workflow.rb +17 -124
- data/lib/smith.rb +3 -0
- metadata +83 -5
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-initializer"
|
|
4
|
+
|
|
5
|
+
require_relative "../errors"
|
|
6
|
+
require_relative "composite/branch_failure"
|
|
7
|
+
|
|
8
|
+
module Smith
|
|
9
|
+
class Workflow
|
|
10
|
+
class FailureRecordValidator
|
|
11
|
+
KNOWN_ERROR_FAMILIES = {
|
|
12
|
+
"Smith::DeterministicStepFailure" => "deterministic_step_failure",
|
|
13
|
+
"Smith::ToolGuardrailFailed" => "tool_guardrail_failed",
|
|
14
|
+
"Smith::ToolCaptureFailed" => "tool_capture_failed",
|
|
15
|
+
"Smith::ToolFailureNotificationFailed" => "tool_failure_notification_failed",
|
|
16
|
+
"Smith::ToolOutcomeUncertain" => "tool_outcome_uncertain",
|
|
17
|
+
"Smith::ToolExecutionNotAdmitted" => "tool_execution_not_admitted",
|
|
18
|
+
"Smith::BoundedCompletionError" => "bounded_completion_error",
|
|
19
|
+
"Smith::PersistedFailureInvalid" => "persisted_failure_invalid",
|
|
20
|
+
"Smith::AgentError" => "agent_error",
|
|
21
|
+
"Smith::BlankAgentOutputError" => "agent_error",
|
|
22
|
+
"Smith::DeadlineExceeded" => "deadline_exceeded",
|
|
23
|
+
"Smith::WorkflowError" => "workflow_error",
|
|
24
|
+
"Smith::UnresolvedTransitionError" => "workflow_error",
|
|
25
|
+
"Smith::Workflow::Composite::BranchFailure" => "workflow_error"
|
|
26
|
+
}.freeze
|
|
27
|
+
KNOWN_FAMILIES = %w[
|
|
28
|
+
deterministic_step_failure tool_guardrail_failed tool_failure_notification_failed tool_capture_failed
|
|
29
|
+
tool_outcome_uncertain tool_execution_not_admitted bounded_completion_error persisted_failure_invalid
|
|
30
|
+
deadline_exceeded agent_error workflow_error other
|
|
31
|
+
].freeze
|
|
32
|
+
RETRY_FORBIDDEN_FAMILIES = %w[
|
|
33
|
+
tool_capture_failed tool_failure_notification_failed tool_outcome_uncertain tool_execution_not_admitted
|
|
34
|
+
bounded_completion_error persisted_failure_invalid
|
|
35
|
+
].freeze
|
|
36
|
+
DETAIL_VALIDATORS = {
|
|
37
|
+
"Smith::ToolCaptureFailed" => ->(details) { Smith::ToolCaptureFailed.from_details(details) },
|
|
38
|
+
"Smith::ToolFailureNotificationFailed" => lambda { |details|
|
|
39
|
+
Smith::ToolFailureNotificationFailed.from_details(details)
|
|
40
|
+
},
|
|
41
|
+
"Smith::Workflow::Composite::BranchFailure" => lambda { |details|
|
|
42
|
+
Smith::Workflow::Composite::BranchFailure.from_details(details)
|
|
43
|
+
}
|
|
44
|
+
}.freeze
|
|
45
|
+
BOOLEAN_VALUES = [true, false].freeze
|
|
46
|
+
private_constant :KNOWN_ERROR_FAMILIES, :KNOWN_FAMILIES, :RETRY_FORBIDDEN_FAMILIES, :DETAIL_VALIDATORS,
|
|
47
|
+
:BOOLEAN_VALUES
|
|
48
|
+
|
|
49
|
+
extend Dry::Initializer
|
|
50
|
+
|
|
51
|
+
param :snapshot
|
|
52
|
+
|
|
53
|
+
def call
|
|
54
|
+
validate_family!
|
|
55
|
+
validate_class_family!
|
|
56
|
+
validate_retry_policy!
|
|
57
|
+
validate_cause!
|
|
58
|
+
validate_details!
|
|
59
|
+
snapshot
|
|
60
|
+
rescue ArgumentError, KeyError, TypeError
|
|
61
|
+
reject!("persisted workflow failure details are invalid")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def family = snapshot[:error_family]
|
|
67
|
+
|
|
68
|
+
def validate_family!
|
|
69
|
+
return if KNOWN_FAMILIES.include?(family)
|
|
70
|
+
|
|
71
|
+
reject!("persisted workflow failure family is invalid")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def validate_class_family!
|
|
75
|
+
expected = KNOWN_ERROR_FAMILIES[snapshot[:error_class]]
|
|
76
|
+
return unless expected && family != expected
|
|
77
|
+
|
|
78
|
+
reject!("persisted workflow failure class and family disagree")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def validate_retry_policy!
|
|
82
|
+
forbidden = snapshot[:error_retry_forbidden]
|
|
83
|
+
return if forbidden.nil?
|
|
84
|
+
|
|
85
|
+
reject!("persisted workflow failure retry policy is invalid") unless BOOLEAN_VALUES.include?(forbidden)
|
|
86
|
+
return if forbidden == RETRY_FORBIDDEN_FAMILIES.include?(family)
|
|
87
|
+
|
|
88
|
+
reject!("persisted workflow failure retry policy disagrees with its family")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Cause classification travels as one unit: capture writes all three
|
|
92
|
+
# attributes together for uncertainty wrappers, so a partial set is
|
|
93
|
+
# corrupt data rather than a legacy shape (legacy records omit all
|
|
94
|
+
# three).
|
|
95
|
+
def validate_cause!
|
|
96
|
+
cause_values = snapshot.values_at(:error_cause_class, :error_cause_family, :error_cause_message)
|
|
97
|
+
return if cause_values.all?(&:nil?)
|
|
98
|
+
|
|
99
|
+
reject!("persisted workflow failure cause is incomplete") if cause_values.any?(&:nil?)
|
|
100
|
+
return if KNOWN_FAMILIES.include?(snapshot[:error_cause_family])
|
|
101
|
+
|
|
102
|
+
reject!("persisted workflow failure cause family is invalid")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def validate_details!
|
|
106
|
+
validator = DETAIL_VALIDATORS[snapshot[:error_class]]
|
|
107
|
+
validator&.call(snapshot[:error_details])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def reject!(message) = raise(Smith::PersistedFailureInvalid, message)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smith
|
|
4
|
+
class Workflow
|
|
5
|
+
module GuardedStepExecution
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def run_guarded_step(transition)
|
|
9
|
+
tracker = Tool::ExecutionTracker.new
|
|
10
|
+
previous_tracker = Tool.current_tool_execution_tracker
|
|
11
|
+
|
|
12
|
+
Thread.handle_interrupt(Object => :never) do
|
|
13
|
+
Tool.current_tool_execution_tracker = tracker
|
|
14
|
+
begin
|
|
15
|
+
Thread.handle_interrupt(Object => :immediate) { run_tracked_guarded_step(transition) }
|
|
16
|
+
rescue StandardError => e
|
|
17
|
+
raise unless tracker.started?
|
|
18
|
+
raise if terminal_retry_error?(e)
|
|
19
|
+
|
|
20
|
+
raise ToolOutcomeUncertain.new(
|
|
21
|
+
"transition failed after tool execution began; retry could replay an uncertain outcome"
|
|
22
|
+
), cause: e
|
|
23
|
+
ensure
|
|
24
|
+
Tool.current_tool_execution_tracker = previous_tracker
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run_tracked_guarded_step(transition)
|
|
30
|
+
return apply_composite_reduction!(transition) if @composite_reduction
|
|
31
|
+
|
|
32
|
+
@resolved_parallel_branch_count = preflight_branch_count(transition)
|
|
33
|
+
run_standard_guarded_step(transition)
|
|
34
|
+
ensure
|
|
35
|
+
@resolved_parallel_branch_count = nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
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,8 +38,22 @@ module Smith
|
|
|
36
38
|
|
|
37
39
|
def self.preferred_error(reasons)
|
|
38
40
|
errors = Array(reasons).compact
|
|
39
|
-
errors
|
|
41
|
+
process_fatal_error(errors) ||
|
|
42
|
+
notification_failure(errors) ||
|
|
43
|
+
terminal_execution_error(errors) ||
|
|
44
|
+
ordinary_error(errors) ||
|
|
45
|
+
errors.first
|
|
40
46
|
end
|
|
47
|
+
|
|
48
|
+
def self.process_fatal_error(errors) = errors.find { !_1.is_a?(StandardError) }
|
|
49
|
+
|
|
50
|
+
def self.notification_failure(errors) = errors.find { _1.is_a?(ToolFailureNotificationFailed) }
|
|
51
|
+
|
|
52
|
+
def self.terminal_execution_error(errors) = errors.find { Smith::Errors.retry_forbidden?(_1) }
|
|
53
|
+
|
|
54
|
+
def self.ordinary_error(errors) = errors.find { !_1.is_a?(Cancellation) }
|
|
55
|
+
|
|
56
|
+
private_class_method :process_fatal_error, :notification_failure, :terminal_execution_error, :ordinary_error
|
|
41
57
|
end
|
|
42
58
|
end
|
|
43
59
|
end
|
|
@@ -48,6 +48,10 @@ module Smith
|
|
|
48
48
|
# cycles so seed_validation can detect when the seed builder
|
|
49
49
|
# has changed in code since this workflow was persisted.
|
|
50
50
|
seed_digest: @seed_digest,
|
|
51
|
+
# Number of immutable seed messages at the beginning of session
|
|
52
|
+
# history. Context strategies may preserve this exact prefix while
|
|
53
|
+
# masking later workflow observations.
|
|
54
|
+
seed_message_count: @seed_message_count || 0,
|
|
51
55
|
# Step-in-progress idempotency marker. Set true between
|
|
52
56
|
# persist-before-advance and persist-after-advance when the
|
|
53
57
|
# workflow class opts into idempotency_mode :strict. Restore
|
|
@@ -76,6 +80,7 @@ module Smith
|
|
|
76
80
|
@ledger = rebuild_ledger(normalized[:budget_consumed] || {})
|
|
77
81
|
@next_transition_name = normalized[:next_transition_name]
|
|
78
82
|
@session_messages = normalized[:session_messages] || []
|
|
83
|
+
@seed_message_count = validated_seed_message_count(normalized)
|
|
79
84
|
@total_cost = normalized[:total_cost] || 0.0
|
|
80
85
|
@total_tokens = normalized[:total_tokens] || 0
|
|
81
86
|
@outcome = normalized[:outcome]
|
|
@@ -120,6 +125,14 @@ module Smith
|
|
|
120
125
|
"persisted workflow persistence_version must be a non-negative integer, got #{version.inspect}"
|
|
121
126
|
end
|
|
122
127
|
|
|
128
|
+
def validated_seed_message_count(normalized)
|
|
129
|
+
count = normalized.fetch(:seed_message_count, 0)
|
|
130
|
+
return count if count.is_a?(Integer) && count >= 0 && count <= @session_messages.length
|
|
131
|
+
|
|
132
|
+
raise Smith::SerializationError,
|
|
133
|
+
"persisted workflow seed_message_count must be between 0 and session message count"
|
|
134
|
+
end
|
|
135
|
+
|
|
123
136
|
def validate_definition_digest!(normalized)
|
|
124
137
|
stored = normalized[:definition_digest]
|
|
125
138
|
return if stored.nil?
|
|
@@ -200,20 +213,11 @@ module Smith
|
|
|
200
213
|
# values become strings).
|
|
201
214
|
def restore_last_failed_step(normalized)
|
|
202
215
|
raw = normalized[:last_failed_step]
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
from: normalize_state_name(h[:from]),
|
|
209
|
-
to: normalize_state_name(h[:to]),
|
|
210
|
-
error_class: h[:error_class],
|
|
211
|
-
error_family: h[:error_family],
|
|
212
|
-
error_message: h[:error_message],
|
|
213
|
-
error_retryable: h[:error_retryable],
|
|
214
|
-
error_kind: h[:error_kind]&.to_sym,
|
|
215
|
-
error_details: h[:error_details]
|
|
216
|
-
}
|
|
216
|
+
FailureRecordRestore.new(
|
|
217
|
+
raw,
|
|
218
|
+
transition_normalizer: method(:normalize_transition_name),
|
|
219
|
+
state_normalizer: method(:normalize_state_name)
|
|
220
|
+
).call
|
|
217
221
|
end
|
|
218
222
|
|
|
219
223
|
def restore_core_fields(normalized)
|
|
@@ -234,9 +238,9 @@ module Smith
|
|
|
234
238
|
end
|
|
235
239
|
|
|
236
240
|
manager = self.class.context_manager
|
|
237
|
-
if manager
|
|
241
|
+
if manager.respond_to?(:persist_mode) && manager.persist_mode == :auto
|
|
238
242
|
ctx = normalized[:context]
|
|
239
|
-
existing = ctx.is_a?(Hash) ? ctx.keys.map
|
|
243
|
+
existing = ctx.is_a?(Hash) ? ctx.keys.map(&:to_sym) : []
|
|
240
244
|
seed = manager.persist_auto_seed.map(&:to_sym)
|
|
241
245
|
@persisted_keys = ::Set.new(existing + seed)
|
|
242
246
|
else
|
|
@@ -309,6 +313,7 @@ module Smith
|
|
|
309
313
|
|
|
310
314
|
def normalize_transition_name(value)
|
|
311
315
|
return if value.nil?
|
|
316
|
+
|
|
312
317
|
transition = self.class.find_transition(value)
|
|
313
318
|
return transition.name if transition
|
|
314
319
|
return value unless value.is_a?(String)
|
|
@@ -416,9 +421,7 @@ module Smith
|
|
|
416
421
|
manager = self.class.context_manager
|
|
417
422
|
return nil unless manager
|
|
418
423
|
|
|
419
|
-
if manager.respond_to?(:persist_mode) && manager.persist_mode == :auto
|
|
420
|
-
return :auto
|
|
421
|
-
end
|
|
424
|
+
return :auto if manager.respond_to?(:persist_mode) && manager.persist_mode == :auto
|
|
422
425
|
|
|
423
426
|
keys = manager.persist
|
|
424
427
|
keys.empty? ? nil : keys
|
|
@@ -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
|
|
|
@@ -24,6 +24,7 @@ module Smith
|
|
|
24
24
|
|
|
25
25
|
def retry_transition_error?(config, error, attempt)
|
|
26
26
|
return false if attempt >= config.fetch(:attempts)
|
|
27
|
+
return false if terminal_retry_error?(error)
|
|
27
28
|
|
|
28
29
|
classes = config.fetch(:error_classes)
|
|
29
30
|
if classes.any?
|
|
@@ -33,6 +34,10 @@ module Smith
|
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
37
|
+
def terminal_retry_error?(error)
|
|
38
|
+
Smith::Errors.retry_forbidden?(error)
|
|
39
|
+
end
|
|
40
|
+
|
|
36
41
|
def sleep_for_retry(schedule, failed_attempt)
|
|
37
42
|
delay = retry_delay(schedule, failed_attempt)
|
|
38
43
|
sleep(delay) if delay.positive?
|
|
@@ -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
|
|
|
@@ -15,7 +15,11 @@ module Smith
|
|
|
15
15
|
current_deadline
|
|
16
16
|
current_ledger
|
|
17
17
|
current_tool_call_allowance
|
|
18
|
+
current_tool_execution_tracker
|
|
18
19
|
current_tool_result_collector
|
|
20
|
+
current_invocation_context
|
|
21
|
+
current_invocation_sequence
|
|
22
|
+
current_invocation
|
|
19
23
|
].freeze
|
|
20
24
|
THREAD_KEYS = %i[
|
|
21
25
|
smith_call_deadline
|
|
@@ -67,14 +71,14 @@ module Smith
|
|
|
67
71
|
def restore_context!
|
|
68
72
|
raise WorkflowError, "thread context snapshot has already been restored" if @restored
|
|
69
73
|
|
|
70
|
-
@tool_values.each { |attribute, value| Tool.
|
|
74
|
+
@tool_values.each { |attribute, value| Tool.send("#{attribute}=", value) }
|
|
71
75
|
@thread_values.each { |key, value| Thread.current[key] = value }
|
|
72
76
|
Smith.scoped_artifacts = @artifact_store if scoped_artifacts
|
|
73
77
|
@restored = true
|
|
74
78
|
end
|
|
75
79
|
|
|
76
80
|
def capture_tool_values
|
|
77
|
-
tool_attributes.to_h { |attribute| [attribute, Tool.
|
|
81
|
+
tool_attributes.to_h { |attribute| [attribute, Tool.send(attribute)] }.freeze
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
def capture_thread_values
|
|
@@ -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,7 @@ module Smith
|
|
|
334
336
|
|
|
335
337
|
raise WorkflowError, "retry_on error classes must inherit from StandardError"
|
|
336
338
|
end
|
|
339
|
+
reject_retry_forbidden!(error_classes)
|
|
337
340
|
|
|
338
341
|
ExponentialBackoff.new(
|
|
339
342
|
attempts:,
|
|
@@ -346,6 +349,14 @@ module Smith
|
|
|
346
349
|
raise WorkflowError, "retry_on #{e.message}"
|
|
347
350
|
end
|
|
348
351
|
|
|
352
|
+
def reject_retry_forbidden!(error_classes)
|
|
353
|
+
forbidden = error_classes.find { |error_class| Smith::Errors.retry_forbidden_class?(error_class) }
|
|
354
|
+
return unless forbidden
|
|
355
|
+
|
|
356
|
+
label = forbidden.name || "an anonymous terminal tool-evidence error class"
|
|
357
|
+
raise WorkflowError, "retry_on cannot retry #{label} because the tool outcome may be uncertain"
|
|
358
|
+
end
|
|
359
|
+
|
|
349
360
|
def normalize_deterministic_routes!(routes)
|
|
350
361
|
return nil if routes.nil?
|
|
351
362
|
raise WorkflowError, "deterministic routes must be an Array" unless routes.is_a?(Array)
|
|
@@ -10,6 +10,7 @@ module Smith
|
|
|
10
10
|
:usage_id,
|
|
11
11
|
:agent_name,
|
|
12
12
|
:model,
|
|
13
|
+
:provider,
|
|
13
14
|
:input_tokens,
|
|
14
15
|
:output_tokens,
|
|
15
16
|
:cost,
|
|
@@ -17,11 +18,20 @@ module Smith
|
|
|
17
18
|
:recorded_at,
|
|
18
19
|
keyword_init: true
|
|
19
20
|
) do
|
|
21
|
+
def initialize(**attributes)
|
|
22
|
+
attributes = attributes.transform_values do |value|
|
|
23
|
+
value.is_a?(String) ? value.dup.freeze : value
|
|
24
|
+
end
|
|
25
|
+
super(**attributes) # rubocop:disable Style/SuperArguments
|
|
26
|
+
freeze
|
|
27
|
+
end
|
|
28
|
+
|
|
20
29
|
def self.from_h(hash)
|
|
21
30
|
sym = hash.transform_keys(&:to_sym)
|
|
22
31
|
filtered = sym.slice(*members)
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
%i[agent_name provider attempt_kind].each do |attribute|
|
|
33
|
+
filtered[attribute] = filtered[attribute].to_sym if filtered[attribute].is_a?(String)
|
|
34
|
+
end
|
|
25
35
|
new(**filtered)
|
|
26
36
|
end
|
|
27
37
|
end
|