swarm_memory 2.1.3 → 2.1.4

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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +2 -15
  5. data/lib/claude_swarm/mcp_generator.rb +1 -0
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/run.rb +2 -2
  11. data/lib/swarm_cli/config_loader.rb +11 -11
  12. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  13. data/lib/swarm_cli/interactive_repl.rb +11 -5
  14. data/lib/swarm_cli/ui/icons.rb +0 -23
  15. data/lib/swarm_cli/version.rb +1 -1
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  17. data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
  18. data/lib/swarm_memory/version.rb +1 -1
  19. data/lib/swarm_memory.rb +1 -1
  20. data/lib/swarm_sdk/agent/builder.rb +58 -0
  21. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  22. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  23. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  24. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  25. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  26. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  27. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  28. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  30. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  31. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  32. data/lib/swarm_sdk/agent/context.rb +2 -2
  33. data/lib/swarm_sdk/agent/definition.rb +66 -154
  34. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  35. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  36. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  37. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  38. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  39. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  40. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  41. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  42. data/lib/swarm_sdk/configuration.rb +65 -543
  43. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  44. data/lib/swarm_sdk/context_compactor.rb +6 -11
  45. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  46. data/lib/swarm_sdk/context_management/context.rb +328 -0
  47. data/lib/swarm_sdk/defaults.rb +196 -0
  48. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  49. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  50. data/lib/swarm_sdk/log_collector.rb +179 -29
  51. data/lib/swarm_sdk/log_stream.rb +29 -0
  52. data/lib/swarm_sdk/node_context.rb +1 -1
  53. data/lib/swarm_sdk/observer/builder.rb +81 -0
  54. data/lib/swarm_sdk/observer/config.rb +45 -0
  55. data/lib/swarm_sdk/observer/manager.rb +236 -0
  56. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  57. data/lib/swarm_sdk/plugin.rb +93 -3
  58. data/lib/swarm_sdk/snapshot.rb +6 -6
  59. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  60. data/lib/swarm_sdk/state_restorer.rb +136 -151
  61. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  62. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  63. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  64. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  65. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  66. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  67. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  68. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  69. data/lib/swarm_sdk/swarm.rb +137 -679
  70. data/lib/swarm_sdk/tools/bash.rb +11 -3
  71. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  72. data/lib/swarm_sdk/tools/edit.rb +8 -13
  73. data/lib/swarm_sdk/tools/glob.rb +9 -1
  74. data/lib/swarm_sdk/tools/grep.rb +7 -0
  75. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  76. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  77. data/lib/swarm_sdk/tools/read.rb +11 -13
  78. data/lib/swarm_sdk/tools/registry.rb +122 -10
  79. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  80. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  81. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  82. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  83. data/lib/swarm_sdk/tools/write.rb +8 -13
  84. data/lib/swarm_sdk/version.rb +1 -1
  85. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  86. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  87. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  88. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  89. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  90. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  91. data/lib/swarm_sdk.rb +33 -3
  92. metadata +37 -14
  93. data/lib/swarm_memory/chat_extension.rb +0 -34
  94. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
@@ -0,0 +1,409 @@
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 or load from markdown content
68
+ #
69
+ # Supports two forms:
70
+ # 1. Inline DSL: agent :name do ... end
71
+ # 2. Markdown content: agent :name, <<~MD ... MD
72
+ #
73
+ # @example Inline DSL
74
+ # agent :backend do
75
+ # model "gpt-5"
76
+ # system_prompt "You build APIs"
77
+ # tools :Read, :Write
78
+ # end
79
+ #
80
+ # @example Markdown content
81
+ # agent :backend, <<~MD
82
+ # ---
83
+ # description: "Backend developer"
84
+ # model: "gpt-4"
85
+ # ---
86
+ #
87
+ # You build APIs.
88
+ # MD
89
+ def agent(name, content = nil, &block)
90
+ # Case 1: agent :name, <<~MD do ... end (markdown + overrides)
91
+ if content.is_a?(String) && block_given? && markdown_content?(content)
92
+ load_agent_from_markdown_with_overrides(content, name, &block)
93
+ # Case 2: agent :name, <<~MD (markdown only)
94
+ elsif content.is_a?(String) && !block_given? && markdown_content?(content)
95
+ load_agent_from_markdown(content, name)
96
+ # Case 3: agent :name do ... end (inline DSL)
97
+ elsif block_given?
98
+ builder = Agent::Builder.new(name)
99
+ builder.instance_eval(&block)
100
+ @agents[name] = builder
101
+ else
102
+ raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
103
+ end
104
+ end
105
+
106
+ # Configure all agents with a block
107
+ #
108
+ # @example
109
+ # all_agents do
110
+ # tools :Read, :Write
111
+ #
112
+ # hook :pre_tool_use, matcher: "Write" do |ctx|
113
+ # # Validation for all agents
114
+ # end
115
+ # end
116
+ def all_agents(&block)
117
+ builder = Swarm::AllAgentsBuilder.new
118
+ builder.instance_eval(&block)
119
+ @all_agents_config = builder
120
+ end
121
+
122
+ # Build the actual Swarm or Workflow instance
123
+ #
124
+ # Subclasses must implement this method.
125
+ #
126
+ # @return [Swarm, Workflow] Configured instance
127
+ def build_swarm
128
+ raise NotImplementedError, "#{self.class} must implement #build_swarm"
129
+ end
130
+
131
+ protected
132
+
133
+ # Check if a string is markdown content (has frontmatter)
134
+ #
135
+ # @param str [String] String to check
136
+ # @return [Boolean] true if string contains markdown frontmatter
137
+ def markdown_content?(str)
138
+ str.start_with?("---") || str.include?("\n---\n")
139
+ end
140
+
141
+ # Load an agent from markdown content
142
+ #
143
+ # Returns a hash of the agent config (not a Definition yet) so that
144
+ # all_agents config can be applied later in the build process.
145
+ #
146
+ # @param content [String] Markdown content with frontmatter
147
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
148
+ # @return [void]
149
+ def load_agent_from_markdown(content, name_override = nil)
150
+ definition = MarkdownParser.parse(content, name_override)
151
+ @agents[definition.name] = { __file_config__: definition.to_h }
152
+ end
153
+
154
+ # Load an agent from markdown content with DSL overrides
155
+ #
156
+ # @param content [String] Markdown content with frontmatter
157
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
158
+ # @yield Block with DSL overrides
159
+ # @return [void]
160
+ def load_agent_from_markdown_with_overrides(content, name_override = nil, &block)
161
+ definition = MarkdownParser.parse(content, name_override)
162
+
163
+ builder = Agent::Builder.new(definition.name)
164
+ apply_definition_to_builder(builder, definition.to_h)
165
+ builder.instance_eval(&block)
166
+
167
+ @agents[definition.name] = builder
168
+ end
169
+
170
+ # Apply agent definition hash to a builder
171
+ #
172
+ # @param builder [Agent::Builder] Builder to configure
173
+ # @param config [Hash] Configuration hash from definition
174
+ # @return [void]
175
+ def apply_definition_to_builder(builder, config)
176
+ builder.description(config[:description]) if config[:description]
177
+ builder.model(config[:model]) if config[:model]
178
+ builder.provider(config[:provider]) if config[:provider]
179
+ builder.base_url(config[:base_url]) if config[:base_url]
180
+ builder.api_version(config[:api_version]) if config[:api_version]
181
+ builder.context_window(config[:context_window]) if config[:context_window]
182
+ builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
183
+ builder.directory(config[:directory]) if config[:directory]
184
+ builder.timeout(config[:timeout]) if config[:timeout]
185
+ builder.parameters(config[:parameters]) if config[:parameters]
186
+ builder.headers(config[:headers]) if config[:headers]
187
+ builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
188
+ builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
189
+ builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
190
+
191
+ # Add tools from markdown
192
+ if config[:tools]&.any?
193
+ tool_names = config[:tools].map do |tool|
194
+ tool.is_a?(Hash) ? tool[:name] : tool
195
+ end
196
+ builder.tools(*tool_names)
197
+ end
198
+
199
+ # Add delegates_to
200
+ builder.delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
201
+
202
+ # Add MCP servers
203
+ config[:mcp_servers]&.each do |server|
204
+ builder.mcp_server(server[:name], **server.except(:name))
205
+ end
206
+ end
207
+
208
+ # Merge all_agents configuration into each agent
209
+ #
210
+ # All_agents values are used as defaults - agent-specific values override.
211
+ # This applies to both inline DSL agents (Builder) and file-loaded agents (config hash).
212
+ #
213
+ # @return [void]
214
+ def merge_all_agents_config_into_agents
215
+ return unless @all_agents_config
216
+
217
+ all_agents_hash = @all_agents_config.to_h
218
+
219
+ @agents.each_value do |agent_builder_or_config|
220
+ if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
221
+ # File-loaded agent - merge into the config hash
222
+ file_config = agent_builder_or_config[:__file_config__]
223
+ merged_config = merge_all_agents_into_config(all_agents_hash, file_config)
224
+ agent_builder_or_config[:__file_config__] = merged_config
225
+ else
226
+ # Builder object (inline DSL agent)
227
+ agent_builder = agent_builder_or_config
228
+
229
+ apply_all_agents_defaults(agent_builder, all_agents_hash)
230
+
231
+ # Merge tools (prepend all_agents tools)
232
+ all_agents_tools = @all_agents_config.tools_list
233
+ agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
234
+
235
+ # Pass all_agents permissions as default_permissions
236
+ if @all_agents_config.permissions_config.any?
237
+ agent_builder.default_permissions = @all_agents_config.permissions_config
238
+ end
239
+ end
240
+ end
241
+ end
242
+
243
+ # Merge all_agents config into file-loaded agent config
244
+ #
245
+ # @param all_agents_hash [Hash] All_agents configuration
246
+ # @param file_config [Hash] File-loaded agent configuration
247
+ # @return [Hash] Merged configuration
248
+ def merge_all_agents_into_config(all_agents_hash, file_config)
249
+ merged = all_agents_hash.dup
250
+
251
+ file_config.each do |key, value|
252
+ case key
253
+ when :tools
254
+ merged[:tools] = Array(merged[:tools]) + Array(value)
255
+ when :delegates_to
256
+ merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
257
+ when :parameters
258
+ merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
259
+ when :headers
260
+ merged[:headers] = (merged[:headers] || {}).merge(value || {})
261
+ else
262
+ merged[key] = value
263
+ end
264
+ end
265
+
266
+ # Pass all_agents permissions as default_permissions
267
+ if all_agents_hash[:permissions] && !merged[:default_permissions]
268
+ merged[:default_permissions] = all_agents_hash[:permissions]
269
+ end
270
+
271
+ merged
272
+ end
273
+
274
+ # Apply all_agents defaults to an agent builder
275
+ #
276
+ # @param agent_builder [Agent::Builder] The agent builder to configure
277
+ # @param all_agents_hash [Hash] All_agents configuration
278
+ # @return [void]
279
+ def apply_all_agents_defaults(agent_builder, all_agents_hash)
280
+ if all_agents_hash[:model] && !agent_builder.model_set?
281
+ agent_builder.model(all_agents_hash[:model])
282
+ end
283
+
284
+ if all_agents_hash[:provider] && !agent_builder.provider_set?
285
+ agent_builder.provider(all_agents_hash[:provider])
286
+ end
287
+
288
+ if all_agents_hash[:base_url] && !agent_builder.base_url_set?
289
+ agent_builder.base_url(all_agents_hash[:base_url])
290
+ end
291
+
292
+ if all_agents_hash[:api_version] && !agent_builder.api_version_set?
293
+ agent_builder.api_version(all_agents_hash[:api_version])
294
+ end
295
+
296
+ if all_agents_hash[:timeout] && !agent_builder.timeout_set?
297
+ agent_builder.timeout(all_agents_hash[:timeout])
298
+ end
299
+
300
+ if all_agents_hash[:parameters]
301
+ merged_params = all_agents_hash[:parameters].merge(agent_builder.parameters)
302
+ agent_builder.parameters(merged_params)
303
+ end
304
+
305
+ if all_agents_hash[:headers]
306
+ merged_headers = all_agents_hash[:headers].merge(agent_builder.headers)
307
+ agent_builder.headers(merged_headers)
308
+ end
309
+
310
+ if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
311
+ agent_builder.coding_agent(all_agents_hash[:coding_agent])
312
+ end
313
+ end
314
+
315
+ # Validate all_agents filesystem tools
316
+ #
317
+ # @raise [ConfigurationError] If filesystem tools are disabled and all_agents has them
318
+ # @return [void]
319
+ def validate_all_agents_filesystem_tools
320
+ resolved_setting = if @allow_filesystem_tools.nil?
321
+ SwarmSDK.settings.allow_filesystem_tools
322
+ else
323
+ @allow_filesystem_tools
324
+ end
325
+
326
+ return if resolved_setting
327
+ return unless @all_agents_config&.tools_list&.any?
328
+
329
+ forbidden = @all_agents_config.tools_list.select do |tool|
330
+ SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool.to_sym)
331
+ end
332
+
333
+ return if forbidden.empty?
334
+
335
+ raise ConfigurationError,
336
+ "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
337
+ "but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
338
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
339
+ "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
340
+ end
341
+
342
+ # Validate individual agent filesystem tools
343
+ #
344
+ # @raise [ConfigurationError] If filesystem tools are disabled and any agent has them
345
+ # @return [void]
346
+ def validate_agent_filesystem_tools
347
+ resolved_setting = if @allow_filesystem_tools.nil?
348
+ SwarmSDK.settings.allow_filesystem_tools
349
+ else
350
+ @allow_filesystem_tools
351
+ end
352
+
353
+ return if resolved_setting
354
+
355
+ @agents.each do |agent_name, agent_builder_or_config|
356
+ tools_list = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
357
+ agent_builder_or_config[:__file_config__][:tools] || []
358
+ elsif agent_builder_or_config.is_a?(Agent::Builder)
359
+ agent_builder_or_config.tools_list
360
+ else
361
+ []
362
+ end
363
+
364
+ tool_names = tools_list.map do |tool|
365
+ name = tool.is_a?(Hash) ? tool[:name] : tool
366
+ name.to_sym
367
+ end
368
+
369
+ forbidden = tool_names.select do |tool|
370
+ SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool)
371
+ end
372
+
373
+ next if forbidden.empty?
374
+
375
+ raise ConfigurationError,
376
+ "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
377
+ "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
378
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
379
+ "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
380
+ end
381
+ end
382
+
383
+ # Build agent definitions from builders or file configs
384
+ #
385
+ # Handles both Agent::Builder (inline DSL) and file configs (from files).
386
+ # Merges all_agents config before building.
387
+ #
388
+ # @return [Hash<Symbol, Agent::Definition>] Agent definitions
389
+ def build_agent_definitions
390
+ # Merge all_agents config first
391
+ merge_all_agents_config_into_agents if @all_agents_config
392
+
393
+ # Build definitions
394
+ agent_definitions = {}
395
+ @agents.each do |agent_name, agent_builder_or_config|
396
+ agent_definitions[agent_name] = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
397
+ # File-loaded agent config (with all_agents merged)
398
+ Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
399
+ else
400
+ # Builder object (from inline DSL) - convert to definition
401
+ agent_builder_or_config.to_definition
402
+ end
403
+ end
404
+
405
+ agent_definitions
406
+ end
407
+ end
408
+ end
409
+ 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