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.
@@ -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 each entry
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 format_files_with_matches(paths, pattern)
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 '#{pattern}'"
194
+ return "No matches found for pattern #{search_desc}"
161
195
  end
162
196
 
163
197
  result = []
164
- result << "Memory entries matching '#{pattern}' (#{paths.size} #{paths.size == 1 ? "entry" : "entries"}):"
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 '#{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 '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} #{total_matches == 1 ? "match" : "matches"}):"
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 '#{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 '#{pattern}' (#{results.size} #{results.size == 1 ? "entry" : "entries"}, #{total_matches} total #{total_matches == 1 ? "match" : "matches"}):"
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 = {}
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.1"
5
5
  end
@@ -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(thoughts:)
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.0.6"
4
+ VERSION = "2.1.1"
5
5
  end
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
- module SwarmSDK
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.0.0
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: 2025-10-26 00:00:00.000000000 Z
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.0'
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.0'
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.2
310
+ rubygems_version: 3.6.9
311
311
  specification_version: 4
312
312
  summary: Persistent memory system for SwarmSDK agents
313
313
  test_files: []