riffer 0.27.2 → 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 (146) 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 +31 -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 +134 -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 +23 -19
  16. data/docs/08_MESSAGES.md +28 -31
  17. data/docs/09_STREAM_EVENTS.md +1 -1
  18. data/docs/10_CONFIGURATION.md +25 -15
  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/response.rb +11 -2
  27. data/lib/riffer/agent/run.rb +308 -0
  28. data/lib/riffer/agent/session/repair.rb +112 -0
  29. data/lib/riffer/agent/session.rb +268 -0
  30. data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
  31. data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
  32. data/lib/riffer/agent.rb +246 -684
  33. data/lib/riffer/config.rb +56 -7
  34. data/lib/riffer/evals/evaluator.rb +13 -3
  35. data/lib/riffer/evals/judge.rb +2 -2
  36. data/lib/riffer/evals/run_result.rb +2 -1
  37. data/lib/riffer/evals/scenario_result.rb +2 -1
  38. data/lib/riffer/guardrails/runner.rb +3 -2
  39. data/lib/riffer/helpers/call_or_value.rb +16 -0
  40. data/lib/riffer/helpers.rb +0 -1
  41. data/lib/riffer/mcp/authenticated_tool.rb +4 -0
  42. data/lib/riffer/mcp/client.rb +1 -1
  43. data/lib/riffer/mcp/registration.rb +2 -3
  44. data/lib/riffer/mcp/registry.rb +3 -1
  45. data/lib/riffer/mcp/tool_factory.rb +5 -0
  46. data/lib/riffer/messages/assistant.rb +9 -3
  47. data/lib/riffer/messages/base.rb +22 -0
  48. data/lib/riffer/messages/converter.rb +6 -6
  49. data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
  50. data/lib/riffer/messages/tool.rb +1 -1
  51. data/lib/riffer/messages/user.rb +4 -4
  52. data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
  53. data/lib/riffer/{param.rb → params/param.rb} +6 -6
  54. data/lib/riffer/params.rb +27 -21
  55. data/lib/riffer/providers/amazon_bedrock.rb +19 -20
  56. data/lib/riffer/providers/anthropic.rb +27 -28
  57. data/lib/riffer/providers/base.rb +10 -9
  58. data/lib/riffer/providers/gemini.rb +15 -12
  59. data/lib/riffer/providers/mock.rb +41 -13
  60. data/lib/riffer/providers/open_ai.rb +24 -22
  61. data/lib/riffer/providers/open_router.rb +318 -0
  62. data/lib/riffer/providers/repository.rb +1 -0
  63. data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
  64. data/lib/riffer/providers.rb +1 -0
  65. data/lib/riffer/runner/fibers.rb +4 -3
  66. data/lib/riffer/runner/sequential.rb +1 -1
  67. data/lib/riffer/runner/threaded.rb +1 -1
  68. data/lib/riffer/runner.rb +1 -1
  69. data/lib/riffer/skills/activate_tool.rb +4 -3
  70. data/lib/riffer/skills/config.rb +1 -1
  71. data/lib/riffer/skills/context.rb +3 -3
  72. data/lib/riffer/skills/filesystem_backend.rb +7 -5
  73. data/lib/riffer/skills/markdown_adapter.rb +1 -1
  74. data/lib/riffer/skills/xml_adapter.rb +1 -1
  75. data/lib/riffer/stream_events/interrupt.rb +10 -3
  76. data/lib/riffer/stream_events/token_usage_done.rb +2 -2
  77. data/lib/riffer/stream_events/web_search_status.rb +1 -1
  78. data/lib/riffer/tool.rb +3 -3
  79. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  80. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  81. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  82. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +21 -15
  83. data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
  84. data/lib/riffer/version.rb +1 -1
  85. data/lib/riffer.rb +2 -1
  86. data/sig/generated/riffer/agent/config.rbs +119 -0
  87. data/sig/generated/riffer/agent/context.rbs +91 -0
  88. data/sig/generated/riffer/agent/response.rbs +10 -2
  89. data/sig/generated/riffer/agent/run.rbs +144 -0
  90. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  91. data/sig/generated/riffer/agent/session.rbs +145 -0
  92. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  93. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  94. data/sig/generated/riffer/agent.rbs +154 -225
  95. data/sig/generated/riffer/config.rbs +50 -5
  96. data/sig/generated/riffer/evals/judge.rbs +2 -2
  97. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  98. data/sig/generated/riffer/helpers.rbs +0 -1
  99. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  100. data/sig/generated/riffer/messages/base.rbs +18 -0
  101. data/sig/generated/riffer/messages/converter.rbs +4 -4
  102. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
  103. data/sig/generated/riffer/messages/user.rbs +4 -4
  104. data/sig/generated/riffer/params/boolean.rbs +10 -0
  105. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  106. data/sig/generated/riffer/params.rbs +15 -15
  107. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  108. data/sig/generated/riffer/providers/anthropic.rbs +4 -4
  109. data/sig/generated/riffer/providers/base.rbs +10 -10
  110. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  111. data/sig/generated/riffer/providers/mock.rbs +25 -5
  112. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  113. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  114. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  115. data/sig/generated/riffer/providers.rbs +1 -0
  116. data/sig/generated/riffer/runner/fibers.rbs +2 -2
  117. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  118. data/sig/generated/riffer/runner/threaded.rbs +2 -2
  119. data/sig/generated/riffer/runner.rbs +2 -2
  120. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  121. data/sig/generated/riffer/skills/config.rbs +1 -1
  122. data/sig/generated/riffer/skills/context.rbs +2 -2
  123. data/sig/generated/riffer/stream_events/interrupt.rbs +7 -2
  124. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  125. data/sig/generated/riffer/tool.rbs +5 -5
  126. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  127. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  128. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  129. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +19 -13
  130. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
  131. data/sig/stubs/agent_ivars.rbs +7 -0
  132. data/sig/stubs/async.rbs +24 -0
  133. data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
  134. data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
  135. data/sig/stubs/extend_self.rbs +11 -0
  136. data/sig/stubs/lib_ivars.rbs +101 -0
  137. data/sig/stubs/mcp_sdk.rbs +22 -0
  138. data/sig/stubs/provider_ivars.rbs +36 -0
  139. data/sig/stubs/provider_sdk_methods.rbs +50 -0
  140. data/sig/stubs/zeitwerk.rbs +12 -0
  141. metadata +54 -33
  142. data/lib/riffer/core.rb +0 -28
  143. data/lib/riffer/helpers/validations.rb +0 -18
  144. data/sig/generated/riffer/boolean.rbs +0 -10
  145. data/sig/generated/riffer/core.rbs +0 -19
  146. data/sig/generated/riffer/helpers/validations.rbs +0 -12
data/lib/riffer/config.rb CHANGED
@@ -12,6 +12,8 @@
12
12
  #
13
13
  # Riffer.config.anthropic.api_key = "sk-ant-..."
14
14
  #
15
+ # Riffer.config.openrouter.api_key = "sk-or-..."
16
+ #
15
17
  # Riffer.config.evals.judge_model = "anthropic/claude-sonnet-4-20250514"
16
18
  #
17
19
  class Riffer::Config
@@ -20,6 +22,7 @@ class Riffer::Config
20
22
  AzureOpenAI = Struct.new(:api_key, :endpoint, keyword_init: true)
21
23
  Gemini = Struct.new(:api_key, :open_timeout, :read_timeout, keyword_init: true)
22
24
  OpenAI = Struct.new(:api_key, keyword_init: true)
25
+ OpenRouter = Struct.new(:api_key, keyword_init: true)
23
26
  Evals = Struct.new(:judge_model, keyword_init: true)
24
27
  Mcp = Struct.new(:credentials, :discovery_runner, keyword_init: true)
25
28
 
@@ -91,6 +94,9 @@ class Riffer::Config
91
94
  # OpenAI configuration (Struct with +api_key+).
92
95
  attr_reader :openai #: Riffer::Config::OpenAI
93
96
 
97
+ # OpenRouter configuration (Struct with +api_key+).
98
+ attr_reader :openrouter #: Riffer::Config::OpenRouter
99
+
94
100
  # Evals configuration (Struct with +judge_model+).
95
101
  attr_reader :evals #: Riffer::Config::Evals
96
102
 
@@ -107,9 +113,9 @@ class Riffer::Config
107
113
 
108
114
  # Global tool runtime configuration (experimental).
109
115
  #
110
- # Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
111
- # or a Proc. Defaults to <tt>Riffer::ToolRuntime::Inline.new</tt>.
112
- attr_reader :tool_runtime #: (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)
116
+ # Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance,
117
+ # or a Proc. Defaults to <tt>Riffer::Tools::Runtime::Inline.new</tt>.
118
+ attr_reader :tool_runtime #: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
113
119
 
114
120
  # Sets the global tool runtime.
115
121
  #
@@ -117,10 +123,10 @@ class Riffer::Config
117
123
  # (ToolRuntime subclass, ToolRuntime instance, or Proc).
118
124
  #
119
125
  #--
120
- #: ((singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)) -> void
126
+ #: ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> void
121
127
  def tool_runtime=(value)
122
- valid = (value.is_a?(Class) && value < Riffer::ToolRuntime) || value.is_a?(Riffer::ToolRuntime) || value.is_a?(Proc)
123
- raise Riffer::ArgumentError, "tool_runtime must be a Riffer::ToolRuntime subclass, instance, or a Proc" unless valid
128
+ valid = (value.is_a?(Class) && value < Riffer::Tools::Runtime) || value.is_a?(Riffer::Tools::Runtime) || value.is_a?(Proc)
129
+ raise Riffer::ArgumentError, "tool_runtime must be a Riffer::Tools::Runtime subclass, instance, or a Proc" unless valid
124
130
  @tool_runtime = value
125
131
  end
126
132
 
@@ -151,6 +157,47 @@ class Riffer::Config
151
157
  @message_id_strategy = value
152
158
  end
153
159
 
160
+ # Experimental: when +true+, riffer keeps the +tool_use+ ↔ +tool_result+
161
+ # invariant intact on its own.
162
+ #
163
+ # - On +Riffer::Agent#generate(messages_array)+, orphaned +tool_use+
164
+ # exchanges and parentless +Riffer::Messages::Tool+ messages are
165
+ # silently stripped from the seed. Pending tool calls on the resume
166
+ # boundary (last assistant whose tail is purely Tool results) are
167
+ # preserved for +execute_pending_tool_calls+.
168
+ # - On any interrupt (caller-issued +interrupt!+ or
169
+ # +INTERRUPT_MAX_STEPS+), riffer fills any orphaned +tool_use+ with a
170
+ # placeholder +Riffer::Messages::Tool+ carrying
171
+ # +error_type: :interrupted+, leaving history valid for the next turn.
172
+ # Filled call_ids are exposed on
173
+ # +Riffer::Agent::Response#healed_tool_call_ids+ (and the streaming
174
+ # +Riffer::StreamEvents::Interrupt+ event).
175
+ #
176
+ # Defaults to +false+ — the pre-healing behavior. Experimental: the
177
+ # surface and default may change without notice.
178
+ attr_reader :experimental_history_healing #: bool
179
+
180
+ # Sets the +experimental_history_healing+ flag.
181
+ #
182
+ # Coerces common boolean representations so values pulled from
183
+ # environment variables don't silently enable healing — the string
184
+ # +"false"+ is truthy in Ruby and would otherwise flip the flag on.
185
+ # Accepts +true+/+false+, +"true"+/+"false"+, +1+/+0+, +"1"+/+"0"+, and
186
+ # +nil+ (treated as +false+, the default). Raises
187
+ # +Riffer::ArgumentError+ for any other value.
188
+ #
189
+ #--
190
+ #: (untyped) -> void
191
+ def experimental_history_healing=(value)
192
+ @experimental_history_healing = case value
193
+ when true, "true", 1, "1" then true
194
+ when false, "false", 0, "0", nil then false
195
+ else
196
+ raise Riffer::ArgumentError,
197
+ "experimental_history_healing must be a boolean (or 'true'/'false'/'1'/'0'/1/0), got #{value.inspect}"
198
+ end
199
+ end
200
+
154
201
  #--
155
202
  #: () -> void
156
203
  def initialize
@@ -159,10 +206,12 @@ class Riffer::Config
159
206
  @azure_openai = AzureOpenAI.new
160
207
  @gemini = Gemini.new
161
208
  @openai = OpenAI.new
209
+ @openrouter = OpenRouter.new
162
210
  @evals = Evals.new
163
211
  @mcp = Mcp.new(credentials: nil, discovery_runner: Riffer::Runner::Sequential.new)
164
- @tool_runtime = Riffer::ToolRuntime::Inline.new
212
+ @tool_runtime = Riffer::Tools::Runtime::Inline.new
165
213
  @skills = Skills.new
166
214
  @message_id_strategy = :none
215
+ @experimental_history_healing = false
167
216
  end
168
217
  end
@@ -31,7 +31,11 @@ class Riffer::Evals::Evaluator
31
31
  #--
32
32
  #: (?bool?) -> bool
33
33
  def higher_is_better(value = nil)
34
- return @higher_is_better.nil? || @higher_is_better if value.nil?
34
+ if value.nil?
35
+ current = @higher_is_better
36
+ return true if current.nil?
37
+ return current
38
+ end
35
39
  @higher_is_better = value
36
40
  end
37
41
 
@@ -87,8 +91,14 @@ class Riffer::Evals::Evaluator
87
91
  return input if input.is_a?(String)
88
92
 
89
93
  input.map do |msg|
90
- role = msg.is_a?(Hash) ? (msg[:role] || msg["role"]) : msg.role
91
- content = msg.is_a?(Hash) ? (msg[:content] || msg["content"]) : msg.content
94
+ if msg.is_a?(Hash)
95
+ hash = msg #: Hash[untyped, untyped]
96
+ role = hash[:role] || hash["role"]
97
+ content = hash[:content] || hash["content"]
98
+ else
99
+ role = msg.role
100
+ content = msg.content
101
+ end
92
102
  "#{role}: #{content}"
93
103
  end.join("\n\n")
94
104
  end
@@ -30,7 +30,7 @@ class Riffer::Evals::Judge
30
30
  end
31
31
 
32
32
  #--
33
- #: (context: Hash[Symbol, untyped]?, score: Float, reason: String) -> Riffer::Tools::Response
33
+ #: (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
34
34
  def call(context:, score:, reason:)
35
35
  json({score: score, reason: reason})
36
36
  end
@@ -94,7 +94,7 @@ class Riffer::Evals::Judge
94
94
  #--
95
95
  #: (input: String, output: String, ?ground_truth: String?) -> String
96
96
  def build_user_message(input:, output:, ground_truth: nil)
97
- parts = []
97
+ parts = [] #: Array[String]
98
98
  parts << "## Input\n\n#{input}"
99
99
  parts << "## Output\n\n#{output}"
100
100
  parts << "## Ground Truth\n\n#{ground_truth}" if ground_truth
@@ -40,7 +40,8 @@ class Riffer::Evals::RunResult
40
40
  end
41
41
  end
42
42
 
43
- totals.each_with_object({}) do |(evaluator, total), hash|
43
+ averages = {} #: Hash[singleton(Riffer::Evals::Evaluator), Float]
44
+ totals.each_with_object(averages) do |(evaluator, total), hash|
44
45
  hash[evaluator] = total / counts[evaluator]
45
46
  end
46
47
  end
@@ -47,7 +47,8 @@ class Riffer::Evals::ScenarioResult
47
47
  #--
48
48
  #: () -> Hash[singleton(Riffer::Evals::Evaluator), Float]
49
49
  def scores
50
- results.each_with_object({}) do |result, hash|
50
+ acc = {} #: Hash[singleton(Riffer::Evals::Evaluator), Float]
51
+ results.each_with_object(acc) do |result, hash|
51
52
  hash[result.evaluator] = result.score
52
53
  end
53
54
  end
@@ -80,7 +80,8 @@ class Riffer::Guardrails::Runner
80
80
  #--
81
81
  #: (Hash[Symbol, untyped]) -> Riffer::Guardrail
82
82
  def instantiate_guardrail(config)
83
- config[:class].new(**config[:options])
83
+ options = config[:options] #: Hash[Symbol, untyped]
84
+ config[:class].new(**options)
84
85
  end
85
86
 
86
87
  #--
@@ -101,7 +102,7 @@ class Riffer::Guardrails::Runner
101
102
  when :before
102
103
  guardrail.process_input(data, context: context)
103
104
  when :after
104
- guardrail.process_output(data, messages: messages, context: context)
105
+ guardrail.process_output(data, messages: messages || [], context: context)
105
106
  else
106
107
  raise Riffer::Error, "Unexpected guardrail phase: #{phase}. Valid phases: #{Riffer::Guardrails::PHASES.join(", ")}"
107
108
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Resolves the "Proc-or-value" idiom: if +thing+ is a Proc, calls it
5
+ # (passing +context+ when its arity is non-zero); otherwise returns
6
+ # +thing+ unchanged. When +thing+ is +nil+, returns +default+.
7
+ module Riffer::Helpers::CallOrValue
8
+ extend self
9
+
10
+ #: (untyped, ?context: untyped, ?default: untyped) -> untyped
11
+ def resolve(thing, context: nil, default: nil)
12
+ return default if thing.nil?
13
+ return thing unless thing.is_a?(Proc)
14
+ thing.arity.zero? ? thing.call : thing.call(context)
15
+ end
16
+ end
@@ -6,6 +6,5 @@
6
6
  # Helpers provide reusable functionality across the library:
7
7
  # - Riffer::Helpers::ClassNameConverter - Class name to path conversion
8
8
  # - Riffer::Helpers::Dependencies - Lazy gem dependency loading
9
- # - Riffer::Helpers::Validations - Input validation
10
9
  module Riffer::Helpers
11
10
  end
@@ -23,6 +23,9 @@ module Riffer::Mcp::AuthenticatedTool
23
23
  tags = matched_tags
24
24
 
25
25
  Class.new(Riffer::Tool) do
26
+ # steep cannot type the body of a dynamically created anonymous class:
27
+ # its ivars and `self` inside define_method are unresolvable.
28
+ # steep:ignore:start
26
29
  @identifier = inner.identifier
27
30
 
28
31
  define_singleton_method(:name) { inner.name }
@@ -60,6 +63,7 @@ module Riffer::Mcp::AuthenticatedTool
60
63
  client = build_call_client(man.endpoint, headers)
61
64
  text(client.tools_call(inner.mcp_server_tool_name, kwargs))
62
65
  end
66
+ # steep:ignore:end
63
67
  end
64
68
  end
65
69
  end
@@ -23,7 +23,7 @@ class Riffer::Mcp::Client
23
23
  depends_on "faraday"
24
24
 
25
25
  @client = client || begin
26
- resolved_headers = headers.is_a?(Proc) ? headers.call : headers
26
+ resolved_headers = Riffer::Helpers::CallOrValue.resolve(headers)
27
27
  transport = MCP::Client::HTTP.new(url: endpoint, headers: resolved_headers)
28
28
  MCP::Client.new(transport: transport)
29
29
  end
@@ -23,7 +23,7 @@ class Riffer::Mcp::Registration
23
23
  def initialize(manifest)
24
24
  @manifest = manifest
25
25
  @cancelled = false
26
- @tools = []
26
+ @tools = [] #: Array[singleton(Riffer::Tool)]
27
27
  @mutex = Mutex.new
28
28
  run_discovery
29
29
  end
@@ -62,8 +62,7 @@ class Riffer::Mcp::Registration
62
62
  tools = Riffer::Mcp::ToolFactory.build(@manifest.name, client, tool_defs)
63
63
 
64
64
  @mutex.synchronize do
65
- next if @cancelled
66
- @tools = tools.freeze
65
+ @tools = tools.freeze unless @cancelled
67
66
  end
68
67
  end
69
68
  end
@@ -18,7 +18,9 @@ module Riffer::Mcp::Registry
18
18
  #--
19
19
  #: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
20
20
  def register(manifest_or_hash)
21
- manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash)
21
+ # steep cannot verify that an untyped Hash splat supplies Manifest's
22
+ # required name:/endpoint: keywords; Manifest validates them at runtime.
23
+ manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
22
24
  registration = Riffer::Mcp::Registration.new(manifest)
23
25
  old = @mutex.synchronize do
24
26
  previous = @store[manifest.name]
@@ -31,7 +31,11 @@ module Riffer::Mcp::ToolFactory
31
31
  private_class_method def self.build_tool_class(manifest_name, client, td)
32
32
  prefixed = "#{sanitize_name_component(manifest_name)}__#{sanitize_name_component(td[:name])}"
33
33
 
34
+ # steep cannot type the body of a dynamically created anonymous class:
35
+ # its ivars and `self` inside define_method are unresolvable, so the
36
+ # block is ignored wholesale (cf. AuthenticatedTool.wrap_one).
34
37
  Class.new(Riffer::Tool) do
38
+ # steep:ignore:start
35
39
  @mcp_client = client
36
40
  @mcp_server_tool_name = td[:name]
37
41
  # Set @identifier directly so .identifier does not fall back to
@@ -49,6 +53,7 @@ module Riffer::Mcp::ToolFactory
49
53
  )
50
54
  text(result)
51
55
  end
56
+ # steep:ignore:end
52
57
  end
53
58
  end
54
59
  end
@@ -17,13 +17,13 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
17
17
  attr_reader :tool_calls #: Array[Riffer::Messages::Assistant::ToolCall]
18
18
 
19
19
  # Token usage data for this response.
20
- attr_reader :token_usage #: Riffer::TokenUsage?
20
+ attr_reader :token_usage #: Riffer::Providers::TokenUsage?
21
21
 
22
22
  # Parsed structured output hash, or nil when not applicable.
23
23
  attr_reader :structured_output #: Hash[Symbol, untyped]?
24
24
 
25
25
  #--
26
- #: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
26
+ #: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
27
27
  def initialize(content, id: nil, tool_calls: [], token_usage: nil, structured_output: nil)
28
28
  super(content, id: id)
29
29
  @tool_calls = tool_calls
@@ -43,6 +43,12 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
43
43
  !@structured_output.nil?
44
44
  end
45
45
 
46
+ #--
47
+ #: () -> bool
48
+ def has_tool_calls?
49
+ !@tool_calls.empty?
50
+ end
51
+
46
52
  #--
47
53
  #: (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant
48
54
  def +(other)
@@ -54,7 +60,7 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
54
60
  #--
55
61
  #: () -> Hash[Symbol, untyped]
56
62
  def to_h
57
- hash = {role: role, content: content}
63
+ hash = {role: role, content: content} #: Hash[Symbol, untyped]
58
64
  hash[:id] = id unless id.nil?
59
65
  hash[:tool_calls] = tool_calls.map(&:to_h) unless tool_calls.empty?
60
66
  hash[:token_usage] = token_usage.to_h if token_usage
@@ -40,6 +40,28 @@ class Riffer::Messages::Base
40
40
  raise NotImplementedError, "Subclasses must implement #role"
41
41
  end
42
42
 
43
+ # Whether this message carries pending tool calls. Defaults to +false+;
44
+ # +Riffer::Messages::Assistant+ overrides this when its +tool_calls+
45
+ # array is non-empty.
46
+ #
47
+ #--
48
+ #: () -> bool
49
+ def has_tool_calls?
50
+ false
51
+ end
52
+
53
+ # Merges another same-role message into this one.
54
+ #
55
+ # Raises NotImplementedError unless implemented by subclass. Mergeable
56
+ # message types (+User+, +Assistant+, +System+) override this; +Tool+
57
+ # messages are never merged.
58
+ #
59
+ #--
60
+ #: (untyped) -> Riffer::Messages::Base
61
+ def +(other)
62
+ raise NotImplementedError, "Subclasses must implement #+"
63
+ end
64
+
43
65
  private
44
66
 
45
67
  #: () -> String?
@@ -21,19 +21,19 @@ module Riffer::Messages::Converter
21
21
  convert_hash_to_message(msg)
22
22
  end
23
23
 
24
- # Converts a hash or FilePart object to a Riffer::FilePart.
24
+ # Converts a hash or FilePart object to a Riffer::Messages::FilePart.
25
25
  #
26
26
  # Accepts:
27
- # - +Riffer::FilePart+ objects (passed through)
27
+ # - +Riffer::Messages::FilePart+ objects (passed through)
28
28
  # - <tt>{url: "https://...", media_type: "..."}</tt> (URL source)
29
29
  # - <tt>{data: "...", media_type: "..."}</tt> (raw base64)
30
30
  #
31
31
  # Raises Riffer::ArgumentError if the hash format is invalid.
32
32
  #
33
33
  #--
34
- #: ((Hash[Symbol, untyped] | Riffer::FilePart)) -> Riffer::FilePart
34
+ #: ((Hash[Symbol, untyped] | Riffer::Messages::FilePart)) -> Riffer::Messages::FilePart
35
35
  def convert_to_file_part(file)
36
- return file if file.is_a?(Riffer::FilePart)
36
+ return file if file.is_a?(Riffer::Messages::FilePart)
37
37
 
38
38
  unless file.is_a?(Hash)
39
39
  raise Riffer::ArgumentError, "File must be a Hash or FilePart object, got #{file.class}"
@@ -45,9 +45,9 @@ module Riffer::Messages::Converter
45
45
  filename = file[:filename]
46
46
 
47
47
  if url
48
- Riffer::FilePart.from_url(url, media_type: media_type)
48
+ Riffer::Messages::FilePart.from_url(url, media_type: media_type)
49
49
  elsif data && media_type
50
- Riffer::FilePart.new(data: data, media_type: media_type, filename: filename)
50
+ Riffer::Messages::FilePart.new(data: data, media_type: media_type, filename: filename)
51
51
  else
52
52
  raise Riffer::ArgumentError, "File hash must include :url or :data with :media_type"
53
53
  end
@@ -10,11 +10,11 @@ require "uri"
10
10
  # - URLs (stored and passed to providers that support them via +from_url+)
11
11
  # - Raw base64 data (via +new+)
12
12
  #
13
- # file = Riffer::FilePart.from_url("https://example.com/doc.pdf", media_type: "application/pdf")
13
+ # file = Riffer::Messages::FilePart.from_url("https://example.com/doc.pdf", media_type: "application/pdf")
14
14
  # file.url? # => true
15
15
  # file.document? # => true
16
16
  #
17
- class Riffer::FilePart
17
+ class Riffer::Messages::FilePart
18
18
  MEDIA_TYPES = {
19
19
  ".jpg" => "image/jpeg",
20
20
  ".jpeg" => "image/jpeg",
@@ -63,10 +63,10 @@ class Riffer::FilePart
63
63
  # Raises Riffer::ArgumentError if media_type cannot be detected.
64
64
  #
65
65
  #--
66
- #: (String, ?media_type: String?) -> Riffer::FilePart
66
+ #: (String, ?media_type: String?) -> Riffer::Messages::FilePart
67
67
  def self.from_url(url, media_type: nil)
68
68
  unless media_type
69
- ext = ::File.extname(URI.parse(url).path).downcase
69
+ ext = ::File.extname(URI.parse(url).path.to_s).downcase
70
70
  media_type = MEDIA_TYPES[ext]
71
71
  raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" unless media_type
72
72
  end
@@ -114,7 +114,7 @@ class Riffer::FilePart
114
114
  #--
115
115
  #: () -> Hash[Symbol, untyped]
116
116
  def to_h
117
- hash = {media_type: media_type}
117
+ hash = {media_type: media_type} #: Hash[Symbol, untyped]
118
118
  hash[:data] = @data if @data
119
119
  hash[:url] = @url_string if @url_string
120
120
  hash[:filename] = filename if filename
@@ -54,7 +54,7 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
54
54
  #--
55
55
  #: () -> Hash[Symbol, untyped]
56
56
  def to_h
57
- hash = {role: role, content: content, tool_call_id: tool_call_id, name: name}
57
+ hash = {role: role, content: content, tool_call_id: tool_call_id, name: name} #: Hash[Symbol, untyped]
58
58
  hash[:id] = id unless id.nil?
59
59
  if error?
60
60
  hash[:error] = error
@@ -8,16 +8,16 @@
8
8
  # msg.content # => "Hello!"
9
9
  #
10
10
  # msg = Riffer::Messages::User.new("Describe this image", files: [file_part])
11
- # msg.files # => [#<Riffer::FilePart ...>]
11
+ # msg.files # => [#<Riffer::Messages::FilePart ...>]
12
12
  #
13
13
  class Riffer::Messages::User < Riffer::Messages::Base
14
14
  # File attachments for this message.
15
- attr_reader :files #: Array[Riffer::FilePart]
15
+ attr_reader :files #: Array[Riffer::Messages::FilePart]
16
16
 
17
17
  # Initializes a user message.
18
18
  #
19
19
  #--
20
- #: (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
20
+ #: (String, ?id: String?, ?files: Array[Riffer::Messages::FilePart]) -> void
21
21
  def initialize(content, id: nil, files: [])
22
22
  super(content, id: id)
23
23
  @files = files
@@ -38,7 +38,7 @@ class Riffer::Messages::User < Riffer::Messages::Base
38
38
  #--
39
39
  #: () -> Hash[Symbol, untyped]
40
40
  def to_h
41
- hash = {role: role, content: content}
41
+ hash = {role: role, content: content} #: Hash[Symbol, untyped]
42
42
  hash[:id] = id unless id.nil?
43
43
  hash[:files] = files.map(&:to_h) unless files.empty?
44
44
  hash
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Boolean is a sentinel type for declaring boolean parameters.
4
+ # Riffer::Params::Boolean is a sentinel type for declaring boolean parameters.
5
5
  #
6
6
  # Ruby has no +Boolean+ class (+true+ is +TrueClass+, +false+ is +FalseClass+).
7
7
  # Use this module wherever you need a single type that means "boolean":
8
8
  #
9
- # required :verbose, Riffer::Boolean
9
+ # required :verbose, Riffer::Params::Boolean
10
10
  #
11
- module Riffer::Boolean
11
+ module Riffer::Params::Boolean
12
12
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Param represents a single parameter definition.
4
+ # Riffer::Params::Param represents a single parameter definition.
5
5
  #
6
6
  # Handles type validation and JSON Schema generation for individual parameters.
7
- class Riffer::Param
7
+ class Riffer::Params::Param
8
8
  # Maps Ruby types to JSON Schema type strings
9
9
  TYPE_MAPPINGS = {
10
10
  String => "string",
11
11
  Integer => "integer",
12
12
  Float => "number",
13
- Riffer::Boolean => "boolean",
13
+ Riffer::Params::Boolean => "boolean",
14
14
  TrueClass => "boolean",
15
15
  FalseClass => "boolean",
16
16
  Array => "array",
@@ -49,7 +49,7 @@ class Riffer::Param
49
49
  def valid_type?(value)
50
50
  return true if value.nil? && !required
51
51
 
52
- if type == Riffer::Boolean || type == TrueClass || type == FalseClass
52
+ if type == Riffer::Params::Boolean || type == TrueClass || type == FalseClass
53
53
  value == true || value == false
54
54
  else
55
55
  value.is_a?(type)
@@ -80,7 +80,7 @@ class Riffer::Param
80
80
  nullable = strict && !required
81
81
 
82
82
  if nullable && enum
83
- schema = {anyOf: [{type: type_name, enum: enum}, {type: "null"}]}
83
+ schema = {anyOf: [{type: type_name, enum: enum}, {type: "null"}]} #: Hash[Symbol, untyped]
84
84
  schema[:description] = description if description
85
85
  return schema
86
86
  end
@@ -88,7 +88,7 @@ class Riffer::Param
88
88
  type = type_name
89
89
  type = [type, "null"] if nullable
90
90
 
91
- schema = {type: type}
91
+ schema = {type: type} #: Hash[Symbol, untyped]
92
92
  schema[:description] = description if description
93
93
  schema[:enum] = enum if enum
94
94