swarm_memory 2.1.5 → 2.1.6

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,717 +0,0 @@
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
- # - Ruby DSL: SwarmSDK.build { ... } (Recommended)
8
- # - YAML String: SwarmSDK.load(yaml, base_dir:)
9
- # - YAML File: SwarmSDK.load_file(path)
10
- # - Direct API: Swarm.new + add_agent (Advanced)
11
- #
12
- # ## Ruby DSL (Recommended)
13
- #
14
- # swarm = SwarmSDK.build do
15
- # name "Development Team"
16
- # lead :backend
17
- #
18
- # agent :backend do
19
- # model "gpt-5"
20
- # description "Backend developer"
21
- # prompt "You build APIs"
22
- # tools :Read, :Edit, :Bash
23
- # end
24
- # end
25
- # result = swarm.execute("Build authentication")
26
- #
27
- # ## YAML String API
28
- #
29
- # yaml = File.read("swarm.yml")
30
- # swarm = SwarmSDK.load(yaml, base_dir: "/path/to/project")
31
- # result = swarm.execute("Build authentication")
32
- #
33
- # ## YAML File API (Convenience)
34
- #
35
- # swarm = SwarmSDK.load_file("swarm.yml")
36
- # result = swarm.execute("Build authentication")
37
- #
38
- # ## Direct API (Advanced)
39
- #
40
- # swarm = Swarm.new(name: "Development Team")
41
- #
42
- # backend_agent = Agent::Definition.new(:backend, {
43
- # description: "Backend developer",
44
- # model: "gpt-5",
45
- # system_prompt: "You build APIs and databases...",
46
- # tools: [:Read, :Edit, :Bash],
47
- # delegates_to: [:database]
48
- # })
49
- # swarm.add_agent(backend_agent)
50
- #
51
- # swarm.lead = :backend
52
- # result = swarm.execute("Build authentication")
53
- #
54
- # ## Architecture
55
- #
56
- # All APIs converge on Agent::Definition for validation.
57
- # Swarm delegates to specialized concerns:
58
- # - Agent::Definition: Validates configuration, builds system prompts
59
- # - AgentInitializer: Complex 5-pass agent setup
60
- # - ToolConfigurator: Tool creation and permissions (via AgentInitializer)
61
- # - McpConfigurator: MCP client management (via AgentInitializer)
62
- #
63
- class Swarm
64
- include Concerns::Snapshotable
65
- include Concerns::Validatable
66
- include Concerns::Cleanupable
67
- include LoggingCallbacks
68
- include HookTriggers
69
-
70
- # Backward compatibility aliases - use Defaults module for new code
71
- DEFAULT_MCP_LOG_LEVEL = Defaults::Logging::MCP_LOG_LEVEL
72
-
73
- # Default tools available to all agents
74
- DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
75
-
76
- attr_reader :name, :agents, :lead_agent, :mcp_clients, :delegation_instances, :agent_definitions, :swarm_id, :parent_swarm_id, :swarm_registry, :scratchpad_storage, :allow_filesystem_tools, :hook_registry, :global_semaphore, :plugin_storages, :config_for_hooks, :observer_configs
77
- attr_accessor :delegation_call_stack
78
-
79
- # Check if scratchpad tools are enabled
80
- #
81
- # @return [Boolean]
82
- def scratchpad_enabled?
83
- @scratchpad_mode == :enabled
84
- end
85
- attr_writer :config_for_hooks
86
-
87
- # Check if first message has been sent (for system reminder injection)
88
- #
89
- # @return [Boolean]
90
- def first_message_sent?
91
- @first_message_sent
92
- end
93
-
94
- # Set first message sent flag (used by snapshot/restore)
95
- #
96
- # @param value [Boolean] New value
97
- # @return [void]
98
- attr_writer :first_message_sent
99
-
100
- # Class-level MCP log level configuration
101
- @mcp_log_level = DEFAULT_MCP_LOG_LEVEL
102
- @mcp_logging_configured = false
103
-
104
- class << self
105
- attr_accessor :mcp_log_level
106
-
107
- # Configure MCP client logging globally
108
- #
109
- # This should be called before creating any swarms that use MCP servers.
110
- # The configuration is global and affects all MCP clients.
111
- #
112
- # @param level [Integer] Log level (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL)
113
- # @return [void]
114
- def configure_mcp_logging(level = DEFAULT_MCP_LOG_LEVEL)
115
- @mcp_log_level = level
116
- apply_mcp_logging_configuration
117
- end
118
-
119
- # Apply MCP logging configuration to RubyLLM::MCP
120
- #
121
- # @return [void]
122
- def apply_mcp_logging_configuration
123
- return if @mcp_logging_configured
124
-
125
- RubyLLM::MCP.configure do |config|
126
- config.log_level = @mcp_log_level
127
- end
128
-
129
- @mcp_logging_configured = true
130
- end
131
- end
132
-
133
- # Initialize a new Swarm
134
- #
135
- # @param name [String] Human-readable swarm name
136
- # @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
137
- # @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
138
- # @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
139
- # @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
140
- # @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
141
- # @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
142
- # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
143
- def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: Defaults::Concurrency::GLOBAL_LIMIT, default_local_concurrency: Defaults::Concurrency::LOCAL_LIMIT, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
144
- @name = name
145
- @swarm_id = swarm_id || generate_swarm_id(name)
146
- @parent_swarm_id = parent_swarm_id
147
- @global_concurrency = global_concurrency
148
- @default_local_concurrency = default_local_concurrency
149
-
150
- # Handle scratchpad_mode parameter
151
- # For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
152
- @scratchpad_mode = validate_swarm_scratchpad_mode(scratchpad_mode)
153
-
154
- # Resolve allow_filesystem_tools with priority:
155
- # 1. Explicit parameter (if not nil)
156
- # 2. Global settings
157
- @allow_filesystem_tools = if allow_filesystem_tools.nil?
158
- SwarmSDK.settings.allow_filesystem_tools
159
- else
160
- allow_filesystem_tools
161
- end
162
-
163
- # Swarm registry for managing sub-swarms (initialized later if needed)
164
- @swarm_registry = nil
165
-
166
- # Delegation call stack for circular dependency detection
167
- @delegation_call_stack = []
168
-
169
- # Shared semaphore for all agents
170
- @global_semaphore = Async::Semaphore.new(@global_concurrency)
171
-
172
- # Shared scratchpad storage for all agents (volatile)
173
- # Use provided scratchpad storage (for testing) or create volatile one based on mode
174
- @scratchpad_storage = if scratchpad
175
- scratchpad # Testing/internal use - explicit instance provided
176
- elsif @scratchpad_mode == :enabled
177
- Tools::Stores::ScratchpadStorage.new
178
- end
179
-
180
- # Per-agent plugin storages (persistent)
181
- # Format: { plugin_name => { agent_name => storage } }
182
- # Will be populated when agents are initialized
183
- @plugin_storages = {}
184
-
185
- # Hook registry for named hooks and swarm defaults
186
- @hook_registry = Hooks::Registry.new
187
-
188
- # Register default logging hooks
189
- register_default_logging_callbacks
190
-
191
- # Agent definitions and instances
192
- @agent_definitions = {}
193
- @agents = {}
194
- @delegation_instances = {} # { "delegate@delegator" => Agent::Chat }
195
- @agents_initialized = false
196
- @agent_contexts = {}
197
-
198
- # MCP clients per agent (for cleanup)
199
- @mcp_clients = Hash.new { |h, k| h[k] = [] }
200
-
201
- @lead_agent = nil
202
-
203
- # Track if first message has been sent
204
- @first_message_sent = false
205
-
206
- # Track if agent_start events have been emitted
207
- # This prevents duplicate emissions and ensures events are emitted when logging is ready
208
- @agent_start_events_emitted = false
209
-
210
- # Observer agent configurations
211
- @observer_configs = []
212
- @observer_manager = nil
213
- end
214
-
215
- # Add an agent to the swarm
216
- #
217
- # Accepts only Agent::Definition objects. This ensures all validation
218
- # happens in a single place (Agent::Definition) and keeps the API clean.
219
- #
220
- # If the definition doesn't specify max_concurrent_tools, the swarm's
221
- # default_local_concurrency is applied.
222
- #
223
- # @param definition [Agent::Definition] Fully configured agent definition
224
- # @return [self]
225
- #
226
- # @example
227
- # definition = Agent::Definition.new(:backend, {
228
- # description: "Backend developer",
229
- # model: "gpt-5",
230
- # system_prompt: "You build APIs"
231
- # })
232
- # swarm.add_agent(definition)
233
- def add_agent(definition)
234
- unless definition.is_a?(Agent::Definition)
235
- raise ArgumentError, "Expected Agent::Definition, got #{definition.class}"
236
- end
237
-
238
- name = definition.name
239
- raise ConfigurationError, "Agent '#{name}' already exists" if @agent_definitions.key?(name)
240
-
241
- # Apply swarm's default_local_concurrency if max_concurrent_tools not set
242
- definition.max_concurrent_tools = @default_local_concurrency if definition.max_concurrent_tools.nil?
243
-
244
- @agent_definitions[name] = definition
245
- self
246
- end
247
-
248
- # Set the lead agent (entry point for swarm execution)
249
- #
250
- # @param name [Symbol, String] Name of agent to make lead
251
- # @return [self]
252
- def lead=(name)
253
- name = name.to_sym
254
-
255
- unless @agent_definitions.key?(name)
256
- raise ConfigurationError, "Cannot set lead: agent '#{name}' not found"
257
- end
258
-
259
- @lead_agent = name
260
- end
261
-
262
- # Execute a task using the lead agent
263
- #
264
- # The lead agent can delegate to other agents via tool calls,
265
- # and the entire swarm coordinates with shared rate limiting.
266
- # Supports reprompting via swarm_stop hooks.
267
- #
268
- # By default, this method blocks until execution completes. Set wait: false
269
- # to return an Async::Task immediately, enabling cancellation via task.stop.
270
- #
271
- # @param prompt [String] Task to execute
272
- # @param wait [Boolean] If true (default), blocks until execution completes.
273
- # If false, returns Async::Task immediately for non-blocking execution.
274
- # @yield [Hash] Log entry if block given (for streaming)
275
- # @return [Result, Async::Task] Result if wait: true, Async::Task if wait: false
276
- #
277
- # @example Blocking execution (default)
278
- # result = swarm.execute("Build auth")
279
- # puts result.content
280
- #
281
- # @example Non-blocking execution with cancellation
282
- # task = swarm.execute("Build auth", wait: false) { |event| puts event }
283
- # # ... do other work ...
284
- # task.stop # Cancel anytime
285
- # result = task.wait # Returns nil for cancelled tasks
286
- def execute(prompt, wait: true, &block)
287
- raise ConfigurationError, "No lead agent set. Set lead= first." unless @lead_agent
288
-
289
- logs = []
290
- current_prompt = prompt
291
- has_logging = block_given?
292
-
293
- # Save original Fiber storage for restoration (preserves parent context for nested swarms)
294
- original_fiber_storage = {
295
- execution_id: Fiber[:execution_id],
296
- swarm_id: Fiber[:swarm_id],
297
- parent_swarm_id: Fiber[:parent_swarm_id],
298
- }
299
-
300
- # Set fiber-local execution context
301
- # Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
302
- Fiber[:execution_id] ||= generate_execution_id
303
- Fiber[:swarm_id] = @swarm_id
304
- Fiber[:parent_swarm_id] = @parent_swarm_id
305
-
306
- # Setup logging FIRST if block given (so swarm_start event can be emitted)
307
- setup_logging(logs, &block) if has_logging
308
-
309
- # Setup observer execution if any observers configured
310
- # MUST happen AFTER setup_logging (which clears Fiber[:log_subscriptions])
311
- setup_observer_execution if @observer_configs.any?
312
-
313
- # Trigger swarm_start hooks (before any execution)
314
- current_prompt = apply_swarm_start_hooks(current_prompt)
315
-
316
- # Trigger first_message hooks on first execution
317
- unless @first_message_sent
318
- trigger_first_message(current_prompt)
319
- @first_message_sent = true
320
- end
321
-
322
- # Lazy initialization of agents (with optional logging)
323
- initialize_agents unless @agents_initialized
324
-
325
- # Emit agent_start events if agents were initialized before logging was set up
326
- emit_retroactive_agent_start_events if has_logging
327
-
328
- # Delegate to Executor for actual execution
329
- executor = Executor.new(self)
330
- @current_task = executor.run(
331
- current_prompt,
332
- wait: wait,
333
- logs: logs,
334
- has_logging: has_logging,
335
- original_fiber_storage: original_fiber_storage,
336
- )
337
- end
338
-
339
- # Get an agent chat instance by name
340
- #
341
- # @param name [Symbol, String] Agent name
342
- # @return [AgentChat] Agent chat instance
343
- def agent(name)
344
- name = name.to_sym
345
- initialize_agents unless @agents_initialized
346
-
347
- @agents[name] || raise(AgentNotFoundError, "Agent '#{name}' not found")
348
- end
349
-
350
- # Get an agent definition by name
351
- #
352
- # Use this to access and modify agent configuration:
353
- # swarm.agent_definition(:backend).bypass_permissions = true
354
- #
355
- # @param name [Symbol, String] Agent name
356
- # @return [AgentDefinition] Agent definition object
357
- def agent_definition(name)
358
- name = name.to_sym
359
-
360
- @agent_definitions[name] || raise(AgentNotFoundError, "Agent '#{name}' not found")
361
- end
362
-
363
- # Get all agent names
364
- #
365
- # @return [Array<Symbol>] Agent names
366
- def agent_names
367
- @agent_definitions.keys
368
- end
369
-
370
- # Implement Snapshotable interface
371
- def primary_agents
372
- @agents
373
- end
374
-
375
- def delegation_instances_hash
376
- @delegation_instances
377
- end
378
-
379
- # NOTE: validate() and emit_validation_warnings() are provided by Concerns::Validatable
380
- # Note: cleanup() is provided by Concerns::Cleanupable
381
-
382
- # Register a named hook that can be referenced in agent configurations
383
- #
384
- # Named hooks are stored in the registry and can be referenced by symbol
385
- # in agent YAML configurations or programmatically.
386
- #
387
- # @param name [Symbol] Unique hook name
388
- # @param block [Proc] Hook implementation
389
- # @return [self]
390
- #
391
- # @example Register a validation hook
392
- # swarm.register_hook(:validate_code) do |context|
393
- # raise SwarmSDK::Hooks::Error, "Invalid" unless valid?(context.tool_call)
394
- # end
395
- def register_hook(name, &block)
396
- @hook_registry.register(name, &block)
397
- self
398
- end
399
-
400
- # Reset context for all agents
401
- #
402
- # Clears conversation history for all agents. This is used by composable swarms
403
- # to reset sub-swarm context when keep_context: false is specified.
404
- #
405
- # @return [void]
406
- def reset_context!
407
- @agents.each_value do |agent_chat|
408
- agent_chat.clear_conversation if agent_chat.respond_to?(:clear_conversation)
409
- end
410
- end
411
-
412
- # Add observer configuration
413
- #
414
- # Called by Swarm::Builder to register observer agent configurations.
415
- # Validates that the referenced agent exists.
416
- #
417
- # @param config [Observer::Config] Observer configuration
418
- # @return [void]
419
- def add_observer_config(config)
420
- validate_observer_agent(config.agent_name)
421
- @observer_configs << config
422
- end
423
-
424
- # Wait for all observer tasks to complete
425
- #
426
- # Called by Executor to wait for observer agents before cleanup.
427
- # Safe to call even if no observers are configured.
428
- #
429
- # @return [void]
430
- def wait_for_observers
431
- @observer_manager&.wait_for_completion
432
- end
433
-
434
- # Cleanup observer subscriptions
435
- #
436
- # Called by Executor.cleanup_after_execution to unsubscribe observers.
437
- # Matches the MCP cleanup pattern.
438
- #
439
- # @return [void]
440
- def cleanup_observers
441
- @observer_manager&.cleanup
442
- @observer_manager = nil
443
- end
444
-
445
- # Create snapshot of current conversation state
446
- #
447
- # Returns a Snapshot object containing:
448
- # - All agent conversations (@messages arrays)
449
- # - Agent context state (warnings, compression, TodoWrite tracking, skills)
450
- # - Delegation instance conversations
451
- # - Scratchpad contents (volatile shared storage)
452
- # - Read tracking state (which files each agent has read with digests)
453
- # - Memory read tracking state (which memory entries each agent has read with digests)
454
- #
455
- # Configuration (agent definitions, tools, prompts) stays in your YAML/DSL
456
- # and is NOT included in snapshots.
457
- #
458
- # @return [Snapshot] Snapshot object with convenient serialization methods
459
- #
460
- # @example Save snapshot to JSON file
461
- # snapshot = swarm.snapshot
462
- # snapshot.write_to_file("session.json")
463
- #
464
- # @example Convert to hash or JSON string
465
- # snapshot = swarm.snapshot
466
- # hash = snapshot.to_hash
467
- # json_string = snapshot.to_json
468
- def snapshot
469
- StateSnapshot.new(self).snapshot
470
- end
471
-
472
- # Restore conversation state from snapshot
473
- #
474
- # Accepts a Snapshot object, hash, or JSON string. Validates compatibility
475
- # between snapshot and current swarm configuration, restores agent conversations,
476
- # context state, scratchpad, and read tracking. Returns RestoreResult with
477
- # warnings about any agents that couldn't be restored due to configuration
478
- # mismatches.
479
- #
480
- # The swarm must be created with the SAME configuration (agent definitions,
481
- # tools, prompts) as when the snapshot was created. Only conversation state
482
- # is restored from the snapshot.
483
- #
484
- # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
485
- # @return [RestoreResult] Result with warnings about skipped agents
486
- #
487
- # @example Restore from Snapshot object
488
- # swarm = SwarmSDK.build { ... } # Same config as snapshot
489
- # snapshot = Snapshot.from_file("session.json")
490
- # result = swarm.restore(snapshot)
491
- # if result.success?
492
- # puts "All agents restored"
493
- # else
494
- # puts result.summary
495
- # result.warnings.each { |w| puts " - #{w[:message]}" }
496
- # end
497
- #
498
- # Restore swarm state from snapshot
499
- #
500
- # By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
501
- # Set preserve_system_prompts: true to use historical prompts from snapshot.
502
- #
503
- # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
504
- # @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
505
- # @return [RestoreResult] Result with warnings about partial restores
506
- def restore(snapshot, preserve_system_prompts: false)
507
- StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
508
- end
509
-
510
- # Override swarm IDs for composable swarms
511
- #
512
- # Used by SwarmLoader to set hierarchical IDs when loading sub-swarms.
513
- # This is called after the swarm is built to ensure proper parent/child relationships.
514
- #
515
- # @param swarm_id [String] New swarm ID
516
- # @param parent_swarm_id [String] New parent swarm ID
517
- # @return [void]
518
- def override_swarm_ids(swarm_id:, parent_swarm_id:)
519
- @swarm_id = swarm_id
520
- @parent_swarm_id = parent_swarm_id
521
- end
522
-
523
- # Set swarm registry for composable swarms
524
- #
525
- # Used by Builder to set the registry after swarm creation.
526
- # This must be called before agent initialization to enable swarm delegation.
527
- #
528
- # @param registry [SwarmRegistry] Configured swarm registry
529
- # @return [void]
530
- attr_writer :swarm_registry
531
-
532
- # --- Internal API (for Executor use only) ---
533
- # Hook triggers for swarm lifecycle events are provided by HookTriggers module
534
-
535
- private
536
-
537
- # Apply swarm_start hooks to prompt
538
- #
539
- # @param prompt [String] Original prompt
540
- # @return [String] Modified prompt (possibly with hook context appended)
541
- def apply_swarm_start_hooks(prompt)
542
- swarm_start_result = trigger_swarm_start(prompt)
543
- if swarm_start_result&.replace?
544
- "#{prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
545
- else
546
- prompt
547
- end
548
- end
549
-
550
- # Validate that observer agent exists
551
- #
552
- # @param agent_name [Symbol] Name of the observer agent
553
- # @raise [ConfigurationError] If agent not found
554
- # @return [void]
555
- def validate_observer_agent(agent_name)
556
- return if @agent_definitions.key?(agent_name)
557
-
558
- raise ConfigurationError,
559
- "Observer agent '#{agent_name}' not found. " \
560
- "Define the agent first with `agent :#{agent_name} do ... end`"
561
- end
562
-
563
- # Setup observer manager and subscriptions
564
- #
565
- # Creates Observer::Manager and registers event subscriptions.
566
- # Must be called AFTER setup_logging (which clears Fiber[:log_subscriptions]).
567
- #
568
- # @return [void]
569
- def setup_observer_execution
570
- @observer_manager = Observer::Manager.new(self)
571
- @observer_configs.each { |c| @observer_manager.add_config(c) }
572
- @observer_manager.setup
573
- end
574
-
575
- # Validate and normalize scratchpad mode for Swarm
576
- #
577
- # Regular Swarms support :enabled or :disabled.
578
- # Rejects :per_node since it only makes sense for Workflow with multiple nodes.
579
- #
580
- # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
581
- # @return [Symbol] :enabled or :disabled
582
- # @raise [ArgumentError] If :per_node used, or invalid value
583
- def validate_swarm_scratchpad_mode(value)
584
- # Convert strings from YAML to symbols
585
- value = value.to_sym if value.is_a?(String)
586
-
587
- case value
588
- when :enabled, :disabled
589
- value
590
- when :per_node
591
- raise ArgumentError,
592
- "scratchpad: :per_node is only valid for Workflow with nodes. " \
593
- "For regular Swarms, use :enabled or :disabled."
594
- else
595
- raise ArgumentError,
596
- "Invalid scratchpad mode for Swarm: #{value.inspect}. " \
597
- "Use :enabled or :disabled."
598
- end
599
- end
600
-
601
- # Generate a unique swarm ID from name
602
- #
603
- # Creates a swarm ID by sanitizing the name and appending a random suffix.
604
- # Used when swarm_id is not explicitly provided.
605
- #
606
- # @param name [String] Swarm name
607
- # @return [String] Generated swarm ID (e.g., "dev_team_a3f2b1c8")
608
- def generate_swarm_id(name)
609
- sanitized = name.to_s.gsub(/[^a-z0-9_-]/i, "_").downcase
610
- "#{sanitized}_#{SecureRandom.hex(4)}"
611
- end
612
-
613
- # Generate a unique execution ID
614
- #
615
- # Creates an execution ID that uniquely identifies a single swarm.execute() call.
616
- # Format: "exec_{swarm_id}_{random_hex}"
617
- #
618
- # @return [String] Generated execution ID (e.g., "exec_main_a3f2b1c8")
619
- def generate_execution_id
620
- "exec_#{@swarm_id}_#{SecureRandom.hex(8)}"
621
- end
622
-
623
- # Initialize all agents using AgentInitializer
624
- #
625
- # This is called automatically (lazy initialization) by execute() and agent().
626
- # Delegates to AgentInitializer which handles the complex 5-pass setup.
627
- #
628
- # @return [void]
629
- def initialize_agents
630
- return if @agents_initialized
631
-
632
- initializer = AgentInitializer.new(self)
633
-
634
- @agents = initializer.initialize_all
635
- @agent_contexts = initializer.agent_contexts
636
- @agents_initialized = true
637
-
638
- # NOTE: agent_start events are emitted in execute() when logging is set up
639
- # This ensures events are never lost, even if agents are initialized early (e.g., by restore())
640
- end
641
-
642
- # Normalize tools to internal format (kept for add_agent)
643
- #
644
- # Handles both Ruby API (simple symbols) and YAML API (already parsed configs)
645
- #
646
- # @param tools [Array] Tool specifications
647
- # @return [Array<Hash>] Normalized tool configs
648
- def normalize_tools(tools)
649
- Array(tools).map do |tool|
650
- case tool
651
- when Symbol, String
652
- # Simple tool from Ruby API
653
- { name: tool.to_sym, permissions: nil }
654
- when Hash
655
- # Already in config format from YAML (has :name and :permissions keys)
656
- if tool.key?(:name)
657
- tool
658
- else
659
- # Inline permissions format: { Write: { allowed_paths: [...] } }
660
- tool_name = tool.keys.first.to_sym
661
- { name: tool_name, permissions: tool[tool_name] }
662
- end
663
- else
664
- raise ConfigurationError, "Invalid tool specification: #{tool.inspect}"
665
- end
666
- end
667
- end
668
-
669
- # Delegation methods for testing (delegate to concerns)
670
- # These allow tests to verify behavior without depending on internal structure
671
-
672
- # Create a tool instance (delegates to ToolConfigurator)
673
- def create_tool_instance(tool_name, agent_name, directory)
674
- ToolConfigurator.new(self, @scratchpad_storage, @plugin_storages).create_tool_instance(tool_name, agent_name, directory)
675
- end
676
-
677
- # Wrap tool with permissions (delegates to ToolConfigurator)
678
- def wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
679
- ToolConfigurator.new(self, @scratchpad_storage, @plugin_storages).wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
680
- end
681
-
682
- # Build MCP transport config (delegates to McpConfigurator)
683
- def build_mcp_transport_config(transport_type, config)
684
- McpConfigurator.new(self).build_transport_config(transport_type, config)
685
- end
686
-
687
- # Create delegation tool (delegates to AgentInitializer)
688
- def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
689
- AgentInitializer.new(self)
690
- .create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
691
- end
692
-
693
- # Extract loggable info from plugin config
694
- #
695
- # Attempts to extract useful information from plugin configuration
696
- # for logging purposes. Handles MemoryConfig, Hashes, and other objects.
697
- #
698
- # @param config [Object] Plugin configuration object
699
- # @return [Hash, nil] Extracted config info or nil
700
- def extract_plugin_config_info(config)
701
- return if config.nil?
702
-
703
- # Handle MemoryConfig object (has directory method)
704
- if config.respond_to?(:directory)
705
- return { directory: config.directory }
706
- end
707
-
708
- # Handle Hash
709
- if config.is_a?(Hash)
710
- return config.slice(:directory, "directory", :adapter, "adapter")
711
- end
712
-
713
- # Unknown config type
714
- nil
715
- end
716
- end
717
- end