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,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