swarm_memory 2.0.0 → 2.1.1
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.rb +4 -2
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_cli.rb +1 -2
- data/lib/swarm_memory/adapters/base.rb +2 -1
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +102 -46
- data/lib/swarm_memory/core/storage.rb +3 -1
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +47 -5
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +203 -123
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +2 -0
- data/lib/swarm_memory/search/text_search.rb +3 -1
- data/lib/swarm_memory/tools/memory_glob.rb +25 -10
- data/lib/swarm_memory/tools/memory_grep.rb +59 -21
- data/lib/swarm_memory/tools/memory_write.rb +16 -0
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_sdk/agent/chat.rb +15 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +4 -1
- data/lib/swarm_sdk/tools/think.rb +3 -3
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +1 -11
- metadata +5 -5
|
@@ -19,6 +19,7 @@ module SwarmMemory
|
|
|
19
19
|
- pattern (REQUIRED): Regular expression pattern to search for (e.g., 'status: pending', 'TODO.*urgent', '\\btask_\\d+\\b')
|
|
20
20
|
|
|
21
21
|
**Optional Parameters:**
|
|
22
|
+
- path: Limit search to specific path (e.g., 'concept/', 'fact/api-design/', 'skill/ruby')
|
|
22
23
|
- case_insensitive: Set to true for case-insensitive search (default: false)
|
|
23
24
|
- output_mode: Choose output format - 'files_with_matches' (default), 'content', or 'count'
|
|
24
25
|
|
|
@@ -44,22 +45,38 @@ module SwarmMemory
|
|
|
44
45
|
- Quantifiers: '*' (0+), '+' (1+), '?' (0 or 1), '{3}' (exactly 3)
|
|
45
46
|
- Alternation: 'pending|in-progress|blocked'
|
|
46
47
|
|
|
48
|
+
**Path Parameter - Directory-Style Filtering:**
|
|
49
|
+
The path parameter works just like searching in directories:
|
|
50
|
+
- 'concept/' - Search only concept entries
|
|
51
|
+
- 'fact/api-design' - Search only in fact/api-design (treats as directory)
|
|
52
|
+
- 'fact/api-design/' - Same as above
|
|
53
|
+
- 'skill/ruby/blocks.md' - Search only that specific file
|
|
54
|
+
|
|
47
55
|
**Examples:**
|
|
48
56
|
```
|
|
49
57
|
# Find entries containing "TODO" (case-sensitive)
|
|
50
58
|
MemoryGrep(pattern: "TODO")
|
|
51
59
|
|
|
60
|
+
# Search only in concepts
|
|
61
|
+
MemoryGrep(pattern: "TODO", path: "concept/")
|
|
62
|
+
|
|
63
|
+
# Search in a specific subdirectory
|
|
64
|
+
MemoryGrep(pattern: "endpoint", path: "fact/api-design")
|
|
65
|
+
|
|
66
|
+
# Search a specific file
|
|
67
|
+
MemoryGrep(pattern: "lambda", path: "skill/ruby/blocks.md")
|
|
68
|
+
|
|
52
69
|
# Find entries with any status (case-insensitive)
|
|
53
70
|
MemoryGrep(pattern: "status:", case_insensitive: true)
|
|
54
71
|
|
|
55
|
-
# Show actual content of matches
|
|
56
|
-
MemoryGrep(pattern: "error|warning|failed", output_mode: "content")
|
|
72
|
+
# Show actual content of matches in skills only
|
|
73
|
+
MemoryGrep(pattern: "error|warning|failed", path: "skill/", output_mode: "content")
|
|
57
74
|
|
|
58
|
-
# Count how many times "completed" appears in
|
|
59
|
-
MemoryGrep(pattern: "completed", output_mode: "count")
|
|
75
|
+
# Count how many times "completed" appears in experiences
|
|
76
|
+
MemoryGrep(pattern: "completed", path: "experience/", output_mode: "count")
|
|
60
77
|
|
|
61
|
-
# Find task numbers
|
|
62
|
-
MemoryGrep(pattern: "task_\\d+")
|
|
78
|
+
# Find task numbers in facts
|
|
79
|
+
MemoryGrep(pattern: "task_\\d+", path: "fact/")
|
|
63
80
|
|
|
64
81
|
# Find incomplete tasks
|
|
65
82
|
MemoryGrep(pattern: "^- \\[ \\]", output_mode: "content")
|
|
@@ -84,6 +101,7 @@ module SwarmMemory
|
|
|
84
101
|
**Tips:**
|
|
85
102
|
- Start with simple literal patterns before using complex regex
|
|
86
103
|
- Use case_insensitive=true for broader matches
|
|
104
|
+
- Use path parameter to limit search scope (faster and more precise)
|
|
87
105
|
- Use output_mode="content" to see context around matches
|
|
88
106
|
- Escape special regex characters with backslash: \\. \\* \\? \\[ \\]
|
|
89
107
|
- Test patterns on a small set before broad searches
|
|
@@ -94,6 +112,10 @@ module SwarmMemory
|
|
|
94
112
|
desc: "Regular expression pattern to search for",
|
|
95
113
|
required: true
|
|
96
114
|
|
|
115
|
+
param :path,
|
|
116
|
+
desc: "Limit search to specific path (e.g., 'concept/', 'fact/api-design/', 'skill/ruby/blocks.md')",
|
|
117
|
+
required: false
|
|
118
|
+
|
|
97
119
|
param :case_insensitive,
|
|
98
120
|
type: "boolean",
|
|
99
121
|
desc: "Set to true for case-insensitive search (default: false)",
|
|
@@ -119,17 +141,19 @@ module SwarmMemory
|
|
|
119
141
|
# Execute the tool
|
|
120
142
|
#
|
|
121
143
|
# @param pattern [String] Regex pattern to search for
|
|
144
|
+
# @param path [String, nil] Optional path filter
|
|
122
145
|
# @param case_insensitive [Boolean] Whether to perform case-insensitive search
|
|
123
146
|
# @param output_mode [String] Output mode
|
|
124
147
|
# @return [String] Formatted search results
|
|
125
|
-
def execute(pattern:, case_insensitive: false, output_mode: "files_with_matches")
|
|
148
|
+
def execute(pattern:, path: nil, case_insensitive: false, output_mode: "files_with_matches")
|
|
126
149
|
results = @storage.grep(
|
|
127
150
|
pattern: pattern,
|
|
151
|
+
path: path,
|
|
128
152
|
case_insensitive: case_insensitive,
|
|
129
153
|
output_mode: output_mode,
|
|
130
154
|
)
|
|
131
155
|
|
|
132
|
-
format_results(results, pattern, output_mode)
|
|
156
|
+
format_results(results, pattern, output_mode, path)
|
|
133
157
|
rescue ArgumentError => e
|
|
134
158
|
validation_error(e.message)
|
|
135
159
|
rescue RegexpError => e
|
|
@@ -142,40 +166,52 @@ module SwarmMemory
|
|
|
142
166
|
"<tool_use_error>InputValidationError: #{message}</tool_use_error>"
|
|
143
167
|
end
|
|
144
168
|
|
|
145
|
-
def format_results(results, pattern, output_mode)
|
|
169
|
+
def format_results(results, pattern, output_mode, path_filter)
|
|
146
170
|
case output_mode
|
|
147
171
|
when "files_with_matches"
|
|
148
|
-
format_files_with_matches(results, pattern)
|
|
172
|
+
format_files_with_matches(results, pattern, path_filter)
|
|
149
173
|
when "content"
|
|
150
|
-
format_content(results, pattern)
|
|
174
|
+
format_content(results, pattern, path_filter)
|
|
151
175
|
when "count"
|
|
152
|
-
format_count(results, pattern)
|
|
176
|
+
format_count(results, pattern, path_filter)
|
|
153
177
|
else
|
|
154
178
|
validation_error("Invalid output_mode: #{output_mode}")
|
|
155
179
|
end
|
|
156
180
|
end
|
|
157
181
|
|
|
158
|
-
def
|
|
182
|
+
def format_search_header(pattern, path_filter)
|
|
183
|
+
if path_filter && !path_filter.empty?
|
|
184
|
+
"'#{pattern}' in #{path_filter}"
|
|
185
|
+
else
|
|
186
|
+
"'#{pattern}'"
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format_files_with_matches(paths, pattern, path_filter)
|
|
191
|
+
search_desc = format_search_header(pattern, path_filter)
|
|
192
|
+
|
|
159
193
|
if paths.empty?
|
|
160
|
-
return "No matches found for pattern
|
|
194
|
+
return "No matches found for pattern #{search_desc}"
|
|
161
195
|
end
|
|
162
196
|
|
|
163
197
|
result = []
|
|
164
|
-
result << "Memory entries matching
|
|
198
|
+
result << "Memory entries matching #{search_desc} (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
|
|
165
199
|
paths.each do |path|
|
|
166
200
|
result << " memory://#{path}"
|
|
167
201
|
end
|
|
168
202
|
result.join("\n")
|
|
169
203
|
end
|
|
170
204
|
|
|
171
|
-
def format_content(results, pattern)
|
|
205
|
+
def format_content(results, pattern, path_filter)
|
|
206
|
+
search_desc = format_search_header(pattern, path_filter)
|
|
207
|
+
|
|
172
208
|
if results.empty?
|
|
173
|
-
return "No matches found for pattern
|
|
209
|
+
return "No matches found for pattern #{search_desc}"
|
|
174
210
|
end
|
|
175
211
|
|
|
176
212
|
total_matches = results.sum { |r| r[:matches].size }
|
|
177
213
|
output = []
|
|
178
|
-
output << "Memory entries matching
|
|
214
|
+
output << "Memory entries matching #{search_desc} (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
|
|
179
215
|
output << ""
|
|
180
216
|
|
|
181
217
|
results.each do |result|
|
|
@@ -189,14 +225,16 @@ module SwarmMemory
|
|
|
189
225
|
output.join("\n").rstrip
|
|
190
226
|
end
|
|
191
227
|
|
|
192
|
-
def format_count(results, pattern)
|
|
228
|
+
def format_count(results, pattern, path_filter)
|
|
229
|
+
search_desc = format_search_header(pattern, path_filter)
|
|
230
|
+
|
|
193
231
|
if results.empty?
|
|
194
|
-
return "No matches found for pattern
|
|
232
|
+
return "No matches found for pattern #{search_desc}"
|
|
195
233
|
end
|
|
196
234
|
|
|
197
235
|
total_matches = results.sum { |r| r[:count] }
|
|
198
236
|
output = []
|
|
199
|
-
output << "Memory entries matching
|
|
237
|
+
output << "Memory entries matching #{search_desc} (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
|
|
200
238
|
|
|
201
239
|
results.each do |result|
|
|
202
240
|
output << " memory://#{result[:path]}: #{result[:count]} #{result[:count] == 1 ? "match" : "matches"}"
|
|
@@ -10,6 +10,10 @@ module SwarmMemory
|
|
|
10
10
|
description <<~DESC
|
|
11
11
|
Store content in persistent memory with structured metadata for semantic search and retrieval.
|
|
12
12
|
|
|
13
|
+
IMPORTANT: Content must be 250 words or less. If content exceeds this limit, extract key entities: concepts, experiences, facts, skills,
|
|
14
|
+
then split into multiple focused memories (each under 250 words) that capture ALL important details.
|
|
15
|
+
Link related memories using the 'related' metadata field with memory:// URIs.
|
|
16
|
+
|
|
13
17
|
CRITICAL: ALL 8 required parameters MUST be provided. Do NOT skip any. If you're missing information, ask the user or infer reasonable defaults.
|
|
14
18
|
|
|
15
19
|
REQUIRED PARAMETERS (provide ALL 8):
|
|
@@ -136,6 +140,18 @@ module SwarmMemory
|
|
|
136
140
|
tools: nil,
|
|
137
141
|
permissions: nil
|
|
138
142
|
)
|
|
143
|
+
# Validate content length (250 word limit)
|
|
144
|
+
word_count = content.split(/\s+/).size
|
|
145
|
+
if word_count > 250
|
|
146
|
+
return validation_error(
|
|
147
|
+
"Content exceeds 250-word limit (#{word_count} words). " \
|
|
148
|
+
"Please extract the key entities and concepts from this content, then split it into multiple smaller, " \
|
|
149
|
+
"focused memories (each under 250 words) that still capture ALL the important details. " \
|
|
150
|
+
"Link related memories together using the 'related' metadata field with memory:// URIs. " \
|
|
151
|
+
"Each memory should cover one specific aspect or concept while preserving completeness.",
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
|
|
139
155
|
# Build metadata hash from params
|
|
140
156
|
# Handle both JSON strings (from LLMs) and Ruby arrays (from tests/code)
|
|
141
157
|
metadata = {}
|
data/lib/swarm_memory/version.rb
CHANGED
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -413,6 +413,18 @@ module SwarmSDK
|
|
|
413
413
|
)
|
|
414
414
|
end
|
|
415
415
|
|
|
416
|
+
# Handle nil response from provider (malformed API response)
|
|
417
|
+
if response.nil?
|
|
418
|
+
raise RubyLLM::Error, "Provider returned nil response. This usually indicates a malformed API response " \
|
|
419
|
+
"that couldn't be parsed.\n\n" \
|
|
420
|
+
"Provider: #{@provider.class.name}\n" \
|
|
421
|
+
"API Base: #{@provider.api_base}\n" \
|
|
422
|
+
"Model: #{@model.id}\n" \
|
|
423
|
+
"Response: #{response.inspect}\n\n" \
|
|
424
|
+
"The API endpoint returned a response that couldn't be parsed into a valid Message object. " \
|
|
425
|
+
"Enable RubyLLM debug logging (RubyLLM.logger.level = Logger::DEBUG) to see the raw API response."
|
|
426
|
+
end
|
|
427
|
+
|
|
416
428
|
@on[:new_message]&.call unless block
|
|
417
429
|
|
|
418
430
|
# Handle schema parsing if needed
|
|
@@ -834,6 +846,9 @@ module SwarmSDK
|
|
|
834
846
|
when "openai", "deepseek", "perplexity", "mistral", "openrouter"
|
|
835
847
|
config.openai_api_base = base_url
|
|
836
848
|
config.openai_api_key = ENV["OPENAI_API_KEY"] || "dummy-key-for-local"
|
|
849
|
+
# Use standard 'system' role instead of 'developer' for OpenAI-compatible proxies
|
|
850
|
+
# Most proxies don't support OpenAI's newer 'developer' role convention
|
|
851
|
+
config.openai_use_system_role = true
|
|
837
852
|
when "ollama"
|
|
838
853
|
config.ollama_api_base = base_url
|
|
839
854
|
when "gpustack"
|
|
@@ -298,7 +298,7 @@ module SwarmSDK
|
|
|
298
298
|
# Check if a tool should be disabled based on disable_default_tools config
|
|
299
299
|
#
|
|
300
300
|
# @param tool_name [Symbol] Tool name to check
|
|
301
|
-
# @param disable_config [nil, Boolean, Array<Symbol>] Disable configuration
|
|
301
|
+
# @param disable_config [nil, Boolean, Symbol, Array<Symbol>] Disable configuration
|
|
302
302
|
# @return [Boolean] True if tool should be disabled
|
|
303
303
|
def tool_disabled?(tool_name, disable_config)
|
|
304
304
|
return false if disable_config.nil?
|
|
@@ -306,6 +306,9 @@ module SwarmSDK
|
|
|
306
306
|
if disable_config == true
|
|
307
307
|
# Disable all default tools
|
|
308
308
|
true
|
|
309
|
+
elsif disable_config.is_a?(Symbol)
|
|
310
|
+
# Single tool name
|
|
311
|
+
disable_config == tool_name
|
|
309
312
|
elsif disable_config.is_a?(Array)
|
|
310
313
|
# Disable only tools in the array
|
|
311
314
|
disable_config.include?(tool_name)
|
|
@@ -72,6 +72,8 @@ module SwarmSDK
|
|
|
72
72
|
back to earlier thinking. Use clear formatting and organization to make it easy to reference
|
|
73
73
|
later. Don't hesitate to think out loud - this tool is designed to augment your cognitive capabilities and help
|
|
74
74
|
you deliver better solutions.
|
|
75
|
+
|
|
76
|
+
**CRITICAL:** The Think tool takes only one parameter: thoughts. Do not include any other parameters.
|
|
75
77
|
DESC
|
|
76
78
|
|
|
77
79
|
param :thoughts,
|
|
@@ -79,9 +81,7 @@ module SwarmSDK
|
|
|
79
81
|
desc: "Your thoughts, plans, calculations, or any notes you want to record",
|
|
80
82
|
required: true
|
|
81
83
|
|
|
82
|
-
def execute(
|
|
83
|
-
return validation_error("thoughts are required") if thoughts.nil? || thoughts.empty?
|
|
84
|
-
|
|
84
|
+
def execute(**kwargs)
|
|
85
85
|
"Thought noted."
|
|
86
86
|
end
|
|
87
87
|
|
data/lib/swarm_sdk/version.rb
CHANGED
data/lib/swarm_sdk.rb
CHANGED
|
@@ -17,8 +17,7 @@ require "async/semaphore"
|
|
|
17
17
|
require "ruby_llm"
|
|
18
18
|
require "ruby_llm/mcp"
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
end
|
|
20
|
+
require_relative "swarm_sdk/version"
|
|
22
21
|
|
|
23
22
|
require "zeitwerk"
|
|
24
23
|
loader = Zeitwerk::Loader.new
|
|
@@ -147,15 +146,6 @@ require "ruby_llm/mcp/parameter"
|
|
|
147
146
|
|
|
148
147
|
module RubyLLM
|
|
149
148
|
module MCP
|
|
150
|
-
class Parameter < RubyLLM::Parameter
|
|
151
|
-
def initialize(name, type: "string", desc: nil, required: true, default: nil, union_type: nil)
|
|
152
|
-
super(name, type: type, desc: desc, required: required)
|
|
153
|
-
@properties = {}
|
|
154
|
-
@union_type = union_type
|
|
155
|
-
@default = default
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
149
|
module Notifications
|
|
160
150
|
class Initialize
|
|
161
151
|
def call
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: swarm_memory
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Arruda
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: async
|
|
@@ -71,14 +71,14 @@ dependencies:
|
|
|
71
71
|
requirements:
|
|
72
72
|
- - "~>"
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '2.
|
|
74
|
+
version: '2.1'
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '2.
|
|
81
|
+
version: '2.1'
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: zeitwerk
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -307,7 +307,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
307
307
|
- !ruby/object:Gem::Version
|
|
308
308
|
version: '0'
|
|
309
309
|
requirements: []
|
|
310
|
-
rubygems_version: 3.6.
|
|
310
|
+
rubygems_version: 3.6.9
|
|
311
311
|
specification_version: 4
|
|
312
312
|
summary: Persistent memory system for SwarmSDK agents
|
|
313
313
|
test_files: []
|