swarm_sdk 2.0.6 → 2.0.7

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 (32) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  3. data/lib/swarm_sdk/agent/builder.rb +16 -42
  4. data/lib/swarm_sdk/agent/chat/context_tracker.rb +43 -0
  5. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +41 -3
  6. data/lib/swarm_sdk/agent/chat.rb +426 -61
  7. data/lib/swarm_sdk/agent/context.rb +5 -1
  8. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  9. data/lib/swarm_sdk/agent/definition.rb +57 -24
  10. data/lib/swarm_sdk/plugin.rb +147 -0
  11. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  12. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +7 -1
  13. data/lib/swarm_sdk/swarm/agent_initializer.rb +80 -12
  14. data/lib/swarm_sdk/swarm/tool_configurator.rb +116 -44
  15. data/lib/swarm_sdk/swarm.rb +44 -8
  16. data/lib/swarm_sdk/tools/clock.rb +44 -0
  17. data/lib/swarm_sdk/tools/grep.rb +16 -19
  18. data/lib/swarm_sdk/tools/registry.rb +23 -12
  19. data/lib/swarm_sdk/tools/todo_write.rb +1 -1
  20. data/lib/swarm_sdk/version.rb +1 -1
  21. data/lib/swarm_sdk.rb +4 -0
  22. metadata +7 -12
  23. data/lib/swarm_sdk/prompts/memory.md.erb +0 -480
  24. data/lib/swarm_sdk/tools/memory/memory_delete.rb +0 -64
  25. data/lib/swarm_sdk/tools/memory/memory_edit.rb +0 -145
  26. data/lib/swarm_sdk/tools/memory/memory_glob.rb +0 -94
  27. data/lib/swarm_sdk/tools/memory/memory_grep.rb +0 -147
  28. data/lib/swarm_sdk/tools/memory/memory_multi_edit.rb +0 -228
  29. data/lib/swarm_sdk/tools/memory/memory_read.rb +0 -82
  30. data/lib/swarm_sdk/tools/memory/memory_write.rb +0 -90
  31. data/lib/swarm_sdk/tools/stores/memory_storage.rb +0 -300
  32. data/lib/swarm_sdk/tools/stores/storage_read_tracker.rb +0 -61
@@ -1,145 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module Memory
6
- # Tool for editing memory entries with exact string replacement
7
- #
8
- # Performs exact string replacements in memory content.
9
- # Each agent has its own isolated memory storage.
10
- class MemoryEdit < RubyLLM::Tool
11
- define_method(:name) { "MemoryEdit" }
12
-
13
- description <<~DESC
14
- Performs exact string replacements in memory entries.
15
- Works like the Edit tool but operates on memory content instead of files.
16
- You must use MemoryRead on the entry before editing it.
17
- When editing text from MemoryRead output, ensure you preserve the exact indentation as it appears AFTER the line number prefix.
18
- The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual content to match.
19
- Never include any part of the line number prefix in the old_string or new_string.
20
- The 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.
21
- Use replace_all for replacing and renaming strings across the entry.
22
- DESC
23
-
24
- param :file_path,
25
- desc: "Path to the memory entry (e.g., 'analysis/report', 'parallel/batch1/task_0')",
26
- required: true
27
-
28
- param :old_string,
29
- desc: "The exact text to replace (must match exactly including whitespace)",
30
- required: true
31
-
32
- param :new_string,
33
- desc: "The text to replace it with (must be different from old_string)",
34
- required: true
35
-
36
- param :replace_all,
37
- desc: "Replace all occurrences of old_string (default false)",
38
- required: false
39
-
40
- class << self
41
- # Create a MemoryEdit tool for a specific memory storage instance
42
- #
43
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
44
- # @param agent_name [Symbol, String] Agent identifier for tracking reads
45
- # @return [MemoryEdit] Tool instance
46
- def create_for_memory(memory_storage, agent_name)
47
- new(memory_storage, agent_name)
48
- end
49
- end
50
-
51
- # Initialize with memory storage instance and agent name
52
- #
53
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
54
- # @param agent_name [Symbol, String] Agent identifier
55
- def initialize(memory_storage, agent_name)
56
- super() # Call RubyLLM::Tool's initialize
57
- @memory_storage = memory_storage
58
- @agent_name = agent_name.to_sym
59
- end
60
-
61
- # Execute the tool
62
- #
63
- # @param file_path [String] Path to memory entry
64
- # @param old_string [String] Text to replace
65
- # @param new_string [String] Replacement text
66
- # @param replace_all [Boolean] Replace all occurrences
67
- # @return [String] Success message or error
68
- def execute(file_path:, old_string:, new_string:, replace_all: false)
69
- # Validate inputs
70
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
71
- return validation_error("old_string is required") if old_string.nil? || old_string.empty?
72
- return validation_error("new_string is required") if new_string.nil?
73
-
74
- # old_string and new_string must be different
75
- if old_string == new_string
76
- return validation_error("old_string and new_string must be different. They are currently identical.")
77
- end
78
-
79
- # Read current content (this will raise ArgumentError if entry doesn't exist)
80
- content = memory_storage.read(file_path: file_path)
81
-
82
- # Enforce read-before-edit
83
- unless Stores::StorageReadTracker.entry_read?(@agent_name, file_path)
84
- return validation_error(
85
- "Cannot edit memory entry without reading it first. " \
86
- "You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
87
- "This ensures you have the current content to match against.",
88
- )
89
- end
90
-
91
- # Check if old_string exists in content
92
- unless content.include?(old_string)
93
- return validation_error(<<~ERROR.chomp)
94
- old_string not found in memory entry. Make sure it matches exactly, including all whitespace and indentation.
95
- Do not include line number prefixes from MemoryRead tool output.
96
- ERROR
97
- end
98
-
99
- # Count occurrences
100
- occurrences = content.scan(old_string).count
101
-
102
- # If not replace_all and multiple occurrences, error
103
- if !replace_all && occurrences > 1
104
- return validation_error(<<~ERROR.chomp)
105
- Found #{occurrences} occurrences of old_string.
106
- Either provide more surrounding context to make the match unique, or use replace_all: true to replace all occurrences.
107
- ERROR
108
- end
109
-
110
- # Perform replacement
111
- new_content = if replace_all
112
- content.gsub(old_string, new_string)
113
- else
114
- content.sub(old_string, new_string)
115
- end
116
-
117
- # Get existing entry metadata
118
- entries = memory_storage.list
119
- existing_entry = entries.find { |e| e[:path] == file_path }
120
-
121
- # Write updated content back (preserving the title)
122
- memory_storage.write(
123
- file_path: file_path,
124
- content: new_content,
125
- title: existing_entry[:title],
126
- )
127
-
128
- # Build success message
129
- replaced_count = replace_all ? occurrences : 1
130
- "Successfully replaced #{replaced_count} occurrence(s) in memory://#{file_path}"
131
- rescue ArgumentError => e
132
- validation_error(e.message)
133
- end
134
-
135
- private
136
-
137
- attr_reader :memory_storage
138
-
139
- def validation_error(message)
140
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
141
- end
142
- end
143
- end
144
- end
145
- end
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module Memory
6
- # Tool for searching memory entries by glob pattern
7
- #
8
- # Finds memory entries matching a glob pattern (like filesystem glob).
9
- # Each agent has its own isolated memory storage.
10
- class MemoryGlob < RubyLLM::Tool
11
- define_method(:name) { "MemoryGlob" }
12
-
13
- description <<~DESC
14
- Search your memory entries by glob pattern.
15
- Works like filesystem glob - use * for wildcards, ** for recursive matching.
16
- Use this to discover entries matching specific patterns.
17
-
18
- Examples:
19
- - "parallel/*" - all entries directly under parallel/
20
- - "parallel/**" - all entries under parallel/ (recursive)
21
- - "*/report" - all entries named "report" in any top-level directory
22
- - "analysis/*/result_*" - entries like "analysis/foo/result_1"
23
- DESC
24
-
25
- param :pattern,
26
- desc: "Glob pattern to match (e.g., '**/*.txt', 'parallel/*/task_*')",
27
- required: true
28
-
29
- class << self
30
- # Create a MemoryGlob tool for a specific memory storage instance
31
- #
32
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
33
- # @return [MemoryGlob] Tool instance
34
- def create_for_memory(memory_storage)
35
- new(memory_storage)
36
- end
37
- end
38
-
39
- # Initialize with memory storage instance
40
- #
41
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
42
- def initialize(memory_storage)
43
- super() # Call RubyLLM::Tool's initialize
44
- @memory_storage = memory_storage
45
- end
46
-
47
- # Execute the tool
48
- #
49
- # @param pattern [String] Glob pattern to match
50
- # @return [String] Formatted list of matching entries
51
- def execute(pattern:)
52
- entries = memory_storage.glob(pattern: pattern)
53
-
54
- if entries.empty?
55
- return "No entries found matching pattern '#{pattern}'"
56
- end
57
-
58
- result = []
59
- result << "Memory entries matching '#{pattern}' (#{entries.size} #{entries.size == 1 ? "entry" : "entries"}):"
60
-
61
- entries.each do |entry|
62
- result << " memory://#{entry[:path]} - \"#{entry[:title]}\" (#{format_bytes(entry[:size])})"
63
- end
64
-
65
- result.join("\n")
66
- rescue ArgumentError => e
67
- validation_error(e.message)
68
- end
69
-
70
- private
71
-
72
- attr_reader :memory_storage
73
-
74
- def validation_error(message)
75
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
76
- end
77
-
78
- # Format bytes to human-readable size
79
- #
80
- # @param bytes [Integer] Number of bytes
81
- # @return [String] Formatted size
82
- def format_bytes(bytes)
83
- if bytes >= 1_000_000
84
- "#{(bytes.to_f / 1_000_000).round(1)}MB"
85
- elsif bytes >= 1_000
86
- "#{(bytes.to_f / 1_000).round(1)}KB"
87
- else
88
- "#{bytes}B"
89
- end
90
- end
91
- end
92
- end
93
- end
94
- end
@@ -1,147 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Tools
5
- module Memory
6
- # Tool for searching memory content by pattern
7
- #
8
- # Searches content stored in memory entries using regex patterns.
9
- # Each agent has its own isolated memory storage.
10
- class MemoryGrep < RubyLLM::Tool
11
- define_method(:name) { "MemoryGrep" }
12
-
13
- description <<~DESC
14
- Search your memory content by pattern (like grep).
15
- Use regex patterns to search content within memory entries.
16
- Returns matching entries and optionally line numbers and content.
17
-
18
- Output modes:
19
- - files_with_matches: Only list paths containing matches (default)
20
- - content: Show matching lines with line numbers
21
- - count: Show number of matches per file
22
- DESC
23
-
24
- param :pattern,
25
- desc: "Regular expression pattern to search for",
26
- required: true
27
-
28
- param :case_insensitive,
29
- desc: "Perform case-insensitive search (default: false)",
30
- required: false
31
-
32
- param :output_mode,
33
- desc: "Output mode: 'files_with_matches' (default), 'content', or 'count'",
34
- required: false
35
-
36
- class << self
37
- # Create a MemoryGrep tool for a specific memory storage instance
38
- #
39
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
40
- # @return [MemoryGrep] Tool instance
41
- def create_for_memory(memory_storage)
42
- new(memory_storage)
43
- end
44
- end
45
-
46
- # Initialize with memory storage instance
47
- #
48
- # @param memory_storage [Stores::MemoryStorage] Per-agent memory storage instance
49
- def initialize(memory_storage)
50
- super() # Call RubyLLM::Tool's initialize
51
- @memory_storage = memory_storage
52
- end
53
-
54
- # Execute the tool
55
- #
56
- # @param pattern [String] Regex pattern to search for
57
- # @param case_insensitive [Boolean] Whether to perform case-insensitive search
58
- # @param output_mode [String] Output mode
59
- # @return [String] Formatted search results
60
- def execute(pattern:, case_insensitive: false, output_mode: "files_with_matches")
61
- results = memory_storage.grep(
62
- pattern: pattern,
63
- case_insensitive: case_insensitive,
64
- output_mode: output_mode,
65
- )
66
-
67
- format_results(results, pattern, output_mode)
68
- rescue ArgumentError => e
69
- validation_error(e.message)
70
- rescue RegexpError => e
71
- validation_error("Invalid regex pattern: #{e.message}")
72
- end
73
-
74
- private
75
-
76
- attr_reader :memory_storage
77
-
78
- def validation_error(message)
79
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
80
- end
81
-
82
- def format_results(results, pattern, output_mode)
83
- case output_mode
84
- when "files_with_matches"
85
- format_files_with_matches(results, pattern)
86
- when "content"
87
- format_content(results, pattern)
88
- when "count"
89
- format_count(results, pattern)
90
- else
91
- validation_error("Invalid output_mode: #{output_mode}")
92
- end
93
- end
94
-
95
- def format_files_with_matches(paths, pattern)
96
- if paths.empty?
97
- return "No matches found for pattern '#{pattern}'"
98
- end
99
-
100
- result = []
101
- result << "Memory entries matching '#{pattern}' (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
102
- paths.each do |path|
103
- result << " memory://#{path}"
104
- end
105
- result.join("\n")
106
- end
107
-
108
- def format_content(results, pattern)
109
- if results.empty?
110
- return "No matches found for pattern '#{pattern}'"
111
- end
112
-
113
- total_matches = results.sum { |r| r[:matches].size }
114
- output = []
115
- output << "Memory entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
116
- output << ""
117
-
118
- results.each do |result|
119
- output << "memory://#{result[:path]}:"
120
- result[:matches].each do |match|
121
- output << " #{match[:line_number]}: #{match[:content]}"
122
- end
123
- output << ""
124
- end
125
-
126
- output.join("\n").rstrip
127
- end
128
-
129
- def format_count(results, pattern)
130
- if results.empty?
131
- return "No matches found for pattern '#{pattern}'"
132
- end
133
-
134
- total_matches = results.sum { |r| r[:count] }
135
- output = []
136
- output << "Memory entries matching '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
137
-
138
- results.each do |result|
139
- output << " memory://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
140
- end
141
-
142
- output.join("\n")
143
- end
144
- end
145
- end
146
- end
147
- end
@@ -1,228 +0,0 @@
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
@@ -1,82 +0,0 @@
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