riffer 0.30.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 (214) 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 +25 -0
  7. data/docs/08_MESSAGES.md +1 -1
  8. data/docs/14_MCP.md +50 -5
  9. data/docs/15_SERIALIZATION.md +23 -12
  10. data/docs/providers/02_AMAZON_BEDROCK.md +14 -0
  11. data/lib/riffer/agent/config.rb +42 -47
  12. data/lib/riffer/agent/context.rb +70 -50
  13. data/lib/riffer/agent/response.rb +4 -20
  14. data/lib/riffer/agent/run.rb +28 -67
  15. data/lib/riffer/agent/serializer.rb +36 -85
  16. data/lib/riffer/agent/session/repair.rb +14 -40
  17. data/lib/riffer/agent/session.rb +25 -67
  18. data/lib/riffer/agent/structured_output/result.rb +3 -11
  19. data/lib/riffer/agent/structured_output.rb +5 -13
  20. data/lib/riffer/agent.rb +81 -199
  21. data/lib/riffer/config.rb +34 -101
  22. data/lib/riffer/evals/evaluator.rb +7 -27
  23. data/lib/riffer/evals/evaluator_runner.rb +11 -19
  24. data/lib/riffer/evals/judge.rb +4 -25
  25. data/lib/riffer/evals/result.rb +1 -18
  26. data/lib/riffer/evals/run_result.rb +0 -11
  27. data/lib/riffer/evals/scenario_result.rb +0 -14
  28. data/lib/riffer/evals.rb +0 -6
  29. data/lib/riffer/guardrail.rb +4 -27
  30. data/lib/riffer/guardrails/modification.rb +0 -10
  31. data/lib/riffer/guardrails/result.rb +3 -30
  32. data/lib/riffer/guardrails/runner.rb +5 -22
  33. data/lib/riffer/guardrails/tripwire.rb +1 -19
  34. data/lib/riffer/guardrails.rb +2 -4
  35. data/lib/riffer/helpers/call_or_value.rb +4 -3
  36. data/lib/riffer/helpers/class_name_converter.rb +3 -1
  37. data/lib/riffer/helpers/dependencies.rb +5 -7
  38. data/lib/riffer/helpers.rb +0 -5
  39. data/lib/riffer/mcp/authenticated_tool.rb +9 -9
  40. data/lib/riffer/mcp/client.rb +12 -17
  41. data/lib/riffer/mcp/manifest.rb +13 -10
  42. data/lib/riffer/mcp/registration.rb +2 -11
  43. data/lib/riffer/mcp/registry.rb +44 -52
  44. data/lib/riffer/mcp/search_tool.rb +53 -0
  45. data/lib/riffer/mcp/tool_factory.rb +13 -18
  46. data/lib/riffer/mcp.rb +12 -17
  47. data/lib/riffer/messages/assistant.rb +2 -9
  48. data/lib/riffer/messages/base.rb +46 -16
  49. data/lib/riffer/messages/file_part.rb +32 -24
  50. data/lib/riffer/messages/system.rb +0 -5
  51. data/lib/riffer/messages/tool.rb +0 -10
  52. data/lib/riffer/messages/user.rb +0 -10
  53. data/lib/riffer/messages.rb +0 -7
  54. data/lib/riffer/params/boolean.rb +2 -4
  55. data/lib/riffer/params/param.rb +28 -39
  56. data/lib/riffer/params.rb +9 -21
  57. data/lib/riffer/providers/amazon_bedrock.rb +42 -28
  58. data/lib/riffer/providers/anthropic.rb +4 -9
  59. data/lib/riffer/providers/azure_open_ai.rb +3 -19
  60. data/lib/riffer/providers/base.rb +13 -26
  61. data/lib/riffer/providers/gemini.rb +4 -4
  62. data/lib/riffer/providers/mock.rb +6 -26
  63. data/lib/riffer/providers/open_ai.rb +6 -8
  64. data/lib/riffer/providers/open_router.rb +4 -10
  65. data/lib/riffer/providers/repository.rb +4 -3
  66. data/lib/riffer/providers/token_usage.rb +9 -20
  67. data/lib/riffer/providers.rb +0 -8
  68. data/lib/riffer/runner/fibers.rb +10 -16
  69. data/lib/riffer/runner/sequential.rb +1 -4
  70. data/lib/riffer/runner/threaded.rb +3 -14
  71. data/lib/riffer/runner.rb +2 -15
  72. data/lib/riffer/skills/activate_tool.rb +2 -11
  73. data/lib/riffer/skills/adapter.rb +4 -22
  74. data/lib/riffer/skills/backend.rb +7 -21
  75. data/lib/riffer/skills/config.rb +10 -31
  76. data/lib/riffer/skills/context.rb +5 -20
  77. data/lib/riffer/skills/filesystem_backend.rb +7 -25
  78. data/lib/riffer/skills/frontmatter.rb +10 -28
  79. data/lib/riffer/skills/markdown_adapter.rb +2 -9
  80. data/lib/riffer/skills/xml_adapter.rb +2 -8
  81. data/lib/riffer/stream_events/base.rb +1 -6
  82. data/lib/riffer/stream_events/guardrail_modification.rb +1 -8
  83. data/lib/riffer/stream_events/guardrail_tripwire.rb +1 -8
  84. data/lib/riffer/stream_events/interrupt.rb +4 -7
  85. data/lib/riffer/stream_events/reasoning_delta.rb +2 -4
  86. data/lib/riffer/stream_events/reasoning_done.rb +2 -4
  87. data/lib/riffer/stream_events/skill_activation.rb +2 -4
  88. data/lib/riffer/stream_events/text_delta.rb +0 -2
  89. data/lib/riffer/stream_events/text_done.rb +1 -3
  90. data/lib/riffer/stream_events/token_usage_done.rb +1 -8
  91. data/lib/riffer/stream_events/tool_call_delta.rb +2 -3
  92. data/lib/riffer/stream_events/tool_call_done.rb +1 -3
  93. data/lib/riffer/stream_events/web_search_done.rb +1 -3
  94. data/lib/riffer/stream_events/web_search_status.rb +2 -3
  95. data/lib/riffer/stream_events.rb +0 -10
  96. data/lib/riffer/tool.rb +6 -13
  97. data/lib/riffer/tools/response.rb +8 -4
  98. data/lib/riffer/tools/runtime/fibers.rb +0 -3
  99. data/lib/riffer/tools/runtime/inline.rb +1 -4
  100. data/lib/riffer/tools/runtime/threaded.rb +0 -2
  101. data/lib/riffer/tools/runtime.rb +5 -38
  102. data/lib/riffer/tools/toolable.rb +5 -16
  103. data/lib/riffer/tools.rb +0 -4
  104. data/lib/riffer/version.rb +1 -1
  105. data/lib/riffer.rb +7 -8
  106. data/sig/generated/riffer/agent/config.rbs +29 -46
  107. data/sig/generated/riffer/agent/context.rbs +40 -48
  108. data/sig/generated/riffer/agent/response.rbs +4 -20
  109. data/sig/generated/riffer/agent/run.rbs +12 -61
  110. data/sig/generated/riffer/agent/serializer.rbs +28 -81
  111. data/sig/generated/riffer/agent/session/repair.rbs +12 -40
  112. data/sig/generated/riffer/agent/session.rbs +25 -67
  113. data/sig/generated/riffer/agent/structured_output/result.rbs +2 -10
  114. data/sig/generated/riffer/agent/structured_output.rbs +5 -12
  115. data/sig/generated/riffer/agent.rbs +62 -191
  116. data/sig/generated/riffer/config.rbs +34 -100
  117. data/sig/generated/riffer/evals/evaluator.rbs +7 -27
  118. data/sig/generated/riffer/evals/evaluator_runner.rbs +9 -19
  119. data/sig/generated/riffer/evals/judge.rbs +4 -24
  120. data/sig/generated/riffer/evals/result.rbs +1 -17
  121. data/sig/generated/riffer/evals/run_result.rbs +0 -10
  122. data/sig/generated/riffer/evals/scenario_result.rbs +0 -13
  123. data/sig/generated/riffer/evals.rbs +0 -6
  124. data/sig/generated/riffer/guardrail.rbs +4 -27
  125. data/sig/generated/riffer/guardrails/modification.rbs +0 -10
  126. data/sig/generated/riffer/guardrails/result.rbs +3 -30
  127. data/sig/generated/riffer/guardrails/runner.rbs +5 -22
  128. data/sig/generated/riffer/guardrails/tripwire.rbs +1 -19
  129. data/sig/generated/riffer/guardrails.rbs +2 -4
  130. data/sig/generated/riffer/helpers/call_or_value.rbs +4 -3
  131. data/sig/generated/riffer/helpers/class_name_converter.rbs +1 -1
  132. data/sig/generated/riffer/helpers/dependencies.rbs +3 -7
  133. data/sig/generated/riffer/helpers.rbs +0 -5
  134. data/sig/generated/riffer/mcp/authenticated_tool.rbs +5 -4
  135. data/sig/generated/riffer/mcp/client.rbs +10 -16
  136. data/sig/generated/riffer/mcp/manifest.rbs +9 -9
  137. data/sig/generated/riffer/mcp/registration.rbs +2 -10
  138. data/sig/generated/riffer/mcp/registry.rbs +11 -18
  139. data/sig/generated/riffer/mcp/search_tool.rbs +26 -0
  140. data/sig/generated/riffer/mcp/tool_factory.rbs +10 -15
  141. data/sig/generated/riffer/mcp.rbs +10 -17
  142. data/sig/generated/riffer/messages/assistant.rbs +2 -8
  143. data/sig/generated/riffer/messages/base.rbs +11 -16
  144. data/sig/generated/riffer/messages/file_part.rbs +13 -23
  145. data/sig/generated/riffer/messages/system.rbs +0 -4
  146. data/sig/generated/riffer/messages/tool.rbs +0 -9
  147. data/sig/generated/riffer/messages/user.rbs +0 -9
  148. data/sig/generated/riffer/messages.rbs +0 -7
  149. data/sig/generated/riffer/params/boolean.rbs +2 -4
  150. data/sig/generated/riffer/params/param.rbs +21 -39
  151. data/sig/generated/riffer/params.rbs +9 -21
  152. data/sig/generated/riffer/providers/amazon_bedrock.rbs +21 -25
  153. data/sig/generated/riffer/providers/anthropic.rbs +2 -7
  154. data/sig/generated/riffer/providers/azure_open_ai.rbs +3 -18
  155. data/sig/generated/riffer/providers/base.rbs +9 -25
  156. data/sig/generated/riffer/providers/gemini.rbs +0 -2
  157. data/sig/generated/riffer/providers/mock.rbs +6 -26
  158. data/sig/generated/riffer/providers/open_ai.rbs +1 -5
  159. data/sig/generated/riffer/providers/open_router.rbs +4 -10
  160. data/sig/generated/riffer/providers/repository.rbs +2 -3
  161. data/sig/generated/riffer/providers/token_usage.rbs +6 -16
  162. data/sig/generated/riffer/providers.rbs +0 -8
  163. data/sig/generated/riffer/runner/fibers.rbs +8 -15
  164. data/sig/generated/riffer/runner/sequential.rbs +1 -3
  165. data/sig/generated/riffer/runner/threaded.rbs +3 -13
  166. data/sig/generated/riffer/runner.rbs +2 -14
  167. data/sig/generated/riffer/skills/activate_tool.rbs +2 -11
  168. data/sig/generated/riffer/skills/adapter.rbs +4 -22
  169. data/sig/generated/riffer/skills/backend.rbs +7 -21
  170. data/sig/generated/riffer/skills/config.rbs +10 -31
  171. data/sig/generated/riffer/skills/context.rbs +5 -20
  172. data/sig/generated/riffer/skills/filesystem_backend.rbs +7 -24
  173. data/sig/generated/riffer/skills/frontmatter.rbs +10 -27
  174. data/sig/generated/riffer/skills/markdown_adapter.rbs +2 -9
  175. data/sig/generated/riffer/skills/xml_adapter.rbs +2 -8
  176. data/sig/generated/riffer/stream_events/base.rbs +1 -6
  177. data/sig/generated/riffer/stream_events/guardrail_modification.rbs +1 -8
  178. data/sig/generated/riffer/stream_events/guardrail_tripwire.rbs +1 -8
  179. data/sig/generated/riffer/stream_events/interrupt.rbs +4 -7
  180. data/sig/generated/riffer/stream_events/reasoning_delta.rbs +2 -4
  181. data/sig/generated/riffer/stream_events/reasoning_done.rbs +2 -4
  182. data/sig/generated/riffer/stream_events/skill_activation.rbs +2 -4
  183. data/sig/generated/riffer/stream_events/text_delta.rbs +0 -2
  184. data/sig/generated/riffer/stream_events/text_done.rbs +1 -3
  185. data/sig/generated/riffer/stream_events/token_usage_done.rbs +1 -7
  186. data/sig/generated/riffer/stream_events/tool_call_delta.rbs +2 -3
  187. data/sig/generated/riffer/stream_events/tool_call_done.rbs +1 -3
  188. data/sig/generated/riffer/stream_events/web_search_done.rbs +1 -3
  189. data/sig/generated/riffer/stream_events/web_search_status.rbs +2 -3
  190. data/sig/generated/riffer/stream_events.rbs +0 -10
  191. data/sig/generated/riffer/tool.rbs +5 -12
  192. data/sig/generated/riffer/tools/response.rbs +6 -4
  193. data/sig/generated/riffer/tools/runtime/fibers.rbs +0 -3
  194. data/sig/generated/riffer/tools/runtime/inline.rbs +1 -3
  195. data/sig/generated/riffer/tools/runtime/threaded.rbs +0 -2
  196. data/sig/generated/riffer/tools/runtime.rbs +5 -37
  197. data/sig/generated/riffer/tools/toolable.rbs +4 -14
  198. data/sig/generated/riffer/tools.rbs +0 -4
  199. data/sig/generated/riffer.rbs +5 -4
  200. data/sig/manual/riffer/agent/session/repair.rbs +5 -0
  201. data/sig/manual/riffer/evals/evaluator_runner.rbs +5 -0
  202. data/sig/manual/riffer/helpers/class_name_converter.rbs +5 -0
  203. data/sig/manual/riffer/helpers/dependencies.rbs +5 -0
  204. data/sig/manual/riffer/mcp/authenticated_tool.rbs +5 -0
  205. data/sig/manual/riffer/mcp/registry.rbs +5 -0
  206. data/sig/manual/riffer/mcp/tool_factory.rbs +5 -0
  207. data/sig/manual/riffer/mcp.rbs +5 -0
  208. data/sig/manual/riffer/providers/repository.rbs +5 -0
  209. data/sig/manual/riffer.rbs +5 -0
  210. metadata +17 -9
  211. data/.agents/rdoc.md +0 -69
  212. data/lib/riffer/messages/converter.rb +0 -90
  213. data/sig/generated/riffer/messages/converter.rbs +0 -33
  214. data/sig/manual/riffer/tools/toolable.rbs +0 -6
@@ -1,38 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Typed value object wrapping the runtime context Hash held by a
5
- # Riffer::Agent. Exposes first-class accessors for the framework-managed
6
- # entries — +skills+ and +token_usage+ and preserves +#[]+ / +#dig+
7
- # reads so tools (which receive +context:+ as a keyword) keep working
8
- # with both built-in and caller-provided keys.
9
- #
10
- # Reserved keys (+:skills+, +:token_usage+) cannot be set by the caller
11
- # at construction; they are owned by Riffer and written through the typed
12
- # setters. Type invariants are enforced on write — +skills+ must be a
13
- # +Riffer::Skills::Context+ (or nil); +token_usage+ must be a
14
- # +Riffer::Providers::TokenUsage+ (or nil).
15
- #
16
- # context = Riffer::Agent::Context.new(user_id: 42)
17
- # context[:user_id] # => 42
18
- # context.skills # => nil
19
- # context.token_usage # => nil
20
- #
4
+ # Typed value object wrapping the runtime context Hash held by a Riffer::Agent.
5
+ # Exposes typed +skills+, +token_usage+, +mcp_progressive_tools+, and
6
+ # +discovered_tools+ accessors while preserving +#[]+ / +#dig+ for caller-provided keys.
21
7
  class Riffer::Agent::Context
22
8
  # @rbs @data: Hash[Symbol, untyped]
23
9
 
24
- # Keys reserved for framework use. Passing any of these to the
25
- # constructor raises +Riffer::ArgumentError+.
26
- RESERVED_KEYS = [:skills, :token_usage].freeze #: Array[Symbol]
10
+ RESERVED_KEYS = [:skills, :token_usage, :mcp_progressive_tools, :discovered_tools].freeze #: Array[Symbol]
27
11
 
28
- # Builds a new context.
29
- #
30
- # [data] caller-provided Hash passed as <tt>Agent.new(context:)</tt>.
31
- # Duped before storage so caller mutations do not affect the
32
- # agent. Must not contain any +RESERVED_KEYS+.
33
- #
34
- # Raises Riffer::ArgumentError when +data+ contains a reserved key.
35
- #
12
+ # Builds a new context. The caller Hash is duped so later caller mutations
13
+ # don't leak in. Raises Riffer::ArgumentError if it contains a reserved key.
36
14
  #--
37
15
  #: (?Hash[Symbol, untyped]) -> void
38
16
  def initialize(data = {})
@@ -45,6 +23,8 @@ class Riffer::Agent::Context
45
23
  @data = data.dup
46
24
  @data[:skills] = nil
47
25
  @data[:token_usage] = nil
26
+ @data[:mcp_progressive_tools] = nil
27
+ @data[:discovered_tools] = nil
48
28
  end
49
29
 
50
30
  # The agent's resolved +Riffer::Skills::Context+, or +nil+ when skills
@@ -56,12 +36,8 @@ class Riffer::Agent::Context
56
36
  @data[:skills]
57
37
  end
58
38
 
59
- # Sets the resolved skills context. Called once by +Riffer::Agent+
60
- # during construction.
61
- #
62
- # Raises Riffer::ArgumentError if +value+ is neither +nil+ nor a
63
- # +Riffer::Skills::Context+.
64
- #
39
+ # Sets the resolved skills context. Raises Riffer::ArgumentError on an
40
+ # invalid value.
65
41
  #--
66
42
  #: (Riffer::Skills::Context?) -> Riffer::Skills::Context?
67
43
  def skills=(value)
@@ -81,12 +57,8 @@ class Riffer::Agent::Context
81
57
  @data[:token_usage]
82
58
  end
83
59
 
84
- # Sets the cumulative token usage. Called by +Riffer::Agent::Run+ after
85
- # each LLM response.
86
- #
87
- # Raises Riffer::ArgumentError if +value+ is neither +nil+ nor a
88
- # +Riffer::Providers::TokenUsage+.
89
- #
60
+ # Sets the cumulative token usage. Raises Riffer::ArgumentError on an invalid
61
+ # value.
90
62
  #--
91
63
  #: (Riffer::Providers::TokenUsage?) -> Riffer::Providers::TokenUsage?
92
64
  def token_usage=(value)
@@ -97,28 +69,76 @@ class Riffer::Agent::Context
97
69
  @data[:token_usage] = value
98
70
  end
99
71
 
100
- # Hash-style read. Preserved so downstream tool runtimes pulling
101
- # caller-provided keys via <tt>context[:agent]</tt> or
102
- # <tt>context[:tenant]</tt> keep working unchanged.
103
- #
72
+ # Hash-style read, preserved so tools can pull caller-provided keys via
73
+ # <tt>context[:agent]</tt>.
104
74
  #--
105
75
  #: (Symbol) -> untyped
106
76
  def [](key)
107
77
  @data[key]
108
78
  end
109
79
 
110
- # Hash-style dig. Preserved for tools using
111
- # <tt>context&.dig(:user_id)</tt>.
112
- #
80
+ # Auth-wrapped MCP tool classes for progressive discovery, or +nil+.
81
+ #--
82
+ #: () -> Array[singleton(Riffer::Tool)]?
83
+ def mcp_progressive_tools
84
+ @data[:mcp_progressive_tools]
85
+ end
86
+
87
+ # Sets progressive MCP tools. Raises Riffer::ArgumentError on an invalid value.
88
+ #--
89
+ #: (Array[singleton(Riffer::Tool)]?) -> Array[singleton(Riffer::Tool)]?
90
+ def mcp_progressive_tools=(value)
91
+ valid = value.nil? || (
92
+ value.is_a?(Array) &&
93
+ value.all? { |tool| tool.is_a?(Class) && tool < Riffer::Tool }
94
+ )
95
+ unless valid
96
+ raise Riffer::ArgumentError,
97
+ "mcp_progressive_tools must be an Array of Riffer::Tool subclasses or nil, got #{value.class}"
98
+ end
99
+ @data[:mcp_progressive_tools] = value
100
+ end
101
+
102
+ # MCP tool classes discovered during progressive search. Accumulates across
103
+ # +generate+ calls and is merged into the active tool list on every LLM call.
104
+ #--
105
+ #: () -> Array[singleton(Riffer::Tool)]?
106
+ def discovered_tools
107
+ @data[:discovered_tools]
108
+ end
109
+
110
+ # Sets the discovered tools array. Raises Riffer::ArgumentError on an invalid value.
111
+ #--
112
+ #: (Array[singleton(Riffer::Tool)]?) -> Array[singleton(Riffer::Tool)]?
113
+ def discovered_tools=(value)
114
+ valid = value.nil? || (
115
+ value.is_a?(Array) &&
116
+ value.all? { |tool| tool.is_a?(Class) && tool < Riffer::Tool }
117
+ )
118
+ unless valid
119
+ raise Riffer::ArgumentError,
120
+ "discovered_tools must be an Array of Riffer::Tool subclasses or nil, got #{value.class}"
121
+ end
122
+ @data[:discovered_tools] = value
123
+ end
124
+
125
+ # Accumulates newly discovered MCP tool classes, deduplicating by name.
126
+ # Each call extends the existing set; calling multiple times is safe.
127
+ #--
128
+ #: (Array[singleton(Riffer::Tool)]) -> Array[singleton(Riffer::Tool)]
129
+ def discover_tools(tools)
130
+ existing = @data[:discovered_tools] || []
131
+ @data[:discovered_tools] = (existing + tools).uniq(&:name)
132
+ end
133
+
113
134
  #--
114
135
  #: (*Symbol) -> untyped
115
136
  def dig(*keys)
116
137
  @data.dig(*keys)
117
138
  end
118
139
 
119
- # Returns a copy of the underlying Hash. Mutating the result does not
120
- # affect this context.
121
- #
140
+ # Returns a copy of the underlying Hash; mutating it does not affect this
141
+ # context.
122
142
  #--
123
143
  #: () -> Hash[Symbol, untyped]
124
144
  def to_h
@@ -1,10 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Wraps agent generation responses with optional tripwire information.
5
- #
6
- # When guardrails block execution, the response will contain a tripwire
7
- # with details about the block. The content will be empty for blocked responses.
4
+ # Wraps an agent generation response. When a guardrail blocks execution,
5
+ # +content+ is empty and +tripwire+ carries the block details.
8
6
  #
9
7
  # response = agent.generate("Hello")
10
8
  # if response.blocked?
@@ -33,24 +31,10 @@ class Riffer::Agent::Response
33
31
  # The full message history from the agent conversation.
34
32
  attr_reader :messages #: Array[Riffer::Messages::Base]
35
33
 
36
- # Call ids of tool_use blocks that riffer filled with placeholder
37
- # results during this turn — populated when an interrupt left them
38
- # unanswered and +Riffer.config.experimental_history_healing+ is on.
39
- # Empty otherwise.
34
+ # Call ids of tool_use blocks riffer filled with placeholder results this
35
+ # turn (when an interrupt left them unanswered and history healing is on).
40
36
  attr_reader :healed_tool_call_ids #: Array[String]
41
37
 
42
- # Creates a new response.
43
- #
44
- # [content] the response content.
45
- # [tripwire] optional tripwire for blocked responses.
46
- # [modifications] guardrail modifications applied during processing.
47
- # [interrupted] whether the agent loop was interrupted by a callback.
48
- # [interrupt_reason] optional reason passed via <tt>throw :riffer_interrupt, reason</tt>.
49
- # [structured_output] parsed structured output when structured output is configured.
50
- # [messages] the full message history from the agent conversation.
51
- # [healed_tool_call_ids] call ids filled with placeholder tool results
52
- # when history healing is enabled.
53
- #
54
38
  #--
55
39
  #: (String, ?tripwire: Riffer::Guardrails::Tripwire?, ?modifications: Array[Riffer::Guardrails::Modification], ?interrupted: bool, ?interrupt_reason: (String | Symbol)?, ?structured_output: Hash[Symbol, untyped]?, ?messages: Array[Riffer::Messages::Base], ?healed_tool_call_ids: Array[String]) -> void
56
40
  def initialize(content, tripwire: nil, modifications: [], interrupted: false, interrupt_reason: nil, structured_output: nil, messages: [], healed_tool_call_ids: [])
@@ -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,56 +3,29 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # Riffer::Agent::Serializer turns a resolved agent into a self-contained,
7
- # provider-neutral data dict 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 dict carries only data — no Procs, no class references, no tool
12
- # runtime. The same dict 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
- # dict = Riffer::Agent::Serializer.to_h(agent: agent)
22
- # rebuilt = Riffer::Agent::Serializer.from_h(dict, 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
- # dict 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
- # Raised by +from_h+ when the dict's +schema_version+ is unsupported.
19
+ # Raised by +from_h+ when the hash's +schema_version+ is unsupported.
40
20
  class VersionError < Riffer::ArgumentError; end
41
21
 
42
22
  # The default +tool_resolver+: synthesizes a body-less tool shell from a
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 dict.
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 dict
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,61 +44,45 @@ module Riffer::Agent::Serializer
71
44
  }
72
45
  end
73
46
 
74
- # Reconstructs a runnable agent from a wire dict.
75
- #
76
- # [hash] a Symbol-keyed wire dict (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 dict 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
- # [tool_resolver] maps a tool descriptor to a Riffer::Tool class. Defaults
83
- # to DEFAULT_TOOL_RESOLVER (body-less shells). Pass a registry lookup to
84
- # rebuild real, in-process tools.
85
- # [tool_runtime] an optional Riffer::Tools::Runtime to inject (e.g. a
86
- # remote runtime for shells). When omitted, the agent uses the configured
87
- # default (+Riffer.config.tool_runtime+).
88
- #
89
- # Raises Riffer::Agent::Serializer::VersionError on an unsupported
90
- # +schema_version+, and Riffer::ArgumentError on a malformed dict.
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+.
91
52
  #
92
53
  #--
93
- #: (Hash[Symbol, untyped], ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
94
- def from_h(hash, context: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
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
55
+ def from_h(hash, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
95
56
  # Version -> decoder dispatch. Adding a +when 2+ arm (a backwards-compatible
96
- # decoder) is how a future breaking change keeps older dicts readable.
57
+ # decoder) is how a future breaking change keeps older hashes readable.
97
58
  case hash[:schema_version]
98
59
  when SCHEMA_VERSION
99
- decode_v1(hash, context: context, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
60
+ decode_v1(hash, context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
100
61
  else
101
62
  raise VersionError, "Unsupported schema_version: #{hash[:schema_version].inspect} (this Riffer supports #{SCHEMA_VERSION})"
102
63
  end
103
64
  end
104
65
 
105
- # Snapshots a resolved agent to a JSON string. Convenience over
106
- # <tt>JSON.generate(to_h(agent:))</tt>.
107
- #
66
+ # Snapshots a resolved agent to a JSON string.
108
67
  #--
109
68
  #: (agent: Riffer::Agent) -> String
110
69
  def to_json(agent:)
111
70
  JSON.generate(to_h(agent: agent))
112
71
  end
113
72
 
114
- # Reconstructs a runnable agent from a JSON string produced by +to_json+.
115
- # 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
116
74
  # +from_h+ for the arguments.
117
- #
118
75
  #--
119
- #: (String, ?context: Hash[Symbol, untyped]?, ?tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), ?tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
120
- def from_json(json, context: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
121
- from_h(JSON.parse(json, symbolize_names: true), context: context, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
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
77
+ def from_json(json, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
78
+ from_h(JSON.parse(json, symbolize_names: true), context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
122
79
  end
123
80
 
124
81
  private
125
82
 
126
83
  #--
127
- #: (Hash[Symbol, untyped], context: Hash[Symbol, untyped]?, tool_resolver: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool), tool_runtime: (singleton(Riffer::Tools::Runtime) | Riffer::Tools::Runtime | Proc)?) -> Riffer::Agent
128
- def decode_v1(hash, context:, tool_resolver:, tool_runtime:)
84
+ #: (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
85
+ def decode_v1(hash, context:, session:, tool_resolver:, tool_runtime:)
129
86
  tools = Array(hash[:tools]).map { |descriptor| tool_resolver.call(descriptor) }
130
87
 
131
88
  config_args = {
@@ -142,7 +99,11 @@ module Riffer::Agent::Serializer
142
99
  # Config default (Riffer.config.tool_runtime) applies.
143
100
  config_args[:tool_runtime] = tool_runtime if tool_runtime
144
101
 
145
- Riffer::Agent.new(config: Riffer::Agent::Config.new(**config_args), context: context)
102
+ # +session+ is forwarded verbatim: when nil, Agent.new seeds a fresh session
103
+ # from the decoded instructions; when supplied, Agent.new uses it as-is to
104
+ # resume persisted history. The hash never carries history (see "What does
105
+ # not transfer"), so this is the only seam for rehydrating a conversation.
106
+ Riffer::Agent.new(config: Riffer::Agent::Config.new(**config_args), context: context, session: session)
146
107
  end
147
108
 
148
109
  #--
@@ -152,20 +113,16 @@ module Riffer::Agent::Serializer
152
113
  Riffer::Params.from_json_schema(schema)
153
114
  end
154
115
 
155
- # The DSL represents unlimited steps as +nil+, but the wire encodes it as
156
- # +-1+ so the dict stays portable across transports where JSON +null+ is
157
- # awkward (e.g. proto3, which can't tell null from an absent field). The
158
- # magic value lives only on the wire — +encode_max_steps+/+decode_max_steps+
159
- # 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).
160
118
  #--
161
119
  #: (Numeric?) -> Numeric
162
120
  def encode_max_steps(value)
163
121
  value.nil? ? -1 : value
164
122
  end
165
123
 
166
- # Reverses +encode_max_steps+: +-1+ (or a literal +null+) means unlimited.
167
- # An absent key falls back to the default — a partial dict must not silently
168
- # 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.
169
126
  #--
170
127
  #: (Hash[Symbol, untyped]) -> Numeric?
171
128
  def decode_max_steps(hash)
@@ -179,12 +136,6 @@ module Riffer::Agent::Serializer
179
136
  tool_class.to_tool_schema(strict: false).merge(timeout: tool_class.timeout)
180
137
  end
181
138
 
182
- # Builds an anonymous, body-less Riffer::Tool subclass that advertises the
183
- # descriptor's schema to the LLM. Its +#call+ raises — a shell only has
184
- # identity, not behavior; route its calls through a remote runtime.
185
- #
186
- # Returns +untyped+: steep can't see that +Class.new(Riffer::Tool)+ is a
187
- # +singleton(Riffer::Tool)+ (cf. Riffer::Mcp::ToolFactory#build_tool_class).
188
139
  #--
189
140
  #: (Hash[Symbol, untyped]) -> untyped
190
141
  def build_tool_shell(descriptor)