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
@@ -19,16 +19,14 @@ class Riffer::Agent
19
19
 
20
20
  extend Riffer::Helpers::ClassNameConverter
21
21
 
22
- extend Riffer::Helpers::Validations
23
-
24
- DEFAULT_MAX_STEPS: Integer
25
-
26
22
  INTERRUPT_MAX_STEPS: Symbol
27
23
 
28
- # Placeholder used to fill orphan +tool_use+ blocks when
29
- # +Riffer.config.experimental_history_healing+ is enabled and an
30
- # interrupt fires mid-tool-use.
31
- HEALING_PLACEHOLDER: ^(Riffer::Messages::Assistant::ToolCall) -> Riffer::Tools::Response
24
+ # Returns the per-class Riffer::Agent::Config value object holding every
25
+ # DSL setting. Lazily initialized on first read; each subclass has its own.
26
+ #
27
+ # --
28
+ # : () -> Riffer::Agent::Config
29
+ def self.config: () -> Riffer::Agent::Config
32
30
 
33
31
  # Gets or sets the agent identifier.
34
32
  #
@@ -75,13 +73,13 @@ class Riffer::Agent
75
73
  # Accepts a Riffer::Params instance or a block evaluated against a new Params.
76
74
  #
77
75
  # --
78
- # : (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?
79
- def self.structured_output: (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?
76
+ # : (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
77
+ def self.structured_output: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
80
78
 
81
79
  # Gets or sets the maximum number of LLM call steps in the tool-use loop.
82
80
  #
83
- # Defaults to DEFAULT_MAX_STEPS (16). Set to +Float::INFINITY+ for
84
- # unlimited steps.
81
+ # Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
82
+ # +Float::INFINITY+ for unlimited steps.
85
83
  #
86
84
  # --
87
85
  # : (?Numeric?) -> Numeric
@@ -93,35 +91,6 @@ class Riffer::Agent
93
91
  # : (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
94
92
  def self.uses_tools: (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
95
93
 
96
- # Returns the tool classes the LLM should see for this agent.
97
- #
98
- # Class-level companion to the instance #resolved_tools. Resolves the
99
- # Proc form of +uses_tools+ and appends the skill activation tool when
100
- # a +skills+ block is configured. Does not read the skills backend —
101
- # the LLM-facing tool schema reflects class-level intent, not the
102
- # runtime state of any backend.
103
- #
104
- # When +uses_tools+ is a Proc, +context+ is forwarded to it.
105
- #
106
- # The activation tool class is resolved from the agent's
107
- # <tt>skills do; activate_tool ...; end</tt> override when set, otherwise
108
- # from <tt>Riffer.config.skills.default_activate_tool</tt>.
109
- #
110
- # Each returned tool class is validated via +validate_as_tool!+, so
111
- # callers serializing this list to a provider can rely on every entry
112
- # having the metadata required for tool use (name + description).
113
- #
114
- # Raises Riffer::ArgumentError on tool name conflicts with the skill
115
- # activation tool, or when a tool class fails +validate_as_tool!+.
116
- #
117
- # --
118
- # : (?context: Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
119
- def self.resolved_tool_classes: (?context: Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
120
-
121
- # --
122
- # : (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
123
- def self.resolve_uses_tools_config: (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
124
-
125
94
  # Opts this agent into tools from all MCP registrations that share any of
126
95
  # the given tag(s).
127
96
  #
@@ -137,15 +106,12 @@ class Riffer::Agent
137
106
 
138
107
  # Gets or sets the tool runtime for this agent.
139
108
  #
140
- # Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
141
- # or a Proc.
142
- #
143
- # Inherited by subclasses. When unset, walks the ancestor chain and
144
- # falls back to the global <tt>Riffer.config.tool_runtime</tt>.
109
+ # Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance,
110
+ # or a Proc. Defaults to <tt>Riffer.config.tool_runtime</tt> when unset.
145
111
  #
146
112
  # --
147
- # : (?(singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?) -> (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?
148
- def self.tool_runtime: (?(singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?) -> (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?
113
+ # : (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
114
+ def self.tool_runtime: (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
149
115
 
150
116
  # Configures skills for this agent via a block DSL.
151
117
  #
@@ -158,8 +124,8 @@ class Riffer::Agent
158
124
  # end
159
125
  #
160
126
  # --
161
- # : () ?{ () -> void } -> Riffer::Skills::Config?
162
- def self.skills: () ?{ () -> void } -> Riffer::Skills::Config?
127
+ # : () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
128
+ def self.skills: () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
163
129
 
164
130
  # Finds an agent class by identifier.
165
131
  #
@@ -175,19 +141,21 @@ class Riffer::Agent
175
141
 
176
142
  # Generates a response using a new agent instance.
177
143
  #
178
- # See #generate for parameters and return value.
144
+ # +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
145
+ # to +#generate+.
179
146
  #
180
147
  # --
181
- # : (*untyped, **untyped) -> Riffer::Agent::Response
182
- def self.generate: (*untyped, **untyped) -> Riffer::Agent::Response
148
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
149
+ def self.generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
183
150
 
184
151
  # Streams a response using a new agent instance.
185
152
  #
186
- # See #stream for parameters and return value.
153
+ # +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
154
+ # to +#stream+.
187
155
  #
188
156
  # --
189
- # : (*untyped, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
190
- def self.stream: (*untyped, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
157
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
158
+ def self.stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
191
159
 
192
160
  # Registers a guardrail for input, output, or both phases.
193
161
  #
@@ -208,71 +176,111 @@ class Riffer::Agent
208
176
  # : (Symbol) -> Array[Hash[Symbol, untyped]]
209
177
  def self.guardrails_for: (Symbol) -> Array[Hash[Symbol, untyped]]
210
178
 
211
- # The message history for the agent.
212
- attr_reader messages: Array[Riffer::Messages::Base]
213
-
214
- # Cumulative token usage across all LLM calls.
215
- attr_reader token_usage: Riffer::TokenUsage?
179
+ # The conversation handle. See Riffer::Agent::Session.
180
+ attr_reader session: Riffer::Agent::Session
181
+
182
+ # The per-instance Riffer::Agent::Config. Either the class-level default or
183
+ # an explicit Config passed to +Agent.new(config:)+.
184
+ attr_reader config: Riffer::Agent::Config
185
+
186
+ # The system message built from the configured +instructions+, or +nil+
187
+ # when no instructions are configured. Built once at +Agent.new+ using the
188
+ # constructor +context:+ and cached. Useful for persistence flows.
189
+ attr_reader instruction_message: Riffer::Messages::System?
190
+
191
+ # The system message describing the configured skills catalog, or +nil+
192
+ # when skills are unconfigured or the catalog is empty. Built once at
193
+ # +Agent.new+ and cached.
194
+ attr_reader skills_message: Riffer::Messages::System?
195
+
196
+ # The mutable runtime context, a +Riffer::Agent::Context+ value object
197
+ # threaded into every Proc-based DSL setting, guardrail, tool runtime,
198
+ # and skills resolution, and shared with every +Riffer::Agent::Run+
199
+ # this agent executes. Exposes:
200
+ #
201
+ # - +context.skills+ — the resolved +Riffer::Skills::Context+ (when
202
+ # skills are configured), set at +Agent.new+ time.
203
+ # - +context.token_usage+ — the cumulative +Riffer::Providers::TokenUsage+,
204
+ # updated by each Run as the loop progresses.
205
+ # - +context[:key]+ / <tt>context.dig(:key)</tt> — Hash-style reads for
206
+ # caller-provided keys (e.g. <tt>context[:agent]</tt>,
207
+ # <tt>context[:tenant]</tt>). +:skills+ and +:token_usage+ are
208
+ # reserved and cannot be passed by the caller.
209
+ attr_reader context: Riffer::Agent::Context
210
+
211
+ # The resolved model name (the part after "provider/"), used as the model
212
+ # argument on every LLM call. Resolved eagerly at +Agent.new+.
213
+ attr_reader model_name: String
214
+
215
+ # The provider client. Built eagerly at +Agent.new+ from the configured
216
+ # provider class and +Config#provider_options+, then handed to every
217
+ # +Riffer::Agent::Run+ this agent executes. Public so tests can pre-queue
218
+ # responses on +Riffer::Providers::Mock+ before calling +#generate+.
219
+ attr_reader provider: Riffer::Providers::Base
220
+
221
+ # The +Riffer::Agent::StructuredOutput+ wrapping the configured schema, or +nil+
222
+ # when structured output is not configured. Resolved eagerly at +Agent.new+.
223
+ attr_reader structured_output: Riffer::Agent::StructuredOutput?
224
+
225
+ # The tool classes the LLM sees on every call this agent makes. Resolved
226
+ # eagerly at +Agent.new+ (Proc-form +uses_tools+ is called against
227
+ # +context+ once; MCP tools and the skill_activate tool are merged in).
228
+ attr_reader tools: Array[singleton(Riffer::Tool)]
229
+
230
+ # The tool runtime instance used to execute tool calls. Resolved eagerly
231
+ # at +Agent.new+ (Proc-form +tool_runtime+ is called against +context+ once).
232
+ attr_reader tool_runtime: Riffer::Tools::Runtime
216
233
 
217
234
  # Initializes a new agent.
218
235
  #
236
+ # When +session:+ is omitted, a fresh +Riffer::Agent::Session+ is built and seeded
237
+ # with the system instruction message and skills catalog (when configured),
238
+ # using +context:+. When +session:+ is provided, the agent uses it as-is —
239
+ # the caller is responsible for the session's contents (typical use case:
240
+ # cross-process resume from persisted history). With
241
+ # +Riffer.config.experimental_history_healing+ on, a provided session is
242
+ # healed at construction time so the +tool_use+ ↔ +tool_result+ invariant
243
+ # holds before the next inference call.
244
+ #
245
+ # +context:+ flows through Proc-based instructions, model, skills resolution,
246
+ # tool resolution, guardrails, and tool runtime. It is fixed for the
247
+ # lifetime of the agent.
248
+ #
219
249
  # Raises Riffer::ArgumentError if the configured model string is invalid
220
250
  # (must be "provider/model" format).
221
251
  #
222
252
  # --
223
- # : () -> void
224
- def initialize: () -> void
253
+ # : (?session: Riffer::Agent::Session?, ?context: Hash[Symbol, untyped]?, ?config: Riffer::Agent::Config?) -> void
254
+ def initialize: (?session: Riffer::Agent::Session?, ?context: Hash[Symbol, untyped]?, ?config: Riffer::Agent::Config?) -> void
225
255
 
226
256
  # Generates a response from the agent.
227
257
  #
228
- # When +Riffer.config.experimental_history_healing+ is enabled, seeded
229
- # message arrays that violate the +tool_use+ +tool_result+ invariant
230
- # are silently repaired before the run begins.
231
- #
232
- # --
233
- # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
234
- def generate: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
235
-
236
- # Streams a response from the agent.
237
- #
238
- # Raises Riffer::ArgumentError if structured output is configured.
239
- #
240
- # See +#generate+ for the +experimental_history_healing+ behavior on
241
- # seeded arrays.
242
- #
243
- # --
244
- # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
245
- def stream: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
246
-
247
- # Registers a callback to be invoked when messages are added during generation.
258
+ # Runs the inference loop via +Riffer::Agent::Run.generate+. When +prompt+
259
+ # is given, a new +Riffer::Messages::User+ is appended to the session
260
+ # (silently +on_message+ does not fire for user inputs) and then the
261
+ # loop runs. When +prompt+ is omitted, the loop runs against the current
262
+ # session — useful for resuming a persisted conversation whose last turn
263
+ # is already a user message, or for picking up pending tool calls after
264
+ # an interrupt.
248
265
  #
249
- # Raises Riffer::ArgumentError if no block is given.
266
+ # +files:+ requires +prompt+. Pass files to attach to the new user message.
250
267
  #
251
268
  # --
252
- # : () { (Riffer::Messages::Base) -> void } -> self
253
- def on_message: () { (Riffer::Messages::Base) -> void } -> self
269
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
270
+ def generate: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Riffer::Agent::Response
254
271
 
255
- # Generates the instruction system message for this agent.
256
- #
257
- # Useful for database persistence workflows where the system messages
258
- # need to be stored independently.
272
+ # Streams a response from the agent.
259
273
  #
260
- # Returns +nil+ when no instructions are configured.
274
+ # Runs the inference loop via +Riffer::Agent::Run.stream+, returning an
275
+ # +Enumerator+ of +Riffer::StreamEvents+.
261
276
  #
262
- # --
263
- # : (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
264
- def generate_instruction_message: (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
265
-
266
- # Generates the skills catalog system message for this agent.
267
- #
268
- # Useful for database persistence workflows where the system messages
269
- # need to be stored independently.
277
+ # Raises Riffer::ArgumentError if structured output is configured.
270
278
  #
271
- # Returns +nil+ when no skills are configured or the catalog is empty.
279
+ # See +#generate+ for prompt/files semantics.
272
280
  #
273
281
  # --
274
- # : (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
275
- def generate_skills_message: (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
282
+ # : (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
283
+ def stream: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> Enumerator[Riffer::StreamEvents::Base, void]
276
284
 
277
285
  # Interrupts the agent loop.
278
286
  #
@@ -290,215 +298,57 @@ class Riffer::Agent
290
298
  # : (?(String | Symbol)?) -> void
291
299
  def interrupt!: (?(String | Symbol)?) -> void
292
300
 
293
- # Returns the message with the given id, or +nil+ when no message matches.
294
- #
295
- # --
296
- # : (String) -> Riffer::Messages::Base?
297
- def message_by_id: (String) -> Riffer::Messages::Base?
298
-
299
- # Returns the +Riffer::Messages::Tool+ message that satisfies +tool_call_id+,
300
- # or +nil+ when no such tool result exists in history.
301
- #
302
- # --
303
- # : (String) -> Riffer::Messages::Tool?
304
- # TODO: Replace with rfind when minimum Ruby is 4.0+
305
- # rubocop:disable Style/ReverseFind
306
- def tool_message_for: (String) -> Riffer::Messages::Tool?
307
-
308
- # Returns the most recent +Riffer::Messages::Assistant+ in history, or
309
- # +nil+ when no assistant message has been recorded yet.
310
- #
311
- # --
312
- # : () -> Riffer::Messages::Assistant?
313
- # TODO: Replace with rfind when minimum Ruby is 4.0+
314
- # rubocop:disable Style/ReverseFind
315
- def last_assistant: () -> Riffer::Messages::Assistant?
316
-
317
- # Returns the call_ids of every +tool_call+ on any assistant message that
318
- # has no matching +Riffer::Messages::Tool+ result anywhere in history.
319
- #
320
- # Zero-cost validation hook for callers that want to check the
321
- # +tool_use+ ↔ +tool_result+ invariant before mutating or persisting.
322
- #
323
- # --
324
- # : () -> Array[String]
325
- def orphaned_tool_call_ids: () -> Array[String]
326
-
327
- # Replaces the content of an assistant message in place. Preserves +id+,
328
- # +tool_calls+, +token_usage+, and +structured_output+. Lookup is by id.
329
- #
330
- # When +content+ is empty, delegates to +remove_message+ — including the
331
- # cascade that drops dependent +Riffer::Messages::Tool+ children.
332
- #
333
- # Raises Riffer::ArgumentError when no assistant message has the given id.
334
- #
335
- # --
336
- # : (id: String, content: String) -> Riffer::Messages::Base?
337
- def replace_assistant_content: (id: String, content: String) -> Riffer::Messages::Base?
338
-
339
- # Removes a message by id. When the target is an assistant message that
340
- # carries +tool_calls+, every +Riffer::Messages::Tool+ result whose
341
- # +tool_call_id+ matches one of those calls is removed atomically — keeping
342
- # the +tool_use+ ↔ +tool_result+ invariant intact.
343
- #
344
- # Raises Riffer::ArgumentError when called on a +Riffer::Messages::Tool+
345
- # message — that would orphan the parent's +tool_use+. Use
346
- # +replace_tool_result+ instead.
347
- #
348
- # Returns the removed message, or +nil+ when no message has the given id
349
- # (idempotent).
350
- #
351
- # --
352
- # : (id: String) -> Riffer::Messages::Base?
353
- def remove_message: (id: String) -> Riffer::Messages::Base?
354
-
355
- # Replaces a tool result's content (and optional error fields) in place.
356
- # Lookup is by +tool_call_id+. Preserves the existing message's +name+ and
357
- # +id+.
358
- #
359
- # Raises Riffer::ArgumentError when no Tool message exists for the given
360
- # +tool_call_id+.
361
- #
362
- # --
363
- # : (tool_call_id: String, content: String, ?error: String?, ?error_type: Symbol?) -> Riffer::Messages::Tool
364
- def replace_tool_result: (tool_call_id: String, content: String, ?error: String?, ?error_type: Symbol?) -> Riffer::Messages::Tool
365
-
366
301
  private
367
302
 
368
- # Fills any orphaned +tool_use+ in history with the +HEALING_PLACEHOLDER+
369
- # response. Each placeholder Tool message is inserted immediately after
370
- # its parent assistant message. Returns the array of call_ids that were
371
- # filled, in order; +[]+ when there are no orphans.
372
- #
373
- # Bypasses +on_message+ — placeholders are not inference output.
374
- # Consumers learn that healing happened via the structured
375
- # +Riffer::Agent::Response#healed_tool_call_ids+ field (and the streaming
376
- # +Interrupt+ event's matching field).
377
- #
378
- # --
379
- # : () -> Array[String]
380
- def heal_orphan_tool_calls: () -> Array[String]
381
-
382
- # --
383
- # : (?Array[Riffer::Guardrails::Modification]) -> Riffer::Agent::Response
384
- def run_generate_loop: (?Array[Riffer::Guardrails::Modification]) -> Riffer::Agent::Response
385
-
386
- # --
387
- # : (Riffer::Messages::Base) -> void
388
- def add_message: (Riffer::Messages::Base) -> void
389
-
390
303
  # --
391
- # : (Riffer::TokenUsage?) -> void
392
- def track_token_usage: (Riffer::TokenUsage?) -> void
304
+ # : () -> Riffer::Messages::System?
305
+ def build_instruction_message: () -> Riffer::Messages::System?
393
306
 
394
307
  # --
395
- # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
396
- def initialize_messages: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
308
+ # : () -> Riffer::Messages::System?
309
+ def build_skills_message: () -> Riffer::Messages::System?
397
310
 
398
- # --
399
- # : (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
400
- def validate_seed_ids!: (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
401
-
402
- # Repairs a seeded message array so the +tool_use+ ↔ +tool_result+
403
- # invariant holds. Drops orphaned tool exchanges (assistant +tool_call+
404
- # with no matching Tool result) and parentless Tool messages. Returns a
405
- # new array; the input is not mutated.
311
+ # Resolves +Config#model+ to a "provider/model" string (calling the Proc
312
+ # form against +@context+), parses it, and looks up the provider class.
406
313
  #
407
- # Pending tool_calls on the resume boundary — the last assistant whose
408
- # tail is purely Tool results (or empty) are preserved. They get swept
409
- # up by +execute_pending_tool_calls+ at the start of the next
410
- # generate/stream call.
411
- #
412
- # Only invoked when +Riffer.config.experimental_history_healing+ is on.
314
+ # Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
315
+ # an invalid model string or an unregistered provider.
413
316
  #
414
317
  # --
415
- # : (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
416
- def heal_seeded_history: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
417
-
418
- # --
419
- # : (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
420
- def build_instruction_message: (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
421
-
422
- # --
423
- # : (?Riffer::Skills::Context?) -> Riffer::Messages::System?
424
- def build_skills_message: (?Riffer::Skills::Context?) -> Riffer::Messages::System?
425
-
426
- # --
427
- # : () -> Integer
428
- def count_assistant_messages: () -> Integer
429
-
430
- # --
431
- # : (Enumerator::Yielder) -> void
432
- def run_stream_loop: (Enumerator::Yielder) -> void
433
-
434
- # --
435
- # : () -> Riffer::Messages::Assistant
436
- def call_llm: () -> Riffer::Messages::Assistant
437
-
438
- # --
439
- # : () -> Enumerator[Riffer::StreamEvents::Base, void]
440
- def call_llm_stream: () -> Enumerator[Riffer::StreamEvents::Base, void]
441
-
442
- # --
443
- # : () -> Riffer::Providers::Base
444
- def provider_instance: () -> Riffer::Providers::Base
318
+ # : () -> [singleton(Riffer::Providers::Base), String]
319
+ def resolve_provider_and_model: () -> [ singleton(Riffer::Providers::Base), String ]
445
320
 
321
+ # Resolves the skills backend, lists skills, and selects an adapter.
322
+ # Returns nil if skills are unconfigured or the backend is empty.
323
+ #
446
324
  # --
447
- # : (Riffer::Messages::Assistant) -> bool
448
- def has_tool_calls?: (Riffer::Messages::Assistant) -> bool
325
+ # : (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
326
+ def resolve_skills: (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
449
327
 
450
328
  # --
451
- # : (Riffer::Messages::Assistant) -> void
452
- def execute_tool_calls: (Riffer::Messages::Assistant) -> void
329
+ # : () -> Riffer::Agent::StructuredOutput?
330
+ def resolve_structured_output: () -> Riffer::Agent::StructuredOutput?
453
331
 
454
- # Executes tool calls left unfinished by a prior interrupt.
332
+ # Resolves the full tool catalog for the agent:
455
333
  #
456
- # When an interrupt fires mid-way through tool execution, some tool calls
457
- # from the last assistant message may not have been executed yet. This
458
- # method detects those gaps by comparing the tool call ids requested by the
459
- # last assistant message against the tool result messages that follow it,
460
- # then executes any that are missing.
334
+ # - The configured +uses_tools+ value (Proc-form resolved against +context+).
335
+ # - The skill activation tool, when a +skills+ block is configured. The
336
+ # activation tool class comes from the per-agent +skills do; activate_tool ...; end+
337
+ # override when set, otherwise from +Riffer.config.skills.default_activate_tool+.
338
+ # - All MCP tools matching any +use_mcp+ tag, optionally wrapped in
339
+ # AuthenticatedTool when +Riffer.config.mcp.credentials+ is configured.
461
340
  #
462
- # --
463
- # : () -> void
464
- # Executes tool calls from the last assistant message that don't yet
465
- # have a corresponding tool result. Safe to call unconditionally —
466
- # returns immediately when there is nothing pending.
467
- def execute_pending_tool_calls: () -> void
468
-
469
- # Returns +[assistant, pending_tool_calls]+ for the last assistant message.
470
- # When there is no assistant message or no pending calls, the second
471
- # element is an empty array.
341
+ # Raises Riffer::ArgumentError on tool name conflicts with the skill
342
+ # activation tool, on duplicate tool names across sources, or on tool
343
+ # classes missing required metadata (description, params).
472
344
  #
473
345
  # --
474
- # : () -> [untyped, Array[Riffer::Messages::Assistant::ToolCall]]
475
- def pending_tool_calls: () -> [ untyped, Array[Riffer::Messages::Assistant::ToolCall] ]
476
-
477
- # --
478
- # : () -> void
479
- def prepare_run: () -> void
480
-
481
- # --
482
- # : (untyped) -> void
483
- def parse_model_string!: (untyped) -> void
484
-
485
- # --
486
- # : () -> void
487
- def clear_resolved_model: () -> void
488
-
489
- # --
490
- # : (?Hash[Symbol, untyped]?) -> String?
491
- def generate_instructions: (?Hash[Symbol, untyped]?) -> String?
492
-
493
- attr_reader resolved_model: String?
494
-
495
- # --
496
- # : () -> String
497
- def resolve_model: () -> String
346
+ # : () -> Array[singleton(Riffer::Tool)]
347
+ def resolve_tools: () -> Array[singleton(Riffer::Tool)]
498
348
 
499
349
  # --
500
- # : () -> Array[singleton(Riffer::Tool)]
501
- def resolve_uses_tools_config: () -> Array[singleton(Riffer::Tool)]
350
+ # : () -> Riffer::Tools::Runtime
351
+ def resolve_tool_runtime: () -> Riffer::Tools::Runtime
502
352
 
503
353
  # --
504
354
  # : () -> Array[singleton(Riffer::Tool)]
@@ -509,60 +359,11 @@ class Riffer::Agent
509
359
  # : (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
510
360
  def gather_mcp_registrations_with_tags: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
511
361
 
512
- # : (Riffer::Mcp::Registration, Array[Symbol], Proc?, Hash[Symbol, untyped]) -> Array[singleton(Riffer::Tool)]
513
- def mcp_tools_for_registration: (Riffer::Mcp::Registration, Array[Symbol], Proc?, Hash[Symbol, untyped]) -> Array[singleton(Riffer::Tool)]
362
+ # : (Riffer::Mcp::Registration, Array[Symbol], (^(manifest: Riffer::Mcp::Manifest, matched_tags: Array[Symbol], context: Riffer::Agent::Context) -> Hash[Symbol, untyped]?)?, Riffer::Agent::Context) -> Array[singleton(Riffer::Tool)]
363
+ def mcp_tools_for_registration: (Riffer::Mcp::Registration, Array[Symbol], (^(manifest: Riffer::Mcp::Manifest, matched_tags: Array[Symbol], context: Riffer::Agent::Context) -> Hash[Symbol, untyped]?)?, Riffer::Agent::Context) -> Array[singleton(Riffer::Tool)]
514
364
 
515
365
  # Raises if two or more tool classes share the same +.name+ (ambiguous dispatch).
516
366
  #
517
367
  # : (Array[singleton(Riffer::Tool)]) -> void
518
368
  def assert_distinct_tool_names!: (Array[singleton(Riffer::Tool)]) -> void
519
-
520
- # : () -> Array[singleton(Riffer::Tool)]
521
- def resolved_tools: () -> Array[singleton(Riffer::Tool)]
522
-
523
- # --
524
- # : () -> Riffer::ToolRuntime
525
- def resolve_tool_runtime: () -> Riffer::ToolRuntime
526
-
527
- # Resolves the skills backend, lists skills, and selects an adapter.
528
- #
529
- # Returns nil if skills are not configured or empty.
530
- # Does not mutate instance state — callers are responsible for
531
- # assigning the returned context.
532
- #
533
- # --
534
- # : (?Hash[Symbol, untyped]?) -> Riffer::Skills::Context?
535
- def resolve_skills: (?Hash[Symbol, untyped]?) -> Riffer::Skills::Context?
536
-
537
- # --
538
- # : () -> singleton(Riffer::Providers::Base)
539
- def provider_class: () -> singleton(Riffer::Providers::Base)
540
-
541
- # --
542
- # : () -> Riffer::Messages::Assistant?
543
- def extract_final_response: () -> Riffer::Messages::Assistant?
544
-
545
- # --
546
- # : () -> [Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
547
- def run_before_guardrails: () -> [ Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification] ]
548
-
549
- # --
550
- # : (Riffer::Messages::Assistant) -> [untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
551
- def run_after_guardrails: (Riffer::Messages::Assistant) -> [ untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification] ]
552
-
553
- # --
554
- # : (Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
555
- def validate_structured_output: (Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
556
-
557
- # --
558
- # : () -> Riffer::StructuredOutput?
559
- def resolve_structured_output: () -> Riffer::StructuredOutput?
560
-
561
- # --
562
- # : () -> Hash[Symbol, untyped]
563
- def merged_model_options: () -> Hash[Symbol, untyped]
564
-
565
- # --
566
- # : (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?healed_tool_call_ids: Array[String]) -> Riffer::Agent::Response
567
- def build_response: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?healed_tool_call_ids: Array[String]) -> Riffer::Agent::Response
568
369
  end
@@ -11,6 +11,8 @@
11
11
  #
12
12
  # Riffer.config.anthropic.api_key = "sk-ant-..."
13
13
  #
14
+ # Riffer.config.openrouter.api_key = "sk-or-..."
15
+ #
14
16
  # Riffer.config.evals.judge_model = "anthropic/claude-sonnet-4-20250514"
15
17
  class Riffer::Config
16
18
  class AmazonBedrock < Struct[untyped]
@@ -56,6 +58,13 @@ class Riffer::Config
56
58
  | ({ ?api_key: untyped }) -> instance
57
59
  end
58
60
 
61
+ class OpenRouter < Struct[untyped]
62
+ attr_accessor api_key(): untyped
63
+
64
+ def self.new: (?api_key: untyped) -> instance
65
+ | ({ ?api_key: untyped }) -> instance
66
+ end
67
+
59
68
  class Evals < Struct[untyped]
60
69
  attr_accessor judge_model(): untyped
61
70
 
@@ -130,6 +139,9 @@ class Riffer::Config
130
139
  # OpenAI configuration (Struct with +api_key+).
131
140
  attr_reader openai: Riffer::Config::OpenAI
132
141
 
142
+ # OpenRouter configuration (Struct with +api_key+).
143
+ attr_reader openrouter: Riffer::Config::OpenRouter
144
+
133
145
  # Evals configuration (Struct with +judge_model+).
134
146
  attr_reader evals: Riffer::Config::Evals
135
147
 
@@ -146,9 +158,9 @@ class Riffer::Config
146
158
 
147
159
  # Global tool runtime configuration (experimental).
148
160
  #
149
- # Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
150
- # or a Proc. Defaults to <tt>Riffer::ToolRuntime::Inline.new</tt>.
151
- attr_reader tool_runtime: singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc
161
+ # Accepts a Riffer::Tools::Runtime subclass, a Riffer::Tools::Runtime instance,
162
+ # or a Proc. Defaults to <tt>Riffer::Tools::Runtime::Inline.new</tt>.
163
+ attr_reader tool_runtime: singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc
152
164
 
153
165
  # Sets the global tool runtime.
154
166
  #
@@ -156,8 +168,8 @@ class Riffer::Config
156
168
  # (ToolRuntime subclass, ToolRuntime instance, or Proc).
157
169
  #
158
170
  # --
159
- # : ((singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)) -> void
160
- def tool_runtime=: (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc) -> void
171
+ # : ((singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)) -> void
172
+ def tool_runtime=: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc) -> void
161
173
 
162
174
  # Skills-related global configuration. Returns a Riffer::Config::Skills
163
175
  # object — see <tt>Riffer.config.skills.default_activate_tool</tt>.