riffer 0.31.0 → 0.32.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 (213) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/code-style.md +63 -4
  3. data/.agents/rbs-inline.md +1 -6
  4. data/.release-please-manifest.json +1 -1
  5. data/AGENTS.md +1 -2
  6. data/CHANGELOG.md +18 -0
  7. data/docs/08_MESSAGES.md +1 -1
  8. data/docs/14_MCP.md +50 -5
  9. data/docs/providers/02_AMAZON_BEDROCK.md +14 -0
  10. data/lib/riffer/agent/config.rb +42 -47
  11. data/lib/riffer/agent/context.rb +70 -50
  12. data/lib/riffer/agent/response.rb +4 -20
  13. data/lib/riffer/agent/run.rb +28 -67
  14. data/lib/riffer/agent/serializer.rb +22 -81
  15. data/lib/riffer/agent/session/repair.rb +14 -40
  16. data/lib/riffer/agent/session.rb +25 -67
  17. data/lib/riffer/agent/structured_output/result.rb +3 -11
  18. data/lib/riffer/agent/structured_output.rb +5 -13
  19. data/lib/riffer/agent.rb +74 -192
  20. data/lib/riffer/config.rb +34 -101
  21. data/lib/riffer/evals/evaluator.rb +7 -27
  22. data/lib/riffer/evals/evaluator_runner.rb +11 -19
  23. data/lib/riffer/evals/judge.rb +4 -25
  24. data/lib/riffer/evals/result.rb +1 -18
  25. data/lib/riffer/evals/run_result.rb +0 -11
  26. data/lib/riffer/evals/scenario_result.rb +0 -14
  27. data/lib/riffer/evals.rb +0 -6
  28. data/lib/riffer/guardrail.rb +4 -27
  29. data/lib/riffer/guardrails/modification.rb +0 -10
  30. data/lib/riffer/guardrails/result.rb +3 -30
  31. data/lib/riffer/guardrails/runner.rb +5 -22
  32. data/lib/riffer/guardrails/tripwire.rb +1 -19
  33. data/lib/riffer/guardrails.rb +2 -4
  34. data/lib/riffer/helpers/call_or_value.rb +4 -3
  35. data/lib/riffer/helpers/class_name_converter.rb +3 -1
  36. data/lib/riffer/helpers/dependencies.rb +5 -7
  37. data/lib/riffer/helpers.rb +0 -5
  38. data/lib/riffer/mcp/authenticated_tool.rb +9 -9
  39. data/lib/riffer/mcp/client.rb +12 -17
  40. data/lib/riffer/mcp/manifest.rb +13 -10
  41. data/lib/riffer/mcp/registration.rb +2 -11
  42. data/lib/riffer/mcp/registry.rb +44 -52
  43. data/lib/riffer/mcp/search_tool.rb +53 -0
  44. data/lib/riffer/mcp/tool_factory.rb +13 -18
  45. data/lib/riffer/mcp.rb +12 -17
  46. data/lib/riffer/messages/assistant.rb +2 -9
  47. data/lib/riffer/messages/base.rb +46 -16
  48. data/lib/riffer/messages/file_part.rb +32 -24
  49. data/lib/riffer/messages/system.rb +0 -5
  50. data/lib/riffer/messages/tool.rb +0 -10
  51. data/lib/riffer/messages/user.rb +0 -10
  52. data/lib/riffer/messages.rb +0 -7
  53. data/lib/riffer/params/boolean.rb +2 -4
  54. data/lib/riffer/params/param.rb +28 -39
  55. data/lib/riffer/params.rb +9 -21
  56. data/lib/riffer/providers/amazon_bedrock.rb +42 -28
  57. data/lib/riffer/providers/anthropic.rb +4 -9
  58. data/lib/riffer/providers/azure_open_ai.rb +3 -19
  59. data/lib/riffer/providers/base.rb +13 -26
  60. data/lib/riffer/providers/gemini.rb +4 -4
  61. data/lib/riffer/providers/mock.rb +6 -26
  62. data/lib/riffer/providers/open_ai.rb +6 -8
  63. data/lib/riffer/providers/open_router.rb +4 -10
  64. data/lib/riffer/providers/repository.rb +4 -3
  65. data/lib/riffer/providers/token_usage.rb +9 -20
  66. data/lib/riffer/providers.rb +0 -8
  67. data/lib/riffer/runner/fibers.rb +10 -16
  68. data/lib/riffer/runner/sequential.rb +1 -4
  69. data/lib/riffer/runner/threaded.rb +3 -14
  70. data/lib/riffer/runner.rb +2 -15
  71. data/lib/riffer/skills/activate_tool.rb +2 -11
  72. data/lib/riffer/skills/adapter.rb +4 -22
  73. data/lib/riffer/skills/backend.rb +7 -21
  74. data/lib/riffer/skills/config.rb +10 -31
  75. data/lib/riffer/skills/context.rb +5 -20
  76. data/lib/riffer/skills/filesystem_backend.rb +7 -25
  77. data/lib/riffer/skills/frontmatter.rb +10 -28
  78. data/lib/riffer/skills/markdown_adapter.rb +2 -9
  79. data/lib/riffer/skills/xml_adapter.rb +2 -8
  80. data/lib/riffer/stream_events/base.rb +1 -6
  81. data/lib/riffer/stream_events/guardrail_modification.rb +1 -8
  82. data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -8
  83. data/lib/riffer/stream_events/interrupt.rb +4 -7
  84. data/lib/riffer/stream_events/reasoning_delta.rb +2 -4
  85. data/lib/riffer/stream_events/reasoning_done.rb +2 -4
  86. data/lib/riffer/stream_events/skill_activation.rb +2 -4
  87. data/lib/riffer/stream_events/text_delta.rb +0 -2
  88. data/lib/riffer/stream_events/text_done.rb +1 -3
  89. data/lib/riffer/stream_events/token_usage_done.rb +1 -8
  90. data/lib/riffer/stream_events/tool_call_delta.rb +2 -3
  91. data/lib/riffer/stream_events/tool_call_done.rb +1 -3
  92. data/lib/riffer/stream_events/web_search_done.rb +1 -3
  93. data/lib/riffer/stream_events/web_search_status.rb +2 -3
  94. data/lib/riffer/stream_events.rb +0 -10
  95. data/lib/riffer/tool.rb +6 -13
  96. data/lib/riffer/tools/response.rb +8 -4
  97. data/lib/riffer/tools/runtime/fibers.rb +0 -3
  98. data/lib/riffer/tools/runtime/inline.rb +1 -4
  99. data/lib/riffer/tools/runtime/threaded.rb +0 -2
  100. data/lib/riffer/tools/runtime.rb +5 -38
  101. data/lib/riffer/tools/toolable.rb +5 -16
  102. data/lib/riffer/tools.rb +0 -4
  103. data/lib/riffer/version.rb +1 -1
  104. data/lib/riffer.rb +7 -8
  105. data/sig/generated/riffer/agent/config.rbs +29 -46
  106. data/sig/generated/riffer/agent/context.rbs +40 -48
  107. data/sig/generated/riffer/agent/response.rbs +4 -20
  108. data/sig/generated/riffer/agent/run.rbs +12 -61
  109. data/sig/generated/riffer/agent/serializer.rbs +21 -80
  110. data/sig/generated/riffer/agent/session/repair.rbs +12 -40
  111. data/sig/generated/riffer/agent/session.rbs +25 -67
  112. data/sig/generated/riffer/agent/structured_output/result.rbs +2 -10
  113. data/sig/generated/riffer/agent/structured_output.rbs +5 -12
  114. data/sig/generated/riffer/agent.rbs +57 -186
  115. data/sig/generated/riffer/config.rbs +34 -100
  116. data/sig/generated/riffer/evals/evaluator.rbs +7 -27
  117. data/sig/generated/riffer/evals/evaluator_runner.rbs +9 -19
  118. data/sig/generated/riffer/evals/judge.rbs +4 -24
  119. data/sig/generated/riffer/evals/result.rbs +1 -17
  120. data/sig/generated/riffer/evals/run_result.rbs +0 -10
  121. data/sig/generated/riffer/evals/scenario_result.rbs +0 -13
  122. data/sig/generated/riffer/evals.rbs +0 -6
  123. data/sig/generated/riffer/guardrail.rbs +4 -27
  124. data/sig/generated/riffer/guardrails/modification.rbs +0 -10
  125. data/sig/generated/riffer/guardrails/result.rbs +3 -30
  126. data/sig/generated/riffer/guardrails/runner.rbs +5 -22
  127. data/sig/generated/riffer/guardrails/tripwire.rbs +1 -19
  128. data/sig/generated/riffer/guardrails.rbs +2 -4
  129. data/sig/generated/riffer/helpers/call_or_value.rbs +4 -3
  130. data/sig/generated/riffer/helpers/class_name_converter.rbs +1 -1
  131. data/sig/generated/riffer/helpers/dependencies.rbs +3 -7
  132. data/sig/generated/riffer/helpers.rbs +0 -5
  133. data/sig/generated/riffer/mcp/authenticated_tool.rbs +5 -4
  134. data/sig/generated/riffer/mcp/client.rbs +10 -16
  135. data/sig/generated/riffer/mcp/manifest.rbs +9 -9
  136. data/sig/generated/riffer/mcp/registration.rbs +2 -10
  137. data/sig/generated/riffer/mcp/registry.rbs +11 -18
  138. data/sig/generated/riffer/mcp/search_tool.rbs +26 -0
  139. data/sig/generated/riffer/mcp/tool_factory.rbs +10 -15
  140. data/sig/generated/riffer/mcp.rbs +10 -17
  141. data/sig/generated/riffer/messages/assistant.rbs +2 -8
  142. data/sig/generated/riffer/messages/base.rbs +11 -16
  143. data/sig/generated/riffer/messages/file_part.rbs +13 -23
  144. data/sig/generated/riffer/messages/system.rbs +0 -4
  145. data/sig/generated/riffer/messages/tool.rbs +0 -9
  146. data/sig/generated/riffer/messages/user.rbs +0 -9
  147. data/sig/generated/riffer/messages.rbs +0 -7
  148. data/sig/generated/riffer/params/boolean.rbs +2 -4
  149. data/sig/generated/riffer/params/param.rbs +21 -39
  150. data/sig/generated/riffer/params.rbs +9 -21
  151. data/sig/generated/riffer/providers/amazon_bedrock.rbs +21 -25
  152. data/sig/generated/riffer/providers/anthropic.rbs +2 -7
  153. data/sig/generated/riffer/providers/azure_open_ai.rbs +3 -18
  154. data/sig/generated/riffer/providers/base.rbs +9 -25
  155. data/sig/generated/riffer/providers/gemini.rbs +0 -2
  156. data/sig/generated/riffer/providers/mock.rbs +6 -26
  157. data/sig/generated/riffer/providers/open_ai.rbs +1 -5
  158. data/sig/generated/riffer/providers/open_router.rbs +4 -10
  159. data/sig/generated/riffer/providers/repository.rbs +2 -3
  160. data/sig/generated/riffer/providers/token_usage.rbs +6 -16
  161. data/sig/generated/riffer/providers.rbs +0 -8
  162. data/sig/generated/riffer/runner/fibers.rbs +8 -15
  163. data/sig/generated/riffer/runner/sequential.rbs +1 -3
  164. data/sig/generated/riffer/runner/threaded.rbs +3 -13
  165. data/sig/generated/riffer/runner.rbs +2 -14
  166. data/sig/generated/riffer/skills/activate_tool.rbs +2 -11
  167. data/sig/generated/riffer/skills/adapter.rbs +4 -22
  168. data/sig/generated/riffer/skills/backend.rbs +7 -21
  169. data/sig/generated/riffer/skills/config.rbs +10 -31
  170. data/sig/generated/riffer/skills/context.rbs +5 -20
  171. data/sig/generated/riffer/skills/filesystem_backend.rbs +7 -24
  172. data/sig/generated/riffer/skills/frontmatter.rbs +10 -27
  173. data/sig/generated/riffer/skills/markdown_adapter.rbs +2 -9
  174. data/sig/generated/riffer/skills/xml_adapter.rbs +2 -8
  175. data/sig/generated/riffer/stream_events/base.rbs +1 -6
  176. data/sig/generated/riffer/stream_events/guardrail_modification.rbs +1 -8
  177. data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +1 -8
  178. data/sig/generated/riffer/stream_events/interrupt.rbs +4 -7
  179. data/sig/generated/riffer/stream_events/reasoning_delta.rbs +2 -4
  180. data/sig/generated/riffer/stream_events/reasoning_done.rbs +2 -4
  181. data/sig/generated/riffer/stream_events/skill_activation.rbs +2 -4
  182. data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
  183. data/sig/generated/riffer/stream_events/text_done.rbs +1 -3
  184. data/sig/generated/riffer/stream_events/token_usage_done.rbs +1 -7
  185. data/sig/generated/riffer/stream_events/tool_call_delta.rbs +2 -3
  186. data/sig/generated/riffer/stream_events/tool_call_done.rbs +1 -3
  187. data/sig/generated/riffer/stream_events/web_search_done.rbs +1 -3
  188. data/sig/generated/riffer/stream_events/web_search_status.rbs +2 -3
  189. data/sig/generated/riffer/stream_events.rbs +0 -10
  190. data/sig/generated/riffer/tool.rbs +5 -12
  191. data/sig/generated/riffer/tools/response.rbs +6 -4
  192. data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -3
  193. data/sig/generated/riffer/tools/runtime/inline.rbs +1 -3
  194. data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -2
  195. data/sig/generated/riffer/tools/runtime.rbs +5 -37
  196. data/sig/generated/riffer/tools/toolable.rbs +4 -14
  197. data/sig/generated/riffer/tools.rbs +0 -4
  198. data/sig/generated/riffer.rbs +5 -4
  199. data/sig/manual/riffer/agent/session/repair.rbs +5 -0
  200. data/sig/manual/riffer/evals/evaluator_runner.rbs +5 -0
  201. data/sig/manual/riffer/helpers/class_name_converter.rbs +5 -0
  202. data/sig/manual/riffer/helpers/dependencies.rbs +5 -0
  203. data/sig/manual/riffer/mcp/authenticated_tool.rbs +5 -0
  204. data/sig/manual/riffer/mcp/registry.rbs +5 -0
  205. data/sig/manual/riffer/mcp/tool_factory.rbs +5 -0
  206. data/sig/manual/riffer/mcp.rbs +5 -0
  207. data/sig/manual/riffer/providers/repository.rbs +5 -0
  208. data/sig/manual/riffer.rbs +5 -0
  209. metadata +17 -9
  210. data/.agents/rdoc.md +0 -69
  211. data/lib/riffer/messages/converter.rb +0 -90
  212. data/sig/generated/riffer/messages/converter.rbs +0 -33
  213. data/sig/manual/riffer/tools/toolable.rbs +0 -6
@@ -1,23 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Agent::Run is the generation loop. A pure module of functions over an
5
- # +agent+ — Agent owns every per-call value (provider, model, tools, tool
6
- # runtime, structured output, session, context); Run just orchestrates.
7
- #
8
- # Tools and user code see the agent's +context+ (a +Riffer::Agent::Context+)
9
- # unchanged through the loop, so downstream tool runtimes can read
10
- # caller-provided keys via <tt>context[:agent]</tt> /
11
- # <tt>context.dig(:key)</tt>, or the framework built-ins via
12
- # +context.skills+. Cumulative token usage is updated into
13
- # +agent.context.token_usage+ as the loop progresses.
14
- #
15
- # Riffer::Agent::Run.generate(agent: my_agent, prompt: "Hello")
16
- # Riffer::Agent::Run.stream(agent: my_agent, prompt: "Hello")
17
- #
4
+ # The generation loop a pure module of functions over an +agent+, which owns
5
+ # every per-call value; Run just orchestrates.
18
6
  module Riffer::Agent::Run
19
7
  extend self
20
- include Riffer::Messages::Converter
21
8
 
22
9
  # Runs the generate loop for the given agent. See Riffer::Agent#generate
23
10
  # for prompt/files semantics.
@@ -41,13 +28,6 @@ module Riffer::Agent::Run
41
28
 
42
29
  private
43
30
 
44
- # The generation loop. When +stream_yielder+ is provided, per-step events are
45
- # pushed to it (and +stream+ discards the return value). When +stream_yielder+
46
- # is +nil+, no events are emitted and +generate+ returns the Response
47
- # directly. The two modes share every step of the loop — the only
48
- # divergences are the LLM call shape (atomic vs. accumulated stream)
49
- # and whether per-step events are emitted.
50
- #
51
31
  #--
52
32
  #: (Riffer::Agent, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
53
33
  def run_loop(agent, stream_yielder: nil)
@@ -86,17 +66,12 @@ module Riffer::Agent::Run
86
66
  return final_response(agent, all_modifications)
87
67
  end
88
68
 
89
- # catch returns the thrown value when throw :riffer_interrupt fires;
90
- # the return above exits on the successful (non-interrupted) path.
91
69
  new_messages, filled = Riffer::Agent::Session::Repair.fill_orphans(agent.session.messages)
92
70
  agent.session.set(new_messages)
93
71
  stream_yielder << Riffer::StreamEvents::Interrupt.new(reason: reason, healed_tool_call_ids: filled) if stream_yielder
94
72
  final_response(agent, all_modifications, interrupted: true, interrupt_reason: reason, healed_tool_call_ids: filled)
95
73
  end
96
74
 
97
- # Consumes one provider stream, forwarding every event to +stream_yielder+
98
- # and folding it into an +Assistant+ message.
99
- #
100
75
  #--
101
76
  #: (Riffer::Agent, Enumerator::Yielder) -> Riffer::Messages::Assistant
102
77
  def accumulate_streamed_response(agent, stream_yielder)
@@ -130,9 +105,6 @@ module Riffer::Agent::Run
130
105
  )
131
106
  end
132
107
 
133
- # Appends +new_modifications+ to +all_modifications+ and emits a
134
- # +GuardrailModification+ event for each one when streaming.
135
- #
136
108
  #--
137
109
  #: (Enumerator::Yielder?, Array[Riffer::Guardrails::Modification], Array[Riffer::Guardrails::Modification]) -> void
138
110
  def record_modifications!(stream_yielder, all_modifications, new_modifications)
@@ -140,9 +112,6 @@ module Riffer::Agent::Run
140
112
  new_modifications.each { |m| stream_yielder << Riffer::StreamEvents::GuardrailModification.new(m) } if stream_yielder
141
113
  end
142
114
 
143
- # Emits a +GuardrailTripwire+ event when streaming and returns the
144
- # short-circuit +Response+ for a tripped guardrail.
145
- #
146
115
  #--
147
116
  #: (Riffer::Agent, Enumerator::Yielder?, Riffer::Guardrails::Tripwire, Array[Riffer::Guardrails::Modification]) -> Riffer::Agent::Response
148
117
  def tripwire_response(agent, stream_yielder, tripwire, all_modifications)
@@ -150,11 +119,6 @@ module Riffer::Agent::Run
150
119
  build_response(agent, "", tripwire: tripwire, modifications: all_modifications)
151
120
  end
152
121
 
153
- # Builds the final +Response+ from the session's last assistant
154
- # message, validating structured output when configured. +extra+
155
- # carries the interrupt-only fields (+interrupted:+, +interrupt_reason:+,
156
- # +healed_tool_call_ids:+) on the interrupt exit path.
157
- #
158
122
  #--
159
123
  #: (Riffer::Agent, Array[Riffer::Guardrails::Modification], **untyped) -> Riffer::Agent::Response
160
124
  def final_response(agent, all_modifications, **extra)
@@ -168,7 +132,7 @@ module Riffer::Agent::Run
168
132
  agent.provider.generate_text(
169
133
  messages: agent.session.messages,
170
134
  model: agent.model_name,
171
- tools: agent.tools,
135
+ tools: effective_tools(agent),
172
136
  **merged_model_options(agent)
173
137
  )
174
138
  end
@@ -179,7 +143,7 @@ module Riffer::Agent::Run
179
143
  agent.provider.stream_text(
180
144
  messages: agent.session.messages,
181
145
  model: agent.model_name,
182
- tools: agent.tools,
146
+ tools: effective_tools(agent),
183
147
  **merged_model_options(agent)
184
148
  )
185
149
  end
@@ -189,7 +153,9 @@ module Riffer::Agent::Run
189
153
  def execute_tool_calls(agent, assistant_message, tool_calls: assistant_message.tool_calls)
190
154
  return if tool_calls.empty?
191
155
 
192
- results = agent.tool_runtime.execute(tool_calls, tools: agent.tools, context: agent.context, assistant_message: assistant_message)
156
+ results = agent.tool_runtime.execute(tool_calls, tools: effective_tools(agent), context: agent.context, assistant_message: assistant_message)
157
+
158
+ inject_discovered_tools(agent, results)
193
159
 
194
160
  results.each do |tool_call, result|
195
161
  agent.session.add(Riffer::Messages::Tool.new(
@@ -202,12 +168,17 @@ module Riffer::Agent::Run
202
168
  end
203
169
  end
204
170
 
205
- # Executes tool calls left unfinished by a prior interrupt.
206
- #
207
- # Detects gaps between the last assistant message's requested tool calls
208
- # and the tool result messages that follow it, executing any that are
209
- # missing. Safe to call unconditionally.
210
- #
171
+ #--
172
+ #: (Riffer::Agent, Array[[Riffer::Messages::Assistant::ToolCall, Riffer::Tools::Response]]) -> void
173
+ def inject_discovered_tools(agent, results)
174
+ to_inject = results.flat_map { |_, result|
175
+ result.is_a?(Riffer::Mcp::SearchTool::Result) ? result.discovered_tools : [] #: Array[singleton(Riffer::Tool)]
176
+ }
177
+ return if to_inject.empty?
178
+
179
+ agent.context.discover_tools(to_inject)
180
+ end
181
+
211
182
  #--
212
183
  #: (Riffer::Agent) -> void
213
184
  def execute_pending_tool_calls(agent)
@@ -215,11 +186,6 @@ module Riffer::Agent::Run
215
186
  execute_tool_calls(agent, assistant_message, tool_calls: pending) if assistant_message
216
187
  end
217
188
 
218
- # Runs the +:before+ guardrail phase. Records any modifications into
219
- # +all_modifications+ (and emits them when streaming). When a tripwire
220
- # fires, yields the short-circuit +Response+ — the caller's block is
221
- # expected to +return+ it from +run_loop+.
222
- #
223
189
  #--
224
190
  #: (Riffer::Agent, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Agent::Response) -> void } -> void
225
191
  def run_before_guardrails(agent, stream_yielder, all_modifications)
@@ -233,12 +199,6 @@ module Riffer::Agent::Run
233
199
  yield tripwire_response(agent, stream_yielder, tripwire, all_modifications) if tripwire
234
200
  end
235
201
 
236
- # Runs the +:after+ guardrail phase against the assistant +response+.
237
- # Records any modifications into +all_modifications+ (and emits them
238
- # when streaming). When a tripwire fires, yields the short-circuit
239
- # +Response+ — the caller's block is expected to +return+ it from
240
- # +run_loop+. Otherwise returns the post-guardrails assistant message.
241
- #
242
202
  #--
243
203
  #: (Riffer::Agent, Riffer::Messages::Assistant, Enumerator::Yielder?, Array[Riffer::Guardrails::Modification]) { (Riffer::Agent::Response) -> void } -> untyped
244
204
  def run_after_guardrails(agent, response, stream_yielder, all_modifications)
@@ -265,6 +225,13 @@ module Riffer::Agent::Run
265
225
  agent.structured_output.parse_and_validate(response.content).object
266
226
  end
267
227
 
228
+ #--
229
+ #: (Riffer::Agent) -> Array[singleton(Riffer::Tool)]
230
+ def effective_tools(agent)
231
+ discovered = agent.context.discovered_tools || []
232
+ discovered.empty? ? agent.tools : agent.tools + discovered
233
+ end
234
+
268
235
  #--
269
236
  #: (Riffer::Agent) -> Hash[Symbol, untyped]
270
237
  def merged_model_options(agent)
@@ -280,24 +247,18 @@ module Riffer::Agent::Run
280
247
  Riffer::Agent::Response.new(content, tripwire: tripwire, modifications: modifications, interrupted: interrupted, interrupt_reason: interrupt_reason, structured_output: structured_output, messages: messages.frozen? ? messages : messages.dup.freeze, healed_tool_call_ids: healed_tool_call_ids)
281
248
  end
282
249
 
283
- # Appends a +User+ message to the session. No-ops when +prompt+ is nil
284
- # and +files+ is empty (the caller had nothing to add). Raises when
285
- # +files+ are supplied without a +prompt+ — the provider needs text to
286
- # anchor the attachments.
287
- #
250
+ # Raises when +files+ are supplied without a +prompt+ the provider needs
251
+ # text to anchor the attachments.
288
252
  #--
289
253
  #: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> void
290
254
  def append_user_message(agent, prompt, files: nil)
291
255
  raise Riffer::ArgumentError, "files: requires a prompt" if files && !files.empty? && prompt.nil?
292
256
  return unless prompt
293
257
 
294
- file_parts = (files || []).map { |f| convert_to_file_part(f) }
258
+ file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
295
259
  agent.session.add(Riffer::Messages::User.new(prompt, files: file_parts), silent: true)
296
260
  end
297
261
 
298
- # Accumulates token usage into +agent.context.token_usage+. Updates the
299
- # context so cumulative usage persists across every run on the agent.
300
- #
301
262
  #--
302
263
  #: (Riffer::Agent, Riffer::Providers::TokenUsage?) -> void
303
264
  def track_token_usage(agent, usage)
@@ -3,37 +3,17 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # Riffer::Agent::Serializer turns a resolved agent into a self-contained,
7
- # provider-neutral data hash and back into a runnable agent. A pure module
8
- # (sibling to Riffer::Agent::Run), reached most often through the
9
- # +Riffer::Agent#to_h+ / +Riffer::Agent.from_h+ delegators.
6
+ # Turns a resolved agent into a self-contained, provider-neutral data hash and
7
+ # back into a runnable agent, behind the +Riffer::Agent#to_h+ /
8
+ # +Riffer::Agent.from_h+ delegators.
10
9
  #
11
- # The hash carries only data — no Procs, no class references, no tool
12
- # runtime. The same hash serves two rehydration targets:
13
- #
14
- # - <b>In-process</b> (a monolith persisting agent definitions): pass a
15
- # +tool_resolver+ that looks tool descriptors up in a local registry and
16
- # returns the real, body-bearing classes. They run on the default runtime.
17
- # - <b>Distributed</b> (a receiver holding only the Riffer gem): the default
18
- # resolver synthesizes body-less tool shells; inject a remote
19
- # +Riffer::Tools::Runtime+ to forward each call back to the origin.
20
- #
21
- # data = Riffer::Agent::Serializer.to_h(agent: agent)
22
- # rebuilt = Riffer::Agent::Serializer.from_h(data, context: {tenant: "acme"})
23
- #
24
- # == What does not transfer
25
- #
26
- # Guardrails and the skills subsystem (backend/adapter/catalog) are not
27
- # serialized; a rebuilt agent enforces no guardrails and renders no skills
28
- # catalog (the +skill_activate+ tool, if present, crosses as an ordinary
29
- # tool). Secrets must not be placed in +provider_options+/+model_options+:
30
- # both ride on the wire as plain data.
10
+ # hash = Riffer::Agent::Serializer.to_h(agent: agent)
11
+ # rebuilt = Riffer::Agent::Serializer.from_h(hash, context: {tenant: "acme"})
31
12
  module Riffer::Agent::Serializer
32
13
  extend self
33
14
 
34
- # The wire format version. Bumped only on an incompatible change to the
35
- # hash shape; +from_h+ refuses any other version. See +from_h+ for the
36
- # dispatch seam that carries back-compat decoders.
15
+ # The wire format version, bumped only on an incompatible change to the hash
16
+ # shape; +from_h+ refuses any other version.
37
17
  SCHEMA_VERSION = 1 #: Integer
38
18
 
39
19
  # Raised by +from_h+ when the hash's +schema_version+ is unsupported.
@@ -43,16 +23,9 @@ module Riffer::Agent::Serializer
43
23
  # descriptor. Its +#call+ raises — route shells through a remote runtime.
44
24
  DEFAULT_TOOL_RESOLVER = ->(descriptor) { build_tool_shell(descriptor) } #: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool)
45
25
 
46
- # Snapshots a resolved agent into a self-contained wire hash.
47
- #
48
- # Reads the agent's resolved instance state — Proc-based settings have
49
- # already been evaluated against the agent's own context, so the hash
50
- # carries plain strings/data, never Procs. Tools are emitted as
51
- # +{name, description, parameters_schema, timeout}+ descriptors (the
52
- # resolved +agent.tools+, including MCP tools and +skill_activate+).
53
- #
54
- # [agent] a resolved Riffer::Agent instance.
55
- #
26
+ # Snapshots a resolved agent into a self-contained wire hash. Proc-based
27
+ # settings are already evaluated against the agent's context, so the hash
28
+ # carries plain data, never Procs.
56
29
  #--
57
30
  #: (agent: Riffer::Agent) -> Hash[Symbol, untyped]
58
31
  def to_h(agent:)
@@ -71,35 +44,17 @@ module Riffer::Agent::Serializer
71
44
  }
72
45
  end
73
46
 
74
- # Reconstructs a runnable agent from a wire hash.
75
- #
76
- # [hash] a Symbol-keyed wire hash (parse JSON with +symbolize_names: true+).
77
- # [context] the rebuilt agent's runtime context — the same value you'd pass
78
- # to +Agent.new(context:)+. It is *not* used to re-resolve serialized
79
- # config (the hash is already resolved); it is threaded into tool dispatch
80
- # and read by tools/runtimes at call time (e.g. a remote runtime keying off
81
- # <tt>context[:tenant]</tt>). Defaults to an empty context.
82
- # [session] an optional Riffer::Agent::Session to seed conversation history,
83
- # forwarded verbatim to +Agent.new(session:)+. The hash carries the agent
84
- # *definition*, not its history (see "What does not transfer"); pass a
85
- # session here to resume a persisted conversation. Used as-is — the caller
86
- # owns its contents, including the system instruction message. When omitted,
87
- # a fresh session seeded with the hash's instructions is built.
88
- # [tool_resolver] maps a tool descriptor to a Riffer::Tool class. Defaults
89
- # to DEFAULT_TOOL_RESOLVER (body-less shells). Pass a registry lookup to
90
- # rebuild real, in-process tools.
91
- # [tool_runtime] an optional Riffer::Tools::Runtime to inject (e.g. a
92
- # remote runtime for shells). When omitted, the agent uses the configured
93
- # default (+Riffer.config.tool_runtime+).
94
- #
95
- # Raises Riffer::Agent::Serializer::VersionError on an unsupported
96
- # +schema_version+, and Riffer::ArgumentError on a malformed hash.
47
+ # Reconstructs a runnable agent from a wire hash. +context+ is threaded into
48
+ # tool dispatch (not used to re-resolve the already-resolved config);
49
+ # +session+ seeds conversation history (the hash carries the agent definition,
50
+ # not its history). Raises Riffer::Agent::Serializer::VersionError on an
51
+ # unsupported +schema_version+.
97
52
  #
98
53
  #--
99
54
  #: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?session: Riffer::Agent::Session?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
100
55
  def from_h(hash, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
101
56
  # Version -> decoder dispatch. Adding a +when 2+ arm (a backwards-compatible
102
- # decoder) is how a future breaking change keeps older dicts readable.
57
+ # decoder) is how a future breaking change keeps older hashes readable.
103
58
  case hash[:schema_version]
104
59
  when SCHEMA_VERSION
105
60
  decode_v1(hash, context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
@@ -108,19 +63,15 @@ module Riffer::Agent::Serializer
108
63
  end
109
64
  end
110
65
 
111
- # Snapshots a resolved agent to a JSON string. Convenience over
112
- # <tt>JSON.generate(to_h(agent:))</tt>.
113
- #
66
+ # Snapshots a resolved agent to a JSON string.
114
67
  #--
115
68
  #: (agent: Riffer::Agent) -> String
116
69
  def to_json(agent:)
117
70
  JSON.generate(to_h(agent: agent))
118
71
  end
119
72
 
120
- # Reconstructs a runnable agent from a JSON string produced by +to_json+.
121
- # Handles the JSON parse (with symbol keys) so callers don't have to. See
73
+ # Reconstructs a runnable agent from a JSON string produced by +to_json+. See
122
74
  # +from_h+ for the arguments.
123
- #
124
75
  #--
125
76
  #: (String, ?context: Hash[Symbol, untyped]?, ?session: Riffer::Agent::Session?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
126
77
  def from_json(json, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
@@ -162,20 +113,16 @@ module Riffer::Agent::Serializer
162
113
  Riffer::Params.from_json_schema(schema)
163
114
  end
164
115
 
165
- # The DSL represents unlimited steps as +nil+, but the wire encodes it as
166
- # +-1+ so the hash stays portable across transports where JSON +null+ is
167
- # awkward (e.g. proto3, which can't tell null from an absent field). The
168
- # magic value lives only on the wire — +encode_max_steps+/+decode_max_steps+
169
- # translate at the boundary so neither the DSL nor consumers see it.
116
+ # Encodes unlimited steps (+nil+ in the DSL) as +-1+ on the wire, where a
117
+ # JSON +null+ is awkward across transports (e.g. proto3).
170
118
  #--
171
119
  #: (Numeric?) -> Numeric
172
120
  def encode_max_steps(value)
173
121
  value.nil? ? -1 : value
174
122
  end
175
123
 
176
- # Reverses +encode_max_steps+: +-1+ (or a literal +null+) means unlimited.
177
- # An absent key falls back to the default — a partial hash must not silently
178
- # become an unbounded loop.
124
+ # Reverses +encode_max_steps+; a missing key falls back to the default so a
125
+ # partial hash can't become an unbounded loop.
179
126
  #--
180
127
  #: (Hash[Symbol, untyped]) -> Numeric?
181
128
  def decode_max_steps(hash)
@@ -189,12 +136,6 @@ module Riffer::Agent::Serializer
189
136
  tool_class.to_tool_schema(strict: false).merge(timeout: tool_class.timeout)
190
137
  end
191
138
 
192
- # Builds an anonymous, body-less Riffer::Tool subclass that advertises the
193
- # descriptor's schema to the LLM. Its +#call+ raises — a shell only has
194
- # identity, not behavior; route its calls through a remote runtime.
195
- #
196
- # Returns +untyped+: steep can't see that +Class.new(Riffer::Tool)+ is a
197
- # +singleton(Riffer::Tool)+ (cf. Riffer::Mcp::ToolFactory#build_tool_class).
198
139
  #--
199
140
  #: (Hash[Symbol, untyped]) -> untyped
200
141
  def build_tool_shell(descriptor)
@@ -1,39 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Agent::Session::Repair holds the pure transformations that keep the
5
- # +tool_use+ ↔ +tool_result+ invariant on a message array. No state, no
6
- # instance — module-level functions only. Each entry point is gated by
7
- # +Riffer.config.experimental_history_healing+: when the flag is off the
8
- # function returns its input unchanged.
9
- #
10
- # Two seams:
11
- #
12
- # - +fill_orphans+ — fills orphan +tool_use+ blocks with placeholder
13
- # results. Used on interrupt (caller-issued or +max_steps+).
14
- # - +prune_orphans+ — drops orphan +tool_use+ blocks and parentless Tool
15
- # messages from a caller-provided seed so it is well-formed before the
16
- # next inference call. Used at construction time when
17
- # +Riffer::Agent.new(session:)+ receives a session.
4
+ # Pure, stateless transformations keeping the +tool_use+ +tool_result+
5
+ # invariant on a message array. Each entry point no-ops when
6
+ # +Riffer.config.experimental_history_healing+ is off.
18
7
  module Riffer::Agent::Session::Repair
19
- # Placeholder used to fill orphan +tool_use+ blocks. Emitted as the
20
- # +Riffer::Tools::Response+ body for each filled call_id.
8
+ extend self
9
+
10
+ # Placeholder response filled in for an orphaned +tool_use+ on interrupt.
21
11
  ORPHAN_PLACEHOLDER = ->(_tool_call) {
22
12
  Riffer::Tools::Response.error("Tool call interrupted before completion.", type: :interrupted)
23
13
  } #: ^(Riffer::Messages::Assistant::ToolCall) -> Riffer::Tools::Response
24
14
 
25
- # Fills any orphaned +tool_use+ in +messages+ with the
26
- # +ORPHAN_PLACEHOLDER+ response. Each placeholder Tool message is
27
- # inserted immediately after its parent assistant message. Returns
28
- # +[new_messages, filled_call_ids]+; +filled_call_ids+ is empty when
29
- # there are no orphans.
30
- #
31
- # No-op when +Riffer.config.experimental_history_healing+ is off:
32
- # returns +[messages, []]+ with the same array reference.
33
- #
15
+ # Fills each orphaned +tool_use+ in +messages+ with an +ORPHAN_PLACEHOLDER+
16
+ # result inserted after its parent. Returns +[new_messages, filled_call_ids]+.
34
17
  #--
35
18
  #: (Array[Riffer::Messages::Base]) -> [Array[Riffer::Messages::Base], Array[String]]
36
- def self.fill_orphans(messages)
19
+ def fill_orphans(messages)
37
20
  return [messages, []] unless Riffer.config.experimental_history_healing
38
21
 
39
22
  result_ids = messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
@@ -62,22 +45,13 @@ module Riffer::Agent::Session::Repair
62
45
  [new_messages, filled]
63
46
  end
64
47
 
65
- # Prunes a seeded message array so the +tool_use+ +tool_result+
66
- # invariant holds. Drops orphaned tool exchanges (assistant +tool_call+
67
- # with no matching Tool result) and parentless Tool messages. Returns a
68
- # new array; the input is not mutated.
69
- #
70
- # Pending tool_calls on the resume boundary — the last assistant whose
71
- # tail is purely Tool results (or empty) — are preserved. They get
72
- # swept up by +execute_pending_tool_calls+ at the start of the next
73
- # generate/stream call.
74
- #
75
- # No-op when +Riffer.config.experimental_history_healing+ is off:
76
- # returns +messages+ unchanged.
77
- #
48
+ # Prunes a seeded message array to the invariant dropping orphaned tool
49
+ # exchanges and parentless Tool messages, but preserving the pending
50
+ # tool_calls on the resume boundary (the last assistant) for
51
+ # +execute_pending_tool_calls+. Returns a new array.
78
52
  #--
79
53
  #: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
80
- def self.prune_orphans(messages)
54
+ def prune_orphans(messages)
81
55
  return messages unless Riffer.config.experimental_history_healing
82
56
 
83
57
  resume_boundary = (messages.length - 1).downto(0).find { |idx|
@@ -1,12 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Riffer::Agent::Session owns the conversation handle for an agent: the message
5
- # array, the +on_message+ callback list, and the +tool_use+ ↔ +tool_result+
6
- # invariant that keeps tool calls and their results consistent.
7
- #
8
- # Access via +agent.session+. Sessions are constructed by +Riffer::Agent+
9
- # and live for the lifetime of the agent.
4
+ # Owns the conversation handle for an agent: the message array, the
5
+ # +on_message+ callbacks, and the +tool_use+ ↔ +tool_result+ invariant that
6
+ # keeps tool calls and their results consistent.
10
7
  #
11
8
  # agent.session.add(msg) # append + fire callbacks
12
9
  # agent.session.set([msg1, msg2]) # bulk replace (silent)
@@ -31,12 +28,6 @@ class Riffer::Agent::Session
31
28
  end
32
29
 
33
30
  # Registers a callback invoked once per message appended via +#add+.
34
- #
35
- # Callbacks do NOT fire for +#set+, +#unset+, +#remove+, or +#update+.
36
- # Returns +self+ to allow chaining.
37
- #
38
- # Raises Riffer::ArgumentError if no block is given.
39
- #
40
31
  #--
41
32
  #: () { (Riffer::Messages::Base) -> void } -> self
42
33
  def on_message(&block)
@@ -45,13 +36,9 @@ class Riffer::Agent::Session
45
36
  self
46
37
  end
47
38
 
48
- # Appends +message+ and fires every registered callback once with it.
49
- #
50
- # Pass +silent: true+ to skip +on_message+ callbacks used for
51
- # non-inference inputs like user messages, which subscribers don't
52
- # expect to observe through the callback channel. Inference-produced
53
- # messages (Assistant, Tool) always go through +add+ without +silent+.
54
- #
39
+ # Appends +message+ and fires every registered callback once with it. Pass
40
+ # +silent: true+ to skip callbacks — used for non-inference inputs like user
41
+ # messages that subscribers don't expect on the callback channel.
55
42
  #--
56
43
  #: (Riffer::Messages::Base, ?silent: bool) -> Riffer::Messages::Base
57
44
  def add(message, silent: false)
@@ -60,13 +47,7 @@ class Riffer::Agent::Session
60
47
  message
61
48
  end
62
49
 
63
- # Replaces the message history wholesale. Does NOT fire +on_message+
64
- # callbacks; registered callbacks persist across the swap.
65
- #
66
- # Used for seeding, guardrail rewrites, and history healing — cases
67
- # where firing callbacks would double-emit messages that subscribers
68
- # have already observed (or never produced).
69
- #
50
+ # Replaces the message history wholesale
70
51
  #--
71
52
  #: (Array[Riffer::Messages::Base]) -> self
72
53
  def set(messages)
@@ -74,9 +55,7 @@ class Riffer::Agent::Session
74
55
  self
75
56
  end
76
57
 
77
- # Clears the session. Does NOT fire +on_message+ callbacks; registered
78
- # callbacks persist.
79
- #
58
+ # Clears the session.
80
59
  #--
81
60
  #: () -> self
82
61
  def unset
@@ -84,18 +63,10 @@ class Riffer::Agent::Session
84
63
  self
85
64
  end
86
65
 
87
- # Removes a message by id. When the target is an assistant message that
88
- # carries +tool_calls+, every +Riffer::Messages::Tool+ result whose
89
- # +tool_call_id+ matches one of those calls is removed atomically — keeping
90
- # the +tool_use+ +tool_result+ invariant intact.
91
- #
92
- # Raises Riffer::ArgumentError when called on a +Riffer::Messages::Tool+
93
- # message — that would orphan the parent's +tool_use+. Use
94
- # +#update+ to rewrite a tool result instead.
95
- #
96
- # Returns the removed message, or +nil+ when no message has the given id
97
- # (idempotent).
98
- #
66
+ # Removes a message by id, cascading to drop the +Tool+ results of a removed
67
+ # assistant's +tool_calls+ so the +tool_use+ +tool_result+ invariant holds.
68
+ # Raises on a +Tool+ message that would orphan its parent; use +#update+
69
+ # instead. Returns +nil+ if no message matches.
99
70
  #--
100
71
  #: (id: String) -> Riffer::Messages::Base?
101
72
  def remove(id:)
@@ -118,19 +89,10 @@ class Riffer::Agent::Session
118
89
  target
119
90
  end
120
91
 
121
- # Partial in-place update. Looks up a message by either +id:+ or
122
- # +tool_call_id:+ (exactly one required), constructs a replacement of the
123
- # same concrete type with +attrs+ overlaid on the existing fields, and
124
- # swaps it in place.
125
- #
126
- # When the target is an assistant message and the update drops one or more
127
- # entries from +tool_calls+, every +Riffer::Messages::Tool+ result whose
128
- # +tool_call_id+ matches a dropped call is removed atomically — keeping the
129
- # +tool_use+ ↔ +tool_result+ invariant intact.
130
- #
131
- # Raises Riffer::ArgumentError when neither or both lookup keys are
132
- # provided, or when no message matches.
133
- #
92
+ # Partial in-place update: looks up a message by +id:+ or +tool_call_id:+
93
+ # (exactly one), overlays +attrs+ onto a same-type replacement, and swaps it
94
+ # in. Dropping +tool_calls+ from an assistant cascades to remove their +Tool+
95
+ # results, preserving the invariant. Raises on neither/both keys or no match.
134
96
  #--
135
97
  #: (?id: String?, ?tool_call_id: String?, **untyped) -> Riffer::Messages::Base
136
98
  def update(id: nil, tool_call_id: nil, **attrs)
@@ -155,12 +117,9 @@ class Riffer::Agent::Session
155
117
  replacement
156
118
  end
157
119
 
158
- # Returns the call_ids of every +tool_call+ on any assistant message that
159
- # has no matching +Riffer::Messages::Tool+ result anywhere in history.
160
- #
161
- # Zero-cost validation hook for callers that want to check the
162
- # +tool_use+ ↔ +tool_result+ invariant before mutating or persisting.
163
- #
120
+ # Returns the call_ids of every +tool_call+ with no matching
121
+ # +Riffer::Messages::Tool+ result anywhere in history — a hook for checking
122
+ # the +tool_use+ ↔ +tool_result+ invariant before mutating or persisting.
164
123
  #--
165
124
  #: () -> Array[String]
166
125
  def orphaned_tool_call_ids
@@ -171,10 +130,8 @@ class Riffer::Agent::Session
171
130
  }
172
131
  end
173
132
 
174
- # Returns +[assistant, pending_tool_calls]+ for the last assistant message.
175
- # When there is no assistant message or no pending calls, the second
176
- # element is an empty array.
177
- #
133
+ # Returns +[last_assistant, pending_tool_calls]+; the second element is empty
134
+ # when there's no assistant message or no pending calls.
178
135
  #--
179
136
  #: () -> [Riffer::Messages::Assistant?, Array[Riffer::Messages::Assistant::ToolCall]]
180
137
  def pending_tool_calls
@@ -191,6 +148,7 @@ class Riffer::Agent::Session
191
148
  [assistant, assistant.tool_calls.reject { |tc| executed_ids.include?(tc.call_id) }]
192
149
  end
193
150
 
151
+ # Yields each message in order, or returns an Enumerator without a block.
194
152
  #--
195
153
  #: () -> Enumerator[Riffer::Messages::Base, self]
196
154
  #: () { (Riffer::Messages::Base) -> void } -> untyped
@@ -199,10 +157,8 @@ class Riffer::Agent::Session
199
157
  @messages.each(&block)
200
158
  end
201
159
 
202
- # The number of LLM steps completed in this session, derived from the
203
- # count of assistant messages. Used by the agent loop to enforce
160
+ # The number of LLM steps completed, used by the agent loop to enforce
204
161
  # +max_steps+ on resume.
205
- #
206
162
  #--
207
163
  #: () -> Integer
208
164
  def steps
@@ -223,6 +179,7 @@ class Riffer::Agent::Session
223
179
 
224
180
  private
225
181
 
182
+ #--
226
183
  #: (Riffer::Messages::Base, Riffer::Messages::Base) -> void
227
184
  def cascade_dropped_tool_calls(old, replacement)
228
185
  return unless old.is_a?(Riffer::Messages::Assistant)
@@ -234,6 +191,7 @@ class Riffer::Agent::Session
234
191
  @messages.reject! { |m| m.is_a?(Riffer::Messages::Tool) && removed_ids.include?(m.tool_call_id) }
235
192
  end
236
193
 
194
+ #--
237
195
  #: (Riffer::Messages::Base, Hash[Symbol, untyped]) -> Riffer::Messages::Base
238
196
  def rebuild_message(old, attrs)
239
197
  case old
@@ -2,19 +2,11 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  # Wraps the result of structured output parsing and validation.
5
- #
6
- # On success, +object+ contains the validated Hash and +error+ is nil.
7
- # On failure, +error+ contains the error message and +object+ is nil.
8
- #
9
- # result = structured_output.parse_and_validate(json_string)
10
- # if result.success?
11
- # result.object #=> {sentiment: "positive", score: 0.9}
12
- # else
13
- # result.error #=> "JSON parse error: ..."
14
- # end
15
- #
16
5
  class Riffer::Agent::StructuredOutput::Result
6
+ # The validated object, or +nil+ on failure.
17
7
  attr_reader :object #: Hash[Symbol, untyped]?
8
+
9
+ # The error message, or +nil+ on success.
18
10
  attr_reader :error #: String?
19
11
 
20
12
  #--