swarm_memory 2.1.2 → 2.1.4

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
@@ -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.1"
4
+ VERSION = "2.1.4"
5
5
  end
@@ -7,11 +7,11 @@ module SwarmMemory
7
7
  # Subclasses must implement all public methods to provide
8
8
  # different storage backends (filesystem, Redis, SQLite, etc.)
9
9
  class Base
10
- # Maximum size per entry (1MB)
11
- MAX_ENTRY_SIZE = 1_000_000
10
+ # Maximum size per entry (3MB)
11
+ MAX_ENTRY_SIZE = 3_000_000
12
12
 
13
- # Maximum total storage size (100MB)
14
- MAX_TOTAL_SIZE = 100_000_000
13
+ # Maximum total storage size (100GB)
14
+ MAX_TOTAL_SIZE = 100_000_000_000
15
15
 
16
16
  # Write content to storage
17
17
  #
@@ -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)
@@ -2,40 +2,77 @@
2
2
 
3
3
  module SwarmMemory
4
4
  module Core
5
- # StorageReadTracker manages read-entry tracking for all agents
5
+ # StorageReadTracker manages read-entry tracking for all agents with content digest verification
6
6
  #
7
7
  # This module maintains a global registry of which memory entries each agent
8
- # has read during their conversation. This enables enforcement of the
9
- # "read-before-edit" rule that ensures agents have context before modifying entries.
8
+ # has read during their conversation along with SHA256 digests of the content.
9
+ # This enables enforcement of the "read-before-edit" rule that ensures agents
10
+ # have context before modifying entries, AND prevents editing entries that have
11
+ # changed externally since being read.
10
12
  #
11
- # Each agent maintains an independent set of read entries, keyed by agent identifier.
13
+ # Each agent maintains an independent map of read entries to content digests.
12
14
  module StorageReadTracker
13
- @read_entries = {}
15
+ @read_entries = {} # { agent_id => { entry_path => sha256_digest } }
14
16
  @mutex = Mutex.new
15
17
 
16
18
  class << self
17
- # Register that an agent has read a storage entry
19
+ # Register that an agent has read a storage entry with content digest
18
20
  #
19
21
  # @param agent_id [Symbol] The agent identifier
20
22
  # @param entry_path [String] The storage entry path
21
- # @return [void]
22
- def register_read(agent_id, entry_path)
23
+ # @param content [String] Entry content (for digest calculation)
24
+ # @return [String] The calculated SHA256 digest
25
+ def register_read(agent_id, entry_path, content)
23
26
  @mutex.synchronize do
24
- @read_entries[agent_id] ||= Set.new
25
- @read_entries[agent_id] << entry_path
27
+ @read_entries[agent_id] ||= {}
28
+ digest = Digest::SHA256.hexdigest(content)
29
+ @read_entries[agent_id][entry_path] = digest
30
+ digest
26
31
  end
27
32
  end
28
33
 
29
- # Check if an agent has read a storage entry
34
+ # Check if an agent has read an entry AND content hasn't changed
30
35
  #
31
36
  # @param agent_id [Symbol] The agent identifier
32
37
  # @param entry_path [String] The storage entry path
33
- # @return [Boolean] true if the agent has read this entry
34
- def entry_read?(agent_id, entry_path)
38
+ # @param storage [Storage] Storage instance to read current content
39
+ # @return [Boolean] true if agent read entry and content matches
40
+ def entry_read?(agent_id, entry_path, storage)
35
41
  @mutex.synchronize do
36
42
  return false unless @read_entries[agent_id]
37
43
 
38
- @read_entries[agent_id].include?(entry_path)
44
+ stored_digest = @read_entries[agent_id][entry_path]
45
+ return false unless stored_digest
46
+
47
+ # Check if entry still matches stored digest
48
+ begin
49
+ current_content = storage.read(file_path: entry_path)
50
+ current_digest = Digest::SHA256.hexdigest(current_content)
51
+ current_digest == stored_digest
52
+ rescue StandardError
53
+ false # Entry deleted or inaccessible
54
+ end
55
+ end
56
+ end
57
+
58
+ # Get all read entries with digests for snapshot
59
+ #
60
+ # @param agent_id [Symbol] The agent identifier
61
+ # @return [Hash] { entry_path => digest }
62
+ def get_read_entries(agent_id)
63
+ @mutex.synchronize do
64
+ @read_entries[agent_id]&.dup || {}
65
+ end
66
+ end
67
+
68
+ # Restore read entries with digests from snapshot
69
+ #
70
+ # @param agent_id [Symbol] The agent identifier
71
+ # @param entries_with_digests [Hash] { entry_path => digest }
72
+ # @return [void]
73
+ def restore_read_entries(agent_id, entries_with_digests)
74
+ @mutex.synchronize do
75
+ @read_entries[agent_id] = entries_with_digests.dup
39
76
  end
40
77
  end
41
78
 
@@ -13,8 +13,9 @@ module SwarmMemory
13
13
  #
14
14
  # @return [void]
15
15
  def register!
16
- # Only register if SwarmCLI is present
17
- return unless defined?(SwarmCLI)
16
+ # Only register if SwarmCLI::CommandRegistry is available
17
+ # Check for the specific class, not just the module
18
+ return unless defined?(SwarmCLI::CommandRegistry)
18
19
 
19
20
  # Load CLI commands explicitly (Zeitwerk might not have loaded it yet)
20
21
  require_relative "../cli/commands"
@@ -156,7 +156,7 @@ module SwarmMemory
156
156
  # @return [String] Memory prompt contribution
157
157
  def system_prompt_contribution(agent_definition:, storage:)
158
158
  # Extract mode from memory config
159
- memory_config = agent_definition.memory
159
+ memory_config = agent_definition.plugin_config(:memory)
160
160
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
161
161
  memory_config.mode # MemoryConfig object from DSL
162
162
  elsif memory_config.respond_to?(:mode)
@@ -204,20 +204,100 @@ module SwarmMemory
204
204
  # @param agent_definition [Agent::Definition] Agent definition
205
205
  # @return [Boolean] True if agent has memory configuration
206
206
  def storage_enabled?(agent_definition)
207
- agent_definition.memory_enabled?
207
+ memory_config = agent_definition.plugin_config(:memory)
208
+ return false if memory_config.nil?
209
+
210
+ # MemoryConfig object (from DSL)
211
+ return memory_config.enabled? if memory_config.respond_to?(:enabled?)
212
+
213
+ # Hash (from YAML) - check for directory key
214
+ if memory_config.is_a?(Hash)
215
+ directory = memory_config[:directory] || memory_config["directory"]
216
+ return !directory.nil? && !directory.to_s.strip.empty?
217
+ end
218
+
219
+ false
208
220
  end
209
221
 
210
222
  # Contribute to agent serialization
211
223
  #
212
- # Preserves memory configuration when agents are cloned (e.g., in NodeOrchestrator).
224
+ # Preserves memory configuration when agents are cloned (e.g., in Workflow).
213
225
  # This allows memory configuration to persist across node transitions.
214
226
  #
215
227
  # @param agent_definition [Agent::Definition] Agent definition
216
228
  # @return [Hash] Memory config to include in to_h
217
229
  def serialize_config(agent_definition:)
218
- return {} unless agent_definition.memory
230
+ memory_config = agent_definition.plugin_config(:memory)
231
+ return {} unless memory_config
232
+
233
+ { memory: memory_config }
234
+ end
235
+
236
+ # Snapshot plugin-specific state for an agent
237
+ #
238
+ # Captures memory read tracking state for session persistence.
239
+ # This allows agents to remember which memory entries they've read
240
+ # across sessions.
241
+ #
242
+ # @param agent_name [Symbol] Agent identifier
243
+ # @return [Hash] Plugin-specific state
244
+ def snapshot_agent_state(agent_name)
245
+ entries_with_digests = Core::StorageReadTracker.get_read_entries(agent_name)
246
+ return {} if entries_with_digests.empty?
247
+
248
+ { read_entries: entries_with_digests }
249
+ end
250
+
251
+ # Restore plugin-specific state for an agent
252
+ #
253
+ # Restores memory read tracking state from snapshot.
254
+ # This is idempotent - calling multiple times with same state
255
+ # produces the same result.
256
+ #
257
+ # @param agent_name [Symbol] Agent identifier
258
+ # @param state [Hash] Previously snapshotted state (with symbol keys)
259
+ # @return [void]
260
+ def restore_agent_state(agent_name, state)
261
+ entries = state[:read_entries] || state["read_entries"]
262
+ return unless entries
263
+
264
+ Core::StorageReadTracker.restore_read_entries(agent_name, entries)
265
+ end
266
+
267
+ # Get digest for a memory tool result
268
+ #
269
+ # Returns the digest for a MemoryRead tool call, enabling change detection
270
+ # hooks to know if a memory entry has been modified since last read.
271
+ #
272
+ # @param agent_name [Symbol] Agent identifier
273
+ # @param tool_name [String] Name of the tool
274
+ # @param path [String] Path of the memory entry
275
+ # @return [String, nil] Digest string or nil if not a memory tool
276
+ def get_tool_result_digest(agent_name:, tool_name:, path:)
277
+ return unless tool_name == "MemoryRead"
278
+
279
+ Core::StorageReadTracker.get_read_entries(agent_name)[path]
280
+ end
219
281
 
220
- { memory: agent_definition.memory }
282
+ # Translate YAML configuration into DSL calls
283
+ #
284
+ # Called during YAML-to-DSL translation. Handles memory-specific YAML
285
+ # configuration and translates it into DSL method calls on the builder.
286
+ #
287
+ # @param builder [Agent::Builder] Builder instance (self in DSL context)
288
+ # @param agent_config [Hash] Full agent config from YAML
289
+ # @return [void]
290
+ def translate_yaml_config(builder, agent_config)
291
+ memory_config = agent_config[:memory]
292
+ return unless memory_config
293
+
294
+ builder.instance_eval do
295
+ memory do
296
+ directory(memory_config[:directory]) if memory_config[:directory]
297
+ adapter(memory_config[:adapter]) if memory_config[:adapter]
298
+ mode(memory_config[:mode]) if memory_config[:mode]
299
+ end
300
+ end
221
301
  end
222
302
 
223
303
  # Lifecycle: Agent initialized
@@ -239,7 +319,7 @@ module SwarmMemory
239
319
  return unless storage # Only proceed if memory is enabled for this agent
240
320
 
241
321
  # Extract mode from memory config
242
- memory_config = agent_definition.memory
322
+ memory_config = agent_definition.plugin_config(:memory)
243
323
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
244
324
  memory_config.mode # MemoryConfig object from DSL
245
325
  elsif memory_config.respond_to?(:mode)
@@ -250,9 +330,12 @@ module SwarmMemory
250
330
  :interactive # Default
251
331
  end
252
332
 
253
- # Store storage and mode for this agent
254
- @storages[agent_name] = storage
255
- @modes[agent_name] = mode
333
+ # V7.0: Extract base name for storage tracking (delegation instances share storage)
334
+ base_name = agent_name.to_s.split("@").first.to_sym
335
+
336
+ # Store storage and mode using BASE NAME
337
+ @storages[base_name] = storage # ← Changed from agent_name to base_name
338
+ @modes[base_name] = mode # ← Changed from agent_name to base_name
256
339
 
257
340
  # Get mode-specific tools
258
341
  allowed_tools = tools_for_mode(mode)
@@ -278,7 +361,7 @@ module SwarmMemory
278
361
  agent_definition: agent_definition,
279
362
  )
280
363
 
281
- agent.with_tool(load_skill_tool)
364
+ agent.add_tool(load_skill_tool)
282
365
  end
283
366
 
284
367
  # Mark mode-specific memory tools + LoadSkill as immutable
@@ -298,9 +381,12 @@ module SwarmMemory
298
381
  # @param is_first_message [Boolean] True if first message
299
382
  # @return [Array<String>] System reminders (0-2 reminders)
300
383
  def on_user_message(agent_name:, prompt:, is_first_message:)
301
- storage = @storages[agent_name]
384
+ # V7.0: Extract base name for storage lookup (delegation instances share storage)
385
+ base_name = agent_name.to_s.split("@").first.to_sym
386
+ storage = @storages[base_name] # ← Changed from agent_name to base_name
387
+
302
388
  return [] unless storage&.semantic_index
303
- return [] if prompt.empty?
389
+ return [] if prompt.nil? || prompt.empty?
304
390
 
305
391
  # Adaptive threshold based on query length
306
392
  # Short queries use lower threshold as they have less semantic richness
@@ -124,8 +124,8 @@ module SwarmMemory
124
124
  # Read current content (this will raise ArgumentError if entry doesn't exist)
125
125
  content = @storage.read(file_path: file_path)
126
126
 
127
- # Enforce read-before-edit
128
- unless Core::StorageReadTracker.entry_read?(@agent_name, file_path)
127
+ # Enforce read-before-edit with content verification
128
+ unless Core::StorageReadTracker.entry_read?(@agent_name, file_path, @storage)
129
129
  return validation_error(
130
130
  "Cannot edit memory entry without reading it first. " \
131
131
  "You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
@@ -140,8 +140,8 @@ module SwarmMemory
140
140
  # Read current content (this will raise ArgumentError if entry doesn't exist)
141
141
  content = @storage.read(file_path: file_path)
142
142
 
143
- # Enforce read-before-edit
144
- unless Core::StorageReadTracker.entry_read?(@agent_name, file_path)
143
+ # Enforce read-before-edit with content verification
144
+ unless Core::StorageReadTracker.entry_read?(@agent_name, file_path, @storage)
145
145
  return validation_error(
146
146
  "Cannot edit memory entry without reading it first. " \
147
147
  "You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
@@ -64,12 +64,12 @@ module SwarmMemory
64
64
  # @param file_path [String] Path to read from
65
65
  # @return [String] JSON with content and metadata
66
66
  def execute(file_path:)
67
- # Register this read in the tracker
68
- Core::StorageReadTracker.register_read(@agent_name, file_path)
69
-
70
67
  # Read full entry with metadata
71
68
  entry = @storage.read_entry(file_path: file_path)
72
69
 
70
+ # Register this read in the tracker with content digest
71
+ Core::StorageReadTracker.register_read(@agent_name, file_path, entry.content)
72
+
73
73
  # Always return JSON format (metadata always exists - at minimum title)
74
74
  format_as_json(entry)
75
75
  rescue ArgumentError => e
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.1.2"
4
+ VERSION = "2.1.4"
5
5
  end
data/lib/swarm_memory.rb CHANGED
@@ -31,13 +31,18 @@ loader = Zeitwerk::Loader.new
31
31
  loader.tag = File.basename(__FILE__, ".rb")
32
32
  loader.push_dir("#{__dir__}/swarm_memory", namespace: SwarmMemory)
33
33
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
34
+ loader.inflector.inflect(
35
+ "cli" => "CLI",
36
+ "dsl" => "DSL",
37
+ "sdk_plugin" => "SDKPlugin",
38
+ )
34
39
  loader.setup
35
40
 
36
41
  # Explicitly load DSL components and extensions to inject into SwarmSDK
37
42
  # These must be loaded after Zeitwerk but before anything uses them
38
43
  require_relative "swarm_memory/dsl/memory_config"
39
44
  require_relative "swarm_memory/dsl/builder_extension"
40
- require_relative "swarm_memory/chat_extension"
45
+ # NOTE: ChatExtension was removed in favor of SDK's built-in remove_tool method
41
46
 
42
47
  module SwarmMemory
43
48
  class << self
@@ -24,6 +24,13 @@ module SwarmSDK
24
24
  # Expose mcp_servers for tests
25
25
  attr_reader :mcp_servers
26
26
 
27
+ # Get tools list as array for validation
28
+ #
29
+ # @return [Array<Symbol>] List of tools
30
+ def tools_list
31
+ @tools.to_a
32
+ end
33
+
27
34
  def initialize(name)
28
35
  @name = name
29
36
  @description = nil
@@ -52,6 +59,8 @@ module SwarmSDK
52
59
  @permissions_config = {}
53
60
  @default_permissions = {} # Set by SwarmBuilder from all_agents
54
61
  @memory_config = nil
62
+ @shared_across_delegations = nil # nil = not set (will default to false in Definition)
63
+ @context_management_config = nil # Context management DSL hooks
55
64
  end
56
65
 
57
66
  # Set/get agent model
@@ -267,6 +276,80 @@ module SwarmSDK
267
276
  @permissions_config = PermissionsBuilder.build(&block)
268
277
  end
269
278
 
279
+ # Configure delegation isolation mode
280
+ #
281
+ # @param enabled [Boolean] If true, allows sharing instances across delegations (old behavior)
282
+ # If false (default), creates isolated instances per delegation
283
+ # @return [self] Returns self for method chaining
284
+ #
285
+ # @example
286
+ # shared_across_delegations true # Allow sharing (old behavior)
287
+ def shared_across_delegations(enabled)
288
+ @shared_across_delegations = enabled
289
+ self
290
+ end
291
+
292
+ # Configure context management handlers
293
+ #
294
+ # Define custom handlers for context warning thresholds (60%, 80%, 90%).
295
+ # Handlers receive a rich context object with message manipulation methods.
296
+ # When a custom handler is registered, automatic compression is disabled
297
+ # for that threshold, giving full control to the handler.
298
+ #
299
+ # @yield Context management DSL block
300
+ # @return [void]
301
+ #
302
+ # @example Basic compression at 60%
303
+ # context_management do
304
+ # on :warning_60 do |ctx|
305
+ # ctx.compress_tool_results(keep_recent: 10)
306
+ # end
307
+ # end
308
+ #
309
+ # @example Multiple thresholds with different strategies
310
+ # context_management do
311
+ # on :warning_60 do |ctx|
312
+ # ctx.compress_tool_results(keep_recent: 15, truncate_to: 500)
313
+ # end
314
+ #
315
+ # on :warning_80 do |ctx|
316
+ # ctx.prune_old_messages(keep_recent: 30)
317
+ # ctx.compress_tool_results(keep_recent: 5, truncate_to: 200)
318
+ # end
319
+ #
320
+ # on :warning_90 do |ctx|
321
+ # ctx.log_action("emergency_pruning", remaining: ctx.tokens_remaining)
322
+ # ctx.prune_old_messages(keep_recent: 15)
323
+ # end
324
+ # end
325
+ #
326
+ # @example Conditional logic based on metrics
327
+ # context_management do
328
+ # on :warning_80 do |ctx|
329
+ # if ctx.usage_percentage > 85
330
+ # ctx.prune_old_messages(keep_recent: 10)
331
+ # else
332
+ # ctx.compress_tool_results(keep_recent: 5)
333
+ # end
334
+ # end
335
+ # end
336
+ def context_management(&block)
337
+ builder = ContextManagement::Builder.new
338
+ builder.instance_eval(&block)
339
+ @context_management_config = builder.build
340
+ end
341
+
342
+ # Set permissions directly from hash (for YAML translation)
343
+ #
344
+ # This is intentionally separate from permissions() to keep the DSL clean.
345
+ # Called by Configuration when translating YAML permissions.
346
+ #
347
+ # @param hash [Hash] Permissions configuration hash
348
+ # @return [void]
349
+ def permissions_hash=(hash)
350
+ @permissions_config = hash || {}
351
+ end
352
+
270
353
  # Check if model has been explicitly set (not default)
271
354
  #
272
355
  # Used by Swarm::Builder to determine if all_agents model should apply.
@@ -374,10 +457,18 @@ module SwarmSDK
374
457
  agent_config[:permissions] = @permissions_config if @permissions_config.any?
375
458
  agent_config[:default_permissions] = @default_permissions if @default_permissions.any?
376
459
  agent_config[:memory] = @memory_config if @memory_config
460
+ agent_config[:shared_across_delegations] = @shared_across_delegations unless @shared_across_delegations.nil?
377
461
 
378
462
  # Convert DSL hooks to HookDefinition format
379
463
  agent_config[:hooks] = convert_hooks_to_definitions if @hooks.any?
380
464
 
465
+ # Merge context management hooks into agent hooks
466
+ if @context_management_config
467
+ agent_config[:hooks] ||= {}
468
+ agent_config[:hooks][:context_warning] ||= []
469
+ agent_config[:hooks][:context_warning].concat(@context_management_config)
470
+ end
471
+
381
472
  Agent::Definition.new(@name, agent_config)
382
473
  end
383
474