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
|
@@ -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 }
|