swarm_sdk 2.4.6 → 2.5.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7155af152f3d943e4dd31a18dc869a021105250d4e9b12f1b020ade293682e61
4
- data.tar.gz: a4bee8bc106e8237bc9f32c70846d4ed4485f92197a126d37e4ee3a97596c378
3
+ metadata.gz: 5043d994b9ec60ad756b1c088571886acec8e5225ebac60f55484b0004e8ff0a
4
+ data.tar.gz: 37ce390a6a458cde546be634388a436459c70db1de9cf3f86b19345c2a556846
5
5
  SHA512:
6
- metadata.gz: d7c433a8cb0f454369613e1d3b1c713f3ec4bc727806dc78072d46eac2e9abf1a9928682df3bc12502095cfb79f189366a0cd799ec1cd9eb3ba467186c078c40
7
- data.tar.gz: ccbebf882f4d5dcdd54d47faa1819ba45d664624b5cf3ac412c03b32fb69722dfff21a4020d517d04b2902421b2e2cb8b4bdfe59a5a3869518fecaf817bbf68d
6
+ metadata.gz: 4c7964fa7d22f1b059fcadd5f2a8f963037018bed4fe57ae9f430ffa4f593275868ab44a83692e7706faf5fb2ac2e933b46922c393b3c684227d7facb46e98d7
7
+ data.tar.gz: 0f8fab5ebd7c97f6de61f12a0ed30af096e1130472691e0bd4ee66bcdc6e6bb7823a583a268ff56b4e01ecb91970217900f31bca40f47d69af1bb0698652e5ca
@@ -148,7 +148,7 @@ module SwarmSDK
148
148
  contributions = []
149
149
 
150
150
  PluginRegistry.all.each do |plugin|
151
- next unless plugin.storage_enabled?(@definition)
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?
@@ -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
@@ -139,11 +139,11 @@ module SwarmSDK
139
139
  []
140
140
  end
141
141
 
142
- # Agent storage enabled for this agent? (optional)
142
+ # Check if memory is configured for this agent (optional)
143
143
  #
144
144
  # @param agent_definition [Agent::Definition] Agent definition
145
145
  # @return [Boolean] True if storage should be created
146
- def storage_enabled?(agent_definition)
146
+ def memory_configured?(agent_definition)
147
147
  false
148
148
  end
149
149
 
@@ -569,7 +569,7 @@ module SwarmSDK
569
569
  PluginRegistry.all.each do |plugin|
570
570
  @swarm.agent_definitions.each do |agent_name, agent_definition|
571
571
  # Check if this plugin needs storage for this agent
572
- next unless plugin.storage_enabled?(agent_definition)
572
+ next unless plugin.memory_configured?(agent_definition)
573
573
 
574
574
  # Get plugin config for this agent
575
575
  config = get_plugin_config(agent_definition, plugin.name)
@@ -60,6 +60,11 @@ module SwarmSDK
60
60
  # Uses the Registry factory pattern to instantiate tools based on their
61
61
  # declared requirements. This eliminates the need for a giant case statement.
62
62
  #
63
+ # Tool lookup order:
64
+ # 1. Plugin tools (registered via SwarmSDK::PluginRegistry)
65
+ # 2. Custom tools (registered via SwarmSDK.register_tool)
66
+ # 3. Built-in tools (SwarmSDK::Tools::Registry)
67
+ #
63
68
  # File tools and TodoWrite require agent context for tracking state.
64
69
  # Scratchpad tools require shared scratchpad instance.
65
70
  # Plugin tools are delegated to their respective plugins.
@@ -80,7 +85,16 @@ module SwarmSDK
80
85
  return create_plugin_tool(tool_name_sym, agent_name, directory, chat, agent_definition)
81
86
  end
82
87
 
83
- # Use Registry factory pattern - tools declare their own requirements
88
+ # Check if tool is a custom registered tool
89
+ if CustomToolRegistry.registered?(tool_name_sym)
90
+ context = {
91
+ agent_name: agent_name,
92
+ directory: directory,
93
+ }
94
+ return CustomToolRegistry.create(tool_name_sym, context)
95
+ end
96
+
97
+ # Use Registry factory pattern for built-in tools
84
98
  context = {
85
99
  agent_name: agent_name,
86
100
  directory: directory,
@@ -267,7 +281,7 @@ module SwarmSDK
267
281
  def register_plugin_tools(chat, agent_name, agent_definition, explicit_tool_names)
268
282
  PluginRegistry.all.each do |plugin|
269
283
  # Check if plugin has storage enabled for this agent
270
- next unless plugin.storage_enabled?(agent_definition)
284
+ next unless plugin.memory_configured?(agent_definition)
271
285
 
272
286
  # Register each tool provided by the plugin
273
287
  plugin.tools.each do |tool_name|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.4.6"
4
+ VERSION = "2.5.0"
5
5
  end
data/lib/swarm_sdk.rb CHANGED
@@ -162,6 +162,149 @@ module SwarmSDK
162
162
  AgentRegistry.clear
163
163
  end
164
164
 
165
+ # Register a custom tool for use in swarms
166
+ #
167
+ # Provides a simple way to add tools without creating a full plugin.
168
+ # Tools can be registered with an explicit name or the name can be
169
+ # inferred from the class name.
170
+ #
171
+ # Custom tools are available to any agent that includes them in their
172
+ # tools configuration, just like built-in tools.
173
+ #
174
+ # @overload register_tool(tool_class)
175
+ # Register a tool with name inferred from class name
176
+ # @param tool_class [Class] Tool class (must inherit from RubyLLM::Tool)
177
+ # @return [Symbol] The registered tool name
178
+ #
179
+ # @overload register_tool(name, tool_class)
180
+ # Register a tool with explicit name
181
+ # @param name [Symbol, String] Tool name
182
+ # @param tool_class [Class] Tool class (must inherit from RubyLLM::Tool)
183
+ # @return [Symbol] The registered tool name
184
+ #
185
+ # @raise [ArgumentError] If tool_class doesn't inherit from RubyLLM::Tool
186
+ # @raise [ArgumentError] If a tool with the same name is already registered
187
+ # @raise [ArgumentError] If the name conflicts with a built-in or plugin tool
188
+ #
189
+ # @example Register with inferred name
190
+ # class WeatherTool < RubyLLM::Tool
191
+ # description "Get weather for a city"
192
+ # param :city, type: "string", required: true
193
+ #
194
+ # def execute(city:)
195
+ # "Weather in #{city}: Sunny, 72°F"
196
+ # end
197
+ # end
198
+ #
199
+ # SwarmSDK.register_tool(WeatherTool) # Registers as :Weather
200
+ #
201
+ # @example Register with explicit name
202
+ # SwarmSDK.register_tool(:GetWeather, WeatherTool)
203
+ #
204
+ # @example Tool with agent context
205
+ # class ContextAwareTool < RubyLLM::Tool
206
+ # # Declare what context the tool needs
207
+ # def self.creation_requirements
208
+ # [:agent_name, :directory]
209
+ # end
210
+ #
211
+ # def initialize(agent_name:, directory:)
212
+ # super()
213
+ # @agent_name = agent_name
214
+ # @directory = directory
215
+ # end
216
+ #
217
+ # description "Shows agent context"
218
+ # def execute
219
+ # "Agent: #{@agent_name} in #{@directory}"
220
+ # end
221
+ # end
222
+ #
223
+ # SwarmSDK.register_tool(ContextAwareTool)
224
+ #
225
+ # @example Use registered tool in a swarm
226
+ # SwarmSDK.register_tool(WeatherTool)
227
+ #
228
+ # swarm = SwarmSDK.build do
229
+ # name "Weather Assistant"
230
+ # lead :assistant
231
+ #
232
+ # agent :assistant do
233
+ # model "claude-sonnet-4"
234
+ # description "Weather helper"
235
+ # tools :Weather, :Read # Custom + built-in tools
236
+ # end
237
+ # end
238
+ #
239
+ # @see CustomToolRegistry For the underlying registry
240
+ # @see Plugin For complex tool systems requiring storage or lifecycle hooks
241
+ def register_tool(name_or_class, tool_class = nil)
242
+ if tool_class.nil?
243
+ # Single argument: infer name from class
244
+ tool_class = name_or_class
245
+ name = CustomToolRegistry.infer_name(tool_class)
246
+ else
247
+ # Two arguments: explicit name
248
+ name = name_or_class.to_sym
249
+ end
250
+
251
+ CustomToolRegistry.register(name, tool_class)
252
+ name
253
+ end
254
+
255
+ # Check if a custom tool is registered
256
+ #
257
+ # @param name [Symbol, String] Tool name
258
+ # @return [Boolean] true if the tool is registered
259
+ #
260
+ # @example
261
+ # SwarmSDK.register_tool(WeatherTool)
262
+ # SwarmSDK.custom_tool_registered?(:Weather) #=> true
263
+ # SwarmSDK.custom_tool_registered?(:Unknown) #=> false
264
+ def custom_tool_registered?(name)
265
+ CustomToolRegistry.registered?(name)
266
+ end
267
+
268
+ # Get all registered custom tool names
269
+ #
270
+ # @return [Array<Symbol>] List of registered custom tool names
271
+ #
272
+ # @example
273
+ # SwarmSDK.register_tool(WeatherTool)
274
+ # SwarmSDK.register_tool(StockTool)
275
+ # SwarmSDK.custom_tools #=> [:Weather, :Stock]
276
+ def custom_tools
277
+ CustomToolRegistry.tool_names
278
+ end
279
+
280
+ # Unregister a custom tool
281
+ #
282
+ # @param name [Symbol, String] Tool name to unregister
283
+ # @return [Class, nil] The unregistered tool class, or nil if not found
284
+ #
285
+ # @example
286
+ # SwarmSDK.register_tool(WeatherTool)
287
+ # SwarmSDK.unregister_tool(:Weather)
288
+ # SwarmSDK.custom_tool_registered?(:Weather) #=> false
289
+ def unregister_tool(name)
290
+ CustomToolRegistry.unregister(name)
291
+ end
292
+
293
+ # Clear all registered custom tools
294
+ #
295
+ # Removes all custom tool registrations. Primarily useful for testing
296
+ # to ensure clean state between tests.
297
+ #
298
+ # @return [void]
299
+ #
300
+ # @example In test teardown
301
+ # def teardown
302
+ # SwarmSDK.clear_custom_tools!
303
+ # end
304
+ def clear_custom_tools!
305
+ CustomToolRegistry.clear
306
+ end
307
+
165
308
  # Main entry point for DSL - builds simple multi-agent swarms
166
309
  #
167
310
  # @return [Swarm] Always returns a Swarm instance
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.6
4
+ version: 2.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -154,6 +154,7 @@ files:
154
154
  - lib/swarm_sdk/context_compactor/token_counter.rb
155
155
  - lib/swarm_sdk/context_management/builder.rb
156
156
  - lib/swarm_sdk/context_management/context.rb
157
+ - lib/swarm_sdk/custom_tool_registry.rb
157
158
  - lib/swarm_sdk/defaults.rb
158
159
  - lib/swarm_sdk/events_to_messages.rb
159
160
  - lib/swarm_sdk/hooks/adapter.rb