swarm_sdk 2.0.6 → 2.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  3. data/lib/swarm_sdk/agent/builder.rb +16 -42
  4. data/lib/swarm_sdk/agent/chat/context_tracker.rb +43 -0
  5. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +41 -3
  6. data/lib/swarm_sdk/agent/chat.rb +426 -61
  7. data/lib/swarm_sdk/agent/context.rb +5 -1
  8. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  9. data/lib/swarm_sdk/agent/definition.rb +57 -24
  10. data/lib/swarm_sdk/plugin.rb +147 -0
  11. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  12. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +7 -1
  13. data/lib/swarm_sdk/swarm/agent_initializer.rb +80 -12
  14. data/lib/swarm_sdk/swarm/tool_configurator.rb +116 -44
  15. data/lib/swarm_sdk/swarm.rb +44 -8
  16. data/lib/swarm_sdk/tools/clock.rb +44 -0
  17. data/lib/swarm_sdk/tools/grep.rb +16 -19
  18. data/lib/swarm_sdk/tools/registry.rb +23 -12
  19. data/lib/swarm_sdk/tools/todo_write.rb +1 -1
  20. data/lib/swarm_sdk/version.rb +1 -1
  21. data/lib/swarm_sdk.rb +4 -0
  22. metadata +7 -12
  23. data/lib/swarm_sdk/prompts/memory.md.erb +0 -480
  24. data/lib/swarm_sdk/tools/memory/memory_delete.rb +0 -64
  25. data/lib/swarm_sdk/tools/memory/memory_edit.rb +0 -145
  26. data/lib/swarm_sdk/tools/memory/memory_glob.rb +0 -94
  27. data/lib/swarm_sdk/tools/memory/memory_grep.rb +0 -147
  28. data/lib/swarm_sdk/tools/memory/memory_multi_edit.rb +0 -228
  29. data/lib/swarm_sdk/tools/memory/memory_read.rb +0 -82
  30. data/lib/swarm_sdk/tools/memory/memory_write.rb +0 -90
  31. data/lib/swarm_sdk/tools/stores/memory_storage.rb +0 -300
  32. data/lib/swarm_sdk/tools/stores/storage_read_tracker.rb +0 -61
@@ -1,90 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module Memory
6
- # Tool for writing content to memory storage
7
- #
8
- # Stores content in persistent, per-agent memory storage with metadata.
9
- # Each agent has its own isolated memory storage that persists across sessions.
10
- class MemoryWrite < RubyLLM::Tool
11
- define_method(:name) { "MemoryWrite" }
12
-
13
- description <<~DESC
14
- Store content in memory for later retrieval.
15
- Use this to save detailed outputs, analysis, or results that would
16
- otherwise bloat tool responses. Only you (this agent) can access your memory.
17
-
18
- IMPORTANT: You must determine the appropriate file_path based on the task you're performing.
19
- Choose a logical, descriptive path that reflects the content type and purpose.
20
- Examples: 'analysis/code_review', 'research/findings', 'parallel/batch_1/results', 'logs/debug_trace'
21
- DESC
22
-
23
- param :file_path,
24
- desc: "File-path-like address you determine based on the task (e.g., 'analysis/report', 'parallel/batch1/task_0')",
25
- required: true
26
-
27
- param :content,
28
- desc: "Content to store in memory (max 1MB per entry)",
29
- required: true
30
-
31
- param :title,
32
- desc: "Brief title describing the content (shown in listings)",
33
- required: true
34
-
35
- class << self
36
- # Create a MemoryWrite tool for a specific memory storage instance
37
- #
38
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
39
- # @return [MemoryWrite] Tool instance
40
- def create_for_memory(memory_storage)
41
- new(memory_storage)
42
- end
43
- end
44
-
45
- # Initialize with memory storage instance
46
- #
47
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
48
- def initialize(memory_storage)
49
- super() # Call RubyLLM::Tool's initialize
50
- @memory_storage = memory_storage
51
- end
52
-
53
- # Execute the tool
54
- #
55
- # @param file_path [String] Path to store content
56
- # @param content [String] Content to store
57
- # @param title [String] Brief title
58
- # @return [String] Success message with path and size
59
- def execute(file_path:, content:, title:)
60
- entry = memory_storage.write(file_path: file_path, content: content, title: title)
61
- "Stored at memory://#{file_path} (#{format_bytes(entry.size)})"
62
- rescue ArgumentError => e
63
- validation_error(e.message)
64
- end
65
-
66
- private
67
-
68
- attr_reader :memory_storage
69
-
70
- def validation_error(message)
71
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
72
- end
73
-
74
- # Format bytes to human-readable size
75
- #
76
- # @param bytes [Integer] Number of bytes
77
- # @return [String] Formatted size
78
- def format_bytes(bytes)
79
- if bytes >= 1_000_000
80
- "#{(bytes.to_f / 1_000_000).round(1)}MB"
81
- elsif bytes >= 1_000
82
- "#{(bytes.to_f / 1_000).round(1)}KB"
83
- else
84
- "#{bytes}B"
85
- end
86
- end
87
- end
88
- end
89
- end
90
- end
@@ -1,300 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module Stores
6
- # MemoryStorage provides persistent, per-agent storage
7
- #
8
- # Features:
9
- # - Per-agent: Each agent has its own isolated storage
10
- # - Persistent: ALWAYS saves to JSON file
11
- # - Path-based: Hierarchical organization using file-path-like addresses
12
- # - Metadata-rich: Stores content + title + timestamp + size
13
- # - Thread-safe: Mutex-protected operations
14
- class MemoryStorage < Storage
15
- # Initialize memory storage with required persistence
16
- #
17
- # @param persist_to [String] Path to JSON file for persistence (REQUIRED)
18
- # @raise [ArgumentError] If persist_to is not provided
19
- def initialize(persist_to:)
20
- super() # Initialize parent Storage class
21
- raise ArgumentError, "persist_to is required for MemoryStorage" if persist_to.nil? || persist_to.to_s.strip.empty?
22
-
23
- @entries = {}
24
- @total_size = 0
25
- @persist_to = persist_to
26
- @mutex = Mutex.new
27
-
28
- # Load existing data if file exists
29
- load_from_file if File.exist?(@persist_to)
30
- end
31
-
32
- # Write content to memory storage
33
- #
34
- # @param file_path [String] Path to store content
35
- # @param content [String] Content to store
36
- # @param title [String] Brief title describing the content
37
- # @raise [ArgumentError] If size limits are exceeded
38
- # @return [Entry] The created entry
39
- def write(file_path:, content:, title:)
40
- @mutex.synchronize do
41
- raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
42
- raise ArgumentError, "content is required" if content.nil?
43
- raise ArgumentError, "title is required" if title.nil? || title.to_s.strip.empty?
44
-
45
- content_size = content.bytesize
46
-
47
- # Check entry size limit
48
- if content_size > MAX_ENTRY_SIZE
49
- raise ArgumentError, "Content exceeds maximum size (#{format_bytes(MAX_ENTRY_SIZE)}). " \
50
- "Current: #{format_bytes(content_size)}"
51
- end
52
-
53
- # Calculate new total size
54
- existing_entry = @entries[file_path]
55
- existing_size = existing_entry ? existing_entry.size : 0
56
- new_total_size = @total_size - existing_size + content_size
57
-
58
- # Check total size limit
59
- if new_total_size > MAX_TOTAL_SIZE
60
- raise ArgumentError, "Memory storage full (#{format_bytes(MAX_TOTAL_SIZE)} limit). " \
61
- "Current: #{format_bytes(@total_size)}, " \
62
- "Would be: #{format_bytes(new_total_size)}. " \
63
- "Clear old entries or use smaller content."
64
- end
65
-
66
- # Create entry
67
- entry = Entry.new(
68
- content: content,
69
- title: title,
70
- updated_at: Time.now,
71
- size: content_size,
72
- )
73
-
74
- # Update storage
75
- @entries[file_path] = entry
76
- @total_size = new_total_size
77
-
78
- # Always persist to file
79
- save_to_file
80
-
81
- entry
82
- end
83
- end
84
-
85
- # Read content from memory storage
86
- #
87
- # @param file_path [String] Path to read from
88
- # @raise [ArgumentError] If path not found
89
- # @return [String] Content at the path
90
- def read(file_path:)
91
- raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
92
-
93
- entry = @entries[file_path]
94
- raise ArgumentError, "memory://#{file_path} not found" unless entry
95
-
96
- entry.content
97
- end
98
-
99
- # Delete a specific entry
100
- #
101
- # @param file_path [String] Path to delete
102
- # @raise [ArgumentError] If path not found
103
- # @return [void]
104
- def delete(file_path:)
105
- @mutex.synchronize do
106
- raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
107
-
108
- entry = @entries[file_path]
109
- raise ArgumentError, "memory://#{file_path} not found" unless entry
110
-
111
- # Update total size
112
- @total_size -= entry.size
113
-
114
- # Remove entry
115
- @entries.delete(file_path)
116
-
117
- # Always persist to file
118
- save_to_file
119
- end
120
- end
121
-
122
- # List memory storage entries, optionally filtered by prefix
123
- #
124
- # @param prefix [String, nil] Filter by path prefix
125
- # @return [Array<Hash>] Array of entry metadata (path, title, size, updated_at)
126
- def list(prefix: nil)
127
- entries = @entries
128
-
129
- # Filter by prefix if provided
130
- if prefix && !prefix.empty?
131
- entries = entries.select { |path, _| path.start_with?(prefix) }
132
- end
133
-
134
- # Return metadata sorted by path
135
- entries.map do |path, entry|
136
- {
137
- path: path,
138
- title: entry.title,
139
- size: entry.size,
140
- updated_at: entry.updated_at,
141
- }
142
- end.sort_by { |e| e[:path] }
143
- end
144
-
145
- # Search entries by glob pattern
146
- #
147
- # @param pattern [String] Glob pattern (e.g., "**/*.txt", "parallel/*/task_*")
148
- # @return [Array<Hash>] Array of matching entry metadata, sorted by most recent first
149
- def glob(pattern:)
150
- raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
151
-
152
- # Convert glob pattern to regex
153
- regex = glob_to_regex(pattern)
154
-
155
- # Filter entries by pattern
156
- matching_entries = @entries.select { |path, _| regex.match?(path) }
157
-
158
- # Return metadata sorted by most recent first
159
- matching_entries.map do |path, entry|
160
- {
161
- path: path,
162
- title: entry.title,
163
- size: entry.size,
164
- updated_at: entry.updated_at,
165
- }
166
- end.sort_by { |e| -e[:updated_at].to_f }
167
- end
168
-
169
- # Search entry content by pattern
170
- #
171
- # @param pattern [String] Regular expression pattern to search for
172
- # @param case_insensitive [Boolean] Whether to perform case-insensitive search
173
- # @param output_mode [String] Output mode: "files_with_matches" (default), "content", or "count"
174
- # @return [Array<Hash>, String] Results based on output_mode
175
- def grep(pattern:, case_insensitive: false, output_mode: "files_with_matches")
176
- raise ArgumentError, "pattern is required" if pattern.nil? || pattern.to_s.strip.empty?
177
-
178
- # Create regex from pattern
179
- flags = case_insensitive ? Regexp::IGNORECASE : 0
180
- regex = Regexp.new(pattern, flags)
181
-
182
- case output_mode
183
- when "files_with_matches"
184
- # Return just the paths that match
185
- matching_paths = @entries.select { |_path, entry| regex.match?(entry.content) }
186
- .map { |path, _| path }
187
- .sort
188
- matching_paths
189
- when "content"
190
- # Return paths with matching lines, sorted by most recent first
191
- results = []
192
- @entries.each do |path, entry|
193
- matching_lines = []
194
- entry.content.each_line.with_index(1) do |line, line_num|
195
- matching_lines << { line_number: line_num, content: line.chomp } if regex.match?(line)
196
- end
197
- results << { path: path, matches: matching_lines, updated_at: entry.updated_at } unless matching_lines.empty?
198
- end
199
- results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
200
- when "count"
201
- # Return paths with match counts, sorted by most recent first
202
- results = []
203
- @entries.each do |path, entry|
204
- count = entry.content.scan(regex).size
205
- results << { path: path, count: count, updated_at: entry.updated_at } if count > 0
206
- end
207
- results.sort_by { |r| -r[:updated_at].to_f }.map { |r| r.except(:updated_at) }
208
- else
209
- raise ArgumentError, "Invalid output_mode: #{output_mode}. Must be 'files_with_matches', 'content', or 'count'"
210
- end
211
- end
212
-
213
- # Clear all entries
214
- #
215
- # @return [void]
216
- def clear
217
- @mutex.synchronize do
218
- @entries.clear
219
- @total_size = 0
220
- save_to_file
221
- end
222
- end
223
-
224
- # Get current total size
225
- #
226
- # @return [Integer] Total size in bytes
227
- attr_reader :total_size
228
-
229
- # Get number of entries
230
- #
231
- # @return [Integer] Number of entries
232
- def size
233
- @entries.size
234
- end
235
-
236
- private
237
-
238
- # Save memory storage data to JSON file
239
- #
240
- # @return [void]
241
- def save_to_file
242
- # Convert entries to serializable format
243
- data = {
244
- version: 1,
245
- total_size: @total_size,
246
- entries: @entries.transform_values do |entry|
247
- {
248
- content: entry.content,
249
- title: entry.title,
250
- updated_at: entry.updated_at.iso8601,
251
- size: entry.size,
252
- }
253
- end,
254
- }
255
-
256
- # Ensure directory exists
257
- dir = File.dirname(@persist_to)
258
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
259
-
260
- # Write to file atomically (write to temp file, then rename)
261
- temp_file = "#{@persist_to}.tmp"
262
- File.write(temp_file, JSON.pretty_generate(data))
263
- File.rename(temp_file, @persist_to)
264
- end
265
-
266
- # Load memory storage data from JSON file
267
- #
268
- # @return [void]
269
- def load_from_file
270
- return unless File.exist?(@persist_to)
271
-
272
- data = JSON.parse(File.read(@persist_to))
273
-
274
- # Restore entries
275
- @entries = data["entries"].transform_values do |entry_data|
276
- Entry.new(
277
- content: entry_data["content"],
278
- title: entry_data["title"],
279
- updated_at: Time.parse(entry_data["updated_at"]),
280
- size: entry_data["size"],
281
- )
282
- end
283
-
284
- # Restore total size
285
- @total_size = data["total_size"]
286
- rescue JSON::ParserError => e
287
- # If file is corrupted, log warning and start fresh
288
- warn("Warning: Failed to load memory storage from #{@persist_to}: #{e.message}. Starting with empty storage.")
289
- @entries = {}
290
- @total_size = 0
291
- rescue StandardError => e
292
- # If any other error occurs, log warning and start fresh
293
- warn("Warning: Failed to load memory storage from #{@persist_to}: #{e.message}. Starting with empty storage.")
294
- @entries = {}
295
- @total_size = 0
296
- end
297
- end
298
- end
299
- end
300
- end
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module Stores
6
- # StorageReadTracker manages read-entry tracking for all agents
7
- #
8
- # This module maintains a global registry of which memory entries each agent
9
- # has read during their conversation. This enables enforcement of the
10
- # "read-before-edit" rule that ensures agents have context before modifying entries.
11
- #
12
- # Each agent maintains an independent set of read entries, keyed by agent identifier.
13
- module StorageReadTracker
14
- @read_entries = {}
15
- @mutex = Mutex.new
16
-
17
- class << self
18
- # Register that an agent has read a storage entry
19
- #
20
- # @param agent_id [Symbol] The agent identifier
21
- # @param entry_path [String] The storage entry path
22
- def register_read(agent_id, entry_path)
23
- @mutex.synchronize do
24
- @read_entries[agent_id] ||= Set.new
25
- @read_entries[agent_id] << entry_path
26
- end
27
- end
28
-
29
- # Check if an agent has read a storage entry
30
- #
31
- # @param agent_id [Symbol] The agent identifier
32
- # @param entry_path [String] The storage entry path
33
- # @return [Boolean] true if the agent has read this entry
34
- def entry_read?(agent_id, entry_path)
35
- @mutex.synchronize do
36
- return false unless @read_entries[agent_id]
37
-
38
- @read_entries[agent_id].include?(entry_path)
39
- end
40
- end
41
-
42
- # Clear read history for an agent (useful for testing)
43
- #
44
- # @param agent_id [Symbol] The agent identifier
45
- def clear(agent_id)
46
- @mutex.synchronize do
47
- @read_entries.delete(agent_id)
48
- end
49
- end
50
-
51
- # Clear all read history (useful for testing)
52
- def clear_all
53
- @mutex.synchronize do
54
- @read_entries.clear
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end