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,228 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
module Memory
|
6
|
+
# Tool for performing multiple edits to a memory entry
|
7
|
+
#
|
8
|
+
# Applies multiple edit operations sequentially to a single memory entry.
|
9
|
+
# Each edit sees the result of all previous edits, allowing for
|
10
|
+
# coordinated multi-step transformations.
|
11
|
+
# Each agent has its own isolated memory storage.
|
12
|
+
class MemoryMultiEdit < RubyLLM::Tool
|
13
|
+
define_method(:name) { "MemoryMultiEdit" }
|
14
|
+
|
15
|
+
description <<~DESC
|
16
|
+
Performs multiple exact string replacements in a single memory entry.
|
17
|
+
Edits are applied sequentially, so later edits see the results of earlier ones.
|
18
|
+
You must use MemoryRead on the entry before editing it.
|
19
|
+
When editing text from MemoryRead output, ensure you preserve the exact indentation as it appears AFTER the line number prefix.
|
20
|
+
The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual content to match.
|
21
|
+
Never include any part of the line number prefix in the old_string or new_string.
|
22
|
+
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.
|
23
|
+
Use replace_all for replacing and renaming strings across the entry.
|
24
|
+
DESC
|
25
|
+
|
26
|
+
param :file_path,
|
27
|
+
desc: "Path to the memory entry (e.g., 'analysis/report', 'parallel/batch1/task_0')",
|
28
|
+
required: true
|
29
|
+
|
30
|
+
param :edits_json,
|
31
|
+
type: "string",
|
32
|
+
desc: <<~DESC.chomp,
|
33
|
+
JSON array of edit operations. Each edit must have:
|
34
|
+
old_string (exact text to replace),
|
35
|
+
new_string (replacement text),
|
36
|
+
and optionally replace_all (boolean, default false).
|
37
|
+
Example: [{"old_string":"foo","new_string":"bar","replace_all":false}]
|
38
|
+
DESC
|
39
|
+
required: true
|
40
|
+
|
41
|
+
class << self
|
42
|
+
# Create a MemoryMultiEdit tool for a specific memory storage instance
|
43
|
+
#
|
44
|
+
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
45
|
+
# @param agent_name [Symbol, String] Agent identifier for tracking reads
|
46
|
+
# @return [MemoryMultiEdit] Tool instance
|
47
|
+
def create_for_memory(memory_storage, agent_name)
|
48
|
+
new(memory_storage, agent_name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Initialize with memory storage instance and agent name
|
53
|
+
#
|
54
|
+
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
55
|
+
# @param agent_name [Symbol, String] Agent identifier
|
56
|
+
def initialize(memory_storage, agent_name)
|
57
|
+
super() # Call RubyLLM::Tool's initialize
|
58
|
+
@memory_storage = memory_storage
|
59
|
+
@agent_name = agent_name.to_sym
|
60
|
+
end
|
61
|
+
|
62
|
+
# Execute the tool
|
63
|
+
#
|
64
|
+
# @param file_path [String] Path to memory entry
|
65
|
+
# @param edits_json [String] JSON array of edit operations
|
66
|
+
# @return [String] Success message or error
|
67
|
+
def execute(file_path:, edits_json:)
|
68
|
+
# Validate inputs
|
69
|
+
return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
|
70
|
+
|
71
|
+
# Parse JSON
|
72
|
+
edits = begin
|
73
|
+
JSON.parse(edits_json)
|
74
|
+
rescue JSON::ParserError
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
return validation_error("Invalid JSON format. Please provide a valid JSON array of edit operations.") if edits.nil?
|
79
|
+
|
80
|
+
return validation_error("edits must be an array") unless edits.is_a?(Array)
|
81
|
+
return validation_error("edits array cannot be empty") if edits.empty?
|
82
|
+
|
83
|
+
# Read current content (this will raise ArgumentError if entry doesn't exist)
|
84
|
+
content = memory_storage.read(file_path: file_path)
|
85
|
+
|
86
|
+
# Enforce read-before-edit
|
87
|
+
unless Stores::StorageReadTracker.entry_read?(@agent_name, file_path)
|
88
|
+
return validation_error(
|
89
|
+
"Cannot edit memory entry without reading it first. " \
|
90
|
+
"You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
|
91
|
+
"This ensures you have the current content to match against.",
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Validate edit operations
|
96
|
+
validated_edits = []
|
97
|
+
edits.each_with_index do |edit, index|
|
98
|
+
unless edit.is_a?(Hash)
|
99
|
+
return validation_error("Edit at index #{index} must be a hash/object with old_string and new_string")
|
100
|
+
end
|
101
|
+
|
102
|
+
# Convert string keys to symbols for consistency
|
103
|
+
edit = edit.transform_keys(&:to_sym)
|
104
|
+
|
105
|
+
unless edit[:old_string]
|
106
|
+
return validation_error("Edit at index #{index} missing required field 'old_string'")
|
107
|
+
end
|
108
|
+
|
109
|
+
unless edit[:new_string]
|
110
|
+
return validation_error("Edit at index #{index} missing required field 'new_string'")
|
111
|
+
end
|
112
|
+
|
113
|
+
# old_string and new_string must be different
|
114
|
+
if edit[:old_string] == edit[:new_string]
|
115
|
+
return validation_error("Edit at index #{index}: old_string and new_string must be different")
|
116
|
+
end
|
117
|
+
|
118
|
+
validated_edits << {
|
119
|
+
old_string: edit[:old_string].to_s,
|
120
|
+
new_string: edit[:new_string].to_s,
|
121
|
+
replace_all: edit[:replace_all] == true,
|
122
|
+
index: index,
|
123
|
+
}
|
124
|
+
end
|
125
|
+
|
126
|
+
# Apply edits sequentially
|
127
|
+
results = []
|
128
|
+
current_content = content
|
129
|
+
|
130
|
+
validated_edits.each do |edit|
|
131
|
+
# Check if old_string exists in current content
|
132
|
+
unless current_content.include?(edit[:old_string])
|
133
|
+
return error_with_results(
|
134
|
+
<<~ERROR.chomp,
|
135
|
+
Edit #{edit[:index]}: old_string not found in memory entry.
|
136
|
+
Make sure it matches exactly, including all whitespace and indentation.
|
137
|
+
Do not include line number prefixes from MemoryRead tool output.
|
138
|
+
Note: This edit follows #{edit[:index]} previous edit(s) which may have changed the content.
|
139
|
+
ERROR
|
140
|
+
results,
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Count occurrences
|
145
|
+
occurrences = current_content.scan(edit[:old_string]).count
|
146
|
+
|
147
|
+
# If not replace_all and multiple occurrences, error
|
148
|
+
if !edit[:replace_all] && occurrences > 1
|
149
|
+
return error_with_results(
|
150
|
+
<<~ERROR.chomp,
|
151
|
+
Edit #{edit[:index]}: Found #{occurrences} occurrences of old_string.
|
152
|
+
Either provide more surrounding context to make the match unique, or set replace_all: true to replace all occurrences.
|
153
|
+
ERROR
|
154
|
+
results,
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Perform replacement
|
159
|
+
new_content = if edit[:replace_all]
|
160
|
+
current_content.gsub(edit[:old_string], edit[:new_string])
|
161
|
+
else
|
162
|
+
current_content.sub(edit[:old_string], edit[:new_string])
|
163
|
+
end
|
164
|
+
|
165
|
+
# Record result
|
166
|
+
replaced_count = edit[:replace_all] ? occurrences : 1
|
167
|
+
results << {
|
168
|
+
index: edit[:index],
|
169
|
+
status: "success",
|
170
|
+
occurrences: replaced_count,
|
171
|
+
message: "Replaced #{replaced_count} occurrence(s)",
|
172
|
+
}
|
173
|
+
|
174
|
+
# Update content for next edit
|
175
|
+
current_content = new_content
|
176
|
+
end
|
177
|
+
|
178
|
+
# Get existing entry metadata
|
179
|
+
entries = memory_storage.list
|
180
|
+
existing_entry = entries.find { |e| e[:path] == file_path }
|
181
|
+
|
182
|
+
# Write updated content back (preserving the title)
|
183
|
+
memory_storage.write(
|
184
|
+
file_path: file_path,
|
185
|
+
content: current_content,
|
186
|
+
title: existing_entry[:title],
|
187
|
+
)
|
188
|
+
|
189
|
+
# Build success message
|
190
|
+
total_replacements = results.sum { |r| r[:occurrences] }
|
191
|
+
message = "Successfully applied #{validated_edits.size} edit(s) to memory://#{file_path}\n"
|
192
|
+
message += "Total replacements: #{total_replacements}\n\n"
|
193
|
+
message += "Details:\n"
|
194
|
+
results.each do |result|
|
195
|
+
message += " Edit #{result[:index]}: #{result[:message]}\n"
|
196
|
+
end
|
197
|
+
|
198
|
+
message
|
199
|
+
rescue ArgumentError => e
|
200
|
+
validation_error(e.message)
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
attr_reader :memory_storage
|
206
|
+
|
207
|
+
def validation_error(message)
|
208
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
209
|
+
end
|
210
|
+
|
211
|
+
def error_with_results(message, results)
|
212
|
+
output = "<tool_use_error>InputValidationError: #{message}\n\n"
|
213
|
+
|
214
|
+
if results.any?
|
215
|
+
output += "Previous successful edits before error:\n"
|
216
|
+
results.each do |result|
|
217
|
+
output += " Edit #{result[:index]}: #{result[:message]}\n"
|
218
|
+
end
|
219
|
+
output += "\n"
|
220
|
+
end
|
221
|
+
|
222
|
+
output += "Note: The memory entry has NOT been modified. All or nothing approach - if any edit fails, no changes are saved.</tool_use_error>"
|
223
|
+
output
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
module Memory
|
6
|
+
# Tool for reading content from memory storage
|
7
|
+
#
|
8
|
+
# Retrieves content stored by this agent using memory_write.
|
9
|
+
# Each agent has its own isolated memory storage.
|
10
|
+
class MemoryRead < RubyLLM::Tool
|
11
|
+
define_method(:name) { "MemoryRead" }
|
12
|
+
|
13
|
+
description <<~DESC
|
14
|
+
Read content from your memory storage.
|
15
|
+
Use this to retrieve detailed outputs, analysis, or results that were
|
16
|
+
stored using memory_write. Only you (this agent) can access your memory.
|
17
|
+
DESC
|
18
|
+
|
19
|
+
param :file_path,
|
20
|
+
desc: "Path to read from memory (e.g., 'analysis/report', 'parallel/batch1/task_0')",
|
21
|
+
required: true
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Create a MemoryRead tool for a specific memory storage instance
|
25
|
+
#
|
26
|
+
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
27
|
+
# @param agent_name [Symbol, String] Agent identifier for tracking reads
|
28
|
+
# @return [MemoryRead] Tool instance
|
29
|
+
def create_for_memory(memory_storage, agent_name)
|
30
|
+
new(memory_storage, agent_name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Initialize with memory storage instance and agent name
|
35
|
+
#
|
36
|
+
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
37
|
+
# @param agent_name [Symbol, String] Agent identifier
|
38
|
+
def initialize(memory_storage, agent_name)
|
39
|
+
super() # Call RubyLLM::Tool's initialize
|
40
|
+
@memory_storage = memory_storage
|
41
|
+
@agent_name = agent_name.to_sym
|
42
|
+
end
|
43
|
+
|
44
|
+
# Execute the tool
|
45
|
+
#
|
46
|
+
# @param file_path [String] Path to read from
|
47
|
+
# @return [String] Content at the path with line numbers, or error message
|
48
|
+
def execute(file_path:)
|
49
|
+
# Register this read in the tracker
|
50
|
+
Stores::StorageReadTracker.register_read(@agent_name, file_path)
|
51
|
+
|
52
|
+
content = memory_storage.read(file_path: file_path)
|
53
|
+
format_with_line_numbers(content)
|
54
|
+
rescue ArgumentError => e
|
55
|
+
validation_error(e.message)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
attr_reader :memory_storage
|
61
|
+
|
62
|
+
def validation_error(message)
|
63
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
64
|
+
end
|
65
|
+
|
66
|
+
# Format content with line numbers (same format as Read tool)
|
67
|
+
#
|
68
|
+
# @param content [String] Content to format
|
69
|
+
# @return [String] Content with line numbers
|
70
|
+
def format_with_line_numbers(content)
|
71
|
+
lines = content.lines
|
72
|
+
output_lines = lines.each_with_index.map do |line, idx|
|
73
|
+
line_number = idx + 1
|
74
|
+
display_line = line.chomp
|
75
|
+
"#{line_number.to_s.rjust(6)}→#{display_line}"
|
76
|
+
end
|
77
|
+
output_lines.join("\n")
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
module Memory
|
6
|
+
# Tool for writing content to memory storage
|
7
|
+
#
|
8
|
+
# Stores content in persistent, per-agent memory storage with metadata.
|
9
|
+
# Each agent has its own isolated memory storage that persists across sessions.
|
10
|
+
class MemoryWrite < RubyLLM::Tool
|
11
|
+
define_method(:name) { "MemoryWrite" }
|
12
|
+
|
13
|
+
description <<~DESC
|
14
|
+
Store content in memory for later retrieval.
|
15
|
+
Use this to save detailed outputs, analysis, or results that would
|
16
|
+
otherwise bloat tool responses. Only you (this agent) can access your memory.
|
17
|
+
|
18
|
+
IMPORTANT: You must determine the appropriate file_path based on the task you're performing.
|
19
|
+
Choose a logical, descriptive path that reflects the content type and purpose.
|
20
|
+
Examples: 'analysis/code_review', 'research/findings', 'parallel/batch_1/results', 'logs/debug_trace'
|
21
|
+
DESC
|
22
|
+
|
23
|
+
param :file_path,
|
24
|
+
desc: "File-path-like address you determine based on the task (e.g., 'analysis/report', 'parallel/batch1/task_0')",
|
25
|
+
required: true
|
26
|
+
|
27
|
+
param :content,
|
28
|
+
desc: "Content to store in memory (max 1MB per entry)",
|
29
|
+
required: true
|
30
|
+
|
31
|
+
param :title,
|
32
|
+
desc: "Brief title describing the content (shown in listings)",
|
33
|
+
required: true
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# Create a MemoryWrite tool for a specific memory storage instance
|
37
|
+
#
|
38
|
+
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
39
|
+
# @return [MemoryWrite] Tool instance
|
40
|
+
def create_for_memory(memory_storage)
|
41
|
+
new(memory_storage)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Initialize with memory storage instance
|
46
|
+
#
|
47
|
+
# @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
|
48
|
+
def initialize(memory_storage)
|
49
|
+
super() # Call RubyLLM::Tool's initialize
|
50
|
+
@memory_storage = memory_storage
|
51
|
+
end
|
52
|
+
|
53
|
+
# Execute the tool
|
54
|
+
#
|
55
|
+
# @param file_path [String] Path to store content
|
56
|
+
# @param content [String] Content to store
|
57
|
+
# @param title [String] Brief title
|
58
|
+
# @return [String] Success message with path and size
|
59
|
+
def execute(file_path:, content:, title:)
|
60
|
+
entry = memory_storage.write(file_path: file_path, content: content, title: title)
|
61
|
+
"Stored at memory://#{file_path} (#{format_bytes(entry.size)})"
|
62
|
+
rescue ArgumentError => e
|
63
|
+
validation_error(e.message)
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
attr_reader :memory_storage
|
69
|
+
|
70
|
+
def validation_error(message)
|
71
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
72
|
+
end
|
73
|
+
|
74
|
+
# Format bytes to human-readable size
|
75
|
+
#
|
76
|
+
# @param bytes [Integer] Number of bytes
|
77
|
+
# @return [String] Formatted size
|
78
|
+
def format_bytes(bytes)
|
79
|
+
if bytes >= 1_000_000
|
80
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
81
|
+
elsif bytes >= 1_000
|
82
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
83
|
+
else
|
84
|
+
"#{bytes}B"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -17,10 +17,18 @@ module SwarmSDK
|
|
17
17
|
Glob: SwarmSDK::Tools::Glob,
|
18
18
|
MultiEdit: :special, # Requires agent context for read-before-edit enforcement
|
19
19
|
TodoWrite: :special, # Requires agent context for todo tracking
|
20
|
-
ScratchpadWrite: :special, # Requires scratchpad instance
|
21
|
-
ScratchpadRead: :special, # Requires scratchpad instance
|
22
|
-
ScratchpadList: :special, # Requires scratchpad instance
|
20
|
+
ScratchpadWrite: :special, # Requires scratchpad storage instance
|
21
|
+
ScratchpadRead: :special, # Requires scratchpad storage instance
|
22
|
+
ScratchpadList: :special, # Requires scratchpad storage instance
|
23
|
+
MemoryWrite: :special, # Requires memory storage instance
|
24
|
+
MemoryRead: :special, # Requires memory storage instance
|
25
|
+
MemoryEdit: :special, # Requires memory storage instance
|
26
|
+
MemoryMultiEdit: :special, # Requires memory storage instance
|
27
|
+
MemoryDelete: :special, # Requires memory storage instance
|
28
|
+
MemoryGlob: :special, # Requires memory storage instance
|
29
|
+
MemoryGrep: :special, # Requires memory storage instance
|
23
30
|
Think: SwarmSDK::Tools::Think,
|
31
|
+
WebFetch: SwarmSDK::Tools::WebFetch,
|
24
32
|
}.freeze
|
25
33
|
|
26
34
|
class << self
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
module Scratchpad
|
6
|
+
# Tool for listing scratchpad entries
|
7
|
+
#
|
8
|
+
# Shows all entries in the shared scratchpad with their metadata.
|
9
|
+
# All agents in the swarm share the same scratchpad.
|
10
|
+
class ScratchpadList < RubyLLM::Tool
|
11
|
+
define_method(:name) { "ScratchpadList" }
|
12
|
+
|
13
|
+
description <<~DESC
|
14
|
+
List all entries in scratchpad with their metadata.
|
15
|
+
Shows path, title, size, and last updated time for each entry.
|
16
|
+
Use this to discover what's stored in the scratchpad.
|
17
|
+
DESC
|
18
|
+
|
19
|
+
param :prefix,
|
20
|
+
desc: "Optional prefix to filter entries (e.g., 'notes/' to list all entries under notes/)",
|
21
|
+
required: false
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Create a ScratchpadList tool for a specific scratchpad storage instance
|
25
|
+
#
|
26
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
27
|
+
# @return [ScratchpadList] Tool instance
|
28
|
+
def create_for_scratchpad(scratchpad_storage)
|
29
|
+
new(scratchpad_storage)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Initialize with scratchpad storage instance
|
34
|
+
#
|
35
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
36
|
+
def initialize(scratchpad_storage)
|
37
|
+
super() # Call RubyLLM::Tool's initialize
|
38
|
+
@scratchpad_storage = scratchpad_storage
|
39
|
+
end
|
40
|
+
|
41
|
+
# Execute the tool
|
42
|
+
#
|
43
|
+
# @param prefix [String, nil] Optional prefix to filter entries
|
44
|
+
# @return [String] Formatted list of entries
|
45
|
+
def execute(prefix: nil)
|
46
|
+
entries = scratchpad_storage.list(prefix: prefix)
|
47
|
+
|
48
|
+
if entries.empty?
|
49
|
+
prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
|
50
|
+
return "No entries found in scratchpad#{prefix_msg}"
|
51
|
+
end
|
52
|
+
|
53
|
+
result = []
|
54
|
+
prefix_msg = prefix ? " with prefix '#{prefix}'" : ""
|
55
|
+
result << "Scratchpad entries#{prefix_msg} (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
|
56
|
+
result << ""
|
57
|
+
|
58
|
+
entries.each do |entry|
|
59
|
+
time_str = entry[:updated_at].strftime("%Y-%m-%d %H:%M:%S")
|
60
|
+
result << " scratchpad://#{entry[:path]}"
|
61
|
+
result << " Title: #{entry[:title]}"
|
62
|
+
result << " Size: #{format_bytes(entry[:size])}"
|
63
|
+
result << " Updated: #{time_str}"
|
64
|
+
result << ""
|
65
|
+
end
|
66
|
+
|
67
|
+
result.join("\n").rstrip
|
68
|
+
rescue ArgumentError => e
|
69
|
+
validation_error(e.message)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
attr_reader :scratchpad_storage
|
75
|
+
|
76
|
+
def validation_error(message)
|
77
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Format bytes to human-readable size
|
81
|
+
#
|
82
|
+
# @param bytes [Integer] Number of bytes
|
83
|
+
# @return [String] Formatted size
|
84
|
+
def format_bytes(bytes)
|
85
|
+
if bytes >= 1_000_000
|
86
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
87
|
+
elsif bytes >= 1_000
|
88
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
89
|
+
else
|
90
|
+
"#{bytes}B"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
module Scratchpad
|
6
|
+
# Tool for reading content from scratchpad storage
|
7
|
+
#
|
8
|
+
# Retrieves content stored by any agent using scratchpad_write.
|
9
|
+
# All agents in the swarm share the same scratchpad.
|
10
|
+
class ScratchpadRead < RubyLLM::Tool
|
11
|
+
define_method(:name) { "ScratchpadRead" }
|
12
|
+
|
13
|
+
description <<~DESC
|
14
|
+
Read content from scratchpad.
|
15
|
+
Use this to retrieve temporary notes, results, or messages stored by any agent.
|
16
|
+
Any agent can read any scratchpad content.
|
17
|
+
DESC
|
18
|
+
|
19
|
+
param :file_path,
|
20
|
+
desc: "Path to read from scratchpad (e.g., 'status', 'result', 'notes/agent_x')",
|
21
|
+
required: true
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# Create a ScratchpadRead tool for a specific scratchpad storage instance
|
25
|
+
#
|
26
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
27
|
+
# @return [ScratchpadRead] Tool instance
|
28
|
+
def create_for_scratchpad(scratchpad_storage)
|
29
|
+
new(scratchpad_storage)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Initialize with scratchpad storage instance
|
34
|
+
#
|
35
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
36
|
+
def initialize(scratchpad_storage)
|
37
|
+
super() # Call RubyLLM::Tool's initialize
|
38
|
+
@scratchpad_storage = scratchpad_storage
|
39
|
+
end
|
40
|
+
|
41
|
+
# Execute the tool
|
42
|
+
#
|
43
|
+
# @param file_path [String] Path to read from
|
44
|
+
# @return [String] Content at the path with line numbers, or error message
|
45
|
+
def execute(file_path:)
|
46
|
+
content = scratchpad_storage.read(file_path: file_path)
|
47
|
+
format_with_line_numbers(content)
|
48
|
+
rescue ArgumentError => e
|
49
|
+
validation_error(e.message)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_reader :scratchpad_storage
|
55
|
+
|
56
|
+
def validation_error(message)
|
57
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Format content with line numbers (same format as Read tool)
|
61
|
+
#
|
62
|
+
# @param content [String] Content to format
|
63
|
+
# @return [String] Content with line numbers
|
64
|
+
def format_with_line_numbers(content)
|
65
|
+
lines = content.lines
|
66
|
+
output_lines = lines.each_with_index.map do |line, idx|
|
67
|
+
line_number = idx + 1
|
68
|
+
display_line = line.chomp
|
69
|
+
"#{line_number.to_s.rjust(6)}→#{display_line}"
|
70
|
+
end
|
71
|
+
output_lines.join("\n")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Tools
|
5
|
+
module Scratchpad
|
6
|
+
# Tool for writing content to scratchpad storage
|
7
|
+
#
|
8
|
+
# Stores content in volatile, shared storage for temporary communication.
|
9
|
+
# All agents in the swarm share the same scratchpad.
|
10
|
+
# Data is lost when the process ends (not persisted).
|
11
|
+
class ScratchpadWrite < RubyLLM::Tool
|
12
|
+
define_method(:name) { "ScratchpadWrite" }
|
13
|
+
|
14
|
+
description <<~DESC
|
15
|
+
Store content in scratchpad for temporary cross-agent communication.
|
16
|
+
Use this for quick notes, intermediate results, or coordination messages.
|
17
|
+
Any agent can read this content. Data is lost when the swarm ends.
|
18
|
+
|
19
|
+
For persistent storage that survives across sessions, use MemoryWrite instead.
|
20
|
+
|
21
|
+
Choose a simple, descriptive path. Examples: 'status', 'result', 'notes/agent_x'
|
22
|
+
DESC
|
23
|
+
|
24
|
+
param :file_path,
|
25
|
+
desc: "Simple path for the content (e.g., 'status', 'result', 'notes/agent_x')",
|
26
|
+
required: true
|
27
|
+
|
28
|
+
param :content,
|
29
|
+
desc: "Content to store in scratchpad (max 1MB per entry)",
|
30
|
+
required: true
|
31
|
+
|
32
|
+
param :title,
|
33
|
+
desc: "Brief title describing the content",
|
34
|
+
required: true
|
35
|
+
|
36
|
+
class << self
|
37
|
+
# Create a ScratchpadWrite tool for a specific scratchpad storage instance
|
38
|
+
#
|
39
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
40
|
+
# @return [ScratchpadWrite] Tool instance
|
41
|
+
def create_for_scratchpad(scratchpad_storage)
|
42
|
+
new(scratchpad_storage)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Initialize with scratchpad storage instance
|
47
|
+
#
|
48
|
+
# @param scratchpad_storage [Stores::ScratchpadStorage] Shared scratchpad storage instance
|
49
|
+
def initialize(scratchpad_storage)
|
50
|
+
super() # Call RubyLLM::Tool's initialize
|
51
|
+
@scratchpad_storage = scratchpad_storage
|
52
|
+
end
|
53
|
+
|
54
|
+
# Execute the tool
|
55
|
+
#
|
56
|
+
# @param file_path [String] Path to store content
|
57
|
+
# @param content [String] Content to store
|
58
|
+
# @param title [String] Brief title
|
59
|
+
# @return [String] Success message with path and size
|
60
|
+
def execute(file_path:, content:, title:)
|
61
|
+
entry = scratchpad_storage.write(file_path: file_path, content: content, title: title)
|
62
|
+
"Stored at scratchpad://#{file_path} (#{format_bytes(entry.size)})"
|
63
|
+
rescue ArgumentError => e
|
64
|
+
validation_error(e.message)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
attr_reader :scratchpad_storage
|
70
|
+
|
71
|
+
def validation_error(message)
|
72
|
+
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
73
|
+
end
|
74
|
+
|
75
|
+
# Format bytes to human-readable size
|
76
|
+
#
|
77
|
+
# @param bytes [Integer] Number of bytes
|
78
|
+
# @return [String] Formatted size
|
79
|
+
def format_bytes(bytes)
|
80
|
+
if bytes >= 1_000_000
|
81
|
+
"#{(bytes.to_f / 1_000_000).round(1)}MB"
|
82
|
+
elsif bytes >= 1_000
|
83
|
+
"#{(bytes.to_f / 1_000).round(1)}KB"
|
84
|
+
else
|
85
|
+
"#{bytes}B"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|