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
@@ -19,12 +19,15 @@ 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
 
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
30
+
28
31
  # Gets or sets the agent identifier.
29
32
  #
30
33
  # --
@@ -70,13 +73,13 @@ class Riffer::Agent
70
73
  # Accepts a Riffer::Params instance or a block evaluated against a new Params.
71
74
  #
72
75
  # --
73
- # : (?Riffer::Params?) ?{ () -> void } -> Riffer::Params?
74
- 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?
75
78
 
76
79
  # Gets or sets the maximum number of LLM call steps in the tool-use loop.
77
80
  #
78
- # Defaults to DEFAULT_MAX_STEPS (16). Set to +Float::INFINITY+ for
79
- # unlimited steps.
81
+ # Defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS (16). Set to
82
+ # +Float::INFINITY+ for unlimited steps.
80
83
  #
81
84
  # --
82
85
  # : (?Numeric?) -> Numeric
@@ -88,35 +91,6 @@ class Riffer::Agent
88
91
  # : (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
89
92
  def self.uses_tools: (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
90
93
 
91
- # Returns the tool classes the LLM should see for this agent.
92
- #
93
- # Class-level companion to the instance #resolved_tools. Resolves the
94
- # Proc form of +uses_tools+ and appends the skill activation tool when
95
- # a +skills+ block is configured. Does not read the skills backend —
96
- # the LLM-facing tool schema reflects class-level intent, not the
97
- # runtime state of any backend.
98
- #
99
- # When +uses_tools+ is a Proc, +context+ is forwarded to it.
100
- #
101
- # The activation tool class is resolved from the agent's
102
- # <tt>skills do; activate_tool ...; end</tt> override when set, otherwise
103
- # from <tt>Riffer.config.skills.default_activate_tool</tt>.
104
- #
105
- # Each returned tool class is validated via +validate_as_tool!+, so
106
- # callers serializing this list to a provider can rely on every entry
107
- # having the metadata required for tool use (name + description).
108
- #
109
- # Raises Riffer::ArgumentError on tool name conflicts with the skill
110
- # activation tool, or when a tool class fails +validate_as_tool!+.
111
- #
112
- # --
113
- # : (?context: Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
114
- def self.resolved_tool_classes: (?context: Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
115
-
116
- # --
117
- # : (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
118
- def self.resolve_uses_tools_config: (Hash[Symbol, untyped]?) -> Array[singleton(Riffer::Tool)]
119
-
120
94
  # Opts this agent into tools from all MCP registrations that share any of
121
95
  # the given tag(s).
122
96
  #
@@ -132,15 +106,12 @@ class Riffer::Agent
132
106
 
133
107
  # Gets or sets the tool runtime for this agent.
134
108
  #
135
- # Accepts a Riffer::ToolRuntime subclass, a Riffer::ToolRuntime instance,
136
- # or a Proc.
137
- #
138
- # Inherited by subclasses. When unset, walks the ancestor chain and
139
- # 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.
140
111
  #
141
112
  # --
142
- # : (?(singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?) -> (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)?
143
- 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)
144
115
 
145
116
  # Configures skills for this agent via a block DSL.
146
117
  #
@@ -153,8 +124,8 @@ class Riffer::Agent
153
124
  # end
154
125
  #
155
126
  # --
156
- # : () ?{ () -> void } -> Riffer::Skills::Config?
157
- 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?
158
129
 
159
130
  # Finds an agent class by identifier.
160
131
  #
@@ -170,19 +141,21 @@ class Riffer::Agent
170
141
 
171
142
  # Generates a response using a new agent instance.
172
143
  #
173
- # See #generate for parameters and return value.
144
+ # +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
145
+ # to +#generate+.
174
146
  #
175
147
  # --
176
- # : (*untyped, **untyped) -> Riffer::Agent::Response
177
- 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
178
150
 
179
151
  # Streams a response using a new agent instance.
180
152
  #
181
- # See #stream for parameters and return value.
153
+ # +context:+ is threaded into +new+; +prompt+ and +files:+ are forwarded
154
+ # to +#stream+.
182
155
  #
183
156
  # --
184
- # : (*untyped, **untyped) -> Enumerator[Riffer::StreamEvents::Base, void]
185
- 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]
186
159
 
187
160
  # Registers a guardrail for input, output, or both phases.
188
161
  #
@@ -203,70 +176,124 @@ class Riffer::Agent
203
176
  # : (Symbol) -> Array[Hash[Symbol, untyped]]
204
177
  def self.guardrails_for: (Symbol) -> Array[Hash[Symbol, untyped]]
205
178
 
206
- # The message history for the agent.
207
- attr_reader messages: Array[Riffer::Messages::Base]
208
-
209
- # Cumulative token usage across all LLM calls.
210
- 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
211
233
 
212
234
  # Initializes a new agent.
213
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
+ #
214
249
  # Raises Riffer::ArgumentError if the configured model string is invalid
215
250
  # (must be "provider/model" format).
216
251
  #
217
252
  # --
218
- # : () -> void
219
- 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
220
255
 
221
256
  # Generates a response from the agent.
222
257
  #
223
- # --
224
- # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
225
- def generate: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
226
-
227
- # Streams a response from the agent.
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.
228
265
  #
229
- # Raises Riffer::ArgumentError if structured output is configured.
266
+ # +files:+ requires +prompt+. Pass files to attach to the new user message.
230
267
  #
231
268
  # --
232
- # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?, ?context: Hash[Symbol, untyped]?) -> Enumerator[Riffer::StreamEvents::Base, void]
233
- 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]
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
234
271
 
235
- # Registers a callback to be invoked when messages are added during generation.
236
- #
237
- # Raises Riffer::ArgumentError if no block is given.
238
- #
239
- # --
240
- # : () { (Riffer::Messages::Base) -> void } -> self
241
- def on_message: () { (Riffer::Messages::Base) -> void } -> self
242
-
243
- # Generates the instruction system message for this agent.
244
- #
245
- # Useful for database persistence workflows where the system messages
246
- # need to be stored independently.
247
- #
248
- # Returns +nil+ when no instructions are configured.
272
+ # Streams a response from the agent.
249
273
  #
250
- # --
251
- # : (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
252
- def generate_instruction_message: (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
253
-
254
- # Generates the skills catalog system message for this agent.
274
+ # Runs the inference loop via +Riffer::Agent::Run.stream+, returning an
275
+ # +Enumerator+ of +Riffer::StreamEvents+.
255
276
  #
256
- # Useful for database persistence workflows where the system messages
257
- # need to be stored independently.
277
+ # Raises Riffer::ArgumentError if structured output is configured.
258
278
  #
259
- # Returns +nil+ when no skills are configured or the catalog is empty.
279
+ # See +#generate+ for prompt/files semantics.
260
280
  #
261
281
  # --
262
- # : (?context: Hash[Symbol, untyped]?) -> Riffer::Messages::System?
263
- 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]
264
284
 
265
285
  # Interrupts the agent loop.
266
286
  #
267
287
  # Call from an +on_message+ callback to cleanly interrupt the loop.
268
288
  # Equivalent to <tt>throw :riffer_interrupt, reason</tt>.
269
289
  #
290
+ # When +Riffer.config.experimental_history_healing+ is enabled, riffer
291
+ # fills any orphaned +tool_use+ on the way out with a placeholder
292
+ # +Riffer::Messages::Tool+ carrying +error_type: :interrupted+. The
293
+ # filled call_ids are exposed on
294
+ # +Riffer::Agent::Response#healed_tool_call_ids+ (and the streaming
295
+ # +Riffer::StreamEvents::Interrupt+ event).
296
+ #
270
297
  # --
271
298
  # : (?(String | Symbol)?) -> void
272
299
  def interrupt!: (?(String | Symbol)?) -> void
@@ -274,103 +301,54 @@ class Riffer::Agent
274
301
  private
275
302
 
276
303
  # --
277
- # : (?Array[Riffer::Guardrails::Modification]) -> Riffer::Agent::Response
278
- def run_generate_loop: (?Array[Riffer::Guardrails::Modification]) -> Riffer::Agent::Response
279
-
280
- # --
281
- # : (Riffer::Messages::Base) -> void
282
- def add_message: (Riffer::Messages::Base) -> void
283
-
284
- # --
285
- # : (Riffer::TokenUsage?) -> void
286
- def track_token_usage: (Riffer::TokenUsage?) -> void
287
-
288
- # --
289
- # : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
290
- def initialize_messages: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
291
-
292
- # --
293
- # : (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
294
- def validate_seed_ids!: (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
295
-
296
- # --
297
- # : (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
298
- def build_instruction_message: (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
299
-
300
- # --
301
- # : (?Riffer::Skills::Context?) -> Riffer::Messages::System?
302
- def build_skills_message: (?Riffer::Skills::Context?) -> Riffer::Messages::System?
303
-
304
- # --
305
- # : () -> Integer
306
- def count_assistant_messages: () -> Integer
304
+ # : () -> Riffer::Messages::System?
305
+ def build_instruction_message: () -> Riffer::Messages::System?
307
306
 
308
307
  # --
309
- # : (Enumerator::Yielder) -> void
310
- def run_stream_loop: (Enumerator::Yielder) -> void
308
+ # : () -> Riffer::Messages::System?
309
+ def build_skills_message: () -> Riffer::Messages::System?
311
310
 
312
- # --
313
- # : () -> Riffer::Messages::Assistant
314
- def call_llm: () -> Riffer::Messages::Assistant
315
-
316
- # --
317
- # : () -> Enumerator[Riffer::StreamEvents::Base, void]
318
- def call_llm_stream: () -> Enumerator[Riffer::StreamEvents::Base, void]
319
-
320
- # --
321
- # : () -> Riffer::Providers::Base
322
- def provider_instance: () -> Riffer::Providers::Base
323
-
324
- # --
325
- # : (Riffer::Messages::Assistant) -> bool
326
- def has_tool_calls?: (Riffer::Messages::Assistant) -> bool
327
-
328
- # --
329
- # : (Riffer::Messages::Assistant) -> void
330
- def execute_tool_calls: (Riffer::Messages::Assistant) -> void
331
-
332
- # Executes tool calls left unfinished by a prior interrupt.
311
+ # Resolves +Config#model+ to a "provider/model" string (calling the Proc
312
+ # form against +@context+), parses it, and looks up the provider class.
333
313
  #
334
- # When an interrupt fires mid-way through tool execution, some tool calls
335
- # from the last assistant message may not have been executed yet. This
336
- # method detects those gaps by comparing the tool call ids requested by the
337
- # last assistant message against the tool result messages that follow it,
338
- # then executes any that are missing.
314
+ # Returns +[provider_class, model_name]+. Raises Riffer::ArgumentError on
315
+ # an invalid model string or an unregistered provider.
339
316
  #
340
317
  # --
341
- # : () -> void
342
- # Executes tool calls from the last assistant message that don't yet
343
- # have a corresponding tool result. Safe to call unconditionally —
344
- # returns immediately when there is nothing pending.
345
- def execute_pending_tool_calls: () -> void
346
-
347
- def pending_tool_calls: () -> untyped
348
-
349
- # --
350
- # : () -> void
351
- def prepare_run: () -> void
352
-
353
- # --
354
- # : (untyped) -> void
355
- def parse_model_string!: (untyped) -> void
318
+ # : () -> [singleton(Riffer::Providers::Base), String]
319
+ def resolve_provider_and_model: () -> [ singleton(Riffer::Providers::Base), String ]
356
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
+ #
357
324
  # --
358
- # : () -> void
359
- def clear_resolved_model: () -> void
325
+ # : (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
326
+ def resolve_skills: (singleton(Riffer::Providers::Base)) -> Riffer::Skills::Context?
360
327
 
361
328
  # --
362
- # : (?Hash[Symbol, untyped]?) -> String?
363
- def generate_instructions: (?Hash[Symbol, untyped]?) -> String?
364
-
365
- attr_reader resolved_model: String?
329
+ # : () -> Riffer::Agent::StructuredOutput?
330
+ def resolve_structured_output: () -> Riffer::Agent::StructuredOutput?
366
331
 
332
+ # Resolves the full tool catalog for the agent:
333
+ #
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.
340
+ #
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).
344
+ #
367
345
  # --
368
- # : () -> String
369
- def resolve_model: () -> String
346
+ # : () -> Array[singleton(Riffer::Tool)]
347
+ def resolve_tools: () -> Array[singleton(Riffer::Tool)]
370
348
 
371
349
  # --
372
- # : () -> Array[singleton(Riffer::Tool)]
373
- def resolve_uses_tools_config: () -> Array[singleton(Riffer::Tool)]
350
+ # : () -> Riffer::Tools::Runtime
351
+ def resolve_tool_runtime: () -> Riffer::Tools::Runtime
374
352
 
375
353
  # --
376
354
  # : () -> Array[singleton(Riffer::Tool)]
@@ -381,60 +359,11 @@ class Riffer::Agent
381
359
  # : (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
382
360
  def gather_mcp_registrations_with_tags: (Array[Hash[Symbol, untyped]]) -> Hash[Riffer::Mcp::Registration, Array[Symbol]]
383
361
 
384
- # : (Riffer::Mcp::Registration, Array[Symbol], Proc?, Hash[Symbol, untyped]) -> Array[singleton(Riffer::Tool)]
385
- 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)]
386
364
 
387
365
  # Raises if two or more tool classes share the same +.name+ (ambiguous dispatch).
388
366
  #
389
367
  # : (Array[singleton(Riffer::Tool)]) -> void
390
368
  def assert_distinct_tool_names!: (Array[singleton(Riffer::Tool)]) -> void
391
-
392
- # : () -> Array[singleton(Riffer::Tool)]
393
- def resolved_tools: () -> Array[singleton(Riffer::Tool)]
394
-
395
- # --
396
- # : () -> Riffer::ToolRuntime
397
- def resolve_tool_runtime: () -> Riffer::ToolRuntime
398
-
399
- # Resolves the skills backend, lists skills, and selects an adapter.
400
- #
401
- # Returns nil if skills are not configured or empty.
402
- # Does not mutate instance state — callers are responsible for
403
- # assigning the returned context.
404
- #
405
- # --
406
- # : (?Hash[Symbol, untyped]?) -> Riffer::Skills::Context?
407
- def resolve_skills: (?Hash[Symbol, untyped]?) -> Riffer::Skills::Context?
408
-
409
- # --
410
- # : () -> singleton(Riffer::Providers::Base)
411
- def provider_class: () -> singleton(Riffer::Providers::Base)
412
-
413
- # --
414
- # : () -> Riffer::Messages::Assistant?
415
- def extract_final_response: () -> Riffer::Messages::Assistant?
416
-
417
- # --
418
- # : () -> [Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
419
- def run_before_guardrails: () -> [ Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification] ]
420
-
421
- # --
422
- # : (Riffer::Messages::Assistant) -> [untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification]]
423
- def run_after_guardrails: (Riffer::Messages::Assistant) -> [ untyped, Riffer::Guardrails::Tripwire?, Array[Riffer::Guardrails::Modification] ]
424
-
425
- # --
426
- # : (Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
427
- def validate_structured_output: (Riffer::Messages::Assistant?) -> Hash[Symbol, untyped]?
428
-
429
- # --
430
- # : () -> Riffer::StructuredOutput?
431
- def resolve_structured_output: () -> Riffer::StructuredOutput?
432
-
433
- # --
434
- # : () -> Hash[Symbol, untyped]
435
- def merged_model_options: () -> Hash[Symbol, untyped]
436
-
437
- # --
438
- # : (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
439
- def build_response: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?) -> Riffer::Agent::Response
440
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>.
@@ -180,6 +192,39 @@ class Riffer::Config
180
192
  # : (Symbol) -> void
181
193
  def message_id_strategy=: (Symbol) -> void
182
194
 
195
+ # Experimental: when +true+, riffer keeps the +tool_use+ ↔ +tool_result+
196
+ # invariant intact on its own.
197
+ #
198
+ # - On +Riffer::Agent#generate(messages_array)+, orphaned +tool_use+
199
+ # exchanges and parentless +Riffer::Messages::Tool+ messages are
200
+ # silently stripped from the seed. Pending tool calls on the resume
201
+ # boundary (last assistant whose tail is purely Tool results) are
202
+ # preserved for +execute_pending_tool_calls+.
203
+ # - On any interrupt (caller-issued +interrupt!+ or
204
+ # +INTERRUPT_MAX_STEPS+), riffer fills any orphaned +tool_use+ with a
205
+ # placeholder +Riffer::Messages::Tool+ carrying
206
+ # +error_type: :interrupted+, leaving history valid for the next turn.
207
+ # Filled call_ids are exposed on
208
+ # +Riffer::Agent::Response#healed_tool_call_ids+ (and the streaming
209
+ # +Riffer::StreamEvents::Interrupt+ event).
210
+ #
211
+ # Defaults to +false+ — the pre-healing behavior. Experimental: the
212
+ # surface and default may change without notice.
213
+ attr_reader experimental_history_healing: bool
214
+
215
+ # Sets the +experimental_history_healing+ flag.
216
+ #
217
+ # Coerces common boolean representations so values pulled from
218
+ # environment variables don't silently enable healing — the string
219
+ # +"false"+ is truthy in Ruby and would otherwise flip the flag on.
220
+ # Accepts +true+/+false+, +"true"+/+"false"+, +1+/+0+, +"1"+/+"0"+, and
221
+ # +nil+ (treated as +false+, the default). Raises
222
+ # +Riffer::ArgumentError+ for any other value.
223
+ #
224
+ # --
225
+ # : (untyped) -> void
226
+ def experimental_history_healing=: (untyped) -> void
227
+
183
228
  # --
184
229
  # : () -> void
185
230
  def initialize: () -> void
@@ -18,8 +18,8 @@ class Riffer::Evals::Judge
18
18
  # Internal tool for structured evaluation output.
19
19
  class EvaluationTool < Riffer::Tool
20
20
  # --
21
- # : (context: Hash[Symbol, untyped]?, score: Float, reason: String) -> Riffer::Tools::Response
22
- def call: (context: Hash[Symbol, untyped]?, score: Float, reason: String) -> Riffer::Tools::Response
21
+ # : (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
22
+ def call: (context: Riffer::Agent::Context?, score: Float, reason: String) -> Riffer::Tools::Response
23
23
  end
24
24
 
25
25
  # The model string (provider/model format).
@@ -0,0 +1,9 @@
1
+ # Generated from lib/riffer/helpers/call_or_value.rb with RBS::Inline
2
+
3
+ # Resolves the "Proc-or-value" idiom: if +thing+ is a Proc, calls it
4
+ # (passing +context+ when its arity is non-zero); otherwise returns
5
+ # +thing+ unchanged. When +thing+ is +nil+, returns +default+.
6
+ module Riffer::Helpers::CallOrValue
7
+ # : (untyped, ?context: untyped, ?default: untyped) -> untyped
8
+ def resolve: (untyped, ?context: untyped, ?default: untyped) -> untyped
9
+ end
@@ -5,6 +5,5 @@
5
5
  # Helpers provide reusable functionality across the library:
6
6
  # - Riffer::Helpers::ClassNameConverter - Class name to path conversion
7
7
  # - Riffer::Helpers::Dependencies - Lazy gem dependency loading
8
- # - Riffer::Helpers::Validations - Input validation
9
8
  module Riffer::Helpers
10
9
  end
@@ -24,14 +24,14 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
24
24
  attr_reader tool_calls: Array[Riffer::Messages::Assistant::ToolCall]
25
25
 
26
26
  # Token usage data for this response.
27
- attr_reader token_usage: Riffer::TokenUsage?
27
+ attr_reader token_usage: Riffer::Providers::TokenUsage?
28
28
 
29
29
  # Parsed structured output hash, or nil when not applicable.
30
30
  attr_reader structured_output: Hash[Symbol, untyped]?
31
31
 
32
32
  # --
33
- # : (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
34
- def initialize: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
33
+ # : (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
34
+ def initialize: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::Providers::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
35
35
 
36
36
  # --
37
37
  # : () -> Symbol
@@ -41,6 +41,10 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
41
41
  # : () -> bool
42
42
  def structured_output?: () -> bool
43
43
 
44
+ # --
45
+ # : () -> bool
46
+ def has_tool_calls?: () -> bool
47
+
44
48
  # --
45
49
  # : (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant
46
50
  def +: (Riffer::Messages::Assistant) -> Riffer::Messages::Assistant