swarm_memory 2.1.2 → 2.1.3

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/configuration.rb +28 -4
  3. data/lib/claude_swarm/mcp_generator.rb +4 -10
  4. data/lib/claude_swarm/version.rb +1 -1
  5. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  6. data/lib/swarm_cli/config_loader.rb +3 -3
  7. data/lib/swarm_cli/version.rb +1 -1
  8. data/lib/swarm_memory/adapters/base.rb +4 -4
  9. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  10. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  11. data/lib/swarm_memory/integration/sdk_plugin.rb +11 -5
  12. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  13. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  14. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  15. data/lib/swarm_memory/version.rb +1 -1
  16. data/lib/swarm_memory.rb +5 -0
  17. data/lib/swarm_sdk/agent/builder.rb +33 -0
  18. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  19. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  20. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  21. data/lib/swarm_sdk/agent/chat.rb +198 -51
  22. data/lib/swarm_sdk/agent/context.rb +6 -2
  23. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  24. data/lib/swarm_sdk/agent/definition.rb +15 -22
  25. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  26. data/lib/swarm_sdk/configuration.rb +420 -103
  27. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  28. data/lib/swarm_sdk/log_collector.rb +31 -5
  29. data/lib/swarm_sdk/log_stream.rb +37 -8
  30. data/lib/swarm_sdk/model_aliases.json +4 -1
  31. data/lib/swarm_sdk/node/agent_config.rb +33 -8
  32. data/lib/swarm_sdk/node/builder.rb +39 -18
  33. data/lib/swarm_sdk/node_orchestrator.rb +293 -26
  34. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  35. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  36. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  37. data/lib/swarm_sdk/restore_result.rb +65 -0
  38. data/lib/swarm_sdk/snapshot.rb +156 -0
  39. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  40. data/lib/swarm_sdk/state_restorer.rb +491 -0
  41. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  42. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  43. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  44. data/lib/swarm_sdk/swarm/builder.rb +208 -12
  45. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  46. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  47. data/lib/swarm_sdk/swarm.rb +367 -90
  48. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  49. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  50. data/lib/swarm_sdk/tools/delegate.rb +92 -7
  51. data/lib/swarm_sdk/tools/read.rb +17 -5
  52. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  53. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  54. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  55. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  56. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  57. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  58. data/lib/swarm_sdk/tools/think.rb +4 -1
  59. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  60. data/lib/swarm_sdk/utils.rb +18 -0
  61. data/lib/swarm_sdk/validation_result.rb +33 -0
  62. data/lib/swarm_sdk/version.rb +1 -1
  63. data/lib/swarm_sdk.rb +362 -21
  64. metadata +17 -5
@@ -218,6 +218,51 @@ module SwarmSDK
218
218
  def size
219
219
  @entries.size
220
220
  end
221
+
222
+ # Get all entries with content for snapshot
223
+ #
224
+ # Thread-safe method that returns a copy of all entries.
225
+ # Used by snapshot/restore functionality.
226
+ #
227
+ # @return [Hash] { path => Entry }
228
+ def all_entries
229
+ @mutex.synchronize do
230
+ @entries.dup
231
+ end
232
+ end
233
+
234
+ # Restore entries from snapshot
235
+ #
236
+ # Restores entries directly without using write() to preserve timestamps.
237
+ # This ensures entry ordering and metadata accuracy after restore.
238
+ #
239
+ # @param entries_data [Hash] { path => { content:, title:, updated_at:, size: } }
240
+ # @return [void]
241
+ def restore_entries(entries_data)
242
+ @mutex.synchronize do
243
+ entries_data.each do |path, data|
244
+ # Handle both symbol and string keys from JSON
245
+ content = data[:content] || data["content"]
246
+ title = data[:title] || data["title"]
247
+ updated_at_str = data[:updated_at] || data["updated_at"]
248
+
249
+ # Parse timestamp from ISO8601 string
250
+ updated_at = Time.parse(updated_at_str)
251
+
252
+ # Create entry with preserved timestamp
253
+ entry = Entry.new(
254
+ content: content,
255
+ title: title,
256
+ updated_at: updated_at,
257
+ size: content.bytesize,
258
+ )
259
+
260
+ # Update storage
261
+ @entries[path] = entry
262
+ @total_size += entry.size
263
+ end
264
+ end
265
+ end
221
266
  end
222
267
  end
223
268
  end
@@ -14,11 +14,11 @@ module SwarmSDK
14
14
  # - Search capabilities: Glob patterns and grep-style content search
15
15
  # - Thread-safe: Mutex-protected operations
16
16
  class Storage
17
- # Maximum size per entry (1MB)
18
- MAX_ENTRY_SIZE = 1_000_000
17
+ # Maximum size per entry (3MB)
18
+ MAX_ENTRY_SIZE = 3_000_000
19
19
 
20
- # Maximum total storage size (100MB)
21
- MAX_TOTAL_SIZE = 100_000_000
20
+ # Maximum total storage size (100GB)
21
+ MAX_TOTAL_SIZE = 100_000_000_000
22
22
 
23
23
  # Represents a single storage entry with metadata
24
24
  Entry = Struct.new(:content, :title, :updated_at, :size, keyword_init: true)
@@ -82,7 +82,10 @@ module SwarmSDK
82
82
  required: true
83
83
 
84
84
  def execute(**kwargs)
85
- "Thought noted."
85
+ <<~RESP
86
+ Thought noted.
87
+ RESP
88
+ # <system-reminder>The user cannot see your thoughts. You MUST NOT stop without giving the user a response.</system-reminder>
86
89
  end
87
90
 
88
91
  private
@@ -8,17 +8,17 @@ module SwarmSDK
8
8
  # Each agent maintains its own independent todo list.
9
9
  class TodoWrite < RubyLLM::Tool
10
10
  description <<~DESC
11
- Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
11
+ Use this tool to create and manage a structured task list for your current work session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
12
12
  It also helps the user understand the progress of the task and overall progress of their requests.
13
13
 
14
14
  ## When to Use This Tool
15
15
  Use this tool proactively in these scenarios:
16
16
 
17
17
  **CRITICAL**: Follow this workflow for multi-step tasks:
18
- 1. FIRST: Analyze the task scope (search files, read code, understand requirements)
19
- 2. SECOND: Create a COMPLETE todo list with ALL known tasks BEFORE starting implementation
18
+ 1. FIRST: Analyze the task scope (gather information, understand requirements)
19
+ 2. SECOND: Create a COMPLETE todo list with ALL known tasks BEFORE starting work
20
20
  3. THIRD: Execute tasks, marking in_progress → completed as you work
21
- 4. ONLY add new todos if unexpected work is discovered during implementation
21
+ 4. ONLY add new todos if unexpected work is discovered during execution
22
22
 
23
23
  Use the todo list when:
24
24
  1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
@@ -27,7 +27,7 @@ module SwarmSDK
27
27
  4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
28
28
  5. After receiving new instructions - After analyzing scope, create complete todo list before starting work
29
29
  6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
30
- 7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
30
+ 7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during execution
31
31
 
32
32
  ## When NOT to Use This Tool
33
33
 
@@ -73,9 +73,21 @@ module SwarmSDK
73
73
  - Create specific, actionable items
74
74
  - Break complex tasks into smaller, manageable steps
75
75
  - Use clear, descriptive task names
76
- - Always provide both forms:
77
- - content: "Fix authentication bug"
78
- - activeForm: "Fixing authentication bug"
76
+ - Always provide both forms (content and activeForm)
77
+
78
+ ## Examples
79
+
80
+ **Coding Tasks**:
81
+ - content: "Fix authentication bug in login handler"
82
+ - activeForm: "Fixing authentication bug in login handler"
83
+
84
+ **Non-Coding Tasks**:
85
+ - content: "Analyze customer feedback from Q4 survey"
86
+ - activeForm: "Analyzing customer feedback from Q4 survey"
87
+
88
+ **Research Tasks**:
89
+ - content: "Research best practices for API rate limiting"
90
+ - activeForm: "Researching best practices for API rate limiting"
79
91
 
80
92
  When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
81
93
  DESC
@@ -45,6 +45,24 @@ module SwarmSDK
45
45
  obj
46
46
  end
47
47
  end
48
+
49
+ # Convert hash to YAML string
50
+ #
51
+ # Converts a Ruby hash to a YAML string. Useful for creating inline
52
+ # swarm definitions from hash configurations.
53
+ #
54
+ # @param hash [Hash] Hash to convert
55
+ # @return [String] YAML string representation
56
+ #
57
+ # @example
58
+ # config = { version: 2, swarm: { name: "Test" } }
59
+ # Utils.hash_to_yaml(config)
60
+ # # => "---\nversion: 2\nswarm:\n name: Test\n"
61
+ def hash_to_yaml(hash)
62
+ # Convert symbols to strings for valid YAML
63
+ stringified = stringify_keys(hash)
64
+ stringified.to_yaml
65
+ end
48
66
  end
49
67
  end
50
68
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Internal result object for validation phase during snapshot restore
5
+ #
6
+ # Used during restore to track which agents can be restored and which
7
+ # need to be skipped due to configuration mismatches.
8
+ #
9
+ # @api private
10
+ class ValidationResult
11
+ attr_reader :warnings,
12
+ :skipped_agents,
13
+ :restorable_agents,
14
+ :skipped_delegations,
15
+ :restorable_delegations
16
+
17
+ # Initialize validation result
18
+ #
19
+ # @param warnings [Array<Hash>] Warning messages with details
20
+ # @param skipped_agents [Array<Symbol>] Names of agents that can't be restored
21
+ # @param restorable_agents [Array<Symbol>] Names of agents that can be restored
22
+ # @param skipped_delegations [Array<String>] Names of delegations that can't be restored
23
+ # @param restorable_delegations [Array<String>] Names of delegations that can be restored
24
+ def initialize(warnings:, skipped_agents:, restorable_agents:,
25
+ skipped_delegations:, restorable_delegations:)
26
+ @warnings = warnings
27
+ @skipped_agents = skipped_agents
28
+ @restorable_agents = restorable_agents
29
+ @skipped_delegations = skipped_delegations
30
+ @restorable_delegations = restorable_delegations
31
+ end
32
+ end
33
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.1.2"
4
+ VERSION = "2.2.0"
5
5
  end
data/lib/swarm_sdk.rb CHANGED
@@ -26,6 +26,8 @@ loader.push_dir("#{__dir__}/swarm_sdk", namespace: SwarmSDK)
26
26
  loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
27
27
  loader.inflector.inflect(
28
28
  "cli" => "CLI",
29
+ "llm_instrumentation_middleware" => "LLMInstrumentationMiddleware",
30
+ "mcp" => "MCP",
29
31
  "openai_with_responses" => "OpenAIWithResponses",
30
32
  )
31
33
  loader.setup
@@ -44,8 +46,176 @@ module SwarmSDK
44
46
  attr_accessor :settings
45
47
 
46
48
  # Main entry point for DSL
47
- def build(&block)
48
- Swarm::Builder.build(&block)
49
+ def build(allow_filesystem_tools: nil, &block)
50
+ Swarm::Builder.build(allow_filesystem_tools: allow_filesystem_tools, &block)
51
+ end
52
+
53
+ # Validate YAML configuration without creating a swarm
54
+ #
55
+ # Performs comprehensive validation of YAML configuration including:
56
+ # - YAML syntax
57
+ # - Required fields (version, swarm name, lead, agents)
58
+ # - Agent configurations (description, directory existence)
59
+ # - Circular dependencies
60
+ # - File references (agent_file paths)
61
+ # - Hook configurations
62
+ #
63
+ # @param yaml_content [String] YAML configuration content
64
+ # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
65
+ # @return [Array<Hash>] Array of error hashes (empty if valid)
66
+ #
67
+ # @example Validate YAML string
68
+ # errors = SwarmSDK.validate(yaml_content)
69
+ # if errors.empty?
70
+ # puts "Configuration is valid!"
71
+ # else
72
+ # errors.each do |error|
73
+ # puts "#{error[:field]}: #{error[:message]}"
74
+ # end
75
+ # end
76
+ #
77
+ # @example Error hash structure
78
+ # {
79
+ # type: :missing_field, # Error type
80
+ # field: "swarm.agents.backend.description", # JSON-style path to field
81
+ # message: "Agent 'backend' missing required 'description' field",
82
+ # agent: "backend" # Optional, present if error is agent-specific
83
+ # }
84
+ def validate(yaml_content, base_dir: Dir.pwd)
85
+ errors = []
86
+
87
+ begin
88
+ config = Configuration.new(yaml_content, base_dir: base_dir)
89
+ config.load_and_validate
90
+
91
+ # Build swarm to trigger DSL validation
92
+ # This catches errors from Agent::Definition, Builder, etc.
93
+ config.to_swarm
94
+ rescue ConfigurationError, CircularDependencyError => e
95
+ errors << parse_configuration_error(e)
96
+ rescue StandardError => e
97
+ errors << {
98
+ type: :unknown_error,
99
+ field: nil,
100
+ message: e.message,
101
+ }
102
+ end
103
+
104
+ errors
105
+ end
106
+
107
+ # Validate YAML configuration file
108
+ #
109
+ # Convenience method that reads the file and validates the content.
110
+ #
111
+ # @param path [String, Pathname] Path to YAML configuration file
112
+ # @return [Array<Hash>] Array of error hashes (empty if valid)
113
+ #
114
+ # @example
115
+ # errors = SwarmSDK.validate_file("config.yml")
116
+ # if errors.empty?
117
+ # puts "Valid configuration!"
118
+ # swarm = SwarmSDK.load_file("config.yml")
119
+ # else
120
+ # errors.each { |e| puts "Error: #{e[:message]}" }
121
+ # end
122
+ def validate_file(path)
123
+ path = Pathname.new(path).expand_path
124
+
125
+ unless path.exist?
126
+ return [{
127
+ type: :file_not_found,
128
+ field: nil,
129
+ message: "Configuration file not found: #{path}",
130
+ }]
131
+ end
132
+
133
+ yaml_content = File.read(path)
134
+ base_dir = path.dirname
135
+
136
+ validate(yaml_content, base_dir: base_dir)
137
+ rescue StandardError => e
138
+ [{
139
+ type: :file_read_error,
140
+ field: nil,
141
+ message: "Error reading file: #{e.message}",
142
+ }]
143
+ end
144
+
145
+ # Load swarm from YAML string
146
+ #
147
+ # This is the primary programmatic API for loading YAML configurations.
148
+ # For file-based loading, use SwarmSDK.load_file for convenience.
149
+ #
150
+ # @param yaml_content [String] YAML configuration content
151
+ # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
152
+ # @return [Swarm, NodeOrchestrator] Configured swarm or orchestrator instance
153
+ # @raise [ConfigurationError] If YAML is invalid or configuration is incorrect
154
+ #
155
+ # @example Load from YAML string
156
+ # yaml = <<~YAML
157
+ # version: 2
158
+ # swarm:
159
+ # name: "Dev Team"
160
+ # lead: backend
161
+ # agents:
162
+ # backend:
163
+ # description: "Backend developer"
164
+ # model: "gpt-4"
165
+ # agent_file: "agents/backend.md" # Resolved relative to base_dir
166
+ # YAML
167
+ #
168
+ # swarm = SwarmSDK.load(yaml, base_dir: "/path/to/project")
169
+ # result = swarm.execute("Build authentication")
170
+ #
171
+ # @example Load with default base_dir (Dir.pwd)
172
+ # yaml = File.read("config.yml")
173
+ # swarm = SwarmSDK.load(yaml) # base_dir defaults to Dir.pwd
174
+ def load(yaml_content, base_dir: Dir.pwd, allow_filesystem_tools: nil)
175
+ config = Configuration.new(yaml_content, base_dir: base_dir)
176
+ config.load_and_validate
177
+ swarm = config.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
178
+
179
+ # Apply hooks if any are configured (YAML-only feature)
180
+ if hooks_configured?(config)
181
+ Hooks::Adapter.apply_hooks(swarm, config)
182
+ end
183
+
184
+ # Store config reference for agent hooks (applied during initialize_agents)
185
+ swarm.config_for_hooks = config
186
+
187
+ swarm
188
+ end
189
+
190
+ # Load swarm from YAML file (convenience method)
191
+ #
192
+ # Reads the YAML file and uses the file's directory as the base directory
193
+ # for resolving agent file paths. This is the recommended method for
194
+ # loading swarms from configuration files.
195
+ #
196
+ # @param path [String, Pathname] Path to YAML configuration file
197
+ # @return [Swarm, NodeOrchestrator] Configured swarm or orchestrator instance
198
+ # @raise [ConfigurationError] If file not found or configuration invalid
199
+ #
200
+ # @example
201
+ # swarm = SwarmSDK.load_file("config.yml")
202
+ # result = swarm.execute("Build authentication")
203
+ #
204
+ # @example With absolute path
205
+ # swarm = SwarmSDK.load_file("/absolute/path/config.yml")
206
+ def load_file(path, allow_filesystem_tools: nil)
207
+ config = Configuration.load_file(path)
208
+ swarm = config.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
209
+
210
+ # Apply hooks if any are configured (YAML-only feature)
211
+ if hooks_configured?(config)
212
+ Hooks::Adapter.apply_hooks(swarm, config)
213
+ end
214
+
215
+ # Store config reference for agent hooks (applied during initialize_agents)
216
+ swarm.config_for_hooks = config
217
+
218
+ swarm
49
219
  end
50
220
 
51
221
  # Configure SwarmSDK global settings
@@ -62,6 +232,180 @@ module SwarmSDK
62
232
  # Alias for backward compatibility
63
233
  alias_method :configuration, :settings
64
234
  alias_method :reset_configuration!, :reset_settings!
235
+
236
+ private
237
+
238
+ # Check if hooks are configured in the configuration
239
+ #
240
+ # @param config [Configuration] Configuration instance
241
+ # @return [Boolean] true if any hooks are configured
242
+ def hooks_configured?(config)
243
+ config.swarm_hooks.any? ||
244
+ config.all_agents_hooks.any? ||
245
+ config.agents.any? { |_, agent_config| agent_config[:hooks]&.any? }
246
+ end
247
+
248
+ # Parse configuration error and extract structured information
249
+ #
250
+ # Attempts to extract field path and agent name from error messages.
251
+ # Returns a structured error hash with type, field, message, and optional agent.
252
+ #
253
+ # @param error [StandardError] The caught error
254
+ # @return [Hash] Structured error hash
255
+ def parse_configuration_error(error)
256
+ message = error.message
257
+ error_hash = { message: message }
258
+
259
+ # Detect error type and extract field information
260
+ case message
261
+ # YAML syntax errors
262
+ when /Invalid YAML syntax/i
263
+ error_hash.merge!(
264
+ type: :syntax_error,
265
+ field: nil,
266
+ )
267
+
268
+ # Missing version field
269
+ when /Missing 'version' field/i
270
+ error_hash.merge!(
271
+ type: :missing_field,
272
+ field: "version",
273
+ )
274
+
275
+ # Invalid version
276
+ when /SwarmSDK requires version: (\d+)/i
277
+ error_hash.merge!(
278
+ type: :invalid_value,
279
+ field: "version",
280
+ )
281
+
282
+ # Missing swarm fields
283
+ when /Missing '(\w+)' field in swarm configuration/i
284
+ field_name = Regexp.last_match(1)
285
+ error_hash.merge!(
286
+ type: :missing_field,
287
+ field: "swarm.#{field_name}",
288
+ )
289
+
290
+ # Agent missing required field
291
+ when /Agent '([^']+)' missing required '([^']+)' field/i
292
+ agent_name = Regexp.last_match(1)
293
+ field_name = Regexp.last_match(2)
294
+ error_hash.merge!(
295
+ type: :missing_field,
296
+ field: "swarm.agents.#{agent_name}.#{field_name}",
297
+ agent: agent_name,
298
+ )
299
+
300
+ # Directory does not exist
301
+ when /Directory '([^']+)' for agent '([^']+)' does not exist/i
302
+ agent_name = Regexp.last_match(2)
303
+ error_hash.merge!(
304
+ type: :directory_not_found,
305
+ field: "swarm.agents.#{agent_name}.directory",
306
+ agent: agent_name,
307
+ )
308
+
309
+ # Error loading agent from file (must come before "Agent file not found")
310
+ when /Error loading agent '([^']+)' from file/i
311
+ agent_name = Regexp.last_match(1)
312
+ error_hash.merge!(
313
+ type: :file_load_error,
314
+ field: "swarm.agents.#{agent_name}.agent_file",
315
+ agent: agent_name,
316
+ )
317
+
318
+ # Agent file not found
319
+ when /Agent file not found: (.+)/i
320
+ # Try to extract agent name from the error context if available
321
+ error_hash.merge!(
322
+ type: :file_not_found,
323
+ field: nil, # We don't know which agent without more context
324
+ )
325
+
326
+ # Lead agent not found
327
+ when /Lead agent '([^']+)' not found in agents/i
328
+ error_hash.merge!(
329
+ type: :invalid_reference,
330
+ field: "swarm.lead",
331
+ )
332
+
333
+ # Unknown agent in connections (old format)
334
+ when /Agent '([^']+)' has connection to unknown agent '([^']+)'/i
335
+ agent_name = Regexp.last_match(1)
336
+ error_hash.merge!(
337
+ type: :invalid_reference,
338
+ field: "swarm.agents.#{agent_name}.delegates_to",
339
+ agent: agent_name,
340
+ )
341
+
342
+ # Unknown agent in connections (new format with composable swarms)
343
+ when /Agent '([^']+)' delegates to unknown target '([^']+)'/i
344
+ agent_name = Regexp.last_match(1)
345
+ error_hash.merge!(
346
+ type: :invalid_reference,
347
+ field: "swarm.agents.#{agent_name}.delegates_to",
348
+ agent: agent_name,
349
+ )
350
+
351
+ # Circular dependency
352
+ when /Circular dependency detected/i
353
+ error_hash.merge!(
354
+ type: :circular_dependency,
355
+ field: nil,
356
+ )
357
+
358
+ # Configuration file not found
359
+ when /Configuration file not found/i
360
+ error_hash.merge!(
361
+ type: :file_not_found,
362
+ field: nil,
363
+ )
364
+
365
+ # Invalid hook event
366
+ when /Invalid hook event '([^']+)' for agent '([^']+)'/i
367
+ agent_name = Regexp.last_match(2)
368
+ error_hash.merge!(
369
+ type: :invalid_value,
370
+ field: "swarm.agents.#{agent_name}.hooks",
371
+ agent: agent_name,
372
+ )
373
+
374
+ # api_version validation error
375
+ when /Agent '([^']+)' has api_version set, but provider is/i
376
+ agent_name = Regexp.last_match(1)
377
+ error_hash.merge!(
378
+ type: :invalid_value,
379
+ field: "swarm.agents.#{agent_name}.api_version",
380
+ agent: agent_name,
381
+ )
382
+
383
+ # api_version invalid value
384
+ when /Agent '([^']+)' has invalid api_version/i
385
+ agent_name = Regexp.last_match(1)
386
+ error_hash.merge!(
387
+ type: :invalid_value,
388
+ field: "swarm.agents.#{agent_name}.api_version",
389
+ agent: agent_name,
390
+ )
391
+
392
+ # No agents defined
393
+ when /No agents defined/i
394
+ error_hash.merge!(
395
+ type: :missing_field,
396
+ field: "swarm.agents",
397
+ )
398
+
399
+ # Default: unknown error
400
+ else
401
+ error_hash.merge!(
402
+ type: :validation_error,
403
+ field: nil,
404
+ )
405
+ end
406
+
407
+ error_hash.compact
408
+ end
65
409
  end
66
410
 
67
411
  # Settings class for SwarmSDK global settings (not to be confused with Configuration for YAML loading)
@@ -69,17 +413,33 @@ module SwarmSDK
69
413
  # WebFetch tool LLM processing configuration
70
414
  attr_accessor :webfetch_provider, :webfetch_model, :webfetch_base_url, :webfetch_max_tokens
71
415
 
416
+ # Filesystem tools control
417
+ attr_accessor :allow_filesystem_tools
418
+
72
419
  def initialize
73
420
  @webfetch_provider = nil
74
421
  @webfetch_model = nil
75
422
  @webfetch_base_url = nil
76
423
  @webfetch_max_tokens = 4096
424
+ @allow_filesystem_tools = parse_env_bool("SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", default: true)
77
425
  end
78
426
 
79
427
  # Check if WebFetch LLM processing is enabled
80
428
  def webfetch_llm_enabled?
81
429
  !@webfetch_provider.nil? && !@webfetch_model.nil?
82
430
  end
431
+
432
+ private
433
+
434
+ def parse_env_bool(key, default:)
435
+ return default unless ENV.key?(key)
436
+
437
+ value = ENV[key].to_s.downcase
438
+ return true if ["true", "yes", "1", "on", "enabled"].include?(value)
439
+ return false if ["false", "no", "0", "off", "disabled"].include?(value)
440
+
441
+ default
442
+ end
83
443
  end
84
444
 
85
445
  # Initialize default settings
@@ -132,22 +492,3 @@ RubyLLM.configure do |config|
132
492
  config.gpustack_api_base ||= ENV["GPUSTACK_API_BASE"]
133
493
  config.gpustack_api_key ||= ENV["GPUSTACK_API_KEY"]
134
494
  end
135
-
136
- # monkey patches
137
- # ruby_llm/mcp
138
- # - add `id` when sending "notifications/initialized" message: https://github.com/patvice/ruby_llm-mcp/issues/65
139
- # - remove `to_sym` on MCP parameter type: https://github.com/patvice/ruby_llm-mcp/issues/62#issuecomment-3421488406
140
- require "ruby_llm/mcp/notifications/initialize"
141
- require "ruby_llm/mcp/parameter"
142
-
143
- module RubyLLM
144
- module MCP
145
- module Notifications
146
- class Initialize
147
- def call
148
- @coordinator.request(notification_body, add_id: true, wait_for_response: false)
149
- end
150
- end
151
- end
152
- end
153
- end