Class: SwarmSDK::Agent::ContextManager

Inherits:
Object
  • Object
show all
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

Examples:

manager = ContextManager.new
manager.add_ephemeral_reminder("<system-reminder>Use caution</system-reminder>")
messages_for_llm = manager.prepare_for_llm(persistent_messages)
manager.clear_ephemeral  # After LLM call

Constant Summary collapse

SYSTEM_REMINDER_REGEX =
%r{<system-reminder>.*?</system-reminder>}m

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeContextManager

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_appliedObject

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.

Parameters:

  • message_index (Integer)

    Index of message to append to

  • content (String)

    Reminder content to append



41
42
43
44
# File 'lib/swarm_sdk/agent/context_manager.rb', line 41

def add_ephemeral_content_for_message(message_index, content)
  @ephemeral_content[message_index] ||= []
  @ephemeral_content[message_index] << 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.

Parameters:

  • content (String)

    Reminder content

  • messages_array (Array<RubyLLM::Message>)

    Message array to get index from



54
55
56
57
58
59
# File 'lib/swarm_sdk/agent/context_manager.rb', line 54

def add_ephemeral_reminder(content, messages_array:)
  message_index = messages_array.size - 1
  return if message_index < 0

  add_ephemeral_content_for_message(message_index, content)
end

#analyze_context_bloat(messages, threshold: 0.7) ⇒ Hash

Future: Detect if context is becoming bloated

Parameters:

  • messages (Array<RubyLLM::Message>)

    Messages to analyze

  • threshold (Float) (defaults to: 0.7)

    Bloat threshold (0.0-1.0)

Returns:

  • (Hash)

    Bloat analysis with recommendations



309
310
311
312
# File 'lib/swarm_sdk/agent/context_manager.rb', line 309

def analyze_context_bloat(messages, 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.

Parameters:

  • messages (Array<RubyLLM::Message>)

    Current message array

  • keep_recent (Integer) (defaults to: 10)

    Number of recent messages to keep full

Returns:

  • (Array<RubyLLM::Message>)

    Compressed messages



288
289
290
291
292
293
294
295
# File 'lib/swarm_sdk/agent/context_manager.rb', line 288

def auto_compress_on_threshold(messages, keep_recent: 10)
  return messages if @compression_applied

  # Mark as applied to avoid compressing multiple times
  @compression_applied = true

  compress_tool_results(messages, keep_recent: keep_recent)
end

#clear_ephemeralvoid

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.

Parameters:

  • msg (RubyLLM::Message)

    Tool message to compress

  • age (Integer)

    How many messages ago (higher = older)

Returns:

  • (RubyLLM::Message)

    Compressed message



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 compress_tool_message(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).

Parameters:

  • messages (Array<RubyLLM::Message>)

    Messages to compress

  • keep_recent (Integer) (defaults to: 10)

    Number of recent messages to keep at full detail

Returns:

  • (Array<RubyLLM::Message>)

    Compressed messages



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(messages, keep_recent: 10)
  messages.map.with_index do |msg, i|
    # Keep recent messages at full detail
    next msg if i >= messages.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
      compress_tool_message(msg, age: messages.size - i)
    else
      msg
    end
  end
end

#detect_tool_name(content) ⇒ String?

Detect tool name from content

Parameters:

  • content (String)

    Tool result content

Returns:

  • (String, nil)

    Tool name or nil



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_countInteger

Get count of messages with ephemeral content

Returns:

  • (Integer)

    Number of messages with ephemeral content attached



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 blocks from content

Parameters:

  • content (String)

    Content to extract from

Returns:

  • (Array<String>)

    Array of system reminder blocks



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

Returns:

  • (Boolean)

    True if ephemeral content exists



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

Parameters:

  • content (String)

    Content to check

Returns:

  • (Boolean)

    True if reminders found



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.

Parameters:

  • persistent_messages (Array<RubyLLM::Message>)

    Messages from @messages

Returns:

  • (Array<RubyLLM::Message>)

    Messages with ephemeral content embedded



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(persistent_messages)
  return persistent_messages.dup if @ephemeral_content.empty?

  # Clone messages and embed ephemeral content
  messages_for_llm = persistent_messages.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
    embedded_content = [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(embedded_content, msg.content.attachments),
        tool_call_id: msg.tool_call_id,
      )
    else
      RubyLLM::Message.new(
        role: msg.role,
        content: embedded_content,
        tool_call_id: msg.tool_call_id,
      )
    end
  end

  messages_for_llm
end

#rerunnable_tool?(tool_name) ⇒ Boolean

Check if a tool is re-runnable (idempotent, can get same data again)

Parameters:

  • tool_name (String, nil)

    Tool name

Returns:

  • (Boolean)

    True if tool can be re-run safely



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_compressionvoid

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 blocks from content

Returns clean content without system reminders.

Parameters:

  • content (String)

    Content to strip from

Returns:

  • (String)

    Clean content



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

Parameters:

  • messages (Array<RubyLLM::Message>)

    Messages to potentially summarize

  • before_index (Integer)

    Summarize messages before this index

  • strategy (Symbol) (defaults to: :truncate)

    Summarization strategy (:llm, :truncate, :remove)

Returns:

  • (Array<RubyLLM::Message>)

    Optimized message array



166
167
168
169
# File 'lib/swarm_sdk/agent/context_manager.rb', line 166

def summarize_old_messages(messages, before_index:, strategy: :truncate)
  # TODO: Implement when needed
  messages
end

#truncate_to_fit(messages, max_tokens:, keep_recent: 10) ⇒ Array<RubyLLM::Message>

Future: Truncate messages to fit within context window

Parameters:

  • messages (Array<RubyLLM::Message>)

    Messages to fit

  • max_tokens (Integer)

    Maximum token budget

  • keep_recent (Integer) (defaults to: 10)

    Number of recent messages to always keep

Returns:

  • (Array<RubyLLM::Message>)

    Truncated messages



177
178
179
180
# File 'lib/swarm_sdk/agent/context_manager.rb', line 177

def truncate_to_fit(messages, max_tokens:, keep_recent: 10)
  # TODO: Implement when needed
  messages
end