swarm_memory 2.1.2 → 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 (118) 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 +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  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/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ # Builds system prompts for agents
6
+ #
7
+ # This class encapsulates all system prompt construction logic, including:
8
+ # - Base system prompt rendering (for coding agents)
9
+ # - Non-coding base prompt rendering
10
+ # - Plugin prompt contribution collection
11
+ # - Combining base and custom prompts
12
+ #
13
+ # ## Safety Note for SwarmMemory Integration
14
+ #
15
+ # This is an INTERNAL helper that receives Definition attributes as input.
16
+ # Definition remains the single source of truth with all instance variables.
17
+ # SwarmMemory uses `agent_definition.instance_eval { binding }` for ERB templating,
18
+ # which requires all properties to be on Definition object. This helper is safe
19
+ # because it doesn't affect Definition's structure - it only extracts logic.
20
+ class SystemPromptBuilder
21
+ BASE_SYSTEM_PROMPT_PATH = File.expand_path("../prompts/base_system_prompt.md.erb", __dir__)
22
+
23
+ class << self
24
+ # Build the complete system prompt for an agent
25
+ #
26
+ # @param custom_prompt [String, nil] Custom system prompt from configuration
27
+ # @param coding_agent [Boolean] Whether agent is configured for coding tasks
28
+ # @param disable_default_tools [Boolean, Array, nil] Default tools disable configuration
29
+ # @param directory [String] Agent's working directory
30
+ # @param definition [Definition] Full definition for plugin contributions
31
+ # @return [String] Complete system prompt
32
+ def build(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:)
33
+ new(
34
+ custom_prompt: custom_prompt,
35
+ coding_agent: coding_agent,
36
+ disable_default_tools: disable_default_tools,
37
+ directory: directory,
38
+ definition: definition,
39
+ ).build
40
+ end
41
+ end
42
+
43
+ def initialize(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:)
44
+ @custom_prompt = custom_prompt
45
+ @coding_agent = coding_agent
46
+ @disable_default_tools = disable_default_tools
47
+ @directory = directory
48
+ @definition = definition
49
+ end
50
+
51
+ def build
52
+ prompt = base_prompt_section
53
+ prompt = append_plugin_contributions(prompt)
54
+ prompt
55
+ end
56
+
57
+ private
58
+
59
+ def base_prompt_section
60
+ if @coding_agent
61
+ build_coding_agent_prompt
62
+ elsif default_tools_enabled?
63
+ build_non_coding_agent_prompt
64
+ else
65
+ (@custom_prompt || "").to_s
66
+ end
67
+ end
68
+
69
+ def build_coding_agent_prompt
70
+ rendered_base = render_base_system_prompt
71
+
72
+ if @custom_prompt && !@custom_prompt.strip.empty?
73
+ "#{rendered_base}\n\n#{@custom_prompt}"
74
+ else
75
+ rendered_base
76
+ end
77
+ end
78
+
79
+ def build_non_coding_agent_prompt
80
+ non_coding_base = render_non_coding_base_prompt
81
+
82
+ if @custom_prompt && !@custom_prompt.strip.empty?
83
+ "#{non_coding_base}\n\n#{@custom_prompt}"
84
+ else
85
+ non_coding_base
86
+ end
87
+ end
88
+
89
+ def default_tools_enabled?
90
+ @disable_default_tools != true
91
+ end
92
+
93
+ def render_base_system_prompt
94
+ cwd = @directory || Dir.pwd
95
+ platform = RUBY_PLATFORM
96
+ os_version = begin
97
+ %x(uname -sr 2>/dev/null).strip
98
+ rescue
99
+ RUBY_PLATFORM
100
+ end
101
+ date = Time.now.strftime("%Y-%m-%d")
102
+
103
+ template_content = File.read(BASE_SYSTEM_PROMPT_PATH)
104
+ ERB.new(template_content).result(binding)
105
+ end
106
+
107
+ def render_non_coding_base_prompt
108
+ cwd = @directory || Dir.pwd
109
+ platform = RUBY_PLATFORM
110
+ os_version = begin
111
+ %x(uname -sr 2>/dev/null).strip
112
+ rescue
113
+ RUBY_PLATFORM
114
+ end
115
+ date = Time.now.strftime("%Y-%m-%d")
116
+
117
+ <<~PROMPT.strip
118
+ # Today's date
119
+
120
+ <today-date>
121
+ #{date}
122
+ #</today-date>
123
+
124
+ # Current Environment
125
+
126
+ <env>
127
+ Working directory: #{cwd}
128
+ Platform: #{platform}
129
+ OS Version: #{os_version}
130
+ </env>
131
+ PROMPT
132
+ end
133
+
134
+ def append_plugin_contributions(prompt)
135
+ contributions = collect_plugin_prompt_contributions
136
+ return prompt if contributions.empty?
137
+
138
+ combined_contributions = contributions.join("\n\n")
139
+
140
+ if prompt && !prompt.strip.empty?
141
+ "#{prompt}\n\n#{combined_contributions}"
142
+ else
143
+ combined_contributions
144
+ end
145
+ end
146
+
147
+ def collect_plugin_prompt_contributions
148
+ contributions = []
149
+
150
+ PluginRegistry.all.each do |plugin|
151
+ next unless plugin.storage_enabled?(@definition)
152
+
153
+ contribution = plugin.system_prompt_contribution(agent_definition: @definition, storage: nil)
154
+ contributions << contribution if contribution && !contribution.strip.empty?
155
+ end
156
+
157
+ contributions
158
+ end
159
+ end
160
+ end
161
+ end
@@ -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