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,16 +1,12 @@
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 Riffer::Agent.
5
- # Exposes typed +skills+, +token_usage+, +mcp_progressive_tools+, and
6
- # +discovered_tools+ accessors while preserving +#[]+ / +#dig+ for caller-provided keys.
7
4
  class Riffer::Agent::Context
8
5
  # @rbs @data: Hash[Symbol, untyped]
9
6
 
10
7
  RESERVED_KEYS = %i[skills token_usage mcp_progressive_tools discovered_tools].freeze #: Array[Symbol]
11
8
 
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.
9
+ # Raises Riffer::ArgumentError if +data+ contains a reserved key.
14
10
  #--
15
11
  #: (?Hash[Symbol, untyped]) -> void
16
12
  def initialize(data = {})
@@ -27,17 +23,12 @@ class Riffer::Agent::Context
27
23
  @data[:discovered_tools] = nil
28
24
  end
29
25
 
30
- # The agent's resolved +Riffer::Skills::Context+, or +nil+ when skills
31
- # are not configured.
32
- #
33
26
  #--
34
27
  #: () -> Riffer::Skills::Context?
35
28
  def skills
36
29
  @data[:skills]
37
30
  end
38
31
 
39
- # Sets the resolved skills context. Raises Riffer::ArgumentError on an
40
- # invalid value.
41
32
  #--
42
33
  #: (Riffer::Skills::Context?) -> Riffer::Skills::Context?
43
34
  def skills=(value)
@@ -48,17 +39,13 @@ class Riffer::Agent::Context
48
39
  @data[:skills] = value
49
40
  end
50
41
 
51
- # The cumulative +Riffer::Providers::TokenUsage+ across every Run on this agent,
52
- # or +nil+ before the first response is recorded.
53
- #
42
+ # Cumulative across every run on this agent.
54
43
  #--
55
44
  #: () -> Riffer::Providers::TokenUsage?
56
45
  def token_usage
57
46
  @data[:token_usage]
58
47
  end
59
48
 
60
- # Sets the cumulative token usage. Raises Riffer::ArgumentError on an invalid
61
- # value.
62
49
  #--
63
50
  #: (Riffer::Providers::TokenUsage?) -> Riffer::Providers::TokenUsage?
64
51
  def token_usage=(value)
@@ -69,22 +56,18 @@ class Riffer::Agent::Context
69
56
  @data[:token_usage] = value
70
57
  end
71
58
 
72
- # Hash-style read, preserved so tools can pull caller-provided keys via
73
- # <tt>context[:agent]</tt>.
74
59
  #--
75
60
  #: (Symbol) -> untyped
76
61
  def [](key)
77
62
  @data[key]
78
63
  end
79
64
 
80
- # Auth-wrapped MCP tool classes for progressive discovery, or +nil+.
81
65
  #--
82
66
  #: () -> Array[singleton(Riffer::Tool)]?
83
67
  def mcp_progressive_tools
84
68
  @data[:mcp_progressive_tools]
85
69
  end
86
70
 
87
- # Sets progressive MCP tools. Raises Riffer::ArgumentError on an invalid value.
88
71
  #--
89
72
  #: (Array[singleton(Riffer::Tool)]?) -> Array[singleton(Riffer::Tool)]?
90
73
  def mcp_progressive_tools=(value)
@@ -99,15 +82,12 @@ class Riffer::Agent::Context
99
82
  @data[:mcp_progressive_tools] = value
100
83
  end
101
84
 
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
85
  #--
105
86
  #: () -> Array[singleton(Riffer::Tool)]?
106
87
  def discovered_tools
107
88
  @data[:discovered_tools]
108
89
  end
109
90
 
110
- # Sets the discovered tools array. Raises Riffer::ArgumentError on an invalid value.
111
91
  #--
112
92
  #: (Array[singleton(Riffer::Tool)]?) -> Array[singleton(Riffer::Tool)]?
113
93
  def discovered_tools=(value)
@@ -122,8 +102,6 @@ class Riffer::Agent::Context
122
102
  @data[:discovered_tools] = value
123
103
  end
124
104
 
125
- # Accumulates newly discovered MCP tool classes, deduplicating by name.
126
- # Each call extends the existing set; calling multiple times is safe.
127
105
  #--
128
106
  #: (Array[singleton(Riffer::Tool)]) -> Array[singleton(Riffer::Tool)]
129
107
  def discover_tools(tools)
@@ -137,8 +115,6 @@ class Riffer::Agent::Context
137
115
  @data.dig(*keys)
138
116
  end
139
117
 
140
- # Returns a copy of the underlying Hash; mutating it does not affect this
141
- # context.
142
118
  #--
143
119
  #: () -> Hash[Symbol, untyped]
144
120
  def to_h
@@ -1,36 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # How a run ended — the single place to read whether the agent completed
5
- # normally and, if not, why. +detail+ carries the specifics when there are any:
6
- # the tripwire reason, the interrupt reason, the provider's raw finish value,
7
- # or the structured output parse/validation error.
8
- #
9
- # response = agent.generate("Analyze this")
10
- # case response.outcome.reason
11
- # when :completed then puts response.structured_output
12
- # when :invalid_structured_output then warn response.outcome.detail
13
- # end
14
4
  class Riffer::Agent::Outcome
15
- # Finish reasons that end a turn normally; every other finish reason means the
16
- # provider cut the turn short and surfaces as the run's outcome verbatim.
17
5
  NORMAL_FINISH_REASONS = %i[stop tool_calls].freeze #: Array[Symbol]
18
6
 
19
- # Derived from the provider vocabulary so a new finish reason becomes an
20
- # outcome without a second list to update.
21
7
  PROVIDER_STOP_REASONS = (Riffer::Providers::FinishReason::VALUES - NORMAL_FINISH_REASONS).freeze #: Array[Symbol]
22
8
 
23
- # The vocabulary every run ends in.
24
9
  VALUES = (%i[completed guardrail_blocked interrupted max_steps invalid_structured_output] +
25
10
  PROVIDER_STOP_REASONS).freeze #: Array[Symbol]
26
11
 
27
- # Why the run ended.
28
12
  attr_reader :reason #: Symbol # @dynamic reason
29
13
 
30
- # Human-readable specifics for +reason+, when there are any.
31
14
  attr_reader :detail #: String? # @dynamic detail
32
15
 
33
- # Raises Riffer::ArgumentError when +reason+ is outside VALUES.
34
16
  #--
35
17
  #: (reason: Symbol, ?detail: String?) -> void
36
18
  def initialize(reason:, detail: nil)
@@ -42,8 +24,6 @@ class Riffer::Agent::Outcome
42
24
  @detail = detail
43
25
  end
44
26
 
45
- # Returns true when the run completed normally.
46
- #
47
27
  #--
48
28
  #: () -> bool
49
29
  def success?
@@ -1,55 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # Wraps an agent generation response. +outcome+ says how the run ended; when a
5
- # guardrail blocks execution, +content+ is empty and +tripwire+ carries the
6
- # block details.
7
- #
8
- # response = agent.generate("Hello")
9
- # if response.outcome.success?
10
- # puts response.content
11
- # else
12
- # puts "#{response.outcome.reason}: #{response.outcome.detail}"
13
- # end
14
4
  class Riffer::Agent::Response
15
- # The response content.
16
5
  attr_reader :content #: String # @dynamic content
17
-
18
- # How the run ended.
19
6
  attr_reader :outcome #: Riffer::Agent::Outcome # @dynamic outcome
20
-
21
- # The tripwire if execution was blocked.
22
7
  attr_reader :tripwire #: Riffer::Guardrails::Tripwire? # @dynamic tripwire
23
-
24
- # The modifications made by guardrails during processing.
25
8
  attr_reader :modifications #: Array[Riffer::Guardrails::Modification] # @dynamic modifications
26
-
27
- # The parsed structured output, if structured output was configured.
9
+ attr_reader :reasoning #: Array[Riffer::Messages::Assistant::ReasoningPart] # @dynamic reasoning
28
10
  attr_reader :structured_output #: Hash[Symbol, untyped]? # @dynamic structured_output
29
-
30
- # The aggregate token usage across this run's LLM calls, if any was reported.
31
11
  attr_reader :token_usage #: Riffer::Providers::TokenUsage? # @dynamic token_usage
32
-
33
- # The number of LLM calls made during this run (0 when a before-guardrail
34
- # blocks before any call). Distinct from the session's cumulative step count.
35
12
  attr_reader :steps #: Integer # @dynamic steps
36
-
37
- # The full message history from the agent conversation.
38
13
  attr_reader :messages #: Array[Riffer::Messages::Base] # @dynamic messages
39
14
 
40
- # Call ids of tool_use blocks riffer filled with placeholder results this
41
- # turn (when an interrupt left them unanswered and history healing is on).
42
- attr_reader :healed_tool_call_ids #: Array[String] # @dynamic healed_tool_call_ids
43
-
44
15
  #--
45
16
  #: (
46
17
  # String,
47
18
  # outcome: Riffer::Agent::Outcome,
48
19
  # ?tripwire: Riffer::Guardrails::Tripwire?,
49
20
  # ?modifications: Array[Riffer::Guardrails::Modification],
21
+ # ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
50
22
  # ?structured_output: Hash[Symbol, untyped]?,
51
23
  # ?messages: Array[Riffer::Messages::Base],
52
- # ?healed_tool_call_ids: Array[String],
53
24
  # ?token_usage: Riffer::Providers::TokenUsage?,
54
25
  # ?steps: Integer
55
26
  # ) -> void
@@ -58,9 +29,9 @@ class Riffer::Agent::Response
58
29
  outcome:,
59
30
  tripwire: nil,
60
31
  modifications: [],
32
+ reasoning: [],
61
33
  structured_output: nil,
62
34
  messages: [],
63
- healed_tool_call_ids: [],
64
35
  token_usage: nil,
65
36
  steps: 0
66
37
  )
@@ -68,15 +39,13 @@ class Riffer::Agent::Response
68
39
  @outcome = outcome
69
40
  @tripwire = tripwire
70
41
  @modifications = modifications
42
+ @reasoning = reasoning
71
43
  @structured_output = structured_output
72
44
  @messages = messages
73
- @healed_tool_call_ids = healed_tool_call_ids
74
45
  @token_usage = token_usage
75
46
  @steps = steps
76
47
  end
77
48
 
78
- # Returns true if any guardrail modified data during processing.
79
- #
80
49
  #--
81
50
  #: () -> bool
82
51
  def modified?
@@ -1,32 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
4
- # The generation loop — a pure module of functions over an +agent+, which owns
5
- # every per-call value; Run just orchestrates.
6
4
  module Riffer::Agent::Run
7
5
  extend self
8
6
 
9
- # Runs the generate loop for the given agent. See Riffer::Agent#generate
10
- # for prompt/files semantics.
11
- #
12
7
  #--
13
- #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
8
+ #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Riffer::Agent::Response
14
9
  def generate(agent:, prompt: nil, files: nil, tags: {})
15
10
  append_user_message(agent, prompt, files: files)
16
11
  run_loop(agent, tags: tags)
17
12
  end
18
13
 
19
- # Runs the streaming loop for the given agent. See Riffer::Agent#stream
20
- # for prompt/files semantics.
21
- #
22
14
  #--
23
- #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
15
+ #: (agent: Riffer::Agent, ?prompt: String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?, ?tags: Hash[(String | Symbol), untyped]) -> Enumerator[Riffer::StreamEvents::Base, Riffer::Agent::Response]
24
16
  def stream(agent:, prompt: nil, files: nil, tags: {})
25
17
  append_user_message(agent, prompt, files: files)
26
18
  # The enumerator body runs in its own fiber, where the fiber-local OTEL
27
19
  # context is empty — capture here so the run span parents to the caller's
28
- # trace. tags ride as an ordinary argument captured in the closure, so they
29
- # cross the fiber boundary without any re-propagation.
20
+ # trace.
30
21
  trace_context = Riffer::Tracing.current_context
31
22
  Enumerator.new do |stream_yielder|
32
23
  Riffer::Tracing.with_context(trace_context) { run_loop(agent, tags: tags, stream_yielder: stream_yielder) }
@@ -35,17 +26,12 @@ module Riffer::Agent::Run
35
26
 
36
27
  private
37
28
 
38
- # Both +generate+ and +stream+ funnel here, so this is the single place raw
39
- # +tags+ are normalized. The clean <tt>String => String</tt> map is then
40
- # threaded to every span builder in the run as +riffer.tag.*+ and to each
41
- # provider call (via +merged_model_options+) for native request-metadata
42
- # mapping.
43
29
  #--
44
30
  #: (Riffer::Agent, ?tags: Hash[(String | Symbol), untyped]?, ?stream_yielder: Enumerator::Yielder?) -> Riffer::Agent::Response
45
31
  def run_loop(agent, tags: {}, stream_yielder: nil)
46
- tags = normalize_tags(tags)
32
+ tags = default_tags(agent).merge(normalize_tags(tags))
47
33
  Riffer::Tracing.in_span(
48
- "invoke_agent #{agent.class.identifier}",
34
+ "invoke_agent #{agent.identifier}",
49
35
  attributes: run_span_attributes(agent, tags),
50
36
  kind: :internal,
51
37
  ) do |span|
@@ -124,20 +110,12 @@ module Riffer::Agent::Run
124
110
  return final_response(agent, all_modifications, token_usage: run_usage, steps: run_steps)
125
111
  end
126
112
 
127
- new_messages, filled = Riffer::Agent::Session::Repair.fill_orphans(agent.session.messages)
128
- agent.session.set(new_messages)
129
- if stream_yielder
130
- stream_yielder << Riffer::StreamEvents::Interrupt.new(
131
- reason: reason,
132
- healed_tool_call_ids: filled,
133
- )
134
- end
113
+ stream_yielder << Riffer::StreamEvents::Interrupt.new(reason: reason) if stream_yielder
135
114
  final_response(
136
115
  agent,
137
116
  all_modifications,
138
117
  interrupted: true,
139
118
  interrupt_reason: reason,
140
- healed_tool_call_ids: filled,
141
119
  token_usage: run_usage,
142
120
  steps: run_steps,
143
121
  )
@@ -153,6 +131,7 @@ module Riffer::Agent::Run
153
131
  def accumulate_streamed_response(agent, stream_yielder, tags = {})
154
132
  accumulated_content = +""
155
133
  accumulated_tool_calls = [] #: Array[Riffer::Messages::Assistant::ToolCall]
134
+ accumulated_reasoning = [] #: Array[Riffer::Messages::Assistant::ReasoningPart]
156
135
  accumulated_token_usage = nil #: Riffer::Providers::TokenUsage?
157
136
  accumulated_finish_reason = nil #: Symbol?
158
137
  accumulated_finish_reason_raw = nil #: String?
@@ -162,12 +141,12 @@ module Riffer::Agent::Run
162
141
 
163
142
  case event
164
143
  when Riffer::StreamEvents::TextDelta
165
- # Append in place rather than += (which reallocates and copies the whole
166
- # buffer per delta, O(n^2) over a stream). accumulated_content stays an
167
- # owned buffer; replace (not =) on TextDone keeps it that way so a later
168
- # delta's << can never mutate the string held by a TextDone event.
144
+ # << rather than +=, which copies the whole buffer per delta (O(n^2)
145
+ # over a stream).
169
146
  accumulated_content << event.content
170
147
  when Riffer::StreamEvents::TextDone
148
+ # replace, not =, so a later delta's << can never mutate the string
149
+ # this event holds.
171
150
  accumulated_content.replace(event.content)
172
151
  when Riffer::StreamEvents::ToolCallDone
173
152
  accumulated_tool_calls << Riffer::Messages::Assistant::ToolCall.new(
@@ -175,6 +154,8 @@ module Riffer::Agent::Run
175
154
  name: event.name,
176
155
  arguments: event.arguments,
177
156
  )
157
+ when Riffer::StreamEvents::ReasoningDone
158
+ accumulated_reasoning << event.part
178
159
  when Riffer::StreamEvents::TokenUsageDone
179
160
  accumulated_token_usage = event.token_usage
180
161
  when Riffer::StreamEvents::FinishReasonDone
@@ -186,6 +167,7 @@ module Riffer::Agent::Run
186
167
  Riffer::Messages::Assistant.new(
187
168
  accumulated_content,
188
169
  tool_calls: accumulated_tool_calls,
170
+ reasoning: accumulated_reasoning,
189
171
  token_usage: accumulated_token_usage,
190
172
  finish_reason: accumulated_finish_reason,
191
173
  finish_reason_raw: accumulated_finish_reason_raw,
@@ -228,19 +210,18 @@ module Riffer::Agent::Run
228
210
  message&.content || "",
229
211
  outcome: final_outcome(message, result, interrupted: interrupted, interrupt_reason: interrupt_reason),
230
212
  modifications: all_modifications,
213
+ reasoning: message&.reasoning || [],
231
214
  structured_output: result&.object,
232
215
  **extra,
233
216
  )
234
217
  end
235
218
 
236
- # Checked in the order things happened. The loop being stopped (max_steps or
237
- # an interrupt) beats the provider's finish reason, which beats riffer's own
238
- # validation of the content. A truncated response that also fails the schema
239
- # therefore reports :length, not :invalid_structured_output.
240
219
  #--
241
220
  #: (Riffer::Messages::Assistant?, Riffer::Agent::StructuredOutput::Result?, interrupted: bool, interrupt_reason: (String | Symbol)?) -> Riffer::Agent::Outcome
242
221
  def final_outcome(message, result, interrupted:, interrupt_reason:)
243
222
  finish_reason = message&.finish_reason
223
+ # Precedence follows the order things happened: a stopped loop beats the
224
+ # provider's finish reason, which beats schema validation of the content.
244
225
  if interrupted && interrupt_reason == Riffer::Agent::INTERRUPT_MAX_STEPS
245
226
  Riffer::Agent::Outcome.new(reason: :max_steps)
246
227
  elsif interrupted
@@ -368,19 +349,23 @@ module Riffer::Agent::Run
368
349
  discovered.empty? ? agent.tools : agent.tools + discovered
369
350
  end
370
351
 
371
- # +tags+ rides in the options hash as a curated key the providers extract for
372
- # native request-metadata mapping (alongside +:structured_output+); it never
373
- # reaches an SDK call verbatim. Span tagging is threaded separately to each
374
- # builder.
375
352
  #--
376
353
  #: (Riffer::Agent, ?Hash[String, String]) -> Hash[Symbol, untyped]
377
354
  def merged_model_options(agent, tags = {})
378
355
  opts = agent.config.model_options.dup
379
356
  opts[:structured_output] = agent.structured_output if agent.structured_output
357
+ # Providers extract :tags for native request metadata; it never reaches an
358
+ # SDK call verbatim.
380
359
  opts[:tags] = tags unless tags.empty?
381
360
  opts
382
361
  end
383
362
 
363
+ #--
364
+ #: (Riffer::Agent) -> Hash[String, String]
365
+ def default_tags(agent)
366
+ { "kind" => "agent", "agent" => agent.identifier }
367
+ end
368
+
384
369
  #--
385
370
  #: (
386
371
  # Riffer::Agent,
@@ -388,8 +373,8 @@ module Riffer::Agent::Run
388
373
  # outcome: Riffer::Agent::Outcome,
389
374
  # ?tripwire: Riffer::Guardrails::Tripwire?,
390
375
  # ?modifications: Array[Riffer::Guardrails::Modification],
376
+ # ?reasoning: Array[Riffer::Messages::Assistant::ReasoningPart],
391
377
  # ?structured_output: Hash[Symbol, untyped]?,
392
- # ?healed_tool_call_ids: Array[String],
393
378
  # ?token_usage: Riffer::Providers::TokenUsage?,
394
379
  # ?steps: Integer
395
380
  # ) -> Riffer::Agent::Response
@@ -399,8 +384,8 @@ module Riffer::Agent::Run
399
384
  outcome:,
400
385
  tripwire: nil,
401
386
  modifications: [],
387
+ reasoning: [],
402
388
  structured_output: nil,
403
- healed_tool_call_ids: [],
404
389
  token_usage: nil,
405
390
  steps: 0
406
391
  )
@@ -410,23 +395,22 @@ module Riffer::Agent::Run
410
395
  outcome: outcome,
411
396
  tripwire: tripwire,
412
397
  modifications: modifications,
398
+ reasoning: reasoning,
413
399
  structured_output: structured_output,
414
400
  messages: messages.frozen? ? messages : messages.dup.freeze,
415
- healed_tool_call_ids: healed_tool_call_ids,
416
401
  token_usage: token_usage,
417
402
  steps: steps,
418
403
  )
419
404
  end
420
405
 
421
- # Raises when +files+ are supplied without a +prompt+ — the provider needs
422
- # text to anchor the attachments.
423
406
  #--
424
- #: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::FilePart]?) -> void
407
+ #: (Riffer::Agent, String?, ?files: Array[Hash[Symbol, untyped] | Riffer::Messages::User::FilePart]?) -> void
425
408
  def append_user_message(agent, prompt, files: nil)
409
+ # The provider needs text to anchor the attachments.
426
410
  raise Riffer::ArgumentError, "files: requires a prompt" if files && !files.empty? && prompt.nil?
427
411
  return unless prompt
428
412
 
429
- file_parts = (files || []).map { |f| Riffer::Messages::FilePart.from_hash(f) }
413
+ file_parts = (files || []).map { |f| Riffer::Messages::User::FilePart.from_hash(f) }
430
414
  agent.session.add(Riffer::Messages::User.new(prompt, files: file_parts), silent: true)
431
415
  end
432
416
 
@@ -451,7 +435,7 @@ module Riffer::Agent::Run
451
435
  def run_span_attributes(agent, tags = {})
452
436
  {
453
437
  "gen_ai.operation.name" => "invoke_agent",
454
- "gen_ai.agent.name" => agent.class.identifier,
438
+ "gen_ai.agent.name" => agent.identifier,
455
439
  "gen_ai.provider.name" => agent.provider.class.semconv_provider_name,
456
440
  "gen_ai.request.model" => agent.model_name,
457
441
  }.merge(tag_attributes(tags))
@@ -3,32 +3,20 @@
3
3
 
4
4
  require "json"
5
5
 
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.
9
- #
10
- # hash = Riffer::Agent::Serializer.to_h(agent: agent)
11
- # rebuilt = Riffer::Agent::Serializer.from_h(hash, context: {tenant: "acme"})
12
6
  module Riffer::Agent::Serializer
13
7
  extend self
14
8
 
15
- # The wire format version, bumped only on an incompatible change to the hash
16
- # shape; +from_h+ refuses any other version.
9
+ # Bump only on an incompatible change to the hash shape.
17
10
  SCHEMA_VERSION = 1 #: Integer
18
11
 
19
- # Raised by +from_h+ when the hash's +schema_version+ is unsupported.
20
12
  class VersionError < Riffer::ArgumentError; end
21
13
 
22
- # The default +tool_resolver+: synthesizes a body-less tool shell from a
23
- # descriptor. Its +#call+ raises — route shells through a remote runtime.
24
14
  DEFAULT_TOOL_RESOLVER = ->(descriptor) { build_tool_shell(descriptor) } #: ^(Hash[Symbol, untyped]) -> singleton(Riffer::Tool)
25
15
 
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.
29
16
  #--
30
17
  #: (agent: Riffer::Agent) -> Hash[Symbol, untyped]
31
18
  def to_h(agent:)
19
+ # Already resolved against the agent's context, so the hash carries plain data, never Procs.
32
20
  config = agent.config
33
21
  {
34
22
  schema_version: SCHEMA_VERSION,
@@ -43,17 +31,11 @@ module Riffer::Agent::Serializer
43
31
  }
44
32
  end
45
33
 
46
- # Reconstructs a runnable agent from a wire hash. +context+ is threaded into
47
- # tool dispatch (not used to re-resolve the already-resolved config);
48
- # +session+ seeds conversation history (the hash carries the agent definition,
49
- # not its history). Raises Riffer::Agent::Serializer::VersionError on an
50
- # unsupported +schema_version+.
51
- #
34
+ # Raises Riffer::Agent::Serializer::VersionError on an unsupported +schema_version+.
52
35
  #--
53
36
  #: (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
54
37
  def from_h(hash, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
55
- # Version -> decoder dispatch. Adding a +when 2+ arm (a backwards-compatible
56
- # decoder) is how a future breaking change keeps older hashes readable.
38
+ # One arm per supported version keeps older hashes decodable after a breaking change.
57
39
  case hash[:schema_version]
58
40
  when SCHEMA_VERSION
59
41
  decode_v1(hash, context: context, session: session, tool_resolver: tool_resolver, tool_runtime: tool_runtime)
@@ -63,15 +45,12 @@ module Riffer::Agent::Serializer
63
45
  end
64
46
  end
65
47
 
66
- # Snapshots a resolved agent to a JSON string.
67
48
  #--
68
49
  #: (agent: Riffer::Agent) -> String
69
50
  def to_json(agent:)
70
51
  JSON.generate(to_h(agent: agent))
71
52
  end
72
53
 
73
- # Reconstructs a runnable agent from a JSON string produced by +to_json+. See
74
- # +from_h+ for the arguments.
75
54
  #--
76
55
  #: (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
56
  def from_json(json, context: nil, session: nil, tool_resolver: DEFAULT_TOOL_RESOLVER, tool_runtime: nil)
@@ -100,14 +79,11 @@ module Riffer::Agent::Serializer
100
79
  max_steps: decode_max_steps(hash),
101
80
  tools_config: tools,
102
81
  } #: Hash[Symbol, untyped]
103
- # tool_runtime= rejects nil, so only inject when supplied; otherwise the
104
- # Config default (Riffer.config.tool_runtime) applies.
82
+ # Config#tool_runtime= rejects nil.
105
83
  config_args[:tool_runtime] = tool_runtime if tool_runtime
106
84
 
107
- # +session+ is forwarded verbatim: when nil, Agent.new seeds a fresh session
108
- # from the decoded instructions; when supplied, Agent.new uses it as-is to
109
- # resume persisted history. The hash never carries history (see "What does
110
- # not transfer"), so this is the only seam for rehydrating a conversation.
85
+ # The hash never carries history, so +session+ is the only seam for rehydrating a conversation.
86
+ # +context+ feeds tool dispatch only; the config was resolved before serialization.
111
87
  Riffer::Agent.new(config: Riffer::Agent::Config.new(**config_args), context: context, session: session)
112
88
  end
113
89
 
@@ -119,19 +95,17 @@ module Riffer::Agent::Serializer
119
95
  Riffer::Params.from_json_schema(schema)
120
96
  end
121
97
 
122
- # Encodes unlimited steps (+nil+ in the DSL) as +-1+ on the wire, where a
123
- # JSON +null+ is awkward across transports (e.g. proto3).
124
98
  #--
125
99
  #: (Numeric?) -> Numeric
126
100
  def encode_max_steps(value)
101
+ # A JSON null is awkward across transports (e.g. proto3), so unlimited travels as -1.
127
102
  value.nil? ? -1 : value
128
103
  end
129
104
 
130
- # Reverses +encode_max_steps+; a missing key falls back to the default so a
131
- # partial hash can't become an unbounded loop.
132
105
  #--
133
106
  #: (Hash[Symbol, untyped]) -> Numeric?
134
107
  def decode_max_steps(hash)
108
+ # A partial hash must not become an unbounded loop.
135
109
  return Riffer::Agent::Config::DEFAULT_MAX_STEPS unless hash.key?(:max_steps)
136
110
 
137
111
  hash[:max_steps] == -1 ? nil : hash[:max_steps]
@@ -151,10 +125,7 @@ module Riffer::Agent::Serializer
151
125
  schema = descriptor[:parameters_schema]
152
126
  tool_timeout = descriptor[:timeout]
153
127
 
154
- # An anonymous Riffer::Tool subclass is the idiom for synthesizing a tool
155
- # from data — the tool DSL is class-level, so there is no value-level
156
- # builder to type against. Same approach as Riffer::Mcp::ToolFactory;
157
- # steep can't type the dynamic class body, hence the ignore block.
128
+ # The tool DSL is class-level, so there is no value-level builder to synthesize a tool from data.
158
129
  Class.new(Riffer::Tool) do
159
130
  # steep:ignore:start
160
131
  identifier tool_name
@@ -1,24 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
  # rbs_inline: enabled
3
3
 
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.
4
+ # Maintains the invariant that every +tool_use+ has a matching +tool_result+
5
+ # and every +tool_result+ has a parent +tool_use+.
7
6
  module Riffer::Agent::Session::Repair
8
7
  extend self
9
8
 
10
- # Placeholder response filled in for an orphaned +tool_use+ on interrupt.
11
9
  ORPHAN_PLACEHOLDER = lambda { |_tool_call|
12
10
  Riffer::Tools::Response.error("Tool call interrupted before completion.", type: :interrupted)
13
11
  } #: ^(Riffer::Messages::Assistant::ToolCall) -> Riffer::Tools::Response
14
12
 
15
- # Fills each orphaned +tool_use+ in +messages+ with an +ORPHAN_PLACEHOLDER+
16
- # result inserted after its parent. Returns +[new_messages, filled_call_ids]+.
17
13
  #--
18
14
  #: (Array[Riffer::Messages::Base]) -> [Array[Riffer::Messages::Base], Array[String]]
19
15
  def fill_orphans(messages)
20
- return [messages, []] unless Riffer.config.experimental_history_healing
21
-
22
16
  result_ids = messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
23
17
  filled = [] #: Array[String]
24
18
  new_messages = [] #: Array[Riffer::Messages::Base]
@@ -45,15 +39,9 @@ module Riffer::Agent::Session::Repair
45
39
  [new_messages, filled]
46
40
  end
47
41
 
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.
52
42
  #--
53
43
  #: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
54
44
  def prune_orphans(messages)
55
- return messages unless Riffer.config.experimental_history_healing
56
-
57
45
  resume_boundary = (messages.length - 1).downto(0).find do |idx|
58
46
  m = messages[idx]
59
47
  m.is_a?(Riffer::Messages::Assistant) &&
@@ -67,7 +55,7 @@ module Riffer::Agent::Session::Repair
67
55
 
68
56
  strip_offenders = messages.each_with_index.flat_map do |m, idx|
69
57
  next [] unless m.is_a?(Riffer::Messages::Assistant) && !m.tool_calls.empty?
70
- next [] if idx == resume_boundary # preserve pending exchange
58
+ next [] if idx == resume_boundary # execute_pending_tool_calls still runs these
71
59
  next [] if m.tool_calls.all? { |tc| result_ids.include?(tc.call_id) }
72
60
 
73
61
  m.tool_calls.map(&:call_id)