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