claude_swarm 1.0.1 → 1.0.4

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 (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +249 -6
  267. data/EXAMPLES.md +0 -164
@@ -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
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ class Swarm
5
+ # Handles tool creation, registration, and permissions wrapping
6
+ #
7
+ # Responsibilities:
8
+ # - Register explicit tools for agents
9
+ # - Register default tools (Read, Grep, Glob, etc.)
10
+ # - Create tool instances (with agent context)
11
+ # - Wrap tools with permissions validators
12
+ #
13
+ # This encapsulates all tool-related logic that was previously in Swarm.
14
+ class ToolConfigurator
15
+ # Default tools available to all agents (unless disable_default_tools is set)
16
+ DEFAULT_TOOLS = [
17
+ :Read,
18
+ :Grep,
19
+ :Glob,
20
+ :TodoWrite,
21
+ :Think,
22
+ :WebFetch,
23
+ :Clock,
24
+ ].freeze
25
+
26
+ # Scratchpad tools (added if scratchpad is enabled)
27
+ SCRATCHPAD_TOOLS = [
28
+ :ScratchpadWrite,
29
+ :ScratchpadRead,
30
+ :ScratchpadList,
31
+ ].freeze
32
+
33
+ def initialize(swarm, scratchpad_storage, plugin_storages = {})
34
+ @swarm = swarm
35
+ @scratchpad_storage = scratchpad_storage
36
+ # Plugin storages: { plugin_name => { agent_name => storage } }
37
+ # e.g., { memory: { agent1: storage1, agent2: storage2 } }
38
+ @plugin_storages = plugin_storages
39
+ end
40
+
41
+ # Register all tools for an agent (both explicit and default)
42
+ #
43
+ # @param chat [AgentChat] The chat instance to register tools with
44
+ # @param agent_name [Symbol] Name of the agent
45
+ # @param agent_definition [AgentDefinition] Agent definition object
46
+ def register_all_tools(chat:, agent_name:, agent_definition:)
47
+ register_explicit_tools(chat, agent_definition.tools, agent_name: agent_name, agent_definition: agent_definition)
48
+ register_default_tools(chat, agent_name: agent_name, agent_definition: agent_definition)
49
+ end
50
+
51
+ # Create a tool instance by name
52
+ #
53
+ # File tools and TodoWrite require agent context for tracking state.
54
+ # Scratchpad tools require shared scratchpad instance.
55
+ # Plugin tools are delegated to their respective plugins.
56
+ #
57
+ # This method is public for testing delegation from Swarm.
58
+ #
59
+ # @param tool_name [Symbol, String] Tool name
60
+ # @param agent_name [Symbol] Agent name for context
61
+ # @param directory [String] Agent's working directory
62
+ # @param chat [Agent::Chat, nil] Optional chat instance for tools that need it
63
+ # @param agent_definition [Agent::Definition, nil] Optional agent definition
64
+ # @return [RubyLLM::Tool] Tool instance
65
+ def create_tool_instance(tool_name, agent_name, directory, chat: nil, agent_definition: nil)
66
+ tool_name_sym = tool_name.to_sym
67
+
68
+ # Check if tool is provided by a plugin
69
+ if PluginRegistry.plugin_tool?(tool_name_sym)
70
+ return create_plugin_tool(tool_name_sym, agent_name, directory, chat, agent_definition)
71
+ end
72
+
73
+ case tool_name_sym
74
+ when :Read
75
+ Tools::Read.new(agent_name: agent_name, directory: directory)
76
+ when :Write
77
+ Tools::Write.new(agent_name: agent_name, directory: directory)
78
+ when :Edit
79
+ Tools::Edit.new(agent_name: agent_name, directory: directory)
80
+ when :MultiEdit
81
+ Tools::MultiEdit.new(agent_name: agent_name, directory: directory)
82
+ when :Bash
83
+ Tools::Bash.new(directory: directory)
84
+ when :Glob
85
+ Tools::Glob.new(directory: directory)
86
+ when :Grep
87
+ Tools::Grep.new(directory: directory)
88
+ when :TodoWrite
89
+ Tools::TodoWrite.new(agent_name: agent_name) # TodoWrite doesn't need directory
90
+ when :ScratchpadWrite
91
+ Tools::Scratchpad::ScratchpadWrite.create_for_scratchpad(@scratchpad_storage)
92
+ when :ScratchpadRead
93
+ Tools::Scratchpad::ScratchpadRead.create_for_scratchpad(@scratchpad_storage)
94
+ when :ScratchpadList
95
+ Tools::Scratchpad::ScratchpadList.create_for_scratchpad(@scratchpad_storage)
96
+ when :Think
97
+ Tools::Think.new
98
+ when :Clock
99
+ Tools::Clock.new
100
+ else
101
+ # Regular tools - get class from registry and instantiate
102
+ tool_class = Tools::Registry.get(tool_name_sym)
103
+ raise ConfigurationError, "Unknown tool: #{tool_name}" unless tool_class
104
+
105
+ # Check if tool is marked as :special but not handled in case statement
106
+ if tool_class == :special
107
+ raise ConfigurationError,
108
+ "Tool '#{tool_name}' requires special initialization but is not handled in create_tool_instance. " \
109
+ "This is a bug - #{tool_name} should be added to the case statement above."
110
+ end
111
+
112
+ tool_class.new
113
+ end
114
+ end
115
+
116
+ # Wrap a tool instance with permissions validator if configured
117
+ #
118
+ # This method is public for testing delegation from Swarm.
119
+ #
120
+ # @param tool_instance [RubyLLM::Tool] Tool instance to wrap
121
+ # @param permissions_config [Hash, nil] Permission configuration
122
+ # @param agent_definition [AgentDefinition] Agent definition
123
+ # @return [RubyLLM::Tool] Either the wrapped tool or original tool
124
+ def wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
125
+ # Skip wrapping if no permissions or agent bypasses permissions
126
+ return tool_instance unless permissions_config
127
+ return tool_instance if agent_definition.bypass_permissions
128
+
129
+ # Create permissions config and wrap tool with validator
130
+ permissions = Permissions::Config.new(
131
+ permissions_config,
132
+ base_directory: agent_definition.directory,
133
+ )
134
+
135
+ Permissions::Validator.new(tool_instance, permissions)
136
+ end
137
+
138
+ private
139
+
140
+ # Register explicitly configured tools
141
+ #
142
+ # @param chat [AgentChat] The chat instance
143
+ # @param tool_configs [Array<Hash>] Tool configurations with optional permissions
144
+ # @param agent_name [Symbol] Agent name
145
+ # @param agent_definition [AgentDefinition] Agent definition
146
+ def register_explicit_tools(chat, tool_configs, agent_name:, agent_definition:)
147
+ tool_configs.each do |tool_config|
148
+ tool_name = tool_config[:name]
149
+ permissions_config = tool_config[:permissions]
150
+
151
+ # Create tool instance
152
+ tool_instance = create_tool_instance(tool_name, agent_name, agent_definition.directory)
153
+
154
+ # Wrap with permissions validator if configured
155
+ tool_instance = wrap_tool_with_permissions(
156
+ tool_instance,
157
+ permissions_config,
158
+ agent_definition,
159
+ )
160
+
161
+ chat.with_tool(tool_instance)
162
+ end
163
+ end
164
+
165
+ # Register default tools for agents (unless disabled)
166
+ #
167
+ # Note: Memory tools are registered separately and are NOT affected by
168
+ # disable_default_tools, since they're configured via memory {} block.
169
+ #
170
+ # @param chat [AgentChat] The chat instance
171
+ # @param agent_name [Symbol] Agent name
172
+ # @param agent_definition [AgentDefinition] Agent definition
173
+ def register_default_tools(chat, agent_name:, agent_definition:)
174
+ # Get explicit tool names to avoid duplicates
175
+ explicit_tool_names = agent_definition.tools.map { |t| t[:name] }.to_set
176
+
177
+ # Register core default tools (unless disabled)
178
+ if agent_definition.disable_default_tools != true
179
+ DEFAULT_TOOLS.each do |tool_name|
180
+ register_tool_if_not_disabled(chat, tool_name, explicit_tool_names, agent_name, agent_definition)
181
+ end
182
+
183
+ # Register scratchpad tools if enabled
184
+ if @swarm.scratchpad_enabled?
185
+ SCRATCHPAD_TOOLS.each do |tool_name|
186
+ register_tool_if_not_disabled(chat, tool_name, explicit_tool_names, agent_name, agent_definition)
187
+ end
188
+ end
189
+ end
190
+
191
+ # Register plugin tools if plugin storage is enabled for this agent
192
+ # Plugin tools ARE affected by disable_default_tools (allows fine-grained control)
193
+ register_plugin_tools(chat, agent_name, agent_definition, explicit_tool_names)
194
+ end
195
+
196
+ # Register a tool if not already explicit or disabled
197
+ def register_tool_if_not_disabled(chat, tool_name, explicit_tool_names, agent_name, agent_definition)
198
+ # Skip if already registered explicitly
199
+ return if explicit_tool_names.include?(tool_name)
200
+
201
+ # Skip if tool is in the disable list
202
+ return if tool_disabled?(tool_name, agent_definition.disable_default_tools)
203
+
204
+ tool_instance = create_tool_instance(tool_name, agent_name, agent_definition.directory)
205
+
206
+ # Resolve permissions for default tool
207
+ permissions_config = agent_definition.agent_permissions[tool_name] ||
208
+ agent_definition.default_permissions[tool_name]
209
+
210
+ # Wrap with permissions validator if configured
211
+ tool_instance = wrap_tool_with_permissions(
212
+ tool_instance,
213
+ permissions_config,
214
+ agent_definition,
215
+ )
216
+
217
+ chat.with_tool(tool_instance)
218
+ end
219
+
220
+ # Create a tool instance via plugin
221
+ #
222
+ # @param tool_name [Symbol] Tool name
223
+ # @param agent_name [Symbol] Agent name
224
+ # @param directory [String] Working directory
225
+ # @param chat [Agent::Chat, nil] Chat instance
226
+ # @param agent_definition [Agent::Definition, nil] Agent definition
227
+ # @return [RubyLLM::Tool] Tool instance
228
+ def create_plugin_tool(tool_name, agent_name, directory, chat, agent_definition)
229
+ plugin = PluginRegistry.plugin_for_tool(tool_name)
230
+ raise ConfigurationError, "Tool #{tool_name} is not provided by any plugin" unless plugin
231
+
232
+ # Get plugin storage for this agent
233
+ plugin_storages = @plugin_storages[plugin.name] || {}
234
+ storage = plugin_storages[agent_name]
235
+
236
+ # Build context for tool creation
237
+ context = {
238
+ agent_name: agent_name,
239
+ directory: directory,
240
+ storage: storage,
241
+ agent_definition: agent_definition,
242
+ chat: chat,
243
+ tool_configurator: self,
244
+ }
245
+
246
+ plugin.create_tool(tool_name, context)
247
+ end
248
+
249
+ # Register plugin-provided tools for an agent
250
+ #
251
+ # Asks all plugins if they have tools to register for this agent.
252
+ #
253
+ # @param chat [Agent::Chat] Chat instance
254
+ # @param agent_name [Symbol] Agent name
255
+ # @param agent_definition [Agent::Definition] Agent definition
256
+ # @param explicit_tool_names [Set<Symbol>] Already-registered tool names
257
+ def register_plugin_tools(chat, agent_name, agent_definition, explicit_tool_names)
258
+ PluginRegistry.all.each do |plugin|
259
+ # Check if plugin has storage enabled for this agent
260
+ next unless plugin.storage_enabled?(agent_definition)
261
+
262
+ # Get plugin storage for this agent
263
+ plugin_storages = @plugin_storages[plugin.name] || {}
264
+ plugin_storages[agent_name]
265
+
266
+ # Register each tool provided by the plugin
267
+ plugin.tools.each do |tool_name|
268
+ # Skip if already registered explicitly
269
+ next if explicit_tool_names.include?(tool_name)
270
+
271
+ # Skip if tool is disabled via disable_default_tools
272
+ next if tool_disabled?(tool_name, agent_definition.disable_default_tools)
273
+
274
+ tool_instance = create_tool_instance(
275
+ tool_name,
276
+ agent_name,
277
+ agent_definition.directory,
278
+ chat: chat,
279
+ agent_definition: agent_definition,
280
+ )
281
+
282
+ # Resolve permissions for plugin tool
283
+ permissions_config = agent_definition.agent_permissions[tool_name] ||
284
+ agent_definition.default_permissions[tool_name]
285
+
286
+ # Wrap with permissions validator if configured
287
+ tool_instance = wrap_tool_with_permissions(
288
+ tool_instance,
289
+ permissions_config,
290
+ agent_definition,
291
+ )
292
+
293
+ chat.with_tool(tool_instance)
294
+ end
295
+ end
296
+ end
297
+
298
+ # Check if a tool should be disabled based on disable_default_tools config
299
+ #
300
+ # @param tool_name [Symbol] Tool name to check
301
+ # @param disable_config [nil, Boolean, Symbol, Array<Symbol>] Disable configuration
302
+ # @return [Boolean] True if tool should be disabled
303
+ def tool_disabled?(tool_name, disable_config)
304
+ return false if disable_config.nil?
305
+
306
+ if disable_config == true
307
+ # Disable all default tools
308
+ true
309
+ elsif disable_config.is_a?(Symbol)
310
+ # Single tool name
311
+ disable_config == tool_name
312
+ elsif disable_config.is_a?(Array)
313
+ # Disable only tools in the array
314
+ disable_config.include?(tool_name)
315
+ else
316
+ false
317
+ end
318
+ end
319
+
320
+ # Register agent delegation tools
321
+ #
322
+ # Creates delegation tools that allow one agent to call another.
323
+ #
324
+ # @param chat [AgentChat] The chat instance
325
+ # @param delegate_names [Array<Symbol>] Names of agents to delegate to
326
+ # @param agent_name [Symbol] Name of the agent doing the delegating
327
+ def register_delegation_tools(chat, delegate_names, agent_name:)
328
+ return if delegate_names.empty?
329
+
330
+ delegate_names.each do |delegate_name|
331
+ delegate_name = delegate_name.to_sym
332
+
333
+ unless @agents.key?(delegate_name)
334
+ raise ConfigurationError, "Agent delegates to unknown agent '#{delegate_name}'"
335
+ end
336
+
337
+ # Create a tool that delegates to the specified agent
338
+ delegate_agent = @agents[delegate_name]
339
+ delegate_definition = @agent_definitions[delegate_name]
340
+
341
+ tool = Tools::Delegate.new(
342
+ delegate_name: delegate_name.to_s,
343
+ delegate_description: delegate_definition.description,
344
+ delegate_chat: delegate_agent,
345
+ agent_name: agent_name,
346
+ swarm: @swarm,
347
+ hook_registry: @hook_registry,
348
+ delegating_chat: chat,
349
+ )
350
+
351
+ chat.with_tool(tool)
352
+ end
353
+ end
354
+
355
+ # Pass 4: Configure hook system
356
+ #
357
+ # Setup the callback system for each agent.
358
+ def pass_4_configure_hooks
359
+ @agents.each do |agent_name, chat|
360
+ agent_definition = @agent_definitions[agent_name]
361
+
362
+ chat.setup_hooks(
363
+ registry: @hook_registry,
364
+ agent_definition: agent_definition,
365
+ swarm: @swarm,
366
+ ) if chat.respond_to?(:setup_hooks)
367
+ end
368
+ end
369
+
370
+ # Pass 5: Apply YAML hooks if present
371
+ #
372
+ # If loaded from YAML, apply agent-specific hooks.
373
+ def pass_5_apply_yaml_hooks
374
+ return unless @config_for_hooks
375
+
376
+ @agents.each do |agent_name, chat|
377
+ agent_def = @config_for_hooks.agents[agent_name]
378
+ next unless agent_def&.hooks
379
+
380
+ HooksAdapter.apply_agent_hooks(chat, agent_name, agent_def.hooks, @swarm.name)
381
+ end
382
+ end
383
+
384
+ # Create an AgentChat instance
385
+ #
386
+ # NOTE: This is dead code, left over from refactoring. AgentInitializer
387
+ # now handles agent creation. This should be removed in a cleanup pass.
388
+ #
389
+ # @param agent_name [Symbol] Agent name
390
+ # @param agent_definition [AgentDefinition] Agent definition
391
+ # @param tool_configurator [ToolConfigurator] Tool configurator
392
+ # @return [AgentChat] Configured chat instance
393
+ def create_agent_chat(agent_name, agent_definition, tool_configurator)
394
+ chat = AgentChat.new(
395
+ definition: agent_definition.to_h,
396
+ global_semaphore: @global_semaphore,
397
+ )
398
+
399
+ # Set agent name on provider for logging (if provider supports it)
400
+ chat.provider.agent_name = agent_name if chat.provider.respond_to?(:agent_name=)
401
+
402
+ # Register tools
403
+ tool_configurator.register_all_tools(
404
+ chat: chat,
405
+ agent_name: agent_name,
406
+ agent_definition: agent_definition,
407
+ )
408
+
409
+ # Register MCP servers if any
410
+ if agent_definition.mcp_servers.any?
411
+ mcp_configurator = McpConfigurator.new(@swarm)
412
+ mcp_configurator.register_mcp_servers(chat, agent_definition.mcp_servers, agent_name: agent_name)
413
+ end
414
+
415
+ chat
416
+ end
417
+ end
418
+ end
419
+ end