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
@@ -3,22 +3,22 @@
3
3
  module SwarmSDK
4
4
  module Tools
5
5
  module Stores
6
- # ScratchpadReadTracker manages read-entry tracking for all agents
6
+ # StorageReadTracker manages read-entry tracking for all agents
7
7
  #
8
- # This module maintains a global registry of which scratchpad entries each agent
8
+ # This module maintains a global registry of which memory entries each agent
9
9
  # has read during their conversation. This enables enforcement of the
10
10
  # "read-before-edit" rule that ensures agents have context before modifying entries.
11
11
  #
12
12
  # Each agent maintains an independent set of read entries, keyed by agent identifier.
13
- module ScratchpadReadTracker
13
+ module StorageReadTracker
14
14
  @read_entries = {}
15
15
  @mutex = Mutex.new
16
16
 
17
17
  class << self
18
- # Register that an agent has read a scratchpad entry
18
+ # Register that an agent has read a storage entry
19
19
  #
20
20
  # @param agent_id [Symbol] The agent identifier
21
- # @param entry_path [String] The scratchpad entry path
21
+ # @param entry_path [String] The storage entry path
22
22
  def register_read(agent_id, entry_path)
23
23
  @mutex.synchronize do
24
24
  @read_entries[agent_id] ||= Set.new
@@ -26,10 +26,10 @@ module SwarmSDK
26
26
  end
27
27
  end
28
28
 
29
- # Check if an agent has read a scratchpad entry
29
+ # Check if an agent has read a storage entry
30
30
  #
31
31
  # @param agent_id [Symbol] The agent identifier
32
- # @param entry_path [String] The scratchpad entry path
32
+ # @param entry_path [String] The storage entry path
33
33
  # @return [Boolean] true if the agent has read this entry
34
34
  def entry_read?(agent_id, entry_path)
35
35
  @mutex.synchronize do
@@ -0,0 +1,261 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Tools
5
+ # WebFetch tool for fetching and processing web content
6
+ #
7
+ # Fetches content from URLs, converts HTML to markdown, and processes it
8
+ # using an AI model to extract information based on a provided prompt.
9
+ class WebFetch < RubyLLM::Tool
10
+ def initialize
11
+ super()
12
+ @cache = {}
13
+ @cache_ttl = 900 # 15 minutes in seconds
14
+ @llm_enabled = SwarmSDK.settings.webfetch_llm_enabled?
15
+ end
16
+
17
+ def name
18
+ "WebFetch"
19
+ end
20
+
21
+ description <<~DESC
22
+ - Fetches content from a specified URL and converts it to markdown
23
+ - Optionally processes the content with an LLM if configured
24
+ - Fetches the URL content, converts HTML to markdown
25
+ - Returns markdown content or LLM analysis (based on configuration)
26
+ - Use this tool when you need to retrieve and analyze web content
27
+
28
+ Usage notes:
29
+ - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with "mcp__".
30
+ - The URL must be a fully-formed valid URL
31
+ - HTTP URLs will be automatically upgraded to HTTPS
32
+ - This tool is read-only and does not modify any files
33
+ - Content will be truncated if very large
34
+ - Includes a self-cleaning 15-minute cache for faster responses
35
+ - When a URL redirects to a different host, the tool will inform you and provide the redirect URL in a special format. You should then make a new WebFetch request with the redirect URL to fetch the content.
36
+
37
+ LLM Processing:
38
+ - When SwarmSDK is configured with webfetch_provider and webfetch_model, the 'prompt' parameter is required
39
+ - The tool will process the markdown content with the configured LLM using your prompt
40
+ - Without this configuration, the tool returns raw markdown and the 'prompt' parameter is optional (ignored if provided)
41
+ - Configure with: SwarmSDK.configure { |c| c.webfetch_provider = "anthropic"; c.webfetch_model = "claude-3-5-haiku-20241022" }
42
+ DESC
43
+
44
+ param :url,
45
+ type: "string",
46
+ desc: "The URL to fetch content from",
47
+ required: true
48
+
49
+ param :prompt,
50
+ type: "string",
51
+ desc: "The prompt to run on the fetched content. Required when SwarmSDK is configured with webfetch_provider and webfetch_model. Optional otherwise (ignored if LLM processing not configured).",
52
+ required: false
53
+
54
+ MAX_CONTENT_LENGTH = 100_000 # characters
55
+ USER_AGENT = "SwarmSDK WebFetch Tool (https://github.com/parruda/claude-swarm)"
56
+ TIMEOUT = 30 # seconds
57
+
58
+ def execute(url:, prompt: nil)
59
+ # Validate inputs
60
+ return validation_error("url is required") if url.nil? || url.empty?
61
+
62
+ # Validate prompt when LLM processing is enabled
63
+ if @llm_enabled && (prompt.nil? || prompt.empty?)
64
+ return validation_error("prompt is required when LLM processing is configured")
65
+ end
66
+
67
+ # Validate and normalize URL
68
+ normalized_url = normalize_url(url)
69
+ return validation_error("Invalid URL format: #{url}") unless normalized_url
70
+
71
+ # Check cache first (cache key includes prompt if LLM is enabled)
72
+ cache_key = @llm_enabled ? "#{normalized_url}:#{prompt}" : normalized_url
73
+ cached = get_from_cache(cache_key)
74
+ return cached if cached
75
+
76
+ # Fetch the URL
77
+ fetch_result = fetch_url(normalized_url)
78
+ return fetch_result if fetch_result.is_a?(String) && fetch_result.start_with?("Error")
79
+
80
+ # Check for redirects to different hosts
81
+ if fetch_result[:redirect_url] && different_host?(normalized_url, fetch_result[:redirect_url])
82
+ return format_redirect_message(fetch_result[:redirect_url])
83
+ end
84
+
85
+ # Convert HTML to markdown
86
+ markdown_content = html_to_markdown(fetch_result[:body])
87
+
88
+ # Truncate if too long
89
+ if markdown_content.length > MAX_CONTENT_LENGTH
90
+ markdown_content = markdown_content[0...MAX_CONTENT_LENGTH]
91
+ markdown_content += "\n\n[Content truncated due to length]"
92
+ end
93
+
94
+ # Process with AI model if LLM is enabled, otherwise return markdown
95
+ result = if @llm_enabled
96
+ process_with_llm(markdown_content, prompt, normalized_url)
97
+ else
98
+ markdown_content
99
+ end
100
+
101
+ # Cache the result
102
+ store_in_cache(cache_key, result)
103
+
104
+ result
105
+ rescue StandardError => e
106
+ error("Unexpected error fetching URL: #{e.class.name} - #{e.message}")
107
+ end
108
+
109
+ private
110
+
111
+ def validation_error(message)
112
+ "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
113
+ end
114
+
115
+ def error(message)
116
+ "Error: #{message}"
117
+ end
118
+
119
+ def normalize_url(url)
120
+ # Upgrade HTTP to HTTPS
121
+ url = url.sub(%r{^http://}, "https://")
122
+
123
+ # Validate URL format
124
+ uri = URI.parse(url)
125
+ return unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
126
+ return unless uri.host
127
+
128
+ uri.to_s
129
+ rescue URI::InvalidURIError
130
+ nil
131
+ end
132
+
133
+ def different_host?(url1, url2)
134
+ uri1 = URI.parse(url1)
135
+ uri2 = URI.parse(url2)
136
+ uri1.host != uri2.host
137
+ rescue URI::InvalidURIError
138
+ false
139
+ end
140
+
141
+ def fetch_url(url)
142
+ require "faraday"
143
+ require "faraday/follow_redirects"
144
+
145
+ response = Faraday.new(url: url) do |conn|
146
+ conn.request(:url_encoded)
147
+ conn.response(:follow_redirects, limit: 5)
148
+ conn.adapter(Faraday.default_adapter)
149
+ conn.options.timeout = TIMEOUT
150
+ conn.options.open_timeout = TIMEOUT
151
+ end.get do |req|
152
+ req.headers["User-Agent"] = USER_AGENT
153
+ req.headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
154
+ end
155
+
156
+ unless response.success?
157
+ return error("HTTP #{response.status}: Failed to fetch URL")
158
+ end
159
+
160
+ # Check final URL for redirects
161
+ final_url = response.env.url.to_s
162
+ redirect_url = final_url if final_url != url
163
+
164
+ {
165
+ body: response.body,
166
+ redirect_url: redirect_url,
167
+ }
168
+ rescue Faraday::TimeoutError
169
+ error("Request timed out after #{TIMEOUT} seconds")
170
+ rescue Faraday::ConnectionFailed => e
171
+ error("Connection failed: #{e.message}")
172
+ rescue StandardError => e
173
+ error("Failed to fetch URL: #{e.class.name} - #{e.message}")
174
+ end
175
+
176
+ def html_to_markdown(html)
177
+ # Use HtmlConverter to handle conversion with optional reverse_markdown gem
178
+ converter = DocumentConverters::HtmlConverter.new
179
+ converter.convert_string(html)
180
+ end
181
+
182
+ def process_with_llm(content, prompt, url)
183
+ # Use configured model for processing
184
+ # Format the prompt to include the content
185
+ full_prompt = <<~PROMPT
186
+ You are analyzing content from the URL: #{url}
187
+
188
+ User request: #{prompt}
189
+
190
+ Content:
191
+ #{content}
192
+
193
+ Please respond to the user's request based on the content above.
194
+ PROMPT
195
+
196
+ # Get settings
197
+ config = SwarmSDK.settings
198
+
199
+ # Build chat with configured provider and model
200
+ chat_params = {
201
+ model: config.webfetch_model,
202
+ provider: config.webfetch_provider.to_sym,
203
+ }
204
+ chat_params[:base_url] = config.webfetch_base_url if config.webfetch_base_url
205
+
206
+ chat = RubyLLM.chat(**chat_params).with_params(max_tokens: config.webfetch_max_tokens)
207
+
208
+ response = chat.ask(full_prompt)
209
+
210
+ # Extract the text response
211
+ response_text = response.content
212
+ return error("Failed to process content with LLM: No response text") unless response_text
213
+
214
+ response_text
215
+ rescue StandardError => e
216
+ error("Failed to process content with LLM: #{e.class.name} - #{e.message}")
217
+ end
218
+
219
+ def format_redirect_message(redirect_url)
220
+ <<~MESSAGE
221
+ This URL redirected to a different host.
222
+
223
+ Redirect URL: #{redirect_url}
224
+
225
+ <system-reminder>
226
+ The requested URL redirected to a different host. To fetch the content from the redirect URL,
227
+ make a new WebFetch request with the redirect URL provided above.
228
+ </system-reminder>
229
+ MESSAGE
230
+ end
231
+
232
+ def get_from_cache(key)
233
+ entry = @cache[key]
234
+ return unless entry
235
+
236
+ # Check if cache entry is still valid
237
+ if Time.now.to_i - entry[:timestamp] > @cache_ttl
238
+ @cache.delete(key)
239
+ return
240
+ end
241
+
242
+ entry[:value]
243
+ end
244
+
245
+ def store_in_cache(key, value)
246
+ # Clean old cache entries
247
+ clean_cache
248
+
249
+ @cache[key] = {
250
+ value: value,
251
+ timestamp: Time.now.to_i,
252
+ }
253
+ end
254
+
255
+ def clean_cache
256
+ now = Time.now.to_i
257
+ @cache.delete_if { |_key, entry| now - entry[:timestamp] > @cache_ttl }
258
+ end
259
+ end
260
+ end
261
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.0.4"
4
+ VERSION = "2.0.5"
5
5
  end
data/lib/swarm_sdk.rb CHANGED
@@ -41,11 +41,50 @@ module SwarmSDK
41
41
  class StateError < Error; end
42
42
 
43
43
  class << self
44
+ # Settings for SwarmSDK (global configuration)
45
+ attr_accessor :settings
46
+
44
47
  # Main entry point for DSL
45
48
  def build(&block)
46
49
  Swarm::Builder.build(&block)
47
50
  end
51
+
52
+ # Configure SwarmSDK global settings
53
+ def configure
54
+ self.settings ||= Settings.new
55
+ yield(settings)
56
+ end
57
+
58
+ # Reset settings to defaults
59
+ def reset_settings!
60
+ self.settings = Settings.new
61
+ end
62
+
63
+ # Alias for backward compatibility
64
+ alias_method :configuration, :settings
65
+ alias_method :reset_configuration!, :reset_settings!
48
66
  end
67
+
68
+ # Settings class for SwarmSDK global settings (not to be confused with Configuration for YAML loading)
69
+ class Settings
70
+ # WebFetch tool LLM processing configuration
71
+ attr_accessor :webfetch_provider, :webfetch_model, :webfetch_base_url, :webfetch_max_tokens
72
+
73
+ def initialize
74
+ @webfetch_provider = nil
75
+ @webfetch_model = nil
76
+ @webfetch_base_url = nil
77
+ @webfetch_max_tokens = 4096
78
+ end
79
+
80
+ # Check if WebFetch LLM processing is enabled
81
+ def webfetch_llm_enabled?
82
+ !@webfetch_provider.nil? && !@webfetch_model.nil?
83
+ end
84
+ end
85
+
86
+ # Initialize default settings
87
+ self.settings = Settings.new
49
88
  end
50
89
 
51
90
  # Automatically configure RubyLLM from environment variables
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.4
4
+ version: 2.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -118,6 +118,7 @@ files:
118
118
  - lib/swarm_sdk/permissions/validator.rb
119
119
  - lib/swarm_sdk/permissions_builder.rb
120
120
  - lib/swarm_sdk/prompts/base_system_prompt.md.erb
121
+ - lib/swarm_sdk/prompts/memory.md.erb
121
122
  - lib/swarm_sdk/providers/openai_with_responses.rb
122
123
  - lib/swarm_sdk/result.rb
123
124
  - lib/swarm_sdk/swarm.rb
@@ -130,6 +131,7 @@ files:
130
131
  - lib/swarm_sdk/tools/delegate.rb
131
132
  - lib/swarm_sdk/tools/document_converters/base_converter.rb
132
133
  - lib/swarm_sdk/tools/document_converters/docx_converter.rb
134
+ - lib/swarm_sdk/tools/document_converters/html_converter.rb
133
135
  - lib/swarm_sdk/tools/document_converters/pdf_converter.rb
134
136
  - lib/swarm_sdk/tools/document_converters/xlsx_converter.rb
135
137
  - lib/swarm_sdk/tools/edit.rb
@@ -138,22 +140,29 @@ files:
138
140
  - lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb
139
141
  - lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb
140
142
  - lib/swarm_sdk/tools/image_formats/tiff_builder.rb
143
+ - lib/swarm_sdk/tools/memory/memory_delete.rb
144
+ - lib/swarm_sdk/tools/memory/memory_edit.rb
145
+ - lib/swarm_sdk/tools/memory/memory_glob.rb
146
+ - lib/swarm_sdk/tools/memory/memory_grep.rb
147
+ - lib/swarm_sdk/tools/memory/memory_multi_edit.rb
148
+ - lib/swarm_sdk/tools/memory/memory_read.rb
149
+ - lib/swarm_sdk/tools/memory/memory_write.rb
141
150
  - lib/swarm_sdk/tools/multi_edit.rb
142
151
  - lib/swarm_sdk/tools/path_resolver.rb
143
152
  - lib/swarm_sdk/tools/read.rb
144
153
  - lib/swarm_sdk/tools/registry.rb
145
- - lib/swarm_sdk/tools/scratchpad_edit.rb
146
- - lib/swarm_sdk/tools/scratchpad_glob.rb
147
- - lib/swarm_sdk/tools/scratchpad_grep.rb
148
- - lib/swarm_sdk/tools/scratchpad_multi_edit.rb
149
- - lib/swarm_sdk/tools/scratchpad_read.rb
150
- - lib/swarm_sdk/tools/scratchpad_write.rb
154
+ - lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb
155
+ - lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb
156
+ - lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb
157
+ - lib/swarm_sdk/tools/stores/memory_storage.rb
151
158
  - lib/swarm_sdk/tools/stores/read_tracker.rb
152
- - lib/swarm_sdk/tools/stores/scratchpad.rb
153
- - lib/swarm_sdk/tools/stores/scratchpad_read_tracker.rb
159
+ - lib/swarm_sdk/tools/stores/scratchpad_storage.rb
160
+ - lib/swarm_sdk/tools/stores/storage.rb
161
+ - lib/swarm_sdk/tools/stores/storage_read_tracker.rb
154
162
  - lib/swarm_sdk/tools/stores/todo_manager.rb
155
163
  - lib/swarm_sdk/tools/think.rb
156
164
  - lib/swarm_sdk/tools/todo_write.rb
165
+ - lib/swarm_sdk/tools/web_fetch.rb
157
166
  - lib/swarm_sdk/tools/write.rb
158
167
  - lib/swarm_sdk/utils.rb
159
168
  - lib/swarm_sdk/version.rb
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for editing scratchpad entries with exact string replacement
6
- #
7
- # Performs exact string replacements in scratchpad content.
8
- # All agents in the swarm share the same scratchpad.
9
- class ScratchpadEdit < RubyLLM::Tool
10
- define_method(:name) { "ScratchpadEdit" }
11
-
12
- description <<~DESC
13
- Performs exact string replacements in scratchpad entries.
14
- Works like the Edit tool but operates on scratchpad content instead of files.
15
- You must use ScratchpadRead on the entry before editing it.
16
- When editing text from ScratchpadRead output, ensure you preserve the exact indentation as it appears AFTER the line number prefix.
17
- The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual content to match.
18
- Never include any part of the line number prefix in the old_string or new_string.
19
- The 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.
20
- Use replace_all for replacing and renaming strings across the entry.
21
- DESC
22
-
23
- param :file_path,
24
- desc: "Path to the scratchpad entry (e.g., 'analysis/report', 'parallel/batch1/task_0')",
25
- required: true
26
-
27
- param :old_string,
28
- desc: "The exact text to replace (must match exactly including whitespace)",
29
- required: true
30
-
31
- param :new_string,
32
- desc: "The text to replace it with (must be different from old_string)",
33
- required: true
34
-
35
- param :replace_all,
36
- desc: "Replace all occurrences of old_string (default false)",
37
- required: false
38
-
39
- class << self
40
- # Create a ScratchpadEdit tool for a specific scratchpad instance
41
- #
42
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
43
- # @param agent_name [Symbol, String] Agent identifier for tracking reads
44
- # @return [ScratchpadEdit] Tool instance
45
- def create_for_scratchpad(scratchpad, agent_name)
46
- new(scratchpad, agent_name)
47
- end
48
- end
49
-
50
- # Initialize with scratchpad instance and agent name
51
- #
52
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
53
- # @param agent_name [Symbol, String] Agent identifier
54
- def initialize(scratchpad, agent_name)
55
- super() # Call RubyLLM::Tool's initialize
56
- @scratchpad = scratchpad
57
- @agent_name = agent_name.to_sym
58
- end
59
-
60
- # Execute the tool
61
- #
62
- # @param file_path [String] Path to scratchpad entry
63
- # @param old_string [String] Text to replace
64
- # @param new_string [String] Replacement text
65
- # @param replace_all [Boolean] Replace all occurrences
66
- # @return [String] Success message or error
67
- def execute(file_path:, old_string:, new_string:, replace_all: false)
68
- # Validate inputs
69
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
70
- return validation_error("old_string is required") if old_string.nil? || old_string.empty?
71
- return validation_error("new_string is required") if new_string.nil?
72
-
73
- # old_string and new_string must be different
74
- if old_string == new_string
75
- return validation_error("old_string and new_string must be different. They are currently identical.")
76
- end
77
-
78
- # Read current content (this will raise ArgumentError if entry doesn't exist)
79
- content = scratchpad.read(file_path: file_path)
80
-
81
- # Enforce read-before-edit
82
- unless Stores::ScratchpadReadTracker.entry_read?(@agent_name, file_path)
83
- return validation_error(
84
- "Cannot edit scratchpad entry without reading it first. " \
85
- "You must use ScratchpadRead on 'scratchpad://#{file_path}' before editing it. " \
86
- "This ensures you have the current content to match against.",
87
- )
88
- end
89
-
90
- # Check if old_string exists in content
91
- unless content.include?(old_string)
92
- return validation_error(<<~ERROR.chomp)
93
- old_string not found in scratchpad entry. Make sure it matches exactly, including all whitespace and indentation.
94
- Do not include line number prefixes from ScratchpadRead tool output.
95
- ERROR
96
- end
97
-
98
- # Count occurrences
99
- occurrences = content.scan(old_string).count
100
-
101
- # If not replace_all and multiple occurrences, error
102
- if !replace_all && occurrences > 1
103
- return validation_error(<<~ERROR.chomp)
104
- Found #{occurrences} occurrences of old_string.
105
- Either provide more surrounding context to make the match unique, or use replace_all: true to replace all occurrences.
106
- ERROR
107
- end
108
-
109
- # Perform replacement
110
- new_content = if replace_all
111
- content.gsub(old_string, new_string)
112
- else
113
- content.sub(old_string, new_string)
114
- end
115
-
116
- # Get existing entry metadata
117
- entries = scratchpad.list
118
- existing_entry = entries.find { |e| e[:path] == file_path }
119
-
120
- # Write updated content back (preserving the title)
121
- scratchpad.write(
122
- file_path: file_path,
123
- content: new_content,
124
- title: existing_entry[:title],
125
- )
126
-
127
- # Build success message
128
- replaced_count = replace_all ? occurrences : 1
129
- "Successfully replaced #{replaced_count} occurrence(s) in scratchpad://#{file_path}"
130
- rescue ArgumentError => e
131
- validation_error(e.message)
132
- end
133
-
134
- private
135
-
136
- attr_reader :scratchpad
137
-
138
- def validation_error(message)
139
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
140
- end
141
- end
142
- end
143
- end
@@ -1,92 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for searching scratchpad entries by glob pattern
6
- #
7
- # Finds scratchpad entries matching a glob pattern (like filesystem glob).
8
- # All agents in the swarm share the same scratchpad.
9
- class ScratchpadGlob < RubyLLM::Tool
10
- define_method(:name) { "ScratchpadGlob" }
11
-
12
- description <<~DESC
13
- Search scratchpad entries by glob pattern.
14
- Works like filesystem glob - use * for wildcards, ** for recursive matching.
15
- Use this to discover entries matching specific patterns.
16
-
17
- Examples:
18
- - "parallel/*" - all entries directly under parallel/
19
- - "parallel/**" - all entries under parallel/ (recursive)
20
- - "*/report" - all entries named "report" in any top-level directory
21
- - "analysis/*/result_*" - entries like "analysis/foo/result_1"
22
- DESC
23
-
24
- param :pattern,
25
- desc: "Glob pattern to match (e.g., '**/*.txt', 'parallel/*/task_*')",
26
- required: true
27
-
28
- class << self
29
- # Create a ScratchpadGlob tool for a specific scratchpad instance
30
- #
31
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
32
- # @return [ScratchpadGlob] Tool instance
33
- def create_for_scratchpad(scratchpad)
34
- new(scratchpad)
35
- end
36
- end
37
-
38
- # Initialize with scratchpad instance
39
- #
40
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
41
- def initialize(scratchpad)
42
- super() # Call RubyLLM::Tool's initialize
43
- @scratchpad = scratchpad
44
- end
45
-
46
- # Execute the tool
47
- #
48
- # @param pattern [String] Glob pattern to match
49
- # @return [String] Formatted list of matching entries
50
- def execute(pattern:)
51
- entries = scratchpad.glob(pattern: pattern)
52
-
53
- if entries.empty?
54
- return "No entries found matching pattern '#{pattern}'"
55
- end
56
-
57
- result = []
58
- result << "Scratchpad entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
59
-
60
- entries.each do |entry|
61
- result << " scratchpad://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
62
- end
63
-
64
- result.join("\n")
65
- rescue ArgumentError => e
66
- validation_error(e.message)
67
- end
68
-
69
- private
70
-
71
- attr_reader :scratchpad
72
-
73
- def validation_error(message)
74
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
75
- end
76
-
77
- # Format bytes to human-readable size
78
- #
79
- # @param bytes [Integer] Number of bytes
80
- # @return [String] Formatted size
81
- def format_bytes(bytes)
82
- if bytes >= 1_000_000
83
- "#{(bytes.to_f / 1_000_000).round(1)}MB"
84
- elsif bytes >= 1_000
85
- "#{(bytes.to_f / 1_000).round(1)}KB"
86
- else
87
- "#{bytes}B"
88
- end
89
- end
90
- end
91
- end
92
- end