Class: SwarmSDK::Agent::ContextManager
- Inherits:
-
Object
- Object
- SwarmSDK::Agent::ContextManager
- Defined in:
- lib/swarm_sdk/agent/context_manager.rb
Overview
Manages conversation context and message optimization
Responsibilities:
- Handle ephemeral messages (sent to LLM but not persisted)
- Extract and strip system reminders
- Prepare messages for LLM API calls
- Future: Context window management, summarization, truncation
Constant Summary collapse
- SYSTEM_REMINDER_REGEX =
%r{<system-reminder>.*?</system-reminder>}m
Instance Attribute Summary collapse
-
#compression_applied ⇒ Object
Expose compression state for snapshot/restore NOTE: @compression_applied initializes to nil (not false), only set to true when compression runs.
Instance Method Summary collapse
-
#add_ephemeral_content_for_message(message_index, content) ⇒ void
Track ephemeral content to append to a specific message.
-
#add_ephemeral_reminder(content, messages_array:) ⇒ void
Add ephemeral reminder to the most recent message.
-
#analyze_context_bloat(messages, threshold: 0.7) ⇒ Hash
Future: Detect if context is becoming bloated.
-
#auto_compress_on_threshold(messages, keep_recent: 10) ⇒ Array<RubyLLM::Message>
Automatically compress messages when context threshold is hit.
-
#clear_ephemeral ⇒ void
Clear all ephemeral content.
-
#compress_tool_message(msg, age:) ⇒ RubyLLM::Message
Compress a single tool message based on age.
-
#compress_tool_results(messages, keep_recent: 10) ⇒ Array<RubyLLM::Message>
Compress verbose tool results for older messages.
-
#detect_tool_name(content) ⇒ String?
Detect tool name from content.
-
#ephemeral_count ⇒ Integer
Get count of messages with ephemeral content.
-
#extract_system_reminders(content) ⇒ Array<String>
Extract all
blocks from content. -
#has_ephemeral? ⇒ Boolean
Check if there is pending ephemeral content.
-
#has_system_reminders?(content) ⇒ Boolean
Check if content contains system reminders.
-
#initialize ⇒ ContextManager
constructor
A new instance of ContextManager.
-
#prepare_for_llm(persistent_messages) ⇒ Array<RubyLLM::Message>
Prepare messages for LLM API call.
-
#rerunnable_tool?(tool_name) ⇒ Boolean
Check if a tool is re-runnable (idempotent, can get same data again).
-
#reset_compression ⇒ void
Reset compression flag (when conversation is reset).
-
#strip_system_reminders(content) ⇒ String
Strip all
blocks from content. -
#summarize_old_messages(messages, before_index:, strategy: :truncate) ⇒ Array<RubyLLM::Message>
Future: Summarize old messages to save context window space.
-
#truncate_to_fit(messages, max_tokens:, keep_recent: 10) ⇒ Array<RubyLLM::Message>
Future: Truncate messages to fit within context window.
Constructor Details
#initialize ⇒ ContextManager
Returns a new instance of ContextManager.
26 27 28 29 30 31 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 26 def initialize # Ephemeral content to append to messages for this turn only # Format: { message_index => [array of reminder strings] } @ephemeral_content = {} # NOTE: @compression_applied is NOT initialized here - starts as nil end |
Instance Attribute Details
#compression_applied ⇒ Object
Expose compression state for snapshot/restore NOTE: @compression_applied initializes to nil (not false), only set to true when compression runs
23 24 25 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 23 def compression_applied @compression_applied end |
Instance Method Details
#add_ephemeral_content_for_message(message_index, content) ⇒ void
This method returns an undefined value.
Track ephemeral content to append to a specific message
Reminders will be embedded in the message content when sent to LLM, but are NOT persisted in the message history.
41 42 43 44 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 41 def (, content) @ephemeral_content[] ||= [] @ephemeral_content[] << content end |
#add_ephemeral_reminder(content, messages_array:) ⇒ void
This method returns an undefined value.
Add ephemeral reminder to the most recent message
This will append the reminder to the last message in the array when preparing for LLM, but won't modify the stored message.
54 55 56 57 58 59 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 54 def add_ephemeral_reminder(content, messages_array:) = .size - 1 return if < 0 (, content) end |
#analyze_context_bloat(messages, threshold: 0.7) ⇒ Hash
Future: Detect if context is becoming bloated
309 310 311 312 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 309 def analyze_context_bloat(, threshold: 0.7) # TODO: Implement when needed { bloated: false, recommendations: [] } end |
#auto_compress_on_threshold(messages, keep_recent: 10) ⇒ Array<RubyLLM::Message>
Automatically compress messages when context threshold is hit
This is called automatically when context usage crosses 60% threshold. Returns compressed messages array for immediate use.
288 289 290 291 292 293 294 295 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 288 def auto_compress_on_threshold(, keep_recent: 10) return if @compression_applied # Mark as applied to avoid compressing multiple times @compression_applied = true compress_tool_results(, keep_recent: keep_recent) end |
#clear_ephemeral ⇒ void
This method returns an undefined value.
Clear all ephemeral content
Should be called after LLM response is received.
106 107 108 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 106 def clear_ephemeral @ephemeral_content.clear end |
#compress_tool_message(msg, age:) ⇒ RubyLLM::Message
Compress a single tool message based on age
Progressive compression: older messages get compressed more. For re-runnable tools (Read, Grep, Glob, etc.), adds instruction to re-run if needed.
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 215 def (msg, age:) content = msg.content.to_s # Progressive compression based on age max_length = case age when 0..10 then return msg # Recent: keep full detail when 11..20 then 1000 # Medium age: light compression when 21..40 then 500 # Old: moderate compression when 41..60 then 200 # Very old: heavy compression else 100 # Ancient: minimal summary end return msg if content.length <= max_length # Compress while preserving structure compressed = content.slice(0, max_length) truncated_chars = content.length - max_length compressed += "\n...[#{truncated_chars} chars truncated for context management]" # Detect if this is a re-runnable tool and add helpful instruction tool_name = detect_tool_name(content) if rerunnable_tool?(tool_name) compressed += "\n\nš” If you need the full output, re-run the #{tool_name} tool with the same parameters." end RubyLLM::Message.new( role: :tool, content: compressed, tool_call_id: msg.tool_call_id, ) end |
#compress_tool_results(messages, keep_recent: 10) ⇒ Array<RubyLLM::Message>
Compress verbose tool results for older messages
Uses progressive compression: older messages are compressed more aggressively. Preserves user/assistant messages at full detail (conversational context).
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 190 def compress_tool_results(, keep_recent: 10) .map.with_index do |msg, i| # Keep recent messages at full detail next msg if i >= .size - keep_recent # Keep user/assistant messages (conversational flow is important) next msg if [:user, :assistant].include?(msg.role) # Compress old tool results if msg.role == :tool (msg, age: .size - i) else msg end end end |
#detect_tool_name(content) ⇒ String?
Detect tool name from content
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 251 def detect_tool_name(content) # Many tool results start with patterns we can detect case content when /^\s*\d+ā/ # Line numbers (Read, MemoryRead) content.include?("memory://") ? "MemoryRead" : "Read" when /^Memory entries matching/ # MemoryGlob "MemoryGlob" when /^Found \d+ files? matching/ # Glob "Glob" when /matches in \d+ files?|No matches found/ # Grep, MemoryGrep content.include?("memory://") ? "MemoryGrep" : "Grep" when %r{^Stored at memory://} # MemoryWrite (not re-runnable but identifiable) "MemoryWrite" when %r{^Deleted memory://} # MemoryDelete "MemoryDelete" end end |
#ephemeral_count ⇒ Integer
Get count of messages with ephemeral content
120 121 122 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 120 def ephemeral_count @ephemeral_content.size end |
#extract_system_reminders(content) ⇒ Array<String>
Extract all
128 129 130 131 132 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 128 def extract_system_reminders(content) return [] if content.nil? || content.empty? content.scan(SYSTEM_REMINDER_REGEX) end |
#has_ephemeral? ⇒ Boolean
Check if there is pending ephemeral content
113 114 115 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 113 def has_ephemeral? @ephemeral_content.any? end |
#has_system_reminders?(content) ⇒ Boolean
Check if content contains system reminders
150 151 152 153 154 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 150 def has_system_reminders?(content) return false if content.nil? || content.empty? SYSTEM_REMINDER_REGEX.match?(content) end |
#prepare_for_llm(persistent_messages) ⇒ Array<RubyLLM::Message>
Prepare messages for LLM API call
Embeds ephemeral content into messages for this turn only. Does NOT modify the persistent messages array.
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 68 def prepare_for_llm() return .dup if @ephemeral_content.empty? # Clone messages and embed ephemeral content = .map.with_index do |msg, index| ephemeral_for_this_msg = @ephemeral_content[index] # No ephemeral content for this message - use as-is next msg unless ephemeral_for_this_msg&.any? # Embed ephemeral content in this message original_content = msg.content.is_a?(RubyLLM::Content) ? msg.content.text : msg.content.to_s = [original_content, *ephemeral_for_this_msg].join("\n\n") # Create new message with embedded content if msg.content.is_a?(RubyLLM::Content) RubyLLM::Message.new( role: msg.role, content: RubyLLM::Content.new(, msg.content.), tool_call_id: msg.tool_call_id, ) else RubyLLM::Message.new( role: msg.role, content: , tool_call_id: msg.tool_call_id, ) end end end |
#rerunnable_tool?(tool_name) ⇒ Boolean
Check if a tool is re-runnable (idempotent, can get same data again)
273 274 275 276 277 278 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 273 def rerunnable_tool?(tool_name) return false if tool_name.nil? # These tools are idempotent - re-running gives same/current data ["Read", "MemoryRead", "Grep", "MemoryGrep", "Glob", "MemoryGlob"].include?(tool_name) end |
#reset_compression ⇒ void
This method returns an undefined value.
Reset compression flag (when conversation is reset)
300 301 302 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 300 def reset_compression @compression_applied = false end |
#strip_system_reminders(content) ⇒ String
Strip all
Returns clean content without system reminders.
140 141 142 143 144 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 140 def strip_system_reminders(content) return content if content.nil? || content.empty? content.gsub(SYSTEM_REMINDER_REGEX, "").strip end |
#summarize_old_messages(messages, before_index:, strategy: :truncate) ⇒ Array<RubyLLM::Message>
Future: Summarize old messages to save context window space
166 167 168 169 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 166 def (, before_index:, strategy: :truncate) # TODO: Implement when needed end |
#truncate_to_fit(messages, max_tokens:, keep_recent: 10) ⇒ Array<RubyLLM::Message>
Future: Truncate messages to fit within context window
177 178 179 180 |
# File 'lib/swarm_sdk/agent/context_manager.rb', line 177 def truncate_to_fit(, max_tokens:, keep_recent: 10) # TODO: Implement when needed end |