Class: SwarmSDK::Tools::Stores::ScratchpadStorage

Inherits:
Storage
  • Object
show all
Defined in:
lib/swarm_sdk/tools/stores/scratchpad_storage.rb

Overview

ScratchpadStorage provides volatile, shared storage

Features:

  • Shared: All agents share the same scratchpad
  • Volatile: NEVER persists - all data lost when process ends
  • Path-based: Hierarchical organization using file-path-like addresses
  • Metadata-rich: Stores content + title + timestamp + size
  • Thread-safe: Mutex-protected operations

Use for temporary, cross-agent communication within a single session.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(total_size_limit: nil) ⇒ ScratchpadStorage

Initialize scratchpad storage (always volatile)

Parameters:

  • total_size_limit (Integer, nil) (defaults to: nil)

    Maximum total size in bytes (defaults to Defaults::Storage::TOTAL_SIZE_BYTES)



20
21
22
23
24
25
26
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 20

def initialize(total_size_limit: nil)
  super() # Initialize parent Storage class
  @entries = {}
  @total_size = 0
  @total_size_limit = total_size_limit || SwarmSDK.config.scratchpad_total_size_limit
  @mutex = Mutex.new
end

Instance Attribute Details

#total_sizeInteger (readonly)

Get current total size

Returns:

  • (Integer)

    Total size in bytes



217
218
219
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 217

def total_size
  @total_size
end

Instance Method Details

#all_entriesHash

Get all entries with content for snapshot

Thread-safe method that returns a copy of all entries. Used by snapshot/restore functionality.

Returns:

  • (Hash)

    { path => Entry }



232
233
234
235
236
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 232

def all_entries
  @mutex.synchronize do
    @entries.dup
  end
end

#clearvoid

This method returns an undefined value.

Clear all entries



207
208
209
210
211
212
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 207

def clear
  @mutex.synchronize do
    @entries.clear
    @total_size = 0
  end
end

#delete(file_path:) ⇒ void

This method returns an undefined value.

Delete a specific entry

Parameters:

  • file_path (String)

    Path to delete

Raises:

  • (ArgumentError)

    If path not found



98
99
100
101
102
103
104
105
106
107
108
109
110
111
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 98

def delete(file_path:)
  @mutex.synchronize do
    raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?

    entry = @entries[file_path]
    raise ArgumentError, "scratchpad://#{file_path} not found" unless entry

    # Update total size
    @total_size -= entry.size

    # Remove entry
    @entries.delete(file_path)
  end
end

#glob(pattern:) ⇒ Array<Hash>

Search entries by glob pattern

Parameters:

  • pattern (String)

    Glob pattern (e.g., "**/.txt", "parallel//task_*")

Returns:

  • (Array<Hash>)

    Array of matching entry metadata, sorted by most recent first

Raises:

  • (ArgumentError)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 140

def glob(pattern:)
  raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?

  # Convert glob pattern to regex
  regex = glob_to_regex(pattern)

  # Filter entries by pattern
  matching_entries = @entries.select { |path, _| regex.match?(path) }

  # Return metadata sorted by most recent first
  matching_entries.map do |path, entry|
    {
      path: path,
      title: entry.title,
      size: entry.size,
      updated_at: entry.updated_at,
    }
  end.sort_by { |e| -e[:updated_at].to_f }
end

#grep(pattern:, case_insensitive: false, output_mode: "files_with_matches") ⇒ Array<Hash>, String

Search entry content by pattern

Parameters:

  • pattern (String)

    Regular expression pattern to search for

  • case_insensitive (Boolean) (defaults to: false)

    Whether to perform case-insensitive search

  • output_mode (String) (defaults to: "files_with_matches")

    Output mode: "files_with_matches" (default), "content", or "count"

Returns:

  • (Array<Hash>, String)

    Results based on output_mode

Raises:

  • (ArgumentError)


166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 166

def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
  raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?

  # Create regex from pattern
  flags = case_insensitive ? Regexp::IGNORECASE : 0
  regex = Regexp.new(pattern, flags)

  case output_mode
  when "files_with_matches"
    # Return just the paths that match
    matching_paths = @entries.select { |_path, entry| regex.match?(entry.content) }
      .map { |path, _| path }
      .sort
    matching_paths
  when "content"
    # Return paths with matching lines, sorted by most recent first
    results = []
    @entries.each do |path, entry|
      matching_lines = []
      entry.content.each_line.with_index(1) do |line, line_num|
        matching_lines << { line_number: line_num, content: line.chomp } if regex.match?(line)
      end
      results << { path: path, matches: matching_lines, updated_at: entry.updated_at } unless matching_lines.empty?
    end
    results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
  when "count"
    # Return paths with match counts, sorted by most recent first
    results = []
    @entries.each do |path, entry|
      count = entry.content.scan(regex).size
      results << { path: path, count: count, updated_at: entry.updated_at } if count > 0
    end
    results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
  else
    raise ArgumentError, "Invalid output_mode: #{output_mode}. Must be 'files_with_matches', 'content', or 'count'"
  end
end

#list(prefix: nil) ⇒ Array<Hash>

List scratchpad entries, optionally filtered by prefix

Parameters:

  • prefix (String, nil) (defaults to: nil)

    Filter by path prefix

Returns:

  • (Array<Hash>)

    Array of entry metadata (path, title, size, updated_at)



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 117

def list(prefix: nil)
  entries = @entries

  # Filter by prefix if provided
  if prefix && !prefix.empty?
    entries = entries.select { |path, _| path.start_with?(prefix) }
  end

  # Return metadata sorted by path
  entries.map do |path, entry|
    {
      path: path,
      title: entry.title,
      size: entry.size,
      updated_at: entry.updated_at,
    }
  end.sort_by { |e| e[:path] }
end

#read(file_path:) ⇒ String

Read content from scratchpad

Parameters:

  • file_path (String)

    Path to read from

Returns:

  • (String)

    Content at the path

Raises:

  • (ArgumentError)

    If path not found



84
85
86
87
88
89
90
91
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 84

def read(file_path:)
  raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?

  entry = @entries[file_path]
  raise ArgumentError, "scratchpad://#{file_path} not found" unless entry

  entry.content
end

#restore_entries(entries_data) ⇒ void

This method returns an undefined value.

Restore entries from snapshot

Restores entries directly without using write() to preserve timestamps. This ensures entry ordering and metadata accuracy after restore.

Parameters:

  • entries_data (Hash)

    { path => { content:, title:, updated_at:, size: } }



245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 245

def restore_entries(entries_data)
  @mutex.synchronize do
    entries_data.each do |path, data|
      # Handle both symbol and string keys from JSON
      content = data[:content] || data["content"]
      title = data[:title] || data["title"]
      updated_at_str = data[:updated_at] || data["updated_at"]

      # Parse timestamp from ISO8601 string
      updated_at = Time.parse(updated_at_str)

      # Create entry with preserved timestamp
      entry = Entry.new(
        content: content,
        title: title,
        updated_at: updated_at,
        size: content.bytesize,
      )

      # Update storage
      @entries[path] = entry
      @total_size += entry.size
    end
  end
end

#sizeInteger

Get number of entries

Returns:

  • (Integer)

    Number of entries



222
223
224
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 222

def size
  @entries.size
end

#write(file_path:, content:, title:) ⇒ Entry

Write content to scratchpad

Parameters:

  • file_path (String)

    Path to store content

  • content (String)

    Content to store

  • title (String)

    Brief title describing the content

Returns:

  • (Entry)

    The created entry

Raises:

  • (ArgumentError)

    If size limits are exceeded



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/swarm_sdk/tools/stores/scratchpad_storage.rb', line 35

def write(file_path:, content:, title:)
  @mutex.synchronize do
    raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
    raise ArgumentError, "content is required" if content.nil?
    raise ArgumentError, "title is required" if title.nil? || title.to_s.strip.empty?

    content_size = content.bytesize

    # Check entry size limit
    entry_size_limit = SwarmSDK.config.scratchpad_entry_size_limit
    if content_size > entry_size_limit
      raise ArgumentError, "Content exceeds maximum size (#{format_bytes(entry_size_limit)}). " \
        "Current: #{format_bytes(content_size)}"
    end

    # Calculate new total size
    existing_entry = @entries[file_path]
    existing_size = existing_entry ? existing_entry.size : 0
    new_total_size = @total_size - existing_size + content_size

    # Check total size limit
    if new_total_size > @total_size_limit
      raise ArgumentError, "Scratchpad full (#{format_bytes(@total_size_limit)} limit). " \
        "Current: #{format_bytes(@total_size)}, " \
        "Would be: #{format_bytes(new_total_size)}. " \
        "Clear old entries or use smaller content."
    end

    # Create entry
    entry = Entry.new(
      content: content,
      title: title,
      updated_at: Time.now,
      size: content_size,
    )

    # Update storage
    @entries[file_path] = entry
    @total_size = new_total_size

    entry
  end
end