swarm_sdk 2.0.4 → 2.0.5

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +41 -0
  3. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +22 -5
  4. data/lib/swarm_sdk/agent/definition.rb +52 -6
  5. data/lib/swarm_sdk/configuration.rb +3 -1
  6. data/lib/swarm_sdk/prompts/memory.md.erb +480 -0
  7. data/lib/swarm_sdk/swarm/agent_initializer.rb +16 -3
  8. data/lib/swarm_sdk/swarm/builder.rb +9 -1
  9. data/lib/swarm_sdk/swarm/tool_configurator.rb +75 -32
  10. data/lib/swarm_sdk/swarm.rb +50 -11
  11. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  12. data/lib/swarm_sdk/tools/memory/memory_delete.rb +64 -0
  13. data/lib/swarm_sdk/tools/memory/memory_edit.rb +145 -0
  14. data/lib/swarm_sdk/tools/memory/memory_glob.rb +94 -0
  15. data/lib/swarm_sdk/tools/memory/memory_grep.rb +147 -0
  16. data/lib/swarm_sdk/tools/memory/memory_multi_edit.rb +228 -0
  17. data/lib/swarm_sdk/tools/memory/memory_read.rb +82 -0
  18. data/lib/swarm_sdk/tools/memory/memory_write.rb +90 -0
  19. data/lib/swarm_sdk/tools/registry.rb +11 -4
  20. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  21. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  22. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  23. data/lib/swarm_sdk/tools/stores/{scratchpad.rb → memory_storage.rb} +72 -88
  24. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  25. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  26. data/lib/swarm_sdk/tools/stores/{scratchpad_read_tracker.rb → storage_read_tracker.rb} +7 -7
  27. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  28. data/lib/swarm_sdk/version.rb +1 -1
  29. data/lib/swarm_sdk.rb +39 -0
  30. metadata +18 -9
  31. data/lib/swarm_sdk/tools/scratchpad_edit.rb +0 -143
  32. data/lib/swarm_sdk/tools/scratchpad_glob.rb +0 -92
  33. data/lib/swarm_sdk/tools/scratchpad_grep.rb +0 -145
  34. data/lib/swarm_sdk/tools/scratchpad_multi_edit.rb +0 -226
  35. data/lib/swarm_sdk/tools/scratchpad_read.rb +0 -80
  36. data/lib/swarm_sdk/tools/scratchpad_write.rb +0 -88
@@ -1,145 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for searching scratchpad content by pattern
6
- #
7
- # Searches content stored in scratchpad entries using regex patterns.
8
- # All agents in the swarm share the same scratchpad.
9
- class ScratchpadGrep < RubyLLM::Tool
10
- define_method(:name) { "ScratchpadGrep" }
11
-
12
- description <<~DESC
13
- Search scratchpad content by pattern (like grep).
14
- Use regex patterns to search content within scratchpad entries.
15
- Returns matching entries and optionally line numbers and content.
16
-
17
- Output modes:
18
- - files_with_matches: Only list paths containing matches (default)
19
- - content: Show matching lines with line numbers
20
- - count: Show number of matches per file
21
- DESC
22
-
23
- param :pattern,
24
- desc: "Regular expression pattern to search for",
25
- required: true
26
-
27
- param :case_insensitive,
28
- desc: "Perform case-insensitive search (default: false)",
29
- required: false
30
-
31
- param :output_mode,
32
- desc: "Output mode: 'files_with_matches' (default), 'content', or 'count'",
33
- required: false
34
-
35
- class << self
36
- # Create a ScratchpadGrep tool for a specific scratchpad instance
37
- #
38
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
39
- # @return [ScratchpadGrep] Tool instance
40
- def create_for_scratchpad(scratchpad)
41
- new(scratchpad)
42
- end
43
- end
44
-
45
- # Initialize with scratchpad instance
46
- #
47
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
48
- def initialize(scratchpad)
49
- super() # Call RubyLLM::Tool's initialize
50
- @scratchpad = scratchpad
51
- end
52
-
53
- # Execute the tool
54
- #
55
- # @param pattern [String] Regex pattern to search for
56
- # @param case_insensitive [Boolean] Whether to perform case-insensitive search
57
- # @param output_mode [String] Output mode
58
- # @return [String] Formatted search results
59
- def execute(pattern:, case_insensitive: false, output_mode: "files_with_matches")
60
- results = scratchpad.grep(
61
- pattern: pattern,
62
- case_insensitive: case_insensitive,
63
- output_mode: output_mode,
64
- )
65
-
66
- format_results(results, pattern, output_mode)
67
- rescue ArgumentError => e
68
- validation_error(e.message)
69
- rescue RegexpError => e
70
- validation_error("Invalid regex pattern: #{e.message}")
71
- end
72
-
73
- private
74
-
75
- attr_reader :scratchpad
76
-
77
- def validation_error(message)
78
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
79
- end
80
-
81
- def format_results(results, pattern, output_mode)
82
- case output_mode
83
- when "files_with_matches"
84
- format_files_with_matches(results, pattern)
85
- when "content"
86
- format_content(results, pattern)
87
- when "count"
88
- format_count(results, pattern)
89
- else
90
- validation_error("Invalid output_mode: #{output_mode}")
91
- end
92
- end
93
-
94
- def format_files_with_matches(paths, pattern)
95
- if paths.empty?
96
- return "No matches found for pattern '#{pattern}'"
97
- end
98
-
99
- result = []
100
- result << "Scratchpad entries matching '#{pattern}' (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
101
- paths.each do |path|
102
- result << " scratchpad://#{path}"
103
- end
104
- result.join("\n")
105
- end
106
-
107
- def format_content(results, pattern)
108
- if results.empty?
109
- return "No matches found for pattern '#{pattern}'"
110
- end
111
-
112
- total_matches = results.sum { |r| r[:matches].size }
113
- output = []
114
- output << "Scratchpad entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
115
- output << ""
116
-
117
- results.each do |result|
118
- output << "scratchpad://#{result[:path]}:"
119
- result[:matches].each do |match|
120
- output << " #{match[:line_number]}: #{match[:content]}"
121
- end
122
- output << ""
123
- end
124
-
125
- output.join("\n").rstrip
126
- end
127
-
128
- def format_count(results, pattern)
129
- if results.empty?
130
- return "No matches found for pattern '#{pattern}'"
131
- end
132
-
133
- total_matches = results.sum { |r| r[:count] }
134
- output = []
135
- output << "Scratchpad entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
136
-
137
- results.each do |result|
138
- output << " scratchpad://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
139
- end
140
-
141
- output.join("\n")
142
- end
143
- end
144
- end
145
- end
@@ -1,226 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for performing multiple edits to a scratchpad entry
6
- #
7
- # Applies multiple edit operations sequentially to a single scratchpad entry.
8
- # Each edit sees the result of all previous edits, allowing for
9
- # coordinated multi-step transformations.
10
- # All agents in the swarm share the same scratchpad.
11
- class ScratchpadMultiEdit < RubyLLM::Tool
12
- define_method(:name) { "ScratchpadMultiEdit" }
13
-
14
- description <<~DESC
15
- Performs multiple exact string replacements in a single scratchpad entry.
16
- Edits are applied sequentially, so later edits see the results of earlier ones.
17
- You must use ScratchpadRead on the entry before editing it.
18
- When editing text from ScratchpadRead output, ensure you preserve the exact indentation as it appears AFTER the line number prefix.
19
- The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual content to match.
20
- Never include any part of the line number prefix in the old_string or new_string.
21
- Each edit will FAIL if old_string is not unique in the entry. Either provide a larger string with more surrounding context to make it unique or use replace_all to change every instance of old_string.
22
- Use replace_all for replacing and renaming strings across the entry.
23
- DESC
24
-
25
- param :file_path,
26
- desc: "Path to the scratchpad entry (e.g., 'analysis/report', 'parallel/batch1/task_0')",
27
- required: true
28
-
29
- param :edits_json,
30
- type: "string",
31
- desc: <<~DESC.chomp,
32
- JSON array of edit operations. Each edit must have:
33
- old_string (exact text to replace),
34
- new_string (replacement text),
35
- and optionally replace_all (boolean, default false).
36
- Example: [{"old_string":"foo","new_string":"bar","replace_all":false}]
37
- DESC
38
- required: true
39
-
40
- class << self
41
- # Create a ScratchpadMultiEdit tool for a specific scratchpad instance
42
- #
43
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
44
- # @param agent_name [Symbol, String] Agent identifier for tracking reads
45
- # @return [ScratchpadMultiEdit] Tool instance
46
- def create_for_scratchpad(scratchpad, agent_name)
47
- new(scratchpad, agent_name)
48
- end
49
- end
50
-
51
- # Initialize with scratchpad instance and agent name
52
- #
53
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
54
- # @param agent_name [Symbol, String] Agent identifier
55
- def initialize(scratchpad, agent_name)
56
- super() # Call RubyLLM::Tool's initialize
57
- @scratchpad = scratchpad
58
- @agent_name = agent_name.to_sym
59
- end
60
-
61
- # Execute the tool
62
- #
63
- # @param file_path [String] Path to scratchpad entry
64
- # @param edits_json [String] JSON array of edit operations
65
- # @return [String] Success message or error
66
- def execute(file_path:, edits_json:)
67
- # Validate inputs
68
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
69
-
70
- # Parse JSON
71
- edits = begin
72
- JSON.parse(edits_json)
73
- rescue JSON::ParserError
74
- nil
75
- end
76
-
77
- return validation_error("Invalid JSON format. Please provide a valid JSON array of edit operations.") if edits.nil?
78
-
79
- return validation_error("edits must be an array") unless edits.is_a?(Array)
80
- return validation_error("edits array cannot be empty") if edits.empty?
81
-
82
- # Read current content (this will raise ArgumentError if entry doesn't exist)
83
- content = scratchpad.read(file_path: file_path)
84
-
85
- # Enforce read-before-edit
86
- unless Stores::ScratchpadReadTracker.entry_read?(@agent_name, file_path)
87
- return validation_error(
88
- "Cannot edit scratchpad entry without reading it first. " \
89
- "You must use ScratchpadRead on 'scratchpad://#{file_path}' before editing it. " \
90
- "This ensures you have the current content to match against.",
91
- )
92
- end
93
-
94
- # Validate edit operations
95
- validated_edits = []
96
- edits.each_with_index do |edit, index|
97
- unless edit.is_a?(Hash)
98
- return validation_error("Edit at index #{index} must be a hash/object with old_string and new_string")
99
- end
100
-
101
- # Convert string keys to symbols for consistency
102
- edit = edit.transform_keys(&:to_sym)
103
-
104
- unless edit[:old_string]
105
- return validation_error("Edit at index #{index} missing required field 'old_string'")
106
- end
107
-
108
- unless edit[:new_string]
109
- return validation_error("Edit at index #{index} missing required field 'new_string'")
110
- end
111
-
112
- # old_string and new_string must be different
113
- if edit[:old_string] == edit[:new_string]
114
- return validation_error("Edit at index #{index}: old_string and new_string must be different")
115
- end
116
-
117
- validated_edits << {
118
- old_string: edit[:old_string].to_s,
119
- new_string: edit[:new_string].to_s,
120
- replace_all: edit[:replace_all] == true,
121
- index: index,
122
- }
123
- end
124
-
125
- # Apply edits sequentially
126
- results = []
127
- current_content = content
128
-
129
- validated_edits.each do |edit|
130
- # Check if old_string exists in current content
131
- unless current_content.include?(edit[:old_string])
132
- return error_with_results(
133
- <<~ERROR.chomp,
134
- Edit #{edit[:index]}: old_string not found in scratchpad entry.
135
- Make sure it matches exactly, including all whitespace and indentation.
136
- Do not include line number prefixes from ScratchpadRead tool output.
137
- Note: This edit follows #{edit[:index]} previous edit(s) which may have changed the content.
138
- ERROR
139
- results,
140
- )
141
- end
142
-
143
- # Count occurrences
144
- occurrences = current_content.scan(edit[:old_string]).count
145
-
146
- # If not replace_all and multiple occurrences, error
147
- if !edit[:replace_all] && occurrences > 1
148
- return error_with_results(
149
- <<~ERROR.chomp,
150
- Edit #{edit[:index]}: Found #{occurrences} occurrences of old_string.
151
- Either provide more surrounding context to make the match unique, or set replace_all: true to replace all occurrences.
152
- ERROR
153
- results,
154
- )
155
- end
156
-
157
- # Perform replacement
158
- new_content = if edit[:replace_all]
159
- current_content.gsub(edit[:old_string], edit[:new_string])
160
- else
161
- current_content.sub(edit[:old_string], edit[:new_string])
162
- end
163
-
164
- # Record result
165
- replaced_count = edit[:replace_all] ? occurrences : 1
166
- results << {
167
- index: edit[:index],
168
- status: "success",
169
- occurrences: replaced_count,
170
- message: "Replaced #{replaced_count} occurrence(s)",
171
- }
172
-
173
- # Update content for next edit
174
- current_content = new_content
175
- end
176
-
177
- # Get existing entry metadata
178
- entries = scratchpad.list
179
- existing_entry = entries.find { |e| e[:path] == file_path }
180
-
181
- # Write updated content back (preserving the title)
182
- scratchpad.write(
183
- file_path: file_path,
184
- content: current_content,
185
- title: existing_entry[:title],
186
- )
187
-
188
- # Build success message
189
- total_replacements = results.sum { |r| r[:occurrences] }
190
- message = "Successfully applied #{validated_edits.size} edit(s) to scratchpad://#{file_path}\n"
191
- message += "Total replacements: #{total_replacements}\n\n"
192
- message += "Details:\n"
193
- results.each do |result|
194
- message += " Edit #{result[:index]}: #{result[:message]}\n"
195
- end
196
-
197
- message
198
- rescue ArgumentError => e
199
- validation_error(e.message)
200
- end
201
-
202
- private
203
-
204
- attr_reader :scratchpad
205
-
206
- def validation_error(message)
207
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
208
- end
209
-
210
- def error_with_results(message, results)
211
- output = "<tool_use_error>InputValidationError: #{message}\n\n"
212
-
213
- if results.any?
214
- output += "Previous successful edits before error:\n"
215
- results.each do |result|
216
- output += " Edit #{result[:index]}: #{result[:message]}\n"
217
- end
218
- output += "\n"
219
- end
220
-
221
- output += "Note: The scratchpad entry has NOT been modified. All or nothing approach - if any edit fails, no changes are saved.</tool_use_error>"
222
- output
223
- end
224
- end
225
- end
226
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for reading content from scratchpad memory
6
- #
7
- # Retrieves content stored by any agent using scratchpad_write.
8
- # All agents in the swarm share the same scratchpad.
9
- class ScratchpadRead < RubyLLM::Tool
10
- define_method(:name) { "ScratchpadRead" }
11
-
12
- description <<~DESC
13
- Read content from scratchpad.
14
- Use this to retrieve detailed outputs, analysis, or results that were
15
- stored using scratchpad_write. Any agent can read any scratchpad content.
16
- DESC
17
-
18
- param :file_path,
19
- desc: "Path to read from scratchpad (e.g., 'analysis/report', 'parallel/batch1/task_0')",
20
- required: true
21
-
22
- class << self
23
- # Create a ScratchpadRead tool for a specific scratchpad instance
24
- #
25
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
26
- # @param agent_name [Symbol, String] Agent identifier for tracking reads
27
- # @return [ScratchpadRead] Tool instance
28
- def create_for_scratchpad(scratchpad, agent_name)
29
- new(scratchpad, agent_name)
30
- end
31
- end
32
-
33
- # Initialize with scratchpad instance and agent name
34
- #
35
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
36
- # @param agent_name [Symbol, String] Agent identifier
37
- def initialize(scratchpad, agent_name)
38
- super() # Call RubyLLM::Tool's initialize
39
- @scratchpad = scratchpad
40
- @agent_name = agent_name.to_sym
41
- end
42
-
43
- # Execute the tool
44
- #
45
- # @param file_path [String] Path to read from
46
- # @return [String] Content at the path with line numbers, or error message
47
- def execute(file_path:)
48
- # Register this read in the tracker
49
- Stores::ScratchpadReadTracker.register_read(@agent_name, file_path)
50
-
51
- content = scratchpad.read(file_path: file_path)
52
- format_with_line_numbers(content)
53
- rescue ArgumentError => e
54
- validation_error(e.message)
55
- end
56
-
57
- private
58
-
59
- attr_reader :scratchpad
60
-
61
- def validation_error(message)
62
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
63
- end
64
-
65
- # Format content with line numbers (same format as Read tool)
66
- #
67
- # @param content [String] Content to format
68
- # @return [String] Content with line numbers
69
- def format_with_line_numbers(content)
70
- lines = content.lines
71
- output_lines = lines.each_with_index.map do |line, idx|
72
- line_number = idx + 1
73
- display_line = line.chomp
74
- "#{line_number.to_s.rjust(6)}→#{display_line}"
75
- end
76
- output_lines.join("\n")
77
- end
78
- end
79
- end
80
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for writing content to scratchpad memory
6
- #
7
- # Stores content in session-scoped, in-memory storage with metadata.
8
- # All agents in the swarm share the same scratchpad.
9
- class ScratchpadWrite < RubyLLM::Tool
10
- define_method(:name) { "ScratchpadWrite" }
11
-
12
- description <<~DESC
13
- Store content in scratchpad for later retrieval.
14
- Use this to save detailed outputs, analysis, or results that would
15
- otherwise bloat tool responses. Any agent can read this content using scratchpad_read.
16
-
17
- IMPORTANT: You must determine the appropriate file_path based on the task you're performing.
18
- Choose a logical, descriptive path that reflects the content type and purpose.
19
- Examples: 'analysis/code_review', 'research/findings', 'parallel/batch_1/results', 'logs/debug_trace'
20
- DESC
21
-
22
- param :file_path,
23
- desc: "File-path-like address you determine based on the task (e.g., 'analysis/report', 'parallel/batch1/task_0')",
24
- required: true
25
-
26
- param :content,
27
- desc: "Content to store in scratchpad (max 1MB per entry)",
28
- required: true
29
-
30
- param :title,
31
- desc: "Brief title describing the content (shown in listings)",
32
- required: true
33
-
34
- class << self
35
- # Create a ScratchpadWrite tool for a specific scratchpad instance
36
- #
37
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
38
- # @return [ScratchpadWrite] Tool instance
39
- def create_for_scratchpad(scratchpad)
40
- new(scratchpad)
41
- end
42
- end
43
-
44
- # Initialize with scratchpad instance
45
- #
46
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
47
- def initialize(scratchpad)
48
- super() # Call RubyLLM::Tool's initialize
49
- @scratchpad = scratchpad
50
- end
51
-
52
- # Execute the tool
53
- #
54
- # @param file_path [String] Path to store content
55
- # @param content [String] Content to store
56
- # @param title [String] Brief title
57
- # @return [String] Success message with path and size
58
- def execute(file_path:, content:, title:)
59
- entry = scratchpad.write(file_path: file_path, content: content, title: title)
60
- "Stored at scratchpad://#{file_path} (#{format_bytes(entry.size)})"
61
- rescue ArgumentError => e
62
- validation_error(e.message)
63
- end
64
-
65
- private
66
-
67
- attr_reader :scratchpad
68
-
69
- def validation_error(message)
70
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
71
- end
72
-
73
- # Format bytes to human-readable size
74
- #
75
- # @param bytes [Integer] Number of bytes
76
- # @return [String] Formatted size
77
- def format_bytes(bytes)
78
- if bytes >= 1_000_000
79
- "#{(bytes.to_f / 1_000_000).round(1)}MB"
80
- elsif bytes >= 1_000
81
- "#{(bytes.to_f / 1_000).round(1)}KB"
82
- else
83
- "#{bytes}B"
84
- end
85
- end
86
- end
87
- end
88
- end