Class: Clacky::MessageHistory
- Inherits:
-
Object
- Object
- Clacky::MessageHistory
- Defined in:
- lib/clacky/message_history.rb
Overview
MessageHistory wraps the conversation message list and exposes business-meaningful operations instead of raw array manipulation.
Internal fields (task_id, created_at, system_injected, etc.) are kept in the internal store but stripped when calling #to_api.
Constant Summary collapse
- INTERNAL_FIELDS =
Fields that are internal to the agent and must not be sent to the API.
%i[ task_id created_at system_injected session_context memory_update subagent_instructions subagent_result token_usage compressed_summary chunk_path truncated transient chunk_index chunk_count ].freeze
Instance Method Summary collapse
-
#append(message) ⇒ Object
Append a single message hash to the history.
-
#delete_where(&block) ⇒ Object
Remove all messages matching the block in-place (e.g. cleanup_memory_messages uses reject! { m }).
- #empty? ⇒ Boolean
-
#estimate_tokens ⇒ Object
Estimate total token count for all messages.
-
#for_task(task_id) ⇒ Object
Return all messages where task_id <= given id (Time Machine support).
-
#initialize(messages = []) ⇒ MessageHistory
constructor
A new instance of MessageHistory.
-
#last_injected_chunk_count ⇒ Object
Return the chunk_count from the most recently injected chunk index message.
-
#last_real_user_index ⇒ Object
Return the index of the last real (non-system-injected) user message.
-
#last_session_context_date ⇒ Object
Return the session_date value from the most recent session_context message.
-
#mutate_last_matching(predicate, &block) ⇒ Object
Mutate the last message matching the predicate lambda in-place.
-
#pending_tool_calls? ⇒ Boolean
True when the last assistant message has tool_calls but no tool_result has been appended yet (would cause a 400 from the API).
-
#pop_last ⇒ Object
Remove and return the last message.
-
#pop_while(&block) ⇒ Object
Remove messages from the end while the block is truthy.
-
#real_user_messages ⇒ Object
Return only real (non-system-injected) user messages.
-
#recent_truncation_count(n) ⇒ Object
Count how many of the last N messages have :truncated set.
-
#replace_all(new_messages) ⇒ Object
Replace the entire message list (used by compression rebuild).
-
#replace_system_prompt(content, **extra) ⇒ Object
Replace (or insert at head) the system prompt message.
-
#rollback_before(message) ⇒ Object
Roll back the history to just before the given message object.
-
#size ⇒ Object
───────────────────────────────────────────── Size helpers ─────────────────────────────────────────────.
-
#subagent_instruction_message ⇒ Object
Return the message with :subagent_instructions set.
-
#to_a ⇒ Object
Return a shallow copy of the message list, excluding transient messages.
-
#to_api ⇒ Object
Return a clean copy of messages suitable for sending to the LLM API: - strips internal-only fields.
-
#truncate_from(index) ⇒ Object
Remove all messages from index onward (used by restore_session on error).
Constructor Details
#initialize(messages = []) ⇒ MessageHistory
Returns a new instance of MessageHistory.
18 19 20 |
# File 'lib/clacky/message_history.rb', line 18 def initialize( = []) @messages = .dup end |
Instance Method Details
#append(message) ⇒ Object
Append a single message hash to the history.
When appending a user message, automatically drop any trailing assistant message that has unanswered tool_calls (no tool_result follows it). This prevents API error 2013 (“tool call result does not follow tool call”) when a previous task ended before observe() could append tool results (e.g. subagent crash, interrupt, or error).
33 34 35 36 37 38 39 |
# File 'lib/clacky/message_history.rb', line 33 def append() if [:role] == "user" drop_dangling_tool_calls! end @messages << self end |
#delete_where(&block) ⇒ Object
Remove all messages matching the block in-place (e.g. cleanup_memory_messages uses reject! { m }).
73 74 75 76 |
# File 'lib/clacky/message_history.rb', line 73 def delete_where(&block) @messages.reject!(&block) self end |
#empty? ⇒ Boolean
169 170 171 |
# File 'lib/clacky/message_history.rb', line 169 def empty? @messages.empty? end |
#estimate_tokens ⇒ Object
Estimate total token count for all messages. Uses the ~4 chars/token heuristic (works well for English/code). Handles string content, array content blocks, and tool_calls.
176 177 178 |
# File 'lib/clacky/message_history.rb', line 176 def estimate_tokens @messages.sum { |m| (m) } end |
#for_task(task_id) ⇒ Object
Return all messages where task_id <= given id (Time Machine support).
151 152 153 |
# File 'lib/clacky/message_history.rb', line 151 def for_task(task_id) @messages.select { |m| !m[:task_id] || m[:task_id] <= task_id } end |
#last_injected_chunk_count ⇒ Object
Return the chunk_count from the most recently injected chunk index message. Used by inject_chunk_index_if_needed to avoid re-injecting when nothing changed.
129 130 131 132 |
# File 'lib/clacky/message_history.rb', line 129 def last_injected_chunk_count msg = @messages.reverse.find { |m| m[:chunk_index] } msg&.dig(:chunk_count) || 0 end |
#last_real_user_index ⇒ Object
Return the index of the last real (non-system-injected) user message. Used by restore_session to trim back to a clean state on error.
141 142 143 |
# File 'lib/clacky/message_history.rb', line 141 def last_real_user_index @messages.rindex { |m| m[:role] == "user" && !m[:system_injected] } end |
#last_session_context_date ⇒ Object
Return the session_date value from the most recent session_context message. Used by inject_session_context_if_needed to avoid re-injecting on the same date.
122 123 124 125 |
# File 'lib/clacky/message_history.rb', line 122 def last_session_context_date msg = @messages.reverse.find { |m| m[:session_context] } msg&.dig(:session_date) end |
#mutate_last_matching(predicate, &block) ⇒ Object
Mutate the last message matching the predicate lambda in-place. Used by execute_skill_with_subagent to update instruction messages.
80 81 82 83 84 |
# File 'lib/clacky/message_history.rb', line 80 def mutate_last_matching(predicate, &block) msg = @messages.reverse.find { |m| predicate.call(m) } block.call(msg) if msg self end |
#pending_tool_calls? ⇒ Boolean
True when the last assistant message has tool_calls but no tool_result has been appended yet (would cause a 400 from the API).
109 110 111 112 113 114 115 116 117 118 |
# File 'lib/clacky/message_history.rb', line 109 def pending_tool_calls? return false if @messages.empty? last = @messages.last return false unless last[:role] == "assistant" && last[:tool_calls]&.any? # Check that there is no tool result message after this assistant message last_assistant_idx = @messages.rindex { |m| m == last } @messages[(last_assistant_idx + 1)..].none? { |m| m[:role] == "tool" || m[:tool_results] } end |
#pop_last ⇒ Object
Remove and return the last message.
61 62 63 |
# File 'lib/clacky/message_history.rb', line 61 def pop_last @messages.pop end |
#pop_while(&block) ⇒ Object
Remove messages from the end while the block is truthy.
66 67 68 69 |
# File 'lib/clacky/message_history.rb', line 66 def pop_while(&block) @messages.pop while !@messages.empty? && block.call(@messages.last) self end |
#real_user_messages ⇒ Object
Return only real (non-system-injected) user messages.
135 136 137 |
# File 'lib/clacky/message_history.rb', line 135 def @messages.select { |m| m[:role] == "user" && !m[:system_injected] } end |
#recent_truncation_count(n) ⇒ Object
Count how many of the last N messages have :truncated set. Used by think() to guard against infinite truncation retry loops.
157 158 159 |
# File 'lib/clacky/message_history.rb', line 157 def recent_truncation_count(n) @messages.last(n).count { |m| m[:truncated] } end |
#replace_all(new_messages) ⇒ Object
Replace the entire message list (used by compression rebuild).
55 56 57 58 |
# File 'lib/clacky/message_history.rb', line 55 def replace_all() @messages = .dup self end |
#replace_system_prompt(content, **extra) ⇒ Object
Replace (or insert at head) the system prompt message. Used by session_serializer#refresh_system_prompt.
43 44 45 46 47 48 49 50 51 52 |
# File 'lib/clacky/message_history.rb', line 43 def replace_system_prompt(content, **extra) msg = { role: "system", content: content }.merge(extra) idx = @messages.index { |m| m[:role] == "system" } if idx @messages[idx] = msg else @messages.unshift(msg) end self end |
#rollback_before(message) ⇒ Object
Roll back the history to just before the given message object. Removes the message and anything appended after it. Used to undo a failed speculative append (e.g. compression message that errored).
95 96 97 98 99 100 101 |
# File 'lib/clacky/message_history.rb', line 95 def rollback_before() idx = @messages.index { |m| m.equal?() } return self unless idx @messages = @messages[0...idx] self end |
#size ⇒ Object
─────────────────────────────────────────────Size helpers ─────────────────────────────────────────────
165 166 167 |
# File 'lib/clacky/message_history.rb', line 165 def size @messages.size end |
#subagent_instruction_message ⇒ Object
Return the message with :subagent_instructions set.
146 147 148 |
# File 'lib/clacky/message_history.rb', line 146 def @messages.find { |m| m[:subagent_instructions] } end |
#to_a ⇒ Object
Return a shallow copy of the message list, excluding transient messages. Transient messages (e.g. brand skill instructions) are valid during the current session but must not be persisted to session.json. For serialization, compression, and cloning.
194 195 196 |
# File 'lib/clacky/message_history.rb', line 194 def to_a @messages.reject { |m| m[:transient] }.dup end |
#to_api ⇒ Object
Return a clean copy of messages suitable for sending to the LLM API:
-
strips internal-only fields
186 187 188 |
# File 'lib/clacky/message_history.rb', line 186 def to_api @messages.map { |m| strip_internal_fields(m) } end |
#truncate_from(index) ⇒ Object
Remove all messages from index onward (used by restore_session on error).
87 88 89 90 |
# File 'lib/clacky/message_history.rb', line 87 def truncate_from(index) @messages = @messages[0...index] self end |