riffer 0.46.0 → 0.47.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac304f75fface8720f1e2565ea2064b643abb2f1c29f5a9dfe5281820e9d3986
4
- data.tar.gz: de5695cffa30697e0356de921227caffdab12ff806e143166369c36e3ae010d6
3
+ metadata.gz: 63cc9b38354166dc44a162e7fb8cc168cf14e950d8cbbba582fb9710721c25a5
4
+ data.tar.gz: 8a386abc38526d731d7253efbab2c8e2ab345da712762d605f469c5708ec5cca
5
5
  SHA512:
6
- metadata.gz: dfe9da5a5cb0a8a1147846766acc79b518a9f7025d1d95fbfddce3604e2315afafc04762b339879eee2db875aba5cc296056045f213f47a17cb9f730e17f3e07
7
- data.tar.gz: b548a4b44b001b79ee295969af5a868f50d0e847a183dc9433e5b23d4b1e7c9effed3ea64a76a8f5022db959a65394d8f8ff28bba9a6d46484841322483ebe02
6
+ metadata.gz: 9a36b54ef99b21ea74a8e215c3ba5abb70c2bf949e40bf15056209f9cc4875c62505406568fc5a258508d17735a5d92c360510a5092a4f158733feebd6f64fb4
7
+ data.tar.gz: 69209c501af21331079d0e5374b4b41119619ddd81afa6debeb8e93da3955bb144370cb3c1b506411043de9544f070855c66ef62819d5affadc3d671410b906f
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.46.0"
2
+ ".": "0.47.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.47.0](https://github.com/janeapp/riffer/compare/riffer/v0.46.1...riffer/v0.47.0) (2026-09-11)
9
+
10
+
11
+ ### Features
12
+
13
+ * **bedrock:** moving cachePoint so the conversation tail is cached ([#431](https://github.com/janeapp/riffer/issues/431)) ([4fa44b8](https://github.com/janeapp/riffer/commit/4fa44b817c00642303ac30002c33dd77c6d502ab))
14
+
15
+ ## [0.46.1](https://github.com/janeapp/riffer/compare/riffer/v0.46.0...riffer/v0.46.1) (2026-09-11)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * accept JSON integers for Float params ([#429](https://github.com/janeapp/riffer/issues/429)) ([928c33b](https://github.com/janeapp/riffer/commit/928c33b1125bf013f66c8b736191bc70eb313ccd))
21
+ * **agent:** type stream's enumerator as returning Response ([#428](https://github.com/janeapp/riffer/issues/428)) ([4d2b2d2](https://github.com/janeapp/riffer/commit/4d2b2d2aa08d1d95b97226e84180284349fff441))
22
+
8
23
  ## [0.46.0](https://github.com/janeapp/riffer/compare/riffer/v0.45.0...riffer/v0.46.0) (2026-09-08)
9
24
 
10
25
 
@@ -59,6 +59,14 @@ response = MyAgent.generate('What is in this image?', files: [
59
59
 
60
60
  Streams a response as an Enumerator. Same prompt/files semantics as `generate`.
61
61
 
62
+ Consuming the enumerator with a block returns the same `Riffer::Agent::Response` that `generate` would, so you can stream events to the user and still inspect the final outcome:
63
+
64
+ ```ruby
65
+ response = MyAgent.stream('Tell me a story').each { |event| handle(event) }
66
+ response.outcome.reason # => :completed
67
+ response.content
68
+ ```
69
+
62
70
  ```ruby
63
71
  # New conversation (class method — recommended for simple calls)
64
72
  MyAgent.stream('Tell me a story').each do |event|
data/docs/TOOLS.md CHANGED
@@ -115,6 +115,8 @@ Options:
115
115
 
116
116
  `Riffer::Params::Boolean` is the preferred way to declare boolean parameters. `TrueClass` and `FalseClass` continue to work for backwards compatibility.
117
117
 
118
+ A `Float` param accepts a whole number too, since JSON Schema's `number` covers integers — a model returning `120` for a `Float` is valid, and the validated value is coerced to `120.0`. `Integer` stays strict: `1.0` is rejected, matching JSON Schema's `integer`.
119
+
118
120
  ### Nested Parameters
119
121
 
120
122
  Tool params support the same nested DSL as structured output — nested objects (`Hash` with block), typed arrays (`Array, of:`), and arrays of objects (`Array` with block). See the [structured output section in Agents](AGENTS.md#nested-objects) for full syntax.
@@ -94,7 +94,10 @@ model_options additional_model_request_fields: {
94
94
 
95
95
  ### cache_control
96
96
 
97
- Enable prompt caching for models that support it (Claude, Nova). Riffer appends a single Converse `cachePoint` to the stable prefix — after the system array, or after the tools when there is no system prompt — so system instructions and tool definitions are reused across the calls in an agent loop and across conversation turns. The volatile message tail is never cached.
97
+ Enable prompt caching for models that support it (Claude, Nova). Riffer mirrors Anthropic's automatic caching with two Converse `cachePoint` blocks:
98
+
99
+ - A **static** checkpoint at the end of the stable prefix — after the system array, or after the tools when there is no system prompt — so system instructions and tool definitions are reused across every call.
100
+ - A **moving** checkpoint after the last content block of the final message (a user turn or a batch of tool results). Bedrock looks back roughly 20 content blocks from a checkpoint for the longest cached prefix, so the second and later calls in a tool loop, and later turns in a conversation, read the accumulated conversation from cache instead of re-billing it at the full input rate.
98
101
 
99
102
  ```ruby
100
103
  # 5-minute TTL (default)
@@ -104,7 +107,7 @@ model_options cache_control: {type: "ephemeral"}
104
107
  model_options cache_control: {type: "ephemeral", ttl: "1h"}
105
108
  ```
106
109
 
107
- Caching is opt-in: omit `cache_control` and no cachePoint is sent. The breakpoint is only honored once the prefix clears the model's minimum token count; on models that don't support `cachePoint`, the Converse request errors. Verify hits via `response.token_usage.cache_read_tokens`.
110
+ Both checkpoints share the same `ttl`. Caching is opt-in: omit `cache_control` and no cachePoint is sent. A checkpoint is only honored once the content before it clears the model's minimum token count (see the per-model limits in the [AWS prompt caching guide](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-caching.html)) and is silently ignored below it, so short conversations may see no cache reads at first. On models that don't support `cachePoint`, the Converse request errors. Verify hits via `response.token_usage.cache_read_tokens`, which should grow with each step of an agent loop as the conversation accumulates.
108
111
 
109
112
  ## Example
110
113
 
@@ -20,7 +20,7 @@ module Riffer::Agent::Run
20
20
  # for prompt/files semantics.
21
21
  #
22
22
  #--
23
- #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
23
+ #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
24
24
  def stream(agent:, prompt: nil, files: nil, tags: {})
25
25
  append_user_message(agent, prompt, files: files)
26
26
  # The enumerator body runs in its own fiber, where the fiber-local OTEL
data/lib/riffer/agent.rb CHANGED
@@ -155,7 +155,7 @@ class Riffer::Agent
155
155
 
156
156
  # Streams a response using a new agent instance.
157
157
  #--
158
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
158
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
159
159
  def self.stream(prompt = nil, files: nil, context: nil, tags: {})
160
160
  new(context: context).stream(prompt, files: files, tags: tags)
161
161
  end
@@ -304,13 +304,14 @@ class Riffer::Agent
304
304
  Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files, tags: tags)
305
305
  end
306
306
 
307
- # Streams a response from the agent, returning an +Enumerator+ of
308
- # +Riffer::StreamEvents+. See +#generate+ for prompt/files/tags semantics.
307
+ # Streams a response from the agent as an +Enumerator+ of
308
+ # +Riffer::StreamEvents+ whose block-form +each+ returns the final
309
+ # Riffer::Agent::Response. See +#generate+ for prompt/files/tags semantics.
309
310
  #
310
311
  # Raises Riffer::ArgumentError if structured output is configured.
311
312
  #
312
313
  #--
313
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
314
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
314
315
  def stream(prompt = nil, files: nil, tags: {})
315
316
  if @structured_output
316
317
  raise Riffer::ArgumentError,
@@ -121,6 +121,8 @@ class Riffer::Params::Param
121
121
 
122
122
  if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(type)
123
123
  [true, false].include?(value)
124
+ elsif type == Float
125
+ value.is_a?(Numeric)
124
126
  else
125
127
  value.is_a?(type)
126
128
  end
data/lib/riffer/params.rb CHANGED
@@ -79,6 +79,11 @@ class Riffer::Params
79
79
 
80
80
  # Validates arguments against parameter definitions.
81
81
  #
82
+ # A Float param accepts an Integer (JSON Schema <tt>"number"</tt> covers
83
+ # integers) and its value is coerced with +to_f+, so callers always get a
84
+ # Float. The same holds for the items of an <tt>of: Float</tt> array. No other
85
+ # type is coerced.
86
+ #
82
87
  # Raises Riffer::ValidationError if validation fails.
83
88
  #
84
89
  #--
@@ -112,7 +117,7 @@ class Riffer::Params
112
117
 
113
118
  value = validate_nested(param, value, errors)
114
119
 
115
- validated[param.name] = value
120
+ validated[param.name] = coerce_value(param.type, value)
116
121
  end
117
122
 
118
123
  raise Riffer::ValidationError, errors.join("; ") if errors.any?
@@ -178,7 +183,6 @@ class Riffer::Params
178
183
  validate_nested_array_of_objects(param, value, errors)
179
184
  elsif param.type == Array && param.item_type
180
185
  validate_typed_array(param, value, errors)
181
- value
182
186
  else
183
187
  value
184
188
  end
@@ -218,20 +222,40 @@ class Riffer::Params
218
222
  end
219
223
  end
220
224
 
225
+ # Returns the array with its valid items coerced by +coerce_value+.
221
226
  #--
222
- #: (Riffer::Params::Param, Array[untyped], Array[String]) -> void
227
+ #: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
223
228
  def validate_typed_array(param, value, errors)
224
229
  item_type = param.item_type
225
- return unless item_type
230
+ return value unless item_type
226
231
 
227
232
  type_name = Riffer::Params::Param::TYPE_MAPPINGS[item_type]
228
233
  valid_item = if [Riffer::Params::Boolean, TrueClass, FalseClass].include?(item_type)
229
234
  ->(item) { [true, false].include?(item) }
235
+ elsif item_type == Float
236
+ ->(item) { item.is_a?(Numeric) }
230
237
  else
231
238
  ->(item) { item.is_a?(item_type) }
232
239
  end
233
- value.each_with_index do |item, i|
234
- errors << "#{param.name}[#{i}] must be a #{type_name}" unless valid_item.call(item)
240
+ value.map.with_index do |item, i|
241
+ unless valid_item.call(item)
242
+ errors << "#{param.name}[#{i}] must be a #{type_name}"
243
+ next item
244
+ end
245
+
246
+ coerce_value(item_type, item)
235
247
  end
236
248
  end
249
+
250
+ # Coerces an already-validated value to the Ruby type its param declares.
251
+ # Only Float coerces today, because JSON Schema "number" accepts integers and
252
+ # callers should not get a type that depends on whether the model wrote a
253
+ # decimal point. Add a branch here rather than inline at a call site.
254
+ #--
255
+ #: (Module, untyped) -> untyped
256
+ def coerce_value(type, value)
257
+ return value.to_f if type == Float
258
+
259
+ value
260
+ end
237
261
  end
@@ -129,21 +129,25 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
129
129
  params
130
130
  end
131
131
 
132
- # Converse chains +tools -> system -> messages+, so a single +cachePoint+ at
133
- # the end of the system array (or the tools array, when there is no system
134
- # prompt) also caches the preceding sections.
132
+ # Converse treats +tools -> system -> messages+ as one prefix and looks back
133
+ # from a +cachePoint+ for the longest cached run, so the point on the final
134
+ # message reuses the previous step's cache wherever that point sat. Mixed
135
+ # ttls must be ordered 1h before 5m, so both points share one.
135
136
  #--
136
137
  #: (Hash[Symbol, untyped], untyped) -> void
137
138
  def apply_cache_point(params, cache_control)
138
139
  cache_point = { cache_point: build_cache_point(cache_control) }
139
140
  system = params[:system]
140
141
  tools = params.dig(:tool_config, :tools)
142
+ last_message = params[:messages].last
141
143
 
142
144
  if system && !system.empty?
143
145
  system << cache_point
144
146
  elsif tools && !tools.empty?
145
147
  tools << cache_point
146
148
  end
149
+
150
+ last_message[:content] << cache_point if last_message
147
151
  end
148
152
 
149
153
  #--
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.46.0" #: String
5
+ VERSION = "0.47.0" #: String
6
6
  end
@@ -14,8 +14,8 @@ module Riffer::Agent::Run
14
14
  # for prompt/files semantics.
15
15
  #
16
16
  # --
17
- # : (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
18
- def stream: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
17
+ # : (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
18
+ def stream: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
19
19
 
20
20
  private
21
21
 
@@ -113,8 +113,8 @@ class Riffer::Agent
113
113
 
114
114
  # Streams a response using a new agent instance.
115
115
  # --
116
- # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
117
- def self.stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
116
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
117
+ def self.stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
118
118
 
119
119
  # Reconstructs a runnable agent from a wire hash produced by +#to_h+.
120
120
  # --
@@ -208,14 +208,15 @@ class Riffer::Agent
208
208
  # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
209
209
  def generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Riffer::Agent::Response
210
210
 
211
- # Streams a response from the agent, returning an +Enumerator+ of
212
- # +Riffer::StreamEvents+. See +#generate+ for prompt/files/tags semantics.
211
+ # Streams a response from the agent as an +Enumerator+ of
212
+ # +Riffer::StreamEvents+ whose block-form +each+ returns the final
213
+ # Riffer::Agent::Response. See +#generate+ for prompt/files/tags semantics.
213
214
  #
214
215
  # Raises Riffer::ArgumentError if structured output is configured.
215
216
  #
216
217
  # --
217
- # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
218
- def stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, void]
218
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
219
+ def stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[String | Symbol, untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
219
220
 
220
221
  # Interrupts the agent loop from an +on_message+ callback. Equivalent to
221
222
  # <tt>throw :riffer_interrupt, reason</tt>.
@@ -40,6 +40,11 @@ class Riffer::Params
40
40
 
41
41
  # Validates arguments against parameter definitions.
42
42
  #
43
+ # A Float param accepts an Integer (JSON Schema <tt>"number"</tt> covers
44
+ # integers) and its value is coerced with +to_f+, so callers always get a
45
+ # Float. The same holds for the items of an <tt>of: Float</tt> array. No other
46
+ # type is coerced.
47
+ #
43
48
  # Raises Riffer::ValidationError if validation fails.
44
49
  #
45
50
  # --
@@ -71,7 +76,16 @@ class Riffer::Params
71
76
  # : (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
72
77
  def validate_nested_array_of_objects: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
73
78
 
79
+ # Returns the array with its valid items coerced by +coerce_value+.
80
+ # --
81
+ # : (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
82
+ def validate_typed_array: (Riffer::Params::Param, Array[untyped], Array[String]) -> Array[untyped]
83
+
84
+ # Coerces an already-validated value to the Ruby type its param declares.
85
+ # Only Float coerces today, because JSON Schema "number" accepts integers and
86
+ # callers should not get a type that depends on whether the model wrote a
87
+ # decimal point. Add a branch here rather than inline at a call site.
74
88
  # --
75
- # : (Riffer::Params::Param, Array[untyped], Array[String]) -> void
76
- def validate_typed_array: (Riffer::Params::Param, Array[untyped], Array[String]) -> void
89
+ # : (Module, untyped) -> untyped
90
+ def coerce_value: (Module, untyped) -> untyped
77
91
  end
@@ -45,9 +45,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
45
45
  # : (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
46
46
  def build_request_params: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
47
47
 
48
- # Converse chains +tools -> system -> messages+, so a single +cachePoint+ at
49
- # the end of the system array (or the tools array, when there is no system
50
- # prompt) also caches the preceding sections.
48
+ # Converse treats +tools -> system -> messages+ as one prefix and looks back
49
+ # from a +cachePoint+ for the longest cached run, so the point on the final
50
+ # message reuses the previous step's cache wherever that point sat. Mixed
51
+ # ttls must be ordered 1h before 5m, so both points share one.
51
52
  # --
52
53
  # : (Hash[Symbol, untyped], untyped) -> void
53
54
  def apply_cache_point: (Hash[Symbol, untyped], untyped) -> void
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riffer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.46.0
4
+ version: 0.47.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall