riffer 0.28.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) 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 +53 -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 +26 -0
  9. data/README.md +17 -18
  10. data/Steepfile +8 -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 +127 -0
  26. data/lib/riffer/agent/response.rb +2 -0
  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 +270 -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 +236 -923
  33. data/lib/riffer/config.rb +14 -7
  34. data/lib/riffer/evals/evaluator.rb +18 -3
  35. data/lib/riffer/evals/judge.rb +7 -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 +3 -1
  43. data/lib/riffer/mcp/registration.rb +6 -3
  44. data/lib/riffer/mcp/registry.rb +6 -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} +7 -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 +37 -31
  56. data/lib/riffer/providers/anthropic.rb +39 -36
  57. data/lib/riffer/providers/base.rb +12 -9
  58. data/lib/riffer/providers/gemini.rb +19 -12
  59. data/lib/riffer/providers/mock.rb +45 -13
  60. data/lib/riffer/providers/open_ai.rb +34 -29
  61. data/lib/riffer/providers/open_router.rb +325 -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 +6 -3
  66. data/lib/riffer/runner/sequential.rb +1 -1
  67. data/lib/riffer/runner/threaded.rb +3 -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 +6 -1
  71. data/lib/riffer/skills/context.rb +6 -3
  72. data/lib/riffer/skills/filesystem_backend.rb +10 -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 +1 -1
  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/tools/response.rb +2 -0
  80. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  81. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  82. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  83. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +11 -9
  84. data/lib/riffer/{toolable.rb → tools/toolable.rb} +19 -9
  85. data/lib/riffer/version.rb +1 -1
  86. data/lib/riffer.rb +4 -1
  87. data/sig/_private/anthropic.rbs +16 -0
  88. data/sig/_private/async.rbs +24 -0
  89. data/sig/_private/aws-sdk-core/seahorse_request_context.rbs +7 -0
  90. data/sig/_private/aws-sdk-core/static_token_provider.rbs +5 -0
  91. data/sig/_private/mcp.rbs +22 -0
  92. data/sig/_private/openai.rbs +29 -0
  93. data/sig/_private/riffer/providers/amazon_bedrock.rbs +4 -0
  94. data/sig/_private/riffer/providers/anthropic.rbs +4 -0
  95. data/sig/_private/riffer/providers/open_ai.rbs +4 -0
  96. data/sig/_private/riffer/providers/open_router.rbs +4 -0
  97. data/sig/_private/zeitwerk.rbs +12 -0
  98. data/sig/generated/riffer/agent/config.rbs +119 -0
  99. data/sig/generated/riffer/agent/context.rbs +93 -0
  100. data/sig/generated/riffer/agent/response.rbs +2 -0
  101. data/sig/generated/riffer/agent/run.rbs +144 -0
  102. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  103. data/sig/generated/riffer/agent/session.rbs +147 -0
  104. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  105. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  106. data/sig/generated/riffer/agent.rbs +145 -342
  107. data/sig/generated/riffer/config.rbs +17 -5
  108. data/sig/generated/riffer/evals/evaluator.rbs +8 -0
  109. data/sig/generated/riffer/evals/judge.rbs +10 -2
  110. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  111. data/sig/generated/riffer/helpers.rbs +0 -1
  112. data/sig/generated/riffer/mcp/client.rbs +2 -0
  113. data/sig/generated/riffer/mcp/registration.rbs +6 -0
  114. data/sig/generated/riffer/mcp/registry.rbs +4 -0
  115. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  116. data/sig/generated/riffer/messages/base.rbs +18 -0
  117. data/sig/generated/riffer/messages/converter.rbs +4 -4
  118. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +7 -5
  119. data/sig/generated/riffer/messages/user.rbs +4 -4
  120. data/sig/generated/riffer/params/boolean.rbs +10 -0
  121. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  122. data/sig/generated/riffer/params.rbs +15 -15
  123. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  124. data/sig/generated/riffer/providers/anthropic.rbs +12 -12
  125. data/sig/generated/riffer/providers/base.rbs +12 -10
  126. data/sig/generated/riffer/providers/gemini.rbs +10 -4
  127. data/sig/generated/riffer/providers/mock.rbs +31 -5
  128. data/sig/generated/riffer/providers/open_ai.rbs +10 -10
  129. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  130. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  131. data/sig/generated/riffer/providers.rbs +1 -0
  132. data/sig/generated/riffer/runner/fibers.rbs +4 -2
  133. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  134. data/sig/generated/riffer/runner/threaded.rbs +4 -2
  135. data/sig/generated/riffer/runner.rbs +2 -2
  136. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  137. data/sig/generated/riffer/skills/config.rbs +9 -1
  138. data/sig/generated/riffer/skills/context.rbs +6 -2
  139. data/sig/generated/riffer/skills/filesystem_backend.rbs +4 -0
  140. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  141. data/sig/generated/riffer/tool.rbs +5 -5
  142. data/sig/generated/riffer/tools/response.rbs +2 -0
  143. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  144. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  145. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  146. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +14 -12
  147. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +18 -6
  148. data/sig/generated/riffer.rbs +2 -0
  149. data/sig/manifest.yaml +3 -0
  150. data/sig/manual/riffer/agent/run.rbs +5 -0
  151. data/sig/manual/riffer/helpers/call_or_value.rbs +5 -0
  152. data/sig/manual/riffer/tools/toolable.rbs +6 -0
  153. metadata +59 -33
  154. data/lib/riffer/core.rb +0 -28
  155. data/lib/riffer/helpers/validations.rb +0 -18
  156. data/sig/generated/riffer/boolean.rbs +0 -10
  157. data/sig/generated/riffer/core.rbs +0 -19
  158. 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
 
@@ -200,9 +206,10 @@ class Riffer::Config
200
206
  @azure_openai = AzureOpenAI.new
201
207
  @gemini = Gemini.new
202
208
  @openai = OpenAI.new
209
+ @openrouter = OpenRouter.new
203
210
  @evals = Evals.new
204
211
  @mcp = Mcp.new(credentials: nil, discovery_runner: Riffer::Runner::Sequential.new)
205
- @tool_runtime = Riffer::ToolRuntime::Inline.new
212
+ @tool_runtime = Riffer::Tools::Runtime::Inline.new
206
213
  @skills = Skills.new
207
214
  @message_id_strategy = :none
208
215
  @experimental_history_healing = false
@@ -16,6 +16,11 @@
16
16
  # end
17
17
  #
18
18
  class Riffer::Evals::Evaluator
19
+ # @rbs self.@instructions: String?
20
+ # @rbs self.@higher_is_better: bool?
21
+ # @rbs self.@judge_model: String?
22
+ # @rbs @judge: Riffer::Evals::Judge?
23
+
19
24
  class << self
20
25
  # Gets or sets the evaluation instructions (criteria and scoring rubric).
21
26
  #
@@ -31,7 +36,11 @@ class Riffer::Evals::Evaluator
31
36
  #--
32
37
  #: (?bool?) -> bool
33
38
  def higher_is_better(value = nil)
34
- return @higher_is_better.nil? || @higher_is_better if value.nil?
39
+ if value.nil?
40
+ current = @higher_is_better
41
+ return true if current.nil?
42
+ return current
43
+ end
35
44
  @higher_is_better = value
36
45
  end
37
46
 
@@ -87,8 +96,14 @@ class Riffer::Evals::Evaluator
87
96
  return input if input.is_a?(String)
88
97
 
89
98
  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
99
+ if msg.is_a?(Hash)
100
+ hash = msg #: Hash[untyped, untyped]
101
+ role = hash[:role] || hash["role"]
102
+ content = hash[:content] || hash["content"]
103
+ else
104
+ role = msg.role
105
+ content = msg.content
106
+ end
92
107
  "#{role}: #{content}"
93
108
  end.join("\n\n")
94
109
  end
@@ -19,6 +19,11 @@ require "json"
19
19
  # result[:reason] # => "The response is relevant..."
20
20
  #
21
21
  class Riffer::Evals::Judge
22
+ # @rbs @provider_options: Hash[Symbol, untyped]
23
+ # @rbs @provider_instance: Riffer::Providers::Base?
24
+ # @rbs @provider_name: String?
25
+ # @rbs @model_name: String?
26
+
22
27
  # Internal tool for structured evaluation output.
23
28
  class EvaluationTool < Riffer::Tool
24
29
  identifier "evaluation"
@@ -30,7 +35,7 @@ class Riffer::Evals::Judge
30
35
  end
31
36
 
32
37
  #--
33
- #: (context: Hash[Symbol, untyped]?, score: Float, reason: String) -> Riffer::Tools::Response
38
+ #: (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
34
39
  def call(context:, score:, reason:)
35
40
  json({score: score, reason: reason})
36
41
  end
@@ -94,7 +99,7 @@ class Riffer::Evals::Judge
94
99
  #--
95
100
  #: (input: String, output: String, ?ground_truth: String?) -> String
96
101
  def build_user_message(input:, output:, ground_truth: nil)
97
- parts = []
102
+ parts = [] #: Array[String]
98
103
  parts << "## Input\n\n#{input}"
99
104
  parts << "## Output\n\n#{output}"
100
105
  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
@@ -16,6 +16,8 @@
16
16
  class Riffer::Mcp::Client
17
17
  include Riffer::Helpers::Dependencies
18
18
 
19
+ # @rbs @client: untyped
20
+
19
21
  #--
20
22
  #: (endpoint: String, ?headers: (Hash[String, String] | Proc), ?client: untyped?) -> void
21
23
  def initialize(endpoint:, headers: {}, client: nil)
@@ -23,7 +25,7 @@ class Riffer::Mcp::Client
23
25
  depends_on "faraday"
24
26
 
25
27
  @client = client || begin
26
- resolved_headers = headers.is_a?(Proc) ? headers.call : headers
28
+ resolved_headers = Riffer::Helpers::CallOrValue.resolve(headers)
27
29
  transport = MCP::Client::HTTP.new(url: endpoint, headers: resolved_headers)
28
30
  MCP::Client.new(transport: transport)
29
31
  end
@@ -7,6 +7,10 @@
7
7
  # +tools/list+ call, then generates tool classes.
8
8
  #
9
9
  class Riffer::Mcp::Registration
10
+ # @rbs @cancelled: bool
11
+ # @rbs @tools: Array[singleton(Riffer::Tool)]
12
+ # @rbs @mutex: Thread::Mutex
13
+
10
14
  # The manifest that describes this server.
11
15
  attr_reader :manifest #: Riffer::Mcp::Manifest
12
16
 
@@ -23,7 +27,7 @@ class Riffer::Mcp::Registration
23
27
  def initialize(manifest)
24
28
  @manifest = manifest
25
29
  @cancelled = false
26
- @tools = []
30
+ @tools = [] #: Array[singleton(Riffer::Tool)]
27
31
  @mutex = Mutex.new
28
32
  run_discovery
29
33
  end
@@ -62,8 +66,7 @@ class Riffer::Mcp::Registration
62
66
  tools = Riffer::Mcp::ToolFactory.build(@manifest.name, client, tool_defs)
63
67
 
64
68
  @mutex.synchronize do
65
- next if @cancelled
66
- @tools = tools.freeze
69
+ @tools = tools.freeze unless @cancelled
67
70
  end
68
71
  end
69
72
  end
@@ -6,6 +6,9 @@
6
6
  # Keyed by manifest name. All public methods are mutex-guarded.
7
7
  #
8
8
  module Riffer::Mcp::Registry
9
+ # @rbs self.@mutex: Thread::Mutex
10
+ # @rbs self.@store: Hash[String, Riffer::Mcp::Registration]
11
+
9
12
  @mutex = Mutex.new
10
13
  @store = {} #: Hash[String, Riffer::Mcp::Registration]
11
14
 
@@ -18,7 +21,9 @@ module Riffer::Mcp::Registry
18
21
  #--
19
22
  #: ((Hash[Symbol, untyped] | Riffer::Mcp::Manifest)) -> Riffer::Mcp::Registration
20
23
  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)
24
+ # steep cannot verify that an untyped Hash splat supplies Manifest's
25
+ # required name:/endpoint: keywords; Manifest validates them at runtime.
26
+ manifest = manifest_or_hash.is_a?(Riffer::Mcp::Manifest) ? manifest_or_hash : Riffer::Mcp::Manifest.new(**manifest_or_hash) # steep:ignore InsufficientKeywordArguments
22
27
  registration = Riffer::Mcp::Registration.new(manifest)
23
28
  old = @mutex.synchronize do
24
29
  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,13 @@ 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
+ # @rbs @url_string: String?
19
+
18
20
  MEDIA_TYPES = {
19
21
  ".jpg" => "image/jpeg",
20
22
  ".jpeg" => "image/jpeg",
@@ -63,10 +65,10 @@ class Riffer::FilePart
63
65
  # Raises Riffer::ArgumentError if media_type cannot be detected.
64
66
  #
65
67
  #--
66
- #: (String, ?media_type: String?) -> Riffer::FilePart
68
+ #: (String, ?media_type: String?) -> Riffer::Messages::FilePart
67
69
  def self.from_url(url, media_type: nil)
68
70
  unless media_type
69
- ext = ::File.extname(URI.parse(url).path).downcase
71
+ ext = ::File.extname(URI.parse(url).path.to_s).downcase
70
72
  media_type = MEDIA_TYPES[ext]
71
73
  raise Riffer::ArgumentError, "Cannot detect media type from URL; provide media_type explicitly" unless media_type
72
74
  end
@@ -114,7 +116,7 @@ class Riffer::FilePart
114
116
  #--
115
117
  #: () -> Hash[Symbol, untyped]
116
118
  def to_h
117
- hash = {media_type: media_type}
119
+ hash = {media_type: media_type} #: Hash[Symbol, untyped]
118
120
  hash[:data] = @data if @data
119
121
  hash[:url] = @url_string if @url_string
120
122
  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