swarm_sdk 2.6.2 → 2.7.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/swarm_sdk/agent/builder.rb +33 -1
- data/lib/swarm_sdk/agent/chat.rb +179 -35
- data/lib/swarm_sdk/agent/definition.rb +7 -1
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +48 -8
- data/lib/swarm_sdk/agent/tool_registry.rb +189 -0
- data/lib/swarm_sdk/builders/base_builder.rb +4 -0
- data/lib/swarm_sdk/config.rb +2 -1
- data/lib/swarm_sdk/configuration/translator.rb +2 -0
- data/lib/swarm_sdk/models.json +296 -238
- data/lib/swarm_sdk/swarm/agent_initializer.rb +51 -3
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +9 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +45 -7
- data/lib/swarm_sdk/swarm/tool_configurator.rb +25 -5
- data/lib/swarm_sdk/tools/base.rb +63 -0
- data/lib/swarm_sdk/tools/bash.rb +1 -1
- data/lib/swarm_sdk/tools/clock.rb +3 -1
- data/lib/swarm_sdk/tools/delegate.rb +14 -3
- data/lib/swarm_sdk/tools/edit.rb +1 -1
- data/lib/swarm_sdk/tools/glob.rb +1 -1
- data/lib/swarm_sdk/tools/grep.rb +1 -1
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +137 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +1 -1
- data/lib/swarm_sdk/tools/read.rb +1 -1
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +1 -1
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +1 -1
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +1 -1
- data/lib/swarm_sdk/tools/think.rb +3 -1
- data/lib/swarm_sdk/tools/todo_write.rb +3 -1
- data/lib/swarm_sdk/tools/web_fetch.rb +1 -1
- data/lib/swarm_sdk/tools/write.rb +1 -1
- data/lib/swarm_sdk/version.rb +1 -1
- metadata +4 -1
|
@@ -25,12 +25,13 @@ module SwarmSDK
|
|
|
25
25
|
|
|
26
26
|
# Initialize all agents with their chat instances and tools
|
|
27
27
|
#
|
|
28
|
-
# This implements a
|
|
28
|
+
# This implements a 6-pass algorithm:
|
|
29
29
|
# 1. Create all Agent::Chat instances
|
|
30
30
|
# 2. Register delegation tools (agents can call each other)
|
|
31
31
|
# 3. Setup agent contexts for tracking
|
|
32
32
|
# 4. Configure hook system
|
|
33
33
|
# 5. Apply YAML hooks (if loaded from YAML)
|
|
34
|
+
# 6. Activate tools (Plan 025: populate @llm_chat.tools from registry after plugins)
|
|
34
35
|
#
|
|
35
36
|
# @return [Hash] agents hash { agent_name => Agent::Chat }
|
|
36
37
|
def initialize_all
|
|
@@ -39,6 +40,7 @@ module SwarmSDK
|
|
|
39
40
|
pass_3_setup_contexts
|
|
40
41
|
pass_4_configure_hooks
|
|
41
42
|
pass_5_apply_yaml_hooks
|
|
43
|
+
pass_6_activate_tools # Plan 025: Activate tools after all plugins registered
|
|
42
44
|
|
|
43
45
|
@agents
|
|
44
46
|
end
|
|
@@ -261,7 +263,12 @@ module SwarmSDK
|
|
|
261
263
|
custom_tool_name: custom_tool_name,
|
|
262
264
|
)
|
|
263
265
|
|
|
264
|
-
|
|
266
|
+
# Register in tool registry (Plan 025)
|
|
267
|
+
delegator_chat.tool_registry.register(
|
|
268
|
+
tool,
|
|
269
|
+
source: :delegation,
|
|
270
|
+
metadata: { delegate_name: swarm_name, delegation_type: :swarm },
|
|
271
|
+
)
|
|
265
272
|
end
|
|
266
273
|
|
|
267
274
|
# Wire delegation to a local agent
|
|
@@ -311,7 +318,15 @@ module SwarmSDK
|
|
|
311
318
|
custom_tool_name: custom_tool_name,
|
|
312
319
|
)
|
|
313
320
|
|
|
314
|
-
|
|
321
|
+
# Register in tool registry (Plan 025)
|
|
322
|
+
delegator_chat.tool_registry.register(
|
|
323
|
+
tool,
|
|
324
|
+
source: :delegation,
|
|
325
|
+
metadata: {
|
|
326
|
+
delegate_name: delegate_name_sym,
|
|
327
|
+
delegation_mode: delegate_definition.shared_across_delegations ? :shared : :isolated,
|
|
328
|
+
},
|
|
329
|
+
)
|
|
315
330
|
end
|
|
316
331
|
|
|
317
332
|
# Pass 3: Setup agent contexts
|
|
@@ -447,6 +462,22 @@ module SwarmSDK
|
|
|
447
462
|
Hooks::Adapter.apply_agent_hooks(chat, agent_name, hooks, @swarm.name)
|
|
448
463
|
end
|
|
449
464
|
|
|
465
|
+
# Pass 6: Activate tools after all plugins have registered (Plan 025)
|
|
466
|
+
#
|
|
467
|
+
# This must be the LAST pass because:
|
|
468
|
+
# - Plugins register tools in on_agent_initialized (e.g., LoadSkill from memory plugin)
|
|
469
|
+
# - Tools must be activated AFTER all registration is complete
|
|
470
|
+
# - This populates @llm_chat.tools from the registry
|
|
471
|
+
#
|
|
472
|
+
# @return [void]
|
|
473
|
+
def pass_6_activate_tools
|
|
474
|
+
# Activate tools for PRIMARY agents
|
|
475
|
+
@agents.each_value(&:activate_tools_for_prompt)
|
|
476
|
+
|
|
477
|
+
# Activate tools for DELEGATION instances
|
|
478
|
+
@swarm.delegation_instances.each_value(&:activate_tools_for_prompt)
|
|
479
|
+
end
|
|
480
|
+
|
|
450
481
|
# Create Agent::Chat instance with rate limiting
|
|
451
482
|
#
|
|
452
483
|
# @param agent_name [Symbol] Agent name
|
|
@@ -476,6 +507,15 @@ module SwarmSDK
|
|
|
476
507
|
mcp_configurator.register_mcp_servers(chat, agent_definition.mcp_servers, agent_name: agent_name)
|
|
477
508
|
end
|
|
478
509
|
|
|
510
|
+
# Setup tool activation dependencies (Plan 025)
|
|
511
|
+
chat.setup_tool_activation(
|
|
512
|
+
tool_configurator: tool_configurator,
|
|
513
|
+
agent_definition: agent_definition,
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
# NOTE: activate_tools_for_prompt is called in Pass 5 after all plugins
|
|
517
|
+
# have registered their tools (e.g., LoadSkill from memory plugin)
|
|
518
|
+
|
|
479
519
|
chat
|
|
480
520
|
end
|
|
481
521
|
|
|
@@ -517,9 +557,17 @@ module SwarmSDK
|
|
|
517
557
|
)
|
|
518
558
|
end
|
|
519
559
|
|
|
560
|
+
# Setup tool activation dependencies (Plan 025)
|
|
561
|
+
chat.setup_tool_activation(
|
|
562
|
+
tool_configurator: tool_configurator,
|
|
563
|
+
agent_definition: agent_definition,
|
|
564
|
+
)
|
|
565
|
+
|
|
520
566
|
# Notify plugins (use instance_name, plugins extract base_name if needed)
|
|
521
567
|
notify_plugins_agent_initialized(instance_name.to_sym, chat, agent_definition, tool_configurator)
|
|
522
568
|
|
|
569
|
+
# NOTE: activate_tools_for_prompt is called in Pass 6 after all plugins
|
|
570
|
+
|
|
523
571
|
chat
|
|
524
572
|
end
|
|
525
573
|
|
|
@@ -35,6 +35,7 @@ module SwarmSDK
|
|
|
35
35
|
@headers = nil
|
|
36
36
|
@coding_agent = nil
|
|
37
37
|
@disable_default_tools = nil
|
|
38
|
+
@streaming = nil
|
|
38
39
|
end
|
|
39
40
|
|
|
40
41
|
# Set model for all agents
|
|
@@ -91,6 +92,13 @@ module SwarmSDK
|
|
|
91
92
|
@disable_default_tools = value
|
|
92
93
|
end
|
|
93
94
|
|
|
95
|
+
# Enable or disable streaming for all agents
|
|
96
|
+
#
|
|
97
|
+
# @param value [Boolean] If true, enables streaming; if false, disables it
|
|
98
|
+
def streaming(value)
|
|
99
|
+
@streaming = value
|
|
100
|
+
end
|
|
101
|
+
|
|
94
102
|
# Add tools that all agents will have
|
|
95
103
|
def tools(*tool_names)
|
|
96
104
|
@tools_list.concat(tool_names)
|
|
@@ -165,6 +173,7 @@ module SwarmSDK
|
|
|
165
173
|
headers: @headers,
|
|
166
174
|
coding_agent: @coding_agent,
|
|
167
175
|
disable_default_tools: @disable_default_tools,
|
|
176
|
+
streaming: @streaming,
|
|
168
177
|
tools: @tools_list,
|
|
169
178
|
permissions: @permissions_config,
|
|
170
179
|
}.compact
|
|
@@ -22,9 +22,22 @@ module SwarmSDK
|
|
|
22
22
|
# Connects to MCP servers and registers their tools with the agent's chat instance.
|
|
23
23
|
# Supports stdio, SSE, and HTTP (streamable) transports.
|
|
24
24
|
#
|
|
25
|
+
# ## Boot Optimization (Plan 025)
|
|
26
|
+
#
|
|
27
|
+
# - If tools specified: Create stubs without tools/list RPC (fast boot, lazy schema)
|
|
28
|
+
# - If tools omitted: Call tools/list to discover all tools (discovery mode)
|
|
29
|
+
#
|
|
25
30
|
# @param chat [AgentChat] The agent's chat instance
|
|
26
31
|
# @param mcp_server_configs [Array<Hash>] MCP server configurations
|
|
27
32
|
# @param agent_name [Symbol] Agent name for tracking clients
|
|
33
|
+
#
|
|
34
|
+
# @example Fast boot mode
|
|
35
|
+
# mcp_server :codebase, type: :stdio, command: "mcp-server", tools: [:search, :list]
|
|
36
|
+
# # Creates tool stubs instantly, no tools/list RPC
|
|
37
|
+
#
|
|
38
|
+
# @example Discovery mode
|
|
39
|
+
# mcp_server :codebase, type: :stdio, command: "mcp-server"
|
|
40
|
+
# # Calls tools/list to discover all available tools
|
|
28
41
|
def register_mcp_servers(chat, mcp_server_configs, agent_name:)
|
|
29
42
|
return if mcp_server_configs.nil? || mcp_server_configs.empty?
|
|
30
43
|
|
|
@@ -37,14 +50,39 @@ module SwarmSDK
|
|
|
37
50
|
# Store client for cleanup
|
|
38
51
|
@mcp_clients[agent_name] << client
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
tools_config = server_config[:tools]
|
|
54
|
+
|
|
55
|
+
if tools_config.nil?
|
|
56
|
+
# Discovery mode: Fetch all tools from server (calls tools/list)
|
|
57
|
+
# client.tools returns RubyLLM::Tool instances (already wrapped by internal Coordinator)
|
|
58
|
+
all_tools = client.tools
|
|
59
|
+
all_tools.each do |tool|
|
|
60
|
+
chat.tool_registry.register(
|
|
61
|
+
tool,
|
|
62
|
+
source: :mcp,
|
|
63
|
+
metadata: { server_name: server_config[:name] },
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
RubyLLM.logger.debug("SwarmSDK: Discovered and registered #{all_tools.size} tools from MCP server '#{server_config[:name]}'")
|
|
67
|
+
else
|
|
68
|
+
# Optimized mode: Create tool stubs without tools/list RPC (Plan 025)
|
|
69
|
+
# Use client directly (it has internal coordinator)
|
|
70
|
+
tools_config.each do |tool_name|
|
|
71
|
+
stub = Tools::McpToolStub.new(
|
|
72
|
+
client: client,
|
|
73
|
+
name: tool_name.to_s,
|
|
74
|
+
)
|
|
75
|
+
chat.tool_registry.register(
|
|
76
|
+
stub,
|
|
77
|
+
source: :mcp,
|
|
78
|
+
metadata: { server_name: server_config[:name] },
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
RubyLLM.logger.debug("SwarmSDK: Registered #{tools_config.size} tool stubs from MCP server '#{server_config[:name]}' (lazy schema)")
|
|
82
|
+
end
|
|
46
83
|
rescue StandardError => e
|
|
47
|
-
RubyLLM.logger.error("SwarmSDK: Failed to initialize MCP server '#{server_config[:name]}' for agent #{agent_name}: #{e.message}")
|
|
84
|
+
RubyLLM.logger.error("SwarmSDK: Failed to initialize MCP server '#{server_config[:name]}' for agent #{agent_name}: #{e.class.name}: #{e.message}")
|
|
85
|
+
RubyLLM.logger.error("SwarmSDK: Backtrace: #{e.backtrace.first(5).join("\n ")}")
|
|
48
86
|
raise ConfigurationError, "Failed to initialize MCP server '#{server_config[:name]}': #{e.message}"
|
|
49
87
|
end
|
|
50
88
|
end
|
|
@@ -219,9 +219,17 @@ module SwarmSDK
|
|
|
219
219
|
# @param permissions_config [Hash, nil] Permissions configuration
|
|
220
220
|
# @param agent_definition [Agent::Definition] Agent definition
|
|
221
221
|
# @return [void]
|
|
222
|
-
def wrap_and_add_tool(chat, tool_instance, permissions_config, agent_definition)
|
|
223
|
-
|
|
224
|
-
|
|
222
|
+
def wrap_and_add_tool(chat, tool_instance, permissions_config, agent_definition, source: :builtin, metadata: {})
|
|
223
|
+
base_tool = tool_instance # Keep reference to unwrapped tool
|
|
224
|
+
wrapped_tool = wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
|
|
225
|
+
|
|
226
|
+
# Register in tool registry (Plan 025)
|
|
227
|
+
chat.tool_registry.register(
|
|
228
|
+
wrapped_tool,
|
|
229
|
+
base_tool: base_tool,
|
|
230
|
+
source: source,
|
|
231
|
+
metadata: metadata.merge(permissions: permissions_config),
|
|
232
|
+
)
|
|
225
233
|
end
|
|
226
234
|
|
|
227
235
|
# Resolve permissions for a default/plugin tool
|
|
@@ -301,7 +309,14 @@ module SwarmSDK
|
|
|
301
309
|
|
|
302
310
|
permissions_config = resolve_default_permissions(tool_name, agent_definition)
|
|
303
311
|
|
|
304
|
-
wrap_and_add_tool(
|
|
312
|
+
wrap_and_add_tool(
|
|
313
|
+
chat,
|
|
314
|
+
tool_instance,
|
|
315
|
+
permissions_config,
|
|
316
|
+
agent_definition,
|
|
317
|
+
source: :plugin,
|
|
318
|
+
metadata: { plugin_name: plugin.class.name },
|
|
319
|
+
)
|
|
305
320
|
end
|
|
306
321
|
end
|
|
307
322
|
end
|
|
@@ -364,7 +379,12 @@ module SwarmSDK
|
|
|
364
379
|
delegating_chat: chat,
|
|
365
380
|
)
|
|
366
381
|
|
|
367
|
-
|
|
382
|
+
# Register in tool registry (Plan 025)
|
|
383
|
+
chat.tool_registry.register(
|
|
384
|
+
tool,
|
|
385
|
+
source: :delegation,
|
|
386
|
+
metadata: { delegate_name: delegate_name },
|
|
387
|
+
)
|
|
368
388
|
end
|
|
369
389
|
end
|
|
370
390
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
# Base class for all SwarmSDK tools
|
|
6
|
+
#
|
|
7
|
+
# Provides:
|
|
8
|
+
# - Declarative removability control
|
|
9
|
+
# - Common tool functionality
|
|
10
|
+
# - Standard initialization patterns
|
|
11
|
+
#
|
|
12
|
+
# ## Removability
|
|
13
|
+
#
|
|
14
|
+
# Tools can be marked as non-removable to ensure they're always available:
|
|
15
|
+
#
|
|
16
|
+
# class Think < Base
|
|
17
|
+
# removable false
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# Non-removable tools are included even when skills specify a limited toolset.
|
|
21
|
+
#
|
|
22
|
+
# @example Removable tool (default)
|
|
23
|
+
# class Read < Base
|
|
24
|
+
# # removable true # Default, can omit
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @example Non-removable tool
|
|
28
|
+
# class Think < Base
|
|
29
|
+
# removable false # Always available
|
|
30
|
+
# end
|
|
31
|
+
class Base < RubyLLM::Tool
|
|
32
|
+
class << self
|
|
33
|
+
# Whether this tool can be deactivated by LoadSkill
|
|
34
|
+
#
|
|
35
|
+
# Non-removable tools are ALWAYS active regardless of skill toolset.
|
|
36
|
+
# Use for essential tools that agents should never lose.
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] True if removable (default: true)
|
|
39
|
+
def removable?
|
|
40
|
+
@removable.nil? ? true : @removable
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Mark tool as removable or non-removable
|
|
44
|
+
#
|
|
45
|
+
# @param value [Boolean] Whether tool can be removed
|
|
46
|
+
# @return [void]
|
|
47
|
+
#
|
|
48
|
+
# @example Make tool always available
|
|
49
|
+
# removable false
|
|
50
|
+
def removable(value)
|
|
51
|
+
@removable = value
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Instance method for checking removability
|
|
56
|
+
#
|
|
57
|
+
# @return [Boolean]
|
|
58
|
+
def removable?
|
|
59
|
+
self.class.removable?
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/swarm_sdk/tools/bash.rb
CHANGED
|
@@ -6,7 +6,7 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# Executes commands in a persistent shell session with timeout support.
|
|
8
8
|
# Provides comprehensive guidance on proper usage patterns.
|
|
9
|
-
class Bash <
|
|
9
|
+
class Bash < Base
|
|
10
10
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
11
11
|
class << self
|
|
12
12
|
def creation_requirements
|
|
@@ -6,7 +6,9 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# Returns current temporal information in a consistent format.
|
|
8
8
|
# Agents use this when they need to know what day/time it is.
|
|
9
|
-
class Clock <
|
|
9
|
+
class Clock < Base
|
|
10
|
+
removable false # Clock is always available
|
|
11
|
+
|
|
10
12
|
description <<~DESC
|
|
11
13
|
Get current date and time.
|
|
12
14
|
|
|
@@ -7,7 +7,8 @@ module SwarmSDK
|
|
|
7
7
|
# Creates agent-specific collaboration tools (e.g., WorkWithBackend)
|
|
8
8
|
# that allow one agent to work with another agent.
|
|
9
9
|
# Supports pre/post delegation hooks for customization.
|
|
10
|
-
class Delegate <
|
|
10
|
+
class Delegate < Base
|
|
11
|
+
removable true # Delegate tools can be controlled by skills
|
|
11
12
|
# Tool name prefix for delegation tools
|
|
12
13
|
# Change this to customize the tool naming pattern (e.g., "DelegateTaskTo", "AskAgent", etc.)
|
|
13
14
|
TOOL_NAME_PREFIX = "WorkWith"
|
|
@@ -19,10 +20,20 @@ module SwarmSDK
|
|
|
19
20
|
# Used both when creating Delegate instances and when predicting tool names
|
|
20
21
|
# for agent context setup.
|
|
21
22
|
#
|
|
23
|
+
# Converts names to PascalCase: backend → Backend, slack_agent → SlackAgent
|
|
24
|
+
#
|
|
22
25
|
# @param delegate_name [String, Symbol] Name of the delegate agent
|
|
23
|
-
# @return [String] Tool name (e.g., "WorkWithBackend")
|
|
26
|
+
# @return [String] Tool name (e.g., "WorkWithBackend", "WorkWithSlackAgent")
|
|
27
|
+
#
|
|
28
|
+
# @example Simple name
|
|
29
|
+
# tool_name_for(:backend) # => "WorkWithBackend"
|
|
30
|
+
#
|
|
31
|
+
# @example Name with underscore
|
|
32
|
+
# tool_name_for(:slack_agent) # => "WorkWithSlackAgent"
|
|
24
33
|
def tool_name_for(delegate_name)
|
|
25
|
-
|
|
34
|
+
# Convert to PascalCase: split on underscore, capitalize each part, join
|
|
35
|
+
pascal_case = delegate_name.to_s.split("_").map(&:capitalize).join
|
|
36
|
+
"#{TOOL_NAME_PREFIX}#{pascal_case}"
|
|
26
37
|
end
|
|
27
38
|
end
|
|
28
39
|
|
data/lib/swarm_sdk/tools/edit.rb
CHANGED
|
@@ -7,7 +7,7 @@ module SwarmSDK
|
|
|
7
7
|
# Uses exact string matching to find and replace content.
|
|
8
8
|
# Requires unique matches and proper Read tool usage beforehand.
|
|
9
9
|
# Enforces read-before-edit rule.
|
|
10
|
-
class Edit <
|
|
10
|
+
class Edit < Base
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
13
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
data/lib/swarm_sdk/tools/glob.rb
CHANGED
|
@@ -6,7 +6,7 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# Finds files and directories matching glob patterns, sorted by modification time.
|
|
8
8
|
# Works efficiently with any codebase size.
|
|
9
|
-
class Glob <
|
|
9
|
+
class Glob < Base
|
|
10
10
|
include PathResolver
|
|
11
11
|
|
|
12
12
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
data/lib/swarm_sdk/tools/grep.rb
CHANGED
|
@@ -6,7 +6,7 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# Powerful search capabilities with regex support, context lines, and filtering.
|
|
8
8
|
# Built on ripgrep (rg) for fast, efficient searching.
|
|
9
|
-
class Grep <
|
|
9
|
+
class Grep < Base
|
|
10
10
|
include PathResolver
|
|
11
11
|
|
|
12
12
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Tools
|
|
5
|
+
# Lazy-loading wrapper for MCP tools
|
|
6
|
+
#
|
|
7
|
+
# Creates minimal tool stub without calling tools/list.
|
|
8
|
+
# Schema is fetched on-demand when LLM needs it.
|
|
9
|
+
#
|
|
10
|
+
# ## Boot Optimization
|
|
11
|
+
#
|
|
12
|
+
# When MCP server tools are pre-specified in configuration:
|
|
13
|
+
# - Boot time: Create stubs instantly (no RPC)
|
|
14
|
+
# - First LLM request: Fetch schema lazily (~100ms one-time cost)
|
|
15
|
+
# - Subsequent requests: Use cached schema (instant)
|
|
16
|
+
#
|
|
17
|
+
# ## Thread Safety
|
|
18
|
+
#
|
|
19
|
+
# Schema loading is protected by Async::Semaphore with double-check pattern
|
|
20
|
+
# to ensure only one fiber fetches the schema even under concurrent access.
|
|
21
|
+
#
|
|
22
|
+
# @example Creating a stub
|
|
23
|
+
# coordinator = RubyLLM::MCP::Coordinator.new(client)
|
|
24
|
+
# stub = McpToolStub.new(
|
|
25
|
+
# coordinator: coordinator,
|
|
26
|
+
# name: "search_code",
|
|
27
|
+
# description: "Search code in repository"
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @example Schema is fetched lazily
|
|
31
|
+
# stub.params_schema # First access triggers tools/list RPC
|
|
32
|
+
# stub.params_schema # Cached, instant
|
|
33
|
+
class McpToolStub < Base
|
|
34
|
+
removable true # MCP tools can be controlled by skills
|
|
35
|
+
|
|
36
|
+
attr_reader :name, :client
|
|
37
|
+
|
|
38
|
+
# Create a new MCP tool stub
|
|
39
|
+
#
|
|
40
|
+
# @param client [RubyLLM::MCP::Client] MCP client instance
|
|
41
|
+
# @param name [String] Tool name
|
|
42
|
+
# @param description [String, nil] Tool description (optional, fetched if nil)
|
|
43
|
+
# @param schema [Hash, nil] Tool input schema (optional, fetched if nil)
|
|
44
|
+
#
|
|
45
|
+
# @example Minimal stub (lazy description + schema)
|
|
46
|
+
# McpToolStub.new(client: client, name: "search")
|
|
47
|
+
#
|
|
48
|
+
# @example With description (lazy schema only)
|
|
49
|
+
# McpToolStub.new(
|
|
50
|
+
# client: client,
|
|
51
|
+
# name: "search",
|
|
52
|
+
# description: "Search the codebase"
|
|
53
|
+
# )
|
|
54
|
+
#
|
|
55
|
+
# @example Fully specified (no lazy loading)
|
|
56
|
+
# McpToolStub.new(
|
|
57
|
+
# client: client,
|
|
58
|
+
# name: "search",
|
|
59
|
+
# description: "Search the codebase",
|
|
60
|
+
# schema: { type: "object", properties: {...} }
|
|
61
|
+
# )
|
|
62
|
+
def initialize(client:, name:, description: nil, schema: nil)
|
|
63
|
+
super()
|
|
64
|
+
@client = client
|
|
65
|
+
@name = name
|
|
66
|
+
@mcp_name = name
|
|
67
|
+
@description = description || "MCP tool: #{name}"
|
|
68
|
+
@input_schema = schema
|
|
69
|
+
@schema_loaded = !schema.nil?
|
|
70
|
+
@schema_mutex = Async::Semaphore.new(1) # Thread-safe schema loading
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Get tool description
|
|
74
|
+
#
|
|
75
|
+
# @return [String]
|
|
76
|
+
attr_reader :description
|
|
77
|
+
|
|
78
|
+
# Get parameter schema (lazy-loaded on first access)
|
|
79
|
+
#
|
|
80
|
+
# This method is called by RubyLLM when building tool schemas for LLM requests.
|
|
81
|
+
# On first access, it triggers a tools/list RPC to fetch the schema.
|
|
82
|
+
#
|
|
83
|
+
# @return [Hash, nil] JSON Schema for tool parameters
|
|
84
|
+
def params_schema
|
|
85
|
+
ensure_schema_loaded!
|
|
86
|
+
@input_schema
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Execute the MCP tool
|
|
90
|
+
#
|
|
91
|
+
# Calls the MCP server's tools/call endpoint with the provided parameters.
|
|
92
|
+
# Schema is NOT required for execution - the server validates parameters.
|
|
93
|
+
#
|
|
94
|
+
# @param params [Hash] Tool parameters
|
|
95
|
+
# @return [String, Hash] Tool result content or error hash
|
|
96
|
+
def execute(**params)
|
|
97
|
+
# Use client.call_tool (client has internal coordinator)
|
|
98
|
+
result = @client.call_tool(
|
|
99
|
+
name: @mcp_name,
|
|
100
|
+
arguments: params,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# client.call_tool returns the result content directly
|
|
104
|
+
result
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
# Lazy-load schema on first access (when LLM needs it)
|
|
110
|
+
#
|
|
111
|
+
# Thread-safe via semaphore with double-check pattern.
|
|
112
|
+
# Multiple concurrent fibers will only trigger one fetch.
|
|
113
|
+
#
|
|
114
|
+
# @return [void]
|
|
115
|
+
def ensure_schema_loaded!
|
|
116
|
+
return if @schema_loaded
|
|
117
|
+
|
|
118
|
+
@schema_mutex.acquire do
|
|
119
|
+
return if @schema_loaded # Double-check after acquiring lock
|
|
120
|
+
|
|
121
|
+
# Fetch tool info from client (calls tools/list if not cached)
|
|
122
|
+
tool_info = @client.tool_info(@mcp_name)
|
|
123
|
+
|
|
124
|
+
if tool_info
|
|
125
|
+
@description = tool_info["description"] || @description
|
|
126
|
+
@input_schema = tool_info["inputSchema"]
|
|
127
|
+
else
|
|
128
|
+
# Tool doesn't exist on server - schema remains nil
|
|
129
|
+
RubyLLM.logger.warn("SwarmSDK: MCP tool '#{@mcp_name}' not found on server during schema fetch")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
@schema_loaded = true
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -8,7 +8,7 @@ module SwarmSDK
|
|
|
8
8
|
# Each edit sees the result of all previous edits, allowing for
|
|
9
9
|
# coordinated multi-step transformations.
|
|
10
10
|
# Enforces read-before-edit rule.
|
|
11
|
-
class MultiEdit <
|
|
11
|
+
class MultiEdit < Base
|
|
12
12
|
include PathResolver
|
|
13
13
|
|
|
14
14
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
data/lib/swarm_sdk/tools/read.rb
CHANGED
|
@@ -7,7 +7,7 @@ module SwarmSDK
|
|
|
7
7
|
# Supports reading entire files or specific line ranges with line numbers.
|
|
8
8
|
# Provides system reminders to guide proper usage.
|
|
9
9
|
# Tracks reads per agent for enforcing read-before-write/edit rules.
|
|
10
|
-
class Read <
|
|
10
|
+
class Read < Base
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
13
|
# NOTE: Line length and limit now accessed via SwarmSDK.config
|
|
@@ -7,7 +7,7 @@ module SwarmSDK
|
|
|
7
7
|
#
|
|
8
8
|
# Shows all entries in the shared scratchpad with their metadata.
|
|
9
9
|
# All agents in the swarm share the same scratchpad.
|
|
10
|
-
class ScratchpadList <
|
|
10
|
+
class ScratchpadList < Base
|
|
11
11
|
define_method(:name) { "ScratchpadList" }
|
|
12
12
|
|
|
13
13
|
description <<~DESC
|
|
@@ -7,7 +7,7 @@ module SwarmSDK
|
|
|
7
7
|
#
|
|
8
8
|
# Retrieves content stored by any agent using scratchpad_write.
|
|
9
9
|
# All agents in the swarm share the same scratchpad.
|
|
10
|
-
class ScratchpadRead <
|
|
10
|
+
class ScratchpadRead < Base
|
|
11
11
|
define_method(:name) { "ScratchpadRead" }
|
|
12
12
|
|
|
13
13
|
description <<~DESC
|
|
@@ -8,7 +8,7 @@ module SwarmSDK
|
|
|
8
8
|
# Stores content in volatile, shared storage for temporary communication.
|
|
9
9
|
# All agents in the swarm share the same scratchpad.
|
|
10
10
|
# Data is lost when the process ends (not persisted).
|
|
11
|
-
class ScratchpadWrite <
|
|
11
|
+
class ScratchpadWrite < Base
|
|
12
12
|
define_method(:name) { "ScratchpadWrite" }
|
|
13
13
|
|
|
14
14
|
description <<~DESC
|
|
@@ -11,7 +11,9 @@ module SwarmSDK
|
|
|
11
11
|
# This is inspired by research showing that explicitly articulating reasoning steps
|
|
12
12
|
# (chain-of-thought prompting) leads to significantly better outcomes, especially
|
|
13
13
|
# for complex tasks requiring multi-step reasoning or arithmetic.
|
|
14
|
-
class Think <
|
|
14
|
+
class Think < Base
|
|
15
|
+
removable false # Think is always available
|
|
16
|
+
|
|
15
17
|
def name
|
|
16
18
|
"Think"
|
|
17
19
|
end
|
|
@@ -6,7 +6,9 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# This tool helps agents track progress on complex multi-step tasks.
|
|
8
8
|
# Each agent maintains its own independent todo list.
|
|
9
|
-
class TodoWrite <
|
|
9
|
+
class TodoWrite < Base
|
|
10
|
+
removable false # TodoWrite is always available
|
|
11
|
+
|
|
10
12
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
|
11
13
|
class << self
|
|
12
14
|
def creation_requirements
|
|
@@ -6,7 +6,7 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# Fetches content from URLs, converts HTML to markdown, and processes it
|
|
8
8
|
# using an AI model to extract information based on a provided prompt.
|
|
9
|
-
class WebFetch <
|
|
9
|
+
class WebFetch < Base
|
|
10
10
|
def initialize
|
|
11
11
|
super()
|
|
12
12
|
@cache = {}
|
|
@@ -7,7 +7,7 @@ module SwarmSDK
|
|
|
7
7
|
# Creates new files or overwrites existing files.
|
|
8
8
|
# Enforces read-before-write rule for existing files.
|
|
9
9
|
# Includes validation and usage guidelines via system reminders.
|
|
10
|
-
class Write <
|
|
10
|
+
class Write < Base
|
|
11
11
|
include PathResolver
|
|
12
12
|
|
|
13
13
|
# Factory pattern: declare what parameters this tool needs for instantiation
|
data/lib/swarm_sdk/version.rb
CHANGED