riffer 0.47.2 → 0.49.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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/rules/comments.md +2 -4
  3. data/.claude/rules/rbs-inline.md +2 -12
  4. data/.release-please-manifest.json +1 -1
  5. data/.rubocop.yml +5 -0
  6. data/CHANGELOG.md +42 -0
  7. data/docs/AGENTS.md +39 -7
  8. data/docs/AGENT_LIFECYCLE.md +14 -16
  9. data/docs/CONFIGURATION.md +23 -34
  10. data/docs/EVALS.md +2 -1
  11. data/docs/MCP.md +0 -4
  12. data/docs/MESSAGES.md +85 -17
  13. data/docs/STREAM_EVENTS.md +8 -4
  14. data/docs/TOOL_ADVANCED.md +1 -3
  15. data/docs/TRACING.md +2 -2
  16. data/docs/providers/AMAZON_BEDROCK.md +32 -1
  17. data/docs/providers/CUSTOM_PROVIDERS.md +56 -4
  18. data/docs/providers/GEMINI.md +1 -1
  19. data/docs/providers/MOCK_PROVIDER.md +17 -0
  20. data/docs/providers/OPENROUTER.md +18 -1
  21. data/docs-site/build.rb +0 -6
  22. data/docs-site/check.rb +1 -5
  23. data/lib/riffer/agent/config.rb +27 -44
  24. data/lib/riffer/agent/context.rb +2 -26
  25. data/lib/riffer/agent/outcome.rb +0 -20
  26. data/lib/riffer/agent/response.rb +4 -35
  27. data/lib/riffer/agent/run.rb +32 -48
  28. data/lib/riffer/agent/serializer.rb +10 -39
  29. data/lib/riffer/agent/session/repair.rb +3 -15
  30. data/lib/riffer/agent/session.rb +26 -38
  31. data/lib/riffer/agent/structured_output/result.rb +0 -8
  32. data/lib/riffer/agent/structured_output.rb +0 -7
  33. data/lib/riffer/agent.rb +31 -127
  34. data/lib/riffer/config/amazon_bedrock.rb +30 -0
  35. data/lib/riffer/config/anthropic.rb +21 -0
  36. data/lib/riffer/config/azure_open_ai.rb +30 -0
  37. data/lib/riffer/config/evals.rb +18 -0
  38. data/lib/riffer/config/files.rb +61 -0
  39. data/lib/riffer/config/gemini.rb +21 -0
  40. data/lib/riffer/config/mcp.rb +31 -0
  41. data/lib/riffer/config/open_ai.rb +30 -0
  42. data/lib/riffer/config/open_router.rb +21 -0
  43. data/lib/riffer/config/pricing/rates.rb +36 -0
  44. data/lib/riffer/config/pricing.rb +64 -0
  45. data/lib/riffer/config/skills.rb +38 -0
  46. data/lib/riffer/config/tracing.rb +43 -0
  47. data/lib/riffer/config.rb +3 -362
  48. data/lib/riffer/evals/evaluator.rb +22 -23
  49. data/lib/riffer/evals/evaluator_runner.rb +0 -15
  50. data/lib/riffer/evals/judge.rb +9 -11
  51. data/lib/riffer/evals/result.rb +1 -11
  52. data/lib/riffer/evals/run_result.rb +0 -12
  53. data/lib/riffer/evals/scenario_result.rb +0 -19
  54. data/lib/riffer/files/downloader.rb +2 -3
  55. data/lib/riffer/files/resolver.rb +5 -10
  56. data/lib/riffer/guardrail.rb +2 -22
  57. data/lib/riffer/guardrails/modification.rb +0 -8
  58. data/lib/riffer/guardrails/result.rb +0 -17
  59. data/lib/riffer/guardrails/runner.rb +2 -11
  60. data/lib/riffer/guardrails/tripwire.rb +0 -8
  61. data/lib/riffer/guardrails.rb +0 -2
  62. data/lib/riffer/helpers/boolean.rb +0 -4
  63. data/lib/riffer/helpers/call_or_value.rb +0 -3
  64. data/lib/riffer/helpers/deep_dup.rb +37 -0
  65. data/lib/riffer/helpers/dependencies.rb +0 -4
  66. data/lib/riffer/helpers/identifier.rb +3 -12
  67. data/lib/riffer/helpers/validate.rb +42 -0
  68. data/lib/riffer/mcp/authenticated_tool.rb +4 -12
  69. data/lib/riffer/mcp/client.rb +0 -7
  70. data/lib/riffer/mcp/manifest.rb +3 -7
  71. data/lib/riffer/mcp/registration.rb +0 -10
  72. data/lib/riffer/mcp/registry.rb +0 -9
  73. data/lib/riffer/mcp/search_tool.rb +0 -4
  74. data/lib/riffer/mcp/tool.rb +1 -4
  75. data/lib/riffer/mcp/tool_factory.rb +3 -6
  76. data/lib/riffer/mcp.rb +2 -23
  77. data/lib/riffer/messages/assistant/reasoning_part.rb +73 -0
  78. data/lib/riffer/messages/assistant/tool_call.rb +55 -0
  79. data/lib/riffer/messages/assistant.rb +43 -15
  80. data/lib/riffer/messages/base.rb +5 -33
  81. data/lib/riffer/messages/system.rb +8 -1
  82. data/lib/riffer/messages/tool.rb +15 -13
  83. data/lib/riffer/messages/{file_part.rb → user/file_part.rb} +16 -40
  84. data/lib/riffer/messages/user.rb +11 -4
  85. data/lib/riffer/params/boolean.rb +1 -5
  86. data/lib/riffer/params/param.rb +15 -31
  87. data/lib/riffer/params.rb +15 -39
  88. data/lib/riffer/providers/amazon_bedrock.rb +104 -44
  89. data/lib/riffer/providers/anthropic.rb +11 -26
  90. data/lib/riffer/providers/azure_open_ai.rb +3 -8
  91. data/lib/riffer/providers/base.rb +30 -32
  92. data/lib/riffer/providers/finish_reason.rb +0 -6
  93. data/lib/riffer/providers/gemini/client.rb +4 -17
  94. data/lib/riffer/providers/gemini.rb +6 -12
  95. data/lib/riffer/providers/mock.rb +18 -27
  96. data/lib/riffer/providers/open_ai.rb +12 -21
  97. data/lib/riffer/providers/open_router.rb +109 -33
  98. data/lib/riffer/providers/repository.rb +2 -14
  99. data/lib/riffer/providers/token_usage.rb +19 -12
  100. data/lib/riffer/registrable.rb +11 -45
  101. data/lib/riffer/runner/fibers.rb +1 -6
  102. data/lib/riffer/runner/sequential.rb +0 -1
  103. data/lib/riffer/runner/threaded.rb +0 -3
  104. data/lib/riffer/runner.rb +0 -3
  105. data/lib/riffer/skills/activate_tool.rb +0 -3
  106. data/lib/riffer/skills/adapter.rb +0 -8
  107. data/lib/riffer/skills/backend.rb +2 -7
  108. data/lib/riffer/skills/config.rb +13 -17
  109. data/lib/riffer/skills/context.rb +0 -29
  110. data/lib/riffer/skills/filesystem_backend.rb +0 -7
  111. data/lib/riffer/skills/frontmatter.rb +2 -16
  112. data/lib/riffer/skills/markdown_adapter.rb +3 -6
  113. data/lib/riffer/skills/xml_adapter.rb +0 -3
  114. data/lib/riffer/stream_events/base.rb +0 -3
  115. data/lib/riffer/stream_events/finish_reason_done.rb +1 -6
  116. data/lib/riffer/stream_events/guardrail_modification.rb +0 -10
  117. data/lib/riffer/stream_events/guardrail_tripwire.rb +0 -10
  118. data/lib/riffer/stream_events/interrupt.rb +2 -12
  119. data/lib/riffer/stream_events/reasoning_delta.rb +0 -3
  120. data/lib/riffer/stream_events/reasoning_done.rb +6 -8
  121. data/lib/riffer/stream_events/skill_activation.rb +0 -3
  122. data/lib/riffer/stream_events/text_delta.rb +0 -2
  123. data/lib/riffer/stream_events/text_done.rb +0 -2
  124. data/lib/riffer/stream_events/token_usage_done.rb +0 -2
  125. data/lib/riffer/stream_events/tool_call_delta.rb +1 -5
  126. data/lib/riffer/stream_events/tool_call_done.rb +0 -5
  127. data/lib/riffer/stream_events/web_search_done.rb +0 -3
  128. data/lib/riffer/stream_events/web_search_status.rb +1 -5
  129. data/lib/riffer/testing/minitest.rb +4 -5
  130. data/lib/riffer/testing.rb +5 -38
  131. data/lib/riffer/tool.rb +2 -28
  132. data/lib/riffer/tools/response.rb +3 -32
  133. data/lib/riffer/tools/runtime/fibers.rb +0 -6
  134. data/lib/riffer/tools/runtime/inline.rb +0 -1
  135. data/lib/riffer/tools/runtime/threaded.rb +0 -6
  136. data/lib/riffer/tools/runtime.rb +8 -30
  137. data/lib/riffer/tools/toolable.rb +0 -37
  138. data/lib/riffer/tracing/capture.rb +5 -9
  139. data/lib/riffer/tracing/no_op.rb +0 -7
  140. data/lib/riffer/tracing/otel.rb +7 -16
  141. data/lib/riffer/tracing/stream_recorder.rb +0 -7
  142. data/lib/riffer/tracing.rb +4 -27
  143. data/lib/riffer/version.rb +1 -1
  144. data/lib/riffer.rb +2 -30
  145. data/sig/generated/riffer/agent/config.rbs +24 -46
  146. data/sig/generated/riffer/agent/context.rbs +2 -26
  147. data/sig/generated/riffer/agent/outcome.rbs +0 -20
  148. data/sig/generated/riffer/agent/response.rbs +4 -27
  149. data/sig/generated/riffer/agent/run.rbs +12 -31
  150. data/sig/generated/riffer/agent/serializer.rbs +2 -27
  151. data/sig/generated/riffer/agent/session/repair.rbs +2 -10
  152. data/sig/generated/riffer/agent/session.rbs +10 -36
  153. data/sig/generated/riffer/agent/structured_output/result.rbs +0 -7
  154. data/sig/generated/riffer/agent/structured_output.rbs +0 -7
  155. data/sig/generated/riffer/agent.rbs +25 -125
  156. data/sig/generated/riffer/config/amazon_bedrock.rbs +21 -0
  157. data/sig/generated/riffer/config/anthropic.rbs +15 -0
  158. data/sig/generated/riffer/config/azure_open_ai.rbs +21 -0
  159. data/sig/generated/riffer/config/evals.rbs +13 -0
  160. data/sig/generated/riffer/config/files.rbs +43 -0
  161. data/sig/generated/riffer/config/gemini.rbs +15 -0
  162. data/sig/generated/riffer/config/mcp.rbs +19 -0
  163. data/sig/generated/riffer/config/open_ai.rbs +21 -0
  164. data/sig/generated/riffer/config/open_router.rbs +15 -0
  165. data/sig/generated/riffer/config/pricing/rates.rbs +19 -0
  166. data/sig/generated/riffer/config/pricing.rbs +31 -0
  167. data/sig/generated/riffer/config/skills.rbs +19 -0
  168. data/sig/generated/riffer/config/tracing.rbs +25 -0
  169. data/sig/generated/riffer/config.rbs +2 -307
  170. data/sig/generated/riffer/evals/evaluator.rbs +12 -21
  171. data/sig/generated/riffer/evals/evaluator_runner.rbs +0 -14
  172. data/sig/generated/riffer/evals/judge.rbs +4 -8
  173. data/sig/generated/riffer/evals/result.rbs +1 -11
  174. data/sig/generated/riffer/evals/run_result.rbs +0 -12
  175. data/sig/generated/riffer/evals/scenario_result.rbs +0 -14
  176. data/sig/generated/riffer/files/resolver.rbs +6 -13
  177. data/sig/generated/riffer/guardrail.rbs +2 -22
  178. data/sig/generated/riffer/guardrails/modification.rbs +0 -6
  179. data/sig/generated/riffer/guardrails/result.rbs +0 -15
  180. data/sig/generated/riffer/guardrails/runner.rbs +0 -11
  181. data/sig/generated/riffer/guardrails/tripwire.rbs +0 -8
  182. data/sig/generated/riffer/guardrails.rbs +0 -2
  183. data/sig/generated/riffer/helpers/boolean.rbs +0 -4
  184. data/sig/generated/riffer/helpers/call_or_value.rbs +0 -3
  185. data/sig/generated/riffer/helpers/deep_dup.rbs +13 -0
  186. data/sig/generated/riffer/helpers/dependencies.rbs +0 -4
  187. data/sig/generated/riffer/helpers/identifier.rbs +0 -12
  188. data/sig/generated/riffer/helpers/validate.rbs +21 -0
  189. data/sig/generated/riffer/mcp/authenticated_tool.rbs +0 -5
  190. data/sig/generated/riffer/mcp/client.rbs +0 -7
  191. data/sig/generated/riffer/mcp/manifest.rbs +3 -7
  192. data/sig/generated/riffer/mcp/registration.rbs +0 -10
  193. data/sig/generated/riffer/mcp/registry.rbs +0 -9
  194. data/sig/generated/riffer/mcp/search_tool.rbs +0 -4
  195. data/sig/generated/riffer/mcp/tool.rbs +0 -4
  196. data/sig/generated/riffer/mcp/tool_factory.rbs +0 -6
  197. data/sig/generated/riffer/mcp.rbs +2 -22
  198. data/sig/generated/riffer/messages/assistant/reasoning_part.rbs +45 -0
  199. data/sig/generated/riffer/messages/assistant/tool_call.rbs +34 -0
  200. data/sig/generated/riffer/messages/assistant.rbs +25 -31
  201. data/sig/generated/riffer/messages/base.rbs +0 -12
  202. data/sig/generated/riffer/messages/system.rbs +4 -1
  203. data/sig/generated/riffer/messages/tool.rbs +4 -10
  204. data/sig/generated/riffer/messages/user/file_part.rbs +76 -0
  205. data/sig/generated/riffer/messages/user.rbs +7 -5
  206. data/sig/generated/riffer/params/boolean.rbs +1 -4
  207. data/sig/generated/riffer/params/param.rbs +7 -31
  208. data/sig/generated/riffer/params.rbs +8 -38
  209. data/sig/generated/riffer/providers/amazon_bedrock.rbs +33 -29
  210. data/sig/generated/riffer/providers/anthropic.rbs +2 -12
  211. data/sig/generated/riffer/providers/azure_open_ai.rbs +0 -8
  212. data/sig/generated/riffer/providers/base.rbs +18 -34
  213. data/sig/generated/riffer/providers/finish_reason.rbs +0 -6
  214. data/sig/generated/riffer/providers/gemini/client.rbs +4 -17
  215. data/sig/generated/riffer/providers/gemini.rbs +4 -10
  216. data/sig/generated/riffer/providers/mock.rbs +6 -27
  217. data/sig/generated/riffer/providers/open_ai.rbs +6 -13
  218. data/sig/generated/riffer/providers/open_router.rbs +37 -18
  219. data/sig/generated/riffer/providers/repository.rbs +2 -14
  220. data/sig/generated/riffer/providers/token_usage.rbs +9 -12
  221. data/sig/generated/riffer/registrable.rbs +0 -45
  222. data/sig/generated/riffer/runner/fibers.rbs +0 -6
  223. data/sig/generated/riffer/runner/sequential.rbs +0 -1
  224. data/sig/generated/riffer/runner/threaded.rbs +0 -3
  225. data/sig/generated/riffer/runner.rbs +0 -3
  226. data/sig/generated/riffer/skills/activate_tool.rbs +0 -3
  227. data/sig/generated/riffer/skills/adapter.rbs +0 -8
  228. data/sig/generated/riffer/skills/backend.rbs +2 -7
  229. data/sig/generated/riffer/skills/config.rbs +6 -17
  230. data/sig/generated/riffer/skills/context.rbs +0 -29
  231. data/sig/generated/riffer/skills/filesystem_backend.rbs +0 -7
  232. data/sig/generated/riffer/skills/frontmatter.rbs +2 -16
  233. data/sig/generated/riffer/skills/markdown_adapter.rbs +0 -6
  234. data/sig/generated/riffer/skills/xml_adapter.rbs +0 -3
  235. data/sig/generated/riffer/stream_events/base.rbs +0 -3
  236. data/sig/generated/riffer/stream_events/finish_reason_done.rbs +1 -6
  237. data/sig/generated/riffer/stream_events/guardrail_modification.rbs +0 -10
  238. data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +0 -10
  239. data/sig/generated/riffer/stream_events/interrupt.rbs +2 -10
  240. data/sig/generated/riffer/stream_events/reasoning_delta.rbs +0 -3
  241. data/sig/generated/riffer/stream_events/reasoning_done.rbs +4 -6
  242. data/sig/generated/riffer/stream_events/skill_activation.rbs +0 -3
  243. data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
  244. data/sig/generated/riffer/stream_events/text_done.rbs +0 -2
  245. data/sig/generated/riffer/stream_events/token_usage_done.rbs +0 -2
  246. data/sig/generated/riffer/stream_events/tool_call_delta.rbs +1 -5
  247. data/sig/generated/riffer/stream_events/tool_call_done.rbs +0 -5
  248. data/sig/generated/riffer/stream_events/web_search_done.rbs +0 -3
  249. data/sig/generated/riffer/stream_events/web_search_status.rbs +1 -5
  250. data/sig/generated/riffer/testing.rbs +0 -38
  251. data/sig/generated/riffer/tool.rbs +0 -27
  252. data/sig/generated/riffer/tools/response.rbs +3 -29
  253. data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -5
  254. data/sig/generated/riffer/tools/runtime/inline.rbs +0 -1
  255. data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -5
  256. data/sig/generated/riffer/tools/runtime.rbs +4 -26
  257. data/sig/generated/riffer/tools/toolable.rbs +0 -37
  258. data/sig/generated/riffer/tracing/capture.rbs +6 -9
  259. data/sig/generated/riffer/tracing/no_op.rbs +0 -7
  260. data/sig/generated/riffer/tracing/otel.rbs +7 -16
  261. data/sig/generated/riffer/tracing/stream_recorder.rbs +0 -2
  262. data/sig/generated/riffer/tracing.rbs +4 -23
  263. data/sig/generated/riffer.rbs +2 -29
  264. data/sig/manual/riffer/helpers/deep_dup.rbs +5 -0
  265. data/sig/manual/riffer/helpers/validate.rbs +5 -0
  266. metadata +39 -3
  267. data/sig/generated/riffer/messages/file_part.rbs +0 -101
@@ -1,23 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
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.
7
- #
8
- # agent.session.add(msg) # append + fire callbacks
9
- # agent.session.set([msg1, msg2]) # bulk replace (silent)
10
- # agent.session.unset # clear (silent)
11
- # agent.session.remove(id: "a_1")
12
- # agent.session.update(id: "a_1", content: "...")
13
- # agent.session.find { |m| m.id == "a_1" }
14
- #
15
4
  class Riffer::Agent::Session
16
5
  include Enumerable #[Riffer::Messages::Base]
17
6
 
18
7
  # @rbs @callbacks: Array[^(Riffer::Messages::Base) -> void]
19
8
 
20
- # The message history.
21
9
  attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
22
10
 
23
11
  #--
@@ -27,7 +15,6 @@ class Riffer::Agent::Session
27
15
  @callbacks = [] #: Array[^(Riffer::Messages::Base) -> void]
28
16
  end
29
17
 
30
- # Registers a callback invoked once per message appended via +#add+.
31
18
  #--
32
19
  #: () { (Riffer::Messages::Base) -> void } -> self
33
20
  def on_message(&block)
@@ -37,18 +24,15 @@ class Riffer::Agent::Session
37
24
  self
38
25
  end
39
26
 
40
- # Appends +message+ and fires every registered callback once with it. Pass
41
- # +silent: true+ to skip callbacks — used for non-inference inputs like user
42
- # messages that subscribers don't expect on the callback channel.
43
27
  #--
44
28
  #: (Riffer::Messages::Base, ?silent: bool) -> Riffer::Messages::Base
45
29
  def add(message, silent: false)
46
- @messages << message
30
+ @messages.insert(insertion_index(message), message)
31
+ # +silent+ is for non-inference inputs (e.g. user messages) that subscribers don't expect.
47
32
  @callbacks.each { |callback| callback.call(message) } unless silent
48
33
  message
49
34
  end
50
35
 
51
- # Replaces the message history wholesale
52
36
  #--
53
37
  #: (Array[Riffer::Messages::Base]) -> self
54
38
  def set(messages)
@@ -56,7 +40,6 @@ class Riffer::Agent::Session
56
40
  self
57
41
  end
58
42
 
59
- # Clears the session.
60
43
  #--
61
44
  #: () -> self
62
45
  def unset
@@ -64,10 +47,6 @@ class Riffer::Agent::Session
64
47
  self
65
48
  end
66
49
 
67
- # Removes a message by id, cascading to drop the +Tool+ results of a removed
68
- # assistant's +tool_calls+ so the +tool_use+ ↔ +tool_result+ invariant holds.
69
- # Raises on a +Tool+ message — that would orphan its parent; use +#update+
70
- # instead. Returns +nil+ if no message matches.
71
50
  #--
72
51
  #: (id: String) -> Riffer::Messages::Base?
73
52
  def remove(id:)
@@ -90,10 +69,6 @@ class Riffer::Agent::Session
90
69
  target
91
70
  end
92
71
 
93
- # Partial in-place update: looks up a message by +id:+ or +tool_call_id:+
94
- # (exactly one), overlays +attrs+ onto a same-type replacement, and swaps it
95
- # in. Dropping +tool_calls+ from an assistant cascades to remove their +Tool+
96
- # results, preserving the invariant. Raises on neither/both keys or no match.
97
72
  #--
98
73
  #: (?id: String?, ?tool_call_id: String?, **untyped) -> Riffer::Messages::Base
99
74
  def update(id: nil, tool_call_id: nil, **attrs)
@@ -118,9 +93,6 @@ class Riffer::Agent::Session
118
93
  replacement
119
94
  end
120
95
 
121
- # Returns the call_ids of every +tool_call+ with no matching
122
- # +Riffer::Messages::Tool+ result anywhere in history — a hook for checking
123
- # the +tool_use+ ↔ +tool_result+ invariant before mutating or persisting.
124
96
  #--
125
97
  #: () -> Array[String]
126
98
  def orphaned_tool_call_ids
@@ -132,8 +104,6 @@ class Riffer::Agent::Session
132
104
  end
133
105
  end
134
106
 
135
- # Returns +[last_assistant, pending_tool_calls]+; the second element is empty
136
- # when there's no assistant message or no pending calls.
137
107
  #--
138
108
  #: () -> [Riffer::Messages::Assistant?, Array[Riffer::Messages::Assistant::ToolCall]]
139
109
  def pending_tool_calls
@@ -150,7 +120,14 @@ class Riffer::Agent::Session
150
120
  [assistant, assistant.tool_calls.reject { |tc| executed_ids.include?(tc.call_id) }]
151
121
  end
152
122
 
153
- # Yields each message in order, or returns an Enumerator without a block.
123
+ #--
124
+ #: () -> Array[String]
125
+ def discard_pending_tool_calls
126
+ messages, filled = Repair.fill_orphans(@messages)
127
+ set(messages)
128
+ filled
129
+ end
130
+
154
131
  #--
155
132
  #: () -> Enumerator[Riffer::Messages::Base, self]
156
133
  #: () { (Riffer::Messages::Base) -> void } -> untyped
@@ -160,17 +137,12 @@ class Riffer::Agent::Session
160
137
  @messages.each(&block)
161
138
  end
162
139
 
163
- # The number of LLM steps completed, used by the agent loop to enforce
164
- # +max_steps+ on resume.
165
140
  #--
166
141
  #: () -> Integer
167
142
  def steps
168
143
  @messages.count { |m| m.is_a?(Riffer::Messages::Assistant) }
169
144
  end
170
145
 
171
- # The most recent +Riffer::Messages::Assistant+ in the session, or +nil+
172
- # when none exists.
173
- #
174
146
  #--
175
147
  #: () -> Riffer::Messages::Assistant?
176
148
  def final_assistant_message
@@ -180,6 +152,21 @@ class Riffer::Agent::Session
180
152
 
181
153
  private
182
154
 
155
+ # Providers require tool results to directly follow their tool_use, but a
156
+ # resumed turn appends the new user message before pending calls execute.
157
+ #--
158
+ #: (Riffer::Messages::Base) -> Integer
159
+ def insertion_index(message)
160
+ return @messages.length unless message.is_a?(Riffer::Messages::Tool)
161
+
162
+ parent_idx = @messages.rindex do |m|
163
+ m.is_a?(Riffer::Messages::Assistant) && m.tool_calls.any? { |tc| tc.call_id == message.tool_call_id }
164
+ end
165
+ return @messages.length unless parent_idx
166
+
167
+ parent_idx + 1 + @messages.drop(parent_idx + 1).take_while { |m| m.is_a?(Riffer::Messages::Tool) }.length
168
+ end
169
+
183
170
  #--
184
171
  #: (Riffer::Messages::Base, Riffer::Messages::Base) -> void
185
172
  def cascade_dropped_tool_calls(old, replacement)
@@ -201,6 +188,7 @@ class Riffer::Agent::Session
201
188
  attrs.fetch(:content, old.content),
202
189
  id: attrs.fetch(:id, old.id),
203
190
  tool_calls: attrs.fetch(:tool_calls, old.tool_calls),
191
+ reasoning: attrs.fetch(:reasoning, old.reasoning),
204
192
  token_usage: attrs.fetch(:token_usage, old.token_usage),
205
193
  structured_output: attrs.fetch(:structured_output, old.structured_output),
206
194
  finish_reason: attrs.fetch(:finish_reason, old.finish_reason),
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Wraps the result of structured output parsing and validation.
5
4
  class Riffer::Agent::StructuredOutput::Result
6
- # The validated object, or +nil+ on failure.
7
5
  attr_reader :object #: Hash[Symbol, untyped]? # @dynamic object
8
-
9
- # The error message, or +nil+ on success.
10
6
  attr_reader :error #: String? # @dynamic error
11
7
 
12
8
  #--
@@ -16,14 +12,10 @@ class Riffer::Agent::StructuredOutput::Result
16
12
  @error = error
17
13
  end
18
14
 
19
- # Returns true when parsing and validation succeeded.
20
- #
21
15
  #--
22
16
  #: () -> bool
23
17
  def success? = @error.nil?
24
18
 
25
- # Returns true when parsing or validation failed.
26
- #
27
19
  #--
28
20
  #: () -> bool
29
21
  def failure? = !success?
@@ -3,10 +3,7 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # Parses and validates structured JSON responses against a Riffer::Params
7
- # schema.
8
6
  class Riffer::Agent::StructuredOutput
9
- # The schema parameters.
10
7
  attr_reader :params #: Riffer::Params # @dynamic params
11
8
 
12
9
  #--
@@ -15,16 +12,12 @@ class Riffer::Agent::StructuredOutput
15
12
  @params = params
16
13
  end
17
14
 
18
- # Returns the JSON Schema for this structured output.
19
- #
20
15
  #--
21
16
  #: (?strict: bool) -> Hash[Symbol, untyped]
22
17
  def json_schema(strict: false)
23
18
  @params.to_json_schema(strict: strict)
24
19
  end
25
20
 
26
- # Parses a JSON string and validates it against the schema, returning a
27
- # Result carrying either the validated object or an error message.
28
21
  #--
29
22
  #: (String) -> Riffer::Agent::StructuredOutput::Result
30
23
  def parse_and_validate(json_string)
data/lib/riffer/agent.rb CHANGED
@@ -3,17 +3,6 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # Base class for all agents in the Riffer framework. Subclass it to define an
7
- # agent's model, instructions, tools, and guardrails.
8
- #
9
- # class MyAgent < Riffer::Agent
10
- # model 'openai/gpt-4o'
11
- # instructions 'You are a helpful assistant.'
12
- # end
13
- #
14
- # agent = MyAgent.new
15
- # agent.generate('Hello!')
16
- #
17
6
  class Riffer::Agent
18
7
  extend Riffer::Registrable
19
8
 
@@ -21,53 +10,48 @@ class Riffer::Agent
21
10
 
22
11
  INTERRUPT_MAX_STEPS = :max_steps #: Symbol
23
12
 
24
- # Returns the per-class Riffer::Agent::Config holding every DSL setting.
25
13
  #--
26
14
  #: () -> Riffer::Agent::Config
27
15
  def self.config
28
16
  @config ||= Riffer::Agent::Config.new
29
17
  end
30
18
 
31
- # Gets or sets the agent identifier.
32
- #
19
+ #--
20
+ #: (Class) -> void
21
+ def self.inherited(subclass)
22
+ super
23
+ copy = config.dup
24
+ # The identifier is identity, not inheritable config: two classes claiming
25
+ # one raise Riffer::DuplicateIdentifierError at the next registry lookup.
26
+ copy.identifier = nil
27
+ subclass.instance_variable_set(:@config, copy)
28
+ end
29
+ private_class_method :inherited
30
+
33
31
  #--
34
32
  #: (?String?) -> String
35
33
  def self.identifier(value = nil)
36
34
  value.nil? ? (config.identifier || Riffer::Helpers::Identifier.for(self)) : (config.identifier = value)
37
35
  end
38
36
 
39
- # Gets or sets the model string (e.g., "openai/gpt-4o").
40
- #
41
37
  #--
42
38
  #: (?(String | Proc)?) -> (String | Proc)?
43
39
  def self.model(value = nil)
44
40
  value.nil? ? config.model : (config.model = value)
45
41
  end
46
42
 
47
- # Gets or sets the agent instructions. A Proc is called at generate time with
48
- # the +context+ hash (which may be +nil+).
49
- #
50
- # instructions "You are a helpful assistant."
51
- #
52
- # instructions -> (context) {
53
- # "You are assisting #{context[:name]}"
54
- # }
55
- #
56
43
  #--
57
44
  #: (?(String | Proc)?) -> (String | Proc)?
58
45
  def self.instructions(value = nil)
59
46
  value.nil? ? config.instructions : (config.instructions = value)
60
47
  end
61
48
 
62
- # Gets or sets model options passed to generate_text/stream_text.
63
- #
64
49
  #--
65
50
  #: (?Hash[Symbol, untyped]?) -> Hash[Symbol, untyped]
66
51
  def self.model_options(options = nil)
67
52
  options.nil? ? config.model_options : (config.model_options = options)
68
53
  end
69
54
 
70
- # Gets or sets the structured output schema for this agent.
71
55
  #--
72
56
  #: (?Riffer::Params?) ?{ (Riffer::Params) [self: Riffer::Params] -> void } -> Riffer::Params?
73
57
  def self.structured_output(params = nil, &block)
@@ -79,62 +63,39 @@ class Riffer::Agent
79
63
  config.structured_output
80
64
  end
81
65
 
82
- # Gets or sets the maximum number of LLM call steps in the tool-use loop.
83
- # The splat distinguishes a getter (no argument) from setting the limit to
84
- # +nil+ (unlimited); it defaults to Riffer::Agent::Config::DEFAULT_MAX_STEPS.
85
- #
86
- # max_steps # reads the current limit
87
- # max_steps 8 # cap the loop at 8 steps
88
- # max_steps nil # unlimited
89
- #
90
66
  #--
91
67
  #: (*Numeric?) -> Numeric?
92
68
  def self.max_steps(*value)
69
+ # The splat tells a bare read apart from +max_steps nil+ (unlimited).
93
70
  return config.max_steps if value.empty?
94
71
 
95
72
  config.max_steps = value.first
96
73
  end
97
74
 
98
- # Gets or sets the tools used by this agent.
99
- #
100
75
  #--
101
76
  #: (?(Array[singleton(Riffer::Tool)] | Proc)?) -> (Array[singleton(Riffer::Tool)] | Proc)?
102
77
  def self.uses_tools(value = nil)
103
78
  value.nil? ? config.tools_config : (config.tools_config = value)
104
79
  end
105
80
 
106
- # Opts this agent into MCP tools from registrations matching the given tag.
107
- # Progressive registrations expose +mcp_search+ instead of every schema up front.
108
- #
81
+ # +progressive+ exposes +mcp_search+ instead of every tool schema up front.
82
+ #--
109
83
  #: (String | Symbol, ?progressive: bool) -> void
110
84
  def self.use_mcp(tag, progressive: true)
111
85
  config.add_mcp(tag, progressive: progressive)
112
86
  end
113
87
 
114
- # Returns the accumulated +use_mcp+ configurations for this agent class.
115
- #
116
88
  #: () -> Array[Hash[Symbol, untyped]]
117
89
  def self.mcp_configs
118
90
  config.mcp_configs
119
91
  end
120
92
 
121
- # Gets or sets the tool runtime for this agent; defaults to
122
- # <tt>Riffer.config.tool_runtime</tt> when unset.
123
93
  #--
124
94
  #: (?(singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)
125
95
  def self.tool_runtime(value = nil)
126
96
  value.nil? ? config.tool_runtime : (config.tool_runtime = value)
127
97
  end
128
98
 
129
- # Configures skills for this agent via a block DSL, or returns the current
130
- # Riffer::Skills::Config when called without a block.
131
- #
132
- # skills do
133
- # backend Riffer::Skills::FilesystemBackend.new(".skills")
134
- # adapter Riffer::Skills::XmlAdapter
135
- # activate ["code-review"]
136
- # end
137
- #
138
99
  #--
139
100
  #: () ?{ (Riffer::Skills::Config) [self: Riffer::Skills::Config] -> void } -> Riffer::Skills::Config?
140
101
  def self.skills(&block)
@@ -146,21 +107,18 @@ class Riffer::Agent
146
107
  config.skills_config
147
108
  end
148
109
 
149
- # Generates a response using a new agent instance.
150
110
  #--
151
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
111
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
152
112
  def self.generate(prompt = nil, files: nil, context: nil, tags: {})
153
113
  new(context: context).generate(prompt, files: files, tags: tags)
154
114
  end
155
115
 
156
- # Streams a response using a new agent instance.
157
116
  #--
158
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
117
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?context: Hash[Symbol, untyped]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
159
118
  def self.stream(prompt = nil, files: nil, context: nil, tags: {})
160
119
  new(context: context).stream(prompt, files: files, tags: tags)
161
120
  end
162
121
 
163
- # Reconstructs a runnable agent from a wire hash produced by +#to_h+.
164
122
  #--
165
123
  #: (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
166
124
  def self.from_h(
@@ -179,7 +137,6 @@ class Riffer::Agent
179
137
  )
180
138
  end
181
139
 
182
- # Reconstructs a runnable agent from a JSON string produced by +#to_json+.
183
140
  #--
184
141
  #: (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
185
142
  def self.from_json(
@@ -198,70 +155,33 @@ class Riffer::Agent
198
155
  )
199
156
  end
200
157
 
201
- # Registers a guardrail for input, output, or both phases. Raises
202
- # Riffer::ArgumentError unless +phase+ is :before, :after, or :around.
203
158
  #--
204
159
  #: (Symbol, with: singleton(Riffer::Guardrail), **untyped) -> void
205
160
  def self.guardrail(phase, with:, **options)
206
161
  config.add_guardrail(phase, klass: with, options: options)
207
162
  end
208
163
 
209
- # Returns the registered guardrail configs for a given phase.
210
164
  #--
211
165
  #: (Symbol) -> Array[Hash[Symbol, untyped]]
212
166
  def self.guardrails_for(phase)
213
167
  config.guardrails_for(phase)
214
168
  end
215
169
 
216
- # The conversation handle.
217
170
  attr_reader :session #: Riffer::Agent::Session # @dynamic session
218
-
219
- # The per-instance Riffer::Agent::Config.
220
171
  attr_reader :config #: Riffer::Agent::Config # @dynamic config
221
-
222
- # The system message built from the configured +instructions+, or +nil+ when
223
- # none are configured.
224
172
  attr_reader :instruction_message #: Riffer::Messages::System? # @dynamic instruction_message
225
-
226
- # The system message describing the configured skills catalog, or +nil+ when
227
- # skills are unconfigured or the catalog is empty.
228
173
  attr_reader :skills_message #: Riffer::Messages::System? # @dynamic skills_message
229
-
230
- # The mutable runtime context shared with every +Riffer::Agent::Run+ this
231
- # agent executes and threaded through all Proc-based settings.
232
174
  attr_reader :context #: Riffer::Agent::Context # @dynamic context
233
-
234
- # The resolved provider name (the part before "/" in the model string),
235
- # e.g. +"openai"+.
236
175
  attr_reader :provider_name #: String # @dynamic provider_name
237
-
238
- # The resolved model name (the part after "/" in the model string), used as
239
- # the model argument on every LLM call.
240
176
  attr_reader :model_name #: String # @dynamic model_name
241
177
 
242
- # The provider client. Public so tests can pre-queue responses on
243
- # +Riffer::Providers::Mock+ before calling +#generate+.
178
+ # Public so tests can pre-queue responses on Riffer::Providers::Mock.
244
179
  attr_reader :provider #: Riffer::Providers::Base # @dynamic provider
245
180
 
246
- # The +Riffer::Agent::StructuredOutput+ wrapping the configured schema, or
247
- # +nil+ when not configured.
248
181
  attr_reader :structured_output #: Riffer::Agent::StructuredOutput? # @dynamic structured_output
249
-
250
- # The tool classes the LLM sees on every call this agent makes.
251
182
  attr_reader :tools #: Array[singleton(Riffer::Tool)] # @dynamic tools
252
-
253
- # The tool runtime instance used to execute tool calls.
254
183
  attr_reader :tool_runtime #: Riffer::Tools::Runtime # @dynamic tool_runtime
255
184
 
256
- # Initializes a new agent.
257
- #
258
- # A provided +session:+ is used as-is — the caller owns its contents (e.g.
259
- # cross-process resume from persisted history); an omitted one is seeded with
260
- # the instruction and skills messages.
261
- #
262
- # Raises Riffer::ArgumentError unless the configured model string is
263
- # "provider/model" format.
264
- #
265
185
  #--
266
186
  #: (?session: Riffer::Agent::Session?, ?context: Hash[Symbol, untyped]?, ?config: Riffer::Agent::Config?) -> void
267
187
  def initialize(session: nil, context: nil, config: nil)
@@ -280,38 +200,26 @@ class Riffer::Agent
280
200
  @instruction_message = build_instruction_message
281
201
  @skills_message = build_skills_message
282
202
 
203
+ # A caller-supplied session owns its contents (e.g. resumed history), so it
204
+ # is not seeded.
283
205
  @session = session || Riffer::Agent::Session.new(messages: [@instruction_message, @skills_message].compact)
284
206
  @session.set(Riffer::Agent::Session::Repair.prune_orphans(@session.messages))
285
207
  end
286
208
 
287
- # Generates a response from the agent.
288
- #
289
- # With +prompt+, a new user message is appended (silently — +on_message+ does
290
- # not fire for user inputs) before the loop runs. Without it, the loop runs
291
- # against the current session, resuming a persisted conversation or pending
292
- # tool calls. +files:+ requires +prompt+.
293
- #
294
- # +tags:+ is an optional flat hash of attribution tags applied to this single
295
- # call: they propagate to the provider's native request-metadata field, and
296
- # are stamped as +riffer.tag.*+ on every span the call emits. See
297
- # +docs/AGENTS.md+ for the per-provider mapping. The reserved key
298
- # +user_id+ also maps to the provider's native user identifier where one
299
- # exists.
300
- #
301
- #--
302
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
209
+ #--
210
+ #: () -> String
211
+ def identifier
212
+ config.identifier || self.class.identifier
213
+ end
214
+
215
+ #--
216
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
303
217
  def generate(prompt = nil, files: nil, tags: {})
304
218
  Riffer::Agent::Run.generate(agent: self, prompt: prompt, files: files, tags: tags)
305
219
  end
306
220
 
307
- # Streams a response from the agent as an +Enumerator+ of
308
- # +Riffer::StreamEvents+ whose block-form +each+ returns the final
309
- # Riffer::Agent::Response. See +#generate+ for prompt/files/tags semantics.
310
- #
311
- # Raises Riffer::ArgumentError if structured output is configured.
312
- #
313
221
  #--
314
- #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
222
+ #: (?String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
315
223
  def stream(prompt = nil, files: nil, tags: {})
316
224
  if @structured_output
317
225
  raise Riffer::ArgumentError,
@@ -321,24 +229,20 @@ class Riffer::Agent
321
229
  Riffer::Agent::Run.stream(agent: self, prompt: prompt, files: files, tags: tags)
322
230
  end
323
231
 
324
- # Interrupts the agent loop from an +on_message+ callback. Equivalent to
325
- # <tt>throw :riffer_interrupt, reason</tt>.
326
232
  #--
327
233
  #: (?(String | Symbol)?) -> void
328
234
  def interrupt!(reason = nil)
329
235
  throw :riffer_interrupt, reason
330
236
  end
331
237
 
332
- # Snapshots this resolved agent into a self-contained, provider-neutral wire
333
- # hash.
334
238
  #--
335
239
  #: () -> Hash[Symbol, untyped]
336
240
  def to_h
337
241
  Riffer::Agent::Serializer.to_h(agent: self)
338
242
  end
339
243
 
340
- # Snapshots this resolved agent into a wire JSON string. The +*+ absorbs the
341
- # JSON generator state argument so <tt>JSON.generate(agent)</tt> works too.
244
+ # The +*+ absorbs JSON's generator-state argument so
245
+ # <tt>JSON.generate(agent)</tt> works.
342
246
  #--
343
247
  #: (*untyped) -> String
344
248
  def to_json(*)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ class Riffer::Config::AmazonBedrock
5
+ attr_reader :api_token #: String? # @dynamic api_token
6
+
7
+ attr_reader :region #: String? # @dynamic region
8
+
9
+ attr_accessor :client #: untyped # @dynamic client, client=
10
+
11
+ #--
12
+ #: () -> void
13
+ def initialize
14
+ @api_token = nil
15
+ @region = nil
16
+ @client = nil
17
+ end
18
+
19
+ #--
20
+ #: (untyped) -> void
21
+ def api_token=(value)
22
+ @api_token = Riffer::Helpers::Validate.optional_string(value, attribute: "api_token")
23
+ end
24
+
25
+ #--
26
+ #: (untyped) -> void
27
+ def region=(value)
28
+ @region = Riffer::Helpers::Validate.optional_string(value, attribute: "region")
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ class Riffer::Config::Anthropic
5
+ attr_reader :api_key #: String? # @dynamic api_key
6
+
7
+ attr_accessor :client #: untyped # @dynamic client, client=
8
+
9
+ #--
10
+ #: () -> void
11
+ def initialize
12
+ @api_key = nil
13
+ @client = nil
14
+ end
15
+
16
+ #--
17
+ #: (untyped) -> void
18
+ def api_key=(value)
19
+ @api_key = Riffer::Helpers::Validate.optional_string(value, attribute: "api_key")
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ class Riffer::Config::AzureOpenAI
5
+ attr_reader :api_key #: String? # @dynamic api_key
6
+
7
+ attr_reader :endpoint #: String? # @dynamic endpoint
8
+
9
+ attr_accessor :client #: untyped # @dynamic client, client=
10
+
11
+ #--
12
+ #: () -> void
13
+ def initialize
14
+ @api_key = nil
15
+ @endpoint = nil
16
+ @client = nil
17
+ end
18
+
19
+ #--
20
+ #: (untyped) -> void
21
+ def api_key=(value)
22
+ @api_key = Riffer::Helpers::Validate.optional_string(value, attribute: "api_key")
23
+ end
24
+
25
+ #--
26
+ #: (untyped) -> void
27
+ def endpoint=(value)
28
+ @endpoint = Riffer::Helpers::Validate.optional_string(value, attribute: "endpoint")
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ class Riffer::Config::Evals
5
+ attr_reader :judge_model #: String? # @dynamic judge_model
6
+
7
+ #--
8
+ #: () -> void
9
+ def initialize
10
+ @judge_model = nil
11
+ end
12
+
13
+ #--
14
+ #: (untyped) -> void
15
+ def judge_model=(value)
16
+ @judge_model = value.nil? ? nil : Riffer::Helpers::Validate.model_id(value, attribute: "judge_model")
17
+ end
18
+ end