swarm_sdk 2.2.0 → 2.3.0
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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1059
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +2 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +65 -543
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
- data/lib/swarm_sdk/swarm.rb +137 -679
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +11 -13
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +33 -3
- metadata +67 -15
- 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
|