claude_swarm 1.0.1 → 1.0.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 (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 +6 -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 +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,982 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Swarm orchestrates multiple AI agents with shared rate limiting and coordination.
5
+ #
6
+ # This is the main user-facing API for SwarmSDK. Users create swarms using:
7
+ # - Direct API: Create Agent::Definition objects and add to swarm
8
+ # - Ruby DSL: Use Swarm::Builder for fluent configuration
9
+ # - YAML: Load from configuration files
10
+ #
11
+ # ## Direct API
12
+ #
13
+ # swarm = Swarm.new(name: "Development Team")
14
+ #
15
+ # backend_agent = Agent::Definition.new(:backend, {
16
+ # description: "Backend developer",
17
+ # model: "gpt-5",
18
+ # system_prompt: "You build APIs and databases...",
19
+ # tools: [:Read, :Edit, :Bash],
20
+ # delegates_to: [:database]
21
+ # })
22
+ # swarm.add_agent(backend_agent)
23
+ #
24
+ # swarm.lead = :backend
25
+ # result = swarm.execute("Build authentication")
26
+ #
27
+ # ## Ruby DSL (Recommended)
28
+ #
29
+ # swarm = SwarmSDK.build do
30
+ # name "Development Team"
31
+ # lead :backend
32
+ #
33
+ # agent :backend do
34
+ # model "gpt-5"
35
+ # description "Backend developer"
36
+ # prompt "You build APIs"
37
+ # tools :Read, :Edit, :Bash
38
+ # end
39
+ # end
40
+ # result = swarm.execute("Build authentication")
41
+ #
42
+ # ## YAML API
43
+ #
44
+ # swarm = Swarm.load("swarm.yml")
45
+ # result = swarm.execute("Build authentication")
46
+ #
47
+ # ## Architecture
48
+ #
49
+ # All three APIs converge on Agent::Definition for validation.
50
+ # Swarm delegates to specialized concerns:
51
+ # - Agent::Definition: Validates configuration, builds system prompts
52
+ # - AgentInitializer: Complex 5-pass agent setup
53
+ # - ToolConfigurator: Tool creation and permissions (via AgentInitializer)
54
+ # - McpConfigurator: MCP client management (via AgentInitializer)
55
+ #
56
+ class Swarm
57
+ DEFAULT_GLOBAL_CONCURRENCY = 50
58
+ DEFAULT_LOCAL_CONCURRENCY = 10
59
+ DEFAULT_MCP_LOG_LEVEL = Logger::WARN
60
+
61
+ # Default tools available to all agents
62
+ DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
63
+
64
+ attr_reader :name, :agents, :lead_agent, :mcp_clients
65
+
66
+ # Check if scratchpad tools are enabled
67
+ #
68
+ # @return [Boolean]
69
+ def scratchpad_enabled?
70
+ @scratchpad_enabled
71
+ end
72
+ attr_writer :config_for_hooks
73
+
74
+ # Class-level MCP log level configuration
75
+ @mcp_log_level = DEFAULT_MCP_LOG_LEVEL
76
+ @mcp_logging_configured = false
77
+
78
+ class << self
79
+ attr_accessor :mcp_log_level
80
+
81
+ # Configure MCP client logging globally
82
+ #
83
+ # This should be called before creating any swarms that use MCP servers.
84
+ # The configuration is global and affects all MCP clients.
85
+ #
86
+ # @param level [Integer] Log level (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL)
87
+ # @return [void]
88
+ def configure_mcp_logging(level = DEFAULT_MCP_LOG_LEVEL)
89
+ @mcp_log_level = level
90
+ apply_mcp_logging_configuration
91
+ end
92
+
93
+ # Apply MCP logging configuration to RubyLLM::MCP
94
+ #
95
+ # @return [void]
96
+ def apply_mcp_logging_configuration
97
+ return if @mcp_logging_configured
98
+
99
+ RubyLLM::MCP.configure do |config|
100
+ config.log_level = @mcp_log_level
101
+ end
102
+
103
+ @mcp_logging_configured = true
104
+ end
105
+
106
+ # Load swarm from YAML configuration file
107
+ #
108
+ # @param config_path [String] Path to YAML configuration file
109
+ # @return [Swarm] Configured swarm instance
110
+ def load(config_path)
111
+ config = Configuration.load(config_path)
112
+ swarm = config.to_swarm
113
+
114
+ # Apply hooks if any are configured (YAML-only feature)
115
+ if hooks_configured?(config)
116
+ Hooks::Adapter.apply_hooks(swarm, config)
117
+ end
118
+
119
+ # Store config reference for agent hooks (applied during initialize_agents)
120
+ swarm.config_for_hooks = config
121
+
122
+ swarm
123
+ end
124
+
125
+ private
126
+
127
+ def hooks_configured?(config)
128
+ config.swarm_hooks.any? ||
129
+ config.all_agents_hooks.any? ||
130
+ config.agents.any? { |_, agent_def| agent_def.hooks&.any? }
131
+ end
132
+ end
133
+
134
+ # Initialize a new Swarm
135
+ #
136
+ # @param name [String] Human-readable swarm name
137
+ # @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
138
+ # @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
139
+ # @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing)
140
+ # @param scratchpad_enabled [Boolean] Whether to enable scratchpad tools (default: true)
141
+ def initialize(name:, global_concurrency: DEFAULT_GLOBAL_CONCURRENCY, default_local_concurrency: DEFAULT_LOCAL_CONCURRENCY, scratchpad: nil, scratchpad_enabled: true)
142
+ @name = name
143
+ @global_concurrency = global_concurrency
144
+ @default_local_concurrency = default_local_concurrency
145
+ @scratchpad_enabled = scratchpad_enabled
146
+
147
+ # Shared semaphore for all agents
148
+ @global_semaphore = Async::Semaphore.new(@global_concurrency)
149
+
150
+ # Shared scratchpad storage for all agents (volatile)
151
+ # Use provided scratchpad storage (for testing) or create volatile one
152
+ @scratchpad_storage = scratchpad || Tools::Stores::ScratchpadStorage.new
153
+
154
+ # Per-agent plugin storages (persistent)
155
+ # Format: { plugin_name => { agent_name => storage } }
156
+ # Will be populated when agents are initialized
157
+ @plugin_storages = {}
158
+
159
+ # Hook registry for named hooks and swarm defaults
160
+ @hook_registry = Hooks::Registry.new
161
+
162
+ # Register default logging hooks
163
+ register_default_logging_callbacks
164
+
165
+ # Agent definitions and instances
166
+ @agent_definitions = {}
167
+ @agents = {}
168
+ @agents_initialized = false
169
+ @agent_contexts = {}
170
+
171
+ # MCP clients per agent (for cleanup)
172
+ @mcp_clients = Hash.new { |h, k| h[k] = [] }
173
+
174
+ @lead_agent = nil
175
+
176
+ # Track if first message has been sent
177
+ @first_message_sent = false
178
+ end
179
+
180
+ # Add an agent to the swarm
181
+ #
182
+ # Accepts only Agent::Definition objects. This ensures all validation
183
+ # happens in a single place (Agent::Definition) and keeps the API clean.
184
+ #
185
+ # If the definition doesn't specify max_concurrent_tools, the swarm's
186
+ # default_local_concurrency is applied.
187
+ #
188
+ # @param definition [Agent::Definition] Fully configured agent definition
189
+ # @return [self]
190
+ #
191
+ # @example
192
+ # definition = Agent::Definition.new(:backend, {
193
+ # description: "Backend developer",
194
+ # model: "gpt-5",
195
+ # system_prompt: "You build APIs"
196
+ # })
197
+ # swarm.add_agent(definition)
198
+ def add_agent(definition)
199
+ unless definition.is_a?(Agent::Definition)
200
+ raise ArgumentError, "Expected Agent::Definition, got #{definition.class}"
201
+ end
202
+
203
+ name = definition.name
204
+ raise ConfigurationError, "Agent '#{name}' already exists" if @agent_definitions.key?(name)
205
+
206
+ # Apply swarm's default_local_concurrency if max_concurrent_tools not set
207
+ definition.max_concurrent_tools = @default_local_concurrency if definition.max_concurrent_tools.nil?
208
+
209
+ @agent_definitions[name] = definition
210
+ self
211
+ end
212
+
213
+ # Set the lead agent (entry point for swarm execution)
214
+ #
215
+ # @param name [Symbol, String] Name of agent to make lead
216
+ # @return [self]
217
+ def lead=(name)
218
+ name = name.to_sym
219
+
220
+ unless @agent_definitions.key?(name)
221
+ raise ConfigurationError, "Cannot set lead: agent '#{name}' not found"
222
+ end
223
+
224
+ @lead_agent = name
225
+ end
226
+
227
+ # Execute a task using the lead agent
228
+ #
229
+ # The lead agent can delegate to other agents via tool calls,
230
+ # and the entire swarm coordinates with shared rate limiting.
231
+ # Supports reprompting via swarm_stop hooks.
232
+ #
233
+ # @param prompt [String] Task to execute
234
+ # @yield [Hash] Log entry if block given (for streaming)
235
+ # @return [Result] Execution result
236
+ def execute(prompt, &block)
237
+ raise ConfigurationError, "No lead agent set. Set lead= first." unless @lead_agent
238
+
239
+ start_time = Time.now
240
+ logs = []
241
+ current_prompt = prompt
242
+
243
+ # Setup logging FIRST if block given (so swarm_start event can be emitted)
244
+ if block_given?
245
+ # Register callback to collect logs and forward to user's block
246
+ LogCollector.on_log do |entry|
247
+ logs << entry
248
+ block.call(entry)
249
+ end
250
+
251
+ # Set LogStream to use LogCollector as emitter
252
+ LogStream.emitter = LogCollector
253
+ end
254
+
255
+ # Trigger swarm_start hooks (before any execution)
256
+ # Hook can append stdout to prompt (exit code 0)
257
+ # Default callback emits swarm_start event to LogStream
258
+ swarm_start_result = trigger_swarm_start(current_prompt)
259
+ if swarm_start_result&.replace?
260
+ # Hook provided stdout to append to prompt
261
+ current_prompt = "#{current_prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
262
+ end
263
+
264
+ # Trigger first_message hooks on first execution
265
+ unless @first_message_sent
266
+ trigger_first_message(current_prompt)
267
+ @first_message_sent = true
268
+ end
269
+
270
+ # Lazy initialization of agents (with optional logging)
271
+ initialize_agents unless @agents_initialized
272
+
273
+ # Execution loop (supports reprompting)
274
+ result = nil
275
+ swarm_stop_triggered = false
276
+
277
+ loop do
278
+ # Execute within Async reactor to enable fiber scheduler for parallel execution
279
+ # This sets Fiber.scheduler, making Faraday fiber-aware so HTTP requests yield during I/O
280
+ # Use finished: false to suppress warnings for expected task failures
281
+ lead = @agents[@lead_agent]
282
+ response = Async(finished: false) do
283
+ lead.ask(current_prompt)
284
+ end.wait
285
+
286
+ # Check if swarm was finished by a hook (finish_swarm)
287
+ if response.is_a?(Hash) && response[:__finish_swarm__]
288
+ result = Result.new(
289
+ content: response[:message],
290
+ agent: @lead_agent.to_s,
291
+ logs: logs,
292
+ duration: Time.now - start_time,
293
+ )
294
+
295
+ # Trigger swarm_stop hooks for event emission
296
+ trigger_swarm_stop(result)
297
+ swarm_stop_triggered = true
298
+
299
+ # Break immediately - don't allow reprompting when swarm is finished by hook
300
+ break
301
+ end
302
+
303
+ result = Result.new(
304
+ content: response.content,
305
+ agent: @lead_agent.to_s,
306
+ logs: logs,
307
+ duration: Time.now - start_time,
308
+ )
309
+
310
+ # Trigger swarm_stop hooks (for reprompt check and event emission)
311
+ hook_result = trigger_swarm_stop(result)
312
+ swarm_stop_triggered = true
313
+
314
+ # Check if hook requests reprompting
315
+ if hook_result&.reprompt?
316
+ current_prompt = hook_result.value
317
+ swarm_stop_triggered = false # Will trigger again in next iteration
318
+ # Continue loop with new prompt
319
+ else
320
+ # Exit loop - execution complete
321
+ break
322
+ end
323
+ end
324
+
325
+ result
326
+ rescue ConfigurationError, AgentNotFoundError
327
+ # Re-raise configuration errors - these should be fixed, not caught
328
+ raise
329
+ rescue TypeError => e
330
+ # Catch the specific "String does not have #dig method" error
331
+ if e.message.include?("does not have #dig method")
332
+ agent_definition = @agent_definitions[@lead_agent]
333
+ error_msg = if agent_definition.base_url
334
+ "LLM API request failed: The proxy/server at '#{agent_definition.base_url}' returned an invalid response. " \
335
+ "This usually means the proxy is unreachable, requires authentication, or returned an error in non-JSON format. " \
336
+ "Original error: #{e.message}"
337
+ else
338
+ "LLM API request failed with unexpected response format. Original error: #{e.message}"
339
+ end
340
+
341
+ result = Result.new(
342
+ content: nil,
343
+ agent: @lead_agent.to_s,
344
+ error: LLMError.new(error_msg),
345
+ logs: logs,
346
+ duration: Time.now - start_time,
347
+ )
348
+ else
349
+ result = Result.new(
350
+ content: nil,
351
+ agent: @lead_agent.to_s,
352
+ error: e,
353
+ logs: logs,
354
+ duration: Time.now - start_time,
355
+ )
356
+ end
357
+ result
358
+ rescue StandardError => e
359
+ result = Result.new(
360
+ content: nil,
361
+ agent: @lead_agent&.to_s || "unknown",
362
+ error: e,
363
+ logs: logs,
364
+ duration: Time.now - start_time,
365
+ )
366
+ result
367
+ ensure
368
+ # Trigger swarm_stop if not already triggered (handles error cases)
369
+ unless swarm_stop_triggered
370
+ trigger_swarm_stop_final(result, start_time, logs)
371
+ end
372
+
373
+ # Cleanup MCP clients after execution
374
+ cleanup
375
+
376
+ # Reset logging state for next execution if we set it up
377
+ #
378
+ # IMPORTANT: Only reset if we set up logging (block_given? == true).
379
+ # When this swarm is a mini-swarm within a NodeOrchestrator workflow,
380
+ # the orchestrator manages LogCollector and we don't set up logging.
381
+ #
382
+ # Flow in NodeOrchestrator:
383
+ # 1. NodeOrchestrator sets up LogCollector + LogStream (no block given to mini-swarms)
384
+ # 2. Each mini-swarm executes without logging block (block_given? == false)
385
+ # 3. Each mini-swarm skips reset (didn't set up logging)
386
+ # 4. NodeOrchestrator resets once at the very end
387
+ #
388
+ # Flow in standalone swarm / interactive REPL:
389
+ # 1. Swarm.execute sets up LogCollector + LogStream (block given)
390
+ # 2. Swarm.execute resets in ensure block (cleanup for next call)
391
+ if block_given?
392
+ LogCollector.reset!
393
+ LogStream.reset!
394
+ end
395
+ end
396
+
397
+ # Get an agent chat instance by name
398
+ #
399
+ # @param name [Symbol, String] Agent name
400
+ # @return [AgentChat] Agent chat instance
401
+ def agent(name)
402
+ name = name.to_sym
403
+ initialize_agents unless @agents_initialized
404
+
405
+ @agents[name] || raise(AgentNotFoundError, "Agent '#{name}' not found")
406
+ end
407
+
408
+ # Get an agent definition by name
409
+ #
410
+ # Use this to access and modify agent configuration:
411
+ # swarm.agent_definition(:backend).bypass_permissions = true
412
+ #
413
+ # @param name [Symbol, String] Agent name
414
+ # @return [AgentDefinition] Agent definition object
415
+ def agent_definition(name)
416
+ name = name.to_sym
417
+
418
+ @agent_definitions[name] || raise(AgentNotFoundError, "Agent '#{name}' not found")
419
+ end
420
+
421
+ # Get all agent names
422
+ #
423
+ # @return [Array<Symbol>] Agent names
424
+ def agent_names
425
+ @agent_definitions.keys
426
+ end
427
+
428
+ # Validate swarm configuration and return warnings
429
+ #
430
+ # This performs lightweight validation checks without creating agents.
431
+ # Useful for displaying configuration warnings before execution.
432
+ #
433
+ # @return [Array<Hash>] Array of warning hashes from all agent definitions
434
+ #
435
+ # @example
436
+ # swarm = Swarm.load("config.yml")
437
+ # warnings = swarm.validate
438
+ # warnings.each do |warning|
439
+ # puts "⚠️ #{warning[:agent]}: #{warning[:model]} not found"
440
+ # end
441
+ def validate
442
+ @agent_definitions.flat_map { |_name, definition| definition.validate }
443
+ end
444
+
445
+ # Emit validation warnings as log events
446
+ #
447
+ # This validates all agent definitions and emits any warnings as
448
+ # model_lookup_warning events through LogStream. Useful for emitting
449
+ # warnings before execution starts (e.g., in REPL after welcome screen).
450
+ #
451
+ # Requires LogStream.emitter to be set.
452
+ #
453
+ # @return [Array<Hash>] The validation warnings that were emitted
454
+ #
455
+ # @example
456
+ # LogCollector.on_log { |event| puts event }
457
+ # LogStream.emitter = LogCollector
458
+ # swarm.emit_validation_warnings
459
+ def emit_validation_warnings
460
+ warnings = validate
461
+
462
+ warnings.each do |warning|
463
+ case warning[:type]
464
+ when :model_not_found
465
+ LogStream.emit(
466
+ type: "model_lookup_warning",
467
+ agent: warning[:agent],
468
+ model: warning[:model],
469
+ error_message: warning[:error_message],
470
+ suggestions: warning[:suggestions],
471
+ timestamp: Time.now.utc.iso8601,
472
+ )
473
+ end
474
+ end
475
+
476
+ warnings
477
+ end
478
+
479
+ # Cleanup all MCP clients
480
+ #
481
+ # Stops all MCP client connections gracefully.
482
+ # Should be called when the swarm is no longer needed.
483
+ #
484
+ # @return [void]
485
+ def cleanup
486
+ return if @mcp_clients.empty?
487
+
488
+ @mcp_clients.each do |agent_name, clients|
489
+ clients.each do |client|
490
+ client.stop if client.alive?
491
+ RubyLLM.logger.debug("SwarmSDK: Stopped MCP client '#{client.name}' for agent #{agent_name}")
492
+ rescue StandardError => e
493
+ RubyLLM.logger.error("SwarmSDK: Error stopping MCP client '#{client.name}' for agent #{agent_name}: #{e.message}")
494
+ end
495
+ end
496
+
497
+ @mcp_clients.clear
498
+ end
499
+
500
+ # Register a named hook that can be referenced in agent configurations
501
+ #
502
+ # Named hooks are stored in the registry and can be referenced by symbol
503
+ # in agent YAML configurations or programmatically.
504
+ #
505
+ # @param name [Symbol] Unique hook name
506
+ # @param block [Proc] Hook implementation
507
+ # @return [self]
508
+ #
509
+ # @example Register a validation hook
510
+ # swarm.register_hook(:validate_code) do |context|
511
+ # raise SwarmSDK::Hooks::Error, "Invalid" unless valid?(context.tool_call)
512
+ # end
513
+ def register_hook(name, &block)
514
+ @hook_registry.register(name, &block)
515
+ self
516
+ end
517
+
518
+ # Add a swarm-level default hook that applies to all agents
519
+ #
520
+ # Default hooks are inherited by all agents unless overridden at agent level.
521
+ # Useful for swarm-wide policies like logging, validation, or monitoring.
522
+ #
523
+ # @param event [Symbol] Event type (e.g., :pre_tool_use, :post_tool_use)
524
+ # @param matcher [String, Regexp, nil] Optional regex pattern for tool names
525
+ # @param priority [Integer] Execution priority (higher = earlier)
526
+ # @param block [Proc] Hook implementation
527
+ # @return [self]
528
+ #
529
+ # @example Add logging for all tool calls
530
+ # swarm.add_default_callback(:pre_tool_use) do |context|
531
+ # puts "[#{context.agent_name}] Calling #{context.tool_call.name}"
532
+ # end
533
+ def add_default_callback(event, matcher: nil, priority: 0, &block)
534
+ @hook_registry.add_default(event, matcher: matcher, priority: priority, &block)
535
+ self
536
+ end
537
+
538
+ private
539
+
540
+ # Initialize all agents using AgentInitializer
541
+ #
542
+ # This is called automatically (lazy initialization) by execute() and agent().
543
+ # Delegates to AgentInitializer which handles the complex 5-pass setup.
544
+ #
545
+ # @return [void]
546
+ def initialize_agents
547
+ return if @agents_initialized
548
+
549
+ initializer = AgentInitializer.new(
550
+ self,
551
+ @agent_definitions,
552
+ @global_semaphore,
553
+ @hook_registry,
554
+ @scratchpad_storage,
555
+ @plugin_storages,
556
+ config_for_hooks: @config_for_hooks,
557
+ )
558
+
559
+ @agents = initializer.initialize_all
560
+ @agent_contexts = initializer.agent_contexts
561
+ @agents_initialized = true
562
+
563
+ # Emit agent_start events for all agents
564
+ emit_agent_start_events
565
+ end
566
+
567
+ # Emit agent_start events for all initialized agents
568
+ def emit_agent_start_events
569
+ # Only emit if LogStream is enabled
570
+ return unless LogStream.emitter
571
+
572
+ @agents.each do |agent_name, chat|
573
+ agent_def = @agent_definitions[agent_name]
574
+
575
+ # Build plugin storage info for logging
576
+ plugin_storage_info = {}
577
+ @plugin_storages.each do |plugin_name, agent_storages|
578
+ next unless agent_storages.key?(agent_name)
579
+
580
+ plugin_storage_info[plugin_name] = {
581
+ enabled: true,
582
+ # Get additional info from agent definition if available
583
+ config: agent_def.respond_to?(plugin_name) ? extract_plugin_config_info(agent_def.public_send(plugin_name)) : nil,
584
+ }
585
+ end
586
+
587
+ LogStream.emit(
588
+ type: "agent_start",
589
+ agent: agent_name,
590
+ swarm_name: @name,
591
+ model: agent_def.model,
592
+ provider: agent_def.provider || "openai",
593
+ directory: agent_def.directory,
594
+ system_prompt: agent_def.system_prompt,
595
+ tools: chat.tools.keys,
596
+ delegates_to: agent_def.delegates_to,
597
+ plugin_storages: plugin_storage_info,
598
+ timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
599
+ )
600
+ end
601
+ end
602
+
603
+ # Normalize tools to internal format (kept for add_agent)
604
+ #
605
+ # Handles both Ruby API (simple symbols) and YAML API (already parsed configs)
606
+ #
607
+ # @param tools [Array] Tool specifications
608
+ # @return [Array<Hash>] Normalized tool configs
609
+ def normalize_tools(tools)
610
+ Array(tools).map do |tool|
611
+ case tool
612
+ when Symbol, String
613
+ # Simple tool from Ruby API
614
+ { name: tool.to_sym, permissions: nil }
615
+ when Hash
616
+ # Already in config format from YAML (has :name and :permissions keys)
617
+ if tool.key?(:name)
618
+ tool
619
+ else
620
+ # Inline permissions format: { Write: { allowed_paths: [...] } }
621
+ tool_name = tool.keys.first.to_sym
622
+ { name: tool_name, permissions: tool[tool_name] }
623
+ end
624
+ else
625
+ raise ConfigurationError, "Invalid tool specification: #{tool.inspect}"
626
+ end
627
+ end
628
+ end
629
+
630
+ # Delegation methods for testing (delegate to concerns)
631
+ # These allow tests to verify behavior without depending on internal structure
632
+
633
+ # Create a tool instance (delegates to ToolConfigurator)
634
+ def create_tool_instance(tool_name, agent_name, directory)
635
+ ToolConfigurator.new(self, @scratchpad_storage, @plugin_storages).create_tool_instance(tool_name, agent_name, directory)
636
+ end
637
+
638
+ # Wrap tool with permissions (delegates to ToolConfigurator)
639
+ def wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
640
+ ToolConfigurator.new(self, @scratchpad_storage, @plugin_storages).wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
641
+ end
642
+
643
+ # Build MCP transport config (delegates to McpConfigurator)
644
+ def build_mcp_transport_config(transport_type, config)
645
+ McpConfigurator.new(self).build_transport_config(transport_type, config)
646
+ end
647
+
648
+ # Create delegation tool (delegates to AgentInitializer)
649
+ def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
650
+ AgentInitializer.new(self, @agent_definitions, @global_semaphore, @hook_registry, @scratchpad_storage, @plugin_storages)
651
+ .create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
652
+ end
653
+
654
+ # Extract loggable info from plugin config
655
+ #
656
+ # Attempts to extract useful information from plugin configuration
657
+ # for logging purposes. Handles MemoryConfig, Hashes, and other objects.
658
+ #
659
+ # @param config [Object] Plugin configuration object
660
+ # @return [Hash, nil] Extracted config info or nil
661
+ def extract_plugin_config_info(config)
662
+ return if config.nil?
663
+
664
+ # Handle MemoryConfig object (has directory method)
665
+ if config.respond_to?(:directory)
666
+ return { directory: config.directory }
667
+ end
668
+
669
+ # Handle Hash
670
+ if config.is_a?(Hash)
671
+ return config.slice(:directory, "directory", :adapter, "adapter")
672
+ end
673
+
674
+ # Unknown config type
675
+ nil
676
+ end
677
+
678
+ # Register default logging hooks that emit LogStream events
679
+ #
680
+ # These hooks implement the standard SwarmSDK logging behavior.
681
+ # Users can override or extend them by registering their own hooks.
682
+ #
683
+ # @return [void]
684
+ def register_default_logging_callbacks
685
+ # Log swarm start
686
+ add_default_callback(:swarm_start, priority: -100) do |context|
687
+ # Only log if LogStream emitter is set (logging enabled)
688
+ next unless LogStream.emitter
689
+
690
+ LogStream.emit(
691
+ type: "swarm_start",
692
+ agent: context.metadata[:lead_agent], # Include agent for consistency
693
+ swarm_name: context.metadata[:swarm_name],
694
+ lead_agent: context.metadata[:lead_agent],
695
+ prompt: context.metadata[:prompt],
696
+ timestamp: context.metadata[:timestamp],
697
+ )
698
+ end
699
+
700
+ # Log swarm stop
701
+ add_default_callback(:swarm_stop, priority: -100) do |context|
702
+ # Only log if LogStream emitter is set (logging enabled)
703
+ next unless LogStream.emitter
704
+
705
+ LogStream.emit(
706
+ type: "swarm_stop",
707
+ swarm_name: context.metadata[:swarm_name],
708
+ lead_agent: context.metadata[:lead_agent],
709
+ last_agent: context.metadata[:last_agent], # Agent that produced final response
710
+ content: context.metadata[:content], # Final response content
711
+ success: context.metadata[:success],
712
+ duration: context.metadata[:duration],
713
+ total_cost: context.metadata[:total_cost],
714
+ total_tokens: context.metadata[:total_tokens],
715
+ agents_involved: context.metadata[:agents_involved],
716
+ timestamp: context.metadata[:timestamp],
717
+ )
718
+ end
719
+
720
+ # Log user requests
721
+ add_default_callback(:user_prompt, priority: -100) do |context|
722
+ # Only log if LogStream emitter is set (logging enabled)
723
+ next unless LogStream.emitter
724
+
725
+ LogStream.emit(
726
+ type: "user_prompt",
727
+ agent: context.agent_name,
728
+ model: context.metadata[:model] || "unknown",
729
+ provider: context.metadata[:provider] || "unknown",
730
+ message_count: context.metadata[:message_count] || 0,
731
+ tools: context.metadata[:tools] || [],
732
+ delegates_to: context.metadata[:delegates_to] || [],
733
+ metadata: context.metadata,
734
+ )
735
+ end
736
+
737
+ # Log intermediate agent responses with tool calls
738
+ add_default_callback(:agent_step, priority: -100) do |context|
739
+ # Only log if LogStream emitter is set (logging enabled)
740
+ next unless LogStream.emitter
741
+
742
+ # Extract top-level fields and remove from metadata to avoid duplication
743
+ metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
744
+
745
+ LogStream.emit(
746
+ type: "agent_step",
747
+ agent: context.agent_name,
748
+ model: context.metadata[:model],
749
+ content: context.metadata[:content],
750
+ tool_calls: context.metadata[:tool_calls],
751
+ finish_reason: context.metadata[:finish_reason],
752
+ usage: context.metadata[:usage],
753
+ tool_executions: context.metadata[:tool_executions],
754
+ metadata: metadata_without_duplicates,
755
+ )
756
+ end
757
+
758
+ # Log final agent responses
759
+ add_default_callback(:agent_stop, priority: -100) do |context|
760
+ # Only log if LogStream emitter is set (logging enabled)
761
+ next unless LogStream.emitter
762
+
763
+ # Extract top-level fields and remove from metadata to avoid duplication
764
+ metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
765
+
766
+ LogStream.emit(
767
+ type: "agent_stop",
768
+ agent: context.agent_name,
769
+ model: context.metadata[:model],
770
+ content: context.metadata[:content],
771
+ tool_calls: context.metadata[:tool_calls],
772
+ finish_reason: context.metadata[:finish_reason],
773
+ usage: context.metadata[:usage],
774
+ tool_executions: context.metadata[:tool_executions],
775
+ metadata: metadata_without_duplicates,
776
+ )
777
+ end
778
+
779
+ # Log tool calls (pre_tool_use)
780
+ add_default_callback(:pre_tool_use, priority: -100) do |context|
781
+ # Only log if LogStream emitter is set (logging enabled)
782
+ next unless LogStream.emitter
783
+
784
+ # Delegation tracking is handled separately in AgentChat
785
+ # Just log the tool call - delegation info will be in metadata if needed
786
+ LogStream.emit(
787
+ type: "tool_call",
788
+ agent: context.agent_name,
789
+ tool_call_id: context.tool_call.id,
790
+ tool: context.tool_call.name,
791
+ arguments: context.tool_call.parameters,
792
+ metadata: context.metadata,
793
+ )
794
+ end
795
+
796
+ # Log tool results (post_tool_use)
797
+ add_default_callback(:post_tool_use, priority: -100) do |context|
798
+ # Only log if LogStream emitter is set (logging enabled)
799
+ next unless LogStream.emitter
800
+
801
+ # Delegation tracking is handled separately in AgentChat
802
+ # Usage tracking is handled in agent_step/agent_stop events
803
+ LogStream.emit(
804
+ type: "tool_result",
805
+ agent: context.agent_name,
806
+ tool_call_id: context.tool_result.tool_call_id,
807
+ tool: context.tool_result.tool_name,
808
+ result: context.tool_result.content,
809
+ metadata: context.metadata,
810
+ )
811
+ end
812
+
813
+ # Log context warnings
814
+ add_default_callback(:context_warning, priority: -100) do |context|
815
+ # Only log if LogStream emitter is set (logging enabled)
816
+ next unless LogStream.emitter
817
+
818
+ LogStream.emit(
819
+ type: "context_limit_warning",
820
+ agent: context.agent_name,
821
+ model: context.metadata[:model] || "unknown",
822
+ threshold: "#{context.metadata[:threshold]}%",
823
+ current_usage: "#{context.metadata[:percentage]}%",
824
+ tokens_used: context.metadata[:tokens_used],
825
+ tokens_remaining: context.metadata[:tokens_remaining],
826
+ context_limit: context.metadata[:context_limit],
827
+ metadata: context.metadata,
828
+ )
829
+ end
830
+ end
831
+
832
+ # Trigger swarm_start hooks when swarm execution begins
833
+ #
834
+ # This is a swarm-level event that fires when Swarm.execute is called
835
+ # (before first user message is sent). Hooks can halt execution or append stdout to prompt.
836
+ # Default callback emits to LogStream for logging.
837
+ #
838
+ # @param prompt [String] The user's task prompt
839
+ # @return [Hooks::Result, nil] Result with stdout to append (if exit 0) or nil
840
+ # @raise [Hooks::Error] If hook halts execution
841
+ def trigger_swarm_start(prompt)
842
+ context = Hooks::Context.new(
843
+ event: :swarm_start,
844
+ agent_name: @lead_agent.to_s,
845
+ swarm: self,
846
+ metadata: {
847
+ swarm_name: @name,
848
+ lead_agent: @lead_agent,
849
+ prompt: prompt,
850
+ timestamp: Time.now.utc.iso8601,
851
+ },
852
+ )
853
+
854
+ executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
855
+ result = executor.execute_safe(event: :swarm_start, context: context, callbacks: [])
856
+
857
+ # Halt execution if hook requests it
858
+ raise Hooks::Error, "Swarm start halted by hook: #{result.value}" if result.halt?
859
+
860
+ # Return result so caller can check for replace (stdout injection)
861
+ result
862
+ rescue StandardError => e
863
+ RubyLLM.logger.error("SwarmSDK: Error in swarm_start hook: #{e.message}")
864
+ raise
865
+ end
866
+
867
+ # Trigger swarm_stop for final event emission (called in ensure block)
868
+ #
869
+ # This ALWAYS emits the swarm_stop event, even if there was an error.
870
+ # It does NOT check for reprompt (that's done in trigger_swarm_stop_for_reprompt_check).
871
+ #
872
+ # @param result [Result, nil] Execution result (may be nil if exception before result created)
873
+ # @param start_time [Time] Execution start time
874
+ # @param logs [Array] Collected logs
875
+ # @return [void]
876
+ def trigger_swarm_stop_final(result, start_time, logs)
877
+ # Create a minimal result if one doesn't exist (exception before result created)
878
+ result ||= Result.new(
879
+ content: nil,
880
+ agent: @lead_agent&.to_s || "unknown",
881
+ logs: logs,
882
+ duration: Time.now - start_time,
883
+ error: StandardError.new("Unknown error"),
884
+ )
885
+
886
+ context = Hooks::Context.new(
887
+ event: :swarm_stop,
888
+ agent_name: @lead_agent.to_s,
889
+ swarm: self,
890
+ metadata: {
891
+ swarm_name: @name,
892
+ lead_agent: @lead_agent,
893
+ last_agent: result.agent, # Agent that produced the final response
894
+ content: result.content, # Final response content
895
+ success: result.success?,
896
+ duration: result.duration,
897
+ total_cost: result.total_cost,
898
+ total_tokens: result.total_tokens,
899
+ agents_involved: result.agents_involved,
900
+ result: result,
901
+ timestamp: Time.now.utc.iso8601,
902
+ },
903
+ )
904
+
905
+ executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
906
+ executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
907
+ rescue StandardError => e
908
+ # Don't let swarm_stop errors break the ensure block
909
+ RubyLLM.logger.error("SwarmSDK: Error in swarm_stop final emission: #{e.message}")
910
+ end
911
+
912
+ # Trigger swarm_stop hooks for reprompt check and event emission
913
+ #
914
+ # This is called in the normal execution flow to check if hooks request reprompting.
915
+ # The default callback also emits the swarm_stop event to LogStream.
916
+ #
917
+ # @param result [Result] The execution result
918
+ # @return [Hooks::Result, nil] Hook result (reprompt action if applicable)
919
+ def trigger_swarm_stop(result)
920
+ context = Hooks::Context.new(
921
+ event: :swarm_stop,
922
+ agent_name: @lead_agent.to_s,
923
+ swarm: self,
924
+ metadata: {
925
+ swarm_name: @name,
926
+ lead_agent: @lead_agent,
927
+ last_agent: result.agent, # Agent that produced the final response
928
+ content: result.content, # Final response content
929
+ success: result.success?,
930
+ duration: result.duration,
931
+ total_cost: result.total_cost,
932
+ total_tokens: result.total_tokens,
933
+ agents_involved: result.agents_involved,
934
+ result: result, # Include full result for hook access
935
+ timestamp: Time.now.utc.iso8601,
936
+ },
937
+ )
938
+
939
+ executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
940
+ hook_result = executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
941
+
942
+ # Return hook result so caller can handle reprompt
943
+ hook_result
944
+ rescue StandardError => e
945
+ RubyLLM.logger.error("SwarmSDK: Error in swarm_stop hook: #{e.message}")
946
+ nil
947
+ end
948
+
949
+ # Trigger first_message hooks when first user message is sent
950
+ #
951
+ # This is a swarm-level event that fires once on the first call to execute().
952
+ # Hooks can halt execution before the first message is sent.
953
+ #
954
+ # @param prompt [String] The first user message
955
+ # @return [void]
956
+ # @raise [Hooks::Error] If hook halts execution
957
+ def trigger_first_message(prompt)
958
+ return if @hook_registry.get_defaults(:first_message).empty?
959
+
960
+ context = Hooks::Context.new(
961
+ event: :first_message,
962
+ agent_name: @lead_agent.to_s,
963
+ swarm: self,
964
+ metadata: {
965
+ swarm_name: @name,
966
+ lead_agent: @lead_agent,
967
+ prompt: prompt,
968
+ timestamp: Time.now.utc.iso8601,
969
+ },
970
+ )
971
+
972
+ executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
973
+ result = executor.execute_safe(event: :first_message, context: context, callbacks: [])
974
+
975
+ # Halt execution if hook requests it
976
+ raise Hooks::Error, "First message halted by hook: #{result.value}" if result.halt?
977
+ rescue StandardError => e
978
+ RubyLLM.logger.error("SwarmSDK: Error in first_message hook: #{e.message}")
979
+ raise
980
+ end
981
+ end
982
+ end