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.
- checksums.yaml +4 -4
- data/lib/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +30 -19
- data/lib/claude_swarm/mcp_generator.rb +5 -10
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +14 -14
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +6 -1
- data/lib/swarm_sdk/agent/builder.rb +91 -0
- data/lib/swarm_sdk/agent/chat.rb +540 -925
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +8 -4
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +79 -174
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +100 -261
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +199 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +192 -16
- data/lib/swarm_sdk/log_stream.rb +66 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
- data/lib/swarm_sdk/state_restorer.rb +476 -0
- data/lib/swarm_sdk/state_snapshot.rb +334 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +69 -407
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
- data/lib/swarm_sdk/swarm.rb +366 -631
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +127 -24
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +28 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +27 -8
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/workflow.rb +554 -0
- data/lib/swarm_sdk.rb +393 -22
- metadata +51 -16
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/node_orchestrator.rb +0 -591
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
data/lib/swarm_cli/ui/icons.rb
CHANGED
|
@@ -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
|
data/lib/swarm_cli/version.rb
CHANGED
|
@@ -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 (
|
|
11
|
-
MAX_ENTRY_SIZE =
|
|
10
|
+
# Maximum size per entry (3MB)
|
|
11
|
+
MAX_ENTRY_SIZE = 3_000_000
|
|
12
12
|
|
|
13
|
-
# Maximum total storage size (
|
|
14
|
-
MAX_TOTAL_SIZE =
|
|
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
|
|
97
|
-
# "concepts/ruby/classes.md" → "concepts
|
|
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 =
|
|
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
|
|
165
|
+
# Strip .md extension
|
|
166
166
|
base_path = file_path.sub(/\.md\z/, "")
|
|
167
|
-
disk_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
|
|
192
|
+
# Strip .md extension
|
|
193
193
|
base_path = file_path.sub(/\.md\z/, "")
|
|
194
|
-
disk_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
|
|
233
|
+
# Strip .md extension
|
|
234
234
|
base_path = file_path.sub(/\.md\z/, "")
|
|
235
|
-
disk_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 =
|
|
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 =
|
|
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
|
|
9
|
-
# "read-before-edit" rule that ensures agents
|
|
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
|
|
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
|
-
# @
|
|
22
|
-
|
|
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] ||=
|
|
25
|
-
|
|
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
|
|
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
|
-
# @
|
|
34
|
-
|
|
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]
|
|
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
|
|
17
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
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.
|
|
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
|
|
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
|
data/lib/swarm_memory/version.rb
CHANGED
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
|
-
|
|
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
|
|