riffer 0.28.0 → 0.29.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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +18 -11
  3. data/.agents/code-style.md +1 -1
  4. data/.agents/rbs-inline.md +2 -2
  5. data/.agents/testing.md +9 -5
  6. data/.release-please-manifest.json +1 -1
  7. data/AGENTS.md +17 -10
  8. data/CHANGELOG.md +19 -0
  9. data/README.md +17 -18
  10. data/Steepfile +7 -1
  11. data/docs/03_AGENTS.md +34 -3
  12. data/docs/04_AGENT_LIFECYCLE.md +87 -86
  13. data/docs/05_AGENT_LOOP.md +2 -2
  14. data/docs/06_TOOLS.md +9 -4
  15. data/docs/07_TOOL_ADVANCED.md +17 -17
  16. data/docs/08_MESSAGES.md +25 -32
  17. data/docs/09_STREAM_EVENTS.md +1 -1
  18. data/docs/10_CONFIGURATION.md +7 -18
  19. data/docs/providers/01_PROVIDERS.md +6 -0
  20. data/docs/providers/06_MOCK_PROVIDER.md +2 -1
  21. data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
  22. data/docs/providers/08_GEMINI.md +2 -2
  23. data/docs/providers/09_OPENROUTER.md +242 -0
  24. data/lib/riffer/agent/config.rb +173 -0
  25. data/lib/riffer/agent/context.rb +125 -0
  26. data/lib/riffer/agent/run.rb +308 -0
  27. data/lib/riffer/agent/session/repair.rb +112 -0
  28. data/lib/riffer/agent/session.rb +268 -0
  29. data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
  30. data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
  31. data/lib/riffer/agent.rb +234 -923
  32. data/lib/riffer/config.rb +14 -7
  33. data/lib/riffer/evals/evaluator.rb +13 -3
  34. data/lib/riffer/evals/judge.rb +2 -2
  35. data/lib/riffer/evals/run_result.rb +2 -1
  36. data/lib/riffer/evals/scenario_result.rb +2 -1
  37. data/lib/riffer/guardrails/runner.rb +3 -2
  38. data/lib/riffer/helpers/call_or_value.rb +16 -0
  39. data/lib/riffer/helpers.rb +0 -1
  40. data/lib/riffer/mcp/authenticated_tool.rb +4 -0
  41. data/lib/riffer/mcp/client.rb +1 -1
  42. data/lib/riffer/mcp/registration.rb +2 -3
  43. data/lib/riffer/mcp/registry.rb +3 -1
  44. data/lib/riffer/mcp/tool_factory.rb +5 -0
  45. data/lib/riffer/messages/assistant.rb +9 -3
  46. data/lib/riffer/messages/base.rb +22 -0
  47. data/lib/riffer/messages/converter.rb +6 -6
  48. data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
  49. data/lib/riffer/messages/tool.rb +1 -1
  50. data/lib/riffer/messages/user.rb +4 -4
  51. data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
  52. data/lib/riffer/{param.rb → params/param.rb} +6 -6
  53. data/lib/riffer/params.rb +27 -21
  54. data/lib/riffer/providers/amazon_bedrock.rb +19 -20
  55. data/lib/riffer/providers/anthropic.rb +27 -28
  56. data/lib/riffer/providers/base.rb +10 -9
  57. data/lib/riffer/providers/gemini.rb +15 -12
  58. data/lib/riffer/providers/mock.rb +41 -13
  59. data/lib/riffer/providers/open_ai.rb +24 -22
  60. data/lib/riffer/providers/open_router.rb +318 -0
  61. data/lib/riffer/providers/repository.rb +1 -0
  62. data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
  63. data/lib/riffer/providers.rb +1 -0
  64. data/lib/riffer/runner/fibers.rb +4 -3
  65. data/lib/riffer/runner/sequential.rb +1 -1
  66. data/lib/riffer/runner/threaded.rb +1 -1
  67. data/lib/riffer/runner.rb +1 -1
  68. data/lib/riffer/skills/activate_tool.rb +4 -3
  69. data/lib/riffer/skills/config.rb +1 -1
  70. data/lib/riffer/skills/context.rb +3 -3
  71. data/lib/riffer/skills/filesystem_backend.rb +7 -5
  72. data/lib/riffer/skills/markdown_adapter.rb +1 -1
  73. data/lib/riffer/skills/xml_adapter.rb +1 -1
  74. data/lib/riffer/stream_events/interrupt.rb +1 -1
  75. data/lib/riffer/stream_events/token_usage_done.rb +2 -2
  76. data/lib/riffer/stream_events/web_search_status.rb +1 -1
  77. data/lib/riffer/tool.rb +3 -3
  78. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  79. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  80. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  81. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +9 -9
  82. data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
  83. data/lib/riffer/version.rb +1 -1
  84. data/lib/riffer.rb +2 -1
  85. data/sig/generated/riffer/agent/config.rbs +119 -0
  86. data/sig/generated/riffer/agent/context.rbs +91 -0
  87. data/sig/generated/riffer/agent/run.rbs +144 -0
  88. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  89. data/sig/generated/riffer/agent/session.rbs +145 -0
  90. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  91. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  92. data/sig/generated/riffer/agent.rbs +143 -342
  93. data/sig/generated/riffer/config.rbs +17 -5
  94. data/sig/generated/riffer/evals/judge.rbs +2 -2
  95. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  96. data/sig/generated/riffer/helpers.rbs +0 -1
  97. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  98. data/sig/generated/riffer/messages/base.rbs +18 -0
  99. data/sig/generated/riffer/messages/converter.rbs +4 -4
  100. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
  101. data/sig/generated/riffer/messages/user.rbs +4 -4
  102. data/sig/generated/riffer/params/boolean.rbs +10 -0
  103. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  104. data/sig/generated/riffer/params.rbs +15 -15
  105. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  106. data/sig/generated/riffer/providers/anthropic.rbs +4 -4
  107. data/sig/generated/riffer/providers/base.rbs +10 -10
  108. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  109. data/sig/generated/riffer/providers/mock.rbs +25 -5
  110. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  111. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  112. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  113. data/sig/generated/riffer/providers.rbs +1 -0
  114. data/sig/generated/riffer/runner/fibers.rbs +2 -2
  115. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  116. data/sig/generated/riffer/runner/threaded.rbs +2 -2
  117. data/sig/generated/riffer/runner.rbs +2 -2
  118. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  119. data/sig/generated/riffer/skills/config.rbs +1 -1
  120. data/sig/generated/riffer/skills/context.rbs +2 -2
  121. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  122. data/sig/generated/riffer/tool.rbs +5 -5
  123. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  124. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  125. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  126. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +12 -12
  127. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
  128. data/sig/stubs/agent_ivars.rbs +7 -0
  129. data/sig/stubs/async.rbs +24 -0
  130. data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
  131. data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
  132. data/sig/stubs/extend_self.rbs +11 -0
  133. data/sig/stubs/lib_ivars.rbs +101 -0
  134. data/sig/stubs/mcp_sdk.rbs +22 -0
  135. data/sig/stubs/provider_ivars.rbs +36 -0
  136. data/sig/stubs/provider_sdk_methods.rbs +50 -0
  137. data/sig/stubs/zeitwerk.rbs +12 -0
  138. metadata +54 -33
  139. data/lib/riffer/core.rb +0 -28
  140. data/lib/riffer/helpers/validations.rb +0 -18
  141. data/sig/generated/riffer/boolean.rbs +0 -10
  142. data/sig/generated/riffer/core.rbs +0 -19
  143. data/sig/generated/riffer/helpers/validations.rbs +0 -12
@@ -63,7 +63,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
63
63
  system: partitioned_messages[:system],
64
64
  messages: partitioned_messages[:conversation],
65
65
  **options.except(:tools, :structured_output)
66
- }
66
+ } #: Hash[Symbol, untyped]
67
67
 
68
68
  if tools && !tools.empty?
69
69
  params[:tool_config] = {
@@ -92,18 +92,17 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
92
92
  end
93
93
 
94
94
  #--
95
- #: (Hash[Symbol, untyped]) -> Aws::BedrockRuntime::Types::ConverseResponse
95
+ #: (Hash[Symbol, untyped]) -> Aws::BedrockRuntime::Client::_ConverseResponseSuccess
96
96
  def execute_generate(params)
97
97
  @client.converse(**params)
98
98
  end
99
99
 
100
100
  #--
101
- #: (Aws::BedrockRuntime::Types::ConverseResponse) -> Riffer::TokenUsage?
101
+ #: (Aws::BedrockRuntime::Client::_ConverseResponseSuccess) -> Riffer::Providers::TokenUsage?
102
102
  def extract_token_usage(response)
103
103
  usage = response.usage
104
- return nil unless usage
105
104
 
106
- Riffer::TokenUsage.new(
105
+ Riffer::Providers::TokenUsage.new(
107
106
  input_tokens: usage.input_tokens,
108
107
  output_tokens: usage.output_tokens,
109
108
  cache_creation_tokens: usage.cache_write_input_tokens,
@@ -112,7 +111,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
112
111
  end
113
112
 
114
113
  #--
115
- #: (Aws::BedrockRuntime::Types::ConverseResponse) -> String
114
+ #: (Aws::BedrockRuntime::Client::_ConverseResponseSuccess) -> String
116
115
  def extract_content(response)
117
116
  content_blocks = response.output&.message&.content
118
117
  return "" if content_blocks.nil? || content_blocks.empty?
@@ -127,12 +126,12 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
127
126
  end
128
127
 
129
128
  #--
130
- #: (Aws::BedrockRuntime::Types::ConverseResponse) -> Array[Riffer::Messages::Assistant::ToolCall]
129
+ #: (Aws::BedrockRuntime::Client::_ConverseResponseSuccess) -> Array[Riffer::Messages::Assistant::ToolCall]
131
130
  def extract_tool_calls(response)
132
131
  content_blocks = response.output&.message&.content
133
132
  return [] if content_blocks.nil? || content_blocks.empty?
134
133
 
135
- tool_calls = []
134
+ tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
136
135
 
137
136
  content_blocks.each do |block|
138
137
  if block.respond_to?(:tool_use) && block.tool_use
@@ -153,7 +152,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
153
152
  current_state = {
154
153
  text: nil,
155
154
  tool_call: nil
156
- }
155
+ } #: Hash[Symbol, untyped]
157
156
 
158
157
  @client.converse_stream(**params) do |stream|
159
158
  stream.on_event do |event|
@@ -196,7 +195,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
196
195
  end
197
196
 
198
197
  #--
199
- #: (Aws::BedrockRuntime::Types::ContentBlockStartEvent, state: Hash[Symbol, untyped], yielder: Enumerator[Riffer::StreamEvents::Base, void]) -> void
198
+ #: (Aws::BedrockRuntime::Types::ContentBlockStartEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
200
199
  def handle_content_block_start_tool_use(event, state:, yielder:)
201
200
  state[:tool_call] = {
202
201
  id: event.start.tool_use.tool_use_id,
@@ -206,7 +205,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
206
205
  end
207
206
 
208
207
  #--
209
- #: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator[Riffer::StreamEvents::Base, void]) -> void
208
+ #: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
210
209
  def handle_content_block_delta_text_delta(event, state:, yielder:)
211
210
  delta_text = event.delta.text
212
211
  state[:text] ||= ""
@@ -215,7 +214,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
215
214
  end
216
215
 
217
216
  #--
218
- #: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator[Riffer::StreamEvents::Base, void]) -> void
217
+ #: (Aws::BedrockRuntime::Types::ContentBlockDeltaEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
219
218
  def handle_content_block_delta_tool_use(event, state:, yielder:)
220
219
  input_delta = event.delta.tool_use.input
221
220
 
@@ -229,14 +228,14 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
229
228
  end
230
229
 
231
230
  #--
232
- #: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator[Riffer::StreamEvents::Base, void]) -> void
231
+ #: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
233
232
  def handle_content_block_stop_text_delta(_event, state:, yielder:)
234
233
  yielder << Riffer::StreamEvents::TextDone.new(state[:text])
235
234
  state[:text] = nil
236
235
  end
237
236
 
238
237
  #--
239
- #: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator[Riffer::StreamEvents::Base, void]) -> void
238
+ #: (Aws::BedrockRuntime::Types::ContentBlockStopEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
240
239
  def handle_content_block_stop_tool_use(_event, state:, yielder:)
241
240
  tool_call = state[:tool_call]
242
241
  yielder << Riffer::StreamEvents::ToolCallDone.new(
@@ -249,10 +248,10 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
249
248
  end
250
249
 
251
250
  #--
252
- #: (Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent, state: Hash[Symbol, untyped], yielder: Enumerator[Riffer::StreamEvents::Base, void]) -> void
251
+ #: (Aws::BedrockRuntime::Types::ConverseStreamMetadataEvent, state: Hash[Symbol, untyped], yielder: Enumerator::Yielder) -> void
253
252
  def handle_metadata_usage(event, state:, yielder:)
254
253
  yielder << Riffer::StreamEvents::TokenUsageDone.new(
255
- token_usage: Riffer::TokenUsage.new(
254
+ token_usage: Riffer::Providers::TokenUsage.new(
256
255
  input_tokens: event.usage.input_tokens,
257
256
  output_tokens: event.usage.output_tokens,
258
257
  cache_creation_tokens: event.usage.cache_write_input_tokens,
@@ -264,8 +263,8 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
264
263
  #--
265
264
  #: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
266
265
  def partition_messages(messages)
267
- system_prompts = []
268
- conversation_messages = []
266
+ system_prompts = [] #: Array[Hash[Symbol, untyped]]
267
+ conversation_messages = [] #: Array[Hash[Symbol, untyped]]
269
268
 
270
269
  messages.each do |message|
271
270
  case message
@@ -291,7 +290,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
291
290
  #--
292
291
  #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
293
292
  def convert_assistant_to_bedrock_format(message)
294
- content = []
293
+ content = [] #: Array[Hash[Symbol, untyped]]
295
294
  content << {text: message.content} if message.content && !message.content.empty?
296
295
 
297
296
  message.tool_calls.each do |tc|
@@ -326,7 +325,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
326
325
  end
327
326
 
328
327
  #--
329
- #: (Riffer::FilePart) -> Hash[Symbol, untyped]
328
+ #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
330
329
  def convert_file_part_to_bedrock_format(file)
331
330
  format = bedrock_format(file.media_type)
332
331
 
@@ -26,7 +26,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
26
26
 
27
27
  api_key ||= Riffer.config.anthropic.api_key
28
28
 
29
- @client = Anthropic::Client.new(api_key: api_key, **options)
29
+ @client = ::Anthropic::Client.new(api_key: api_key, **options)
30
30
  end
31
31
 
32
32
  private
@@ -46,11 +46,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
46
46
  messages: partitioned_messages[:conversation],
47
47
  max_tokens: max_tokens,
48
48
  **options.except(:tools, :max_tokens, :structured_output, :web_search)
49
- }
49
+ } #: Hash[Symbol, untyped]
50
50
 
51
51
  params[:system] = partitioned_messages[:system] if partitioned_messages[:system]
52
52
 
53
- anthropic_tools = []
53
+ anthropic_tools = [] #: Array[Hash[Symbol, untyped]]
54
54
  anthropic_tools.concat(tools.map { |t| convert_tool_to_anthropic_format(t) }) if tools && !tools.empty?
55
55
 
56
56
  if web_search
@@ -83,12 +83,11 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
83
83
  end
84
84
 
85
85
  #--
86
- #: (Anthropic::Models::Message) -> Riffer::TokenUsage?
86
+ #: (Anthropic::Models::Message) -> Riffer::Providers::TokenUsage?
87
87
  def extract_token_usage(response)
88
88
  usage = response.usage
89
- return nil unless usage
90
89
 
91
- Riffer::TokenUsage.new(
90
+ Riffer::Providers::TokenUsage.new(
92
91
  input_tokens: usage.input_tokens,
93
92
  output_tokens: usage.output_tokens,
94
93
  cache_creation_tokens: usage.cache_creation_input_tokens,
@@ -105,7 +104,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
105
104
  text_content = ""
106
105
 
107
106
  content_blocks.each do |block|
108
- text_content = block.text if block.type.to_s == "text"
107
+ text_content = block.text if block.is_a?(::Anthropic::Models::TextBlock)
109
108
  end
110
109
 
111
110
  text_content
@@ -117,10 +116,10 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
117
116
  content_blocks = response.content
118
117
  return [] if content_blocks.nil? || content_blocks.empty?
119
118
 
120
- tool_calls = []
119
+ tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
121
120
 
122
121
  content_blocks.each do |block|
123
- if block.type.to_s == "tool_use"
122
+ if block.is_a?(::Anthropic::Models::ToolUseBlock)
124
123
  tool_calls << Riffer::Messages::Assistant::ToolCall.new(
125
124
  call_id: block.id,
126
125
  name: decode_tool_name(block.name, tools: @current_tools),
@@ -142,7 +141,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
142
141
  web_search_index: nil,
143
142
  web_search_json: nil,
144
143
  web_search_query: nil
145
- }
144
+ } #: Hash[Symbol, untyped]
146
145
 
147
146
  # Workaround for anthropics/anthropic-sdk-ruby#182: force identity
148
147
  # encoding so Net::HTTP/Zlib doesn't buffer SSE chunks until EOF.
@@ -154,24 +153,24 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
154
153
  begin
155
154
  stream.each do |event|
156
155
  case event
157
- when Anthropic::Models::RawContentBlockStartEvent
156
+ when ::Anthropic::Models::RawContentBlockStartEvent
158
157
  handle_raw_content_block_start(event, state: current_state)
159
- when Anthropic::Models::RawContentBlockDeltaEvent
158
+ when ::Anthropic::Models::RawContentBlockDeltaEvent
160
159
  handle_raw_content_block_delta(event, state: current_state)
161
- when Anthropic::Streaming::TextEvent
160
+ when ::Anthropic::Helpers::Streaming::TextEvent
162
161
  handle_text_event(event, state: current_state, yielder: yielder)
163
- when Anthropic::Streaming::ThinkingEvent
162
+ when ::Anthropic::Helpers::Streaming::ThinkingEvent
164
163
  handle_thinking_event(event, state: current_state, yielder: yielder)
165
- when Anthropic::Streaming::InputJsonEvent
164
+ when ::Anthropic::Helpers::Streaming::InputJsonEvent
166
165
  handle_input_json_event(event, state: current_state, yielder: yielder)
167
- when Anthropic::Streaming::ContentBlockStopEvent
168
- block_type = event.content_block&.type.to_s
169
- handle_content_block_stop_text(event, state: current_state, yielder: yielder) if block_type == "text" && current_state[:text]
170
- handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if block_type == "tool_use"
171
- handle_content_block_stop_thinking(event, state: current_state, yielder: yielder) if block_type == "thinking" && current_state[:reasoning]
172
- handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder) if block_type == "server_tool_use"
173
- handle_content_block_stop_web_search_result(event, state: current_state, yielder: yielder) if block_type == "web_search_tool_result"
174
- when Anthropic::Streaming::MessageStopEvent
166
+ when ::Anthropic::Helpers::Streaming::ContentBlockStopEvent
167
+ block = event.content_block
168
+ handle_content_block_stop_text(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::TextBlock) && current_state[:text]
169
+ handle_content_block_stop_tool_use(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::ToolUseBlock)
170
+ handle_content_block_stop_thinking(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::ThinkingBlock) && current_state[:reasoning]
171
+ handle_content_block_stop_server_tool_use(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::ServerToolUseBlock)
172
+ handle_content_block_stop_web_search_result(event, state: current_state, yielder: yielder) if block.is_a?(::Anthropic::Models::WebSearchToolResultBlock)
173
+ when ::Anthropic::Helpers::Streaming::MessageStopEvent
175
174
  handle_message_stop(event, accumulated_message: stream.accumulated_message, yielder: yielder)
176
175
  end
177
176
  end
@@ -292,7 +291,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
292
291
  return unless usage
293
292
 
294
293
  yielder << Riffer::StreamEvents::TokenUsageDone.new(
295
- token_usage: Riffer::TokenUsage.new(
294
+ token_usage: Riffer::Providers::TokenUsage.new(
296
295
  input_tokens: usage.input_tokens,
297
296
  output_tokens: usage.output_tokens,
298
297
  cache_creation_tokens: usage.cache_creation_input_tokens,
@@ -304,8 +303,8 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
304
303
  #--
305
304
  #: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
306
305
  def partition_messages(messages)
307
- system_prompts = []
308
- conversation_messages = []
306
+ system_prompts = [] #: Array[Hash[Symbol, untyped]]
307
+ conversation_messages = [] #: Array[Hash[Symbol, untyped]]
309
308
 
310
309
  messages.each do |message|
311
310
  case message
@@ -342,7 +341,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
342
341
  #--
343
342
  #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
344
343
  def convert_assistant_to_anthropic_format(message)
345
- content = []
344
+ content = [] #: Array[Hash[Symbol, untyped]]
346
345
  content << {type: "text", text: message.content} if message.content && !message.content.empty?
347
346
 
348
347
  message.tool_calls.each do |tc|
@@ -358,7 +357,7 @@ class Riffer::Providers::Anthropic < Riffer::Providers::Base
358
357
  end
359
358
 
360
359
  #--
361
- #: (Riffer::FilePart) -> Hash[Symbol, untyped]
360
+ #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
362
361
  def convert_file_part_to_anthropic_format(file)
363
362
  type = file.image? ? "image" : "document"
364
363
 
@@ -39,10 +39,10 @@ class Riffer::Providers::Base
39
39
  # Generates text using the provider.
40
40
  #
41
41
  #--
42
- #: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, **untyped) -> Riffer::Messages::Assistant
42
+ #: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, **untyped) -> Riffer::Messages::Assistant
43
43
  def generate_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
44
44
  validate_input!(prompt: prompt, system: system, messages: messages)
45
- @current_tools = options[:tools] || []
45
+ @current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
46
46
  messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
47
47
  validate_normalized_messages!(messages)
48
48
  messages = merge_consecutive_messages(messages)
@@ -65,10 +65,10 @@ class Riffer::Providers::Base
65
65
  # Streams text from the provider.
66
66
  #
67
67
  #--
68
- #: (?prompt: String?, ?system: String?, ?messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?model: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
68
+ #: (?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]
69
69
  def stream_text(prompt: nil, system: nil, messages: nil, model: nil, files: nil, **options)
70
70
  validate_input!(prompt: prompt, system: system, messages: messages)
71
- @current_tools = options[:tools] || []
71
+ @current_tools = options[:tools] || [] #: Array[singleton(Riffer::Tool)]
72
72
  messages = normalize_messages(prompt: prompt, system: system, messages: messages, files: files)
73
73
  validate_normalized_messages!(messages)
74
74
  messages = merge_consecutive_messages(messages)
@@ -87,7 +87,7 @@ class Riffer::Providers::Base
87
87
  end
88
88
 
89
89
  #--
90
- #: (String, tools: Array[Riffer::Tool]) -> String
90
+ #: (String, tools: Array[singleton(Riffer::Tool)]) -> String
91
91
  def decode_tool_name(wire_name, tools:)
92
92
  tool = tools.find { |t| encode_tool_name(t.name) == wire_name }
93
93
  tool ? tool.name : wire_name
@@ -112,7 +112,7 @@ class Riffer::Providers::Base
112
112
  end
113
113
 
114
114
  #--
115
- #: (untyped) -> Riffer::TokenUsage?
115
+ #: (untyped) -> Riffer::Providers::TokenUsage?
116
116
  def extract_token_usage(response)
117
117
  raise NotImplementedError, "Subclasses must implement #extract_token_usage"
118
118
  end
@@ -166,7 +166,7 @@ class Riffer::Providers::Base
166
166
  end
167
167
 
168
168
  #--
169
- #: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> Array[Riffer::Messages::Base]
169
+ #: (prompt: String?, system: String?, messages: Array[Hash[Symbol, untyped] | Riffer::Messages::Base]?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Array[Riffer::Messages::Base]
170
170
  def normalize_messages(prompt:, system:, messages:, files: nil)
171
171
  if messages && files && !files.empty?
172
172
  raise Riffer::ArgumentError, "cannot provide both files and messages; attach files to individual messages instead"
@@ -176,10 +176,11 @@ class Riffer::Providers::Base
176
176
  return messages.map { |msg| convert_to_message_object(msg) }
177
177
  end
178
178
 
179
- result = []
179
+ result = [] #: Array[Riffer::Messages::Base]
180
180
  result << Riffer::Messages::System.new(system) if system
181
181
  file_parts = (files || []).map { |f| convert_to_file_part(f) }
182
- result << Riffer::Messages::User.new(prompt, files: file_parts)
182
+ prompt_text = prompt #: String
183
+ result << Riffer::Messages::User.new(prompt_text, files: file_parts)
183
184
  result
184
185
  end
185
186
 
@@ -36,7 +36,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
36
36
  params = {
37
37
  model: model,
38
38
  contents: partitioned[:contents]
39
- }
39
+ } #: Hash[Symbol, untyped]
40
40
 
41
41
  params[:systemInstruction] = partitioned[:system_instruction] if partitioned[:system_instruction]
42
42
 
@@ -96,12 +96,12 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
96
96
  end
97
97
 
98
98
  #--
99
- #: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
99
+ #: (Hash[Symbol, untyped]) -> Riffer::Providers::TokenUsage?
100
100
  def extract_token_usage(response)
101
101
  usage = response[:usageMetadata]
102
102
  return nil unless usage
103
103
 
104
- Riffer::TokenUsage.new(
104
+ Riffer::Providers::TokenUsage.new(
105
105
  input_tokens: usage[:promptTokenCount] || 0,
106
106
  output_tokens: usage[:candidatesTokenCount] || 0
107
107
  )
@@ -114,6 +114,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
114
114
  body = params.except(:model)
115
115
 
116
116
  uri = URI("#{BASE_URI}/#{api_path(model, "streamGenerateContent")}?alt=sse")
117
+ host = uri.hostname #: String
117
118
  request = Net::HTTP::Post.new(uri)
118
119
  request["Content-Type"] = "application/json"
119
120
  request["x-goog-api-key"] = @api_key
@@ -126,7 +127,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
126
127
  buffer << chunk
127
128
 
128
129
  while (match = buffer.match(/\r?\n\r?\n/))
129
- frame = buffer.slice!(0, match.end(0)).strip
130
+ match_end = match.end(0) #: Integer
131
+ frame = buffer.slice!(0, match_end).to_s.strip
130
132
  next unless frame.start_with?("data: ")
131
133
 
132
134
  json_str = frame.delete_prefix("data: ").strip
@@ -155,7 +157,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
155
157
  usage = parsed[:usageMetadata]
156
158
  if usage && usage[:candidatesTokenCount]
157
159
  yielder << Riffer::StreamEvents::TokenUsageDone.new(
158
- token_usage: Riffer::TokenUsage.new(
160
+ token_usage: Riffer::Providers::TokenUsage.new(
159
161
  input_tokens: usage[:promptTokenCount] || 0,
160
162
  output_tokens: usage[:candidatesTokenCount] || 0
161
163
  )
@@ -164,7 +166,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
164
166
  end
165
167
  end
166
168
 
167
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http|
169
+ Net::HTTP.start(host, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http|
168
170
  http.request(request) do |response|
169
171
  handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
170
172
 
@@ -182,8 +184,8 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
182
184
  #--
183
185
  #: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
184
186
  def partition_messages(messages)
185
- system_parts = []
186
- contents = []
187
+ system_parts = [] #: Array[Hash[Symbol, untyped]]
188
+ contents = [] #: Array[Hash[Symbol, untyped]]
187
189
 
188
190
  messages.each do |message|
189
191
  case message
@@ -212,7 +214,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
212
214
  end
213
215
  end
214
216
 
215
- result = {contents: contents}
217
+ result = {contents: contents} #: Hash[Symbol, untyped]
216
218
  result[:system_instruction] = {parts: system_parts} unless system_parts.empty?
217
219
  result
218
220
  end
@@ -220,7 +222,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
220
222
  #--
221
223
  #: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
222
224
  def convert_assistant_to_gemini_format(message)
223
- parts = []
225
+ parts = [] #: Array[Hash[Symbol, untyped]]
224
226
  parts << {text: message.content} if message.content && !message.content.empty?
225
227
 
226
228
  message.tool_calls.each do |tc|
@@ -236,7 +238,7 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
236
238
  end
237
239
 
238
240
  #--
239
- #: (Riffer::FilePart) -> Hash[Symbol, untyped]
241
+ #: (Riffer::Messages::FilePart) -> Hash[Symbol, untyped]
240
242
  def convert_file_part_to_gemini_format(file)
241
243
  if file.url?
242
244
  raise Riffer::ArgumentError,
@@ -268,11 +270,12 @@ class Riffer::Providers::Gemini < Riffer::Providers::Base
268
270
  #: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
269
271
  def post_request(path, body)
270
272
  uri = URI("#{BASE_URI}/#{path}")
273
+ host = uri.hostname #: String
271
274
  request = Net::HTTP::Post.new(uri)
272
275
  request["Content-Type"] = "application/json"
273
276
  request["x-goog-api-key"] = @api_key
274
277
  request.body = body.to_json
275
- Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) }
278
+ Net::HTTP.start(host, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) }
276
279
  end
277
280
 
278
281
  #--
@@ -25,10 +25,21 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
25
25
 
26
26
  # Initializes the mock provider.
27
27
  #
28
+ # +responses:+ accepts an array of response hashes in the same shape
29
+ # +#stub_response+ takes — raw +tool_calls:+ hashes are normalised to
30
+ # +Riffer::Messages::Assistant::ToolCall+ instances. This is the canonical
31
+ # way to pre-configure canned LLM responses on an agent via
32
+ # +provider_options responses: [...]+.
33
+ #
34
+ # Riffer::Providers::Mock.new(responses: [
35
+ # {content: "", tool_calls: [{name: "tool_a", arguments: "{}"}]},
36
+ # {content: "Final answer"}
37
+ # ])
38
+ #
28
39
  #--
29
40
  #: (**untyped) -> void
30
41
  def initialize(**options)
31
- @responses = options[:responses] || []
42
+ @responses = (options[:responses] || []).map { |r| normalize_response(r) }
32
43
  @current_index = 0
33
44
  @calls = []
34
45
  @stubbed_responses = []
@@ -40,19 +51,12 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
40
51
  #
41
52
  # provider.stub_response("Hello")
42
53
  # provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
43
- # provider.stub_response("Final response", token_usage: Riffer::TokenUsage.new(input_tokens: 10, output_tokens: 5))
54
+ # provider.stub_response("Final response", token_usage: Riffer::Providers::TokenUsage.new(input_tokens: 10, output_tokens: 5))
44
55
  #
45
56
  #--
46
- #: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::TokenUsage?) -> void
57
+ #: (String, ?tool_calls: Array[Hash[Symbol, untyped]], ?token_usage: Riffer::Providers::TokenUsage?) -> void
47
58
  def stub_response(content, tool_calls: [], token_usage: nil)
48
- formatted_tool_calls = tool_calls.map.with_index do |tc, idx|
49
- Riffer::Messages::Assistant::ToolCall.new(
50
- call_id: tc[:call_id] || tc[:id] || "mock_call_#{idx}",
51
- name: tc[:name],
52
- arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
53
- )
54
- end
55
- @stubbed_responses << {role: "assistant", content: content, tool_calls: formatted_tool_calls, token_usage: token_usage}
59
+ @stubbed_responses << normalize_response(content: content, tool_calls: tool_calls, token_usage: token_usage)
56
60
  end
57
61
 
58
62
  # Clears all stubbed responses.
@@ -65,6 +69,30 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
65
69
 
66
70
  private
67
71
 
72
+ # Normalises a response hash into Mock's internal format. Accepts the
73
+ # +#stub_response+ kwargs shape (+content:+, +tool_calls:+, +token_usage:+)
74
+ # or a pre-built hash with already-converted ToolCall instances. Raw
75
+ # +tool_calls:+ hashes are wrapped in +Riffer::Messages::Assistant::ToolCall+.
76
+ #
77
+ #--
78
+ #: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
79
+ def normalize_response(response)
80
+ formatted_tool_calls = (response[:tool_calls] || []).map.with_index do |tc, idx|
81
+ next tc if tc.is_a?(Riffer::Messages::Assistant::ToolCall)
82
+ Riffer::Messages::Assistant::ToolCall.new(
83
+ call_id: tc[:call_id] || tc[:id] || "mock_call_#{idx}",
84
+ name: tc[:name],
85
+ arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
86
+ )
87
+ end
88
+ {
89
+ role: response[:role] || "assistant",
90
+ content: response[:content] || "",
91
+ tool_calls: formatted_tool_calls,
92
+ token_usage: response[:token_usage]
93
+ }
94
+ end
95
+
68
96
  #--
69
97
  #: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
70
98
  def build_request_params(messages, model, options)
@@ -82,7 +110,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
82
110
  end
83
111
 
84
112
  #--
85
- #: (untyped) -> Riffer::TokenUsage?
113
+ #: (untyped) -> Riffer::Providers::TokenUsage?
86
114
  def extract_token_usage(response)
87
115
  response[:token_usage]
88
116
  end
@@ -146,7 +174,7 @@ class Riffer::Providers::Mock < Riffer::Providers::Base
146
174
  def next_response
147
175
  if @stubbed_responses.any?
148
176
  @stubbed_responses.shift
149
- elsif @responses[@current_index]
177
+ elsif @current_index < @responses.size
150
178
  response = @responses[@current_index]
151
179
  @current_index += 1
152
180
  response