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