class Riffer::Agent::Session
Owns the conversation handle for an agent: the message array, the on_message callbacks, and the tool_use โ tool_result invariant that keeps tool calls and their results consistent.
agent.session.add(msg) # append + fire callbacks agent.session.set([msg1, msg2]) # bulk replace (silent) agent.session.unset # clear (silent) agent.session.remove(id: "a_1") agent.session.update(id: "a_1", content: "...") agent.session.find { |m| m.id == "a_1" }
Attributes
The message history.
Public Class Methods
Source
# File lib/riffer/agent/session.rb, line 25 def initialize(messages: []) @messages = messages @callbacks = [] #: Array[^(Riffer::Messages::Base) -> void] end
Public Instance Methods
Source
# File lib/riffer/agent/session.rb, line 44 def add(message, silent: false) @messages << message @callbacks.each { |callback| callback.call(message) } unless silent message end
Appends message and fires every registered callback once with it. Pass +silent: true+ to skip callbacks โ used for non-inference inputs like user messages that subscribers donโt expect on the callback channel.
Source
# File lib/riffer/agent/session.rb, line 155 def each(&block) return @messages.each unless block @messages.each(&block) end
Yields each message in order, or returns an Enumerator without a block.
Source
# File lib/riffer/agent/session.rb, line 173 def final_assistant_message # TODO: Replace with rfind when minimum Ruby is 4.0+ # rubocop:disable Style/ReverseFind @messages.reverse_each.find { |m| m.is_a?(Riffer::Messages::Assistant) } #: Riffer::Messages::Assistant? # rubocop:enable Style/ReverseFind end
The most recent Riffer::Messages::Assistant in the session, or nil when none exists.
Source
# File lib/riffer/agent/session.rb, line 33 def on_message(&block) raise Riffer::ArgumentError, "on_message requires a block" unless block_given? @callbacks << block self end
Registers a callback invoked once per message appended via add.
Source
# File lib/riffer/agent/session.rb, line 125 def orphaned_tool_call_ids result_ids = @messages.filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) } @messages.flat_map { |m| next [] unless m.is_a?(Riffer::Messages::Assistant) m.tool_calls.reject { |tc| result_ids.include?(tc.call_id) }.map(&:call_id) } end
Returns the call_ids of every tool_call with no matching Riffer::Messages::Tool result anywhere in history โ a hook for checking the tool_use โ tool_result invariant before mutating or persisting.
Source
# File lib/riffer/agent/session.rb, line 137 def pending_tool_calls last_assistant_idx = @messages.rindex { |m| m.is_a?(Riffer::Messages::Assistant) } return [nil, []] unless last_assistant_idx assistant = @messages[last_assistant_idx] #: Riffer::Messages::Assistant return [assistant, []] if assistant.tool_calls.empty? executed_ids = (@messages[(last_assistant_idx + 1)..] || []).filter_map { |m| m.tool_call_id if m.is_a?(Riffer::Messages::Tool) } [assistant, assistant.tool_calls.reject { |tc| executed_ids.include?(tc.call_id) }] end
Returns +[last_assistant, pending_tool_calls]+; the second element is empty when thereโs no assistant message or no pending calls.
Source
# File lib/riffer/agent/session.rb, line 72 def remove(id:) idx = @messages.index { |m| m.id == id } return nil unless idx target = @messages[idx] if target.is_a?(Riffer::Messages::Tool) raise Riffer::ArgumentError, "remove cannot drop a Tool message (would orphan the parent's tool_use); use #update instead" end if target.is_a?(Riffer::Messages::Assistant) && !target.tool_calls.empty? child_ids = target.tool_calls.map(&:call_id) @messages.reject! { |m| m.is_a?(Riffer::Messages::Tool) && child_ids.include?(m.tool_call_id) } @messages.delete(target) else @messages.delete_at(idx) end target end
Removes a message by id, cascading to drop the Tool results of a removed assistantโs tool_calls so the tool_use โ tool_result invariant holds. Raises on a Tool message โ that would orphan its parent; use update instead. Returns nil if no message matches.
Source
# File lib/riffer/agent/session.rb, line 53 def set(messages) @messages = messages self end
Replaces the message history wholesale
Source
# File lib/riffer/agent/session.rb, line 164 def steps @messages.count { |m| m.is_a?(Riffer::Messages::Assistant) } end
The number of LLM steps completed, used by the agent loop to enforce max_steps on resume.
Source
# File lib/riffer/agent/session.rb, line 61 def unset @messages = [] self end
Clears the session.
Source
# File lib/riffer/agent/session.rb, line 98 def update(id: nil, tool_call_id: nil, **attrs) raise Riffer::ArgumentError, "update requires either id: or tool_call_id:" if id.nil? && tool_call_id.nil? raise Riffer::ArgumentError, "update accepts id: or tool_call_id:, not both" if id && tool_call_id idx = if id @messages.index { |m| m.id == id } else @messages.index { |m| m.is_a?(Riffer::Messages::Tool) && m.tool_call_id == tool_call_id } end unless idx key = id ? "id #{id.inspect}" : "tool_call_id #{tool_call_id.inspect}" raise Riffer::ArgumentError, "no message found for #{key}" end old = @messages[idx] #: Riffer::Messages::Base replacement = rebuild_message(old, attrs) @messages[idx] = replacement cascade_dropped_tool_calls(old, replacement) replacement end
Partial in-place update: looks up a message by id: or tool_call_id: (exactly one), overlays attrs onto a same-type replacement, and swaps it in. Dropping tool_calls from an assistant cascades to remove their Tool results, preserving the invariant. Raises on neither/both keys or no match.