riffer 0.42.0 → 0.44.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.release-please-manifest.json +1 -1
  3. data/CHANGELOG.md +19 -0
  4. data/Steepfile +1 -0
  5. data/docs/AGENTS.md +23 -2
  6. data/docs/AGENT_LOOP.md +6 -4
  7. data/docs/CONFIGURATION.md +32 -0
  8. data/docs/MESSAGES.md +10 -0
  9. data/docs/OVERVIEW.md +1 -0
  10. data/docs/TESTING.md +129 -0
  11. data/docs/TOOLS.md +27 -3
  12. data/docs/TOOL_ADVANCED.md +20 -7
  13. data/docs/TRACING.md +5 -4
  14. data/docs/providers/AMAZON_BEDROCK.md +17 -0
  15. data/docs/providers/GEMINI.md +2 -3
  16. data/docs-site/manifest.yml +3 -0
  17. data/lib/riffer/config.rb +94 -0
  18. data/lib/riffer/files/downloader.rb +78 -0
  19. data/lib/riffer/files/resolver.rb +97 -0
  20. data/lib/riffer/files.rb +5 -0
  21. data/lib/riffer/helpers/identifier.rb +11 -3
  22. data/lib/riffer/messages/file_part.rb +61 -9
  23. data/lib/riffer/messages/tool.rb +2 -1
  24. data/lib/riffer/providers/amazon_bedrock.rb +7 -7
  25. data/lib/riffer/providers/base.rb +8 -0
  26. data/lib/riffer/providers/gemini.rb +6 -5
  27. data/lib/riffer/providers/open_ai.rb +6 -0
  28. data/lib/riffer/providers/open_router.rb +6 -0
  29. data/lib/riffer/registrable.rb +105 -22
  30. data/lib/riffer/testing/minitest.rb +21 -0
  31. data/lib/riffer/testing/rspec.rb +11 -0
  32. data/lib/riffer/testing.rb +130 -0
  33. data/lib/riffer/tool.rb +24 -9
  34. data/lib/riffer/tools/response.rb +10 -5
  35. data/lib/riffer/tools/runtime.rb +20 -11
  36. data/lib/riffer/version.rb +1 -1
  37. data/lib/riffer.rb +34 -1
  38. data/sig/_private/minitest.rbs +9 -0
  39. data/sig/_private/riffer/testing/minitest.rbs +6 -0
  40. data/sig/_private/rspec.rbs +7 -0
  41. data/sig/_private/zeitwerk.rbs +2 -0
  42. data/sig/generated/riffer/config.rbs +64 -0
  43. data/sig/generated/riffer/files/downloader.rbs +16 -0
  44. data/sig/generated/riffer/files/resolver.rbs +36 -0
  45. data/sig/generated/riffer/files.rbs +4 -0
  46. data/sig/generated/riffer/helpers/identifier.rbs +8 -0
  47. data/sig/generated/riffer/messages/file_part.rbs +38 -6
  48. data/sig/generated/riffer/messages/tool.rbs +2 -1
  49. data/sig/generated/riffer/providers/amazon_bedrock.rbs +4 -0
  50. data/sig/generated/riffer/providers/base.rbs +4 -0
  51. data/sig/generated/riffer/providers/gemini.rbs +4 -0
  52. data/sig/generated/riffer/providers/open_ai.rbs +4 -0
  53. data/sig/generated/riffer/providers/open_router.rbs +4 -0
  54. data/sig/generated/riffer/registrable.rbs +57 -9
  55. data/sig/generated/riffer/testing/rspec.rbs +2 -0
  56. data/sig/generated/riffer/testing.rbs +75 -0
  57. data/sig/generated/riffer/tool.rbs +4 -5
  58. data/sig/generated/riffer/tools/response.rbs +8 -4
  59. data/sig/generated/riffer/tools/runtime.rbs +5 -4
  60. data/sig/generated/riffer.rbs +35 -1
  61. data/sig/manifest.yaml +1 -0
  62. data/sig/manual/riffer/testing.rbs +5 -0
  63. metadata +31 -1
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Builds throwaway agents and tools a test suite can resolve by identifier or
5
+ # constant name, and removes them again. Require <tt>riffer/testing/rspec</tt>
6
+ # or <tt>riffer/testing/minitest</tt> to get +stub_agent+/+stub_tool+ in every
7
+ # example plus per-test cleanup; otherwise include this module and call +reset!+
8
+ # from your own teardown, or call the methods on the module directly.
9
+ #
10
+ # Tracking is not synchronized — stub from a single-threaded test, before
11
+ # concurrent lookups begin.
12
+ module Riffer::Testing
13
+ extend self
14
+
15
+ # @rbs self.@registrations: Array[[Class, String?]]?
16
+
17
+ CONST_NAME_PATTERN = /\A[A-Z][A-Za-z0-9_]*\z/ #: Regexp
18
+ private_constant :CONST_NAME_PATTERN
19
+
20
+ # Builds an agent class, evaluates the optional body in it, and makes it
21
+ # resolvable until the next +reset!+. A +name+ assigns a top-level constant
22
+ # and derives the identifier; the body may set +identifier+ like any other
23
+ # agent config.
24
+ #
25
+ # agent = stub_agent("SupportAgent") { model "mock/gpt-5-mini" }
26
+ #
27
+ # Raises Riffer::ArgumentError when the constant is already defined or the
28
+ # stub ends up with no identifier, and Riffer::DuplicateIdentifierError when
29
+ # another agent already holds the identifier.
30
+ #
31
+ #--
32
+ #: (?(String | Symbol)?, ?base: singleton(Riffer::Agent)) ?{ () [self: singleton(Riffer::Agent)] -> void } -> singleton(Riffer::Agent)
33
+ def stub_agent(name = nil, base: Riffer::Agent, &body)
34
+ build_stub(name, base: base, &body) #: singleton(Riffer::Agent)
35
+ end
36
+
37
+ # Builds a tool class, evaluates the optional body in it, and makes it
38
+ # resolvable until the next +reset!+. A +name+ assigns a top-level constant
39
+ # and derives the identifier; the body may set +identifier+ like any other
40
+ # tool config.
41
+ #
42
+ # tool = stub_tool("KbSearch") { def call(context:, **) = text("stubbed") }
43
+ #
44
+ # Raises Riffer::ArgumentError when the constant is already defined or the
45
+ # stub ends up with no identifier, and Riffer::DuplicateIdentifierError when
46
+ # another tool already holds the identifier.
47
+ #
48
+ #--
49
+ #: (?(String | Symbol)?, ?base: singleton(Riffer::Tool)) ?{ () [self: singleton(Riffer::Tool)] -> void } -> singleton(Riffer::Tool)
50
+ def stub_tool(name = nil, base: Riffer::Tool, &body)
51
+ build_stub(name, base: base, &body) #: singleton(Riffer::Tool)
52
+ end
53
+
54
+ # Removes every stub built since the last reset — its registration and any
55
+ # constant it created — newest first, and forgets them. A no-op when nothing
56
+ # has been stubbed.
57
+ #--
58
+ #: () -> void
59
+ def reset!
60
+ tracked = Riffer::Testing.registrations
61
+ tracked.reverse_each do |stub, const_name|
62
+ registrable = stub.superclass #: untyped
63
+ registrable.unregister(stub)
64
+ remove_stub_const(const_name, stub) if const_name
65
+ end
66
+ tracked.clear
67
+ end
68
+
69
+ # The stub classes awaiting cleanup. Lives on the module rather than the
70
+ # caller so an including test case and a direct
71
+ # <tt>Riffer::Testing.stub_*</tt> call share one list.
72
+ #--
73
+ #: () -> Array[[Class, String?]]
74
+ def self.registrations # :nodoc:
75
+ @registrations ||= []
76
+ end
77
+
78
+ private
79
+
80
+ #--
81
+ #: ((String | Symbol)?, base: Class) ?{ () [self: untyped] -> void } -> Class
82
+ def build_stub(name, base:, &body)
83
+ const_name = validate_const_name(name)
84
+ stub = Class.new(base)
85
+ configurable = stub #: untyped
86
+ configurable.identifier(Riffer::Helpers::Identifier.derive(const_name)) if const_name
87
+ configurable.class_eval(&body) if body
88
+ if configurable.identifier.to_s.strip.empty?
89
+ raise Riffer::ArgumentError, "a stub needs a name, or an identifier set in its block"
90
+ end
91
+
92
+ # The class must be registered while still anonymous: naming it first makes
93
+ # it implicitly live, and +register+ rejects an identifier the registry
94
+ # already resolves — even to this same class.
95
+ registrable = base #: untyped
96
+ registrable.register(stub)
97
+ Object.const_set(const_name, stub) if const_name
98
+ Riffer::Testing.registrations << [stub, const_name]
99
+
100
+ stub
101
+ end
102
+
103
+ #--
104
+ #: ((String | Symbol)?) -> String?
105
+ def validate_const_name(name)
106
+ return nil if name.nil?
107
+
108
+ const_name = name.to_s
109
+ unless CONST_NAME_PATTERN.match?(const_name)
110
+ raise Riffer::ArgumentError, "#{const_name.inspect} is not a simple top-level constant name"
111
+ end
112
+
113
+ # True of a pending autoload too — the name is taken either way.
114
+ if Object.const_defined?(const_name, false)
115
+ raise Riffer::ArgumentError, "#{const_name} is already defined; use stub_const to replace a real class"
116
+ end
117
+
118
+ const_name
119
+ end
120
+
121
+ # A test may have removed or replaced the constant itself, so never clobber
122
+ # one that no longer points at the stub.
123
+ #--
124
+ #: (String, Class) -> void
125
+ def remove_stub_const(const_name, stub)
126
+ return unless Object.const_defined?(const_name, false) && Object.const_get(const_name, false).equal?(stub)
127
+
128
+ Object.send(:remove_const, const_name)
129
+ end
130
+ end
data/lib/riffer/tool.rb CHANGED
@@ -56,19 +56,23 @@ class Riffer::Tool
56
56
  Riffer::Tools::Response.error(message, type: type)
57
57
  end
58
58
 
59
- # Executes the tool with validation and timeout (used by Agent).
60
- #
61
- # Raises Riffer::ValidationError if validation fails.
62
- # Raises Riffer::TimeoutError if execution exceeds the configured timeout.
63
- # Raises Riffer::Error if the tool does not return a Response object.
59
+ # Executes the tool with validation and timeout, folding every +StandardError+
60
+ # into an error Response. Anything outside +StandardError+ — an unimplemented
61
+ # +#call+ above all — still propagates, because a broken tool is a broken
62
+ # deploy rather than a bad request.
64
63
  #
65
64
  #--
66
65
  #: (context: Riffer::Agent::Context?, **untyped) -> Riffer::Tools::Response
67
66
  def call_with_validation(context:, **kwargs)
68
67
  params_builder = self.class.params
69
- validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
70
68
 
71
- result = Timeout.timeout(self.class.timeout) do
69
+ begin
70
+ validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
71
+ rescue Riffer::ValidationError => e
72
+ return Riffer::Tools::Response.error(e.message, type: :validation_error)
73
+ end
74
+
75
+ result = Timeout.timeout(self.class.timeout, Riffer::TimeoutError) do
72
76
  call(context: context, **validated_args) #: untyped
73
77
  end
74
78
 
@@ -77,7 +81,18 @@ class Riffer::Tool
77
81
  end
78
82
 
79
83
  result
80
- rescue Timeout::Error
81
- raise Riffer::TimeoutError, "Tool execution timed out after #{self.class.timeout} seconds"
84
+ rescue Riffer::TimeoutError
85
+ Riffer::Tools::Response.error(
86
+ "Tool execution timed out after #{self.class.timeout} seconds",
87
+ type: :timeout_error,
88
+ )
89
+ rescue Riffer::ToolExecutionError => e
90
+ Riffer::Tools::Response.error(e.message, type: :execution_error)
91
+ rescue StandardError => e
92
+ Riffer::Tools::Response.error(
93
+ "Error executing tool: #{e.class}: #{e.message}",
94
+ type: :unhandled_error,
95
+ exception: e,
96
+ )
82
97
  end
83
98
  end
@@ -28,6 +28,10 @@ class Riffer::Tools::Response
28
28
  # The error type, or +nil+ on success.
29
29
  attr_reader :error_type #: Symbol?
30
30
 
31
+ # The exception an unhandled failure was folded from, or +nil+. Kept out of
32
+ # every serialized form so it never reaches an LLM or a message payload.
33
+ attr_reader :exception #: Exception?
34
+
31
35
  # Creates a success response.
32
36
  #
33
37
  # Raises Riffer::ArgumentError if format is invalid.
@@ -62,9 +66,9 @@ class Riffer::Tools::Response
62
66
  # Creates an error response.
63
67
  #
64
68
  #--
65
- #: (String, ?type: Symbol) -> Riffer::Tools::Response
66
- def self.error(message, type: :execution_error)
67
- new(content: message, success: false, error_message: message, error_type: type)
69
+ #: (String, ?type: Symbol, ?exception: Exception?) -> Riffer::Tools::Response
70
+ def self.error(message, type: :execution_error, exception: nil)
71
+ new(content: message, success: false, error_message: message, error_type: type, exception: exception)
68
72
  end
69
73
 
70
74
  # Returns true if the tool execution succeeded.
@@ -88,11 +92,12 @@ class Riffer::Tools::Response
88
92
  private
89
93
 
90
94
  #--
91
- #: (content: String, success: bool, ?error_message: String?, ?error_type: Symbol?) -> void
92
- def initialize(content:, success:, error_message: nil, error_type: nil)
95
+ #: (content: String, success: bool, ?error_message: String?, ?error_type: Symbol?, ?exception: Exception?) -> void
96
+ def initialize(content:, success:, error_message: nil, error_type: nil, exception: nil)
93
97
  @content = content
94
98
  @success = success
95
99
  @error_message = error_message
96
100
  @error_type = error_type
101
+ @exception = exception
97
102
  end
98
103
  end
@@ -88,19 +88,20 @@ class Riffer::Tools::Runtime
88
88
  tool_instance = tool_class.new
89
89
  arguments = parse_arguments(tool_call.arguments)
90
90
 
91
+ unless arguments.is_a?(Hash)
92
+ return Riffer::Tools::Response.error(
93
+ "Invalid JSON in tool arguments: expected an object, got #{arguments.class}",
94
+ type: :validation_error,
95
+ )
96
+ end
97
+
91
98
  tool_instance.call_with_validation(context: context, **arguments)
92
- rescue Riffer::TimeoutError => e
93
- Riffer::Tools::Response.error(e.message, type: :timeout_error)
94
- rescue Riffer::ValidationError => e
95
- Riffer::Tools::Response.error(e.message, type: :validation_error)
96
- rescue Riffer::ToolExecutionError => e
97
- Riffer::Tools::Response.error(e.message, type: :execution_error)
98
- rescue RuntimeError => e
99
- Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
99
+ rescue JSON::ParserError => e
100
+ Riffer::Tools::Response.error("Invalid JSON in tool arguments: #{e.message}", type: :validation_error)
100
101
  end
101
102
 
102
103
  #--
103
- #: (String?) -> Hash[Symbol, untyped]
104
+ #: (String?) -> untyped
104
105
  def parse_arguments(arguments)
105
106
  return {} if arguments.nil? || arguments.empty?
106
107
 
@@ -144,13 +145,21 @@ class Riffer::Tools::Runtime
144
145
  tags.transform_keys { |key| "riffer.tag.#{key}" }
145
146
  end
146
147
 
147
- # A returned error Response is a handled outcome, so its status stays unset —
148
- # an error span status is reserved for a raised exception.
148
+ # A deliberate error Response is a handled outcome, so its status stays unset.
149
+ # An error status is reserved for a Response carrying the exception it was
150
+ # folded from — the tool failed for a reason nobody anticipated.
149
151
  #--
150
152
  #: ((Riffer::Tracing::Otel::Span | Riffer::Tracing::NoOp::Span), Riffer::Tools::Response) -> void
151
153
  def record_tool_outcome(span, result)
152
154
  error_type = result.error_type
153
155
  span.set_attribute("error.type", error_type.to_s) if error_type
156
+
157
+ exception = result.exception
158
+ if exception
159
+ span.record_exception(exception)
160
+ span.error!(exception.message)
161
+ end
162
+
154
163
  capture_tool_result(span, result)
155
164
  end
156
165
 
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module Riffer
5
- VERSION = "0.42.0" #: String
5
+ VERSION = "0.44.0" #: String
6
6
  end
data/lib/riffer.rb CHANGED
@@ -10,6 +10,13 @@ loader.inflector.inflect(
10
10
  "azure_open_ai" => "AzureOpenAI",
11
11
  "open_router" => "OpenRouter",
12
12
  )
13
+ # Test-framework wiring a consumer requires by hand; neither file defines the
14
+ # constant its path implies, and both reference framework constants riffer
15
+ # never loads itself.
16
+ loader.ignore(
17
+ "#{__dir__}/riffer/testing/rspec.rb",
18
+ "#{__dir__}/riffer/testing/minitest.rb",
19
+ )
13
20
  loader.setup
14
21
 
15
22
  module Riffer
@@ -26,12 +33,38 @@ module Riffer
26
33
  # Raised when tool parameter validation fails.
27
34
  class ValidationError < Error; end
28
35
 
29
- # Raised when tool execution times out.
36
+ # Raised inside a tool's +call+ when execution exceeds the configured
37
+ # timeout. Rescue it in the tool to clean up; otherwise it becomes a
38
+ # +:timeout_error+ response.
30
39
  class TimeoutError < Error; end
31
40
 
32
41
  # Raised when a tool encounters an expected execution error.
33
42
  class ToolExecutionError < Error; end
34
43
 
44
+ # Base error for a file attachment that can't be resolved for the provider.
45
+ class FileError < Error; end
46
+
47
+ # Raised when the provider can't accept the file at all.
48
+ class FileUnsupportedError < FileError; end
49
+
50
+ # Raised when a download is required but Riffer.config.files.allow_downloads is false.
51
+ class FileDownloadsDisabledError < FileError; end
52
+
53
+ # Raised when a message carries more files than Riffer.config.files.max_per_message allows.
54
+ class TooManyFilesError < FileError; end
55
+
56
+ # Raised when there's an issue downloading a file
57
+ class FileDownloadError < FileError; end
58
+
59
+ # Raised when a file size exceeds Riffer.config.files.max_bytes
60
+ class FileTooLargeError < FileError; end
61
+
62
+ # Raised when a downloaded or inline file's sha256 doesn't match.
63
+ class FileChecksumMismatchError < FileError; end
64
+
65
+ # Raised when a FilePart's data isn't valid base64
66
+ class FileEncodingError < FileError; end
67
+
35
68
  # Raised when two registered subclasses share the same identifier.
36
69
  class DuplicateIdentifierError < Error; end
37
70
 
@@ -0,0 +1,9 @@
1
+ # Minimal signatures for `minitest`, a development-only framework riffer never
2
+ # depends on. Only the surface `lib/riffer/testing/minitest.rb` touches is
3
+ # declared; the file is required by hand from a consumer's test_helper, so this
4
+ # stub keeps Steep aware of the constants without a `library` dependency.
5
+ module Minitest
6
+ class Test
7
+ def after_teardown: () -> void
8
+ end
9
+ end
@@ -0,0 +1,6 @@
1
+ # Hand-written because the signature names `Minitest`, an optional
2
+ # development-only framework that must never appear in a shipped signature —
3
+ # `lib/riffer/testing/minitest.rb` therefore leaves rbs-inline disabled.
4
+ module Riffer::Testing::MinitestCleanup : Minitest::Test
5
+ def after_teardown: () -> void
6
+ end
@@ -0,0 +1,7 @@
1
+ # Minimal signatures for `rspec`, a development-only framework riffer never
2
+ # depends on. Only the surface `lib/riffer/testing/rspec.rb` touches is
3
+ # declared; the file is required by hand from a consumer's spec_helper, so this
4
+ # stub keeps Steep aware of the constants without a `library` dependency.
5
+ module RSpec
6
+ def self.configure: () { (untyped) -> void } -> void
7
+ end
@@ -5,6 +5,8 @@ module Zeitwerk
5
5
  class Loader
6
6
  def self.for_gem: (*untyped) -> Zeitwerk::Loader
7
7
 
8
+ def ignore: (*String) -> void
9
+
8
10
  def inflector: () -> untyped
9
11
 
10
12
  def setup: () -> void
@@ -147,6 +147,68 @@ class Riffer::Config
147
147
  def backend=: (untyped) -> void
148
148
  end
149
149
 
150
+ # File-attachment-download policy for +Riffer::Messages::FilePart+ URL sources
151
+ class Files
152
+ # Allow file attachments to be downloaded to send to providers.
153
+ attr_reader allow_downloads: bool
154
+
155
+ # Maximum file size to download before failing.
156
+ attr_reader max_bytes: Integer
157
+
158
+ # Maximum amount of time to spend downloading a file before failing.
159
+ attr_reader timeout: Integer
160
+
161
+ # Maximum number of files to include in an individual message.
162
+ attr_reader max_per_message: Integer?
163
+
164
+ # Execution pattern for downloading files.
165
+ attr_reader runner: Riffer::Runner
166
+
167
+ # The object used to fetch a URL source's bytes
168
+ attr_reader downloader: untyped
169
+
170
+ # --
171
+ # : () -> void
172
+ def initialize: () -> void
173
+
174
+ # Sets the allow_downloads flag, coercing boolean-ish values so an env-var
175
+ # +"false"+ (truthy in Ruby) doesn't silently enable downloads. Raises
176
+ # Riffer::ArgumentError on an unrecognized value.
177
+ # --
178
+ # : (untyped) -> void
179
+ def allow_downloads=: (untyped) -> void
180
+
181
+ # Sets max_bytes, provided value is a positive integer.
182
+ # Raises Riffer::ArgumentError if value is not an Integer or less than or equal to 0.
183
+ # --
184
+ # : (untyped) -> void
185
+ def max_bytes=: (untyped) -> void
186
+
187
+ # Sets timeout, provided value is a positive integer.
188
+ # Raises Riffer::ArgumentError if value is not an Integer or is less than or equal to 0.
189
+ # --
190
+ # : (untyped) -> void
191
+ def timeout=: (untyped) -> void
192
+
193
+ # Sets max_per_message, provided value is either nil or a positive integer.
194
+ # Raises Riffer::ArgumentError if value is not an Integer or nil, or is less than or equal to 0.
195
+ # --
196
+ # : (untyped) -> void
197
+ def max_per_message=: (untyped) -> void
198
+
199
+ # Sets the runner used to process file downloads, provided value is a Riffer::Runner.
200
+ # Raises Riffer::ArgumentError if value is not a Riffer::Runner.
201
+ # --
202
+ # : (untyped) -> void
203
+ def runner=: (untyped) -> void
204
+
205
+ # Sets the object used to download bytes from a URL.
206
+ # Raises Riffer::ArgumentError if value does not respond to +#call+.
207
+ # --
208
+ # : (untyped) -> void
209
+ def downloader=: (untyped) -> void
210
+ end
211
+
150
212
  # Consumer-configured token pricing, keyed by +provider/model+ id. Riffer
151
213
  # ships no price table, so an unconfigured model carries no cost.
152
214
  class Pricing
@@ -256,6 +318,8 @@ class Riffer::Config
256
318
  # Tracing-related global configuration.
257
319
  attr_reader tracing: Riffer::Config::Tracing
258
320
 
321
+ attr_reader files: Riffer::Config::Files
322
+
259
323
  # Consumer-configured per-model token pricing.
260
324
  attr_reader pricing: Riffer::Config::Pricing
261
325
 
@@ -0,0 +1,16 @@
1
+ # Generated from lib/riffer/files/downloader.rb with RBS::Inline
2
+
3
+ class Riffer::Files::Downloader
4
+ MAX_REDIRECTS: Integer
5
+
6
+ # : (String, max_bytes: Integer, timeout: Integer) -> String
7
+ def call: (String, max_bytes: Integer, timeout: Integer) -> String
8
+
9
+ private
10
+
11
+ # : (String, max_bytes: Integer, timeout: Integer, redirects_remaining: Integer) -> String
12
+ def fetch: (String, max_bytes: Integer, timeout: Integer, redirects_remaining: Integer) -> String
13
+
14
+ # : (Net::HTTPResponse, max_bytes: Integer) -> String
15
+ def read_capped: (Net::HTTPResponse, max_bytes: Integer) -> String
16
+ end
@@ -0,0 +1,36 @@
1
+ # Generated from lib/riffer/files/resolver.rb with RBS::Inline
2
+
3
+ class Riffer::Files::Resolver
4
+ @provider: Riffer::Providers::Base
5
+
6
+ @config: Riffer::Config::Files
7
+
8
+ # : (provider: Riffer::Providers::Base) -> void
9
+ def initialize: (provider: Riffer::Providers::Base) -> void
10
+
11
+ # Resolves every file in every User message in place - downloading,
12
+ # verifying, and caching as the provider's capability and each file's
13
+ # sha256 require. Raises Riffer::FileError on any file that can't be
14
+ # resolved
15
+ # : (Array[Riffer::Messages::Base]) -> void
16
+ def resolve!: (Array[Riffer::Messages::Base]) -> void
17
+
18
+ private
19
+
20
+ # : (Riffer::Messages::User) -> void
21
+ def check_file_count!: (Riffer::Messages::User) -> void
22
+
23
+ # : (Riffer::Messages::FilePart) -> void
24
+ def resolve_file!: (Riffer::Messages::FilePart) -> void
25
+
26
+ # : (Riffer::Messages::FilePart) -> void
27
+ def verify_inline!: (Riffer::Messages::FilePart) -> void
28
+
29
+ # +cache:+ is false for a :url-delivery provider verifying a sha256 — the
30
+ # request still sends the URL, never the downloaded bytes, so caching them
31
+ # would hold memory nothing reads and let later turns skip re-verifying.
32
+ # : (Riffer::Messages::FilePart, cache: (false | Symbol)) -> void
33
+ def download!: (Riffer::Messages::FilePart, cache: false | Symbol) -> void
34
+
35
+ def verify_bytes!: (untyped bytes, untyped sha256) -> untyped
36
+ end
@@ -0,0 +1,4 @@
1
+ # Generated from lib/riffer/files.rb with RBS::Inline
2
+
3
+ module Riffer::Files
4
+ end
@@ -16,4 +16,12 @@ module Riffer::Helpers::Identifier
16
16
  # --
17
17
  # : (Module) -> String
18
18
  def for: (Module) -> String
19
+
20
+ # Returns the class-path name of a class or module, or +nil+ when anonymous.
21
+ # Tool classes shadow Module#name with the identifier DSL, so the real name
22
+ # must come from Module's own implementation.
23
+ #
24
+ # --
25
+ # : (Module) -> String?
26
+ def real_name: (Module) -> String?
19
27
  end
@@ -3,29 +3,40 @@
3
3
  # Represents a file attachment (image or document) — from a URL (+from_url+) or
4
4
  # raw base64 data (+new+).
5
5
  class Riffer::Messages::FilePart
6
+ @data_bytes: String?
7
+
8
+ @downloaded_data: String?
9
+
10
+ @data: String?
11
+
6
12
  @url_string: String?
7
13
 
8
14
  MEDIA_TYPES: Hash[String, String]
9
15
 
10
16
  SUPPORTED_MEDIA_TYPES: Array[String]
11
17
 
18
+ SHA256_PATTERN: Regexp
19
+
12
20
  # The MIME type of the file.
13
21
  attr_reader media_type: String
14
22
 
15
23
  # The filename, if available.
16
24
  attr_reader filename: String?
17
25
 
26
+ # The expected SHA-256 of the file contents, if the caller supplied one.
27
+ attr_reader sha256: String?
28
+
18
29
  # Raises Riffer::ArgumentError unless +data+ or +url+ is given and
19
30
  # +media_type+ is supported.
20
31
  # --
21
- # : (media_type: String, ?data: String?, ?filename: String?, ?url: String?) -> void
22
- def initialize: (media_type: String, ?data: String?, ?filename: String?, ?url: String?) -> void
32
+ # : (media_type: String, ?data: String?, ?filename: String?, ?url: String?, ?sha256: String?) -> void
33
+ def initialize: (media_type: String, ?data: String?, ?filename: String?, ?url: String?, ?sha256: String?) -> void
23
34
 
24
35
  # Creates a FilePart from a URL, detecting +media_type+ from the path
25
36
  # extension when omitted. Raises Riffer::ArgumentError if it can't be detected.
26
37
  # --
27
- # : (String, ?media_type: String?) -> Riffer::Messages::FilePart
28
- def self.from_url: (String, ?media_type: String?) -> Riffer::Messages::FilePart
38
+ # : (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
39
+ def self.from_url: (String, ?media_type: String?, ?filename: String?, ?sha256: String?) -> Riffer::Messages::FilePart
29
40
 
30
41
  # Builds a FilePart from a +{url:, media_type:}+ or +{data:, media_type:}+ hash,
31
42
  # or returns +file+ unchanged when it is already a FilePart. Raises
@@ -34,8 +45,29 @@ class Riffer::Messages::FilePart
34
45
  # : ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
35
46
  def self.from_hash: (Hash[Symbol, untyped] | Riffer::Messages::FilePart) -> Riffer::Messages::FilePart
36
47
 
37
- # Returns the base64-encoded data, or nil for URL-only sources.
38
- attr_reader data: String?
48
+ # The base64-encoded contents - caller-supplied, or filled in by the file
49
+ # resolver after a download. Nil for a URL source riffer hasn't fetched.
50
+ # --
51
+ # : () -> String?
52
+ def data: () -> String?
53
+
54
+ # : () -> String?
55
+ def data_bytes: () -> String?
56
+
57
+ # : (String) -> void
58
+ def cache_data_bytes: (String) -> void
59
+
60
+ # Whether data was supplied directly, as opposed to filled in later by the
61
+ # file resolver after a download
62
+ # : () -> bool
63
+ def inline_data?: () -> bool
64
+
65
+ # Caches bytes fetched for a URL source. Deliberately absent from +to_h+:
66
+ # the agent loop re-sends history on every turn, so the cache saves refreshing
67
+ # the same file, while persisted history stays free of megabytes of base64
68
+ # --
69
+ # : (String) -> void
70
+ def cache_downloaded_data: (String) -> void
39
71
 
40
72
  # Returns the URL if the source was a URL, nil otherwise.
41
73
  #
@@ -11,7 +11,8 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
11
11
  # The error message if the tool execution failed.
12
12
  attr_reader error: String?
13
13
 
14
- # The type of error (:unknown_tool, :validation_error, :execution_error, :timeout_error).
14
+ # The type of error (:unknown_tool, :validation_error, :execution_error,
15
+ # :timeout_error, :unhandled_error).
15
16
  attr_reader error_type: Symbol?
16
17
 
17
18
  # --
@@ -24,6 +24,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
24
24
  # : () -> void
25
25
  def initialize: () -> void
26
26
 
27
+ # --
28
+ # : (Riffer::Messages::FilePart) -> Symbol
29
+ def file_delivery: (Riffer::Messages::FilePart) -> Symbol
30
+
27
31
  private
28
32
 
29
33
  # --
@@ -41,6 +41,10 @@ class Riffer::Providers::Base
41
41
  # : (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
42
42
  def stream_text: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
43
43
 
44
+ # --
45
+ # : (Riffer::Messages::FilePart) -> Symbol
46
+ def file_delivery: (Riffer::Messages::FilePart) -> Symbol
47
+
44
48
  private
45
49
 
46
50
  # : (String) -> true
@@ -11,6 +11,10 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
11
11
  # : () -> String
12
12
  def self.semconv_provider_name: () -> String
13
13
 
14
+ # --
15
+ # : (Riffer::Messages::FilePart) -> Symbol
16
+ def file_delivery: (Riffer::Messages::FilePart) -> Symbol
17
+
14
18
  private
15
19
 
16
20
  # --
@@ -13,6 +13,10 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
13
13
  # : () -> void
14
14
  def initialize: () -> void
15
15
 
16
+ # --
17
+ # : (Riffer::Messages::FilePart) -> Symbol
18
+ def file_delivery: (Riffer::Messages::FilePart) -> Symbol
19
+
16
20
  private
17
21
 
18
22
  # --