claude_swarm 1.0.9 → 1.0.11

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
  3. data/CLAUDE.md +346 -191
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
  8. data/docs/v2/README.md +20 -5
  9. data/docs/v2/guides/complete-tutorial.md +95 -9
  10. data/docs/v2/guides/getting-started.md +10 -8
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/migrating-to-2.x.md +746 -0
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/snapshots.md +14 -14
  16. data/docs/v2/guides/swarm-memory.md +2 -13
  17. data/docs/v2/reference/architecture-flow.md +3 -3
  18. data/docs/v2/reference/cli.md +0 -1
  19. data/docs/v2/reference/configuration_reference.md +300 -0
  20. data/docs/v2/reference/event_payload_structures.md +27 -5
  21. data/docs/v2/reference/ruby-dsl.md +614 -18
  22. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  23. data/docs/v2/reference/yaml.md +172 -54
  24. data/examples/snapshot_demo.rb +2 -2
  25. data/lib/claude_swarm/mcp_generator.rb +8 -21
  26. data/lib/claude_swarm/orchestrator.rb +8 -1
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/swarm_cli/commands/run.rb +2 -2
  29. data/lib/swarm_cli/config_loader.rb +11 -11
  30. data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
  31. data/lib/swarm_cli/interactive_repl.rb +2 -2
  32. data/lib/swarm_cli/ui/icons.rb +0 -23
  33. data/lib/swarm_cli/version.rb +1 -1
  34. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  35. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  36. data/lib/swarm_memory/core/storage.rb +7 -2
  37. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  38. data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
  39. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  40. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  41. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  42. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  43. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  44. data/lib/swarm_memory/version.rb +1 -1
  45. data/lib/swarm_memory.rb +8 -6
  46. data/lib/swarm_sdk/agent/builder.rb +58 -0
  47. data/lib/swarm_sdk/agent/chat.rb +527 -1061
  48. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
  49. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  50. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
  51. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  52. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
  53. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
  54. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  55. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
  56. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  57. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
  58. data/lib/swarm_sdk/agent/context.rb +1 -2
  59. data/lib/swarm_sdk/agent/definition.rb +66 -154
  60. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  61. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  62. data/lib/swarm_sdk/agent_registry.rb +146 -0
  63. data/lib/swarm_sdk/builders/base_builder.rb +488 -0
  64. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  65. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  66. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  67. data/lib/swarm_sdk/config.rb +302 -0
  68. data/lib/swarm_sdk/configuration/parser.rb +373 -0
  69. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  70. data/lib/swarm_sdk/configuration.rb +77 -546
  71. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  72. data/lib/swarm_sdk/context_compactor.rb +6 -11
  73. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  74. data/lib/swarm_sdk/context_management/context.rb +328 -0
  75. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  76. data/lib/swarm_sdk/defaults.rb +196 -0
  77. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  78. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  79. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  80. data/lib/swarm_sdk/log_collector.rb +179 -29
  81. data/lib/swarm_sdk/log_stream.rb +29 -0
  82. data/lib/swarm_sdk/models.json +4333 -1
  83. data/lib/swarm_sdk/models.rb +43 -2
  84. data/lib/swarm_sdk/node_context.rb +1 -1
  85. data/lib/swarm_sdk/observer/builder.rb +81 -0
  86. data/lib/swarm_sdk/observer/config.rb +45 -0
  87. data/lib/swarm_sdk/observer/manager.rb +236 -0
  88. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  89. data/lib/swarm_sdk/plugin.rb +95 -5
  90. data/lib/swarm_sdk/result.rb +52 -0
  91. data/lib/swarm_sdk/snapshot.rb +6 -6
  92. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  93. data/lib/swarm_sdk/state_restorer.rb +136 -151
  94. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  95. data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
  96. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  97. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  98. data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
  99. data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
  100. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  101. data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
  102. data/lib/swarm_sdk/swarm.rb +203 -683
  103. data/lib/swarm_sdk/tools/bash.rb +14 -8
  104. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  105. data/lib/swarm_sdk/tools/edit.rb +8 -13
  106. data/lib/swarm_sdk/tools/glob.rb +12 -4
  107. data/lib/swarm_sdk/tools/grep.rb +7 -0
  108. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  109. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  110. data/lib/swarm_sdk/tools/read.rb +16 -18
  111. data/lib/swarm_sdk/tools/registry.rb +122 -10
  112. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  113. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  114. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  115. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  116. data/lib/swarm_sdk/tools/write.rb +8 -13
  117. data/lib/swarm_sdk/version.rb +1 -1
  118. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  119. data/lib/swarm_sdk/workflow/builder.rb +192 -0
  120. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  121. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  122. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  123. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  124. data/lib/swarm_sdk.rb +294 -108
  125. data/rubocop/cop/security/no_reflection_methods.rb +1 -1
  126. data/swarm_cli.gemspec +1 -1
  127. data/swarm_memory.gemspec +8 -3
  128. data/swarm_sdk.gemspec +6 -4
  129. data/team_full.yml +124 -320
  130. metadata +42 -14
  131. data/lib/swarm_memory/chat_extension.rb +0 -34
  132. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  133. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
  134. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -0,0 +1,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Builders
5
+ # Base builder with shared DSL methods for Swarm and Workflow builders
6
+ #
7
+ # Provides common functionality:
8
+ # - Basic configuration (name, id, scratchpad)
9
+ # - Agent definition (inline DSL, markdown files, with overrides)
10
+ # - All agents configuration
11
+ # - External swarms registry
12
+ # - Validation helpers
13
+ # - Merging logic
14
+ #
15
+ # Subclasses must implement:
16
+ # - build_swarm - Build and return the appropriate instance
17
+ # - Type-specific DSL methods (lead for Swarm, node/start_node for Workflow)
18
+ #
19
+ class BaseBuilder
20
+ def initialize(allow_filesystem_tools: nil)
21
+ @swarm_id = nil
22
+ @swarm_name = nil
23
+ @agents = {}
24
+ @all_agents_config = nil
25
+ @swarm_registry_config = []
26
+ @scratchpad = :disabled
27
+ @allow_filesystem_tools = allow_filesystem_tools
28
+ end
29
+
30
+ # Set swarm ID
31
+ #
32
+ # @param swarm_id [String] Unique identifier for this swarm/workflow
33
+ def id(swarm_id)
34
+ @swarm_id = swarm_id
35
+ end
36
+
37
+ # Set swarm/workflow name
38
+ def name(swarm_name)
39
+ @swarm_name = swarm_name
40
+ end
41
+
42
+ # Configure scratchpad mode
43
+ #
44
+ # For Workflow: :enabled (shared across nodes), :per_node (isolated), or :disabled
45
+ # For Swarm: :enabled or :disabled
46
+ #
47
+ # @param mode [Symbol, Boolean] Scratchpad mode
48
+ def scratchpad(mode)
49
+ @scratchpad = mode
50
+ end
51
+
52
+ # Register external swarms for composable swarms
53
+ #
54
+ # @example
55
+ # swarms do
56
+ # register "code_review", file: "./swarms/code_review.rb"
57
+ # register "testing", file: "./swarms/testing.yml", keep_context: false
58
+ # end
59
+ #
60
+ # @yield Block containing register() calls
61
+ def swarms(&block)
62
+ builder = Swarm::SwarmRegistryBuilder.new
63
+ builder.instance_eval(&block)
64
+ @swarm_registry_config = builder.registrations
65
+ end
66
+
67
+ # Define an agent with fluent API, load from markdown, or reference registry
68
+ #
69
+ # Supports multiple forms:
70
+ # 1. Registry lookup: agent :name (pulls from global registry)
71
+ # 2. Registry + overrides: agent :name do ... end (when registered)
72
+ # 3. Inline DSL: agent :name do ... end (when not registered)
73
+ # 4. Markdown content: agent :name, <<~MD ... MD
74
+ # 5. Markdown + overrides: agent :name, <<~MD do ... end
75
+ #
76
+ # @example Inline DSL
77
+ # agent :backend do
78
+ # model "gpt-5"
79
+ # system_prompt "You build APIs"
80
+ # tools :Read, :Write
81
+ # end
82
+ #
83
+ # @example Registry lookup (agent must be registered with SwarmSDK.agent)
84
+ # agent :backend # Pulls configuration from registry
85
+ #
86
+ # @example Registry + overrides
87
+ # agent :backend do
88
+ # # Base config from registry, then apply overrides
89
+ # tools :CustomTool # Adds to registry-defined tools
90
+ # end
91
+ #
92
+ # @example Markdown content
93
+ # agent :backend, <<~MD
94
+ # ---
95
+ # description: "Backend developer"
96
+ # model: "gpt-4"
97
+ # ---
98
+ #
99
+ # You build APIs.
100
+ # MD
101
+ def agent(name, content = nil, &block)
102
+ name = name.to_sym
103
+
104
+ # Case 1: agent :name, <<~MD do ... end (markdown + overrides)
105
+ if content.is_a?(String) && block_given? && markdown_content?(content)
106
+ load_agent_from_markdown_with_overrides(content, name, &block)
107
+
108
+ # Case 2: agent :name, <<~MD (markdown only)
109
+ elsif content.is_a?(String) && !block_given? && markdown_content?(content)
110
+ load_agent_from_markdown(content, name)
111
+
112
+ # Case 3: agent :name (registry lookup only - no content, no block)
113
+ elsif content.nil? && !block_given?
114
+ load_agent_from_registry(name)
115
+
116
+ # Case 4: agent :name do ... end (with registered agent - registry + overrides)
117
+ elsif content.nil? && block_given? && AgentRegistry.registered?(name)
118
+ load_agent_from_registry_with_overrides(name, &block)
119
+
120
+ # Case 5: agent :name do ... end (inline DSL - not registered)
121
+ elsif block_given?
122
+ builder = Agent::Builder.new(name)
123
+ builder.instance_eval(&block)
124
+ @agents[name] = builder
125
+
126
+ else
127
+ raise ArgumentError,
128
+ "Invalid agent definition for '#{name}'. Use:\n " \
129
+ "agent :#{name} { ... } # Inline DSL\n " \
130
+ "agent :#{name} # Registry lookup\n " \
131
+ "agent :#{name} { ... } # Registry + overrides (if registered)\n " \
132
+ "agent :#{name}, <<~MD ... MD # Markdown\n " \
133
+ "agent :#{name}, <<~MD do ... end # Markdown + overrides"
134
+ end
135
+ end
136
+
137
+ # Configure all agents with a block
138
+ #
139
+ # @example
140
+ # all_agents do
141
+ # tools :Read, :Write
142
+ #
143
+ # hook :pre_tool_use, matcher: "Write" do |ctx|
144
+ # # Validation for all agents
145
+ # end
146
+ # end
147
+ def all_agents(&block)
148
+ builder = Swarm::AllAgentsBuilder.new
149
+ builder.instance_eval(&block)
150
+ @all_agents_config = builder
151
+ end
152
+
153
+ # Build the actual Swarm or Workflow instance
154
+ #
155
+ # Subclasses must implement this method.
156
+ #
157
+ # @return [Swarm, Workflow] Configured instance
158
+ def build_swarm
159
+ raise NotImplementedError, "#{self.class} must implement #build_swarm"
160
+ end
161
+
162
+ protected
163
+
164
+ # Check if a string is markdown content (has frontmatter)
165
+ #
166
+ # @param str [String] String to check
167
+ # @return [Boolean] true if string contains markdown frontmatter
168
+ def markdown_content?(str)
169
+ str.start_with?("---") || str.include?("\n---\n")
170
+ end
171
+
172
+ # Load an agent from the global registry
173
+ #
174
+ # Retrieves the registered agent block and executes it in the context
175
+ # of a new Agent::Builder.
176
+ #
177
+ # @param name [Symbol] Agent name
178
+ # @return [void]
179
+ # @raise [ConfigurationError] If agent is not registered
180
+ #
181
+ # @example
182
+ # load_agent_from_registry(:backend)
183
+ def load_agent_from_registry(name)
184
+ registered_proc = AgentRegistry.get(name)
185
+ unless registered_proc
186
+ raise ConfigurationError,
187
+ "Agent '#{name}' not found in registry. " \
188
+ "Either define inline with `agent :#{name} do ... end` or " \
189
+ "register globally with `SwarmSDK.agent :#{name} do ... end`"
190
+ end
191
+
192
+ builder = Agent::Builder.new(name)
193
+ builder.instance_eval(&registered_proc)
194
+ @agents[name] = builder
195
+ end
196
+
197
+ # Load an agent from the registry with additional overrides
198
+ #
199
+ # Applies the registered configuration first, then executes the
200
+ # override block to customize the agent.
201
+ #
202
+ # @param name [Symbol] Agent name
203
+ # @yield Override block with additional configuration
204
+ # @return [void]
205
+ #
206
+ # @example
207
+ # load_agent_from_registry_with_overrides(:backend) do
208
+ # tools :CustomTool # Adds to registry-defined tools
209
+ # end
210
+ def load_agent_from_registry_with_overrides(name, &override_block)
211
+ registered_proc = AgentRegistry.get(name)
212
+ # Guaranteed to exist since we checked in the condition
213
+
214
+ builder = Agent::Builder.new(name)
215
+ builder.instance_eval(&registered_proc) # Base config from registry
216
+ builder.instance_eval(&override_block) # Apply overrides
217
+ @agents[name] = builder
218
+ end
219
+
220
+ # Load an agent from markdown content
221
+ #
222
+ # Returns a hash of the agent config (not a Definition yet) so that
223
+ # all_agents config can be applied later in the build process.
224
+ #
225
+ # @param content [String] Markdown content with frontmatter
226
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
227
+ # @return [void]
228
+ def load_agent_from_markdown(content, name_override = nil)
229
+ definition = MarkdownParser.parse(content, name_override)
230
+ @agents[definition.name] = { __file_config__: definition.to_h }
231
+ end
232
+
233
+ # Load an agent from markdown content with DSL overrides
234
+ #
235
+ # @param content [String] Markdown content with frontmatter
236
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
237
+ # @yield Block with DSL overrides
238
+ # @return [void]
239
+ def load_agent_from_markdown_with_overrides(content, name_override = nil, &block)
240
+ definition = MarkdownParser.parse(content, name_override)
241
+
242
+ builder = Agent::Builder.new(definition.name)
243
+ apply_definition_to_builder(builder, definition.to_h)
244
+ builder.instance_eval(&block)
245
+
246
+ @agents[definition.name] = builder
247
+ end
248
+
249
+ # Apply agent definition hash to a builder
250
+ #
251
+ # @param builder [Agent::Builder] Builder to configure
252
+ # @param config [Hash] Configuration hash from definition
253
+ # @return [void]
254
+ def apply_definition_to_builder(builder, config)
255
+ builder.description(config[:description]) if config[:description]
256
+ builder.model(config[:model]) if config[:model]
257
+ builder.provider(config[:provider]) if config[:provider]
258
+ builder.base_url(config[:base_url]) if config[:base_url]
259
+ builder.api_version(config[:api_version]) if config[:api_version]
260
+ builder.context_window(config[:context_window]) if config[:context_window]
261
+ builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
262
+ builder.directory(config[:directory]) if config[:directory]
263
+ builder.timeout(config[:timeout]) if config[:timeout]
264
+ builder.parameters(config[:parameters]) if config[:parameters]
265
+ builder.headers(config[:headers]) if config[:headers]
266
+ builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
267
+ builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
268
+ builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
269
+
270
+ # Add tools from markdown
271
+ if config[:tools]&.any?
272
+ tool_names = config[:tools].map do |tool|
273
+ tool.is_a?(Hash) ? tool[:name] : tool
274
+ end
275
+ builder.tools(*tool_names)
276
+ end
277
+
278
+ # Add delegates_to
279
+ builder.delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
280
+
281
+ # Add MCP servers
282
+ config[:mcp_servers]&.each do |server|
283
+ builder.mcp_server(server[:name], **server.except(:name))
284
+ end
285
+ end
286
+
287
+ # Merge all_agents configuration into each agent
288
+ #
289
+ # All_agents values are used as defaults - agent-specific values override.
290
+ # This applies to both inline DSL agents (Builder) and file-loaded agents (config hash).
291
+ #
292
+ # @return [void]
293
+ def merge_all_agents_config_into_agents
294
+ return unless @all_agents_config
295
+
296
+ all_agents_hash = @all_agents_config.to_h
297
+
298
+ @agents.each_value do |agent_builder_or_config|
299
+ if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
300
+ # File-loaded agent - merge into the config hash
301
+ file_config = agent_builder_or_config[:__file_config__]
302
+ merged_config = merge_all_agents_into_config(all_agents_hash, file_config)
303
+ agent_builder_or_config[:__file_config__] = merged_config
304
+ else
305
+ # Builder object (inline DSL agent)
306
+ agent_builder = agent_builder_or_config
307
+
308
+ apply_all_agents_defaults(agent_builder, all_agents_hash)
309
+
310
+ # Merge tools (prepend all_agents tools)
311
+ all_agents_tools = @all_agents_config.tools_list
312
+ agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
313
+
314
+ # Pass all_agents permissions as default_permissions
315
+ if @all_agents_config.permissions_config.any?
316
+ agent_builder.default_permissions = @all_agents_config.permissions_config
317
+ end
318
+ end
319
+ end
320
+ end
321
+
322
+ # Merge all_agents config into file-loaded agent config
323
+ #
324
+ # @param all_agents_hash [Hash] All_agents configuration
325
+ # @param file_config [Hash] File-loaded agent configuration
326
+ # @return [Hash] Merged configuration
327
+ def merge_all_agents_into_config(all_agents_hash, file_config)
328
+ merged = all_agents_hash.dup
329
+
330
+ file_config.each do |key, value|
331
+ case key
332
+ when :tools
333
+ merged[:tools] = Array(merged[:tools]) + Array(value)
334
+ when :delegates_to
335
+ merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
336
+ when :parameters
337
+ merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
338
+ when :headers
339
+ merged[:headers] = (merged[:headers] || {}).merge(value || {})
340
+ else
341
+ merged[key] = value
342
+ end
343
+ end
344
+
345
+ # Pass all_agents permissions as default_permissions
346
+ if all_agents_hash[:permissions] && !merged[:default_permissions]
347
+ merged[:default_permissions] = all_agents_hash[:permissions]
348
+ end
349
+
350
+ merged
351
+ end
352
+
353
+ # Apply all_agents defaults to an agent builder
354
+ #
355
+ # @param agent_builder [Agent::Builder] The agent builder to configure
356
+ # @param all_agents_hash [Hash] All_agents configuration
357
+ # @return [void]
358
+ def apply_all_agents_defaults(agent_builder, all_agents_hash)
359
+ if all_agents_hash[:model] && !agent_builder.model_set?
360
+ agent_builder.model(all_agents_hash[:model])
361
+ end
362
+
363
+ if all_agents_hash[:provider] && !agent_builder.provider_set?
364
+ agent_builder.provider(all_agents_hash[:provider])
365
+ end
366
+
367
+ if all_agents_hash[:base_url] && !agent_builder.base_url_set?
368
+ agent_builder.base_url(all_agents_hash[:base_url])
369
+ end
370
+
371
+ if all_agents_hash[:api_version] && !agent_builder.api_version_set?
372
+ agent_builder.api_version(all_agents_hash[:api_version])
373
+ end
374
+
375
+ if all_agents_hash[:timeout] && !agent_builder.timeout_set?
376
+ agent_builder.timeout(all_agents_hash[:timeout])
377
+ end
378
+
379
+ if all_agents_hash[:parameters]
380
+ merged_params = all_agents_hash[:parameters].merge(agent_builder.parameters)
381
+ agent_builder.parameters(merged_params)
382
+ end
383
+
384
+ if all_agents_hash[:headers]
385
+ merged_headers = all_agents_hash[:headers].merge(agent_builder.headers)
386
+ agent_builder.headers(merged_headers)
387
+ end
388
+
389
+ if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
390
+ agent_builder.coding_agent(all_agents_hash[:coding_agent])
391
+ end
392
+ end
393
+
394
+ # Validate all_agents filesystem tools
395
+ #
396
+ # @raise [ConfigurationError] If filesystem tools are disabled and all_agents has them
397
+ # @return [void]
398
+ def validate_all_agents_filesystem_tools
399
+ resolved_setting = if @allow_filesystem_tools.nil?
400
+ SwarmSDK.config.allow_filesystem_tools
401
+ else
402
+ @allow_filesystem_tools
403
+ end
404
+
405
+ return if resolved_setting
406
+ return unless @all_agents_config&.tools_list&.any?
407
+
408
+ forbidden = @all_agents_config.tools_list.select do |tool|
409
+ SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool.to_sym)
410
+ end
411
+
412
+ return if forbidden.empty?
413
+
414
+ raise ConfigurationError,
415
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
416
+ "but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
417
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
418
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
419
+ end
420
+
421
+ # Validate individual agent filesystem tools
422
+ #
423
+ # @raise [ConfigurationError] If filesystem tools are disabled and any agent has them
424
+ # @return [void]
425
+ def validate_agent_filesystem_tools
426
+ resolved_setting = if @allow_filesystem_tools.nil?
427
+ SwarmSDK.config.allow_filesystem_tools
428
+ else
429
+ @allow_filesystem_tools
430
+ end
431
+
432
+ return if resolved_setting
433
+
434
+ @agents.each do |agent_name, agent_builder_or_config|
435
+ tools_list = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
436
+ agent_builder_or_config[:__file_config__][:tools] || []
437
+ elsif agent_builder_or_config.is_a?(Agent::Builder)
438
+ agent_builder_or_config.tools_list
439
+ else
440
+ []
441
+ end
442
+
443
+ tool_names = tools_list.map do |tool|
444
+ name = tool.is_a?(Hash) ? tool[:name] : tool
445
+ name.to_sym
446
+ end
447
+
448
+ forbidden = tool_names.select do |tool|
449
+ SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool)
450
+ end
451
+
452
+ next if forbidden.empty?
453
+
454
+ raise ConfigurationError,
455
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
456
+ "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
457
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
458
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
459
+ end
460
+ end
461
+
462
+ # Build agent definitions from builders or file configs
463
+ #
464
+ # Handles both Agent::Builder (inline DSL) and file configs (from files).
465
+ # Merges all_agents config before building.
466
+ #
467
+ # @return [Hash<Symbol, Agent::Definition>] Agent definitions
468
+ def build_agent_definitions
469
+ # Merge all_agents config first
470
+ merge_all_agents_config_into_agents if @all_agents_config
471
+
472
+ # Build definitions
473
+ agent_definitions = {}
474
+ @agents.each do |agent_name, agent_builder_or_config|
475
+ agent_definitions[agent_name] = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
476
+ # File-loaded agent config (with all_agents merged)
477
+ Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
478
+ else
479
+ # Builder object (from inline DSL) - convert to definition
480
+ agent_builder_or_config.to_definition
481
+ end
482
+ end
483
+
484
+ agent_definitions
485
+ end
486
+ end
487
+ end
488
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Concerns
5
+ # Shared cleanup functionality for Swarm and Workflow
6
+ #
7
+ # Both classes must have:
8
+ # - mcp_clients: Hash of MCP client arrays
9
+ # - delegation_instances_hash: Hash of delegation instances (via Snapshotable)
10
+ #
11
+ module Cleanupable
12
+ # Cleanup all MCP clients
13
+ #
14
+ # Stops all MCP client connections gracefully.
15
+ # Should be called when the swarm/workflow is no longer needed.
16
+ #
17
+ # @return [void]
18
+ def cleanup
19
+ # Check if there's anything to clean up
20
+ return if @mcp_clients.empty? && (!delegation_instances_hash || delegation_instances_hash.empty?)
21
+
22
+ # Stop MCP clients for all agents
23
+ @mcp_clients.each do |agent_name, clients|
24
+ clients.each do |client|
25
+ client.stop
26
+ RubyLLM.logger.debug("SwarmSDK: Stopped MCP client '#{client.name}' for agent #{agent_name}")
27
+ rescue StandardError => e
28
+ RubyLLM.logger.debug("SwarmSDK: Error stopping MCP client '#{client.name}': #{e.message}")
29
+ end
30
+ end
31
+
32
+ @mcp_clients.clear
33
+
34
+ # Clear delegation instances
35
+ delegation_instances_hash&.clear
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Concerns
5
+ # Shared snapshot and restore functionality for Swarm and Workflow
6
+ #
7
+ # Both classes must implement:
8
+ # - primary_agents: Returns hash of primary agent instances
9
+ # - delegation_instances_hash: Returns hash of delegation instances
10
+ # - agent_definitions: Returns hash of agent definitions
11
+ # - swarm_id: Returns the swarm/workflow ID
12
+ # - parent_swarm_id: Returns the parent ID (or nil)
13
+ # - name: Returns the swarm/workflow name
14
+ #
15
+ module Snapshotable
16
+ # Create snapshot of current conversation state
17
+ #
18
+ # Returns a Snapshot object containing:
19
+ # - All agent conversations (@messages arrays)
20
+ # - Agent context state (warnings, compression, TodoWrite tracking, skills)
21
+ # - Delegation instance conversations
22
+ # - Scratchpad contents (volatile shared storage)
23
+ # - Read tracking state (which files each agent has read with digests)
24
+ # - Memory read tracking state (which memory entries each agent has read with digests)
25
+ #
26
+ # @return [Snapshot] Snapshot object with convenient serialization methods
27
+ def snapshot
28
+ StateSnapshot.new(self).snapshot
29
+ end
30
+
31
+ # Restore conversation state from snapshot
32
+ #
33
+ # Accepts a Snapshot object, hash, or JSON string. Validates compatibility
34
+ # between snapshot and current configuration, restores agent conversations,
35
+ # context state, scratchpad, and read tracking.
36
+ #
37
+ # The swarm/workflow must be created with the SAME configuration (agent definitions,
38
+ # tools, prompts) as when the snapshot was created. Only conversation state
39
+ # is restored from the snapshot.
40
+ #
41
+ # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
42
+ # @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
43
+ # @return [RestoreResult] Result with warnings about skipped agents
44
+ def restore(snapshot, preserve_system_prompts: false)
45
+ StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
46
+ end
47
+
48
+ # Interface method: Get primary agent instances
49
+ #
50
+ # Must be implemented by including class.
51
+ #
52
+ # @return [Hash<Symbol, Agent::Chat>] Primary agent instances
53
+ def primary_agents
54
+ raise NotImplementedError, "#{self.class} must implement #primary_agents"
55
+ end
56
+
57
+ # Interface method: Get delegation instance hash
58
+ #
59
+ # Must be implemented by including class.
60
+ #
61
+ # @return [Hash<String, Agent::Chat>] Delegation instances with keys like "delegate@delegator"
62
+ def delegation_instances_hash
63
+ raise NotImplementedError, "#{self.class} must implement #delegation_instances_hash"
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Concerns
5
+ # Shared validation functionality for Swarm and Workflow
6
+ #
7
+ # Both classes must have:
8
+ # - agent_definitions: Hash of Agent::Definition objects
9
+ # - swarm_id: Swarm/workflow identifier
10
+ # - parent_swarm_id: Parent identifier (or nil)
11
+ #
12
+ module Validatable
13
+ # Validate swarm/workflow configuration and return warnings
14
+ #
15
+ # This performs lightweight validation checks without creating agents.
16
+ # Useful for displaying configuration warnings before execution.
17
+ #
18
+ # @return [Array<Hash>] Array of warning hashes from all agent definitions
19
+ def validate
20
+ @agent_definitions.flat_map { |_name, definition| definition.validate }
21
+ end
22
+
23
+ # Emit validation warnings as log events
24
+ #
25
+ # This validates all agent definitions and emits any warnings as
26
+ # model_lookup_warning events through LogStream. Useful for emitting
27
+ # warnings before execution starts (e.g., in REPL after welcome screen).
28
+ #
29
+ # Requires LogStream.emitter to be set.
30
+ #
31
+ # @return [Array<Hash>] The validation warnings that were emitted
32
+ def emit_validation_warnings
33
+ warnings = validate
34
+
35
+ warnings.each do |warning|
36
+ case warning[:type]
37
+ when :model_not_found
38
+ LogStream.emit(
39
+ type: "model_lookup_warning",
40
+ agent: warning[:agent],
41
+ swarm_id: @swarm_id,
42
+ parent_swarm_id: @parent_swarm_id,
43
+ model: warning[:model],
44
+ error_message: warning[:error_message],
45
+ suggestions: warning[:suggestions],
46
+ timestamp: Time.now.utc.iso8601,
47
+ )
48
+ end
49
+ end
50
+
51
+ warnings
52
+ end
53
+ end
54
+ end
55
+ end