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.
- checksums.yaml +4 -4
- data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
- data/CLAUDE.md +346 -191
- data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
- data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
- data/docs/v2/README.md +20 -5
- data/docs/v2/guides/complete-tutorial.md +95 -9
- data/docs/v2/guides/getting-started.md +10 -8
- data/docs/v2/guides/memory-adapters.md +41 -0
- data/docs/v2/guides/migrating-to-2.x.md +746 -0
- data/docs/v2/guides/plugins.md +52 -5
- data/docs/v2/guides/rails-integration.md +6 -0
- data/docs/v2/guides/snapshots.md +14 -14
- data/docs/v2/guides/swarm-memory.md +2 -13
- data/docs/v2/reference/architecture-flow.md +3 -3
- data/docs/v2/reference/cli.md +0 -1
- data/docs/v2/reference/configuration_reference.md +300 -0
- data/docs/v2/reference/event_payload_structures.md +27 -5
- data/docs/v2/reference/ruby-dsl.md +614 -18
- data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
- data/docs/v2/reference/yaml.md +172 -54
- data/examples/snapshot_demo.rb +2 -2
- data/lib/claude_swarm/mcp_generator.rb +8 -21
- data/lib/claude_swarm/orchestrator.rb +8 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +11 -11
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
- data/lib/swarm_cli/interactive_repl.rb +2 -2
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/semantic_index.rb +10 -2
- data/lib/swarm_memory/core/storage.rb +7 -2
- data/lib/swarm_memory/dsl/memory_config.rb +37 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
- data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
- data/lib/swarm_memory/tools/load_skill.rb +0 -1
- data/lib/swarm_memory/tools/memory_edit.rb +2 -1
- data/lib/swarm_memory/tools/memory_read.rb +1 -1
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +8 -6
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1061
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +488 -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/config.rb +302 -0
- data/lib/swarm_sdk/configuration/parser.rb +373 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +77 -546
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- 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/custom_tool_registry.rb +226 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/models.rb +43 -2
- 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 +95 -5
- data/lib/swarm_sdk/result.rb +52 -0
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
- data/lib/swarm_sdk/swarm.rb +203 -683
- data/lib/swarm_sdk/tools/bash.rb +14 -8
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +12 -4
- 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 +16 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +192 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +294 -108
- data/rubocop/cop/security/no_reflection_methods.rb +1 -1
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +8 -3
- data/swarm_sdk.gemspec +6 -4
- data/team_full.yml +124 -320
- metadata +42 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
- /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
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
|
@@ -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)
|
|
@@ -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
|
-
#
|
|
193
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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|
|
|
115
|
-
|
|
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
|
-
#
|
|
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
|
|
140
|
-
Core::Storage.new(
|
|
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
|
|
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
|
|
207
|
-
agent_definition.
|
|
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
|
|
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
|
-
|
|
260
|
+
memory_config = agent_definition.plugin_config(:memory)
|
|
261
|
+
return {} unless memory_config
|
|
219
262
|
|
|
220
|
-
{ 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.
|
|
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
|
-
#
|
|
434
|
+
# Fallback chain: config → ENV → default
|
|
314
435
|
word_count = prompt.split.size
|
|
315
|
-
word_cutoff =
|
|
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
|
-
|
|
441
|
+
config[:discovery_threshold_short] ||
|
|
442
|
+
ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"]&.to_f ||
|
|
443
|
+
0.25
|
|
319
444
|
else
|
|
320
|
-
|
|
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 (
|
|
406
|
-
|
|
407
|
-
|
|
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 (
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
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
|
|
@@ -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
|
|
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
|
data/lib/swarm_memory/version.rb
CHANGED