Class: Riffer::Agent::Session
- Inherits:
-
Object
- Object
- Riffer::Agent::Session
- Includes:
- Enumerable
- Defined in:
- lib/riffer/agent/session.rb
Overview
Riffer::Agent::Session owns the conversation handle for an agent: the message array, the on_message callback list, and the tool_use ↔ tool_result invariant that keeps tool calls and their results consistent.
Access via agent.session. Sessions are constructed by Riffer::Agent and live for the lifetime of the agent.
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" }
Defined Under Namespace
Modules: Repair
Instance Attribute Summary collapse
-
#messages ⇒ Object
readonly
The message history.
Instance Method Summary collapse
-
#add(message, silent: false) ⇒ Object
Appends
messageand fires every registered callback once with it. -
#each(&block) ⇒ Object
– : () -> Enumerator[Riffer::Messages::Base, self] : () { (Riffer::Messages::Base) -> void } -> untyped.
-
#final_assistant_message ⇒ Object
The most recent
Riffer::Messages::Assistantin the session, ornilwhen none exists. -
#initialize(messages: []) ⇒ Session
constructor
– : (?messages: Array) -> void.
-
#on_message(&block) ⇒ Object
Registers a callback invoked once per message appended via
#add. -
#orphaned_tool_call_ids ⇒ Object
Returns the call_ids of every
tool_callon any assistant message that has no matchingRiffer::Messages::Toolresult anywhere in history. -
#pending_tool_calls ⇒ Object
Returns [assistant, pending_tool_calls] for the last assistant message.
-
#remove(id:) ⇒ Object
Removes a message by id.
-
#set(messages) ⇒ Object
Replaces the message history wholesale.
-
#steps ⇒ Object
The number of LLM steps completed in this session, derived from the count of assistant messages.
-
#unset ⇒ Object
Clears the session.
-
#update(id: nil, tool_call_id: nil, **attrs) ⇒ Object
Partial in-place update.
Constructor Details
Instance Attribute Details
#messages ⇒ Object (readonly)
The message history.
22 23 24 |
# File 'lib/riffer/agent/session.rb', line 22 def @messages end |
Instance Method Details
#add(message, silent: false) ⇒ Object
Appends message and fires every registered callback once with it.
Pass silent: true to skip on_message callbacks — used for non-inference inputs like user messages, which subscribers don’t expect to observe through the callback channel. Inference-produced messages (Assistant, Tool) always go through add without silent.
– : (Riffer::Messages::Base, ?silent: bool) -> Riffer::Messages::Base
55 56 57 58 59 |
# File 'lib/riffer/agent/session.rb', line 55 def add(, silent: false) @messages << @callbacks.each { |callback| callback.call() } unless silent end |
#each(&block) ⇒ Object
– : () -> Enumerator[Riffer::Messages::Base, self] : () { (Riffer::Messages::Base) -> void } -> untyped
195 196 197 198 |
# File 'lib/riffer/agent/session.rb', line 195 def each(&block) return @messages.each unless block @messages.each(&block) end |
#final_assistant_message ⇒ Object
The most recent Riffer::Messages::Assistant in the session, or nil when none exists.
– : () -> Riffer::Messages::Assistant?
215 216 217 218 219 220 |
# File 'lib/riffer/agent/session.rb', line 215 def # 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 |
#on_message(&block) ⇒ Object
Registers a callback invoked once per message appended via #add.
Callbacks do NOT fire for #set, #unset, #remove, or #update. Returns self to allow chaining.
Raises Riffer::ArgumentError if no block is given.
– : () { (Riffer::Messages::Base) -> void } -> self
40 41 42 43 44 |
# File 'lib/riffer/agent/session.rb', line 40 def (&block) raise Riffer::ArgumentError, "on_message requires a block" unless block_given? @callbacks << block self end |
#orphaned_tool_call_ids ⇒ Object
Returns the call_ids of every tool_call on any assistant message that has no matching Riffer::Messages::Tool result anywhere in history.
Zero-cost validation hook for callers that want to check the tool_use ↔ tool_result invariant before mutating or persisting.
– : () -> Array
164 165 166 167 168 169 170 |
# File 'lib/riffer/agent/session.rb', line 164 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 |
#pending_tool_calls ⇒ Object
Returns [assistant, pending_tool_calls] for the last assistant message. When there is no assistant message or no pending calls, the second element is an empty array.
– : () -> [Riffer::Messages::Assistant?, Array]
178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/riffer/agent/session.rb', line 178 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 |
#remove(id:) ⇒ Object
Removes a message by id. When the target is an assistant message that carries tool_calls, every Riffer::Messages::Tool result whose tool_call_id matches one of those calls is removed atomically — keeping the tool_use ↔ tool_result invariant intact.
Raises Riffer::ArgumentError when called on a Riffer::Messages::Tool message — that would orphan the parent’s tool_use. Use #update to rewrite a tool result instead.
Returns the removed message, or nil when no message has the given id (idempotent).
– : (id: String) -> Riffer::Messages::Base?
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/riffer/agent/session.rb', line 99 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 |
#set(messages) ⇒ Object
Replaces the message history wholesale. Does NOT fire on_message callbacks; registered callbacks persist across the swap.
Used for seeding, guardrail rewrites, and history healing — cases where firing callbacks would double-emit messages that subscribers have already observed (or never produced).
– : (Array) -> self
70 71 72 73 |
# File 'lib/riffer/agent/session.rb', line 70 def set() @messages = self end |
#steps ⇒ Object
The number of LLM steps completed in this session, derived from the count of assistant messages. Used by the agent loop to enforce max_steps on resume.
– : () -> Integer
206 207 208 |
# File 'lib/riffer/agent/session.rb', line 206 def steps @messages.count { |m| m.is_a?(Riffer::Messages::Assistant) } end |
#unset ⇒ Object
Clears the session. Does NOT fire on_message callbacks; registered callbacks persist.
– : () -> self
80 81 82 83 |
# File 'lib/riffer/agent/session.rb', line 80 def unset @messages = [] self end |
#update(id: nil, tool_call_id: nil, **attrs) ⇒ Object
Partial in-place update. Looks up a message by either id: or tool_call_id: (exactly one required), constructs a replacement of the same concrete type with attrs overlaid on the existing fields, and swaps it in place.
When the target is an assistant message and the update drops one or more entries from tool_calls, every Riffer::Messages::Tool result whose tool_call_id matches a dropped call is removed atomically — keeping the tool_use ↔ tool_result invariant intact.
Raises Riffer::ArgumentError when neither or both lookup keys are provided, or when no message matches.
– : (?id: String?, ?tool_call_id: String?, **untyped) -> Riffer::Messages::Base
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/riffer/agent/session.rb', line 134 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 = (old, attrs) @messages[idx] = replacement cascade_dropped_tool_calls(old, replacement) replacement end |