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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/builder.rb +41 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +22 -5
- data/lib/swarm_sdk/agent/definition.rb +52 -6
- data/lib/swarm_sdk/configuration.rb +3 -1
- data/lib/swarm_sdk/prompts/memory.md.erb +480 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +16 -3
- data/lib/swarm_sdk/swarm/builder.rb +9 -1
- data/lib/swarm_sdk/swarm/tool_configurator.rb +73 -23
- data/lib/swarm_sdk/swarm.rb +51 -7
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/memory/memory_delete.rb +64 -0
- data/lib/swarm_sdk/tools/memory/memory_edit.rb +145 -0
- data/lib/swarm_sdk/tools/memory/memory_glob.rb +94 -0
- data/lib/swarm_sdk/tools/memory/memory_grep.rb +147 -0
- data/lib/swarm_sdk/tools/memory/memory_multi_edit.rb +228 -0
- data/lib/swarm_sdk/tools/memory/memory_read.rb +82 -0
- data/lib/swarm_sdk/tools/memory/memory_write.rb +90 -0
- data/lib/swarm_sdk/tools/registry.rb +11 -3
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/memory_storage.rb +300 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/storage_read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +39 -0
- metadata +18 -5
- data/lib/swarm_sdk/tools/scratchpad_list.rb +0 -88
- data/lib/swarm_sdk/tools/scratchpad_read.rb +0 -59
- data/lib/swarm_sdk/tools/scratchpad_write.rb +0 -88
- 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
|
data/lib/swarm_sdk/version.rb
CHANGED
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
|
+
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/
|
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
|