swarm_sdk 2.0.0.pre.2
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 +7 -0
- data/lib/swarm_sdk/agent/builder.rb +333 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +271 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +99 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +114 -0
- data/lib/swarm_sdk/agent/chat.rb +779 -0
- data/lib/swarm_sdk/agent/context.rb +108 -0
- data/lib/swarm_sdk/agent/definition.rb +335 -0
- data/lib/swarm_sdk/configuration.rb +251 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +163 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +143 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +83 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +46 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +237 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +224 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +62 -0
- data/lib/swarm_sdk/swarm/builder.rb +240 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +267 -0
- data/lib/swarm_sdk/swarm.rb +837 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/delegate.rb +152 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +231 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +73 -0
- data/lib/swarm_sdk/tools/scratchpad_list.rb +88 -0
- data/lib/swarm_sdk/tools/scratchpad_read.rb +59 -0
- data/lib/swarm_sdk/tools/scratchpad_write.rb +88 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad.rb +153 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +69 -0
- metadata +169 -0
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
class Swarm
|
5
|
+
# Handles the complex 5-pass agent initialization process
|
6
|
+
#
|
7
|
+
# Responsibilities:
|
8
|
+
# - Create all agent chat instances (pass 1)
|
9
|
+
# - Register delegation tools (pass 2)
|
10
|
+
# - Setup agent contexts (pass 3)
|
11
|
+
# - Configure hook system (pass 4)
|
12
|
+
# - Apply YAML hooks if present (pass 5)
|
13
|
+
#
|
14
|
+
# This encapsulates the complex initialization logic that was previously
|
15
|
+
# embedded in Swarm#initialize_agents.
|
16
|
+
class AgentInitializer
|
17
|
+
def initialize(swarm, agent_definitions, global_semaphore, hook_registry, scratchpad, config_for_hooks: nil)
|
18
|
+
@swarm = swarm
|
19
|
+
@agent_definitions = agent_definitions
|
20
|
+
@global_semaphore = global_semaphore
|
21
|
+
@hook_registry = hook_registry
|
22
|
+
@scratchpad = scratchpad
|
23
|
+
@config_for_hooks = config_for_hooks
|
24
|
+
@agents = {}
|
25
|
+
@agent_contexts = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
# Initialize all agents with their chat instances and tools
|
29
|
+
#
|
30
|
+
# This implements a 5-pass algorithm:
|
31
|
+
# 1. Create all Agent::Chat instances
|
32
|
+
# 2. Register delegation tools (agents can call each other)
|
33
|
+
# 3. Setup agent contexts for tracking
|
34
|
+
# 4. Configure hook system
|
35
|
+
# 5. Apply YAML hooks (if loaded from YAML)
|
36
|
+
#
|
37
|
+
# @return [Hash] agents hash { agent_name => Agent::Chat }
|
38
|
+
def initialize_all
|
39
|
+
pass_1_create_agents
|
40
|
+
pass_2_register_delegation_tools
|
41
|
+
pass_3_setup_contexts
|
42
|
+
pass_4_configure_hooks
|
43
|
+
pass_5_apply_yaml_hooks
|
44
|
+
|
45
|
+
@agents
|
46
|
+
end
|
47
|
+
|
48
|
+
# Provide access to agent contexts for Swarm
|
49
|
+
attr_reader :agent_contexts
|
50
|
+
|
51
|
+
# Create a tool that delegates work to another agent
|
52
|
+
#
|
53
|
+
# This method is public for testing delegation from Swarm.
|
54
|
+
#
|
55
|
+
# @param name [String] Delegate agent name
|
56
|
+
# @param description [String] Delegate agent description
|
57
|
+
# @param delegate_chat [Agent::Chat] The delegate's chat instance
|
58
|
+
# @param agent_name [Symbol] Name of the delegating agent
|
59
|
+
# @return [Tools::Delegate] Delegation tool
|
60
|
+
def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
|
61
|
+
Tools::Delegate.new(
|
62
|
+
delegate_name: name,
|
63
|
+
delegate_description: description,
|
64
|
+
delegate_chat: delegate_chat,
|
65
|
+
agent_name: agent_name,
|
66
|
+
swarm: @swarm,
|
67
|
+
hook_registry: @hook_registry,
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Pass 1: Create all agent chat instances
|
74
|
+
#
|
75
|
+
# This creates the Agent::Chat instances but doesn't wire them together yet.
|
76
|
+
# Each agent gets its own chat instance with configured tools.
|
77
|
+
def pass_1_create_agents
|
78
|
+
tool_configurator = ToolConfigurator.new(@swarm, @scratchpad)
|
79
|
+
|
80
|
+
@agent_definitions.each do |name, agent_definition|
|
81
|
+
chat = create_agent_chat(name, agent_definition, tool_configurator)
|
82
|
+
@agents[name] = chat
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Pass 2: Register agent delegation tools
|
87
|
+
#
|
88
|
+
# Now that all agents exist, we can create delegation tools
|
89
|
+
# that allow agents to call each other.
|
90
|
+
def pass_2_register_delegation_tools
|
91
|
+
@agent_definitions.each do |name, agent_definition|
|
92
|
+
register_delegation_tools(@agents[name], agent_definition.delegates_to, agent_name: name)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Pass 3: Setup agent contexts
|
97
|
+
#
|
98
|
+
# Create Agent::Context for each agent to track delegations and metadata.
|
99
|
+
# This is needed regardless of whether logging is enabled.
|
100
|
+
def pass_3_setup_contexts
|
101
|
+
@agents.each do |agent_name, chat|
|
102
|
+
agent_definition = @agent_definitions[agent_name]
|
103
|
+
delegate_tool_names = agent_definition.delegates_to.map do |delegate_name|
|
104
|
+
"DelegateTaskTo#{delegate_name.to_s.capitalize}"
|
105
|
+
end
|
106
|
+
|
107
|
+
# Create agent context
|
108
|
+
context = Agent::Context.new(
|
109
|
+
name: agent_name,
|
110
|
+
delegation_tools: delegate_tool_names,
|
111
|
+
metadata: {},
|
112
|
+
)
|
113
|
+
@agent_contexts[agent_name] = context
|
114
|
+
|
115
|
+
# Always set agent context (needed for delegation tracking)
|
116
|
+
chat.setup_context(context) if chat.respond_to?(:setup_context)
|
117
|
+
|
118
|
+
# Configure logging callbacks if logging is enabled
|
119
|
+
next unless LogStream.emitter
|
120
|
+
|
121
|
+
chat.setup_logging if chat.respond_to?(:setup_logging)
|
122
|
+
|
123
|
+
# Emit any model lookup warnings that occurred during initialization
|
124
|
+
chat.emit_model_lookup_warning(agent_name) if chat.respond_to?(:emit_model_lookup_warning)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Pass 4: Configure hook system
|
129
|
+
#
|
130
|
+
# Setup the callback system for each agent, integrating with RubyLLM callbacks.
|
131
|
+
def pass_4_configure_hooks
|
132
|
+
@agents.each do |agent_name, chat|
|
133
|
+
agent_definition = @agent_definitions[agent_name]
|
134
|
+
|
135
|
+
# Configure callback system (integrates with RubyLLM callbacks)
|
136
|
+
chat.setup_hooks(
|
137
|
+
registry: @hook_registry,
|
138
|
+
agent_definition: agent_definition,
|
139
|
+
swarm: @swarm,
|
140
|
+
) if chat.respond_to?(:setup_hooks)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
# Pass 5: Apply YAML hooks
|
145
|
+
#
|
146
|
+
# If the swarm was loaded from YAML with agent-specific hooks,
|
147
|
+
# apply them now via HooksAdapter.
|
148
|
+
def pass_5_apply_yaml_hooks
|
149
|
+
return unless @config_for_hooks
|
150
|
+
|
151
|
+
@agents.each do |agent_name, chat|
|
152
|
+
agent_def = @config_for_hooks.agents[agent_name]
|
153
|
+
next unless agent_def&.hooks
|
154
|
+
|
155
|
+
# Apply agent-specific hooks via Hooks::Adapter
|
156
|
+
Hooks::Adapter.apply_agent_hooks(chat, agent_name, agent_def.hooks, @swarm.name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Create Agent::Chat instance with rate limiting
|
161
|
+
#
|
162
|
+
# @param agent_name [Symbol] Agent name
|
163
|
+
# @param agent_definition [Agent::Definition] Agent definition object
|
164
|
+
# @param tool_configurator [ToolConfigurator] Tool configuration helper
|
165
|
+
# @return [Agent::Chat] Configured agent chat instance
|
166
|
+
def create_agent_chat(agent_name, agent_definition, tool_configurator)
|
167
|
+
chat = Agent::Chat.new(
|
168
|
+
definition: agent_definition.to_h,
|
169
|
+
global_semaphore: @global_semaphore,
|
170
|
+
)
|
171
|
+
|
172
|
+
# Set agent name on provider for logging (if provider supports it)
|
173
|
+
chat.provider.agent_name = agent_name if chat.provider.respond_to?(:agent_name=)
|
174
|
+
|
175
|
+
# Register tools using ToolConfigurator
|
176
|
+
tool_configurator.register_all_tools(
|
177
|
+
chat: chat,
|
178
|
+
agent_name: agent_name,
|
179
|
+
agent_definition: agent_definition,
|
180
|
+
)
|
181
|
+
|
182
|
+
# Register MCP servers using McpConfigurator
|
183
|
+
if agent_definition.mcp_servers.any?
|
184
|
+
mcp_configurator = McpConfigurator.new(@swarm)
|
185
|
+
mcp_configurator.register_mcp_servers(chat, agent_definition.mcp_servers, agent_name: agent_name)
|
186
|
+
end
|
187
|
+
|
188
|
+
chat
|
189
|
+
end
|
190
|
+
|
191
|
+
# Register agent delegation tools
|
192
|
+
#
|
193
|
+
# Creates delegation tools that allow one agent to call another.
|
194
|
+
#
|
195
|
+
# @param chat [Agent::Chat] The chat instance
|
196
|
+
# @param delegate_names [Array<Symbol>] Names of agents to delegate to
|
197
|
+
# @param agent_name [Symbol] Name of the agent doing the delegating
|
198
|
+
def register_delegation_tools(chat, delegate_names, agent_name:)
|
199
|
+
return if delegate_names.empty?
|
200
|
+
|
201
|
+
delegate_names.each do |delegate_name|
|
202
|
+
delegate_name = delegate_name.to_sym
|
203
|
+
|
204
|
+
unless @agents.key?(delegate_name)
|
205
|
+
raise ConfigurationError, "Agent delegates to unknown agent '#{delegate_name}'"
|
206
|
+
end
|
207
|
+
|
208
|
+
# Create a tool that delegates to the specified agent
|
209
|
+
delegate_agent = @agents[delegate_name]
|
210
|
+
delegate_definition = @agent_definitions[delegate_name]
|
211
|
+
|
212
|
+
tool = create_delegation_tool(
|
213
|
+
name: delegate_name.to_s,
|
214
|
+
description: delegate_definition.description,
|
215
|
+
delegate_chat: delegate_agent,
|
216
|
+
agent_name: agent_name,
|
217
|
+
)
|
218
|
+
|
219
|
+
chat.with_tool(tool)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
class Swarm
|
5
|
+
# AllAgentsBuilder for configuring settings that apply to all agents
|
6
|
+
class AllAgentsBuilder
|
7
|
+
attr_reader :hooks, :permissions_config
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@tools_list = []
|
11
|
+
@hooks = []
|
12
|
+
@permissions_config = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Get tools list
|
16
|
+
attr_reader :tools_list
|
17
|
+
|
18
|
+
# Add tools that all agents will have
|
19
|
+
def tools(*tool_names)
|
20
|
+
@tools_list.concat(tool_names)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Add hook for all agents (agent-level events only)
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# hook :pre_tool_use, matcher: "Write" do |ctx|
|
27
|
+
# # Applies to all agents
|
28
|
+
# end
|
29
|
+
def hook(event, matcher: nil, command: nil, timeout: nil, &block)
|
30
|
+
# Validate agent-level events
|
31
|
+
agent_events = [
|
32
|
+
:pre_tool_use,
|
33
|
+
:post_tool_use,
|
34
|
+
:user_prompt,
|
35
|
+
:agent_stop,
|
36
|
+
:first_message,
|
37
|
+
:pre_delegation,
|
38
|
+
:post_delegation,
|
39
|
+
:context_warning,
|
40
|
+
]
|
41
|
+
|
42
|
+
unless agent_events.include?(event)
|
43
|
+
raise ArgumentError, "Invalid all_agents hook: #{event}. Swarm-level events (:swarm_start, :swarm_stop) cannot be used in all_agents block."
|
44
|
+
end
|
45
|
+
|
46
|
+
@hooks << { event: event, matcher: matcher, command: command, timeout: timeout, block: block }
|
47
|
+
end
|
48
|
+
|
49
|
+
# Configure permissions for all agents
|
50
|
+
#
|
51
|
+
# @example
|
52
|
+
# permissions do
|
53
|
+
# Write.allow_paths "tmp/**/*"
|
54
|
+
# Write.deny_paths "tmp/secrets/**"
|
55
|
+
# Bash.allow_commands "^git status$"
|
56
|
+
# end
|
57
|
+
def permissions(&block)
|
58
|
+
@permissions_config = PermissionsBuilder.build(&block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
class Swarm
|
5
|
+
# Builder provides a beautiful Ruby DSL for building swarms
|
6
|
+
#
|
7
|
+
# The DSL combines YAML simplicity with Ruby power, enabling:
|
8
|
+
# - Fluent, chainable configuration
|
9
|
+
# - Hooks as Ruby blocks OR shell commands
|
10
|
+
# - Full Ruby language features (variables, conditionals, loops)
|
11
|
+
# - Type-safe, IDE-friendly API
|
12
|
+
#
|
13
|
+
# @example Basic usage
|
14
|
+
# swarm = SwarmSDK.build do
|
15
|
+
# name "Dev Team"
|
16
|
+
# lead :backend
|
17
|
+
#
|
18
|
+
# agent :backend do
|
19
|
+
# model "gpt-5"
|
20
|
+
# prompt "You build APIs"
|
21
|
+
# tools :Read, :Write, :Bash
|
22
|
+
#
|
23
|
+
# # Hook as Ruby block - inline logic!
|
24
|
+
# hook :pre_tool_use, matcher: "Bash" do |ctx|
|
25
|
+
# SwarmSDK::Hooks::Result.halt("Blocked!") if ctx.tool_call.parameters[:command].include?("rm -rf")
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# swarm.execute("Build auth API")
|
31
|
+
class Builder
|
32
|
+
# Main entry point for DSL
|
33
|
+
#
|
34
|
+
# @example
|
35
|
+
# swarm = SwarmSDK.build do
|
36
|
+
# name "Team"
|
37
|
+
# agent :backend { ... }
|
38
|
+
# end
|
39
|
+
class << self
|
40
|
+
def build(&block)
|
41
|
+
builder = new
|
42
|
+
builder.instance_eval(&block)
|
43
|
+
builder.build_swarm
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize
|
48
|
+
@swarm_name = nil
|
49
|
+
@lead_agent = nil
|
50
|
+
@agents = {}
|
51
|
+
@all_agents_config = nil
|
52
|
+
@swarm_hooks = []
|
53
|
+
end
|
54
|
+
|
55
|
+
# Set swarm name
|
56
|
+
def name(swarm_name)
|
57
|
+
@swarm_name = swarm_name
|
58
|
+
end
|
59
|
+
|
60
|
+
# Set lead agent
|
61
|
+
def lead(agent_name)
|
62
|
+
@lead_agent = agent_name
|
63
|
+
end
|
64
|
+
|
65
|
+
# Define an agent with fluent API
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# agent :backend do
|
69
|
+
# model "gpt-5"
|
70
|
+
# prompt "You build APIs"
|
71
|
+
# tools :Read, :Write
|
72
|
+
#
|
73
|
+
# hook :pre_tool_use, matcher: "Bash" do |ctx|
|
74
|
+
# # Inline validation logic!
|
75
|
+
# end
|
76
|
+
# end
|
77
|
+
def agent(name, &block)
|
78
|
+
builder = Agent::Builder.new(name)
|
79
|
+
builder.instance_eval(&block)
|
80
|
+
@agents[name] = builder
|
81
|
+
end
|
82
|
+
|
83
|
+
# Add swarm-level hook (swarm_start, swarm_stop only)
|
84
|
+
#
|
85
|
+
# @example Shell command
|
86
|
+
# hook :swarm_start, command: "echo 'Starting' >> log.txt"
|
87
|
+
#
|
88
|
+
# @example Ruby block
|
89
|
+
# hook :swarm_start do |ctx|
|
90
|
+
# puts "Swarm starting: #{ctx.metadata[:prompt]}"
|
91
|
+
# end
|
92
|
+
def hook(event, command: nil, timeout: nil, &block)
|
93
|
+
# Validate swarm-level events
|
94
|
+
unless [:swarm_start, :swarm_stop].include?(event)
|
95
|
+
raise ArgumentError, "Invalid swarm-level hook: #{event}. Only :swarm_start and :swarm_stop allowed at swarm level. Use all_agents { hook ... } or agent { hook ... } for other events."
|
96
|
+
end
|
97
|
+
|
98
|
+
@swarm_hooks << { event: event, command: command, timeout: timeout, block: block }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Configure all agents with a block
|
102
|
+
#
|
103
|
+
# @example
|
104
|
+
# all_agents do
|
105
|
+
# tools :Read, :Write
|
106
|
+
#
|
107
|
+
# hook :pre_tool_use, matcher: "Write" do |ctx|
|
108
|
+
# # Validation for all agents
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
def all_agents(&block)
|
112
|
+
builder = AllAgentsBuilder.new
|
113
|
+
builder.instance_eval(&block)
|
114
|
+
@all_agents_config = builder
|
115
|
+
end
|
116
|
+
|
117
|
+
# Build the actual Swarm instance
|
118
|
+
def build_swarm
|
119
|
+
raise ConfigurationError, "Swarm name not set. Use: name 'My Swarm'" unless @swarm_name
|
120
|
+
raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
|
121
|
+
raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
|
122
|
+
|
123
|
+
# Create swarm using SDK
|
124
|
+
swarm = Swarm.new(name: @swarm_name)
|
125
|
+
|
126
|
+
# Merge all_agents config into each agent
|
127
|
+
merge_all_agents_config_into_agents if @all_agents_config
|
128
|
+
|
129
|
+
# Build definitions and add to swarm
|
130
|
+
@agents.each do |_agent_name, agent_builder|
|
131
|
+
definition = agent_builder.to_definition
|
132
|
+
swarm.add_agent(definition)
|
133
|
+
end
|
134
|
+
|
135
|
+
# Set lead
|
136
|
+
swarm.lead = @lead_agent
|
137
|
+
|
138
|
+
# Apply swarm hooks (Ruby blocks)
|
139
|
+
# These are swarm-level hooks (swarm_start, swarm_stop)
|
140
|
+
@swarm_hooks.each do |hook_config|
|
141
|
+
apply_swarm_hook(swarm, hook_config)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Apply all_agents hooks (Ruby blocks)
|
145
|
+
# These become swarm-level default callbacks that apply to all agents
|
146
|
+
@all_agents_config&.hooks&.each do |hook_config|
|
147
|
+
apply_all_agents_hook(swarm, hook_config)
|
148
|
+
end
|
149
|
+
|
150
|
+
# NOTE: Agent-specific hooks are already stored in Agent::Definition.callbacks
|
151
|
+
# They'll be applied automatically during agent initialization (pass_4_configure_hooks)
|
152
|
+
# This ensures they're applied at the right time, after LogStream is set up
|
153
|
+
|
154
|
+
swarm
|
155
|
+
end
|
156
|
+
|
157
|
+
private
|
158
|
+
|
159
|
+
# Merge all_agents configuration into each agent
|
160
|
+
def merge_all_agents_config_into_agents
|
161
|
+
return unless @all_agents_config
|
162
|
+
|
163
|
+
@agents.each_value do |agent_builder|
|
164
|
+
# Merge tools (prepend all_agents tools)
|
165
|
+
all_agents_tools = @all_agents_config.tools_list
|
166
|
+
agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
|
167
|
+
|
168
|
+
# Pass all_agents permissions as default_permissions
|
169
|
+
if @all_agents_config.permissions_config.any?
|
170
|
+
agent_builder.default_permissions = @all_agents_config.permissions_config
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def apply_swarm_hook(swarm, config)
|
176
|
+
event = config[:event]
|
177
|
+
|
178
|
+
if config[:block]
|
179
|
+
# Ruby block hook - register directly
|
180
|
+
swarm.add_default_callback(event, &config[:block])
|
181
|
+
elsif config[:command]
|
182
|
+
# Shell command hook - use ShellExecutor
|
183
|
+
swarm.add_default_callback(event) do |context|
|
184
|
+
input_json = build_hook_input(context, event)
|
185
|
+
Hooks::ShellExecutor.execute(
|
186
|
+
command: config[:command],
|
187
|
+
input_json: input_json,
|
188
|
+
timeout: config[:timeout] || 60,
|
189
|
+
swarm_name: swarm.name,
|
190
|
+
event: event,
|
191
|
+
)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def apply_all_agents_hook(swarm, config)
|
197
|
+
event = config[:event]
|
198
|
+
matcher = config[:matcher]
|
199
|
+
|
200
|
+
if config[:block]
|
201
|
+
# Ruby block hook
|
202
|
+
swarm.add_default_callback(event, matcher: matcher, &config[:block])
|
203
|
+
elsif config[:command]
|
204
|
+
# Shell command hook
|
205
|
+
swarm.add_default_callback(event, matcher: matcher) do |context|
|
206
|
+
input_json = build_hook_input(context, event)
|
207
|
+
Hooks::ShellExecutor.execute(
|
208
|
+
command: config[:command],
|
209
|
+
input_json: input_json,
|
210
|
+
timeout: config[:timeout] || 60,
|
211
|
+
agent_name: context.agent_name,
|
212
|
+
swarm_name: swarm.name,
|
213
|
+
event: event,
|
214
|
+
)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def build_hook_input(context, event)
|
220
|
+
# Build JSON input for shell hooks (similar to HooksAdapter)
|
221
|
+
base = { event: event.to_s }
|
222
|
+
|
223
|
+
case event
|
224
|
+
when :pre_tool_use
|
225
|
+
base.merge(tool: context.tool_call.name, parameters: context.tool_call.parameters)
|
226
|
+
when :post_tool_use
|
227
|
+
base.merge(result: context.tool_result.content, success: context.tool_result.success?)
|
228
|
+
when :user_prompt
|
229
|
+
base.merge(prompt: context.metadata[:prompt])
|
230
|
+
when :swarm_start
|
231
|
+
base.merge(prompt: context.metadata[:prompt])
|
232
|
+
when :swarm_stop
|
233
|
+
base.merge(success: context.metadata[:success], duration: context.metadata[:duration])
|
234
|
+
else
|
235
|
+
base
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
class Swarm
|
5
|
+
# Handles MCP (Model Context Protocol) server configuration and client management
|
6
|
+
#
|
7
|
+
# Responsibilities:
|
8
|
+
# - Register MCP servers for agents
|
9
|
+
# - Initialize MCP clients (stdio, SSE, streamable transports)
|
10
|
+
# - Build transport-specific configurations
|
11
|
+
# - Track clients for cleanup
|
12
|
+
#
|
13
|
+
# This encapsulates all MCP-related logic that was previously in Swarm.
|
14
|
+
class McpConfigurator
|
15
|
+
def initialize(swarm)
|
16
|
+
@swarm = swarm
|
17
|
+
@mcp_clients = swarm.mcp_clients
|
18
|
+
end
|
19
|
+
|
20
|
+
# Register MCP servers for an agent
|
21
|
+
#
|
22
|
+
# Connects to MCP servers and registers their tools with the agent's chat instance.
|
23
|
+
# Supports stdio, SSE, and HTTP (streamable) transports.
|
24
|
+
#
|
25
|
+
# @param chat [AgentChat] The agent's chat instance
|
26
|
+
# @param mcp_server_configs [Array<Hash>] MCP server configurations
|
27
|
+
# @param agent_name [Symbol] Agent name for tracking clients
|
28
|
+
def register_mcp_servers(chat, mcp_server_configs, agent_name:)
|
29
|
+
return if mcp_server_configs.nil? || mcp_server_configs.empty?
|
30
|
+
|
31
|
+
# Ensure MCP logging is configured before creating clients
|
32
|
+
Swarm.apply_mcp_logging_configuration
|
33
|
+
|
34
|
+
mcp_server_configs.each do |server_config|
|
35
|
+
client = initialize_mcp_client(server_config)
|
36
|
+
|
37
|
+
# Store client for cleanup
|
38
|
+
@mcp_clients[agent_name] << client
|
39
|
+
|
40
|
+
# Fetch tools from MCP server and register with chat
|
41
|
+
# Tools are already in RubyLLM::Tool format
|
42
|
+
tools = client.tools
|
43
|
+
tools.each { |tool| chat.with_tool(tool) }
|
44
|
+
|
45
|
+
RubyLLM.logger.debug("SwarmSDK: Registered #{tools.size} tools from MCP server '#{server_config[:name]}' for agent #{agent_name}")
|
46
|
+
rescue StandardError => e
|
47
|
+
RubyLLM.logger.error("SwarmSDK: Failed to initialize MCP server '#{server_config[:name]}' for agent #{agent_name}: #{e.message}")
|
48
|
+
raise ConfigurationError, "Failed to initialize MCP server '#{server_config[:name]}': #{e.message}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Build transport-specific configuration for MCP client
|
53
|
+
#
|
54
|
+
# This method is public for testing delegation from Swarm.
|
55
|
+
#
|
56
|
+
# @param transport_type [Symbol] Transport type (:stdio, :sse, :streamable)
|
57
|
+
# @param config [Hash] MCP server configuration
|
58
|
+
# @return [Hash] Transport-specific configuration
|
59
|
+
def build_transport_config(transport_type, config)
|
60
|
+
case transport_type
|
61
|
+
when :stdio
|
62
|
+
build_stdio_config(config)
|
63
|
+
when :sse
|
64
|
+
build_sse_config(config)
|
65
|
+
when :streamable
|
66
|
+
build_streamable_config(config)
|
67
|
+
else
|
68
|
+
raise ArgumentError, "Unsupported transport type: #{transport_type}"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Initialize an MCP client from configuration
|
75
|
+
#
|
76
|
+
# @param config [Hash] MCP server configuration
|
77
|
+
# @return [RubyLLM::MCP::Client] Initialized MCP client
|
78
|
+
def initialize_mcp_client(config)
|
79
|
+
# Convert timeout from seconds to milliseconds
|
80
|
+
timeout_seconds = config[:timeout] || 30
|
81
|
+
timeout_ms = timeout_seconds * 1000
|
82
|
+
|
83
|
+
# Determine transport type
|
84
|
+
transport_type = determine_transport_type(config[:type])
|
85
|
+
|
86
|
+
# Build transport-specific configuration
|
87
|
+
client_config = build_transport_config(transport_type, config)
|
88
|
+
|
89
|
+
# Create and start MCP client
|
90
|
+
RubyLLM::MCP.client(
|
91
|
+
name: config[:name],
|
92
|
+
transport_type: transport_type,
|
93
|
+
request_timeout: timeout_ms,
|
94
|
+
config: client_config,
|
95
|
+
)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Determine transport type from configuration
|
99
|
+
#
|
100
|
+
# @param type [Symbol, String, nil] Transport type from config
|
101
|
+
# @return [Symbol] Normalized transport type
|
102
|
+
def determine_transport_type(type)
|
103
|
+
case type&.to_sym
|
104
|
+
when :stdio then :stdio
|
105
|
+
when :sse then :sse
|
106
|
+
when :http, :streamable then :streamable
|
107
|
+
else
|
108
|
+
raise ArgumentError, "Unknown MCP transport type: #{type}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Build stdio transport configuration
|
113
|
+
#
|
114
|
+
# @param config [Hash] MCP server configuration
|
115
|
+
# @return [Hash] Stdio configuration
|
116
|
+
def build_stdio_config(config)
|
117
|
+
{
|
118
|
+
command: config[:command],
|
119
|
+
args: config[:args] || [],
|
120
|
+
env: Utils.stringify_keys(config[:env] || {}),
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Build SSE transport configuration
|
125
|
+
#
|
126
|
+
# @param config [Hash] MCP server configuration
|
127
|
+
# @return [Hash] SSE configuration
|
128
|
+
def build_sse_config(config)
|
129
|
+
{
|
130
|
+
url: config[:url],
|
131
|
+
headers: config[:headers] || {},
|
132
|
+
version: config[:version]&.to_sym || :http2,
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
# Build streamable (HTTP) transport configuration
|
137
|
+
#
|
138
|
+
# @param config [Hash] MCP server configuration
|
139
|
+
# @return [Hash] Streamable configuration
|
140
|
+
def build_streamable_config(config)
|
141
|
+
{
|
142
|
+
url: config[:url],
|
143
|
+
headers: config[:headers] || {},
|
144
|
+
version: config[:version]&.to_sym || :http2,
|
145
|
+
oauth: config[:oauth],
|
146
|
+
rate_limit: config[:rate_limit],
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|