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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/lib/swarm_sdk/agent/builder.rb +333 -0
  3. data/lib/swarm_sdk/agent/chat/context_tracker.rb +271 -0
  4. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  5. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +99 -0
  6. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +114 -0
  7. data/lib/swarm_sdk/agent/chat.rb +779 -0
  8. data/lib/swarm_sdk/agent/context.rb +108 -0
  9. data/lib/swarm_sdk/agent/definition.rb +335 -0
  10. data/lib/swarm_sdk/configuration.rb +251 -0
  11. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  12. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  13. data/lib/swarm_sdk/context_compactor.rb +340 -0
  14. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  15. data/lib/swarm_sdk/hooks/context.rb +163 -0
  16. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  17. data/lib/swarm_sdk/hooks/error.rb +29 -0
  18. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  19. data/lib/swarm_sdk/hooks/registry.rb +143 -0
  20. data/lib/swarm_sdk/hooks/result.rb +150 -0
  21. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  22. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  23. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  24. data/lib/swarm_sdk/log_collector.rb +83 -0
  25. data/lib/swarm_sdk/log_stream.rb +69 -0
  26. data/lib/swarm_sdk/markdown_parser.rb +46 -0
  27. data/lib/swarm_sdk/permissions/config.rb +239 -0
  28. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  29. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  30. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  31. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  32. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +237 -0
  33. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  34. data/lib/swarm_sdk/result.rb +97 -0
  35. data/lib/swarm_sdk/swarm/agent_initializer.rb +224 -0
  36. data/lib/swarm_sdk/swarm/all_agents_builder.rb +62 -0
  37. data/lib/swarm_sdk/swarm/builder.rb +240 -0
  38. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  39. data/lib/swarm_sdk/swarm/tool_configurator.rb +267 -0
  40. data/lib/swarm_sdk/swarm.rb +837 -0
  41. data/lib/swarm_sdk/tools/bash.rb +274 -0
  42. data/lib/swarm_sdk/tools/delegate.rb +152 -0
  43. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  44. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  45. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  46. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  47. data/lib/swarm_sdk/tools/edit.rb +150 -0
  48. data/lib/swarm_sdk/tools/glob.rb +158 -0
  49. data/lib/swarm_sdk/tools/grep.rb +231 -0
  50. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  51. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  52. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  53. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  54. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  55. data/lib/swarm_sdk/tools/read.rb +251 -0
  56. data/lib/swarm_sdk/tools/registry.rb +73 -0
  57. data/lib/swarm_sdk/tools/scratchpad_list.rb +88 -0
  58. data/lib/swarm_sdk/tools/scratchpad_read.rb +59 -0
  59. data/lib/swarm_sdk/tools/scratchpad_write.rb +88 -0
  60. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  61. data/lib/swarm_sdk/tools/stores/scratchpad.rb +153 -0
  62. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  63. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  64. data/lib/swarm_sdk/tools/write.rb +117 -0
  65. data/lib/swarm_sdk/utils.rb +50 -0
  66. data/lib/swarm_sdk/version.rb +5 -0
  67. data/lib/swarm_sdk.rb +69 -0
  68. 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