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.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +264 -1
  3. data/README.md +71 -1
  4. data/docs/CONFIGURATION.md +22 -1
  5. data/docs/PERSISTENCE.md +27 -1
  6. data/docs/TOOLS_AND_GUARDRAILS.md +293 -2
  7. data/lib/smith/agent/chat_construction.rb +112 -0
  8. data/lib/smith/agent/completion.rb +46 -0
  9. data/lib/smith/agent/completion_usage_recording.rb +25 -0
  10. data/lib/smith/agent/dynamic_configuration.rb +69 -0
  11. data/lib/smith/agent/fallback_configuration.rb +26 -0
  12. data/lib/smith/agent/invocation_preparation.rb +104 -0
  13. data/lib/smith/agent/lifecycle.rb +7 -276
  14. data/lib/smith/agent/model_reference.rb +76 -0
  15. data/lib/smith/agent/provider_attempt.rb +28 -0
  16. data/lib/smith/agent/provider_candidate_sequence.rb +55 -0
  17. data/lib/smith/agent/provider_completion.rb +130 -0
  18. data/lib/smith/agent/provider_failure_handling.rb +93 -0
  19. data/lib/smith/agent/provider_usage.rb +28 -0
  20. data/lib/smith/agent/reserved_input_bridge.rb +40 -0
  21. data/lib/smith/agent/usage_entry_recording.rb +43 -0
  22. data/lib/smith/agent/usage_tracking.rb +113 -0
  23. data/lib/smith/agent.rb +31 -177
  24. data/lib/smith/context/observation_masking.rb +28 -4
  25. data/lib/smith/context/session.rb +7 -2
  26. data/lib/smith/diagnostic_text.rb +67 -0
  27. data/lib/smith/doctor/checks/configuration.rb +16 -3
  28. data/lib/smith/doctor/checks/models_registry.rb +75 -17
  29. data/lib/smith/errors.rb +34 -1
  30. data/lib/smith/models/ambiguous_profile_error.rb +9 -0
  31. data/lib/smith/models/collision_error.rb +9 -0
  32. data/lib/smith/models/inference.rb +14 -0
  33. data/lib/smith/models/normalizer.rb +11 -67
  34. data/lib/smith/models/provider_qualified_registry.rb +101 -0
  35. data/lib/smith/models/tool_routing.rb +140 -0
  36. data/lib/smith/models.rb +24 -78
  37. data/lib/smith/persisted_failure_invalid.rb +7 -0
  38. data/lib/smith/pricing.rb +138 -17
  39. data/lib/smith/pricing_configuration_error.rb +7 -0
  40. data/lib/smith/provider_permanent_failure.rb +16 -0
  41. data/lib/smith/tool/argument_container_reader.rb +97 -0
  42. data/lib/smith/tool/argument_scalar_snapshot.rb +99 -0
  43. data/lib/smith/tool/argument_snapshot.rb +19 -0
  44. data/lib/smith/tool/argument_snapshot_accounting.rb +69 -0
  45. data/lib/smith/tool/argument_snapshot_result.rb +15 -0
  46. data/lib/smith/tool/argument_snapshot_traversal.rb +119 -0
  47. data/lib/smith/tool/bounded_completion_context.rb +131 -0
  48. data/lib/smith/tool/bounded_completion_controls.rb +113 -0
  49. data/lib/smith/tool/bounded_completion_guard.rb +101 -0
  50. data/lib/smith/tool/bounded_completion_installation.rb +20 -0
  51. data/lib/smith/tool/bounded_completion_state.rb +57 -0
  52. data/lib/smith/tool/budget_enforcement.rb +17 -11
  53. data/lib/smith/tool/call_admission.rb +44 -0
  54. data/lib/smith/tool/call_allowance.rb +134 -0
  55. data/lib/smith/tool/call_allowance_counter.rb +43 -0
  56. data/lib/smith/tool/call_batch.rb +47 -0
  57. data/lib/smith/tool/call_budget.rb +51 -0
  58. data/lib/smith/tool/call_reservation.rb +42 -0
  59. data/lib/smith/tool/capture.rb +60 -4
  60. data/lib/smith/tool/capture_configuration.rb +20 -0
  61. data/lib/smith/tool/chat_execution_callbacks.rb +58 -0
  62. data/lib/smith/tool/chat_execution_context.rb +24 -0
  63. data/lib/smith/tool/execution_authority.rb +42 -0
  64. data/lib/smith/tool/execution_authorization.rb +17 -0
  65. data/lib/smith/tool/execution_batch.rb +100 -0
  66. data/lib/smith/tool/execution_batch_admission.rb +56 -0
  67. data/lib/smith/tool/execution_batch_builder.rb +122 -0
  68. data/lib/smith/tool/execution_batch_collection.rb +70 -0
  69. data/lib/smith/tool/execution_batch_invocations.rb +28 -0
  70. data/lib/smith/tool/execution_batch_lifecycle.rb +91 -0
  71. data/lib/smith/tool/execution_batch_registry.rb +99 -0
  72. data/lib/smith/tool/execution_batch_requests.rb +59 -0
  73. data/lib/smith/tool/execution_batch_source_call.rb +36 -0
  74. data/lib/smith/tool/execution_batch_source_metadata.rb +107 -0
  75. data/lib/smith/tool/execution_batch_sources.rb +32 -0
  76. data/lib/smith/tool/execution_batch_state.rb +110 -0
  77. data/lib/smith/tool/execution_dispatch.rb +78 -0
  78. data/lib/smith/tool/execution_failure_handling.rb +101 -0
  79. data/lib/smith/tool/execution_lifecycle.rb +28 -0
  80. data/lib/smith/tool/execution_tracker.rb +20 -0
  81. data/lib/smith/tool/fail_fast_completion.rb +24 -0
  82. data/lib/smith/tool/graceful_completion.rb +20 -0
  83. data/lib/smith/tool/invocation.rb +27 -0
  84. data/lib/smith/tool/invocation_request.rb +37 -0
  85. data/lib/smith/tool/invocation_sequence.rb +26 -0
  86. data/lib/smith/tool/legacy_call_allowance.rb +57 -0
  87. data/lib/smith/tool/scoped_context.rb +123 -0
  88. data/lib/smith/tool.rb +78 -57
  89. data/lib/smith/tool_capture_failed.rb +106 -0
  90. data/lib/smith/tool_execution_not_admitted.rb +7 -0
  91. data/lib/smith/tool_failure_notification_failed.rb +94 -0
  92. data/lib/smith/version.rb +2 -2
  93. data/lib/smith/workflow/agent_result.rb +4 -3
  94. data/lib/smith/workflow/artifact_integration.rb +2 -1
  95. data/lib/smith/workflow/composite/branch_failure.rb +34 -13
  96. data/lib/smith/workflow/composite/branch_outcome.rb +19 -2
  97. data/lib/smith/workflow/composite/effects.rb +3 -1
  98. data/lib/smith/workflow/composite/error.rb +53 -5
  99. data/lib/smith/workflow/composite/error_evidence.rb +10 -2
  100. data/lib/smith/workflow/deadline_enforcement.rb +20 -2
  101. data/lib/smith/workflow/execution.rb +2 -9
  102. data/lib/smith/workflow/failure_detail_snapshot.rb +33 -0
  103. data/lib/smith/workflow/failure_reconstructor.rb +79 -0
  104. data/lib/smith/workflow/failure_record.rb +139 -0
  105. data/lib/smith/workflow/failure_record_restore.rb +145 -0
  106. data/lib/smith/workflow/failure_record_text.rb +83 -0
  107. data/lib/smith/workflow/failure_record_validator.rb +113 -0
  108. data/lib/smith/workflow/fanout_execution.rb +12 -11
  109. data/lib/smith/workflow/guarded_step_execution.rb +39 -0
  110. data/lib/smith/workflow/parallel/cancellation_signal.rb +1 -1
  111. data/lib/smith/workflow/parallel/root_execution.rb +1 -1
  112. data/lib/smith/workflow/parallel.rb +17 -1
  113. data/lib/smith/workflow/persistence.rb +22 -19
  114. data/lib/smith/workflow/prepared_branch_execution.rb +9 -2
  115. data/lib/smith/workflow/retry_execution.rb +5 -0
  116. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +13 -4
  117. data/lib/smith/workflow/thread_context_snapshot.rb +6 -2
  118. data/lib/smith/workflow/transition.rb +11 -0
  119. data/lib/smith/workflow/usage_entry.rb +12 -2
  120. data/lib/smith/workflow.rb +17 -124
  121. data/lib/smith.rb +3 -0
  122. metadata +83 -5
data/lib/smith/tool.rb CHANGED
@@ -4,15 +4,67 @@ 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"
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"
7
38
  require_relative "tool/budget_enforcement"
39
+ require_relative "tool_capture_failed"
8
40
  require_relative "tool/capture"
41
+ require_relative "tool/capture_configuration"
9
42
  require_relative "tool/compatibility"
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"
55
+ require_relative "tool/chat_execution_context"
10
56
 
11
57
  module Smith
12
58
  class Tool < RubyLLM::Tool
13
59
  include Policy
60
+ include ExecutionAuthorization
61
+ include ExecutionLifecycle
14
62
  include BudgetEnforcement
15
63
  include Capture
64
+ extend CaptureConfiguration
65
+ extend ScopedContext
66
+
67
+ private_constant :ExecutionAuthority
16
68
 
17
69
  class << self
18
70
  # Tool subclasses inherit the parent's compatible_with spec by
@@ -40,46 +92,6 @@ module Smith
40
92
 
41
93
  attr_reader :compatible_with_spec
42
94
 
43
- def current_guardrails
44
- Thread.current[:smith_tool_guardrails]
45
- end
46
-
47
- def current_guardrails=(value)
48
- Thread.current[:smith_tool_guardrails] = value
49
- end
50
-
51
- def current_deadline
52
- Thread.current[:smith_tool_deadline]
53
- end
54
-
55
- def current_deadline=(value)
56
- Thread.current[:smith_tool_deadline] = value
57
- end
58
-
59
- def current_ledger
60
- Thread.current[:smith_tool_ledger]
61
- end
62
-
63
- def current_ledger=(value)
64
- Thread.current[:smith_tool_ledger] = value
65
- end
66
-
67
- def current_tool_call_allowance
68
- Thread.current[:smith_tool_call_allowance]
69
- end
70
-
71
- def current_tool_call_allowance=(value)
72
- Thread.current[:smith_tool_call_allowance] = value
73
- end
74
-
75
- def current_tool_result_collector
76
- Thread.current[:smith_tool_result_collector]
77
- end
78
-
79
- def current_tool_result_collector=(value)
80
- Thread.current[:smith_tool_result_collector] = value
81
- end
82
-
83
95
  def category(value = nil)
84
96
  return @category if value.nil?
85
97
 
@@ -105,32 +117,35 @@ module Smith
105
117
 
106
118
  @before_execute = block
107
119
  end
108
-
109
- def capture_result(&block)
110
- return @capture_result unless block_given?
111
-
112
- @capture_result = block
113
- end
114
120
  end
115
121
 
116
122
  def execute(**kwargs)
117
- run_before_execute_hook!(kwargs)
118
- check_tool_deadline!
119
- check_privilege!(kwargs)
120
- check_authorization!(kwargs)
121
- run_tool_guardrails!(kwargs)
122
- check_tool_deadline!
123
- charge_tool_call!
124
-
125
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
126
- result = perform(**kwargs)
127
- 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)
128
127
 
129
128
  emit_tool_trace(kwargs, result, duration)
130
129
  capture_result_if_configured(kwargs, result)
131
130
  result
132
131
  end
133
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
+
134
149
  private
135
150
 
136
151
  def run_before_execute_hook!(kwargs)
@@ -164,6 +179,12 @@ module Smith
164
179
  raise DeadlineExceeded, "wall_clock deadline exceeded during tool execution" if Time.now.utc >= deadline
165
180
  end
166
181
 
182
+ def check_dispatch_deadline!
183
+ check_tool_deadline!
184
+ rescue DeadlineExceeded
185
+ raise ToolDispatchRejected, "tool deadline expired before dispatch"
186
+ end
187
+
167
188
  def perform(**kwargs)
168
189
  raise NotImplementedError, "#{self.class} must implement #perform"
169
190
  end
@@ -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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error"
4
+
5
+ module Smith
6
+ class ToolExecutionNotAdmitted < Error; end
7
+ 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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Smith
4
- VERSION = "0.6.1"
5
- EXECUTION_SEMANTICS_VERSION = "2"
4
+ VERSION = "0.8.0"
5
+ EXECUTION_SEMANTICS_VERSION = "5"
6
6
  end
@@ -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
 
@@ -33,7 +33,8 @@ module Smith
33
33
  Context::Session.new(
34
34
  messages: messages,
35
35
  context_manager: manager,
36
- persisted_context: @context
36
+ persisted_context: @context,
37
+ seed_message_count: @seed_message_count || 0
37
38
  )
38
39
  end
39
40
  end
@@ -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
- DETAIL_NAMES = DETAIL_KEYS.values.freeze
18
- private_constant :DETAIL_KEYS, :DETAIL_NAMES
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
- error = Error.new(
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 = DETAIL_NAMES - normalized.keys
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
- @error_class = error.class_name.dup.freeze
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
- }.freeze
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
- expected = PayloadDigest.call(self.class.send(:serializable, to_h.except(:digest)))
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
@@ -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
@@ -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 deadline_exceeded
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
- owned = self.class.normalize_attributes(attributes)
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