swarm_memory 2.1.7 → 2.2.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd1cc28cb4976bd82c75c38ae17d92b15b6e9b91d4ef92ae7269759af0a90084
4
- data.tar.gz: dd78b137e33ecb3132f864cefb077ecf4add04196421f6206bad4a428bba42d3
3
+ metadata.gz: 1ec97e62d89963ca0ba1c40d2d4bed76f3cd7ce12c5971594cf93a877228d894
4
+ data.tar.gz: a6cd54d0e51d4743968758bb3363aad5cdae6e7e21ff25dd688c8f5c36ecfb24
5
5
  SHA512:
6
- metadata.gz: 25e9b5ead6b852bf5338b7c342043d08a9b3c38e3eff6f3fd2043c14f5caea264764f3fbd2ce4bb3398db286b886eea2d53d4b3ef7e789c26f8d8c4177b37722
7
- data.tar.gz: 20c98443c3e0e7f4b2d662d80d03a0ecb0dbcf29d726a12c75a4d506c87c88f2a97e51420033b8d62e5b82573228dfeb4ca2788a30f813e4fe58b26db68ad437
6
+ metadata.gz: d96db1ab430ec011d572e2472866a121a470ebe3b6afca676d78f1c87d64ba3bdbd4857731a7c25feb597e350dec82749464ab98fca738b93bf799c35ada8776
7
+ data.tar.gz: 1a8ae4efa93e6259d24962e59453f10ca003913cd2a555086848c586d948896965888ac5c8d8c2dde44961e2d8e33fb4b2a53b908667e29be9e8cc6f82d1a243
@@ -22,6 +22,9 @@ module SwarmMemory
22
22
  # Track memory mode for each agent: { agent_name => mode }
23
23
  # Modes: :assistant (default), :retrieval, :researcher
24
24
  @modes = {}
25
+ # Track threshold configuration for each agent: { agent_name => config }
26
+ # Enables per-adapter threshold tuning with ENV fallback
27
+ @threshold_configs = {}
25
28
  end
26
29
 
27
30
  # Plugin identifier
@@ -49,7 +52,6 @@ module SwarmMemory
49
52
  :MemoryGrep,
50
53
  :MemoryWrite,
51
54
  :MemoryEdit,
52
- :MemoryMultiEdit,
53
55
  :MemoryDelete,
54
56
  :MemoryDefrag,
55
57
  ]
@@ -75,7 +77,6 @@ module SwarmMemory
75
77
  :MemoryGrep,
76
78
  :MemoryWrite,
77
79
  :MemoryEdit,
78
- :MemoryMultiEdit,
79
80
  :MemoryDelete,
80
81
  :MemoryDefrag,
81
82
  ]
@@ -199,24 +200,36 @@ module SwarmMemory
199
200
  end
200
201
  end
201
202
 
202
- # Check if storage should be created for this agent
203
+ # Check if memory is configured for this agent
204
+ #
205
+ # Delegates adapter-specific validation to the adapter itself.
206
+ # Filesystem adapter requires 'directory', custom adapters may use other keys.
203
207
  #
204
208
  # @param agent_definition [Agent::Definition] Agent definition
205
- # @return [Boolean] True if agent has memory configuration
206
- def storage_enabled?(agent_definition)
209
+ # @return [Boolean] True if agent has valid memory configuration
210
+ def memory_configured?(agent_definition)
207
211
  memory_config = agent_definition.plugin_config(:memory)
208
212
  return false if memory_config.nil?
209
213
 
210
- # MemoryConfig object (from DSL)
214
+ # MemoryConfig object (from DSL) - delegates to its enabled? method
211
215
  return memory_config.enabled? if memory_config.respond_to?(:enabled?)
212
216
 
213
- # Hash (from YAML) - check for directory key
214
- if memory_config.is_a?(Hash)
217
+ # Hash (from YAML)
218
+ return false unless memory_config.is_a?(Hash)
219
+ return false if memory_config.empty?
220
+
221
+ adapter = (memory_config[:adapter] || memory_config["adapter"] || :filesystem).to_sym
222
+
223
+ case adapter
224
+ when :filesystem
225
+ # Filesystem adapter requires directory
215
226
  directory = memory_config[:directory] || memory_config["directory"]
216
- return !directory.nil? && !directory.to_s.strip.empty?
227
+ !directory.nil? && !directory.to_s.strip.empty?
228
+ else
229
+ # Custom adapters: presence of config is sufficient
230
+ # Adapter will validate its own requirements during initialization
231
+ true
217
232
  end
218
-
219
- false
220
233
  end
221
234
 
222
235
  # Contribute to agent serialization
@@ -293,9 +306,18 @@ module SwarmMemory
293
306
 
294
307
  builder.instance_eval do
295
308
  memory do
309
+ # Standard options
296
310
  directory(memory_config[:directory]) if memory_config[:directory]
297
311
  adapter(memory_config[:adapter]) if memory_config[:adapter]
298
312
  mode(memory_config[:mode]) if memory_config[:mode]
313
+
314
+ # Pass through all custom adapter options
315
+ # Handle both symbol and string keys (YAML may have either)
316
+ standard_keys = [:directory, :adapter, :mode, "directory", "adapter", "mode"]
317
+ custom_keys = memory_config.keys - standard_keys
318
+ custom_keys.each do |key|
319
+ option(key.to_sym, memory_config[key]) # Normalize to symbol
320
+ end
299
321
  end
300
322
  end
301
323
  end
@@ -336,6 +358,7 @@ module SwarmMemory
336
358
  # Store storage and mode using BASE NAME
337
359
  @storages[base_name] = storage # ← Changed from agent_name to base_name
338
360
  @modes[base_name] = mode # ← Changed from agent_name to base_name
361
+ @threshold_configs[base_name] = extract_threshold_config(memory_config)
339
362
 
340
363
  # Get mode-specific tools
341
364
  allowed_tools = tools_for_mode(mode)
@@ -384,20 +407,27 @@ module SwarmMemory
384
407
  # V7.0: Extract base name for storage lookup (delegation instances share storage)
385
408
  base_name = agent_name.to_s.split("@").first.to_sym
386
409
  storage = @storages[base_name] # ← Changed from agent_name to base_name
410
+ config = @threshold_configs[base_name] || {}
387
411
 
388
412
  return [] unless storage&.semantic_index
389
413
  return [] if prompt.nil? || prompt.empty?
390
414
 
391
415
  # Adaptive threshold based on query length
392
416
  # Short queries use lower threshold as they have less semantic richness
393
- # Optimal: cutoff=10 words, short=0.25, normal=0.35 (discovered via systematic evaluation)
417
+ # Fallback chain: config ENV default
394
418
  word_count = prompt.split.size
395
- word_cutoff = (ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"] || "10").to_i
419
+ word_cutoff = config[:adaptive_word_cutoff] ||
420
+ ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"]&.to_i ||
421
+ 10
396
422
 
397
423
  threshold = if word_count < word_cutoff
398
- (ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"] || "0.25").to_f
424
+ config[:discovery_threshold_short] ||
425
+ ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"]&.to_f ||
426
+ 0.25
399
427
  else
400
- (ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"] || "0.35").to_f
428
+ config[:discovery_threshold] ||
429
+ ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"]&.to_f ||
430
+ 0.35
401
431
  end
402
432
  reminders = []
403
433
 
@@ -482,9 +512,15 @@ module SwarmMemory
482
512
  }
483
513
  end
484
514
 
485
- # Get actual weights being used (from ENV or defaults)
486
- semantic_weight = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
487
- keyword_weight = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
515
+ # Get actual weights being used (fallback chain: config → ENV defaults)
516
+ base_name = agent_name.to_s.split("@").first.to_sym
517
+ config = @threshold_configs[base_name] || {}
518
+ semantic_weight = config[:semantic_weight] ||
519
+ ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"]&.to_f ||
520
+ 0.5
521
+ keyword_weight = config[:keyword_weight] ||
522
+ ENV["SWARM_MEMORY_KEYWORD_WEIGHT"]&.to_f ||
523
+ 0.5
488
524
 
489
525
  SwarmSDK::LogStream.emit(
490
526
  type: "semantic_skill_search",
@@ -545,9 +581,15 @@ module SwarmMemory
545
581
  }
546
582
  end
547
583
 
548
- # Get actual weights being used (from ENV or defaults)
549
- semantic_weight = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
550
- keyword_weight = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
584
+ # Get actual weights being used (fallback chain: config → ENV defaults)
585
+ base_name = agent_name.to_s.split("@").first.to_sym
586
+ config = @threshold_configs[base_name] || {}
587
+ semantic_weight = config[:semantic_weight] ||
588
+ ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"]&.to_f ||
589
+ 0.5
590
+ keyword_weight = config[:keyword_weight] ||
591
+ ENV["SWARM_MEMORY_KEYWORD_WEIGHT"]&.to_f ||
592
+ 0.5
551
593
 
552
594
  SwarmSDK::LogStream.emit(
553
595
  type: "semantic_memory_search",
@@ -626,6 +668,40 @@ module SwarmMemory
626
668
 
627
669
  reminder
628
670
  end
671
+
672
+ # Extract threshold configuration from memory config
673
+ #
674
+ # Supports both MemoryConfig objects (from DSL) and Hash configs (from YAML).
675
+ # Extracts semantic search thresholds and hybrid search weights.
676
+ #
677
+ # @param memory_config [MemoryConfig, Hash, nil] Memory configuration
678
+ # @return [Hash] Threshold config with symbol keys
679
+ def extract_threshold_config(memory_config)
680
+ return {} unless memory_config
681
+
682
+ threshold_keys = [
683
+ :discovery_threshold,
684
+ :discovery_threshold_short,
685
+ :adaptive_word_cutoff,
686
+ :semantic_weight,
687
+ :keyword_weight,
688
+ ]
689
+
690
+ if memory_config.respond_to?(:adapter_options)
691
+ # MemoryConfig object (from DSL)
692
+ memory_config.adapter_options.slice(*threshold_keys)
693
+ elsif memory_config.is_a?(Hash)
694
+ # Hash (from YAML) - handle both symbol and string keys
695
+ result = {}
696
+ threshold_keys.each do |key|
697
+ value = memory_config[key] || memory_config[key.to_s]
698
+ result[key] = value if value
699
+ end
700
+ result
701
+ else
702
+ {}
703
+ end
704
+ end
629
705
  end
630
706
  end
631
707
  end
@@ -191,7 +191,7 @@ module SwarmMemory
191
191
  " Semantic similarity: N/A (no embeddings)"
192
192
  end
193
193
  report << ""
194
- report << " **Suggestion:** Review both entries and consider merging with MemoryMultiEdit"
194
+ report << " **Suggestion:** Review both entries and consider merging with MemoryEdit"
195
195
  report << ""
196
196
  end
197
197
 
@@ -28,7 +28,6 @@ You have persistent memory that learns from conversations and helps you answer q
28
28
  - `MemoryGlob` - Browse memory by path pattern
29
29
  - `MemoryWrite` - Create new memory
30
30
  - `MemoryEdit` - Update existing memory
31
- - `MemoryMultiEdit` - Update multiple memories at once
32
31
  - `MemoryDelete` - Delete a memory
33
32
  - `MemoryDefrag` - Optimize memory storage
34
33
  - `LoadSkill` - Load a skill and swap tools
@@ -148,7 +148,6 @@ module SwarmMemory
148
148
  "MemoryWrite",
149
149
  "MemoryRead",
150
150
  "MemoryEdit",
151
- "MemoryMultiEdit",
152
151
  "MemoryDelete",
153
152
  "MemoryGlob",
154
153
  "MemoryGrep",
@@ -162,11 +162,12 @@ module SwarmMemory
162
162
  # Get existing entry metadata
163
163
  entry = @storage.read_entry(file_path: file_path)
164
164
 
165
- # Write updated content back (preserving the title)
165
+ # Write updated content back (preserving the title and metadata)
166
166
  @storage.write(
167
167
  file_path: file_path,
168
168
  content: new_content,
169
169
  title: entry.title,
170
+ metadata: entry.metadata,
170
171
  )
171
172
 
172
173
  # Build success message
@@ -35,7 +35,7 @@ module SwarmMemory
35
35
  - MemoryRead(file_path: "skill/debugging/api-errors.md") - Read a skill before loading it
36
36
 
37
37
  **Important:**
38
- - Always read entries before editing them with MemoryEdit or MemoryMultiEdit
38
+ - Always read entries before editing them with MemoryEdit
39
39
  - Line numbers in output are for reference only - don't include them when editing
40
40
  - Each read is tracked to enforce read-before-edit patterns
41
41
  DESC
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.1.7"
4
+ VERSION = "2.2.1"
5
5
  end
data/lib/swarm_memory.rb CHANGED
@@ -123,8 +123,6 @@ module SwarmMemory
123
123
  Tools::MemoryRead.new(storage: storage, agent_name: agent_name)
124
124
  when :MemoryEdit
125
125
  Tools::MemoryEdit.new(storage: storage, agent_name: agent_name)
126
- when :MemoryMultiEdit
127
- Tools::MemoryMultiEdit.new(storage: storage, agent_name: agent_name)
128
126
  when :MemoryDelete
129
127
  Tools::MemoryDelete.new(storage: storage)
130
128
  when :MemoryGlob
@@ -158,7 +156,6 @@ module SwarmMemory
158
156
  Tools::MemoryWrite.new(storage: storage, agent_name: agent_name),
159
157
  Tools::MemoryRead.new(storage: storage, agent_name: agent_name),
160
158
  Tools::MemoryEdit.new(storage: storage, agent_name: agent_name),
161
- Tools::MemoryMultiEdit.new(storage: storage, agent_name: agent_name),
162
159
  Tools::MemoryDelete.new(storage: storage),
163
160
  Tools::MemoryGlob.new(storage: storage),
164
161
  Tools::MemoryGrep.new(storage: storage),
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.7
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -71,14 +71,14 @@ dependencies:
71
71
  requirements:
72
72
  - - "~>"
73
73
  - !ruby/object:Gem::Version
74
- version: 2.4.1
74
+ version: 2.5.0
75
75
  type: :runtime
76
76
  prerelease: false
77
77
  version_requirements: !ruby/object:Gem::Requirement
78
78
  requirements:
79
79
  - - "~>"
80
80
  - !ruby/object:Gem::Version
81
- version: 2.4.1
81
+ version: 2.5.0
82
82
  - !ruby/object:Gem::Dependency
83
83
  name: zeitwerk
84
84
  requirement: !ruby/object:Gem::Requirement
@@ -138,7 +138,6 @@ files:
138
138
  - lib/swarm_memory/tools/memory_edit.rb
139
139
  - lib/swarm_memory/tools/memory_glob.rb
140
140
  - lib/swarm_memory/tools/memory_grep.rb
141
- - lib/swarm_memory/tools/memory_multi_edit.rb
142
141
  - lib/swarm_memory/tools/memory_read.rb
143
142
  - lib/swarm_memory/tools/memory_write.rb
144
143
  - lib/swarm_memory/utils.rb
@@ -1,281 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmMemory
4
- module Tools
5
- # Tool for performing multiple edits to a memory entry
6
- #
7
- # Applies multiple edit operations sequentially to a single memory entry.
8
- # Each edit sees the result of all previous edits, allowing for
9
- # coordinated multi-step transformations.
10
- # Each agent has its own isolated memory storage.
11
- class MemoryMultiEdit < RubyLLM::Tool
12
- description <<~DESC
13
- Perform multiple exact string replacements in a single memory entry (applies edits sequentially).
14
-
15
- REQUIRED: Provide BOTH parameters - file_path and edits_json.
16
-
17
- **Required Parameters:**
18
- - file_path (REQUIRED): Path to memory entry - MUST start with concept/, fact/, skill/, or experience/
19
- - edits_json (REQUIRED): JSON array of edit operations - each must have old_string, new_string, and optionally replace_all
20
-
21
- **MEMORY STRUCTURE (4 Fixed Categories Only):**
22
- - concept/{domain}/** - Abstract ideas
23
- - fact/{subfolder}/** - Concrete information
24
- - skill/{domain}/** - Procedures
25
- - experience/** - Lessons
26
- INVALID: documentation/, reference/, project/, code/, parallel/
27
-
28
- **JSON Format:**
29
- ```json
30
- [
31
- {"old_string": "text to find", "new_string": "replacement text", "replace_all": false},
32
- {"old_string": "another find", "new_string": "another replace", "replace_all": true}
33
- ]
34
- ```
35
-
36
- **CRITICAL - Before Using This Tool:**
37
- 1. You MUST use MemoryRead on the entry first - edits without reading will FAIL
38
- 2. Copy text exactly from MemoryRead output, EXCLUDING the line number prefix
39
- 3. Line number format: " 123→actual content" - only use text AFTER the arrow
40
- 4. Edits are applied SEQUENTIALLY - later edits see results of earlier edits
41
- 5. If ANY edit fails, NO changes are saved (all-or-nothing)
42
-
43
- **How Sequential Edits Work:**
44
- ```
45
- Original: "status: pending, priority: low"
46
-
47
- Edit 1: "pending" → "in-progress"
48
- Result: "status: in-progress, priority: low"
49
-
50
- Edit 2: "low" → "high" (sees Edit 1's result)
51
- Final: "status: in-progress, priority: high"
52
- ```
53
-
54
- **Use Cases:**
55
- - Making multiple coordinated changes in one operation
56
- - Updating several related fields at once
57
- - Chaining transformations where order matters
58
- - Bulk find-and-replace operations
59
-
60
- **Examples:**
61
- ```
62
- # Update multiple fields in an experience
63
- MemoryMultiEdit(
64
- file_path: "experience/api-debugging.md",
65
- edits_json: '[
66
- {"old_string": "status: in-progress", "new_string": "status: resolved"},
67
- {"old_string": "confidence: medium", "new_string": "confidence: high"}
68
- ]'
69
- )
70
-
71
- # Rename function and update calls in a concept
72
- MemoryMultiEdit(
73
- file_path: "concept/ruby/functions.md",
74
- edits_json: '[
75
- {"old_string": "def old_func_name", "new_string": "def new_func_name"},
76
- {"old_string": "old_func_name()", "new_string": "new_func_name()", "replace_all": true}
77
- ]'
78
- )
79
- ```
80
-
81
- **Important Notes:**
82
- - All edits in the array must be valid JSON objects
83
- - Each old_string must be different from its new_string
84
- - Each old_string must be unique in content UNLESS replace_all is true
85
- - Failed edit shows which previous edits succeeded
86
- - More efficient than multiple MemoryEdit calls
87
- DESC
88
-
89
- param :file_path,
90
- desc: "Path to memory entry - MUST start with concept/, fact/, skill/, or experience/ (e.g., 'experience/api-debugging.md', 'concept/ruby/functions.md')",
91
- required: true
92
-
93
- param :edits_json,
94
- type: "string",
95
- desc: <<~DESC.chomp,
96
- JSON array of edit operations. Each edit must have:
97
- old_string (exact text to replace),
98
- new_string (replacement text),
99
- and optionally replace_all (boolean, default false).
100
- Example: [{"old_string":"foo","new_string":"bar","replace_all":false}]
101
- DESC
102
- required: true
103
-
104
- # Initialize with storage instance and agent name
105
- #
106
- # @param storage [Core::Storage] Storage instance
107
- # @param agent_name [String, Symbol] Agent identifier
108
- def initialize(storage:, agent_name:)
109
- super()
110
- @storage = storage
111
- @agent_name = agent_name.to_sym
112
- end
113
-
114
- # Override name to return simple "MemoryMultiEdit"
115
- def name
116
- "MemoryMultiEdit"
117
- end
118
-
119
- # Execute the tool
120
- #
121
- # @param file_path [String] Path to memory entry
122
- # @param edits_json [String] JSON array of edit operations
123
- # @return [String] Success message or error
124
- def execute(file_path:, edits_json:)
125
- # Validate inputs
126
- return validation_error("file_path is required") if file_path.nil? || file_path.to_s.strip.empty?
127
-
128
- # Parse JSON
129
- edits = begin
130
- JSON.parse(edits_json)
131
- rescue JSON::ParserError
132
- nil
133
- end
134
-
135
- return validation_error("Invalid JSON format. Please provide a valid JSON array of edit operations.") if edits.nil?
136
-
137
- return validation_error("edits must be an array") unless edits.is_a?(Array)
138
- return validation_error("edits array cannot be empty") if edits.empty?
139
-
140
- # Read current content (this will raise ArgumentError if entry doesn't exist)
141
- content = @storage.read(file_path: file_path)
142
-
143
- # Enforce read-before-edit with content verification
144
- unless Core::StorageReadTracker.entry_read?(@agent_name, file_path, @storage)
145
- return validation_error(
146
- "Cannot edit memory entry without reading it first. " \
147
- "You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
148
- "This ensures you have the current content to match against.",
149
- )
150
- end
151
-
152
- # Validate edit operations
153
- validated_edits = []
154
- edits.each_with_index do |edit, index|
155
- unless edit.is_a?(Hash)
156
- return validation_error("Edit at index #{index} must be a hash/object with old_string and new_string")
157
- end
158
-
159
- # Convert string keys to symbols for consistency
160
- edit = edit.transform_keys(&:to_sym)
161
-
162
- unless edit[:old_string]
163
- return validation_error("Edit at index #{index} missing required field 'old_string'")
164
- end
165
-
166
- unless edit[:new_string]
167
- return validation_error("Edit at index #{index} missing required field 'new_string'")
168
- end
169
-
170
- # old_string and new_string must be different
171
- if edit[:old_string] == edit[:new_string]
172
- return validation_error("Edit at index #{index}: old_string and new_string must be different")
173
- end
174
-
175
- validated_edits << {
176
- old_string: edit[:old_string].to_s,
177
- new_string: edit[:new_string].to_s,
178
- replace_all: edit[:replace_all] == true,
179
- index: index,
180
- }
181
- end
182
-
183
- # Apply edits sequentially
184
- results = []
185
- current_content = content
186
-
187
- validated_edits.each do |edit|
188
- # Check if old_string exists in current content
189
- unless current_content.include?(edit[:old_string])
190
- return error_with_results(
191
- <<~ERROR.chomp,
192
- Edit #{edit[:index]}: old_string not found in memory entry.
193
- Make sure it matches exactly, including all whitespace and indentation.
194
- Do not include line number prefixes from MemoryRead tool output.
195
- Note: This edit follows #{edit[:index]} previous edit(s) which may have changed the content.
196
- ERROR
197
- results,
198
- )
199
- end
200
-
201
- # Count occurrences
202
- occurrences = current_content.scan(edit[:old_string]).count
203
-
204
- # If not replace_all and multiple occurrences, error
205
- if !edit[:replace_all] && occurrences > 1
206
- return error_with_results(
207
- <<~ERROR.chomp,
208
- Edit #{edit[:index]}: Found #{occurrences} occurrences of old_string.
209
- Either provide more surrounding context to make the match unique, or set replace_all: true to replace all occurrences.
210
- ERROR
211
- results,
212
- )
213
- end
214
-
215
- # Perform replacement
216
- new_content = if edit[:replace_all]
217
- current_content.gsub(edit[:old_string], edit[:new_string])
218
- else
219
- current_content.sub(edit[:old_string], edit[:new_string])
220
- end
221
-
222
- # Record result
223
- replaced_count = edit[:replace_all] ? occurrences : 1
224
- results << {
225
- index: edit[:index],
226
- status: "success",
227
- occurrences: replaced_count,
228
- message: "Replaced #{replaced_count} occurrence(s)",
229
- }
230
-
231
- # Update content for next edit
232
- current_content = new_content
233
- end
234
-
235
- # Get existing entry
236
- entry = @storage.read_entry(file_path: file_path)
237
-
238
- # Write updated content back (preserving the title)
239
- @storage.write(
240
- file_path: file_path,
241
- content: current_content,
242
- title: entry.title,
243
- )
244
-
245
- # Build success message
246
- total_replacements = results.sum { |r| r[:occurrences] }
247
- message = "Successfully applied #{validated_edits.size} edit(s) to memory://#{file_path}\n"
248
- message += "Total replacements: #{total_replacements}\n\n"
249
- message += "Details:\n"
250
- results.each do |result|
251
- message += " Edit #{result[:index]}: #{result[:message]}\n"
252
- end
253
-
254
- message
255
- rescue ArgumentError => e
256
- validation_error(e.message)
257
- end
258
-
259
- private
260
-
261
- def validation_error(message)
262
- "<tool_use_error>InputValidationError: #{message}</tool_use_error>"
263
- end
264
-
265
- def error_with_results(message, results)
266
- output = "<tool_use_error>InputValidationError: #{message}\n\n"
267
-
268
- if results.any?
269
- output += "Previous successful edits before error:\n"
270
- results.each do |result|
271
- output += " Edit #{result[:index]}: #{result[:message]}\n"
272
- end
273
- output += "\n"
274
- end
275
-
276
- output += "Note: The memory entry has NOT been modified. All or nothing approach - if any edit fails, no changes are saved.</tool_use_error>"
277
- output
278
- end
279
- end
280
- end
281
- end