claude_swarm 1.0.9 → 1.0.11

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
  3. data/CLAUDE.md +346 -191
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
  8. data/docs/v2/README.md +20 -5
  9. data/docs/v2/guides/complete-tutorial.md +95 -9
  10. data/docs/v2/guides/getting-started.md +10 -8
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/migrating-to-2.x.md +746 -0
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/snapshots.md +14 -14
  16. data/docs/v2/guides/swarm-memory.md +2 -13
  17. data/docs/v2/reference/architecture-flow.md +3 -3
  18. data/docs/v2/reference/cli.md +0 -1
  19. data/docs/v2/reference/configuration_reference.md +300 -0
  20. data/docs/v2/reference/event_payload_structures.md +27 -5
  21. data/docs/v2/reference/ruby-dsl.md +614 -18
  22. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  23. data/docs/v2/reference/yaml.md +172 -54
  24. data/examples/snapshot_demo.rb +2 -2
  25. data/lib/claude_swarm/mcp_generator.rb +8 -21
  26. data/lib/claude_swarm/orchestrator.rb +8 -1
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/swarm_cli/commands/run.rb +2 -2
  29. data/lib/swarm_cli/config_loader.rb +11 -11
  30. data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
  31. data/lib/swarm_cli/interactive_repl.rb +2 -2
  32. data/lib/swarm_cli/ui/icons.rb +0 -23
  33. data/lib/swarm_cli/version.rb +1 -1
  34. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  35. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  36. data/lib/swarm_memory/core/storage.rb +7 -2
  37. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  38. data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
  39. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  40. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  41. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  42. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  43. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  44. data/lib/swarm_memory/version.rb +1 -1
  45. data/lib/swarm_memory.rb +8 -6
  46. data/lib/swarm_sdk/agent/builder.rb +58 -0
  47. data/lib/swarm_sdk/agent/chat.rb +527 -1061
  48. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
  49. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  50. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
  51. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  52. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
  53. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
  54. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  55. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
  56. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  57. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
  58. data/lib/swarm_sdk/agent/context.rb +1 -2
  59. data/lib/swarm_sdk/agent/definition.rb +66 -154
  60. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  61. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  62. data/lib/swarm_sdk/agent_registry.rb +146 -0
  63. data/lib/swarm_sdk/builders/base_builder.rb +488 -0
  64. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  65. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  66. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  67. data/lib/swarm_sdk/config.rb +302 -0
  68. data/lib/swarm_sdk/configuration/parser.rb +373 -0
  69. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  70. data/lib/swarm_sdk/configuration.rb +77 -546
  71. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  72. data/lib/swarm_sdk/context_compactor.rb +6 -11
  73. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  74. data/lib/swarm_sdk/context_management/context.rb +328 -0
  75. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  76. data/lib/swarm_sdk/defaults.rb +196 -0
  77. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  78. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  79. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  80. data/lib/swarm_sdk/log_collector.rb +179 -29
  81. data/lib/swarm_sdk/log_stream.rb +29 -0
  82. data/lib/swarm_sdk/models.json +4333 -1
  83. data/lib/swarm_sdk/models.rb +43 -2
  84. data/lib/swarm_sdk/node_context.rb +1 -1
  85. data/lib/swarm_sdk/observer/builder.rb +81 -0
  86. data/lib/swarm_sdk/observer/config.rb +45 -0
  87. data/lib/swarm_sdk/observer/manager.rb +236 -0
  88. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  89. data/lib/swarm_sdk/plugin.rb +95 -5
  90. data/lib/swarm_sdk/result.rb +52 -0
  91. data/lib/swarm_sdk/snapshot.rb +6 -6
  92. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  93. data/lib/swarm_sdk/state_restorer.rb +136 -151
  94. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  95. data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
  96. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  97. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  98. data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
  99. data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
  100. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  101. data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
  102. data/lib/swarm_sdk/swarm.rb +203 -683
  103. data/lib/swarm_sdk/tools/bash.rb +14 -8
  104. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  105. data/lib/swarm_sdk/tools/edit.rb +8 -13
  106. data/lib/swarm_sdk/tools/glob.rb +12 -4
  107. data/lib/swarm_sdk/tools/grep.rb +7 -0
  108. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  109. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  110. data/lib/swarm_sdk/tools/read.rb +16 -18
  111. data/lib/swarm_sdk/tools/registry.rb +122 -10
  112. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  113. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  114. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  115. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  116. data/lib/swarm_sdk/tools/write.rb +8 -13
  117. data/lib/swarm_sdk/version.rb +1 -1
  118. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  119. data/lib/swarm_sdk/workflow/builder.rb +192 -0
  120. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  121. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  122. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  123. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  124. data/lib/swarm_sdk.rb +294 -108
  125. data/rubocop/cop/security/no_reflection_methods.rb +1 -1
  126. data/swarm_cli.gemspec +1 -1
  127. data/swarm_memory.gemspec +8 -3
  128. data/swarm_sdk.gemspec +6 -4
  129. data/team_full.yml +124 -320
  130. metadata +42 -14
  131. data/lib/swarm_memory/chat_extension.rb +0 -34
  132. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  133. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
  134. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -31,29 +31,6 @@ module SwarmCLI
31
31
  ARROW_RIGHT = "→"
32
32
  BULLET = "•"
33
33
  COMPRESS = "🗜️"
34
-
35
- # All icons as hash for backward compatibility
36
- ALL = {
37
- thinking: THINKING,
38
- response: RESPONSE,
39
- success: SUCCESS,
40
- error: ERROR,
41
- info: INFO,
42
- warning: WARNING,
43
- agent: AGENT,
44
- tool: TOOL,
45
- delegate: DELEGATE,
46
- result: RESULT,
47
- hook: HOOK,
48
- llm: LLM,
49
- tokens: TOKENS,
50
- cost: COST,
51
- time: TIME,
52
- sparkles: SPARKLES,
53
- arrow_right: ARROW_RIGHT,
54
- bullet: BULLET,
55
- compress: COMPRESS,
56
- }.freeze
57
34
  end
58
35
  end
59
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.1.3"
4
+ VERSION = "2.1.7"
5
5
  end
@@ -93,10 +93,10 @@ module SwarmMemory
93
93
  "Clear old entries or use smaller content."
94
94
  end
95
95
 
96
- # Strip .md extension and flatten path for disk storage
97
- # "concepts/ruby/classes.md" → "concepts--ruby--classes"
96
+ # Strip .md extension for disk storage
97
+ # "concepts/ruby/classes.md" → "concepts/ruby/classes"
98
98
  base_path = file_path.sub(/\.md\z/, "")
99
- disk_path = flatten_path(base_path)
99
+ disk_path = base_path
100
100
 
101
101
  # 1. Write content to .md file (stored exactly as provided)
102
102
  md_file = File.join(@directory, "#{disk_path}.md")
@@ -162,9 +162,9 @@ module SwarmMemory
162
162
  return entry.content
163
163
  end
164
164
 
165
- # Strip .md extension and flatten path
165
+ # Strip .md extension
166
166
  base_path = file_path.sub(/\.md\z/, "")
167
- disk_path = flatten_path(base_path)
167
+ disk_path = base_path
168
168
  md_file = File.join(@directory, "#{disk_path}.md")
169
169
 
170
170
  raise ArgumentError, "memory://#{file_path} not found" unless File.exist?(md_file)
@@ -189,9 +189,9 @@ module SwarmMemory
189
189
  return load_virtual_entry(file_path)
190
190
  end
191
191
 
192
- # Strip .md extension and flatten path
192
+ # Strip .md extension
193
193
  base_path = file_path.sub(/\.md\z/, "")
194
- disk_path = flatten_path(base_path)
194
+ disk_path = base_path
195
195
  md_file = File.join(@directory, "#{disk_path}.md")
196
196
  yaml_file = File.join(@directory, "#{disk_path}.yml")
197
197
 
@@ -230,9 +230,9 @@ module SwarmMemory
230
230
  @semaphore.acquire do
231
231
  raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
232
232
 
233
- # Strip .md extension and flatten path
233
+ # Strip .md extension
234
234
  base_path = file_path.sub(/\.md\z/, "")
235
- disk_path = flatten_path(base_path)
235
+ disk_path = base_path
236
236
  md_file = File.join(@directory, "#{disk_path}.md")
237
237
 
238
238
  raise ArgumentError, "memory://#{file_path} not found" unless File.exist?(md_file)
@@ -500,29 +500,6 @@ module SwarmMemory
500
500
  )
501
501
  end
502
502
 
503
- # Flatten path for disk storage
504
- # "concepts/ruby/classes" → "concepts--ruby--classes"
505
- #
506
- # @param logical_path [String] Logical path with slashes
507
- # @return [String] Flattened path with --
508
- # Identity function - paths are now stored hierarchically
509
- # Kept for backward compatibility during transition
510
- #
511
- # @param logical_path [String] Logical path
512
- # @return [String] Same path (no flattening)
513
- def flatten_path(logical_path)
514
- logical_path
515
- end
516
-
517
- # Identity function - paths are now stored hierarchically
518
- # Kept for backward compatibility during transition
519
- #
520
- # @param disk_path [String] Disk path
521
- # @return [String] Same path (no unflattening)
522
- def unflatten_path(disk_path)
523
- disk_path
524
- end
525
-
526
503
  # Check if content is a stub (redirect)
527
504
  #
528
505
  # @param content [String] File content
@@ -566,7 +543,7 @@ module SwarmMemory
566
543
  # @return [void]
567
544
  def increment_hits(file_path)
568
545
  base_path = file_path.sub(/\.md\z/, "")
569
- disk_path = flatten_path(base_path)
546
+ disk_path = base_path
570
547
  yaml_file = File.join(@directory, "#{disk_path}.yml")
571
548
  return unless File.exist?(yaml_file)
572
549
 
@@ -587,7 +564,7 @@ module SwarmMemory
587
564
  # @return [Integer] Size in bytes
588
565
  def get_entry_size(file_path)
589
566
  base_path = file_path.sub(/\.md\z/, "")
590
- disk_path = flatten_path(base_path)
567
+ disk_path = base_path
591
568
  yaml_file = File.join(@directory, "#{disk_path}.yml")
592
569
 
593
570
  if File.exist?(yaml_file)
@@ -181,6 +181,9 @@ module SwarmMemory
181
181
 
182
182
  # Calculate hybrid scores combining semantic similarity and keyword matching
183
183
  #
184
+ # When keyword_score is 0 (no tag matches), falls back to pure semantic scoring
185
+ # to avoid penalizing results that have excellent semantic matches but no tag overlap.
186
+ #
184
187
  # @param results [Array<Hash>] Results with semantic :similarity scores
185
188
  # @param query_keywords [Array<String>] Keywords from query
186
189
  # @return [Array<Hash>] Results with updated :similarity (hybrid score) and debug info
@@ -189,8 +192,13 @@ module SwarmMemory
189
192
  semantic_score = result[:similarity]
190
193
  keyword_score = calculate_keyword_score(result, query_keywords)
191
194
 
192
- # Hybrid score: weighted combination
193
- hybrid_score = (@semantic_weight * semantic_score) + (@keyword_weight * keyword_score)
195
+ # Fallback to pure semantic when no keyword matches
196
+ # This prevents penalizing results with excellent semantic matches but no tag overlap
197
+ hybrid_score = if keyword_score.zero?
198
+ semantic_score
199
+ else
200
+ (@semantic_weight * semantic_score) + (@keyword_weight * keyword_score)
201
+ end
194
202
 
195
203
  # Update result with hybrid score and debug info
196
204
  result.merge(
@@ -18,7 +18,9 @@ module SwarmMemory
18
18
  #
19
19
  # @param adapter [Adapters::Base] Storage adapter
20
20
  # @param embedder [Embeddings::Embedder, nil] Optional embedder for semantic search
21
- def initialize(adapter:, embedder: nil)
21
+ # @param semantic_weight [Float, nil] Weight for semantic similarity in hybrid search (0.0-1.0)
22
+ # @param keyword_weight [Float, nil] Weight for keyword matching in hybrid search (0.0-1.0)
23
+ def initialize(adapter:, embedder: nil, semantic_weight: nil, keyword_weight: nil)
22
24
  raise ArgumentError, "adapter is required" unless adapter.is_a?(Adapters::Base)
23
25
 
24
26
  @adapter = adapter
@@ -26,7 +28,10 @@ module SwarmMemory
26
28
 
27
29
  # Create semantic index if embedder is provided
28
30
  @semantic_index = if embedder
29
- SemanticIndex.new(adapter: adapter, embedder: embedder)
31
+ index_options = { adapter: adapter, embedder: embedder }
32
+ index_options[:semantic_weight] = semantic_weight if semantic_weight
33
+ index_options[:keyword_weight] = keyword_weight if keyword_weight
34
+ SemanticIndex.new(**index_options)
30
35
  end
31
36
  end
32
37
 
@@ -84,6 +84,43 @@ module SwarmMemory
84
84
  @mode = value.to_sym
85
85
  end
86
86
 
87
+ # DSL method to set/get semantic weight for hybrid search
88
+ #
89
+ # Controls how much semantic (embedding) similarity affects search results.
90
+ # Default is 0.5 (50%). Set to 1.0 for pure semantic search.
91
+ #
92
+ # @param value [Float, nil] Weight between 0.0 and 1.0
93
+ # @return [Float, nil] Current semantic weight
94
+ #
95
+ # @example Pure semantic search (no keyword penalty)
96
+ # semantic_weight 1.0
97
+ # keyword_weight 0.0
98
+ def semantic_weight(value = nil)
99
+ if value.nil?
100
+ @adapter_options[:semantic_weight]
101
+ else
102
+ @adapter_options[:semantic_weight] = value.to_f
103
+ end
104
+ end
105
+
106
+ # DSL method to set/get keyword weight for hybrid search
107
+ #
108
+ # Controls how much keyword (tag) matching affects search results.
109
+ # Default is 0.5 (50%). Set to 0.0 to disable keyword matching.
110
+ #
111
+ # @param value [Float, nil] Weight between 0.0 and 1.0
112
+ # @return [Float, nil] Current keyword weight
113
+ #
114
+ # @example Disable keyword matching
115
+ # keyword_weight 0.0
116
+ def keyword_weight(value = nil)
117
+ if value.nil?
118
+ @adapter_options[:keyword_weight]
119
+ else
120
+ @adapter_options[:keyword_weight] = value.to_f
121
+ end
122
+ end
123
+
87
124
  # Check if memory is enabled
88
125
  #
89
126
  # @return [Boolean] True if adapter is configured with required options
@@ -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
  ]
@@ -109,10 +110,12 @@ module SwarmMemory
109
110
  # MemoryConfig object (from DSL)
110
111
  [config.adapter_type, config.adapter_options]
111
112
  elsif config.is_a?(Hash)
112
- # Hash (from YAML)
113
+ # Hash (from YAML) - symbolize keys for adapter compatibility
113
114
  adapter = (config[:adapter] || config["adapter"] || :filesystem).to_sym
114
- options = config.reject { |k, _v| k == :adapter || k == "adapter" || k == :mode || k == "mode" }
115
- [adapter, options]
115
+ options = config.reject { |k, _v| [:adapter, "adapter", :mode, "mode"].include?(k) }
116
+ # Symbolize keys so adapter receives keyword arguments correctly
117
+ symbolized_options = options.transform_keys { |k| k.to_s.to_sym }
118
+ [adapter, symbolized_options]
116
119
  else
117
120
  raise SwarmSDK::ConfigurationError, "Invalid memory configuration for #{agent_name}"
118
121
  end
@@ -124,7 +127,17 @@ module SwarmMemory
124
127
  raise SwarmSDK::ConfigurationError, "#{e.message} for agent #{agent_name}"
125
128
  end
126
129
 
127
- # Instantiate adapter with options
130
+ # Extract hybrid search weights and other SDK-level config (before passing to adapter)
131
+ # Keys are already symbolized at this point
132
+ semantic_weight = adapter_options.delete(:semantic_weight)
133
+ keyword_weight = adapter_options.delete(:keyword_weight)
134
+
135
+ # Remove other SDK-level threshold configs that shouldn't go to adapter
136
+ adapter_options.delete(:discovery_threshold)
137
+ adapter_options.delete(:discovery_threshold_short)
138
+ adapter_options.delete(:adaptive_word_cutoff)
139
+
140
+ # Instantiate adapter with options (weights removed, adapter doesn't need them)
128
141
  # Note: Adapter is responsible for validating its own requirements
129
142
  begin
130
143
  adapter = adapter_class.new(**adapter_options)
@@ -136,8 +149,13 @@ module SwarmMemory
136
149
  # Create embedder for semantic search
137
150
  embedder = Embeddings::InformersEmbedder.new
138
151
 
139
- # Create storage with embedder (enables semantic features)
140
- Core::Storage.new(adapter: adapter, embedder: embedder)
152
+ # Create storage with embedder and hybrid search weights
153
+ Core::Storage.new(
154
+ adapter: adapter,
155
+ embedder: embedder,
156
+ semantic_weight: semantic_weight,
157
+ keyword_weight: keyword_weight,
158
+ )
141
159
  end
142
160
 
143
161
  # Parse memory configuration
@@ -156,7 +174,7 @@ module SwarmMemory
156
174
  # @return [String] Memory prompt contribution
157
175
  def system_prompt_contribution(agent_definition:, storage:)
158
176
  # Extract mode from memory config
159
- memory_config = agent_definition.memory
177
+ memory_config = agent_definition.plugin_config(:memory)
160
178
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
161
179
  memory_config.mode # MemoryConfig object from DSL
162
180
  elsif memory_config.respond_to?(:mode)
@@ -199,25 +217,126 @@ module SwarmMemory
199
217
  end
200
218
  end
201
219
 
202
- # Check if storage should be created for this agent
220
+ # Check if memory is configured for this agent
221
+ #
222
+ # Delegates adapter-specific validation to the adapter itself.
223
+ # Filesystem adapter requires 'directory', custom adapters may use other keys.
203
224
  #
204
225
  # @param agent_definition [Agent::Definition] Agent definition
205
- # @return [Boolean] True if agent has memory configuration
206
- def storage_enabled?(agent_definition)
207
- agent_definition.memory_enabled?
226
+ # @return [Boolean] True if agent has valid memory configuration
227
+ def memory_configured?(agent_definition)
228
+ memory_config = agent_definition.plugin_config(:memory)
229
+ return false if memory_config.nil?
230
+
231
+ # MemoryConfig object (from DSL) - delegates to its enabled? method
232
+ return memory_config.enabled? if memory_config.respond_to?(:enabled?)
233
+
234
+ # Hash (from YAML)
235
+ return false unless memory_config.is_a?(Hash)
236
+ return false if memory_config.empty?
237
+
238
+ adapter = (memory_config[:adapter] || memory_config["adapter"] || :filesystem).to_sym
239
+
240
+ case adapter
241
+ when :filesystem
242
+ # Filesystem adapter requires directory
243
+ directory = memory_config[:directory] || memory_config["directory"]
244
+ !directory.nil? && !directory.to_s.strip.empty?
245
+ else
246
+ # Custom adapters: presence of config is sufficient
247
+ # Adapter will validate its own requirements during initialization
248
+ true
249
+ end
208
250
  end
209
251
 
210
252
  # Contribute to agent serialization
211
253
  #
212
- # Preserves memory configuration when agents are cloned (e.g., in NodeOrchestrator).
254
+ # Preserves memory configuration when agents are cloned (e.g., in Workflow).
213
255
  # This allows memory configuration to persist across node transitions.
214
256
  #
215
257
  # @param agent_definition [Agent::Definition] Agent definition
216
258
  # @return [Hash] Memory config to include in to_h
217
259
  def serialize_config(agent_definition:)
218
- return {} unless agent_definition.memory
260
+ memory_config = agent_definition.plugin_config(:memory)
261
+ return {} unless memory_config
219
262
 
220
- { memory: agent_definition.memory }
263
+ { memory: memory_config }
264
+ end
265
+
266
+ # Snapshot plugin-specific state for an agent
267
+ #
268
+ # Captures memory read tracking state for session persistence.
269
+ # This allows agents to remember which memory entries they've read
270
+ # across sessions.
271
+ #
272
+ # @param agent_name [Symbol] Agent identifier
273
+ # @return [Hash] Plugin-specific state
274
+ def snapshot_agent_state(agent_name)
275
+ entries_with_digests = Core::StorageReadTracker.get_read_entries(agent_name)
276
+ return {} if entries_with_digests.empty?
277
+
278
+ { read_entries: entries_with_digests }
279
+ end
280
+
281
+ # Restore plugin-specific state for an agent
282
+ #
283
+ # Restores memory read tracking state from snapshot.
284
+ # This is idempotent - calling multiple times with same state
285
+ # produces the same result.
286
+ #
287
+ # @param agent_name [Symbol] Agent identifier
288
+ # @param state [Hash] Previously snapshotted state (with symbol keys)
289
+ # @return [void]
290
+ def restore_agent_state(agent_name, state)
291
+ entries = state[:read_entries] || state["read_entries"]
292
+ return unless entries
293
+
294
+ Core::StorageReadTracker.restore_read_entries(agent_name, entries)
295
+ end
296
+
297
+ # Get digest for a memory tool result
298
+ #
299
+ # Returns the digest for a MemoryRead tool call, enabling change detection
300
+ # hooks to know if a memory entry has been modified since last read.
301
+ #
302
+ # @param agent_name [Symbol] Agent identifier
303
+ # @param tool_name [String] Name of the tool
304
+ # @param path [String] Path of the memory entry
305
+ # @return [String, nil] Digest string or nil if not a memory tool
306
+ def get_tool_result_digest(agent_name:, tool_name:, path:)
307
+ return unless tool_name == "MemoryRead"
308
+
309
+ Core::StorageReadTracker.get_read_entries(agent_name)[path]
310
+ end
311
+
312
+ # Translate YAML configuration into DSL calls
313
+ #
314
+ # Called during YAML-to-DSL translation. Handles memory-specific YAML
315
+ # configuration and translates it into DSL method calls on the builder.
316
+ #
317
+ # @param builder [Agent::Builder] Builder instance (self in DSL context)
318
+ # @param agent_config [Hash] Full agent config from YAML
319
+ # @return [void]
320
+ def translate_yaml_config(builder, agent_config)
321
+ memory_config = agent_config[:memory]
322
+ return unless memory_config
323
+
324
+ builder.instance_eval do
325
+ memory do
326
+ # Standard options
327
+ directory(memory_config[:directory]) if memory_config[:directory]
328
+ adapter(memory_config[:adapter]) if memory_config[:adapter]
329
+ mode(memory_config[:mode]) if memory_config[:mode]
330
+
331
+ # Pass through all custom adapter options
332
+ # Handle both symbol and string keys (YAML may have either)
333
+ standard_keys = [:directory, :adapter, :mode, "directory", "adapter", "mode"]
334
+ custom_keys = memory_config.keys - standard_keys
335
+ custom_keys.each do |key|
336
+ option(key.to_sym, memory_config[key]) # Normalize to symbol
337
+ end
338
+ end
339
+ end
221
340
  end
222
341
 
223
342
  # Lifecycle: Agent initialized
@@ -239,7 +358,7 @@ module SwarmMemory
239
358
  return unless storage # Only proceed if memory is enabled for this agent
240
359
 
241
360
  # Extract mode from memory config
242
- memory_config = agent_definition.memory
361
+ memory_config = agent_definition.plugin_config(:memory)
243
362
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
244
363
  memory_config.mode # MemoryConfig object from DSL
245
364
  elsif memory_config.respond_to?(:mode)
@@ -256,6 +375,7 @@ module SwarmMemory
256
375
  # Store storage and mode using BASE NAME
257
376
  @storages[base_name] = storage # ← Changed from agent_name to base_name
258
377
  @modes[base_name] = mode # ← Changed from agent_name to base_name
378
+ @threshold_configs[base_name] = extract_threshold_config(memory_config)
259
379
 
260
380
  # Get mode-specific tools
261
381
  allowed_tools = tools_for_mode(mode)
@@ -281,7 +401,7 @@ module SwarmMemory
281
401
  agent_definition: agent_definition,
282
402
  )
283
403
 
284
- agent.with_tool(load_skill_tool)
404
+ agent.add_tool(load_skill_tool)
285
405
  end
286
406
 
287
407
  # Mark mode-specific memory tools + LoadSkill as immutable
@@ -304,20 +424,27 @@ module SwarmMemory
304
424
  # V7.0: Extract base name for storage lookup (delegation instances share storage)
305
425
  base_name = agent_name.to_s.split("@").first.to_sym
306
426
  storage = @storages[base_name] # ← Changed from agent_name to base_name
427
+ config = @threshold_configs[base_name] || {}
307
428
 
308
429
  return [] unless storage&.semantic_index
309
430
  return [] if prompt.nil? || prompt.empty?
310
431
 
311
432
  # Adaptive threshold based on query length
312
433
  # Short queries use lower threshold as they have less semantic richness
313
- # Optimal: cutoff=10 words, short=0.25, normal=0.35 (discovered via systematic evaluation)
434
+ # Fallback chain: config ENV default
314
435
  word_count = prompt.split.size
315
- word_cutoff = (ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"] || "10").to_i
436
+ word_cutoff = config[:adaptive_word_cutoff] ||
437
+ ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"]&.to_i ||
438
+ 10
316
439
 
317
440
  threshold = if word_count < word_cutoff
318
- (ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"] || "0.25").to_f
441
+ config[:discovery_threshold_short] ||
442
+ ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"]&.to_f ||
443
+ 0.25
319
444
  else
320
- (ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"] || "0.35").to_f
445
+ config[:discovery_threshold] ||
446
+ ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"]&.to_f ||
447
+ 0.35
321
448
  end
322
449
  reminders = []
323
450
 
@@ -402,9 +529,15 @@ module SwarmMemory
402
529
  }
403
530
  end
404
531
 
405
- # Get actual weights being used (from ENV or defaults)
406
- semantic_weight = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
407
- keyword_weight = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
532
+ # Get actual weights being used (fallback chain: config → ENV defaults)
533
+ base_name = agent_name.to_s.split("@").first.to_sym
534
+ config = @threshold_configs[base_name] || {}
535
+ semantic_weight = config[:semantic_weight] ||
536
+ ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"]&.to_f ||
537
+ 0.5
538
+ keyword_weight = config[:keyword_weight] ||
539
+ ENV["SWARM_MEMORY_KEYWORD_WEIGHT"]&.to_f ||
540
+ 0.5
408
541
 
409
542
  SwarmSDK::LogStream.emit(
410
543
  type: "semantic_skill_search",
@@ -465,9 +598,15 @@ module SwarmMemory
465
598
  }
466
599
  end
467
600
 
468
- # Get actual weights being used (from ENV or defaults)
469
- semantic_weight = (ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"] || "0.5").to_f
470
- keyword_weight = (ENV["SWARM_MEMORY_KEYWORD_WEIGHT"] || "0.5").to_f
601
+ # Get actual weights being used (fallback chain: config → ENV defaults)
602
+ base_name = agent_name.to_s.split("@").first.to_sym
603
+ config = @threshold_configs[base_name] || {}
604
+ semantic_weight = config[:semantic_weight] ||
605
+ ENV["SWARM_MEMORY_SEMANTIC_WEIGHT"]&.to_f ||
606
+ 0.5
607
+ keyword_weight = config[:keyword_weight] ||
608
+ ENV["SWARM_MEMORY_KEYWORD_WEIGHT"]&.to_f ||
609
+ 0.5
471
610
 
472
611
  SwarmSDK::LogStream.emit(
473
612
  type: "semantic_memory_search",
@@ -546,6 +685,40 @@ module SwarmMemory
546
685
 
547
686
  reminder
548
687
  end
688
+
689
+ # Extract threshold configuration from memory config
690
+ #
691
+ # Supports both MemoryConfig objects (from DSL) and Hash configs (from YAML).
692
+ # Extracts semantic search thresholds and hybrid search weights.
693
+ #
694
+ # @param memory_config [MemoryConfig, Hash, nil] Memory configuration
695
+ # @return [Hash] Threshold config with symbol keys
696
+ def extract_threshold_config(memory_config)
697
+ return {} unless memory_config
698
+
699
+ threshold_keys = [
700
+ :discovery_threshold,
701
+ :discovery_threshold_short,
702
+ :adaptive_word_cutoff,
703
+ :semantic_weight,
704
+ :keyword_weight,
705
+ ]
706
+
707
+ if memory_config.respond_to?(:adapter_options)
708
+ # MemoryConfig object (from DSL)
709
+ memory_config.adapter_options.slice(*threshold_keys)
710
+ elsif memory_config.is_a?(Hash)
711
+ # Hash (from YAML) - handle both symbol and string keys
712
+ result = {}
713
+ threshold_keys.each do |key|
714
+ value = memory_config[key] || memory_config[key.to_s]
715
+ result[key] = value if value
716
+ end
717
+ result
718
+ else
719
+ {}
720
+ end
721
+ end
549
722
  end
550
723
  end
551
724
  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.3"
4
+ VERSION = "2.2.3"
5
5
  end