riffer 0.46.0 → 0.46.1

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: 569f1c5929847037c97cc5552eb38a8609db6a1e67ce99b8135961d50aa7a0b5
4
+ data.tar.gz: c8e03583eb04ff2c8922b8d554c10f5a47fb4421b15600905ba21b0d69031aed
5
5
  SHA512:
6
- metadata.gz: dfe9da5a5cb0a8a1147846766acc79b518a9f7025d1d95fbfddce3604e2315afafc04762b339879eee2db875aba5cc296056045f213f47a17cb9f730e17f3e07
7
- data.tar.gz: b548a4b44b001b79ee295969af5a868f50d0e847a183dc9433e5b23d4b1e7c9effed3ea64a76a8f5022db959a65394d8f8ff28bba9a6d46484841322483ebe02
6
+ metadata.gz: 6a0380696a959b4c291a5bf52c03eb7a1e06971a60ded5d058311bbdfe9efd99e58bf94506980e2d4b7b0e21257773894d11be4d1a896ea1e7e3ba8e2ce3359e
7
+ data.tar.gz: 965f4d1fe6b97f4bd887a1fddd310c7a4cb81b5a0789cf8b91c58c49796abe79f396ec410b6008895275bd16281593a29f53d870e591124ade85b38c62a8fb22
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.46.0"
2
+ ".": "0.46.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,14 @@ 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.46.1](https://github.com/janeapp/riffer/compare/riffer/v0.46.0...riffer/v0.46.1) (2026-09-11)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * accept JSON integers for Float params ([#429](https://github.com/janeapp/riffer/issues/429)) ([928c33b](https://github.com/janeapp/riffer/commit/928c33b1125bf013f66c8b736191bc70eb313ccd))
14
+ * **agent:** type stream's enumerator as returning Response ([#428](https://github.com/janeapp/riffer/issues/428)) ([4d2b2d2](https://github.com/janeapp/riffer/commit/4d2b2d2aa08d1d95b97226e84180284349fff441))
15
+
8
16
  ## [0.46.0](https://github.com/janeapp/riffer/compare/riffer/v0.45.0...riffer/v0.46.0) (2026-09-08)
9
17
 
10
18
 
@@ -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.
@@ -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
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.46.0" #: String
5
+ VERSION = "0.46.1" #: 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
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.46.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jake Bottrall