swarm_sdk 2.1.3 → 2.2.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 +33 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
- data/lib/swarm_sdk/agent/chat.rb +198 -51
- data/lib/swarm_sdk/agent/context.rb +6 -2
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +14 -2
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +387 -94
- data/lib/swarm_sdk/events_to_messages.rb +181 -0
- data/lib/swarm_sdk/log_collector.rb +31 -5
- data/lib/swarm_sdk/log_stream.rb +37 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node/agent_config.rb +33 -8
- data/lib/swarm_sdk/node/builder.rb +39 -18
- data/lib/swarm_sdk/node_orchestrator.rb +293 -26
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
- data/lib/swarm_sdk/state_restorer.rb +491 -0
- data/lib/swarm_sdk/state_snapshot.rb +369 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +208 -12
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
- data/lib/swarm_sdk/swarm.rb +337 -42
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/delegate.rb +92 -7
- data/lib/swarm_sdk/tools/read.rb +17 -5
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +40 -8
- metadata +17 -6
- data/lib/swarm_sdk/mcp.rb +0 -16
|
@@ -4,7 +4,7 @@ module SwarmSDK
|
|
|
4
4
|
class Configuration
|
|
5
5
|
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
6
6
|
|
|
7
|
-
attr_reader :swarm_name, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled
|
|
7
|
+
attr_reader :swarm_name, :swarm_id, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled, :nodes, :start_node, :external_swarms
|
|
8
8
|
|
|
9
9
|
class << self
|
|
10
10
|
# Load configuration from YAML file
|
|
@@ -41,10 +41,14 @@ module SwarmSDK
|
|
|
41
41
|
|
|
42
42
|
@yaml_content = yaml_content
|
|
43
43
|
@base_dir = Pathname.new(base_dir).expand_path
|
|
44
|
-
@
|
|
44
|
+
@swarm_id = nil # Optional swarm ID from YAML
|
|
45
|
+
@agents = {} # Parsed agent configs (hashes, not Definitions)
|
|
45
46
|
@all_agents_config = {} # Settings applied to all agents
|
|
46
47
|
@swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
|
|
47
48
|
@all_agents_hooks = {} # Hooks applied to all agents
|
|
49
|
+
@external_swarms = {} # External swarms for composable swarms
|
|
50
|
+
@nodes = {} # Parsed node configs (hashes)
|
|
51
|
+
@start_node = nil # Starting node for workflows
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
def load_and_validate
|
|
@@ -61,6 +65,7 @@ module SwarmSDK
|
|
|
61
65
|
load_hooks_config
|
|
62
66
|
validate_swarm
|
|
63
67
|
load_agents
|
|
68
|
+
load_nodes
|
|
64
69
|
detect_circular_dependencies
|
|
65
70
|
self
|
|
66
71
|
rescue Psych::SyntaxError => e
|
|
@@ -72,36 +77,84 @@ module SwarmSDK
|
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
def connections_for(agent_name)
|
|
75
|
-
@agents[agent_name]
|
|
80
|
+
agent_config = @agents[agent_name]
|
|
81
|
+
return [] unless agent_config
|
|
82
|
+
|
|
83
|
+
# Extract delegates_to from hash and convert to symbols
|
|
84
|
+
delegates = agent_config[:delegates_to] || []
|
|
85
|
+
Array(delegates).map(&:to_sym)
|
|
76
86
|
end
|
|
77
87
|
|
|
78
|
-
# Convert configuration to Swarm
|
|
88
|
+
# Convert configuration to Swarm or NodeOrchestrator using DSL
|
|
79
89
|
#
|
|
80
|
-
# This method
|
|
81
|
-
#
|
|
90
|
+
# This method translates YAML configuration to Ruby DSL calls.
|
|
91
|
+
# The DSL (Swarm::Builder) handles all validation, merging, and construction.
|
|
82
92
|
#
|
|
83
|
-
# @
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
94
|
+
# @return [Swarm, NodeOrchestrator] Configured swarm or orchestrator
|
|
95
|
+
def to_swarm(allow_filesystem_tools: nil)
|
|
96
|
+
builder = Swarm::Builder.new(allow_filesystem_tools: allow_filesystem_tools)
|
|
97
|
+
|
|
98
|
+
# Translate basic swarm config to DSL
|
|
99
|
+
builder.id(@swarm_id) if @swarm_id
|
|
100
|
+
builder.name(@swarm_name)
|
|
101
|
+
builder.lead(@lead_agent)
|
|
102
|
+
builder.scratchpad(@scratchpad_mode)
|
|
103
|
+
|
|
104
|
+
# Translate external swarms
|
|
105
|
+
if @external_swarms&.any?
|
|
106
|
+
builder.swarms do
|
|
107
|
+
@external_swarms.each do |name, config|
|
|
108
|
+
source = config[:source]
|
|
109
|
+
case source[:type]
|
|
110
|
+
when :file
|
|
111
|
+
register(name, file: source[:value], keep_context: config[:keep_context])
|
|
112
|
+
when :yaml
|
|
113
|
+
register(name, yaml: source[:value], keep_context: config[:keep_context])
|
|
114
|
+
else
|
|
115
|
+
raise ConfigurationError, "Unknown source type: #{source[:type]}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
95
119
|
end
|
|
96
120
|
|
|
97
|
-
#
|
|
98
|
-
|
|
121
|
+
# Translate all_agents config to DSL (if present)
|
|
122
|
+
translate_all_agents(builder) if @all_agents_config.any?
|
|
123
|
+
|
|
124
|
+
# Translate agents to DSL
|
|
125
|
+
translate_agents(builder)
|
|
126
|
+
|
|
127
|
+
# Translate swarm-level hooks to DSL (if present)
|
|
128
|
+
translate_swarm_hooks(builder) if @swarm_hooks.any?
|
|
129
|
+
|
|
130
|
+
# Translate nodes to DSL (if present)
|
|
131
|
+
if @nodes.any?
|
|
132
|
+
translate_nodes(builder)
|
|
133
|
+
builder.start_node(@start_node)
|
|
134
|
+
end
|
|
99
135
|
|
|
100
|
-
swarm
|
|
136
|
+
# Build the swarm or orchestrator (DSL decides based on presence of nodes)
|
|
137
|
+
builder.build_swarm
|
|
101
138
|
end
|
|
102
139
|
|
|
103
140
|
private
|
|
104
141
|
|
|
142
|
+
def parse_scratchpad_mode(value)
|
|
143
|
+
return :disabled if value.nil? # Default
|
|
144
|
+
|
|
145
|
+
# Convert strings from YAML to symbols
|
|
146
|
+
value = value.to_sym if value.is_a?(String)
|
|
147
|
+
|
|
148
|
+
# Validate symbols
|
|
149
|
+
case value
|
|
150
|
+
when :enabled, :disabled, :per_node
|
|
151
|
+
value
|
|
152
|
+
else
|
|
153
|
+
raise ConfigurationError,
|
|
154
|
+
"Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
105
158
|
def interpolate_env_vars!(obj)
|
|
106
159
|
case obj
|
|
107
160
|
when String
|
|
@@ -170,8 +223,46 @@ module SwarmSDK
|
|
|
170
223
|
raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
|
|
171
224
|
|
|
172
225
|
@swarm_name = swarm[:name]
|
|
226
|
+
@swarm_id = swarm[:id] # Optional - will auto-generate if missing
|
|
173
227
|
@lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
|
|
174
|
-
@
|
|
228
|
+
@scratchpad_mode = parse_scratchpad_mode(swarm[:scratchpad])
|
|
229
|
+
|
|
230
|
+
# Load external swarms for composable swarms
|
|
231
|
+
load_external_swarms(swarm[:swarms]) if swarm[:swarms]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def load_external_swarms(swarms_config)
|
|
235
|
+
@external_swarms = {}
|
|
236
|
+
swarms_config.each do |name, config|
|
|
237
|
+
# Determine source type: file, yaml string, or inline swarm definition
|
|
238
|
+
source = if config[:file]
|
|
239
|
+
# File path - resolve relative to base_dir
|
|
240
|
+
file_path = if config[:file].start_with?("/")
|
|
241
|
+
config[:file]
|
|
242
|
+
else
|
|
243
|
+
(@base_dir / config[:file]).to_s
|
|
244
|
+
end
|
|
245
|
+
{ type: :file, value: file_path }
|
|
246
|
+
elsif config[:yaml]
|
|
247
|
+
# YAML string provided directly
|
|
248
|
+
{ type: :yaml, value: config[:yaml] }
|
|
249
|
+
elsif config[:swarm]
|
|
250
|
+
# Inline swarm definition - convert to YAML string
|
|
251
|
+
inline_config = {
|
|
252
|
+
version: 2,
|
|
253
|
+
swarm: config[:swarm],
|
|
254
|
+
}
|
|
255
|
+
yaml_string = Utils.hash_to_yaml(inline_config)
|
|
256
|
+
{ type: :yaml, value: yaml_string }
|
|
257
|
+
else
|
|
258
|
+
raise ConfigurationError, "Swarm '#{name}' must specify either 'file:', 'yaml:', or 'swarm:' (inline definition)"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
@external_swarms[name.to_sym] = {
|
|
262
|
+
source: source,
|
|
263
|
+
keep_context: config.fetch(:keep_context, true),
|
|
264
|
+
}
|
|
265
|
+
end
|
|
175
266
|
end
|
|
176
267
|
|
|
177
268
|
def load_agents
|
|
@@ -182,28 +273,30 @@ module SwarmSDK
|
|
|
182
273
|
# 1. String: assistant: "agents/assistant.md" (file path)
|
|
183
274
|
# 2. Hash with agent_file: assistant: { agent_file: "..." }
|
|
184
275
|
# 3. Hash with inline definition: assistant: { description: "...", model: "..." }
|
|
276
|
+
# 4. nil: Invalid (will be caught when building swarm)
|
|
185
277
|
|
|
186
|
-
if agent_config.
|
|
278
|
+
parsed_config = if agent_config.nil?
|
|
279
|
+
# Null config - store empty hash, will fail during swarm building
|
|
280
|
+
{}
|
|
281
|
+
elsif agent_config.is_a?(String)
|
|
187
282
|
# Format 1: Direct file path as string
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
283
|
+
{ agent_file: agent_config }
|
|
284
|
+
elsif agent_config.is_a?(Hash) && agent_config[:agent_file]
|
|
285
|
+
# Format 2: Hash with agent_file key
|
|
286
|
+
agent_config
|
|
191
287
|
else
|
|
192
|
-
# Format
|
|
193
|
-
agent_config
|
|
194
|
-
|
|
195
|
-
# Merge all_agents_config into agent config
|
|
196
|
-
# Agent-specific config overrides all_agents config
|
|
197
|
-
merged_config = merge_all_agents_config(agent_config)
|
|
288
|
+
# Format 3: Inline definition
|
|
289
|
+
agent_config || {}
|
|
290
|
+
end
|
|
198
291
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
#
|
|
204
|
-
Agent::Definition.new(name, merged_config)
|
|
205
|
-
end
|
|
292
|
+
# Validate required fields for inline definitions (strict validation for YAML)
|
|
293
|
+
# File-based agents are validated when loaded
|
|
294
|
+
if parsed_config[:agent_file].nil? && parsed_config[:description].nil?
|
|
295
|
+
raise ConfigurationError,
|
|
296
|
+
"Agent '#{name}' missing required 'description' field"
|
|
206
297
|
end
|
|
298
|
+
|
|
299
|
+
@agents[name] = parsed_config
|
|
207
300
|
end
|
|
208
301
|
|
|
209
302
|
unless @agents.key?(@lead_agent)
|
|
@@ -211,76 +304,272 @@ module SwarmSDK
|
|
|
211
304
|
end
|
|
212
305
|
end
|
|
213
306
|
|
|
214
|
-
|
|
215
|
-
|
|
307
|
+
def load_nodes
|
|
308
|
+
return unless @config[:swarm][:nodes]
|
|
309
|
+
|
|
310
|
+
@nodes = Utils.symbolize_keys(@config[:swarm][:nodes])
|
|
311
|
+
@start_node = @config[:swarm][:start_node]&.to_sym
|
|
312
|
+
|
|
313
|
+
# Validate start_node is required if nodes defined
|
|
314
|
+
if @nodes.any? && !@start_node
|
|
315
|
+
raise ConfigurationError, "start_node required when nodes are defined"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Validate start_node exists
|
|
319
|
+
if @start_node && !@nodes.key?(@start_node)
|
|
320
|
+
raise ConfigurationError, "start_node '#{@start_node}' not found in nodes"
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Basic node structure validation
|
|
324
|
+
@nodes.each do |node_name, node_config|
|
|
325
|
+
unless node_config.is_a?(Hash)
|
|
326
|
+
raise ConfigurationError, "Node '#{node_name}' must be a hash"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
# Validate agents if present (optional for agent-less nodes)
|
|
330
|
+
if node_config[:agents]
|
|
331
|
+
unless node_config[:agents].is_a?(Array)
|
|
332
|
+
raise ConfigurationError, "Node '#{node_name}' agents must be an array"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Validate each agent config
|
|
336
|
+
node_config[:agents].each do |agent_config|
|
|
337
|
+
unless agent_config.is_a?(Hash) && agent_config[:agent]
|
|
338
|
+
raise ConfigurationError,
|
|
339
|
+
"Node '#{node_name}' agents must be hashes with 'agent' key"
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Validate agent exists in swarm agents
|
|
343
|
+
agent_sym = agent_config[:agent].to_sym
|
|
344
|
+
unless @agents.key?(agent_sym)
|
|
345
|
+
raise ConfigurationError,
|
|
346
|
+
"Node '#{node_name}' references undefined agent '#{agent_config[:agent]}'"
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Validate dependencies if present
|
|
352
|
+
next unless node_config[:dependencies]
|
|
353
|
+
unless node_config[:dependencies].is_a?(Array)
|
|
354
|
+
raise ConfigurationError, "Node '#{node_name}' dependencies must be an array"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Validate each dependency exists
|
|
358
|
+
node_config[:dependencies].each do |dep|
|
|
359
|
+
dep_sym = dep.to_sym
|
|
360
|
+
unless @nodes.key?(dep_sym)
|
|
361
|
+
raise ConfigurationError,
|
|
362
|
+
"Node '#{node_name}' depends on undefined node '#{dep}'"
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Translate all_agents configuration to DSL
|
|
216
369
|
#
|
|
217
|
-
#
|
|
218
|
-
#
|
|
219
|
-
|
|
220
|
-
|
|
370
|
+
# @param builder [Swarm::Builder] DSL builder instance
|
|
371
|
+
# @return [void]
|
|
372
|
+
def translate_all_agents(builder)
|
|
373
|
+
# Capture instance variables for block scope
|
|
374
|
+
all_agents_cfg = @all_agents_config
|
|
375
|
+
all_agents_hks = @all_agents_hooks
|
|
376
|
+
|
|
377
|
+
builder.all_agents do
|
|
378
|
+
# Translate each all_agents field to DSL method calls
|
|
379
|
+
tools(*all_agents_cfg[:tools]) if all_agents_cfg[:tools]&.any?
|
|
380
|
+
model(all_agents_cfg[:model]) if all_agents_cfg[:model]
|
|
381
|
+
provider(all_agents_cfg[:provider]) if all_agents_cfg[:provider]
|
|
382
|
+
base_url(all_agents_cfg[:base_url]) if all_agents_cfg[:base_url]
|
|
383
|
+
api_version(all_agents_cfg[:api_version]) if all_agents_cfg[:api_version]
|
|
384
|
+
timeout(all_agents_cfg[:timeout]) if all_agents_cfg[:timeout]
|
|
385
|
+
parameters(all_agents_cfg[:parameters]) if all_agents_cfg[:parameters]
|
|
386
|
+
headers(all_agents_cfg[:headers]) if all_agents_cfg[:headers]
|
|
387
|
+
coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
|
|
388
|
+
disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
|
|
389
|
+
|
|
390
|
+
# Translate all_agents hooks
|
|
391
|
+
if all_agents_hks.any?
|
|
392
|
+
all_agents_hks.each do |event, hook_specs|
|
|
393
|
+
Array(hook_specs).each do |spec|
|
|
394
|
+
matcher = spec[:matcher]
|
|
395
|
+
hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Permissions - set directly as hash (YAML doesn't use DSL block syntax)
|
|
401
|
+
self.permissions_hash = all_agents_cfg[:permissions] if all_agents_cfg[:permissions]
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# Translate agents to DSL
|
|
221
406
|
#
|
|
222
|
-
# @param
|
|
223
|
-
# @return [
|
|
224
|
-
def
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
407
|
+
# @param builder [Swarm::Builder] DSL builder instance
|
|
408
|
+
# @return [void]
|
|
409
|
+
def translate_agents(builder)
|
|
410
|
+
@agents.each do |name, agent_config|
|
|
411
|
+
translate_agent(builder, name, agent_config)
|
|
412
|
+
rescue ConfigurationError => e
|
|
413
|
+
# Re-raise with agent context for better error messages
|
|
414
|
+
raise ConfigurationError, "Error in swarm.agents.#{name}: #{e.message}"
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Translate single agent to DSL
|
|
419
|
+
#
|
|
420
|
+
# @param builder [Swarm::Builder] DSL builder instance
|
|
421
|
+
# @param name [Symbol] Agent name
|
|
422
|
+
# @param config [Hash] Agent configuration
|
|
423
|
+
# @return [void]
|
|
424
|
+
def translate_agent(builder, name, config)
|
|
425
|
+
if config[:agent_file]
|
|
426
|
+
# Load from file
|
|
427
|
+
agent_file_path = resolve_agent_file_path(config[:agent_file])
|
|
428
|
+
|
|
429
|
+
unless File.exist?(agent_file_path)
|
|
430
|
+
raise ConfigurationError, "Agent file not found: #{agent_file_path}"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
content = File.read(agent_file_path)
|
|
434
|
+
|
|
435
|
+
# Check if there are overrides besides agent_file
|
|
436
|
+
overrides = config.except(:agent_file)
|
|
437
|
+
|
|
438
|
+
if overrides.any?
|
|
439
|
+
# Load from markdown with DSL overrides
|
|
440
|
+
builder.agent(name, content, &create_agent_config_block(overrides))
|
|
249
441
|
else
|
|
250
|
-
#
|
|
251
|
-
|
|
252
|
-
merged[key] = value
|
|
442
|
+
# Load from markdown only
|
|
443
|
+
builder.agent(name, content)
|
|
253
444
|
end
|
|
445
|
+
else
|
|
446
|
+
# Inline definition - translate to DSL
|
|
447
|
+
builder.agent(name, &create_agent_config_block(config))
|
|
254
448
|
end
|
|
449
|
+
rescue StandardError => e
|
|
450
|
+
raise ConfigurationError, "Error loading agent '#{name}': #{e.message}"
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Create a block that configures an agent builder with the given config
|
|
454
|
+
#
|
|
455
|
+
# Returns a proc that can be passed to builder.agent
|
|
456
|
+
#
|
|
457
|
+
# @param config [Hash] Agent configuration hash
|
|
458
|
+
# @return [Proc] Block that configures agent builder
|
|
459
|
+
def create_agent_config_block(config)
|
|
460
|
+
proc do
|
|
461
|
+
description(config[:description]) if config[:description]
|
|
462
|
+
model(config[:model]) if config[:model]
|
|
463
|
+
provider(config[:provider]) if config[:provider]
|
|
464
|
+
base_url(config[:base_url]) if config[:base_url]
|
|
465
|
+
api_version(config[:api_version]) if config[:api_version]
|
|
466
|
+
context_window(config[:context_window]) if config[:context_window]
|
|
467
|
+
system_prompt(config[:system_prompt]) if config[:system_prompt]
|
|
468
|
+
directory(config[:directory]) if config[:directory]
|
|
469
|
+
timeout(config[:timeout]) if config[:timeout]
|
|
470
|
+
parameters(config[:parameters]) if config[:parameters]
|
|
471
|
+
headers(config[:headers]) if config[:headers]
|
|
472
|
+
coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
|
|
473
|
+
bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
|
474
|
+
disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
475
|
+
shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
|
|
476
|
+
|
|
477
|
+
# Tools
|
|
478
|
+
if config[:tools]&.any?
|
|
479
|
+
tool_names = config[:tools].map { |t| t.is_a?(Hash) ? t[:name] : t }
|
|
480
|
+
tools(*tool_names)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Delegation
|
|
484
|
+
delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
|
|
485
|
+
|
|
486
|
+
# MCP servers
|
|
487
|
+
config[:mcp_servers]&.each do |server|
|
|
488
|
+
mcp_server(server[:name], **server.except(:name))
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# Hooks (YAML-style command hooks)
|
|
492
|
+
config[:hooks]&.each do |event, hook_specs|
|
|
493
|
+
Array(hook_specs).each do |spec|
|
|
494
|
+
matcher = spec[:matcher]
|
|
495
|
+
hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
# Memory
|
|
500
|
+
if config[:memory]
|
|
501
|
+
memory do
|
|
502
|
+
directory(config[:memory][:directory]) if config[:memory][:directory]
|
|
503
|
+
adapter(config[:memory][:adapter]) if config[:memory][:adapter]
|
|
504
|
+
mode(config[:memory][:mode]) if config[:memory][:mode]
|
|
505
|
+
end
|
|
506
|
+
end
|
|
255
507
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
merged[:default_permissions] = @all_agents_config[:permissions]
|
|
508
|
+
# Permissions - set directly as hash (YAML doesn't use DSL block syntax)
|
|
509
|
+
self.permissions_hash = config[:permissions] if config[:permissions]
|
|
259
510
|
end
|
|
511
|
+
end
|
|
260
512
|
|
|
261
|
-
|
|
513
|
+
# Translate swarm-level hooks to DSL
|
|
514
|
+
#
|
|
515
|
+
# @param builder [Swarm::Builder] Swarm builder instance
|
|
516
|
+
# @return [void]
|
|
517
|
+
def translate_swarm_hooks(builder)
|
|
518
|
+
@swarm_hooks.each do |event, hook_specs|
|
|
519
|
+
Array(hook_specs).each do |spec|
|
|
520
|
+
if spec[:type] == "command"
|
|
521
|
+
builder.hook(event, command: spec[:command], timeout: spec[:timeout])
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
end
|
|
262
525
|
end
|
|
263
526
|
|
|
264
|
-
|
|
265
|
-
|
|
527
|
+
# Translate nodes to DSL
|
|
528
|
+
#
|
|
529
|
+
# @param builder [Swarm::Builder] Swarm builder instance
|
|
530
|
+
# @return [void]
|
|
531
|
+
def translate_nodes(builder)
|
|
532
|
+
@nodes.each do |node_name, node_config|
|
|
533
|
+
builder.node(node_name) do
|
|
534
|
+
# Translate agents
|
|
535
|
+
node_config[:agents]&.each do |agent_config|
|
|
536
|
+
agent_name = agent_config[:agent].to_sym
|
|
537
|
+
delegates = agent_config[:delegates_to] || []
|
|
538
|
+
reset_ctx = agent_config.key?(:reset_context) ? agent_config[:reset_context] : true
|
|
539
|
+
tools_override = agent_config[:tools]
|
|
540
|
+
|
|
541
|
+
# Build agent config with fluent API
|
|
542
|
+
agent_cfg = agent(agent_name, reset_context: reset_ctx)
|
|
543
|
+
|
|
544
|
+
# Apply delegation if present
|
|
545
|
+
agent_cfg = agent_cfg.delegates_to(*delegates) if delegates.any?
|
|
546
|
+
|
|
547
|
+
# Apply tools override if present
|
|
548
|
+
agent_cfg.tools(*tools_override) if tools_override # Return config (finalize will be called automatically)
|
|
549
|
+
end
|
|
266
550
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
end
|
|
551
|
+
# Translate dependencies
|
|
552
|
+
depends_on(*node_config[:dependencies]) if node_config[:dependencies]&.any?
|
|
270
553
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
agent_def_from_file = MarkdownParser.parse(content, name)
|
|
554
|
+
# Translate lead override
|
|
555
|
+
lead(node_config[:lead].to_sym) if node_config[:lead]
|
|
274
556
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
557
|
+
# Translate transformers
|
|
558
|
+
if node_config[:input_command]
|
|
559
|
+
input_command(node_config[:input_command], timeout: node_config[:input_timeout] || 60)
|
|
560
|
+
end
|
|
278
561
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
562
|
+
if node_config[:output_command]
|
|
563
|
+
output_command(node_config[:output_command], timeout: node_config[:output_timeout] || 60)
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
end
|
|
282
567
|
end
|
|
283
568
|
|
|
569
|
+
# Resolve agent file path relative to base_dir
|
|
570
|
+
#
|
|
571
|
+
# @param file_path [String] Relative or absolute file path
|
|
572
|
+
# @return [String] Resolved absolute path
|
|
284
573
|
def resolve_agent_file_path(file_path)
|
|
285
574
|
return file_path if Pathname.new(file_path).absolute?
|
|
286
575
|
|
|
@@ -307,8 +596,12 @@ module SwarmSDK
|
|
|
307
596
|
path.push(agent_name)
|
|
308
597
|
connections_for(agent_name).each do |connection|
|
|
309
598
|
connection_sym = connection.to_sym # Convert to symbol for lookup
|
|
599
|
+
|
|
600
|
+
# Skip external swarms - they are not local agents and don't have circular dependency issues
|
|
601
|
+
next if @external_swarms.key?(connection_sym)
|
|
602
|
+
|
|
310
603
|
unless @agents.key?(connection_sym)
|
|
311
|
-
raise ConfigurationError, "Agent '#{agent_name}'
|
|
604
|
+
raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{connection}' (not a local agent or registered swarm)"
|
|
312
605
|
end
|
|
313
606
|
|
|
314
607
|
detect_cycle_from(connection_sym, visited, path)
|