swarm_sdk 2.0.3 → 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 (34) 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 +73 -23
  10. data/lib/swarm_sdk/swarm.rb +51 -7
  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 -3
  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/memory_storage.rb +300 -0
  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/storage_read_tracker.rb +61 -0
  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 -5
  31. data/lib/swarm_sdk/tools/scratchpad_list.rb +0 -88
  32. data/lib/swarm_sdk/tools/scratchpad_read.rb +0 -59
  33. data/lib/swarm_sdk/tools/scratchpad_write.rb +0 -88
  34. data/lib/swarm_sdk/tools/stores/scratchpad.rb +0 -153
@@ -0,0 +1,61 @@
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
@@ -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.3"
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.3
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,18 +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_list.rb
146
- - lib/swarm_sdk/tools/scratchpad_read.rb
147
- - 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
148
158
  - lib/swarm_sdk/tools/stores/read_tracker.rb
149
- - lib/swarm_sdk/tools/stores/scratchpad.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
150
162
  - lib/swarm_sdk/tools/stores/todo_manager.rb
151
163
  - lib/swarm_sdk/tools/think.rb
152
164
  - lib/swarm_sdk/tools/todo_write.rb
165
+ - lib/swarm_sdk/tools/web_fetch.rb
153
166
  - lib/swarm_sdk/tools/write.rb
154
167
  - lib/swarm_sdk/utils.rb
155
168
  - lib/swarm_sdk/version.rb
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- # Tool for listing scratchpad entries with metadata
6
- #
7
- # Lists available scratchpad entries with titles and sizes.
8
- # Supports filtering by path prefix.
9
- class ScratchpadList < RubyLLM::Tool
10
- define_method(:name) { "ScratchpadList" }
11
-
12
- description <<~DESC
13
- List available scratchpad entries with titles and metadata.
14
- Use this to discover what content is available in scratchpad memory.
15
- Optionally filter by path prefix.
16
- DESC
17
-
18
- param :prefix,
19
- desc: "Filter by path prefix (e.g., 'parallel/', 'analysis/'). Leave empty to list all entries.",
20
- required: false
21
-
22
- class << self
23
- # Create a ScratchpadList tool for a specific scratchpad instance
24
- #
25
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
26
- # @return [ScratchpadList] Tool instance
27
- def create_for_scratchpad(scratchpad)
28
- new(scratchpad)
29
- end
30
- end
31
-
32
- # Initialize with scratchpad instance
33
- #
34
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
35
- def initialize(scratchpad)
36
- super() # Call RubyLLM::Tool's initialize
37
- @scratchpad = scratchpad
38
- end
39
-
40
- # Execute the tool
41
- #
42
- # @param prefix [String, nil] Optional path prefix filter
43
- # @return [String] Formatted list of entries
44
- def execute(prefix: nil)
45
- entries = scratchpad.list(prefix: prefix)
46
-
47
- if entries.empty?
48
- return "Scratchpad is empty" if prefix.nil? || prefix.empty?
49
-
50
- return "No entries found with prefix '#{prefix}'"
51
- end
52
-
53
- result = []
54
- result << "Scratchpad contents (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
55
-
56
- entries.each do |entry|
57
- result << " scratchpad://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
58
- end
59
-
60
- result.join("\n")
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
@@ -1,59 +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
- # @return [ScratchpadRead] Tool instance
27
- def create_for_scratchpad(scratchpad)
28
- new(scratchpad)
29
- end
30
- end
31
-
32
- # Initialize with scratchpad instance
33
- #
34
- # @param scratchpad [Stores::Scratchpad] Shared scratchpad instance
35
- def initialize(scratchpad)
36
- super() # Call RubyLLM::Tool's initialize
37
- @scratchpad = scratchpad
38
- end
39
-
40
- # Execute the tool
41
- #
42
- # @param file_path [String] Path to read from
43
- # @return [String] Content at the path or error message
44
- def execute(file_path:)
45
- scratchpad.read(file_path: file_path)
46
- rescue ArgumentError => e
47
- validation_error(e.message)
48
- end
49
-
50
- private
51
-
52
- attr_reader :scratchpad
53
-
54
- def validation_error(message)
55
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
56
- end
57
- end
58
- end
59
- 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