claude_swarm 1.0.10 → 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} +3 -0
- data/CLAUDE.md +0 -1
- data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +12 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +139 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +249 -1
- data/docs/v2/README.md +15 -5
- data/docs/v2/guides/complete-tutorial.md +93 -7
- data/docs/v2/guides/getting-started.md +3 -1
- data/docs/v2/guides/memory-adapters.md +41 -0
- data/docs/v2/guides/{migrating-to-2.3.md → migrating-to-2.x.md} +213 -8
- data/docs/v2/guides/plugins.md +52 -5
- data/docs/v2/guides/rails-integration.md +6 -0
- data/docs/v2/guides/swarm-memory.md +2 -13
- 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 +26 -4
- data/docs/v2/reference/ruby-dsl.md +457 -4
- data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
- data/docs/v2/reference/yaml.md +2 -2
- data/lib/claude_swarm/mcp_generator.rb +1 -1
- data/lib/claude_swarm/orchestrator.rb +8 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/version.rb +1 -1
- 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 +120 -27
- 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 +7 -5
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +4 -0
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +38 -4
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +2 -2
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +48 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +3 -3
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +1 -1
- data/lib/swarm_sdk/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +91 -12
- data/lib/swarm_sdk/config.rb +302 -0
- data/lib/swarm_sdk/configuration/parser.rb +22 -2
- data/lib/swarm_sdk/configuration.rb +13 -4
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -3
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/models.rb +43 -2
- data/lib/swarm_sdk/plugin.rb +2 -2
- data/lib/swarm_sdk/result.rb +52 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +1 -1
- data/lib/swarm_sdk/swarm/hook_triggers.rb +1 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +18 -4
- data/lib/swarm_sdk/swarm.rb +76 -13
- data/lib/swarm_sdk/tools/bash.rb +7 -9
- data/lib/swarm_sdk/tools/glob.rb +5 -5
- data/lib/swarm_sdk/tools/read.rb +8 -8
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +4 -3
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -18
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +49 -0
- data/lib/swarm_sdk/workflow/node_builder.rb +4 -2
- data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
- data/lib/swarm_sdk.rb +261 -105
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +8 -3
- data/swarm_sdk.gemspec +4 -4
- data/team_full.yml +104 -300
- metadata +9 -5
- data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
- /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
|
@@ -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
|
|
@@ -199,24 +217,36 @@ 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
|
|
226
|
+
# @return [Boolean] True if agent has valid memory configuration
|
|
227
|
+
def memory_configured?(agent_definition)
|
|
207
228
|
memory_config = agent_definition.plugin_config(:memory)
|
|
208
229
|
return false if memory_config.nil?
|
|
209
230
|
|
|
210
|
-
# MemoryConfig object (from DSL)
|
|
231
|
+
# MemoryConfig object (from DSL) - delegates to its enabled? method
|
|
211
232
|
return memory_config.enabled? if memory_config.respond_to?(:enabled?)
|
|
212
233
|
|
|
213
|
-
# Hash (from YAML)
|
|
214
|
-
|
|
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
|
|
215
243
|
directory = memory_config[:directory] || memory_config["directory"]
|
|
216
|
-
|
|
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
|
|
217
249
|
end
|
|
218
|
-
|
|
219
|
-
false
|
|
220
250
|
end
|
|
221
251
|
|
|
222
252
|
# Contribute to agent serialization
|
|
@@ -293,9 +323,18 @@ module SwarmMemory
|
|
|
293
323
|
|
|
294
324
|
builder.instance_eval do
|
|
295
325
|
memory do
|
|
326
|
+
# Standard options
|
|
296
327
|
directory(memory_config[:directory]) if memory_config[:directory]
|
|
297
328
|
adapter(memory_config[:adapter]) if memory_config[:adapter]
|
|
298
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
|
|
299
338
|
end
|
|
300
339
|
end
|
|
301
340
|
end
|
|
@@ -336,6 +375,7 @@ module SwarmMemory
|
|
|
336
375
|
# Store storage and mode using BASE NAME
|
|
337
376
|
@storages[base_name] = storage # ← Changed from agent_name to base_name
|
|
338
377
|
@modes[base_name] = mode # ← Changed from agent_name to base_name
|
|
378
|
+
@threshold_configs[base_name] = extract_threshold_config(memory_config)
|
|
339
379
|
|
|
340
380
|
# Get mode-specific tools
|
|
341
381
|
allowed_tools = tools_for_mode(mode)
|
|
@@ -384,20 +424,27 @@ module SwarmMemory
|
|
|
384
424
|
# V7.0: Extract base name for storage lookup (delegation instances share storage)
|
|
385
425
|
base_name = agent_name.to_s.split("@").first.to_sym
|
|
386
426
|
storage = @storages[base_name] # ← Changed from agent_name to base_name
|
|
427
|
+
config = @threshold_configs[base_name] || {}
|
|
387
428
|
|
|
388
429
|
return [] unless storage&.semantic_index
|
|
389
430
|
return [] if prompt.nil? || prompt.empty?
|
|
390
431
|
|
|
391
432
|
# Adaptive threshold based on query length
|
|
392
433
|
# Short queries use lower threshold as they have less semantic richness
|
|
393
|
-
#
|
|
434
|
+
# Fallback chain: config → ENV → default
|
|
394
435
|
word_count = prompt.split.size
|
|
395
|
-
word_cutoff =
|
|
436
|
+
word_cutoff = config[:adaptive_word_cutoff] ||
|
|
437
|
+
ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"]&.to_i ||
|
|
438
|
+
10
|
|
396
439
|
|
|
397
440
|
threshold = if word_count < word_cutoff
|
|
398
|
-
|
|
441
|
+
config[:discovery_threshold_short] ||
|
|
442
|
+
ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"]&.to_f ||
|
|
443
|
+
0.25
|
|
399
444
|
else
|
|
400
|
-
|
|
445
|
+
config[:discovery_threshold] ||
|
|
446
|
+
ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"]&.to_f ||
|
|
447
|
+
0.35
|
|
401
448
|
end
|
|
402
449
|
reminders = []
|
|
403
450
|
|
|
@@ -482,9 +529,15 @@ module SwarmMemory
|
|
|
482
529
|
}
|
|
483
530
|
end
|
|
484
531
|
|
|
485
|
-
# Get actual weights being used (
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
488
541
|
|
|
489
542
|
SwarmSDK::LogStream.emit(
|
|
490
543
|
type: "semantic_skill_search",
|
|
@@ -545,9 +598,15 @@ module SwarmMemory
|
|
|
545
598
|
}
|
|
546
599
|
end
|
|
547
600
|
|
|
548
|
-
# Get actual weights being used (
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
551
610
|
|
|
552
611
|
SwarmSDK::LogStream.emit(
|
|
553
612
|
type: "semantic_memory_search",
|
|
@@ -626,6 +685,40 @@ module SwarmMemory
|
|
|
626
685
|
|
|
627
686
|
reminder
|
|
628
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
|
|
629
722
|
end
|
|
630
723
|
end
|
|
631
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
data/lib/swarm_memory.rb
CHANGED
|
@@ -21,8 +21,8 @@ rescue LoadError
|
|
|
21
21
|
warn("Warning: informers gem not found. Semantic search will be unavailable. Run: gem install informers")
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# Load
|
|
25
|
-
require_relative "swarm_memory/
|
|
24
|
+
# Load error classes and version first (before Zeitwerk)
|
|
25
|
+
require_relative "swarm_memory/error"
|
|
26
26
|
require_relative "swarm_memory/version"
|
|
27
27
|
|
|
28
28
|
# Setup Zeitwerk loader
|
|
@@ -36,6 +36,11 @@ loader.inflector.inflect(
|
|
|
36
36
|
"dsl" => "DSL",
|
|
37
37
|
"sdk_plugin" => "SDKPlugin",
|
|
38
38
|
)
|
|
39
|
+
|
|
40
|
+
# Ignore files that are manually loaded above
|
|
41
|
+
loader.ignore("#{__dir__}/swarm_memory/error.rb")
|
|
42
|
+
loader.ignore("#{__dir__}/swarm_memory/version.rb")
|
|
43
|
+
|
|
39
44
|
loader.setup
|
|
40
45
|
|
|
41
46
|
# Explicitly load DSL components and extensions to inject into SwarmSDK
|
|
@@ -118,8 +123,6 @@ module SwarmMemory
|
|
|
118
123
|
Tools::MemoryRead.new(storage: storage, agent_name: agent_name)
|
|
119
124
|
when :MemoryEdit
|
|
120
125
|
Tools::MemoryEdit.new(storage: storage, agent_name: agent_name)
|
|
121
|
-
when :MemoryMultiEdit
|
|
122
|
-
Tools::MemoryMultiEdit.new(storage: storage, agent_name: agent_name)
|
|
123
126
|
when :MemoryDelete
|
|
124
127
|
Tools::MemoryDelete.new(storage: storage)
|
|
125
128
|
when :MemoryGlob
|
|
@@ -153,7 +156,6 @@ module SwarmMemory
|
|
|
153
156
|
Tools::MemoryWrite.new(storage: storage, agent_name: agent_name),
|
|
154
157
|
Tools::MemoryRead.new(storage: storage, agent_name: agent_name),
|
|
155
158
|
Tools::MemoryEdit.new(storage: storage, agent_name: agent_name),
|
|
156
|
-
Tools::MemoryMultiEdit.new(storage: storage, agent_name: agent_name),
|
|
157
159
|
Tools::MemoryDelete.new(storage: storage),
|
|
158
160
|
Tools::MemoryGlob.new(storage: storage),
|
|
159
161
|
Tools::MemoryGrep.new(storage: storage),
|
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -122,7 +122,7 @@ module SwarmSDK
|
|
|
122
122
|
max_concurrent_tools = definition[:max_concurrent_tools]
|
|
123
123
|
base_url = definition[:base_url]
|
|
124
124
|
api_version = definition[:api_version]
|
|
125
|
-
timeout = definition[:timeout] ||
|
|
125
|
+
timeout = definition[:timeout] || SwarmSDK.config.agent_request_timeout
|
|
126
126
|
assume_model_exists = definition[:assume_model_exists]
|
|
127
127
|
system_prompt = definition[:system_prompt]
|
|
128
128
|
parameters = definition[:parameters]
|
|
@@ -55,10 +55,14 @@ module SwarmSDK
|
|
|
55
55
|
#
|
|
56
56
|
# Registers RubyLLM callbacks to collect data and emit log events.
|
|
57
57
|
# Should only be called when LogStream.emitter is set.
|
|
58
|
+
# This method is idempotent - calling it multiple times has no effect.
|
|
58
59
|
#
|
|
59
60
|
# @return [void]
|
|
60
61
|
def setup_logging
|
|
62
|
+
return if @logging_setup
|
|
63
|
+
|
|
61
64
|
register_logging_callbacks
|
|
65
|
+
@logging_setup = true
|
|
62
66
|
end
|
|
63
67
|
|
|
64
68
|
# Extract agent name from delegation tool name
|
|
@@ -117,7 +117,7 @@ module SwarmSDK
|
|
|
117
117
|
|
|
118
118
|
# Trigger automatic compression at 60% ONLY if no custom handler
|
|
119
119
|
compression_triggered = false
|
|
120
|
-
if threshold ==
|
|
120
|
+
if threshold == SwarmSDK.config.context_compression_threshold && !has_custom_handler
|
|
121
121
|
compressed_count = apply_automatic_compression
|
|
122
122
|
compression_triggered = compressed_count > 0
|
|
123
123
|
end
|
|
@@ -47,7 +47,7 @@ module SwarmSDK
|
|
|
47
47
|
#
|
|
48
48
|
# @return [RubyLLM::Chat] Chat instance
|
|
49
49
|
def instantiate_chat(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
|
|
50
|
-
if base_url || timeout !=
|
|
50
|
+
if base_url || timeout != SwarmSDK.config.agent_request_timeout
|
|
51
51
|
instantiate_with_custom_context(
|
|
52
52
|
model_id: model_id,
|
|
53
53
|
provider_name: provider_name,
|
|
@@ -123,29 +123,63 @@ module SwarmSDK
|
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
# Configure provider-specific base URL
|
|
126
|
+
#
|
|
127
|
+
# @param config [RubyLLM::Config] RubyLLM configuration context
|
|
128
|
+
# @param provider [String] Provider name
|
|
129
|
+
# @param base_url [String] Custom base URL
|
|
130
|
+
# @raise [ConfigurationError] If API key is required but not configured
|
|
131
|
+
# @raise [ArgumentError] If provider doesn't support custom base_url
|
|
126
132
|
def configure_provider_base_url(config, provider, base_url)
|
|
127
133
|
case provider.to_s
|
|
128
134
|
when "openai", "deepseek", "perplexity", "mistral", "openrouter"
|
|
129
135
|
config.openai_api_base = base_url
|
|
130
|
-
config.openai_api_key
|
|
136
|
+
api_key = SwarmSDK.config.openai_api_key
|
|
137
|
+
|
|
138
|
+
# For local endpoints, API key is optional
|
|
139
|
+
# For cloud endpoints, require API key
|
|
140
|
+
unless api_key || local_endpoint?(base_url)
|
|
141
|
+
raise ConfigurationError,
|
|
142
|
+
"OpenAI API key required for '#{provider}' with base_url '#{base_url}'. " \
|
|
143
|
+
"Configure with: SwarmSDK.configure { |c| c.openai_api_key = '...' }"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
config.openai_api_key = api_key if api_key
|
|
131
147
|
config.openai_use_system_role = true
|
|
132
148
|
when "ollama"
|
|
133
149
|
config.ollama_api_base = base_url
|
|
150
|
+
# Ollama doesn't need an API key
|
|
134
151
|
when "gpustack"
|
|
135
152
|
config.gpustack_api_base = base_url
|
|
136
|
-
config.gpustack_api_key
|
|
153
|
+
api_key = SwarmSDK.config.gpustack_api_key
|
|
154
|
+
config.gpustack_api_key = api_key if api_key
|
|
137
155
|
else
|
|
138
156
|
raise ArgumentError, "Provider '#{provider}' doesn't support custom base_url."
|
|
139
157
|
end
|
|
140
158
|
end
|
|
141
159
|
|
|
160
|
+
# Check if a URL points to a local endpoint
|
|
161
|
+
#
|
|
162
|
+
# @param url [String] URL to check
|
|
163
|
+
# @return [Boolean] true if URL is a local endpoint
|
|
164
|
+
def local_endpoint?(url)
|
|
165
|
+
uri = URI.parse(url)
|
|
166
|
+
["localhost", "127.0.0.1", "0.0.0.0"].include?(uri.host)
|
|
167
|
+
rescue URI::InvalidURIError
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
142
171
|
# Fetch real model info for accurate context tracking
|
|
143
172
|
#
|
|
173
|
+
# Uses SwarmSDK::Models for model lookup (reads from models.json).
|
|
174
|
+
# Falls back to RubyLLM.models if not found in SwarmSDK.
|
|
175
|
+
#
|
|
144
176
|
# @param model_id [String] Model ID to lookup
|
|
145
177
|
def fetch_real_model_info(model_id)
|
|
146
178
|
@model_lookup_error = nil
|
|
147
179
|
@real_model_info = begin
|
|
148
|
-
|
|
180
|
+
# Try SwarmSDK::Models first (reads from local models.json)
|
|
181
|
+
# Returns ModelInfo object with method access (context_window, etc.)
|
|
182
|
+
SwarmSDK::Models.find(model_id) || RubyLLM.models.find(model_id)
|
|
149
183
|
rescue StandardError => e
|
|
150
184
|
suggestions = suggest_similar_models(model_id)
|
|
151
185
|
@model_lookup_error = {
|
|
@@ -74,8 +74,8 @@ module SwarmSDK
|
|
|
74
74
|
model_info = SwarmSDK::Models.find(message.model_id)
|
|
75
75
|
return zero_cost unless model_info
|
|
76
76
|
|
|
77
|
-
# Extract pricing from SwarmSDK's
|
|
78
|
-
pricing = model_info
|
|
77
|
+
# Extract pricing from SwarmSDK's ModelInfo (method access for top-level, Hash for nested)
|
|
78
|
+
pricing = model_info.pricing
|
|
79
79
|
return zero_cost unless pricing
|
|
80
80
|
|
|
81
81
|
text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
|
|
@@ -22,9 +22,6 @@ module SwarmSDK
|
|
|
22
22
|
<system-reminder>The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.</system-reminder>
|
|
23
23
|
REMINDER
|
|
24
24
|
|
|
25
|
-
# Backward compatibility alias - use Defaults module for new code
|
|
26
|
-
TODOWRITE_REMINDER_INTERVAL = Defaults::Context::TODOWRITE_REMINDER_INTERVAL
|
|
27
|
-
|
|
28
25
|
class << self
|
|
29
26
|
# Check if this is the first user message in the conversation
|
|
30
27
|
#
|
|
@@ -106,15 +103,16 @@ module SwarmSDK
|
|
|
106
103
|
end
|
|
107
104
|
|
|
108
105
|
# Check if enough messages have passed since last TodoWrite
|
|
106
|
+
reminder_interval = SwarmSDK.config.todowrite_reminder_interval
|
|
109
107
|
if last_todo_index.nil? && last_todowrite_index.nil?
|
|
110
108
|
# Never used TodoWrite - check if we've exceeded interval
|
|
111
|
-
chat.message_count >=
|
|
109
|
+
chat.message_count >= reminder_interval
|
|
112
110
|
elsif last_todo_index
|
|
113
111
|
# Recently used - don't remind
|
|
114
112
|
false
|
|
115
113
|
elsif last_todowrite_index
|
|
116
114
|
# Used before - check if interval has passed
|
|
117
|
-
chat.message_count - last_todowrite_index >=
|
|
115
|
+
chat.message_count - last_todowrite_index >= reminder_interval
|
|
118
116
|
else
|
|
119
117
|
false
|
|
120
118
|
end
|
|
@@ -84,6 +84,35 @@ module SwarmSDK
|
|
|
84
84
|
limit - cumulative_total_tokens
|
|
85
85
|
end
|
|
86
86
|
|
|
87
|
+
# Calculate cumulative input cost based on tokens and model pricing
|
|
88
|
+
#
|
|
89
|
+
# @return [Float] Total input cost in dollars
|
|
90
|
+
def cumulative_input_cost
|
|
91
|
+
pricing = model_pricing
|
|
92
|
+
return 0.0 unless pricing
|
|
93
|
+
|
|
94
|
+
input_price = pricing["input_per_million"] || pricing[:input_per_million] || 0.0
|
|
95
|
+
(cumulative_input_tokens / 1_000_000.0) * input_price
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Calculate cumulative output cost based on tokens and model pricing
|
|
99
|
+
#
|
|
100
|
+
# @return [Float] Total output cost in dollars
|
|
101
|
+
def cumulative_output_cost
|
|
102
|
+
pricing = model_pricing
|
|
103
|
+
return 0.0 unless pricing
|
|
104
|
+
|
|
105
|
+
output_price = pricing["output_per_million"] || pricing[:output_per_million] || 0.0
|
|
106
|
+
(cumulative_output_tokens / 1_000_000.0) * output_price
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Calculate cumulative total cost (input + output)
|
|
110
|
+
#
|
|
111
|
+
# @return [Float] Total cost in dollars
|
|
112
|
+
def cumulative_total_cost
|
|
113
|
+
cumulative_input_cost + cumulative_output_cost
|
|
114
|
+
end
|
|
115
|
+
|
|
87
116
|
# Compact the conversation history to reduce token usage
|
|
88
117
|
#
|
|
89
118
|
# @param options [Hash] Compression options
|
|
@@ -92,6 +121,25 @@ module SwarmSDK
|
|
|
92
121
|
compactor = ContextCompactor.new(self, options)
|
|
93
122
|
compactor.compact
|
|
94
123
|
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
# Get pricing info for the current model
|
|
128
|
+
#
|
|
129
|
+
# Extracts standard text token pricing from model info.
|
|
130
|
+
#
|
|
131
|
+
# @return [Hash, nil] Pricing hash with input_per_million and output_per_million
|
|
132
|
+
def model_pricing
|
|
133
|
+
return unless @real_model_info&.pricing
|
|
134
|
+
|
|
135
|
+
pricing = @real_model_info.pricing
|
|
136
|
+
text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
|
|
137
|
+
return unless text_pricing
|
|
138
|
+
|
|
139
|
+
text_pricing["standard"] || text_pricing[:standard]
|
|
140
|
+
rescue StandardError
|
|
141
|
+
nil
|
|
142
|
+
end
|
|
95
143
|
end
|
|
96
144
|
end
|
|
97
145
|
end
|
|
@@ -30,8 +30,7 @@ module SwarmSDK
|
|
|
30
30
|
# 60% triggers automatic compression, 80%/90% are informational warnings
|
|
31
31
|
CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
COMPRESSION_THRESHOLD = Defaults::Context::COMPRESSION_THRESHOLD_PERCENT
|
|
33
|
+
# NOTE: Compression threshold now accessed via SwarmSDK.config.context_compression_threshold
|
|
35
34
|
|
|
36
35
|
attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit, :swarm_id, :parent_swarm_id
|
|
37
36
|
|
|
@@ -67,14 +67,14 @@ module SwarmSDK
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
@description = config[:description]
|
|
70
|
-
@model = config[:model] ||
|
|
71
|
-
@provider = config[:provider] ||
|
|
70
|
+
@model = config[:model] || SwarmSDK.config.default_model
|
|
71
|
+
@provider = config[:provider] || SwarmSDK.config.default_provider
|
|
72
72
|
@base_url = config[:base_url]
|
|
73
73
|
@api_version = config[:api_version]
|
|
74
74
|
@context_window = config[:context_window] # Explicit context window override
|
|
75
75
|
@parameters = config[:parameters] || {}
|
|
76
76
|
@headers = Utils.stringify_keys(config[:headers] || {})
|
|
77
|
-
@timeout = config[:timeout] ||
|
|
77
|
+
@timeout = config[:timeout] || SwarmSDK.config.agent_request_timeout
|
|
78
78
|
@bypass_permissions = config[:bypass_permissions] || false
|
|
79
79
|
@max_concurrent_tools = config[:max_concurrent_tools]
|
|
80
80
|
# Always assume model exists - SwarmSDK validates models separately using models.json
|
|
@@ -148,7 +148,7 @@ module SwarmSDK
|
|
|
148
148
|
contributions = []
|
|
149
149
|
|
|
150
150
|
PluginRegistry.all.each do |plugin|
|
|
151
|
-
next unless plugin.
|
|
151
|
+
next unless plugin.memory_configured?(@definition)
|
|
152
152
|
|
|
153
153
|
contribution = plugin.system_prompt_contribution(agent_definition: @definition, storage: nil)
|
|
154
154
|
contributions << contribution if contribution && !contribution.strip.empty?
|