riffer 0.27.2 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/.agents/architecture.md +18 -11
  3. data/.agents/code-style.md +1 -1
  4. data/.agents/rbs-inline.md +2 -2
  5. data/.agents/testing.md +9 -5
  6. data/.release-please-manifest.json +1 -1
  7. data/AGENTS.md +17 -10
  8. data/CHANGELOG.md +31 -0
  9. data/README.md +17 -18
  10. data/Steepfile +7 -1
  11. data/docs/03_AGENTS.md +34 -3
  12. data/docs/04_AGENT_LIFECYCLE.md +134 -86
  13. data/docs/05_AGENT_LOOP.md +2 -2
  14. data/docs/06_TOOLS.md +9 -4
  15. data/docs/07_TOOL_ADVANCED.md +23 -19
  16. data/docs/08_MESSAGES.md +28 -31
  17. data/docs/09_STREAM_EVENTS.md +1 -1
  18. data/docs/10_CONFIGURATION.md +25 -15
  19. data/docs/providers/01_PROVIDERS.md +6 -0
  20. data/docs/providers/06_MOCK_PROVIDER.md +2 -1
  21. data/docs/providers/07_CUSTOM_PROVIDERS.md +4 -4
  22. data/docs/providers/08_GEMINI.md +2 -2
  23. data/docs/providers/09_OPENROUTER.md +242 -0
  24. data/lib/riffer/agent/config.rb +173 -0
  25. data/lib/riffer/agent/context.rb +125 -0
  26. data/lib/riffer/agent/response.rb +11 -2
  27. data/lib/riffer/agent/run.rb +308 -0
  28. data/lib/riffer/agent/session/repair.rb +112 -0
  29. data/lib/riffer/agent/session.rb +268 -0
  30. data/lib/riffer/{structured_output → agent/structured_output}/result.rb +1 -1
  31. data/lib/riffer/{structured_output.rb → agent/structured_output.rb} +4 -4
  32. data/lib/riffer/agent.rb +246 -684
  33. data/lib/riffer/config.rb +56 -7
  34. data/lib/riffer/evals/evaluator.rb +13 -3
  35. data/lib/riffer/evals/judge.rb +2 -2
  36. data/lib/riffer/evals/run_result.rb +2 -1
  37. data/lib/riffer/evals/scenario_result.rb +2 -1
  38. data/lib/riffer/guardrails/runner.rb +3 -2
  39. data/lib/riffer/helpers/call_or_value.rb +16 -0
  40. data/lib/riffer/helpers.rb +0 -1
  41. data/lib/riffer/mcp/authenticated_tool.rb +4 -0
  42. data/lib/riffer/mcp/client.rb +1 -1
  43. data/lib/riffer/mcp/registration.rb +2 -3
  44. data/lib/riffer/mcp/registry.rb +3 -1
  45. data/lib/riffer/mcp/tool_factory.rb +5 -0
  46. data/lib/riffer/messages/assistant.rb +9 -3
  47. data/lib/riffer/messages/base.rb +22 -0
  48. data/lib/riffer/messages/converter.rb +6 -6
  49. data/lib/riffer/{file_part.rb → messages/file_part.rb} +5 -5
  50. data/lib/riffer/messages/tool.rb +1 -1
  51. data/lib/riffer/messages/user.rb +4 -4
  52. data/lib/riffer/{boolean.rb → params/boolean.rb} +3 -3
  53. data/lib/riffer/{param.rb → params/param.rb} +6 -6
  54. data/lib/riffer/params.rb +27 -21
  55. data/lib/riffer/providers/amazon_bedrock.rb +19 -20
  56. data/lib/riffer/providers/anthropic.rb +27 -28
  57. data/lib/riffer/providers/base.rb +10 -9
  58. data/lib/riffer/providers/gemini.rb +15 -12
  59. data/lib/riffer/providers/mock.rb +41 -13
  60. data/lib/riffer/providers/open_ai.rb +24 -22
  61. data/lib/riffer/providers/open_router.rb +318 -0
  62. data/lib/riffer/providers/repository.rb +1 -0
  63. data/lib/riffer/{token_usage.rb → providers/token_usage.rb} +4 -4
  64. data/lib/riffer/providers.rb +1 -0
  65. data/lib/riffer/runner/fibers.rb +4 -3
  66. data/lib/riffer/runner/sequential.rb +1 -1
  67. data/lib/riffer/runner/threaded.rb +1 -1
  68. data/lib/riffer/runner.rb +1 -1
  69. data/lib/riffer/skills/activate_tool.rb +4 -3
  70. data/lib/riffer/skills/config.rb +1 -1
  71. data/lib/riffer/skills/context.rb +3 -3
  72. data/lib/riffer/skills/filesystem_backend.rb +7 -5
  73. data/lib/riffer/skills/markdown_adapter.rb +1 -1
  74. data/lib/riffer/skills/xml_adapter.rb +1 -1
  75. data/lib/riffer/stream_events/interrupt.rb +10 -3
  76. data/lib/riffer/stream_events/token_usage_done.rb +2 -2
  77. data/lib/riffer/stream_events/web_search_status.rb +1 -1
  78. data/lib/riffer/tool.rb +3 -3
  79. data/lib/riffer/{tool_runtime → tools/runtime}/fibers.rb +2 -2
  80. data/lib/riffer/{tool_runtime → tools/runtime}/inline.rb +1 -1
  81. data/lib/riffer/{tool_runtime → tools/runtime}/threaded.rb +2 -2
  82. data/lib/riffer/{tool_runtime.rb → tools/runtime.rb} +21 -15
  83. data/lib/riffer/{toolable.rb → tools/toolable.rb} +12 -9
  84. data/lib/riffer/version.rb +1 -1
  85. data/lib/riffer.rb +2 -1
  86. data/sig/generated/riffer/agent/config.rbs +119 -0
  87. data/sig/generated/riffer/agent/context.rbs +91 -0
  88. data/sig/generated/riffer/agent/response.rbs +10 -2
  89. data/sig/generated/riffer/agent/run.rbs +144 -0
  90. data/sig/generated/riffer/agent/session/repair.rbs +51 -0
  91. data/sig/generated/riffer/agent/session.rbs +145 -0
  92. data/sig/generated/riffer/{structured_output → agent/structured_output}/result.rbs +2 -2
  93. data/sig/generated/riffer/{structured_output.rbs → agent/structured_output.rbs} +6 -6
  94. data/sig/generated/riffer/agent.rbs +154 -225
  95. data/sig/generated/riffer/config.rbs +50 -5
  96. data/sig/generated/riffer/evals/judge.rbs +2 -2
  97. data/sig/generated/riffer/helpers/call_or_value.rbs +9 -0
  98. data/sig/generated/riffer/helpers.rbs +0 -1
  99. data/sig/generated/riffer/messages/assistant.rbs +7 -3
  100. data/sig/generated/riffer/messages/base.rbs +18 -0
  101. data/sig/generated/riffer/messages/converter.rbs +4 -4
  102. data/sig/generated/riffer/{file_part.rbs → messages/file_part.rbs} +5 -5
  103. data/sig/generated/riffer/messages/user.rbs +4 -4
  104. data/sig/generated/riffer/params/boolean.rbs +10 -0
  105. data/sig/generated/riffer/{param.rbs → params/param.rbs} +3 -3
  106. data/sig/generated/riffer/params.rbs +15 -15
  107. data/sig/generated/riffer/providers/amazon_bedrock.rbs +22 -22
  108. data/sig/generated/riffer/providers/anthropic.rbs +4 -4
  109. data/sig/generated/riffer/providers/base.rbs +10 -10
  110. data/sig/generated/riffer/providers/gemini.rbs +4 -4
  111. data/sig/generated/riffer/providers/mock.rbs +25 -5
  112. data/sig/generated/riffer/providers/open_ai.rbs +4 -4
  113. data/sig/generated/riffer/providers/open_router.rbs +85 -0
  114. data/sig/generated/riffer/{token_usage.rbs → providers/token_usage.rbs} +5 -5
  115. data/sig/generated/riffer/providers.rbs +1 -0
  116. data/sig/generated/riffer/runner/fibers.rbs +2 -2
  117. data/sig/generated/riffer/runner/sequential.rbs +2 -2
  118. data/sig/generated/riffer/runner/threaded.rbs +2 -2
  119. data/sig/generated/riffer/runner.rbs +2 -2
  120. data/sig/generated/riffer/skills/activate_tool.rbs +4 -3
  121. data/sig/generated/riffer/skills/config.rbs +1 -1
  122. data/sig/generated/riffer/skills/context.rbs +2 -2
  123. data/sig/generated/riffer/stream_events/interrupt.rbs +7 -2
  124. data/sig/generated/riffer/stream_events/token_usage_done.rbs +3 -3
  125. data/sig/generated/riffer/tool.rbs +5 -5
  126. data/sig/generated/riffer/{tool_runtime → tools/runtime}/fibers.rbs +3 -3
  127. data/sig/generated/riffer/{tool_runtime → tools/runtime}/inline.rbs +2 -2
  128. data/sig/generated/riffer/{tool_runtime → tools/runtime}/threaded.rbs +3 -3
  129. data/sig/generated/riffer/{tool_runtime.rbs → tools/runtime.rbs} +19 -13
  130. data/sig/generated/riffer/{toolable.rbs → tools/toolable.rbs} +6 -6
  131. data/sig/stubs/agent_ivars.rbs +7 -0
  132. data/sig/stubs/async.rbs +24 -0
  133. data/sig/stubs/aws-sdk-core/seahorse_request_context.rbs +7 -0
  134. data/sig/stubs/aws-sdk-core/static_token_provider.rbs +5 -0
  135. data/sig/stubs/extend_self.rbs +11 -0
  136. data/sig/stubs/lib_ivars.rbs +101 -0
  137. data/sig/stubs/mcp_sdk.rbs +22 -0
  138. data/sig/stubs/provider_ivars.rbs +36 -0
  139. data/sig/stubs/provider_sdk_methods.rbs +50 -0
  140. data/sig/stubs/zeitwerk.rbs +12 -0
  141. metadata +54 -33
  142. data/lib/riffer/core.rb +0 -28
  143. data/lib/riffer/helpers/validations.rb +0 -18
  144. data/sig/generated/riffer/boolean.rbs +0 -10
  145. data/sig/generated/riffer/core.rbs +0 -19
  146. data/sig/generated/riffer/helpers/validations.rbs +0 -12
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Riffer::Agent::Session::Repair holds the pure transformations that keep the
5
+ # +tool_use+ ↔ +tool_result+ invariant on a message array. No state, no
6
+ # instance — module-level functions only. Each entry point is gated by
7
+ # +Riffer.config.experimental_history_healing+: when the flag is off the
8
+ # function returns its input unchanged.
9
+ #
10
+ # Two seams:
11
+ #
12
+ # - +fill_orphans+ — fills orphan +tool_use+ blocks with placeholder
13
+ # results. Used on interrupt (caller-issued or +max_steps+).
14
+ # - +prune_orphans+ — drops orphan +tool_use+ blocks and parentless Tool
15
+ # messages from a caller-provided seed so it is well-formed before the
16
+ # next inference call. Used at construction time when
17
+ # +Riffer::Agent.new(session:)+ receives a session.
18
+ module Riffer::Agent::Session::Repair
19
+ # Placeholder used to fill orphan +tool_use+ blocks. Emitted as the
20
+ # +Riffer::Tools::Response+ body for each filled call_id.
21
+ ORPHAN_PLACEHOLDER = ->(_tool_call) {
22
+ Riffer::Tools::Response.error("Tool call interrupted before completion.", type: :interrupted)
23
+ } #: ^(Riffer::Messages::Assistant::ToolCall) -> Riffer::Tools::Response
24
+
25
+ # Fills any orphaned +tool_use+ in +messages+ with the
26
+ # +ORPHAN_PLACEHOLDER+ response. Each placeholder Tool message is
27
+ # inserted immediately after its parent assistant message. Returns
28
+ # +[new_messages, filled_call_ids]+; +filled_call_ids+ is empty when
29
+ # there are no orphans.
30
+ #
31
+ # No-op when +Riffer.config.experimental_history_healing+ is off:
32
+ # returns +[messages, []]+ with the same array reference.
33
+ #
34
+ #--
35
+ #: (Array[Riffer::Messages::Base]) -> [Array[Riffer::Messages::Base], Array[String]]
36
+ def self.fill_orphans(messages)
37
+ return [messages, []] unless Riffer.config.experimental_history_healing
38
+
39
+ result_ids = messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
40
+ filled = [] #: Array[String]
41
+ new_messages = [] #: Array[Riffer::Messages::Base]
42
+
43
+ messages.each do |m|
44
+ new_messages << m
45
+ next unless m.is_a?(Riffer::Messages::Assistant) && !m.tool_calls.empty?
46
+
47
+ m.tool_calls.each do |tc|
48
+ next if result_ids.include?(tc.call_id)
49
+
50
+ response = ORPHAN_PLACEHOLDER.call(tc)
51
+ new_messages << Riffer::Messages::Tool.new(
52
+ response.content,
53
+ tool_call_id: tc.call_id,
54
+ name: tc.name,
55
+ error: response.error_message,
56
+ error_type: response.error_type
57
+ )
58
+ filled << tc.call_id
59
+ end
60
+ end
61
+
62
+ [new_messages, filled]
63
+ end
64
+
65
+ # Prunes a seeded message array so the +tool_use+ ↔ +tool_result+
66
+ # invariant holds. Drops orphaned tool exchanges (assistant +tool_call+
67
+ # with no matching Tool result) and parentless Tool messages. Returns a
68
+ # new array; the input is not mutated.
69
+ #
70
+ # Pending tool_calls on the resume boundary — the last assistant whose
71
+ # tail is purely Tool results (or empty) — are preserved. They get
72
+ # swept up by +execute_pending_tool_calls+ at the start of the next
73
+ # generate/stream call.
74
+ #
75
+ # No-op when +Riffer.config.experimental_history_healing+ is off:
76
+ # returns +messages+ unchanged.
77
+ #
78
+ #--
79
+ #: (Array[Riffer::Messages::Base]) -> Array[Riffer::Messages::Base]
80
+ def self.prune_orphans(messages)
81
+ return messages unless Riffer.config.experimental_history_healing
82
+
83
+ resume_boundary = (messages.length - 1).downto(0).find { |idx|
84
+ m = messages[idx]
85
+ m.is_a?(Riffer::Messages::Assistant) &&
86
+ (messages[(idx + 1)..] || []).all? { |later| later.is_a?(Riffer::Messages::Tool) }
87
+ }
88
+
89
+ result_ids = messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
90
+ parent_ids = messages.flat_map { |m|
91
+ m.is_a?(Riffer::Messages::Assistant) ? m.tool_calls.map(&:call_id) : []
92
+ }
93
+
94
+ strip_offenders = messages.each_with_index.flat_map { |m, idx|
95
+ next [] unless m.is_a?(Riffer::Messages::Assistant) && !m.tool_calls.empty?
96
+ next [] if idx == resume_boundary # preserve pending exchange
97
+ next [] if m.tool_calls.all? { |tc| result_ids.include?(tc.call_id) }
98
+ m.tool_calls.map(&:call_id)
99
+ }
100
+
101
+ messages.reject { |m|
102
+ case m
103
+ when Riffer::Messages::Assistant
104
+ !m.tool_calls.empty? && m.tool_calls.any? { |tc| strip_offenders.include?(tc.call_id) }
105
+ when Riffer::Messages::Tool
106
+ strip_offenders.include?(m.tool_call_id) || !parent_ids.include?(m.tool_call_id)
107
+ else
108
+ false
109
+ end
110
+ }
111
+ end
112
+ end
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+ # rbs_inline: enabled
3
+
4
+ # Riffer::Agent::Session owns the conversation handle for an agent: the message
5
+ # array, the +on_message+ callback list, and the +tool_use+ ↔ +tool_result+
6
+ # invariant that keeps tool calls and their results consistent.
7
+ #
8
+ # Access via +agent.session+. Sessions are constructed by +Riffer::Agent+
9
+ # and live for the lifetime of the agent.
10
+ #
11
+ # agent.session.add(msg) # append + fire callbacks
12
+ # agent.session.set([msg1, msg2]) # bulk replace (silent)
13
+ # agent.session.unset # clear (silent)
14
+ # agent.session.remove(id: "a_1")
15
+ # agent.session.update(id: "a_1", content: "...")
16
+ # agent.session.find { |m| m.id == "a_1" }
17
+ #
18
+ class Riffer::Agent::Session
19
+ include Enumerable #[Riffer::Messages::Base]
20
+
21
+ # The message history.
22
+ attr_reader :messages #: Array[Riffer::Messages::Base]
23
+
24
+ #--
25
+ #: (?messages: Array[Riffer::Messages::Base]) -> void
26
+ def initialize(messages: [])
27
+ @messages = messages
28
+ @callbacks = [] #: Array[^(Riffer::Messages::Base) -> void]
29
+ end
30
+
31
+ # Registers a callback invoked once per message appended via +#add+.
32
+ #
33
+ # Callbacks do NOT fire for +#set+, +#unset+, +#remove+, or +#update+.
34
+ # Returns +self+ to allow chaining.
35
+ #
36
+ # Raises Riffer::ArgumentError if no block is given.
37
+ #
38
+ #--
39
+ #: () { (Riffer::Messages::Base) -> void } -> self
40
+ def on_message(&block)
41
+ raise Riffer::ArgumentError, "on_message requires a block" unless block_given?
42
+ @callbacks << block
43
+ self
44
+ end
45
+
46
+ # Appends +message+ and fires every registered callback once with it.
47
+ #
48
+ # Pass +silent: true+ to skip +on_message+ callbacks — used for
49
+ # non-inference inputs like user messages, which subscribers don't
50
+ # expect to observe through the callback channel. Inference-produced
51
+ # messages (Assistant, Tool) always go through +add+ without +silent+.
52
+ #
53
+ #--
54
+ #: (Riffer::Messages::Base, ?silent: bool) -> Riffer::Messages::Base
55
+ def add(message, silent: false)
56
+ @messages << message
57
+ @callbacks.each { |callback| callback.call(message) } unless silent
58
+ message
59
+ end
60
+
61
+ # Replaces the message history wholesale. Does NOT fire +on_message+
62
+ # callbacks; registered callbacks persist across the swap.
63
+ #
64
+ # Used for seeding, guardrail rewrites, and history healing — cases
65
+ # where firing callbacks would double-emit messages that subscribers
66
+ # have already observed (or never produced).
67
+ #
68
+ #--
69
+ #: (Array[Riffer::Messages::Base]) -> self
70
+ def set(messages)
71
+ @messages = messages
72
+ self
73
+ end
74
+
75
+ # Clears the session. Does NOT fire +on_message+ callbacks; registered
76
+ # callbacks persist.
77
+ #
78
+ #--
79
+ #: () -> self
80
+ def unset
81
+ @messages = []
82
+ self
83
+ end
84
+
85
+ # Removes a message by id. When the target is an assistant message that
86
+ # carries +tool_calls+, every +Riffer::Messages::Tool+ result whose
87
+ # +tool_call_id+ matches one of those calls is removed atomically — keeping
88
+ # the +tool_use+ ↔ +tool_result+ invariant intact.
89
+ #
90
+ # Raises Riffer::ArgumentError when called on a +Riffer::Messages::Tool+
91
+ # message — that would orphan the parent's +tool_use+. Use
92
+ # +#update+ to rewrite a tool result instead.
93
+ #
94
+ # Returns the removed message, or +nil+ when no message has the given id
95
+ # (idempotent).
96
+ #
97
+ #--
98
+ #: (id: String) -> Riffer::Messages::Base?
99
+ def remove(id:)
100
+ idx = @messages.index { |m| m.id == id }
101
+ return nil unless idx
102
+
103
+ target = @messages[idx]
104
+ if target.is_a?(Riffer::Messages::Tool)
105
+ raise Riffer::ArgumentError,
106
+ "remove cannot drop a Tool message (would orphan the parent's tool_use); use #update instead"
107
+ end
108
+
109
+ if target.is_a?(Riffer::Messages::Assistant) && !target.tool_calls.empty?
110
+ child_ids = target.tool_calls.map(&:call_id)
111
+ @messages.reject! { |m| m.is_a?(Riffer::Messages::Tool) && child_ids.include?(m.tool_call_id) }
112
+ @messages.delete(target)
113
+ else
114
+ @messages.delete_at(idx)
115
+ end
116
+ target
117
+ end
118
+
119
+ # Partial in-place update. Looks up a message by either +id:+ or
120
+ # +tool_call_id:+ (exactly one required), constructs a replacement of the
121
+ # same concrete type with +attrs+ overlaid on the existing fields, and
122
+ # swaps it in place.
123
+ #
124
+ # When the target is an assistant message and the update drops one or more
125
+ # entries from +tool_calls+, every +Riffer::Messages::Tool+ result whose
126
+ # +tool_call_id+ matches a dropped call is removed atomically — keeping the
127
+ # +tool_use+ ↔ +tool_result+ invariant intact.
128
+ #
129
+ # Raises Riffer::ArgumentError when neither or both lookup keys are
130
+ # provided, or when no message matches.
131
+ #
132
+ #--
133
+ #: (?id: String?, ?tool_call_id: String?, **untyped) -> Riffer::Messages::Base
134
+ def update(id: nil, tool_call_id: nil, **attrs)
135
+ raise Riffer::ArgumentError, "update requires either id: or tool_call_id:" if id.nil? && tool_call_id.nil?
136
+ raise Riffer::ArgumentError, "update accepts id: or tool_call_id:, not both" if id && tool_call_id
137
+
138
+ idx = if id
139
+ @messages.index { |m| m.id == id }
140
+ else
141
+ @messages.index { |m| m.is_a?(Riffer::Messages::Tool) && m.tool_call_id == tool_call_id }
142
+ end
143
+
144
+ unless idx
145
+ key = id ? "id #{id.inspect}" : "tool_call_id #{tool_call_id.inspect}"
146
+ raise Riffer::ArgumentError, "no message found for #{key}"
147
+ end
148
+
149
+ old = @messages[idx] #: Riffer::Messages::Base
150
+ replacement = rebuild_message(old, attrs)
151
+ @messages[idx] = replacement
152
+ cascade_dropped_tool_calls(old, replacement)
153
+ replacement
154
+ end
155
+
156
+ # Returns the call_ids of every +tool_call+ on any assistant message that
157
+ # has no matching +Riffer::Messages::Tool+ result anywhere in history.
158
+ #
159
+ # Zero-cost validation hook for callers that want to check the
160
+ # +tool_use+ ↔ +tool_result+ invariant before mutating or persisting.
161
+ #
162
+ #--
163
+ #: () -> Array[String]
164
+ def orphaned_tool_call_ids
165
+ result_ids = @messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) }
166
+ @messages.flat_map { |m|
167
+ next [] unless m.is_a?(Riffer::Messages::Assistant)
168
+ m.tool_calls.reject { |tc| result_ids.include?(tc.call_id) }.map(&:call_id)
169
+ }
170
+ end
171
+
172
+ # Returns +[assistant, pending_tool_calls]+ for the last assistant message.
173
+ # When there is no assistant message or no pending calls, the second
174
+ # element is an empty array.
175
+ #
176
+ #--
177
+ #: () -> [Riffer::Messages::Assistant?, Array[Riffer::Messages::Assistant::ToolCall]]
178
+ def pending_tool_calls
179
+ last_assistant_idx = @messages.rindex { |m| m.is_a?(Riffer::Messages::Assistant) }
180
+ return [nil, []] unless last_assistant_idx
181
+
182
+ assistant = @messages[last_assistant_idx] #: Riffer::Messages::Assistant
183
+ return [assistant, []] if assistant.tool_calls.empty?
184
+
185
+ executed_ids = (@messages[(last_assistant_idx + 1)..] || []).filter_map { |m|
186
+ m.tool_call_id if m.is_a?(Riffer::Messages::Tool)
187
+ }
188
+
189
+ [assistant, assistant.tool_calls.reject { |tc| executed_ids.include?(tc.call_id) }]
190
+ end
191
+
192
+ #--
193
+ #: () -> Enumerator[Riffer::Messages::Base, self]
194
+ #: () { (Riffer::Messages::Base) -> void } -> untyped
195
+ def each(&block)
196
+ return @messages.each unless block
197
+ @messages.each(&block)
198
+ end
199
+
200
+ # The number of LLM steps completed in this session, derived from the
201
+ # count of assistant messages. Used by the agent loop to enforce
202
+ # +max_steps+ on resume.
203
+ #
204
+ #--
205
+ #: () -> Integer
206
+ def steps
207
+ @messages.count { |m| m.is_a?(Riffer::Messages::Assistant) }
208
+ end
209
+
210
+ # The most recent +Riffer::Messages::Assistant+ in the session, or +nil+
211
+ # when none exists.
212
+ #
213
+ #--
214
+ #: () -> Riffer::Messages::Assistant?
215
+ def final_assistant_message
216
+ # TODO: Replace with rfind when minimum Ruby is 4.0+
217
+ # rubocop:disable Style/ReverseFind
218
+ @messages.reverse_each.find { |m| m.is_a?(Riffer::Messages::Assistant) } #: Riffer::Messages::Assistant?
219
+ # rubocop:enable Style/ReverseFind
220
+ end
221
+
222
+ private
223
+
224
+ #: (Riffer::Messages::Base, Riffer::Messages::Base) -> void
225
+ def cascade_dropped_tool_calls(old, replacement)
226
+ return unless old.is_a?(Riffer::Messages::Assistant)
227
+ return unless replacement.is_a?(Riffer::Messages::Assistant)
228
+
229
+ removed_ids = old.tool_calls.map(&:call_id) - replacement.tool_calls.map(&:call_id)
230
+ return if removed_ids.empty?
231
+
232
+ @messages.reject! { |m| m.is_a?(Riffer::Messages::Tool) && removed_ids.include?(m.tool_call_id) }
233
+ end
234
+
235
+ #: (Riffer::Messages::Base, Hash[Symbol, untyped]) -> Riffer::Messages::Base
236
+ def rebuild_message(old, attrs)
237
+ case old
238
+ when Riffer::Messages::Assistant
239
+ Riffer::Messages::Assistant.new(
240
+ attrs.fetch(:content, old.content),
241
+ id: attrs.fetch(:id, old.id),
242
+ tool_calls: attrs.fetch(:tool_calls, old.tool_calls),
243
+ token_usage: attrs.fetch(:token_usage, old.token_usage),
244
+ structured_output: attrs.fetch(:structured_output, old.structured_output)
245
+ )
246
+ when Riffer::Messages::Tool
247
+ Riffer::Messages::Tool.new(
248
+ attrs.fetch(:content, old.content),
249
+ tool_call_id: old.tool_call_id,
250
+ name: attrs.fetch(:name, old.name),
251
+ id: attrs.fetch(:id, old.id),
252
+ error: attrs.fetch(:error, old.error),
253
+ error_type: attrs.fetch(:error_type, old.error_type)
254
+ )
255
+ when Riffer::Messages::User
256
+ Riffer::Messages::User.new(
257
+ attrs.fetch(:content, old.content),
258
+ id: attrs.fetch(:id, old.id),
259
+ files: attrs.fetch(:files, old.files)
260
+ )
261
+ else
262
+ old.class.new(
263
+ attrs.fetch(:content, old.content),
264
+ id: attrs.fetch(:id, old.id)
265
+ )
266
+ end
267
+ end
268
+ end
@@ -13,7 +13,7 @@
13
13
  # result.error #=> "JSON parse error: ..."
14
14
  # end
15
15
  #
16
- class Riffer::StructuredOutput::Result
16
+ class Riffer::Agent::StructuredOutput::Result
17
17
  attr_reader :object #: Hash[Symbol, untyped]?
18
18
  attr_reader :error #: String?
19
19
 
@@ -3,16 +3,16 @@
3
3
 
4
4
  require "json"
5
5
 
6
- # Riffer::StructuredOutput provides parse/validate for structured JSON
6
+ # Riffer::Agent::StructuredOutput provides parse/validate for structured JSON
7
7
  # responses from LLM providers.
8
8
  #
9
9
  # params = Riffer::Params.new
10
10
  # params.required(:sentiment, String)
11
- # so = Riffer::StructuredOutput.new(params)
11
+ # so = Riffer::Agent::StructuredOutput.new(params)
12
12
  # result = so.parse_and_validate('{"sentiment":"positive","score":0.9}')
13
13
  # result.object #=> {sentiment: "positive", score: 0.9}
14
14
  #
15
- class Riffer::StructuredOutput
15
+ class Riffer::Agent::StructuredOutput
16
16
  attr_reader :params #: Riffer::Params
17
17
 
18
18
  #--
@@ -34,7 +34,7 @@ class Riffer::StructuredOutput
34
34
  # Returns a Result with the validated object on success, or an error message on failure.
35
35
  #
36
36
  #--
37
- #: (String) -> Riffer::StructuredOutput::Result
37
+ #: (String) -> Riffer::Agent::StructuredOutput::Result
38
38
  def parse_and_validate(json_string)
39
39
  parsed = JSON.parse(json_string, symbolize_names: true)
40
40
  validated = @params.validate(parsed)