smith-agents 0.7.0 → 0.10.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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +456 -0
  3. data/README.md +71 -1
  4. data/docs/CONFIGURATION.md +27 -6
  5. data/docs/PERSISTENCE.md +27 -1
  6. data/docs/TOOLS_AND_GUARDRAILS.md +233 -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 +43 -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 +128 -0
  13. data/lib/smith/agent/lifecycle.rb +8 -275
  14. data/lib/smith/agent/model_reference.rb +76 -0
  15. data/lib/smith/agent/provider_attempt.rb +34 -0
  16. data/lib/smith/agent/provider_call_timing.rb +74 -0
  17. data/lib/smith/agent/provider_candidate_sequence.rb +55 -0
  18. data/lib/smith/agent/provider_completion.rb +148 -0
  19. data/lib/smith/agent/provider_failure_handling.rb +93 -0
  20. data/lib/smith/agent/provider_usage.rb +28 -0
  21. data/lib/smith/agent/reserved_input_bridge.rb +40 -0
  22. data/lib/smith/agent/usage_entry_recording.rb +66 -0
  23. data/lib/smith/agent/usage_traces.rb +49 -0
  24. data/lib/smith/agent/usage_tracking.rb +116 -0
  25. data/lib/smith/agent.rb +29 -197
  26. data/lib/smith/attribution.rb +115 -0
  27. data/lib/smith/context/observation_masking.rb +28 -4
  28. data/lib/smith/context/session.rb +7 -2
  29. data/lib/smith/diagnostic_text.rb +67 -0
  30. data/lib/smith/doctor/checks/configuration.rb +16 -3
  31. data/lib/smith/doctor/checks/models_registry.rb +75 -17
  32. data/lib/smith/errors.rb +34 -1
  33. data/lib/smith/event.rb +10 -2
  34. data/lib/smith/events/bus.rb +63 -5
  35. data/lib/smith/events/step_completed.rb +3 -0
  36. data/lib/smith/events/step_failed.rb +25 -0
  37. data/lib/smith/events/subscription.rb +8 -0
  38. data/lib/smith/models/ambiguous_profile_error.rb +9 -0
  39. data/lib/smith/models/collision_error.rb +9 -0
  40. data/lib/smith/models/inference.rb +14 -0
  41. data/lib/smith/models/normalizer.rb +11 -67
  42. data/lib/smith/models/provider_qualified_registry.rb +101 -0
  43. data/lib/smith/models/tool_routing.rb +140 -0
  44. data/lib/smith/models.rb +24 -78
  45. data/lib/smith/persisted_failure_invalid.rb +7 -0
  46. data/lib/smith/pricing.rb +138 -17
  47. data/lib/smith/pricing_configuration_error.rb +7 -0
  48. data/lib/smith/provider_permanent_failure.rb +16 -0
  49. data/lib/smith/tool/argument_container_reader.rb +97 -0
  50. data/lib/smith/tool/argument_scalar_snapshot.rb +99 -0
  51. data/lib/smith/tool/argument_snapshot.rb +19 -0
  52. data/lib/smith/tool/argument_snapshot_accounting.rb +69 -0
  53. data/lib/smith/tool/argument_snapshot_result.rb +15 -0
  54. data/lib/smith/tool/argument_snapshot_traversal.rb +119 -0
  55. data/lib/smith/tool/bounded_completion_context.rb +131 -0
  56. data/lib/smith/tool/bounded_completion_controls.rb +113 -0
  57. data/lib/smith/tool/bounded_completion_guard.rb +101 -0
  58. data/lib/smith/tool/bounded_completion_installation.rb +20 -0
  59. data/lib/smith/tool/bounded_completion_state.rb +57 -0
  60. data/lib/smith/tool/budget_enforcement.rb +15 -7
  61. data/lib/smith/tool/call_admission.rb +44 -0
  62. data/lib/smith/tool/call_allowance.rb +101 -28
  63. data/lib/smith/tool/call_allowance_counter.rb +43 -0
  64. data/lib/smith/tool/call_batch.rb +47 -0
  65. data/lib/smith/tool/call_budget.rb +51 -0
  66. data/lib/smith/tool/call_reservation.rb +42 -0
  67. data/lib/smith/tool/capture.rb +9 -1
  68. data/lib/smith/tool/chat_execution_callbacks.rb +58 -0
  69. data/lib/smith/tool/chat_execution_context.rb +7 -88
  70. data/lib/smith/tool/execution_authority.rb +42 -0
  71. data/lib/smith/tool/execution_authorization.rb +17 -0
  72. data/lib/smith/tool/execution_batch.rb +100 -0
  73. data/lib/smith/tool/execution_batch_admission.rb +56 -0
  74. data/lib/smith/tool/execution_batch_builder.rb +122 -0
  75. data/lib/smith/tool/execution_batch_collection.rb +70 -0
  76. data/lib/smith/tool/execution_batch_invocations.rb +28 -0
  77. data/lib/smith/tool/execution_batch_lifecycle.rb +91 -0
  78. data/lib/smith/tool/execution_batch_registry.rb +99 -0
  79. data/lib/smith/tool/execution_batch_requests.rb +59 -0
  80. data/lib/smith/tool/execution_batch_source_call.rb +36 -0
  81. data/lib/smith/tool/execution_batch_source_metadata.rb +107 -0
  82. data/lib/smith/tool/execution_batch_sources.rb +32 -0
  83. data/lib/smith/tool/execution_batch_state.rb +110 -0
  84. data/lib/smith/tool/execution_dispatch.rb +78 -0
  85. data/lib/smith/tool/execution_failure_handling.rb +101 -0
  86. data/lib/smith/tool/execution_lifecycle.rb +28 -0
  87. data/lib/smith/tool/execution_tracker.rb +20 -0
  88. data/lib/smith/tool/fail_fast_completion.rb +24 -0
  89. data/lib/smith/tool/graceful_completion.rb +20 -0
  90. data/lib/smith/tool/invocation.rb +27 -0
  91. data/lib/smith/tool/invocation_request.rb +37 -0
  92. data/lib/smith/tool/invocation_sequence.rb +26 -0
  93. data/lib/smith/tool/legacy_call_allowance.rb +57 -0
  94. data/lib/smith/tool/scoped_context.rb +60 -8
  95. data/lib/smith/tool.rb +81 -13
  96. data/lib/smith/tool_execution_not_admitted.rb +7 -0
  97. data/lib/smith/tool_failure_notification_failed.rb +94 -0
  98. data/lib/smith/trace/logger.rb +1 -0
  99. data/lib/smith/trace/memory.rb +40 -4
  100. data/lib/smith/trace/open_telemetry.rb +47 -3
  101. data/lib/smith/trace.rb +24 -5
  102. data/lib/smith/version.rb +2 -2
  103. data/lib/smith/workflow/agent_result.rb +4 -3
  104. data/lib/smith/workflow/artifact_integration.rb +2 -1
  105. data/lib/smith/workflow/composite/effects.rb +64 -31
  106. data/lib/smith/workflow/deadline_enforcement.rb +20 -2
  107. data/lib/smith/workflow/deterministic_execution.rb +2 -1
  108. data/lib/smith/workflow/deterministic_step.rb +14 -1
  109. data/lib/smith/workflow/dsl.rb +25 -1
  110. data/lib/smith/workflow/evaluator_optimizer.rb +3 -1
  111. data/lib/smith/workflow/event_integration.rb +48 -1
  112. data/lib/smith/workflow/execution.rb +10 -9
  113. data/lib/smith/workflow/failure_detail_snapshot.rb +33 -0
  114. data/lib/smith/workflow/failure_reconstructor.rb +79 -0
  115. data/lib/smith/workflow/failure_record.rb +139 -0
  116. data/lib/smith/workflow/failure_record_restore.rb +145 -0
  117. data/lib/smith/workflow/failure_record_text.rb +83 -0
  118. data/lib/smith/workflow/failure_record_validator.rb +113 -0
  119. data/lib/smith/workflow/fanout_execution.rb +1 -1
  120. data/lib/smith/workflow/guarded_step_execution.rb +39 -0
  121. data/lib/smith/workflow/guardrail_integration.rb +25 -0
  122. data/lib/smith/workflow/parallel.rb +14 -3
  123. data/lib/smith/workflow/parallel_execution.rb +6 -0
  124. data/lib/smith/workflow/persistence.rb +55 -20
  125. data/lib/smith/workflow/prepared_branch_execution.rb +13 -4
  126. data/lib/smith/workflow/retry_execution.rb +5 -1
  127. data/lib/smith/workflow/split_step_persistence/composite_branch_execution.rb +9 -1
  128. data/lib/smith/workflow/split_step_persistence/state_snapshot.rb +1 -0
  129. data/lib/smith/workflow/step_completion.rb +9 -0
  130. data/lib/smith/workflow/step_context.rb +46 -0
  131. data/lib/smith/workflow/thread_context_snapshot.rb +6 -2
  132. data/lib/smith/workflow/transition.rb +9 -4
  133. data/lib/smith/workflow/usage_entry.rb +43 -5
  134. data/lib/smith/workflow.rb +43 -131
  135. data/lib/smith.rb +10 -2
  136. metadata +82 -11
@@ -15,10 +15,14 @@ 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
19
20
  current_invocation_context
21
+ current_invocation_sequence
22
+ current_invocation
20
23
  ].freeze
21
24
  THREAD_KEYS = %i[
25
+ smith_attribution
22
26
  smith_call_deadline
23
27
  smith_call_ledger
24
28
  smith_failed_agent_results
@@ -68,14 +72,14 @@ module Smith
68
72
  def restore_context!
69
73
  raise WorkflowError, "thread context snapshot has already been restored" if @restored
70
74
 
71
- @tool_values.each { |attribute, value| Tool.public_send("#{attribute}=", value) }
75
+ @tool_values.each { |attribute, value| Tool.send("#{attribute}=", value) }
72
76
  @thread_values.each { |key, value| Thread.current[key] = value }
73
77
  Smith.scoped_artifacts = @artifact_store if scoped_artifacts
74
78
  @restored = true
75
79
  end
76
80
 
77
81
  def capture_tool_values
78
- tool_attributes.to_h { |attribute| [attribute, Tool.public_send(attribute)] }.freeze
82
+ tool_attributes.to_h { |attribute| [attribute, Tool.send(attribute)] }.freeze
79
83
  end
80
84
 
81
85
  def capture_thread_values
@@ -336,10 +336,7 @@ module Smith
336
336
 
337
337
  raise WorkflowError, "retry_on error classes must inherit from StandardError"
338
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
339
+ reject_retry_forbidden!(error_classes)
343
340
 
344
341
  ExponentialBackoff.new(
345
342
  attempts:,
@@ -352,6 +349,14 @@ module Smith
352
349
  raise WorkflowError, "retry_on #{e.message}"
353
350
  end
354
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
+
355
360
  def normalize_deterministic_routes!(routes)
356
361
  return nil if routes.nil?
357
362
  raise WorkflowError, "deterministic routes must be an Array" unless routes.is_a?(Array)
@@ -2,26 +2,64 @@
2
2
 
3
3
  module Smith
4
4
  class Workflow
5
- # One row per agent provider call. `usage_id` is a UUID generated at
6
- # recording time and stable across persist/restore so hosts can use it as an
7
- # idempotency anchor.
5
+ # One row per provider RESPONSE within an agent provider attempt.
6
+ # `usage_id` is a UUID generated at recording time and stable across
7
+ # persist/restore so hosts can use it as an idempotency anchor.
8
+ #
9
+ # `transition`, `branch_key`, `round`, and `workflow` come from the
10
+ # ambient Smith::Attribution at recording time, so hosts can attribute
11
+ # usage to the exact step, fan-out branch, optimizer round, and (for
12
+ # nested workflows) the exact graph that spent it.
13
+ # `attempt_id` is shared by every entry the same provider attempt
14
+ # produced (an attempt with an N-round tool loop records N entries):
15
+ # join on it against the `:provider_call` trace event for the attempt's
16
+ # single measured duration; never sum a duration across entries. Every
17
+ # attempt emits its `:provider_call` (an attempt aborted by a
18
+ # non-provider error emits with `outcome: :aborted` before re-raising),
19
+ # so prefix-accounted entries always have their join target.
20
+ # All optional members are nil on entries restored from checkpoints written by
21
+ # earlier Smith versions, and nil attribution members are omitted from
22
+ # serialization so those documents re-serialize byte-identically.
8
23
  # rubocop:disable Style/RedundantStructKeywordInit
9
24
  UsageEntry = Struct.new(
10
25
  :usage_id,
11
26
  :agent_name,
12
27
  :model,
28
+ :provider,
13
29
  :input_tokens,
14
30
  :output_tokens,
15
31
  :cost,
16
32
  :attempt_kind,
17
33
  :recorded_at,
34
+ :transition,
35
+ :branch_key,
36
+ :round,
37
+ :attempt_id,
38
+ :workflow,
18
39
  keyword_init: true
19
40
  ) do
41
+ def initialize(**attributes)
42
+ attributes = attributes.transform_values do |value|
43
+ value.is_a?(String) ? value.dup.freeze : value
44
+ end
45
+ super(**attributes) # rubocop:disable Style/SuperArguments
46
+ freeze
47
+ end
48
+
49
+ # Serialization is additive only when present: nil attribution members
50
+ # are omitted so an entry restored from a pre-attribution checkpoint
51
+ # re-serializes byte-identically. Hosts that digest whole persisted
52
+ # documents (exact-mutation proofs) depend on this stability.
53
+ def to_h
54
+ super.reject { |key, value| value.nil? && %i[transition branch_key round attempt_id workflow].include?(key) }
55
+ end
56
+
20
57
  def self.from_h(hash)
21
58
  sym = hash.transform_keys(&:to_sym)
22
59
  filtered = sym.slice(*members)
23
- filtered[:agent_name] = filtered[:agent_name].to_sym if filtered[:agent_name].is_a?(String)
24
- filtered[:attempt_kind] = filtered[:attempt_kind].to_sym if filtered[:attempt_kind].is_a?(String)
60
+ %i[agent_name provider attempt_kind transition branch_key].each do |attribute|
61
+ filtered[attribute] = filtered[attribute].to_sym if filtered[attribute].is_a?(String)
62
+ end
25
63
  new(**filtered)
26
64
  end
27
65
  end
@@ -3,11 +3,13 @@
3
3
  require "digest"
4
4
  require "json"
5
5
  require "securerandom"
6
- require "set"
7
6
  require "time"
8
7
 
9
8
  require_relative "workflow/agent_result"
10
9
  require_relative "workflow/branch_env"
10
+ require_relative "workflow/failure_record"
11
+ require_relative "workflow/failure_record_restore"
12
+ require_relative "workflow/failure_reconstructor"
11
13
  require_relative "workflow/run_result"
12
14
  require_relative "workflow/string_snapshot"
13
15
  require_relative "workflow/usage_entry"
@@ -30,51 +32,6 @@ module Smith
30
32
 
31
33
  DEFAULT_MAX_TRANSITIONS = 100
32
34
 
33
- # Reconstruct Smith error classes from `@last_failed_step` snapshots.
34
- # Order matters: more-specific subclasses first, so a real DSF doesn't
35
- # get caught by the WorkflowError handler. Each lambda preserves the
36
- # billing-critical attributes (`retryable`, `kind`, `details`) by
37
- # routing through the original constructor — Smith's retryable errors
38
- # expose `attr_reader :retryable` only, with no setter, so kwargs
39
- # MUST flow through `initialize`.
40
- KNOWN_RECONSTRUCTORS = {
41
- "Smith::ToolCaptureFailed" => ->(s) {
42
- Smith::ToolCaptureFailed.from_details(s.fetch(:error_details))
43
- },
44
- "Smith::ToolGuardrailFailed" => ->(s) {
45
- Smith::ToolGuardrailFailed.new(s[:error_message], retryable: s[:error_retryable])
46
- },
47
- "Smith::DeterministicStepFailure" => ->(s) {
48
- Smith::DeterministicStepFailure.new(
49
- s[:error_message],
50
- retryable: s[:error_retryable],
51
- kind: s[:error_kind],
52
- details: s[:error_details]
53
- )
54
- },
55
- "Smith::AgentError" => ->(s) { Smith::AgentError.new(s[:error_message]) },
56
- "Smith::DeadlineExceeded" => ->(s) { Smith::DeadlineExceeded.new(s[:error_message]) },
57
- "Smith::WorkflowError" => ->(s) { Smith::WorkflowError.new(s[:error_message]) },
58
- "Smith::Workflow::Composite::BranchFailure" => lambda { |s|
59
- Smith::Workflow::Composite::BranchFailure.from_details(s[:error_details])
60
- },
61
- # Smith errors with non-message constructors map to compatible
62
- # superclass — message preserved, original metadata (agent_name,
63
- # model_used, requested_name, workflow_class, origin_state) lossy
64
- # but `is_a?` classification round-trips via the superclass.
65
- "Smith::BlankAgentOutputError" => ->(s) { Smith::AgentError.new(s[:error_message]) },
66
- "Smith::UnresolvedTransitionError" => ->(s) { Smith::WorkflowError.new(s[:error_message]) }
67
- }.freeze
68
- private_constant :KNOWN_RECONSTRUCTORS
69
-
70
- # Families whose retryable/kind/details attributes are billing-critical.
71
- # For these, the reconstruction path bypasses `const_get(...).new(message)`
72
- # (which would succeed for unknown subclasses with message-only
73
- # constructors but discard the kwargs) and uses the family fallback
74
- # directly so the parent-class constructor preserves the attrs.
75
- RETRYABLE_BEARING_FAMILIES = %w[deterministic_step_failure tool_guardrail_failed].freeze
76
- private_constant :RETRYABLE_BEARING_FAMILIES
77
-
78
35
  attr_reader :state, :last_prepared_input, :ledger
79
36
 
80
37
  def session_messages
@@ -101,6 +58,14 @@ module Smith
101
58
  @usage_entries = []
102
59
  @usage_mutex = Mutex.new
103
60
  @last_output = nil
61
+ # Durable attribution of the most recent serial `execute :agent` step:
62
+ # { model:, provider: } of the model that actually served it (post
63
+ # fallback resolution), exposed to the following deterministic step as
64
+ # `last_agent_model` / `last_agent_provider`. `@pending_agent_execution`
65
+ # is the transient per-step carrier from `execute_serial_step` to
66
+ # `complete_step`; it is never persisted.
67
+ @last_agent_execution = nil
68
+ @pending_agent_execution = nil
104
69
  @last_failed_step = nil
105
70
  # Optimistic-locking version. Incremented on each persist!; restored
106
71
  # from the persisted payload. Adapters that support store_versioned
@@ -113,6 +78,7 @@ module Smith
113
78
  # seed_validation is :warn or :strict; nil when no seed builder
114
79
  # ran or its output was empty.
115
80
  @seed_digest = nil
81
+ @seed_message_count = 0
116
82
  # Idempotency marker stamped between persist-before-advance and
117
83
  # persist-after-advance under idempotency_mode :strict; restored
118
84
  # workflows with the marker set raise
@@ -199,11 +165,23 @@ module Smith
199
165
  end
200
166
  end
201
167
 
168
+ # Public, read-only view of the per-provider-call usage ledger, so hosts
169
+ # can diff usage across a step boundary without paying a full to_state
170
+ # serialization. The entries themselves are frozen; the returned array is
171
+ # a frozen copy taken under the recording mutex. Tolerates allocated but
172
+ # unrestored instances (no mutex yet), which have no entries.
173
+ def usage_entries
174
+ mutex = @usage_mutex
175
+ return [].freeze unless mutex
176
+
177
+ mutex.synchronize { @usage_entries.dup.freeze }
178
+ end
179
+
202
180
  private
203
181
 
204
182
  def initial_persist_auto_seed
205
183
  manager = self.class.context_manager
206
- return [] unless manager && manager.respond_to?(:persist_mode) && manager.persist_mode == :auto
184
+ return [] unless manager.respond_to?(:persist_mode) && manager.persist_mode == :auto
207
185
 
208
186
  manager.persist_auto_seed.map(&:to_sym)
209
187
  end
@@ -223,31 +201,7 @@ module Smith
223
201
  return unless step_result
224
202
 
225
203
  if step_result[:error]
226
- err = step_result[:error]
227
- # error_family preserves classification across reconstruction
228
- # even when the exact class can't be rebuilt. Order matters:
229
- # specific subclasses first (DSF before WorkflowError, etc.)
230
- # so a real DSF doesn't get classified as workflow_error.
231
- error_family = case err
232
- when Smith::DeterministicStepFailure then "deterministic_step_failure"
233
- when Smith::ToolCaptureFailed then "tool_capture_failed"
234
- when Smith::ToolGuardrailFailed then "tool_guardrail_failed"
235
- when Smith::DeadlineExceeded then "deadline_exceeded"
236
- when Smith::AgentError then "agent_error"
237
- when Smith::WorkflowError then "workflow_error"
238
- else "other"
239
- end
240
- @last_failed_step = {
241
- transition: step_result[:transition],
242
- from: step_result[:from],
243
- to: step_result[:to],
244
- error_class: err.class.name,
245
- error_family: error_family,
246
- error_message: err.message,
247
- error_retryable: err.respond_to?(:retryable) ? err.retryable : nil,
248
- error_kind: err.respond_to?(:kind) ? err.kind : nil,
249
- error_details: err.respond_to?(:details) ? err.details : nil
250
- }
204
+ @last_failed_step = FailureRecord.capture(step_result)
251
205
  else
252
206
  # Successful step: clear any prior failed-step snapshot
253
207
  # (workflow handled the failure and continued) and capture
@@ -255,6 +209,10 @@ module Smith
255
209
  # matches `RunResult#output`'s `.compact.first` semantics).
256
210
  @last_failed_step = nil
257
211
  @last_output = step_result[:output] if step_result.key?(:output) && !step_result[:output].nil?
212
+ # Capture serial agent attribution only on steps that carried it (an
213
+ # `execute :agent` step); deterministic steps have no :model key, so
214
+ # the last agent execution persists across intervening compute steps.
215
+ @last_agent_execution = { model: step_result[:model], provider: step_result[:provider] } if step_result.key?(:model)
258
216
  end
259
217
  end
260
218
 
@@ -275,7 +233,7 @@ module Smith
275
233
  name = @next_transition_name
276
234
  @next_transition_name = nil
277
235
  transition = self.class.find_transition(name) ||
278
- raise(UnresolvedTransitionError.new(name, self.class, @state))
236
+ raise(UnresolvedTransitionError.new(name, self.class, @state))
279
237
  validate_transition_origin!(transition)
280
238
  transition
281
239
  else
@@ -316,10 +274,10 @@ module Smith
316
274
  # terminal state never produces a synthetic error even if the
317
275
  # snapshot wasn't cleared.
318
276
  effective_steps = if steps.empty? && failed? && @last_failed_step
319
- [reconstruct_failed_step]
320
- else
321
- steps
322
- end
277
+ [reconstruct_failed_step]
278
+ else
279
+ steps
280
+ end
323
281
 
324
282
  RunResult.new(
325
283
  state: @state,
@@ -336,59 +294,11 @@ module Smith
336
294
  end
337
295
 
338
296
  def reconstruct_failed_step
339
- snap = @last_failed_step
340
- builder = KNOWN_RECONSTRUCTORS[snap[:error_class]]
341
- error = if builder
342
- builder.call(snap)
343
- elsif RETRYABLE_BEARING_FAMILIES.include?(snap[:error_family])
344
- # Skip const_get for retryable-bearing families. An unknown
345
- # subclass with a message-only constructor would const_get
346
- # successfully but discard the snapshot's `retryable`/`kind`/
347
- # `details` (defaults to nil), and host retry classification
348
- # could misclassify a retryable failure as terminal.
349
- # Family fallback rebuilds the parent class with kwargs intact.
350
- family_fallback(snap)
351
- else
352
- # Unknown subclass without retryable-bearing semantics. Try
353
- # the exact class for shape preservation; fall back via family
354
- # if the constructor doesn't accept message-only args (or the
355
- # class doesn't exist).
356
- begin
357
- Kernel.const_get(snap[:error_class]).new(snap[:error_message])
358
- rescue NameError, ArgumentError
359
- family_fallback(snap)
360
- end
361
- end
362
-
363
- # Symbol coercion on the way out: live steps carry these as
364
- # symbols; JSON round-trip stringifies them; coerce back to
365
- # match fresh-run shape exactly.
366
- {
367
- transition: normalize_transition_name(snap[:transition]),
368
- from: normalize_state_name(snap[:from]),
369
- to: normalize_state_name(snap[:to]),
370
- error: error
371
- }
372
- end
373
-
374
- def family_fallback(snap)
375
- case snap[:error_family]
376
- when "deterministic_step_failure"
377
- Smith::DeterministicStepFailure.new(
378
- snap[:error_message],
379
- retryable: snap[:error_retryable],
380
- kind: snap[:error_kind],
381
- details: snap[:error_details]
382
- )
383
- when "tool_guardrail_failed"
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))
387
- when "deadline_exceeded" then Smith::DeadlineExceeded.new(snap[:error_message])
388
- when "agent_error" then Smith::AgentError.new(snap[:error_message])
389
- when "workflow_error" then Smith::WorkflowError.new(snap[:error_message])
390
- else RuntimeError.new(snap[:error_message])
391
- end
297
+ FailureReconstructor.new(
298
+ snapshot: @last_failed_step,
299
+ transition_normalizer: method(:normalize_transition_name),
300
+ state_normalizer: method(:normalize_state_name)
301
+ ).call
392
302
  end
393
303
 
394
304
  def seed_initial_session_messages
@@ -396,6 +306,7 @@ module Smith
396
306
  return if messages.nil?
397
307
 
398
308
  @session_messages = messages
309
+ @seed_message_count = messages.length
399
310
  @seed_digest = compute_seed_digest(messages)
400
311
  end
401
312
 
@@ -453,7 +364,8 @@ module Smith
453
364
  # Same rule applies to nested-workflow rollup (see
454
365
  # `nested_execution.rb`).
455
366
  def snapshot_usage_entries
456
- @usage_entries.map { |entry| Workflow::UsageEntry.from_h(snapshot_value(entry.to_h)) }
367
+ entries = @usage_mutex.synchronize { @usage_entries.dup }
368
+ entries.map { |entry| Workflow::UsageEntry.from_h(snapshot_value(entry.to_h)) }
457
369
  end
458
370
 
459
371
  def tool_result_collector
data/lib/smith.rb CHANGED
@@ -40,11 +40,15 @@ module Smith
40
40
  setting :trace_transitions, default: true
41
41
  setting :trace_tool_calls, default: true
42
42
  setting :trace_token_usage, default: true
43
+ setting :trace_provider_calls, default: true
43
44
  setting :trace_cost, default: true
44
45
  setting :trace_fields
45
46
  setting :trace_content, default: false
46
- setting :trace_retention
47
- setting :trace_tenant_isolation, default: false
47
+ # Merge the ambient Smith::Attribution fields (execution_key, transition,
48
+ # branch_key, round, ...) into every trace payload. Attribution keys are
49
+ # identifiers, not content; they still pass through any configured
50
+ # trace_fields allowlist, and caller-supplied keys win on conflict.
51
+ setting :trace_attribution, default: true
48
52
 
49
53
  # Pricing (§4.5 — model-call cost computation)
50
54
  setting :pricing, default: nil
@@ -177,6 +181,7 @@ end
177
181
  # Leaf modules (no internal dependencies)
178
182
  require_relative "smith/types"
179
183
  require_relative "smith/errors"
184
+ require_relative "smith/tool_failure_notification_failed"
180
185
  require_relative "smith/persistence_payload_conflict"
181
186
  require_relative "smith/exponential_backoff"
182
187
 
@@ -186,6 +191,7 @@ require_relative "smith/events"
186
191
  require_relative "smith/events/subscription"
187
192
  require_relative "smith/events/bus"
188
193
  require_relative "smith/events/step_completed"
194
+ require_relative "smith/events/step_failed"
189
195
 
190
196
  # Budget (depends on Errors)
191
197
  require_relative "smith/budget"
@@ -205,6 +211,7 @@ require_relative "smith/pricing"
205
211
  require_relative "smith/models/profile"
206
212
  require_relative "smith/models"
207
213
  require_relative "smith/models/inference"
214
+ require_relative "smith/models/tool_routing"
208
215
  require_relative "smith/models/normalizer"
209
216
 
210
217
  # OpenAI /v1/responses routing prepend. Dormant until
@@ -254,6 +261,7 @@ require_relative "smith/context/session"
254
261
 
255
262
  # Agent (depends on RubyLLM::Agent)
256
263
  require_relative "smith/agent"
264
+ require_relative "smith/agent/completion"
257
265
  require_relative "smith/agent/lifecycle"
258
266
  require_relative "smith/agent/registry_binding"
259
267
  require_relative "smith/agent/registry"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smith-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Ralak
@@ -117,22 +117,16 @@ dependencies:
117
117
  name: ruby_llm
118
118
  requirement: !ruby/object:Gem::Requirement
119
119
  requirements:
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: '1.15'
123
- - - "<"
120
+ - - '='
124
121
  - !ruby/object:Gem::Version
125
- version: '1.17'
122
+ version: 1.16.0
126
123
  type: :runtime
127
124
  prerelease: false
128
125
  version_requirements: !ruby/object:Gem::Requirement
129
126
  requirements:
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: '1.15'
133
- - - "<"
127
+ - - '='
134
128
  - !ruby/object:Gem::Version
135
- version: '1.17'
129
+ version: 1.16.0
136
130
  description: Smith is a workflow-first multi-agent orchestration library built on
137
131
  RubyLLM. It provides state machine modeling, typed contracts, budget enforcement,
138
132
  guardrails, and observability for agent workflows.
@@ -159,16 +153,34 @@ files:
159
153
  - lib/generators/smith/install/templates/smith.rb.tt
160
154
  - lib/smith.rb
161
155
  - lib/smith/agent.rb
156
+ - lib/smith/agent/chat_construction.rb
157
+ - lib/smith/agent/completion.rb
158
+ - lib/smith/agent/completion_usage_recording.rb
159
+ - lib/smith/agent/dynamic_configuration.rb
160
+ - lib/smith/agent/fallback_configuration.rb
161
+ - lib/smith/agent/invocation_preparation.rb
162
162
  - lib/smith/agent/lifecycle.rb
163
+ - lib/smith/agent/model_reference.rb
164
+ - lib/smith/agent/provider_attempt.rb
165
+ - lib/smith/agent/provider_call_timing.rb
166
+ - lib/smith/agent/provider_candidate_sequence.rb
167
+ - lib/smith/agent/provider_completion.rb
168
+ - lib/smith/agent/provider_failure_handling.rb
169
+ - lib/smith/agent/provider_usage.rb
163
170
  - lib/smith/agent/registry.rb
164
171
  - lib/smith/agent/registry/execution_binding_capture.rb
165
172
  - lib/smith/agent/registry/introspection.rb
166
173
  - lib/smith/agent/registry/mutation_boundary.rb
167
174
  - lib/smith/agent/registry_binding.rb
175
+ - lib/smith/agent/reserved_input_bridge.rb
176
+ - lib/smith/agent/usage_entry_recording.rb
177
+ - lib/smith/agent/usage_traces.rb
178
+ - lib/smith/agent/usage_tracking.rb
168
179
  - lib/smith/artifacts.rb
169
180
  - lib/smith/artifacts/file.rb
170
181
  - lib/smith/artifacts/memory.rb
171
182
  - lib/smith/artifacts/scoped_store.rb
183
+ - lib/smith/attribution.rb
172
184
  - lib/smith/budget.rb
173
185
  - lib/smith/budget/amount_contract.rb
174
186
  - lib/smith/budget/amount_representation.rb
@@ -185,6 +197,7 @@ files:
185
197
  - lib/smith/context/observation_masking.rb
186
198
  - lib/smith/context/session.rb
187
199
  - lib/smith/context/state_injection.rb
200
+ - lib/smith/diagnostic_text.rb
188
201
  - lib/smith/doctor.rb
189
202
  - lib/smith/doctor/check.rb
190
203
  - lib/smith/doctor/checks/baseline.rb
@@ -208,15 +221,21 @@ files:
208
221
  - lib/smith/events/.keep
209
222
  - lib/smith/events/bus.rb
210
223
  - lib/smith/events/step_completed.rb
224
+ - lib/smith/events/step_failed.rb
211
225
  - lib/smith/events/subscription.rb
212
226
  - lib/smith/exponential_backoff.rb
213
227
  - lib/smith/guardrails.rb
214
228
  - lib/smith/guardrails/runner.rb
215
229
  - lib/smith/guardrails/url_verifier.rb
216
230
  - lib/smith/models.rb
231
+ - lib/smith/models/ambiguous_profile_error.rb
232
+ - lib/smith/models/collision_error.rb
217
233
  - lib/smith/models/inference.rb
218
234
  - lib/smith/models/normalizer.rb
219
235
  - lib/smith/models/profile.rb
236
+ - lib/smith/models/provider_qualified_registry.rb
237
+ - lib/smith/models/tool_routing.rb
238
+ - lib/smith/persisted_failure_invalid.rb
220
239
  - lib/smith/persistence_adapters.rb
221
240
  - lib/smith/persistence_adapters/active_record_connection_errors.rb
222
241
  - lib/smith/persistence_adapters/active_record_exact_predicate.rb
@@ -236,22 +255,67 @@ files:
236
255
  - lib/smith/persistence_adapters/version_expectation.rb
237
256
  - lib/smith/persistence_payload_conflict.rb
238
257
  - lib/smith/pricing.rb
258
+ - lib/smith/pricing_configuration_error.rb
259
+ - lib/smith/provider_permanent_failure.rb
239
260
  - lib/smith/providers/openai/responses.rb
240
261
  - lib/smith/providers/openai/routing.rb
241
262
  - lib/smith/providers/openai/tools_extensions.rb
242
263
  - lib/smith/railtie.rb
243
264
  - lib/smith/tasks/doctor.rake
244
265
  - lib/smith/tool.rb
266
+ - lib/smith/tool/argument_container_reader.rb
267
+ - lib/smith/tool/argument_scalar_snapshot.rb
268
+ - lib/smith/tool/argument_snapshot.rb
269
+ - lib/smith/tool/argument_snapshot_accounting.rb
270
+ - lib/smith/tool/argument_snapshot_result.rb
271
+ - lib/smith/tool/argument_snapshot_traversal.rb
272
+ - lib/smith/tool/bounded_completion_context.rb
273
+ - lib/smith/tool/bounded_completion_controls.rb
274
+ - lib/smith/tool/bounded_completion_guard.rb
275
+ - lib/smith/tool/bounded_completion_installation.rb
276
+ - lib/smith/tool/bounded_completion_state.rb
245
277
  - lib/smith/tool/budget_enforcement.rb
278
+ - lib/smith/tool/call_admission.rb
246
279
  - lib/smith/tool/call_allowance.rb
280
+ - lib/smith/tool/call_allowance_counter.rb
281
+ - lib/smith/tool/call_batch.rb
282
+ - lib/smith/tool/call_budget.rb
283
+ - lib/smith/tool/call_reservation.rb
247
284
  - lib/smith/tool/capability_builder.rb
248
285
  - lib/smith/tool/capture.rb
249
286
  - lib/smith/tool/capture_configuration.rb
287
+ - lib/smith/tool/chat_execution_callbacks.rb
250
288
  - lib/smith/tool/chat_execution_context.rb
251
289
  - lib/smith/tool/compatibility.rb
290
+ - lib/smith/tool/execution_authority.rb
291
+ - lib/smith/tool/execution_authorization.rb
292
+ - lib/smith/tool/execution_batch.rb
293
+ - lib/smith/tool/execution_batch_admission.rb
294
+ - lib/smith/tool/execution_batch_builder.rb
295
+ - lib/smith/tool/execution_batch_collection.rb
296
+ - lib/smith/tool/execution_batch_invocations.rb
297
+ - lib/smith/tool/execution_batch_lifecycle.rb
298
+ - lib/smith/tool/execution_batch_registry.rb
299
+ - lib/smith/tool/execution_batch_requests.rb
300
+ - lib/smith/tool/execution_batch_source_call.rb
301
+ - lib/smith/tool/execution_batch_source_metadata.rb
302
+ - lib/smith/tool/execution_batch_sources.rb
303
+ - lib/smith/tool/execution_batch_state.rb
304
+ - lib/smith/tool/execution_dispatch.rb
305
+ - lib/smith/tool/execution_failure_handling.rb
306
+ - lib/smith/tool/execution_lifecycle.rb
307
+ - lib/smith/tool/execution_tracker.rb
308
+ - lib/smith/tool/fail_fast_completion.rb
309
+ - lib/smith/tool/graceful_completion.rb
310
+ - lib/smith/tool/invocation.rb
311
+ - lib/smith/tool/invocation_request.rb
312
+ - lib/smith/tool/invocation_sequence.rb
313
+ - lib/smith/tool/legacy_call_allowance.rb
252
314
  - lib/smith/tool/policy.rb
253
315
  - lib/smith/tool/scoped_context.rb
254
316
  - lib/smith/tool_capture_failed.rb
317
+ - lib/smith/tool_execution_not_admitted.rb
318
+ - lib/smith/tool_failure_notification_failed.rb
255
319
  - lib/smith/tools.rb
256
320
  - lib/smith/tools/think.rb
257
321
  - lib/smith/tools/url_fetcher.rb
@@ -314,6 +378,12 @@ files:
314
378
  - lib/smith/workflow/execution_binding_resolution.rb
315
379
  - lib/smith/workflow/execution_frame.rb
316
380
  - lib/smith/workflow/execution_result_snapshot.rb
381
+ - lib/smith/workflow/failure_detail_snapshot.rb
382
+ - lib/smith/workflow/failure_reconstructor.rb
383
+ - lib/smith/workflow/failure_record.rb
384
+ - lib/smith/workflow/failure_record_restore.rb
385
+ - lib/smith/workflow/failure_record_text.rb
386
+ - lib/smith/workflow/failure_record_validator.rb
317
387
  - lib/smith/workflow/fanout_execution.rb
318
388
  - lib/smith/workflow/graph.rb
319
389
  - lib/smith/workflow/graph/contract_helpers.rb
@@ -348,6 +418,7 @@ files:
348
418
  - lib/smith/workflow/graph/transition_snapshot.rb
349
419
  - lib/smith/workflow/graph/validator.rb
350
420
  - lib/smith/workflow/graph_dsl.rb
421
+ - lib/smith/workflow/guarded_step_execution.rb
351
422
  - lib/smith/workflow/guardrail_integration.rb
352
423
  - lib/smith/workflow/identifier.rb
353
424
  - lib/smith/workflow/message_admission.rb