smith-agents 0.12.2 → 0.12.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 746bb384c941e4a7276937eec58e74e1b1f11230e7310190a64401e348a92f61
4
- data.tar.gz: 26b5e327966a11488d166214a0363bc2f554a79230b54392775609bcc2d25b6f
3
+ metadata.gz: e2c791c5165889ff1098cc5c92976b1affe2c3f82108815fda1c98fae56c0430
4
+ data.tar.gz: 5b7d1d9f41e2949c335d6dc8b26a7f69aff73c2edf4ee39e100760ca3c4f4145
5
5
  SHA512:
6
- metadata.gz: 652ec2c82ed914ce2aa798ccd87ee6a70afbc9149b731bd16e2ff2a14eecef13c23161b2039d46a5943c2a0eb4bcf9135b48c1ba8308e8585fba827aded3cdc7
7
- data.tar.gz: 2f86f38c713ce0e62cd01ebfbc7b62270d30c30a34f8db2954526c73a9996be0792fd3ef874dbc500e2489883a178e267215b17b53494aaab648d3f9bd612f70
6
+ metadata.gz: e76354065df47cf04703bc41cda18256df9f2118a9d714f046f3fe04c567256d969be828235b0a6da2ff0ee83fe8953ba8c381ed4209c4f49776cde1658ab5d0
7
+ data.tar.gz: 3e73cddd45cd37191712cc2ed4d5cdc1fe62dbe07a2de73b86bd626f43eda0ba74efcbf8b40fabbbc15db21e6517495567ac346c4339f3c9864e316894170d3f
data/CHANGELOG.md CHANGED
@@ -4,6 +4,56 @@ All notable changes to Smith are documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Smith is pre-1.0 and under active development; expect occasional contract tightening between minor versions until 1.0.
6
6
 
7
+ ## [0.12.4] - 2026-10-13
8
+
9
+ ### Changed
10
+
11
+ - A provider's refusal for want of credit or past a spending limit is
12
+ `RubyLLM::PaymentRequiredError` whatever status it came with
13
+ (`Smith::BillingRefusals`), with the error RubyLLM raised as its cause. A
14
+ refusal is told by what the provider says it is: Anthropic's `billing_error`,
15
+ its tier's monthly spend cap (a 429 whose `error_code` is
16
+ `enforced_spend_limit_reached`), its 400s for spent credit ("Your credit
17
+ balance is too low") and for a spend limit the account set ("You have
18
+ reached your specified API usage limits"), and OpenAI's
19
+ `insufficient_quota`. A streamed request's refusal, which RubyLLM raises as
20
+ a `RubyLLM::ServerError`, is one too, and so is one met by
21
+ `Smith::Providers::Anthropic::Batches#submit`. It is the provider account's
22
+ failure, so another provider in the fallback chain is tried, and a host can
23
+ name `RubyLLM::PaymentRequiredError` in `interrupting_errors` to have a step
24
+ wait for credit instead of failing. Any other error is unchanged: a rate
25
+ limit that mentions a quota (Gemini's) stays a rate limit.
26
+ - A model candidate that met an interrupting error outlasts how a later
27
+ fallback fails: the step is cut short, as it would have been with no
28
+ fallback, instead of failing on the fallback's error.
29
+
30
+ ## [0.12.3] - 2026-10-12
31
+
32
+ ### Changed
33
+
34
+ - What identifies a request leaves prompt-cache markers out
35
+ (`Smith::CacheMarkers`): the response memo's key, a batch request's digest
36
+ and a seed's drift digest (`seed_validation`). A marker says how a request
37
+ is billed, never what it asks, so a host may decide call by call whether to
38
+ cache a part (an agent's instructions, a content part, the whole request)
39
+ and a replay still finds what the first call got, as long as the part keeps
40
+ its form (the same text block, with or without its marker). Only the places
41
+ Anthropic puts `cache_control` are looked at (the request, its system and
42
+ message blocks, a tool result's blocks, its tools) and Smith's own
43
+ `"cache" => true` on a seed's content parts; a tool's input or schema, or
44
+ anything else a request carries as data, is never touched. A batch result
45
+ for a request that differs from the one sent only in its markers is no
46
+ longer taken as a changed request.
47
+
48
+ ### Upgrading
49
+
50
+ - Response memo records kept by 0.12.2 for calls that carried a cache marker
51
+ are not found after the upgrade (their key took the marker in), so such a
52
+ call cut by the upgrade's deploy is paid for once more. A batch request
53
+ captured by 0.12.2, and a run's seed digest taken by 0.12.2, still match as
54
+ they were taken, cache markers included: one whose markers change after the
55
+ upgrade counts as changed, as before.
56
+
7
57
  ## [0.12.2] - 2026-10-12
8
58
 
9
59
  ### Added
data/README.md CHANGED
@@ -30,7 +30,7 @@ What you get:
30
30
 
31
31
  ```ruby
32
32
  # Gemfile
33
- gem "smith-agents", "0.12.2", require: "smith"
33
+ gem "smith-agents", "0.12.4", require: "smith"
34
34
  ```
35
35
 
36
36
  In a Rails app:
data/docs/AGENTS.md CHANGED
@@ -216,7 +216,10 @@ without sleeping: `Smith::Providers::Anthropic::Batches` gives the host
216
216
  `submit`, `status`, `results` and `cancel`. `resume!` with
217
217
  `{custom_id => result}` runs the step again, each call taking its result as
218
218
  the reply it would have got. A call that renders another request by then (a
219
- date in its prompt, data read afresh, a prompt changed in a deploy) does what
219
+ date in its prompt, data read afresh, a prompt changed in a deploy; a prompt
220
+ cache marker added or dropped is no change, as it never changes what the
221
+ request asks, though a request captured by Smith 0.12.2 is matched as it was
222
+ sent, markers included) does what
220
223
  its step's `on_request_change:` says: `:keep` (the default) takes the result,
221
224
  which answers the request as sent, and its `:provider_call` trace says
222
225
  `request_changed: true`; `:resend` sends the request as rendered now over the
@@ -99,6 +99,23 @@ next advanced; a strict run is found under way on restore
99
99
  (`Smith::StepInProgressOnRestore`), as after a crash. The instance that
100
100
  raised is not advanced again: restore the run.
101
101
 
102
+ A provider's refusal for want of credit or past a spending limit is
103
+ `RubyLLM::PaymentRequiredError` whatever status it came with, its cause the
104
+ error RubyLLM raised. Smith tells one by what the provider says it is:
105
+ Anthropic's `billing_error` (402), its tier's monthly spend cap (a 429 whose
106
+ `error.details.error_code` is `enforced_spend_limit_reached`), its 400s for
107
+ spent credit ("Your credit balance is too low") and for a spend limit the
108
+ account set ("You have reached your specified API usage limits"), and
109
+ OpenAI's `insufficient_quota` (429). RubyLLM raises a streamed request's
110
+ refusal as a `RubyLLM::ServerError`; it is one all the same. It is the
111
+ provider account's, so another provider in the fallback chain is tried;
112
+ naming `RubyLLM::PaymentRequiredError` makes a step that meets it wait for
113
+ credit instead of failing, even when a fallback then fails some other way.
114
+ `Smith::Providers::Anthropic::Batches#submit` raises it too. Any other error
115
+ stays as it is: a rate limit whose message mentions a quota (Gemini's) is a
116
+ rate limit. RubyLLM's own HTTP retries (`max_retries`) may repeat a 429 or a
117
+ streamed refusal before Smith sees it; a refused request is not charged.
118
+
102
119
  Name only errors that mean your own systems failed. A provider's own failure
103
120
  (`RubyLLM::Error`, or its HTTP client's `Faraday::Error`) is the provider's
104
121
  whatever it was raised from, so naming a low-level class such as
@@ -138,10 +155,12 @@ attempts it runs under (its step's, and each enclosing run's for a nested
138
155
  workflow, so a retry never takes the attempt before's answer), its attempt
139
156
  index (its model in the fallback chain, or a follow-up's place), its place
140
157
  among the attempt's calls, and a digest of its request (model, messages,
141
- tools with their descriptions and parameters, schema, params). A replay
142
- reaches the same key; a call whose request changed since (a deployed prompt or
143
- tool, another model, a date in the prompt) misses and is sent again, as a miss
144
- always may be.
158
+ tools with their descriptions and parameters, schema, params), Anthropic's
159
+ prompt-cache markers aside: whether a part is cached never changes what the
160
+ call asks, so a host may decide it call by call, keeping the part's form (the
161
+ same text block, with or without its marker). A replay reaches the same key; a
162
+ call whose request changed since (a deployed prompt or tool, another model, a
163
+ date in the prompt) misses and is sent again, as a miss always may be.
145
164
 
146
165
  Its record (`{"version" => 1 | 2, "messages" => [...], "complete" => true |
147
166
  false}`, JSON) holds the messages the call added: the model's replies
@@ -123,7 +123,9 @@ module Smith
123
123
  # have their join target; the error then propagates unchanged. A
124
124
  # provider failure's own reported usage is accounted beside the
125
125
  # completed prefix, so its trace totals both.
126
+ # A billing refusal is recorded as the provider account's (BillingRefusals).
126
127
  def failed_provider_attempt(error, observed_reference, timer, facts, prefix_usages)
128
+ error = BillingRefusals.classified(error)
127
129
  aborted = !provider_failure?(error)
128
130
  usages = aborted ? prefix_usages : [*prefix_usages, ProviderUsage.from_message(error)].compact
129
131
  attempt = ProviderAttempt.failure(
@@ -34,18 +34,16 @@ module Smith
34
34
 
35
35
  def attempt_candidates(agent_class, prepared_input, chain, shape)
36
36
  candidates = ProviderCandidateSequence.new(chain)
37
+ interrupted = nil
37
38
  candidates.each do |model_reference, index|
38
39
  check_deadline! if index.positive?
39
40
  withdraw_streamed(index.positive? ? :fallback : :retry)
40
41
  attempt = attempt_model(agent_class, prepared_input, model_reference, shape:, attempt_index: index)
41
42
  return attempt if attempt.success?
42
43
 
44
+ interrupted ||= attempt if Interruptions.find(attempt.error)
43
45
  candidates.suppress(account_failed_provider(attempt, model_reference))
44
- handle_provider_failure!(
45
- attempt.error, attempt.model_reference, agent_class,
46
- fallback_available: candidates.fallback_available?,
47
- attempt_id: attempt.attempt_id
48
- )
46
+ fail_over!(attempt, agent_class, candidates, interrupted)
49
47
  end
50
48
 
51
49
  # Fail closed: an empty candidate chain (agent without a model)
@@ -55,6 +53,22 @@ module Smith
55
53
  "no executable model candidate for #{agent_class}; declare a model or fallback_models"
56
54
  end
57
55
 
56
+ # Returns when another candidate is left to try. A candidate that met
57
+ # an interrupting error (an account out of credit) outlasts how a later
58
+ # one fails: the step was cut short, not failed, and is taken again
59
+ # from its first candidate once the host can do its part.
60
+ def fail_over!(attempt, agent_class, candidates, interrupted)
61
+ handle_provider_failure!(
62
+ attempt.error, attempt.model_reference, agent_class,
63
+ fallback_available: candidates.fallback_available?,
64
+ attempt_id: attempt.attempt_id
65
+ )
66
+ rescue StandardError => e
67
+ raise if interrupted.nil? || Interruptions.find(e)
68
+
69
+ raise terminal_provider_error(interrupted.error, interrupted.model_reference), cause: interrupted.error
70
+ end
71
+
58
72
  # Suppression keys on the provider that actually rejected the
59
73
  # account; when the chat is unobservable the attempted reference's
60
74
  # declared provider is the best available attribution, so a dead
data/lib/smith/batch.rb CHANGED
@@ -45,16 +45,18 @@ module Smith
45
45
  deep_freeze(JSON.parse(encoded))
46
46
  end
47
47
 
48
- # What identifies a request's content: a SHA-256 of its payload and
49
- # headers as JSON gives them, so the request a delivery answers can be
50
- # told from one the step would render now.
51
- def digest(request) = digest_of(request.fetch("params"), request.fetch("headers"))
52
-
53
- # Whether a call renders a request other than the one sent (`digest`).
54
- # Nothing is sent, so nothing in it is refused; one JSON cannot give
55
- # counts as changed.
48
+ # What identifies a request's content: a SHA-256 of its payload, prompt
49
+ # cache markers aside (CacheMarkers), and headers as JSON gives them, so
50
+ # the request a delivery answers can be told from one the step would
51
+ # render now.
52
+ def digest(request) = digest_of(CacheMarkers.strip_request(request.fetch("params")), request.fetch("headers"))
53
+
54
+ # Whether a call renders a request other than the one sent (`digest`),
55
+ # cache markers aside; a digest Smith 0.12.2 took with its markers still
56
+ # matches the request as it was. Nothing is sent, so nothing in it is
57
+ # refused; one JSON cannot give counts as changed.
56
58
  def changed?(digest, params, headers)
57
- digest_of(params, headers.to_h) != digest
59
+ [CacheMarkers.strip_request(params), params].none? { digest_of(_1, headers.to_h) == digest }
58
60
  rescue JSON::GeneratorError, JSON::NestingError, Encoding::UndefinedConversionError
59
61
  true
60
62
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "ruby_llm"
5
+
6
+ module Smith
7
+ # A provider's refusal for want of credit or past a spending limit, whatever
8
+ # status it came with. Smith takes each as RubyLLM::PaymentRequiredError,
9
+ # the error RubyLLM raises for a 402 (its cause the error RubyLLM raised),
10
+ # so the refusal is the provider account's (another provider in the
11
+ # fallback chain is tried) and a host can name that one class in
12
+ # interrupting_errors to have a step wait for credit instead of failing.
13
+ #
14
+ # A refusal is told by what the provider says it is, never by its status
15
+ # alone: Anthropic's billing_error (402), its tier's monthly spend cap (429,
16
+ # error_code enforced_spend_limit_reached), its spent credit and a spend
17
+ # limit the account set (400s known only by how their message begins), and
18
+ # OpenAI's insufficient_quota (429). A streamed request's refusal arrives
19
+ # as a RubyLLM::ServerError, since RubyLLM takes every streamed error as a
20
+ # 500. Any other error stays as it is: a rate limit that mentions a quota
21
+ # (Gemini's) is a rate limit.
22
+ module BillingRefusals
23
+ CLASSES = [RubyLLM::BadRequestError, RubyLLM::RateLimitError, RubyLLM::ServerError].freeze
24
+ TYPES = %w[billing_error insufficient_quota].freeze
25
+ CODES = %w[enforced_spend_limit_reached].freeze
26
+ MESSAGES = [
27
+ /\AYour credit balance is too low/i,
28
+ /\AYou have reached your specified (?:workspace )?API usage limits/i
29
+ ].freeze
30
+ private_constant :CLASSES, :TYPES, :CODES, :MESSAGES
31
+
32
+ module_function
33
+
34
+ # The error as Smith takes it: a billing refusal as a
35
+ # RubyLLM::PaymentRequiredError, anything else unchanged.
36
+ def classified(error)
37
+ return error unless refusal?(error)
38
+
39
+ begin
40
+ raise RubyLLM::PaymentRequiredError.new(error.response, error.message), cause: error
41
+ rescue RubyLLM::PaymentRequiredError => e
42
+ e
43
+ end
44
+ end
45
+
46
+ def refusal?(error)
47
+ return false unless CLASSES.any? { error.is_a?(_1) }
48
+
49
+ said = provider_error(error)
50
+ details = said["details"].is_a?(Hash) ? said["details"] : {}
51
+
52
+ TYPES.include?(said["type"]) ||
53
+ CODES.include?(details["error_code"]) ||
54
+ MESSAGES.any? { error.message.to_s.match?(_1) }
55
+ end
56
+
57
+ # The provider's own error object in the response body, or {} when the
58
+ # body carries none.
59
+ def provider_error(error)
60
+ body = error.response.respond_to?(:body) ? error.response.body : nil
61
+ body = JSON.parse(body) if body.is_a?(String)
62
+ said = body["error"] if body.is_a?(Hash)
63
+ said.is_a?(Hash) ? said : {}
64
+ rescue JSON::ParserError
65
+ {}
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ # Anthropic's prompt-cache markers (`cache_control: { type: "ephemeral" }`
5
+ # on a request, a system block, a content block, a tool result's block or
6
+ # a tool) and Smith's own `"cache" => true` on a content part say how a
7
+ # request is billed, never what it asks: the reply is the same with or
8
+ # without them. What identifies a request (the response memo's key, a batch
9
+ # request's digest, a seed's drift digest) leaves them out, so a host that
10
+ # caches a prompt on one call and not on the next still finds what the
11
+ # first call got. Only the places markers can sit are looked at: a tool's
12
+ # input or schema, or anything else a request carries as data, is never
13
+ # touched, and nothing is walked deeper than a tool result's blocks.
14
+ module CacheMarkers
15
+ MARKER_KEYS = [:cache_control, "cache_control"].freeze
16
+ private_constant :MARKER_KEYS
17
+
18
+ module_function
19
+
20
+ # A request payload (a batch request's params, a chat's params): its own
21
+ # marker, and those of its system blocks, its messages' blocks and its
22
+ # tools.
23
+ def strip_request(request)
24
+ return request unless request.is_a?(Hash)
25
+
26
+ request = unmarked(request)
27
+ request = replaced(request, :system) { strip_blocks(_1) }
28
+ request = replaced(request, :messages) do |messages|
29
+ messages.is_a?(Array) ? messages.map { |message| replaced(message, :content) { strip_blocks(_1) } } : messages
30
+ end
31
+ replaced(request, :tools) { |tools| tools.is_a?(Array) ? tools.map { unmarked(_1) } : tools }
32
+ end
33
+
34
+ # A message's content: a block or blocks, a tool result's own blocks
35
+ # included.
36
+ def strip_blocks(content)
37
+ case content
38
+ when Array then content.map { strip_block(_1) }
39
+ when Hash then strip_block(content)
40
+ else content
41
+ end
42
+ end
43
+
44
+ # Seed messages as Smith keeps them: each content part without its
45
+ # `"cache" => true`.
46
+ def strip_parts(messages)
47
+ return messages unless messages.is_a?(Array)
48
+
49
+ messages.map do |message|
50
+ replaced(message, :content) { |content| content.is_a?(Array) ? content.map { unflagged(_1) } : content }
51
+ end
52
+ end
53
+
54
+ def unflagged(part) = part.is_a?(Hash) ? part.except("cache", :cache) : part
55
+
56
+ def strip_block(block)
57
+ return block unless block.is_a?(Hash)
58
+
59
+ block = unmarked(block)
60
+ return block unless (block[:type] || block["type"]).to_s == "tool_result"
61
+
62
+ replaced(block, :content) { |content| content.is_a?(Array) ? content.map { unmarked(_1) } : unmarked(content) }
63
+ end
64
+
65
+ def unmarked(value)
66
+ return value unless value.is_a?(Hash)
67
+
68
+ value.reject { |key, item| MARKER_KEYS.include?(key) && marker?(item) }
69
+ end
70
+
71
+ def marker?(value) = value.is_a?(Hash) && (value[:type] || value["type"]).to_s == "ephemeral"
72
+
73
+ # The Hash with its value under `key` (a Symbol or a String key) passed
74
+ # through the block.
75
+ def replaced(hash, key)
76
+ return hash unless hash.is_a?(Hash)
77
+
78
+ [key, key.to_s].each do |name|
79
+ hash = hash.merge(name => yield(hash[name])) if hash.key?(name)
80
+ end
81
+ hash
82
+ end
83
+
84
+ private_class_method :strip_block, :unflagged, :unmarked, :marker?, :replaced
85
+ end
86
+ end
@@ -34,13 +34,13 @@ module Smith
34
34
  @connection = RubyLLM::Providers::Anthropic.new(config).connection
35
35
  end
36
36
 
37
- # Submits the requests; returns the batch id.
37
+ # Submits the requests; returns the batch id. A refusal for want of
38
+ # credit raises RubyLLM::PaymentRequiredError (BillingRefusals).
38
39
  def submit(requests)
39
40
  entries = Array(requests).map { |entry| entry.transform_keys(&:to_sym) }
40
41
  body = { requests: entries.map { |entry| { custom_id: entry.fetch(:custom_id), params: params(entry) } } }
41
42
  check_batch!(entries, body)
42
- headers = shared_headers(entries)
43
- @connection.post(PATH, body) { |request| add_headers(request.headers, headers) }.body.fetch("id")
43
+ create(body, shared_headers(entries))
44
44
  end
45
45
 
46
46
  # The batch's processing status ("in_progress", "canceling" or
@@ -67,6 +67,12 @@ module Smith
67
67
 
68
68
  private
69
69
 
70
+ def create(body, headers)
71
+ @connection.post(PATH, body) { |request| add_headers(request.headers, headers) }.body.fetch("id")
72
+ rescue RubyLLM::Error => e
73
+ raise BillingRefusals.classified(e)
74
+ end
75
+
70
76
  def params(entry)
71
77
  request = entry.fetch(:request).transform_keys(&:to_s)
72
78
  raise ArgumentError, "a request for #{request["provider"]} cannot go in an Anthropic batch" unless
@@ -149,7 +149,7 @@ module Smith
149
149
 
150
150
  def request_digest(chat)
151
151
  shape = [chat.model.id, chat.model.provider.to_s, chat.messages.map { message_shape(_1) },
152
- tools_shape(chat.tools), chat.schema, chat.params]
152
+ tools_shape(chat.tools), chat.schema, CacheMarkers.strip_request(chat.params)]
153
153
  Digest::SHA256.hexdigest(JSON.generate(shape))
154
154
  rescue StandardError
155
155
  nil
@@ -158,7 +158,7 @@ module Smith
158
158
  def message_shape(message)
159
159
  content = message.content
160
160
  body = case content
161
- when RubyLLM::Content::Raw then content.value
161
+ when RubyLLM::Content::Raw then CacheMarkers.strip_blocks(content.value)
162
162
  when RubyLLM::Content
163
163
  [content.text, content.attachments.map { [_1.filename, _1.mime_type, _1.content.to_s.bytesize] }]
164
164
  else content
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.12.2"
4
+ VERSION = "0.12.4"
5
5
  EXECUTION_SEMANTICS_VERSION = "7"
6
6
  end
@@ -228,13 +228,16 @@ module Smith
228
228
  validate_step_in_progress!(fields.to_h { |field| [field, state[field] || state[field.to_s]] })
229
229
  end
230
230
 
231
+ # A digest Smith before 0.12.3 took, its parts' cache flags included.
232
+ def flagged_seed_digest?(messages, stored) = compute_seed_digest(messages, cache_flags: true) == stored
233
+
231
234
  def validate_seed_digest!(normalized)
232
235
  stored_digest = normalized[:seed_digest]
233
236
  return if stored_digest.nil?
234
237
 
235
238
  current_messages = compute_seed_messages
236
239
  current_digest = compute_seed_digest(current_messages)
237
- return if current_digest == stored_digest
240
+ return if current_digest == stored_digest || flagged_seed_digest?(current_messages, stored_digest)
238
241
 
239
242
  case self.class.seed_validation
240
243
  when :strict
@@ -412,10 +412,12 @@ module Smith
412
412
  normalize_seed_messages(seeded)
413
413
  end
414
414
 
415
- def compute_seed_digest(messages)
415
+ # A seed's drift digest, its parts' cache flags aside (CacheMarkers);
416
+ # with them, as Smith before 0.12.3 took it.
417
+ def compute_seed_digest(messages, cache_flags: false)
416
418
  return nil if messages.nil? || messages.empty?
417
419
 
418
- Digest::SHA256.hexdigest(JSON.generate(messages))
420
+ Digest::SHA256.hexdigest(JSON.generate(cache_flags ? messages : CacheMarkers.strip_parts(messages)))
419
421
  rescue JSON::GeneratorError, Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e
420
422
  raise WorkflowError,
421
423
  "seed_messages content must be valid UTF-8 (Smith hashes via JSON.generate " \
data/lib/smith.rb CHANGED
@@ -212,6 +212,7 @@ require_relative "smith/tool_failure_notification_failed"
212
212
  require_relative "smith/persistence_payload_conflict"
213
213
  require_relative "smith/exponential_backoff"
214
214
  require_relative "smith/interruptions"
215
+ require_relative "smith/billing_refusals"
215
216
 
216
217
  # Event system (depends on Types)
217
218
  require_relative "smith/event"
@@ -227,6 +228,7 @@ require_relative "smith/budget/ledger"
227
228
  require_relative "smith/budget/host_ledger"
228
229
  require_relative "smith/budget/request_hold"
229
230
  require_relative "smith/budget/continuation"
231
+ require_relative "smith/cache_markers"
230
232
  require_relative "smith/response_memo"
231
233
 
232
234
  # Pricing (depends on Smith config)
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.12.2
4
+ version: 0.12.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Ralak
@@ -203,6 +203,7 @@ files:
203
203
  - lib/smith/attribution.rb
204
204
  - lib/smith/batch.rb
205
205
  - lib/smith/batch/message_shape.rb
206
+ - lib/smith/billing_refusals.rb
206
207
  - lib/smith/budget.rb
207
208
  - lib/smith/budget/amount.rb
208
209
  - lib/smith/budget/amount_contract.rb
@@ -219,6 +220,7 @@ files:
219
220
  - lib/smith/budget/reservation.rb
220
221
  - lib/smith/budget/reservation_contract.rb
221
222
  - lib/smith/budget/spent_reservation.rb
223
+ - lib/smith/cache_markers.rb
222
224
  - lib/smith/child_run.rb
223
225
  - lib/smith/child_runs_failed.rb
224
226
  - lib/smith/cli.rb