swarm_sdk 2.7.14 → 3.0.0.alpha1

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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  42. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  43. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  44. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  45. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  46. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  47. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  48. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  49. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  50. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  51. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  52. data/lib/swarm_sdk/v3.rb +145 -0
  53. metadata +83 -148
  54. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  55. data/lib/swarm_sdk/agent/builder.rb +0 -705
  56. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  57. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  58. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  59. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  60. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  61. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  62. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  63. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  64. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  65. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  66. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  67. data/lib/swarm_sdk/agent/context.rb +0 -115
  68. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  69. data/lib/swarm_sdk/agent/definition.rb +0 -588
  70. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  71. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  72. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  73. data/lib/swarm_sdk/agent_registry.rb +0 -146
  74. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  75. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  76. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  77. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  78. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  79. data/lib/swarm_sdk/config.rb +0 -368
  80. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  81. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  82. data/lib/swarm_sdk/configuration.rb +0 -165
  83. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  84. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  85. data/lib/swarm_sdk/context_compactor.rb +0 -335
  86. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  87. data/lib/swarm_sdk/context_management/context.rb +0 -328
  88. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  89. data/lib/swarm_sdk/defaults.rb +0 -251
  90. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  91. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  92. data/lib/swarm_sdk/hooks/context.rb +0 -197
  93. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  94. data/lib/swarm_sdk/hooks/error.rb +0 -29
  95. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  96. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  97. data/lib/swarm_sdk/hooks/result.rb +0 -150
  98. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  99. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  100. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  101. data/lib/swarm_sdk/log_collector.rb +0 -227
  102. data/lib/swarm_sdk/log_stream.rb +0 -127
  103. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  104. data/lib/swarm_sdk/model_aliases.json +0 -8
  105. data/lib/swarm_sdk/models.json +0 -44002
  106. data/lib/swarm_sdk/models.rb +0 -161
  107. data/lib/swarm_sdk/node_context.rb +0 -245
  108. data/lib/swarm_sdk/observer/builder.rb +0 -81
  109. data/lib/swarm_sdk/observer/config.rb +0 -45
  110. data/lib/swarm_sdk/observer/manager.rb +0 -248
  111. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  112. data/lib/swarm_sdk/permissions/config.rb +0 -239
  113. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  114. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  115. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  116. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  117. data/lib/swarm_sdk/plugin.rb +0 -309
  118. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  119. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  120. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  121. data/lib/swarm_sdk/restore_result.rb +0 -65
  122. data/lib/swarm_sdk/result.rb +0 -241
  123. data/lib/swarm_sdk/snapshot.rb +0 -156
  124. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  125. data/lib/swarm_sdk/state_restorer.rb +0 -476
  126. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  127. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  128. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  129. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  130. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  131. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  132. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  133. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  134. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  135. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  136. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  137. data/lib/swarm_sdk/swarm.rb +0 -973
  138. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  139. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  140. data/lib/swarm_sdk/tools/base.rb +0 -63
  141. data/lib/swarm_sdk/tools/bash.rb +0 -280
  142. data/lib/swarm_sdk/tools/clock.rb +0 -46
  143. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  144. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  145. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  146. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  147. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  148. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  149. data/lib/swarm_sdk/tools/edit.rb +0 -145
  150. data/lib/swarm_sdk/tools/glob.rb +0 -166
  151. data/lib/swarm_sdk/tools/grep.rb +0 -235
  152. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  153. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  154. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  155. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  156. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  157. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  158. data/lib/swarm_sdk/tools/read.rb +0 -261
  159. data/lib/swarm_sdk/tools/registry.rb +0 -205
  160. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  161. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  163. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  164. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  165. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  166. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  167. data/lib/swarm_sdk/tools/think.rb +0 -100
  168. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  169. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  170. data/lib/swarm_sdk/tools/write.rb +0 -112
  171. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  172. data/lib/swarm_sdk/utils.rb +0 -68
  173. data/lib/swarm_sdk/validation_result.rb +0 -33
  174. data/lib/swarm_sdk/version.rb +0 -5
  175. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  176. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  177. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  178. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  179. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  180. data/lib/swarm_sdk/workflow.rb +0 -589
  181. data/lib/swarm_sdk.rb +0 -721
@@ -1,973 +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
- # NOTE: MCP log level now accessed via SwarmSDK.config.mcp_log_level
71
-
72
- # Default tools available to all agents
73
- DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
74
-
75
- 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, :execution_timeout, :stop_signal_read
76
-
77
- # Check if scratchpad tools are enabled
78
- #
79
- # @return [Boolean]
80
- def scratchpad_enabled?
81
- @scratchpad_mode == :enabled
82
- end
83
- attr_writer :config_for_hooks
84
-
85
- # Check if first message has been sent (for system reminder injection)
86
- #
87
- # @return [Boolean]
88
- def first_message_sent?
89
- @first_message_sent
90
- end
91
-
92
- # Set first message sent flag (used by snapshot/restore)
93
- #
94
- # @param value [Boolean] New value
95
- # @return [void]
96
- attr_writer :first_message_sent
97
-
98
- # Class-level MCP log level configuration
99
- @mcp_log_level = nil
100
- @mcp_logging_configured = false
101
-
102
- class << self
103
- attr_accessor :mcp_log_level
104
-
105
- # Configure MCP client logging globally
106
- #
107
- # This should be called before creating any swarms that use MCP servers.
108
- # The configuration is global and affects all MCP clients.
109
- #
110
- # @param level [Integer] Log level (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL)
111
- # @return [void]
112
- def configure_mcp_logging(level = nil)
113
- @mcp_log_level = level || SwarmSDK.config.mcp_log_level
114
- apply_mcp_logging_configuration
115
- end
116
-
117
- # Apply MCP logging configuration to RubyLLM::MCP
118
- #
119
- # @return [void]
120
- def apply_mcp_logging_configuration
121
- return if @mcp_logging_configured
122
-
123
- RubyLLM::MCP.configure do |config|
124
- config.log_level = @mcp_log_level || SwarmSDK.config.mcp_log_level
125
- end
126
-
127
- @mcp_logging_configured = true
128
- end
129
- end
130
-
131
- # Initialize a new Swarm
132
- #
133
- # @param name [String] Human-readable swarm name
134
- # @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
135
- # @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
136
- # @param global_concurrency [Integer, nil] Max concurrent LLM calls across entire swarm (nil uses config default)
137
- # @param default_local_concurrency [Integer, nil] Default max concurrent tool calls per agent (nil uses config default)
138
- # @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
139
- # @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
140
- # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
141
- def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: nil, default_local_concurrency: nil, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil, execution_timeout: :__use_default__)
142
- @name = name
143
- @swarm_id = swarm_id || generate_swarm_id(name)
144
- @parent_swarm_id = parent_swarm_id
145
- @global_concurrency = global_concurrency || SwarmSDK.config.global_concurrency_limit
146
- @default_local_concurrency = default_local_concurrency || SwarmSDK.config.local_concurrency_limit
147
-
148
- # Use default from config unless explicitly set (including nil to disable)
149
- @execution_timeout = if execution_timeout == :__use_default__
150
- SwarmSDK.config.default_execution_timeout
151
- else
152
- execution_timeout # Could be a number OR nil (to disable)
153
- end
154
-
155
- # Validate execution_timeout is positive if set
156
- if @execution_timeout && @execution_timeout <= 0
157
- raise ConfigurationError, "execution_timeout must be positive (got #{@execution_timeout})"
158
- end
159
-
160
- # Handle scratchpad_mode parameter
161
- # For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
162
- @scratchpad_mode = validate_swarm_scratchpad_mode(scratchpad_mode)
163
-
164
- # Resolve allow_filesystem_tools with priority:
165
- # 1. Explicit parameter (if not nil)
166
- # 2. Global config
167
- @allow_filesystem_tools = if allow_filesystem_tools.nil?
168
- SwarmSDK.config.allow_filesystem_tools
169
- else
170
- allow_filesystem_tools
171
- end
172
-
173
- # Swarm registry for managing sub-swarms (initialized later if needed)
174
- @swarm_registry = nil
175
-
176
- # Shared semaphore for all agents
177
- @global_semaphore = Async::Semaphore.new(@global_concurrency)
178
-
179
- # Shared scratchpad storage for all agents (volatile)
180
- # Use provided scratchpad storage (for testing) or create volatile one based on mode
181
- @scratchpad_storage = if scratchpad
182
- scratchpad # Testing/internal use - explicit instance provided
183
- elsif @scratchpad_mode == :enabled
184
- Tools::Stores::ScratchpadStorage.new
185
- end
186
-
187
- # Per-agent plugin storages (persistent)
188
- # Format: { plugin_name => { agent_name => storage } }
189
- # Will be populated when agents are initialized
190
- @plugin_storages = {}
191
-
192
- # Hook registry for named hooks and swarm defaults
193
- @hook_registry = Hooks::Registry.new
194
-
195
- # Register default logging hooks
196
- register_default_logging_callbacks
197
-
198
- # Agent definitions and instances
199
- @agent_definitions = {}
200
- @agents = {}
201
- @delegation_instances = {} # { "delegate@delegator" => Agent::Chat }
202
- @agents_initialized = false
203
- @agent_contexts = {}
204
-
205
- # MCP clients per agent (for cleanup)
206
- @mcp_clients = Hash.new { |h, k| h[k] = [] }
207
-
208
- @lead_agent = nil
209
-
210
- # Track if first message has been sent
211
- @first_message_sent = false
212
-
213
- # Track if agent_start events have been emitted
214
- # This prevents duplicate emissions and ensures events are emitted when logging is ready
215
- @agent_start_events_emitted = false
216
-
217
- # Observer agent configurations
218
- @observer_configs = []
219
- @observer_manager = nil
220
-
221
- # Stop mechanism state
222
- @stop_requested = false
223
- @execution_barrier = nil
224
- @stop_signal_read = nil
225
- @stop_signal_write = nil
226
- @active_agent_chats = {}
227
- end
228
-
229
- # Add an agent to the swarm
230
- #
231
- # Accepts only Agent::Definition objects. This ensures all validation
232
- # happens in a single place (Agent::Definition) and keeps the API clean.
233
- #
234
- # If the definition doesn't specify max_concurrent_tools, the swarm's
235
- # default_local_concurrency is applied.
236
- #
237
- # @param definition [Agent::Definition] Fully configured agent definition
238
- # @return [self]
239
- #
240
- # @example
241
- # definition = Agent::Definition.new(:backend, {
242
- # description: "Backend developer",
243
- # model: "gpt-5",
244
- # system_prompt: "You build APIs"
245
- # })
246
- # swarm.add_agent(definition)
247
- def add_agent(definition)
248
- unless definition.is_a?(Agent::Definition)
249
- raise ArgumentError, "Expected Agent::Definition, got #{definition.class}"
250
- end
251
-
252
- name = definition.name
253
- raise ConfigurationError, "Agent '#{name}' already exists" if @agent_definitions.key?(name)
254
-
255
- # Apply swarm's default_local_concurrency if max_concurrent_tools not set
256
- definition.max_concurrent_tools = @default_local_concurrency if definition.max_concurrent_tools.nil?
257
-
258
- @agent_definitions[name] = definition
259
- self
260
- end
261
-
262
- # Set the lead agent (entry point for swarm execution)
263
- #
264
- # @param name [Symbol, String] Name of agent to make lead
265
- # @return [self]
266
- def lead=(name)
267
- name = name.to_sym
268
-
269
- unless @agent_definitions.key?(name)
270
- raise ConfigurationError, "Cannot set lead: agent '#{name}' not found"
271
- end
272
-
273
- @lead_agent = name
274
- end
275
-
276
- # Execute a task using the lead agent
277
- #
278
- # The lead agent can delegate to other agents via tool calls,
279
- # and the entire swarm coordinates with shared rate limiting.
280
- # Supports reprompting via swarm_stop hooks.
281
- #
282
- # By default, this method blocks until execution completes. Set wait: false
283
- # to return an Async::Task immediately, enabling cancellation via task.stop.
284
- #
285
- # @param prompt [String] Task to execute
286
- # @param wait [Boolean] If true (default), blocks until execution completes.
287
- # If false, returns Async::Task immediately for non-blocking execution.
288
- # @yield [Hash] Log entry if block given (for streaming)
289
- # @return [Result, Async::Task] Result if wait: true, Async::Task if wait: false
290
- #
291
- # @example Blocking execution (default)
292
- # result = swarm.execute("Build auth")
293
- # puts result.content
294
- #
295
- # @example Non-blocking execution with cancellation
296
- # task = swarm.execute("Build auth", wait: false) { |event| puts event }
297
- # # ... do other work ...
298
- # task.stop # Cancel anytime
299
- # result = task.wait # Returns nil for cancelled tasks
300
- def execute(prompt, wait: true, &block)
301
- raise ConfigurationError, "No lead agent set. Set lead= first." unless @lead_agent
302
-
303
- logs = []
304
- current_prompt = prompt
305
- has_logging = block_given?
306
-
307
- # Save original Fiber storage for restoration (preserves parent context for nested swarms)
308
- original_fiber_storage = {
309
- execution_id: Fiber[:execution_id],
310
- swarm_id: Fiber[:swarm_id],
311
- parent_swarm_id: Fiber[:parent_swarm_id],
312
- }
313
-
314
- # Set fiber-local execution context
315
- # Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
316
- Fiber[:execution_id] ||= generate_execution_id
317
- Fiber[:swarm_id] = @swarm_id
318
- Fiber[:parent_swarm_id] = @parent_swarm_id
319
-
320
- # Setup logging FIRST if block given (so swarm_start event can be emitted)
321
- setup_logging(logs, &block) if has_logging
322
-
323
- # Setup observer execution if any observers configured
324
- # MUST happen AFTER setup_logging (which clears Fiber[:log_subscriptions])
325
- setup_observer_execution if @observer_configs.any?
326
-
327
- # Trigger swarm_start hooks (before any execution)
328
- current_prompt = apply_swarm_start_hooks(current_prompt)
329
-
330
- # Trigger first_message hooks on first execution
331
- unless @first_message_sent
332
- trigger_first_message(current_prompt)
333
- @first_message_sent = true
334
- end
335
-
336
- # Lazy initialization of agents (with optional logging)
337
- initialize_agents unless @agents_initialized
338
-
339
- # Emit agent_start events if agents were initialized before logging was set up
340
- emit_retroactive_agent_start_events if has_logging
341
-
342
- # Delegate to Executor for actual execution
343
- executor = Executor.new(self)
344
- @current_task = executor.run(
345
- current_prompt,
346
- wait: wait,
347
- logs: logs,
348
- has_logging: has_logging,
349
- original_fiber_storage: original_fiber_storage,
350
- )
351
- end
352
-
353
- # Get an agent chat instance by name
354
- #
355
- # @param name [Symbol, String] Agent name
356
- # @return [AgentChat] Agent chat instance
357
- def agent(name)
358
- name = name.to_sym
359
- initialize_agents unless @agents_initialized
360
-
361
- @agents[name] || raise(AgentNotFoundError, "Agent '#{name}' not found")
362
- end
363
-
364
- # Get an agent definition by name
365
- #
366
- # Use this to access and modify agent configuration:
367
- # swarm.agent_definition(:backend).bypass_permissions = true
368
- #
369
- # @param name [Symbol, String] Agent name
370
- # @return [AgentDefinition] Agent definition object
371
- def agent_definition(name)
372
- name = name.to_sym
373
-
374
- @agent_definitions[name] || raise(AgentNotFoundError, "Agent '#{name}' not found")
375
- end
376
-
377
- # Get all agent names
378
- #
379
- # @return [Array<Symbol>] Agent names
380
- def agent_names
381
- @agent_definitions.keys
382
- end
383
-
384
- # Get context usage breakdown for all agents
385
- #
386
- # Returns per-agent context statistics including tokens used, context limit,
387
- # usage percentage, and cost. Useful for monitoring context window consumption
388
- # across the swarm.
389
- #
390
- # @return [Hash{Symbol => Hash}] Per-agent context breakdown
391
- #
392
- # @example
393
- # breakdown = swarm.context_breakdown
394
- # breakdown[:backend]
395
- # # => {
396
- # # input_tokens: 15000,
397
- # # output_tokens: 5000,
398
- # # total_tokens: 20000,
399
- # # cached_tokens: 2000,
400
- # # context_limit: 200000,
401
- # # usage_percentage: 10.0,
402
- # # tokens_remaining: 180000,
403
- # # input_cost: 0.045,
404
- # # output_cost: 0.075,
405
- # # total_cost: 0.12
406
- # # }
407
- def context_breakdown
408
- initialize_agents unless @agents_initialized
409
-
410
- breakdown = {}
411
-
412
- # Include primary agents
413
- @agents.each do |name, chat|
414
- breakdown[name] = build_agent_context_info(chat)
415
- end
416
-
417
- # Include delegation instances
418
- @delegation_instances.each do |instance_name, chat|
419
- breakdown[instance_name.to_sym] = build_agent_context_info(chat)
420
- end
421
-
422
- breakdown
423
- end
424
-
425
- # Implement Snapshotable interface
426
- def primary_agents
427
- @agents
428
- end
429
-
430
- def delegation_instances_hash
431
- @delegation_instances
432
- end
433
-
434
- # NOTE: validate() and emit_validation_warnings() are provided by Concerns::Validatable
435
- # Note: cleanup() is provided by Concerns::Cleanupable
436
-
437
- # Register a named hook that can be referenced in agent configurations
438
- #
439
- # Named hooks are stored in the registry and can be referenced by symbol
440
- # in agent YAML configurations or programmatically.
441
- #
442
- # @param name [Symbol] Unique hook name
443
- # @param block [Proc] Hook implementation
444
- # @return [self]
445
- #
446
- # @example Register a validation hook
447
- # swarm.register_hook(:validate_code) do |context|
448
- # raise SwarmSDK::Hooks::Error, "Invalid" unless valid?(context.tool_call)
449
- # end
450
- def register_hook(name, &block)
451
- @hook_registry.register(name, &block)
452
- self
453
- end
454
-
455
- # Reset context for all agents
456
- #
457
- # Clears conversation history for all agents. This is used by composable swarms
458
- # to reset sub-swarm context when keep_context: false is specified.
459
- #
460
- # @return [void]
461
- def reset_context!
462
- @agents.each_value do |agent_chat|
463
- agent_chat.clear_conversation if agent_chat.respond_to?(:clear_conversation)
464
- end
465
- end
466
-
467
- # Add observer configuration
468
- #
469
- # Called by Swarm::Builder to register observer agent configurations.
470
- # Validates that the referenced agent exists.
471
- #
472
- # @param config [Observer::Config] Observer configuration
473
- # @return [void]
474
- def add_observer_config(config)
475
- validate_observer_agent(config.agent_name)
476
- @observer_configs << config
477
- end
478
-
479
- # Wait for all observer tasks to complete
480
- #
481
- # If a stop was requested, stops observer tasks immediately instead of waiting.
482
- # Called by Executor to wait for observer agents before cleanup.
483
- # Safe to call even if no observers are configured.
484
- #
485
- # @return [void]
486
- def wait_for_observers
487
- if @stop_requested
488
- stop_observers
489
- else
490
- @observer_manager&.wait_for_completion
491
- end
492
- end
493
-
494
- # Cleanup observer subscriptions
495
- #
496
- # Called by Executor.cleanup_after_execution to unsubscribe observers.
497
- # Matches the MCP cleanup pattern.
498
- #
499
- # @return [void]
500
- def cleanup_observers
501
- @observer_manager&.cleanup
502
- @observer_manager = nil
503
- end
504
-
505
- # Stop all swarm execution immediately
506
- #
507
- # Thread-safe method that signals the execution to stop. Uses IO.pipe
508
- # for cross-thread signaling, which wakes the Async scheduler from any
509
- # thread. The stop listener task then calls barrier.stop within the
510
- # reactor to cancel all executing tasks.
511
- #
512
- # Safe to call from event callbacks, other threads, or signal handlers.
513
- # No-op if no execution is in progress or stop was already requested.
514
- #
515
- # @return [void]
516
- #
517
- # @example Stop from event callback
518
- # swarm.execute("Build auth") do |event|
519
- # swarm.stop if event[:type] == "tool_call" && event[:tool] == "Dangerous"
520
- # end
521
- #
522
- # @example Stop from another thread
523
- # Thread.new { swarm.execute("Build auth") }
524
- # sleep 10
525
- # swarm.stop
526
- def stop
527
- return if @stop_requested
528
-
529
- @stop_requested = true
530
- begin
531
- @stop_signal_write&.write("x") unless @stop_signal_write&.closed?
532
- @stop_signal_write&.close unless @stop_signal_write&.closed?
533
- rescue IOError, Errno::EPIPE
534
- # Pipe already closed - normal during cleanup
535
- end
536
- end
537
-
538
- # Check if a stop has been requested
539
- #
540
- # @return [Boolean] true if stop was requested
541
- def stop_requested?
542
- @stop_requested
543
- end
544
-
545
- # Prepare stop signaling for a new execution
546
- #
547
- # Resets the stop flag and creates a new IO.pipe for signaling.
548
- # Called by Executor at the start of each execution.
549
- #
550
- # @return [void]
551
- def prepare_for_execution
552
- @stop_requested = false
553
- @stop_signal_read, @stop_signal_write = IO.pipe
554
- @active_agent_chats = {}
555
- end
556
-
557
- # Close the stop signal pipe
558
- #
559
- # Called by Executor after execution completes.
560
- #
561
- # @return [void]
562
- def cleanup_stop_signal
563
- @stop_signal_read&.close unless @stop_signal_read&.closed?
564
- @stop_signal_write&.close unless @stop_signal_write&.closed?
565
- @stop_signal_read = nil
566
- @stop_signal_write = nil
567
- end
568
-
569
- # Register the execution barrier for stop cancellation
570
- #
571
- # @param barrier [Async::Barrier] The barrier wrapping execution tasks
572
- # @return [void]
573
- def register_execution_barrier(barrier)
574
- @execution_barrier = barrier
575
- end
576
-
577
- # Clear the execution barrier reference
578
- #
579
- # @return [void]
580
- def clear_execution_barrier
581
- @execution_barrier = nil
582
- end
583
-
584
- # Mark an agent as actively executing an LLM call
585
- #
586
- # Called by Agent::Chat#execute_ask to track which agents are mid-execution.
587
- # Used during interruption to emit agent_stop events for active agents.
588
- #
589
- # @param name [Symbol] Agent name
590
- # @param chat [Agent::Chat] Agent chat instance
591
- # @return [void]
592
- def mark_agent_active(name, chat)
593
- @active_agent_chats[name] = chat
594
- end
595
-
596
- # Mark an agent as no longer actively executing
597
- #
598
- # @param name [Symbol] Agent name
599
- # @return [void]
600
- def mark_agent_inactive(name)
601
- @active_agent_chats.delete(name)
602
- end
603
-
604
- # Get a snapshot of currently active agent chats
605
- #
606
- # Returns a copy to avoid concurrent modification issues.
607
- #
608
- # @return [Hash{Symbol => Agent::Chat}] Copy of active agent chats
609
- def active_agent_chats
610
- @active_agent_chats.dup
611
- end
612
-
613
- # Stop all observer tasks immediately
614
- #
615
- # Interrupts in-flight observer LLM calls.
616
- # Called during swarm interruption instead of wait_for_completion.
617
- #
618
- # @return [void]
619
- def stop_observers
620
- @observer_manager&.stop
621
- end
622
-
623
- # Create snapshot of current conversation state
624
- #
625
- # Returns a Snapshot object containing:
626
- # - All agent conversations (@messages arrays)
627
- # - Agent context state (warnings, compression, TodoWrite tracking, skills)
628
- # - Delegation instance conversations
629
- # - Scratchpad contents (volatile shared storage)
630
- # - Read tracking state (which files each agent has read with digests)
631
- # - Memory read tracking state (which memory entries each agent has read with digests)
632
- #
633
- # Configuration (agent definitions, tools, prompts) stays in your YAML/DSL
634
- # and is NOT included in snapshots.
635
- #
636
- # @return [Snapshot] Snapshot object with convenient serialization methods
637
- #
638
- # @example Save snapshot to JSON file
639
- # snapshot = swarm.snapshot
640
- # snapshot.write_to_file("session.json")
641
- #
642
- # @example Convert to hash or JSON string
643
- # snapshot = swarm.snapshot
644
- # hash = snapshot.to_hash
645
- # json_string = snapshot.to_json
646
- def snapshot
647
- StateSnapshot.new(self).snapshot
648
- end
649
-
650
- # Restore conversation state from snapshot
651
- #
652
- # Accepts a Snapshot object, hash, or JSON string. Validates compatibility
653
- # between snapshot and current swarm configuration, restores agent conversations,
654
- # context state, scratchpad, and read tracking. Returns RestoreResult with
655
- # warnings about any agents that couldn't be restored due to configuration
656
- # mismatches.
657
- #
658
- # The swarm must be created with the SAME configuration (agent definitions,
659
- # tools, prompts) as when the snapshot was created. Only conversation state
660
- # is restored from the snapshot.
661
- #
662
- # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
663
- # @return [RestoreResult] Result with warnings about skipped agents
664
- #
665
- # @example Restore from Snapshot object
666
- # swarm = SwarmSDK.build { ... } # Same config as snapshot
667
- # snapshot = Snapshot.from_file("session.json")
668
- # result = swarm.restore(snapshot)
669
- # if result.success?
670
- # puts "All agents restored"
671
- # else
672
- # puts result.summary
673
- # result.warnings.each { |w| puts " - #{w[:message]}" }
674
- # end
675
- #
676
- # Restore swarm state from snapshot
677
- #
678
- # By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
679
- # Set preserve_system_prompts: true to use historical prompts from snapshot.
680
- #
681
- # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
682
- # @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
683
- # @return [RestoreResult] Result with warnings about partial restores
684
- def restore(snapshot, preserve_system_prompts: false)
685
- StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
686
- end
687
-
688
- # Override swarm IDs for composable swarms
689
- #
690
- # Used by SwarmLoader to set hierarchical IDs when loading sub-swarms.
691
- # This is called after the swarm is built to ensure proper parent/child relationships.
692
- #
693
- # @param swarm_id [String] New swarm ID
694
- # @param parent_swarm_id [String] New parent swarm ID
695
- # @return [void]
696
- def override_swarm_ids(swarm_id:, parent_swarm_id:)
697
- @swarm_id = swarm_id
698
- @parent_swarm_id = parent_swarm_id
699
- end
700
-
701
- # Set swarm registry for composable swarms
702
- #
703
- # Used by Builder to set the registry after swarm creation.
704
- # This must be called before agent initialization to enable swarm delegation.
705
- #
706
- # @param registry [SwarmRegistry] Configured swarm registry
707
- # @return [void]
708
- attr_writer :swarm_registry
709
-
710
- # Force initialization of all lazy delegation instances
711
- #
712
- # By default, delegation instances for isolated delegates are lazy-loaded
713
- # (created on first delegation call). This method forces immediate initialization
714
- # of all lazy delegates, which can be useful for:
715
- # - Testing: Verify delegation instance properties without mocking
716
- # - Preloading: Initialize all agents upfront for predictable timing
717
- #
718
- # This method is recursive - it will initialize nested lazy delegates until
719
- # all delegation chains are fully initialized.
720
- #
721
- # @return [void]
722
- #
723
- # @example Force-initialize all delegates for testing
724
- # swarm.initialize_agents
725
- # swarm.initialize_lazy_delegates!
726
- # assert swarm.delegation_instances.key?("backend@lead")
727
- def initialize_lazy_delegates!
728
- initialize_agents unless @agents_initialized
729
-
730
- # Keep initializing until no more lazy delegates are found
731
- # This handles nested delegation chains (A -> B -> C)
732
- loop do
733
- initialized_any = false
734
-
735
- # Find all delegation tools with lazy loaders in primary agents
736
- @agents.each_value do |chat|
737
- chat.tools.each_value do |tool|
738
- next unless tool.is_a?(Tools::Delegate)
739
- next unless tool.lazy? && !tool.initialized?
740
-
741
- tool.initialize_delegate!
742
- initialized_any = true
743
- end
744
- end
745
-
746
- # Also check delegation instances for their own lazy delegates (nested)
747
- @delegation_instances.values.each do |chat|
748
- # Skip if this is still a LazyDelegateChat (shouldn't happen after above loop)
749
- next if chat.is_a?(Swarm::LazyDelegateChat)
750
-
751
- chat.tools.each_value do |tool|
752
- next unless tool.is_a?(Tools::Delegate)
753
- next unless tool.lazy? && !tool.initialized?
754
-
755
- tool.initialize_delegate!
756
- initialized_any = true
757
- end
758
- end
759
-
760
- # Exit loop when no more lazy delegates were initialized
761
- break unless initialized_any
762
- end
763
- end
764
-
765
- # --- Internal API (for Executor use only) ---
766
- # Hook triggers for swarm lifecycle events are provided by HookTriggers module
767
-
768
- private
769
-
770
- # Apply swarm_start hooks to prompt
771
- #
772
- # @param prompt [String] Original prompt
773
- # @return [String] Modified prompt (possibly with hook context appended)
774
- def apply_swarm_start_hooks(prompt)
775
- swarm_start_result = trigger_swarm_start(prompt)
776
- if swarm_start_result&.replace?
777
- "#{prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
778
- else
779
- prompt
780
- end
781
- end
782
-
783
- # Build context info hash for an agent chat instance
784
- #
785
- # @param chat [Agent::Chat] Agent chat instance with TokenTracking
786
- # @return [Hash] Context usage information
787
- def build_agent_context_info(chat)
788
- return {} unless chat.respond_to?(:cumulative_input_tokens)
789
-
790
- {
791
- input_tokens: chat.cumulative_input_tokens,
792
- output_tokens: chat.cumulative_output_tokens,
793
- total_tokens: chat.cumulative_total_tokens,
794
- cached_tokens: chat.cumulative_cached_tokens,
795
- cache_creation_tokens: chat.cumulative_cache_creation_tokens,
796
- effective_input_tokens: chat.effective_input_tokens,
797
- context_limit: chat.context_limit,
798
- usage_percentage: chat.context_usage_percentage,
799
- tokens_remaining: chat.tokens_remaining,
800
- input_cost: chat.cumulative_input_cost,
801
- output_cost: chat.cumulative_output_cost,
802
- total_cost: chat.cumulative_total_cost,
803
- }
804
- end
805
-
806
- # Validate that observer agent exists
807
- #
808
- # @param agent_name [Symbol] Name of the observer agent
809
- # @raise [ConfigurationError] If agent not found
810
- # @return [void]
811
- def validate_observer_agent(agent_name)
812
- return if @agent_definitions.key?(agent_name)
813
-
814
- raise ConfigurationError,
815
- "Observer agent '#{agent_name}' not found. " \
816
- "Define the agent first with `agent :#{agent_name} do ... end`"
817
- end
818
-
819
- # Setup observer manager and subscriptions
820
- #
821
- # Creates Observer::Manager and registers event subscriptions.
822
- # Must be called AFTER setup_logging (which clears Fiber[:log_subscriptions]).
823
- #
824
- # @return [void]
825
- def setup_observer_execution
826
- @observer_manager = Observer::Manager.new(self)
827
- @observer_configs.each { |c| @observer_manager.add_config(c) }
828
- @observer_manager.setup
829
- end
830
-
831
- # Validate and normalize scratchpad mode for Swarm
832
- #
833
- # Regular Swarms support :enabled or :disabled.
834
- # Rejects :per_node since it only makes sense for Workflow with multiple nodes.
835
- #
836
- # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
837
- # @return [Symbol] :enabled or :disabled
838
- # @raise [ArgumentError] If :per_node used, or invalid value
839
- def validate_swarm_scratchpad_mode(value)
840
- # Convert strings from YAML to symbols
841
- value = value.to_sym if value.is_a?(String)
842
-
843
- case value
844
- when :enabled, :disabled
845
- value
846
- when :per_node
847
- raise ArgumentError,
848
- "scratchpad: :per_node is only valid for Workflow with nodes. " \
849
- "For regular Swarms, use :enabled or :disabled."
850
- else
851
- raise ArgumentError,
852
- "Invalid scratchpad mode for Swarm: #{value.inspect}. " \
853
- "Use :enabled or :disabled."
854
- end
855
- end
856
-
857
- # Generate a unique swarm ID from name
858
- #
859
- # Creates a swarm ID by sanitizing the name and appending a random suffix.
860
- # Used when swarm_id is not explicitly provided.
861
- #
862
- # @param name [String] Swarm name
863
- # @return [String] Generated swarm ID (e.g., "dev_team_a3f2b1c8")
864
- def generate_swarm_id(name)
865
- sanitized = name.to_s.gsub(/[^a-z0-9_-]/i, "_").downcase
866
- "#{sanitized}_#{SecureRandom.hex(4)}"
867
- end
868
-
869
- # Generate a unique execution ID
870
- #
871
- # Creates an execution ID that uniquely identifies a single swarm.execute() call.
872
- # Format: "exec_{swarm_id}_{random_hex}"
873
- #
874
- # @return [String] Generated execution ID (e.g., "exec_main_a3f2b1c8")
875
- def generate_execution_id
876
- "exec_#{@swarm_id}_#{SecureRandom.hex(8)}"
877
- end
878
-
879
- # Initialize all agents using AgentInitializer
880
- #
881
- # This is called automatically (lazy initialization) by execute() and agent().
882
- # Delegates to AgentInitializer which handles the complex 5-pass setup.
883
- #
884
- # @return [void]
885
- def initialize_agents
886
- return if @agents_initialized
887
-
888
- initializer = AgentInitializer.new(self)
889
-
890
- @agents = initializer.initialize_all
891
- @agent_contexts = initializer.agent_contexts
892
- @agents_initialized = true
893
-
894
- # NOTE: agent_start events are emitted in execute() when logging is set up
895
- # This ensures events are never lost, even if agents are initialized early (e.g., by restore())
896
- end
897
-
898
- # Normalize tools to internal format (kept for add_agent)
899
- #
900
- # Handles both Ruby API (simple symbols) and YAML API (already parsed configs)
901
- #
902
- # @param tools [Array] Tool specifications
903
- # @return [Array<Hash>] Normalized tool configs
904
- def normalize_tools(tools)
905
- Array(tools).map do |tool|
906
- case tool
907
- when Symbol, String
908
- # Simple tool from Ruby API
909
- { name: tool.to_sym, permissions: nil }
910
- when Hash
911
- # Already in config format from YAML (has :name and :permissions keys)
912
- if tool.key?(:name)
913
- tool
914
- else
915
- # Inline permissions format: { Write: { allowed_paths: [...] } }
916
- tool_name = tool.keys.first.to_sym
917
- { name: tool_name, permissions: tool[tool_name] }
918
- end
919
- else
920
- raise ConfigurationError, "Invalid tool specification: #{tool.inspect}"
921
- end
922
- end
923
- end
924
-
925
- # Delegation methods for testing (delegate to concerns)
926
- # These allow tests to verify behavior without depending on internal structure
927
-
928
- # Create a tool instance (delegates to ToolConfigurator)
929
- def create_tool_instance(tool_name, agent_name, directory)
930
- ToolConfigurator.new(self, @scratchpad_storage, @plugin_storages).create_tool_instance(tool_name, agent_name, directory)
931
- end
932
-
933
- # Wrap tool with permissions (delegates to ToolConfigurator)
934
- def wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
935
- ToolConfigurator.new(self, @scratchpad_storage, @plugin_storages).wrap_tool_with_permissions(tool_instance, permissions_config, agent_definition)
936
- end
937
-
938
- # Build MCP transport config (delegates to McpConfigurator)
939
- def build_mcp_transport_config(transport_type, config)
940
- McpConfigurator.new(self).build_transport_config(transport_type, config)
941
- end
942
-
943
- # Create delegation tool (delegates to AgentInitializer)
944
- def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
945
- AgentInitializer.new(self)
946
- .create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
947
- end
948
-
949
- # Extract loggable info from plugin config
950
- #
951
- # Attempts to extract useful information from plugin configuration
952
- # for logging purposes. Handles MemoryConfig, Hashes, and other objects.
953
- #
954
- # @param config [Object] Plugin configuration object
955
- # @return [Hash, nil] Extracted config info or nil
956
- def extract_plugin_config_info(config)
957
- return if config.nil?
958
-
959
- # Handle MemoryConfig object (has directory method)
960
- if config.respond_to?(:directory)
961
- return { directory: config.directory }
962
- end
963
-
964
- # Handle Hash
965
- if config.is_a?(Hash)
966
- return config.slice(:directory, "directory", :adapter, "adapter")
967
- end
968
-
969
- # Unknown config type
970
- nil
971
- end
972
- end
973
- end