swarm_sdk 2.6.2 → 2.7.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 +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/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
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Agent
|
|
5
|
+
# Per-agent tool registry managing available and active tools
|
|
6
|
+
#
|
|
7
|
+
# ## Architecture
|
|
8
|
+
#
|
|
9
|
+
# - **Available tools**: All tool instances the agent CAN use (registry)
|
|
10
|
+
# - **Active tools**: Subset sent to LLM based on skill state
|
|
11
|
+
#
|
|
12
|
+
# ## Thread Safety
|
|
13
|
+
#
|
|
14
|
+
# Registry access is protected by Async::Semaphore for fiber-safe operations.
|
|
15
|
+
#
|
|
16
|
+
# @example Registering tools
|
|
17
|
+
# registry = ToolRegistry.new
|
|
18
|
+
# registry.register(Read.new, source: :builtin)
|
|
19
|
+
# registry.register(delegate_tool, source: :delegation, metadata: { delegate_name: :backend })
|
|
20
|
+
#
|
|
21
|
+
# @example Getting active tools (no skill)
|
|
22
|
+
# active = registry.active_tools(skill_state: nil)
|
|
23
|
+
# # Returns ALL available tools
|
|
24
|
+
#
|
|
25
|
+
# @example Getting active tools (with skill)
|
|
26
|
+
# skill_state = SkillState.new( # From SwarmMemory plugin
|
|
27
|
+
# file_path: "skill/audit.md",
|
|
28
|
+
# tools: ["Read", "Grep"],
|
|
29
|
+
# permissions: { "Bash" => { deny_commands: ["rm"] } }
|
|
30
|
+
# )
|
|
31
|
+
# active = registry.active_tools(skill_state: skill_state)
|
|
32
|
+
# # Returns: skill's tools + non-removable tools
|
|
33
|
+
class ToolRegistry
|
|
34
|
+
# Tool metadata stored in registry
|
|
35
|
+
#
|
|
36
|
+
# @!attribute instance [r] Tool instance (possibly wrapped with permissions)
|
|
37
|
+
# @!attribute base_instance [r] Unwrapped tool instance (for skill permission override)
|
|
38
|
+
# @!attribute removable [r] Can be deactivated by skills
|
|
39
|
+
# @!attribute source [r] Tool source (:builtin, :delegation, :mcp, :plugin, :custom)
|
|
40
|
+
# @!attribute metadata [r] Source-specific metadata
|
|
41
|
+
ToolEntry = Data.define(:instance, :base_instance, :removable, :source, :metadata)
|
|
42
|
+
|
|
43
|
+
def initialize
|
|
44
|
+
@available_tools = {} # String name => ToolEntry
|
|
45
|
+
@mutex = Async::Semaphore.new(1) # Fiber-safe mutex
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Register a tool in the available tools registry
|
|
49
|
+
#
|
|
50
|
+
# @param tool [RubyLLM::Tool] Tool instance (possibly wrapped)
|
|
51
|
+
# @param base_tool [RubyLLM::Tool, nil] Unwrapped instance (for permission override)
|
|
52
|
+
# @param source [Symbol] Tool source (:builtin, :delegation, :mcp, :plugin, :custom)
|
|
53
|
+
# @param metadata [Hash] Source-specific metadata (server_name, plugin_name, etc.)
|
|
54
|
+
# @return [void]
|
|
55
|
+
#
|
|
56
|
+
# @example Register builtin tool
|
|
57
|
+
# registry.register(Read.new, source: :builtin)
|
|
58
|
+
#
|
|
59
|
+
# @example Register delegation tool
|
|
60
|
+
# registry.register(delegate_tool, source: :delegation, metadata: { delegate_name: :backend })
|
|
61
|
+
#
|
|
62
|
+
# @example Register MCP tool
|
|
63
|
+
# registry.register(mcp_tool, source: :mcp, metadata: { server_name: "codebase" })
|
|
64
|
+
#
|
|
65
|
+
# @example Register with permission wrapper
|
|
66
|
+
# wrapped_tool = PermissionWrapper.new(base_tool, permissions)
|
|
67
|
+
# registry.register(wrapped_tool, base_tool: base_tool, source: :builtin)
|
|
68
|
+
def register(tool, base_tool: nil, source:, metadata: {})
|
|
69
|
+
@mutex.acquire do
|
|
70
|
+
# Infer removability from tool class
|
|
71
|
+
removable = tool.respond_to?(:removable?) ? tool.removable? : true
|
|
72
|
+
|
|
73
|
+
@available_tools[tool.name] = ToolEntry.new(
|
|
74
|
+
instance: tool,
|
|
75
|
+
base_instance: base_tool || tool, # If no base, use same instance
|
|
76
|
+
removable: removable,
|
|
77
|
+
source: source,
|
|
78
|
+
metadata: metadata,
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Unregister a tool (for testing/cleanup)
|
|
84
|
+
#
|
|
85
|
+
# @param name [String, Symbol] Tool name
|
|
86
|
+
# @return [ToolEntry, nil] Removed entry
|
|
87
|
+
def unregister(name)
|
|
88
|
+
@mutex.acquire do
|
|
89
|
+
@available_tools.delete(name.to_s)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Get active tools based on skill state
|
|
94
|
+
#
|
|
95
|
+
# Returns Hash of tool instances ready for RubyLLM::Chat.
|
|
96
|
+
#
|
|
97
|
+
# Logic:
|
|
98
|
+
# - If skill_state is nil: Return ALL available tools
|
|
99
|
+
# - If skill_state restricts tools: Return skill's tools + non-removable tools
|
|
100
|
+
# - Skill permissions are applied during activation (wrapping base_instance)
|
|
101
|
+
#
|
|
102
|
+
# @param skill_state [Object, nil] Skill state object (from plugin), or nil for all
|
|
103
|
+
# @param tool_configurator [ToolConfigurator, nil] For permission wrapping
|
|
104
|
+
# @param agent_definition [Agent::Definition, nil] For permission wrapping
|
|
105
|
+
# @return [Hash{String => RubyLLM::Tool}] name => instance mapping
|
|
106
|
+
#
|
|
107
|
+
# @example No skill loaded - all tools
|
|
108
|
+
# registry.active_tools(skill_state: nil)
|
|
109
|
+
# # => { "Read" => <Read>, "WorkWithBackend" => <Delegate>, ... }
|
|
110
|
+
#
|
|
111
|
+
# @example Skill loaded with focused toolset
|
|
112
|
+
# registry.active_tools(skill_state: skill_state)
|
|
113
|
+
# # => { "Read" => <Read>, "WorkWithBackend" => <Delegate>, "Think" => <Think>, "MemoryRead" => <MemoryRead> }
|
|
114
|
+
# # Includes: requested tools + non-removable tools
|
|
115
|
+
def active_tools(skill_state: nil, tool_configurator: nil, agent_definition: nil)
|
|
116
|
+
@mutex.acquire do
|
|
117
|
+
result = if skill_state&.restricts_tools?
|
|
118
|
+
# Skill loaded with tool restriction - only skill's tools + non-removable
|
|
119
|
+
filtered = {}
|
|
120
|
+
|
|
121
|
+
# Always include non-removable tools (use wrapped instance)
|
|
122
|
+
@available_tools.each do |name, entry|
|
|
123
|
+
filtered[name] = entry.instance unless entry.removable
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Add requested tools from skill
|
|
127
|
+
skill_state.tools.each do |name|
|
|
128
|
+
entry = @available_tools[name.to_s]
|
|
129
|
+
next unless entry
|
|
130
|
+
|
|
131
|
+
# Check if skill has custom permissions for this tool
|
|
132
|
+
skill_permissions = skill_state.permissions_for(name)
|
|
133
|
+
|
|
134
|
+
if skill_permissions && tool_configurator && agent_definition
|
|
135
|
+
# Skill overrides permissions - wrap the BASE instance
|
|
136
|
+
wrapped = tool_configurator.wrap_tool_with_permissions(
|
|
137
|
+
entry.base_instance,
|
|
138
|
+
skill_permissions,
|
|
139
|
+
agent_definition,
|
|
140
|
+
)
|
|
141
|
+
filtered[name.to_s] = wrapped
|
|
142
|
+
else
|
|
143
|
+
# No skill permission override - use registered instance
|
|
144
|
+
filtered[name.to_s] = entry.instance
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
filtered
|
|
149
|
+
else
|
|
150
|
+
# No skill OR skill doesn't restrict tools - all available tools
|
|
151
|
+
@available_tools.transform_values(&:instance)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
result
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check if tool exists in registry
|
|
159
|
+
#
|
|
160
|
+
# @param name [String, Symbol] Tool name
|
|
161
|
+
# @return [Boolean]
|
|
162
|
+
def has_tool?(name)
|
|
163
|
+
@available_tools.key?(name.to_s)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Get all available tool names
|
|
167
|
+
#
|
|
168
|
+
# @return [Array<String>]
|
|
169
|
+
def tool_names
|
|
170
|
+
@available_tools.keys
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Get tool entry with metadata
|
|
174
|
+
#
|
|
175
|
+
# @param name [String, Symbol] Tool name
|
|
176
|
+
# @return [ToolEntry, nil]
|
|
177
|
+
def get(name)
|
|
178
|
+
@available_tools[name.to_s]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Get all non-removable tool names
|
|
182
|
+
#
|
|
183
|
+
# @return [Array<String>]
|
|
184
|
+
def non_removable_tool_names
|
|
185
|
+
@available_tools.select { |_name, entry| !entry.removable }.keys
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
@@ -446,6 +446,10 @@ module SwarmSDK
|
|
|
446
446
|
if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
|
|
447
447
|
agent_builder.coding_agent(all_agents_hash[:coding_agent])
|
|
448
448
|
end
|
|
449
|
+
|
|
450
|
+
if !all_agents_hash[:streaming].nil? && !agent_builder.streaming_set?
|
|
451
|
+
agent_builder.streaming(all_agents_hash[:streaming])
|
|
452
|
+
end
|
|
449
453
|
end
|
|
450
454
|
|
|
451
455
|
# Validate all_agents filesystem tools
|
data/lib/swarm_sdk/config.rb
CHANGED
|
@@ -94,6 +94,7 @@ module SwarmSDK
|
|
|
94
94
|
webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
|
|
95
95
|
allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
|
|
96
96
|
env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
|
|
97
|
+
streaming: ["SWARM_SDK_STREAMING", true],
|
|
97
98
|
}.freeze
|
|
98
99
|
|
|
99
100
|
class << self
|
|
@@ -282,7 +283,7 @@ module SwarmSDK
|
|
|
282
283
|
# @return [Integer, Float, Boolean, String] The parsed value
|
|
283
284
|
def parse_env_value(value, key)
|
|
284
285
|
case key
|
|
285
|
-
when :allow_filesystem_tools, :env_interpolation
|
|
286
|
+
when :allow_filesystem_tools, :env_interpolation, :streaming
|
|
286
287
|
# Convert string to boolean
|
|
287
288
|
case value.to_s.downcase
|
|
288
289
|
when "true", "yes", "1", "on", "enabled"
|
|
@@ -99,6 +99,7 @@ module SwarmSDK
|
|
|
99
99
|
headers(all_agents_cfg[:headers]) if all_agents_cfg[:headers]
|
|
100
100
|
coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
|
|
101
101
|
disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
|
|
102
|
+
streaming(all_agents_cfg[:streaming]) unless all_agents_cfg[:streaming].nil?
|
|
102
103
|
|
|
103
104
|
if all_agents_hks.any?
|
|
104
105
|
all_agents_hks.each do |event, hook_specs|
|
|
@@ -162,6 +163,7 @@ module SwarmSDK
|
|
|
162
163
|
bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
|
163
164
|
disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
164
165
|
shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
|
|
166
|
+
streaming(config[:streaming]) unless config[:streaming].nil?
|
|
165
167
|
|
|
166
168
|
if config[:tools]&.any?
|
|
167
169
|
tool_names = config[:tools].map { |t| t.is_a?(Hash) ? t[:name] : t }
|
|
@@ -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
|