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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +3 -0
  3. data/CLAUDE.md +0 -1
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +12 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +139 -0
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +249 -1
  8. data/docs/v2/README.md +15 -5
  9. data/docs/v2/guides/complete-tutorial.md +93 -7
  10. data/docs/v2/guides/getting-started.md +3 -1
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/{migrating-to-2.3.md → migrating-to-2.x.md} +213 -8
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/swarm-memory.md +2 -13
  16. data/docs/v2/reference/cli.md +0 -1
  17. data/docs/v2/reference/configuration_reference.md +300 -0
  18. data/docs/v2/reference/event_payload_structures.md +26 -4
  19. data/docs/v2/reference/ruby-dsl.md +457 -4
  20. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  21. data/docs/v2/reference/yaml.md +2 -2
  22. data/lib/claude_swarm/mcp_generator.rb +1 -1
  23. data/lib/claude_swarm/orchestrator.rb +8 -1
  24. data/lib/claude_swarm/version.rb +1 -1
  25. data/lib/swarm_cli/version.rb +1 -1
  26. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  27. data/lib/swarm_memory/core/storage.rb +7 -2
  28. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  29. data/lib/swarm_memory/integration/sdk_plugin.rb +120 -27
  30. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  31. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  32. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  33. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  34. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  35. data/lib/swarm_memory/version.rb +1 -1
  36. data/lib/swarm_memory.rb +7 -5
  37. data/lib/swarm_sdk/agent/chat.rb +1 -1
  38. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +4 -0
  39. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
  40. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +38 -4
  41. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +2 -2
  42. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
  43. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +48 -0
  44. data/lib/swarm_sdk/agent/context.rb +1 -2
  45. data/lib/swarm_sdk/agent/definition.rb +3 -3
  46. data/lib/swarm_sdk/agent/system_prompt_builder.rb +1 -1
  47. data/lib/swarm_sdk/agent_registry.rb +146 -0
  48. data/lib/swarm_sdk/builders/base_builder.rb +91 -12
  49. data/lib/swarm_sdk/config.rb +302 -0
  50. data/lib/swarm_sdk/configuration/parser.rb +22 -2
  51. data/lib/swarm_sdk/configuration.rb +13 -4
  52. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  53. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  54. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  55. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -3
  56. data/lib/swarm_sdk/models.json +4333 -1
  57. data/lib/swarm_sdk/models.rb +43 -2
  58. data/lib/swarm_sdk/plugin.rb +2 -2
  59. data/lib/swarm_sdk/result.rb +52 -0
  60. data/lib/swarm_sdk/swarm/agent_initializer.rb +1 -1
  61. data/lib/swarm_sdk/swarm/hook_triggers.rb +1 -0
  62. data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
  63. data/lib/swarm_sdk/swarm/tool_configurator.rb +18 -4
  64. data/lib/swarm_sdk/swarm.rb +76 -13
  65. data/lib/swarm_sdk/tools/bash.rb +7 -9
  66. data/lib/swarm_sdk/tools/glob.rb +5 -5
  67. data/lib/swarm_sdk/tools/read.rb +8 -8
  68. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +4 -3
  69. data/lib/swarm_sdk/tools/web_fetch.rb +20 -18
  70. data/lib/swarm_sdk/version.rb +1 -1
  71. data/lib/swarm_sdk/workflow/builder.rb +49 -0
  72. data/lib/swarm_sdk/workflow/node_builder.rb +4 -2
  73. data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
  74. data/lib/swarm_sdk.rb +261 -105
  75. data/swarm_cli.gemspec +1 -1
  76. data/swarm_memory.gemspec +8 -3
  77. data/swarm_sdk.gemspec +4 -4
  78. data/team_full.yml +104 -300
  79. metadata +9 -5
  80. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  81. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -38,9 +38,13 @@ module SwarmSDK
38
38
  # Load configuration from YAML file
39
39
  #
40
40
  # @param path [String, Pathname] Path to YAML configuration file
41
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
42
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
43
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
44
+ # When false, skips interpolation entirely.
41
45
  # @return [Configuration] Validated configuration instance
42
46
  # @raise [ConfigurationError] If file not found or invalid
43
- def load_file(path)
47
+ def load_file(path, env_interpolation: nil)
44
48
  path = Pathname.new(path).expand_path
45
49
 
46
50
  unless path.exist?
@@ -50,7 +54,7 @@ module SwarmSDK
50
54
  yaml_content = File.read(path)
51
55
  base_dir = path.dirname
52
56
 
53
- new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
57
+ new(yaml_content, base_dir: base_dir, env_interpolation: env_interpolation).tap(&:load_and_validate)
54
58
  rescue Errno::ENOENT
55
59
  raise ConfigurationError, "Configuration file not found: #{path}"
56
60
  end
@@ -60,12 +64,17 @@ module SwarmSDK
60
64
  #
61
65
  # @param yaml_content [String] YAML configuration content
62
66
  # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
63
- def initialize(yaml_content, base_dir: Dir.pwd)
67
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
68
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
69
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
70
+ # When false, skips interpolation entirely.
71
+ def initialize(yaml_content, base_dir: Dir.pwd, env_interpolation: nil)
64
72
  raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
65
73
  raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
66
74
 
67
75
  @yaml_content = yaml_content
68
76
  @base_dir = Pathname.new(base_dir).expand_path
77
+ @env_interpolation = env_interpolation
69
78
  @parser = nil
70
79
  @translator = nil
71
80
  end
@@ -77,7 +86,7 @@ module SwarmSDK
77
86
  #
78
87
  # @return [self]
79
88
  def load_and_validate
80
- @parser = Parser.new(@yaml_content, base_dir: @base_dir)
89
+ @parser = Parser.new(@yaml_content, base_dir: @base_dir, env_interpolation: @env_interpolation)
81
90
  @parser.parse
82
91
 
83
92
  # Sync parsed data to instance variables for backward compatibility
@@ -17,10 +17,6 @@ module SwarmSDK
17
17
  # total_tokens = TokenCounter.estimate_messages(messages)
18
18
  #
19
19
  class TokenCounter
20
- # Backward compatibility aliases - use Defaults module for new code
21
- CHARS_PER_TOKEN_PROSE = Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE
22
- CHARS_PER_TOKEN_CODE = Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE
23
-
24
20
  class << self
25
21
  # Estimate tokens for a single message
26
22
  #
@@ -78,9 +74,9 @@ module SwarmSDK
78
74
 
79
75
  # Choose characters per token based on content type
80
76
  chars_per_token = if code_ratio > 0.1
81
- CHARS_PER_TOKEN_CODE # Code
77
+ SwarmSDK.config.chars_per_token_code # Code
82
78
  else
83
- CHARS_PER_TOKEN_PROSE # Prose
79
+ SwarmSDK.config.chars_per_token_prose # Prose
84
80
  end
85
81
 
86
82
  (text.length / chars_per_token).ceil
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+
5
+ module SwarmSDK
6
+ # Registry for user-defined custom tools
7
+ #
8
+ # Provides a simple way to register custom tools without creating a full plugin.
9
+ # Custom tools are registered globally and available to all agents that request them.
10
+ #
11
+ # ## When to Use Custom Tools vs Plugins
12
+ #
13
+ # **Use Custom Tools when:**
14
+ # - You have simple, stateless tools
15
+ # - Tools don't need persistent storage
16
+ # - Tools don't need lifecycle hooks
17
+ # - Tools don't need system prompt contributions
18
+ #
19
+ # **Use Plugins when:**
20
+ # - Tools need persistent storage per agent
21
+ # - Tools need lifecycle hooks (on_agent_initialized, on_user_message, etc.)
22
+ # - Tools need to contribute to system prompts
23
+ # - You have a suite of related tools that share configuration
24
+ #
25
+ # @example Register a simple tool
26
+ # class WeatherTool < RubyLLM::Tool
27
+ # description "Get weather for a city"
28
+ # param :city, type: "string", required: true
29
+ #
30
+ # def execute(city:)
31
+ # "Weather in #{city}: Sunny"
32
+ # end
33
+ # end
34
+ #
35
+ # SwarmSDK.register_tool(WeatherTool)
36
+ #
37
+ # @example Register with explicit name
38
+ # SwarmSDK.register_tool(:Weather, WeatherTool)
39
+ #
40
+ # @example Tool with creation requirements
41
+ # class AgentAwareTool < RubyLLM::Tool
42
+ # def self.creation_requirements
43
+ # [:agent_name, :directory]
44
+ # end
45
+ #
46
+ # def initialize(agent_name:, directory:)
47
+ # super()
48
+ # @agent_name = agent_name
49
+ # @directory = directory
50
+ # end
51
+ #
52
+ # def execute
53
+ # "Agent: #{@agent_name}, Dir: #{@directory}"
54
+ # end
55
+ # end
56
+ #
57
+ # SwarmSDK.register_tool(AgentAwareTool)
58
+ #
59
+ module CustomToolRegistry
60
+ # Wrapper that overrides the tool's name to match the registered name
61
+ #
62
+ # This ensures that when a user registers a tool with a specific name,
63
+ # that name is what gets used for tool lookup (has_tool?) and LLM tool calls.
64
+ class NamedToolWrapper < SimpleDelegator
65
+ def initialize(tool, registered_name)
66
+ super(tool)
67
+ @registered_name = registered_name.to_s
68
+ end
69
+
70
+ # Override name to return the registered name
71
+ def name
72
+ @registered_name
73
+ end
74
+ end
75
+
76
+ @tools = {}
77
+
78
+ class << self
79
+ # Register a custom tool
80
+ #
81
+ # @param name [Symbol] Tool name
82
+ # @param tool_class [Class] Tool class (must be a RubyLLM::Tool subclass)
83
+ # @raise [ArgumentError] If tool_class is not a RubyLLM::Tool subclass
84
+ # @raise [ArgumentError] If a tool with the same name is already registered
85
+ # @return [void]
86
+ def register(name, tool_class)
87
+ name = name.to_sym
88
+
89
+ unless tool_class.is_a?(Class) && tool_class < RubyLLM::Tool
90
+ raise ArgumentError, "Tool class must inherit from RubyLLM::Tool"
91
+ end
92
+
93
+ if @tools.key?(name)
94
+ raise ArgumentError, "Custom tool '#{name}' is already registered"
95
+ end
96
+
97
+ if PluginRegistry.plugin_tool?(name)
98
+ raise ArgumentError, "Tool '#{name}' is already provided by a plugin"
99
+ end
100
+
101
+ if Tools::Registry.exists?(name)
102
+ raise ArgumentError, "Tool '#{name}' is a built-in tool and cannot be overridden"
103
+ end
104
+
105
+ @tools[name] = tool_class
106
+ end
107
+
108
+ # Check if a custom tool is registered
109
+ #
110
+ # @param name [Symbol, String] Tool name
111
+ # @return [Boolean]
112
+ def registered?(name)
113
+ @tools.key?(name.to_sym)
114
+ end
115
+
116
+ # Get a registered tool class
117
+ #
118
+ # @param name [Symbol, String] Tool name
119
+ # @return [Class, nil] Tool class or nil if not found
120
+ def get(name)
121
+ @tools[name.to_sym]
122
+ end
123
+
124
+ # Get all registered custom tool names
125
+ #
126
+ # @return [Array<Symbol>]
127
+ def tool_names
128
+ @tools.keys
129
+ end
130
+
131
+ # Create a tool instance
132
+ #
133
+ # Uses the tool's `creation_requirements` class method (if defined) to determine
134
+ # what parameters to pass to the constructor. The created tool is wrapped with
135
+ # NamedToolWrapper to ensure the registered name is used for tool lookup.
136
+ #
137
+ # @param name [Symbol, String] Tool name
138
+ # @param context [Hash] Available context for tool creation
139
+ # @option context [Symbol] :agent_name Agent identifier
140
+ # @option context [String] :directory Agent's working directory
141
+ # @return [RubyLLM::Tool] Instantiated tool (wrapped with registered name)
142
+ # @raise [ConfigurationError] If tool is unknown or has unmet requirements
143
+ def create(name, context = {})
144
+ name_sym = name.to_sym
145
+ tool_class = @tools[name_sym]
146
+
147
+ raise ConfigurationError, "Unknown custom tool: #{name}" unless tool_class
148
+
149
+ # Create the tool instance
150
+ tool = if tool_class.respond_to?(:creation_requirements)
151
+ requirements = tool_class.creation_requirements
152
+ params = extract_params(requirements, context, name)
153
+ tool_class.new(**params)
154
+ else
155
+ # No requirements - simple instantiation
156
+ tool_class.new
157
+ end
158
+
159
+ # Wrap with NamedToolWrapper to ensure registered name is used
160
+ NamedToolWrapper.new(tool, name_sym)
161
+ end
162
+
163
+ # Unregister a custom tool
164
+ #
165
+ # @param name [Symbol, String] Tool name
166
+ # @return [Class, nil] The unregistered tool class, or nil if not found
167
+ def unregister(name)
168
+ @tools.delete(name.to_sym)
169
+ end
170
+
171
+ # Clear all registered custom tools
172
+ #
173
+ # Primarily useful for testing.
174
+ #
175
+ # @return [void]
176
+ def clear
177
+ @tools.clear
178
+ end
179
+
180
+ # Infer tool name from class name
181
+ #
182
+ # @param tool_class [Class] Tool class
183
+ # @return [Symbol] Inferred tool name
184
+ #
185
+ # @example
186
+ # infer_name(WeatherTool) #=> :Weather
187
+ # infer_name(MyApp::Tools::StockPrice) #=> :StockPrice
188
+ # infer_name(MyApp::Tools::StockPriceTool) #=> :StockPrice
189
+ def infer_name(tool_class)
190
+ # Get the class name without module prefix
191
+ class_name = tool_class.name.split("::").last
192
+
193
+ # Remove "Tool" suffix if present
194
+ name = class_name.sub(/Tool\z/, "")
195
+
196
+ name.to_sym
197
+ end
198
+
199
+ private
200
+
201
+ # Extract required parameters from context
202
+ #
203
+ # @param requirements [Array<Symbol>] Required parameter names
204
+ # @param context [Hash] Available context
205
+ # @param tool_name [Symbol] Tool name for error messages
206
+ # @return [Hash] Parameters to pass to tool constructor
207
+ # @raise [ConfigurationError] If required parameter is missing
208
+ def extract_params(requirements, context, tool_name)
209
+ params = {}
210
+
211
+ requirements.each do |req|
212
+ unless context.key?(req)
213
+ raise ConfigurationError,
214
+ "Custom tool '#{tool_name}' requires '#{req}' but it was not provided. " \
215
+ "Ensure the tool's `creation_requirements` only includes supported keys: " \
216
+ ":agent_name, :directory"
217
+ end
218
+
219
+ params[req] = context[req]
220
+ end
221
+
222
+ params
223
+ end
224
+ end
225
+ end
226
+ end
@@ -167,7 +167,7 @@ module SwarmSDK
167
167
  def create_hook_callback(hook_def, event_symbol, agent_name, swarm_name)
168
168
  # Support both string and symbol keys (YAML may be symbolized)
169
169
  command = hook_def[:command] || hook_def["command"]
170
- timeout = hook_def[:timeout] || hook_def["timeout"] || ShellExecutor::DEFAULT_TIMEOUT
170
+ timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
171
171
 
172
172
  lambda do |context|
173
173
  input_json = build_input_json(context, event_symbol, agent_name)
@@ -191,7 +191,7 @@ module SwarmSDK
191
191
  def create_all_agents_hook_callback(hook_def, event_symbol, swarm_name)
192
192
  # Support both string and symbol keys (YAML may be symbolized)
193
193
  command = hook_def[:command] || hook_def["command"]
194
- timeout = hook_def[:timeout] || hook_def["timeout"] || ShellExecutor::DEFAULT_TIMEOUT
194
+ timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
195
195
 
196
196
  lambda do |context|
197
197
  # Agent name comes from context
@@ -217,7 +217,7 @@ module SwarmSDK
217
217
  def create_swarm_hook_callback(hook_def, event_symbol, swarm_name)
218
218
  # Support both string and symbol keys (YAML may be symbolized)
219
219
  command = hook_def[:command] || hook_def["command"]
220
- timeout = hook_def[:timeout] || hook_def["timeout"] || ShellExecutor::DEFAULT_TIMEOUT
220
+ timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
221
221
 
222
222
  lambda do |context|
223
223
  input_json = build_swarm_input_json(context, event_symbol, swarm_name)
@@ -47,8 +47,7 @@ module SwarmSDK
47
47
  # )
48
48
  # # => Result (continue or halt based on exit code)
49
49
  class ShellExecutor
50
- # Backward compatibility alias - use Defaults module for new code
51
- DEFAULT_TIMEOUT = Defaults::Timeouts::HOOK_SHELL_SECONDS
50
+ # NOTE: Timeout now accessed via SwarmSDK.config.hook_shell_timeout
52
51
 
53
52
  class << self
54
53
  # Execute a shell command hook
@@ -60,7 +59,9 @@ module SwarmSDK
60
59
  # @param swarm_name [String, nil] Swarm name for environment variables
61
60
  # @param event [Symbol] Event type for context-aware behavior
62
61
  # @return [Result] Result based on exit code (continue or halt)
63
- def execute(command:, input_json:, timeout: DEFAULT_TIMEOUT, agent_name: nil, swarm_name: nil, event: nil)
62
+ def execute(command:, input_json:, timeout: nil, agent_name: nil, swarm_name: nil, event: nil)
63
+ timeout ||= SwarmSDK.config.hook_shell_timeout
64
+
64
65
  # Build environment variables
65
66
  env = build_environment(agent_name: agent_name, swarm_name: swarm_name)
66
67