swarm_sdk 2.1.2 → 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 +15 -22
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +420 -103
- 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/prompts/base_system_prompt.md.erb +0 -126
- 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 +367 -90
- 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/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- 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/tools/stores/storage.rb +4 -4
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +20 -8
- 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 +362 -21
- metadata +17 -5
|
@@ -4,25 +4,55 @@ module SwarmSDK
|
|
|
4
4
|
class Configuration
|
|
5
5
|
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
6
6
|
|
|
7
|
-
attr_reader :
|
|
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
|
-
|
|
11
|
-
|
|
10
|
+
# Load configuration from YAML file
|
|
11
|
+
#
|
|
12
|
+
# Convenience method that reads the file and uses the file's directory
|
|
13
|
+
# as the base directory for resolving agent file paths.
|
|
14
|
+
#
|
|
15
|
+
# @param path [String, Pathname] Path to YAML configuration file
|
|
16
|
+
# @return [Configuration] Validated configuration instance
|
|
17
|
+
# @raise [ConfigurationError] If file not found or invalid
|
|
18
|
+
def load_file(path)
|
|
19
|
+
path = Pathname.new(path).expand_path
|
|
20
|
+
|
|
21
|
+
unless path.exist?
|
|
22
|
+
raise ConfigurationError, "Configuration file not found: #{path}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
yaml_content = File.read(path)
|
|
26
|
+
base_dir = path.dirname
|
|
27
|
+
|
|
28
|
+
new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
|
|
29
|
+
rescue Errno::ENOENT
|
|
30
|
+
raise ConfigurationError, "Configuration file not found: #{path}"
|
|
12
31
|
end
|
|
13
32
|
end
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
34
|
+
# Initialize configuration from YAML string
|
|
35
|
+
#
|
|
36
|
+
# @param yaml_content [String] YAML configuration content
|
|
37
|
+
# @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
|
|
38
|
+
def initialize(yaml_content, base_dir: Dir.pwd)
|
|
39
|
+
raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
|
|
40
|
+
raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
|
|
41
|
+
|
|
42
|
+
@yaml_content = yaml_content
|
|
43
|
+
@base_dir = Pathname.new(base_dir).expand_path
|
|
44
|
+
@swarm_id = nil # Optional swarm ID from YAML
|
|
45
|
+
@agents = {} # Parsed agent configs (hashes, not Definitions)
|
|
19
46
|
@all_agents_config = {} # Settings applied to all agents
|
|
20
47
|
@swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
|
|
21
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
|
|
22
52
|
end
|
|
23
53
|
|
|
24
54
|
def load_and_validate
|
|
25
|
-
@config = YAML.
|
|
55
|
+
@config = YAML.safe_load(@yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
26
56
|
|
|
27
57
|
unless @config.is_a?(Hash)
|
|
28
58
|
raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
|
|
@@ -35,10 +65,9 @@ module SwarmSDK
|
|
|
35
65
|
load_hooks_config
|
|
36
66
|
validate_swarm
|
|
37
67
|
load_agents
|
|
68
|
+
load_nodes
|
|
38
69
|
detect_circular_dependencies
|
|
39
70
|
self
|
|
40
|
-
rescue Errno::ENOENT
|
|
41
|
-
raise ConfigurationError, "Configuration file not found: #{@config_path}"
|
|
42
71
|
rescue Psych::SyntaxError => e
|
|
43
72
|
raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
|
|
44
73
|
end
|
|
@@ -48,36 +77,84 @@ module SwarmSDK
|
|
|
48
77
|
end
|
|
49
78
|
|
|
50
79
|
def connections_for(agent_name)
|
|
51
|
-
@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)
|
|
52
86
|
end
|
|
53
87
|
|
|
54
|
-
# Convert configuration to Swarm
|
|
88
|
+
# Convert configuration to Swarm or NodeOrchestrator using DSL
|
|
55
89
|
#
|
|
56
|
-
# This method
|
|
57
|
-
#
|
|
90
|
+
# This method translates YAML configuration to Ruby DSL calls.
|
|
91
|
+
# The DSL (Swarm::Builder) handles all validation, merging, and construction.
|
|
58
92
|
#
|
|
59
|
-
# @
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
71
119
|
end
|
|
72
120
|
|
|
73
|
-
#
|
|
74
|
-
|
|
121
|
+
# Translate all_agents config to DSL (if present)
|
|
122
|
+
translate_all_agents(builder) if @all_agents_config.any?
|
|
75
123
|
|
|
76
|
-
|
|
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
|
|
135
|
+
|
|
136
|
+
# Build the swarm or orchestrator (DSL decides based on presence of nodes)
|
|
137
|
+
builder.build_swarm
|
|
77
138
|
end
|
|
78
139
|
|
|
79
140
|
private
|
|
80
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
|
+
|
|
81
158
|
def interpolate_env_vars!(obj)
|
|
82
159
|
case obj
|
|
83
160
|
when String
|
|
@@ -146,8 +223,46 @@ module SwarmSDK
|
|
|
146
223
|
raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
|
|
147
224
|
|
|
148
225
|
@swarm_name = swarm[:name]
|
|
226
|
+
@swarm_id = swarm[:id] # Optional - will auto-generate if missing
|
|
149
227
|
@lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
|
|
150
|
-
@
|
|
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
|
|
151
266
|
end
|
|
152
267
|
|
|
153
268
|
def load_agents
|
|
@@ -158,28 +273,30 @@ module SwarmSDK
|
|
|
158
273
|
# 1. String: assistant: "agents/assistant.md" (file path)
|
|
159
274
|
# 2. Hash with agent_file: assistant: { agent_file: "..." }
|
|
160
275
|
# 3. Hash with inline definition: assistant: { description: "...", model: "..." }
|
|
276
|
+
# 4. nil: Invalid (will be caught when building swarm)
|
|
161
277
|
|
|
162
|
-
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)
|
|
163
282
|
# Format 1: Direct file path as string
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
167
287
|
else
|
|
168
|
-
# Format
|
|
169
|
-
agent_config
|
|
170
|
-
|
|
171
|
-
# Merge all_agents_config into agent config
|
|
172
|
-
# Agent-specific config overrides all_agents config
|
|
173
|
-
merged_config = merge_all_agents_config(agent_config)
|
|
288
|
+
# Format 3: Inline definition
|
|
289
|
+
agent_config || {}
|
|
290
|
+
end
|
|
174
291
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
#
|
|
180
|
-
Agent::Definition.new(name, merged_config)
|
|
181
|
-
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"
|
|
182
297
|
end
|
|
298
|
+
|
|
299
|
+
@agents[name] = parsed_config
|
|
183
300
|
end
|
|
184
301
|
|
|
185
302
|
unless @agents.key?(@lead_agent)
|
|
@@ -187,80 +304,276 @@ module SwarmSDK
|
|
|
187
304
|
end
|
|
188
305
|
end
|
|
189
306
|
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
369
|
+
#
|
|
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
|
|
192
406
|
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
197
419
|
#
|
|
198
|
-
# @param
|
|
199
|
-
# @
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# Merge headers: all_agents.headers + agent.headers
|
|
220
|
-
# Agent values override all_agents values for same keys
|
|
221
|
-
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
|
222
|
-
when :disable_default_tools
|
|
223
|
-
# Convert array elements to symbols if it's an array
|
|
224
|
-
merged[key] = value.is_a?(Array) ? value.map(&:to_sym) : value
|
|
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))
|
|
225
441
|
else
|
|
226
|
-
#
|
|
227
|
-
|
|
228
|
-
merged[key] = value
|
|
442
|
+
# Load from markdown only
|
|
443
|
+
builder.agent(name, content)
|
|
229
444
|
end
|
|
445
|
+
else
|
|
446
|
+
# Inline definition - translate to DSL
|
|
447
|
+
builder.agent(name, &create_agent_config_block(config))
|
|
230
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?
|
|
231
485
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
|
507
|
+
|
|
508
|
+
# Permissions - set directly as hash (YAML doesn't use DSL block syntax)
|
|
509
|
+
self.permissions_hash = config[:permissions] if config[:permissions]
|
|
235
510
|
end
|
|
511
|
+
end
|
|
236
512
|
|
|
237
|
-
|
|
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
|
|
238
525
|
end
|
|
239
526
|
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
550
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
end
|
|
551
|
+
# Translate dependencies
|
|
552
|
+
depends_on(*node_config[:dependencies]) if node_config[:dependencies]&.any?
|
|
246
553
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
agent_def_from_file = MarkdownParser.parse(content, name)
|
|
554
|
+
# Translate lead override
|
|
555
|
+
lead(node_config[:lead].to_sym) if node_config[:lead]
|
|
250
556
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
557
|
+
# Translate transformers
|
|
558
|
+
if node_config[:input_command]
|
|
559
|
+
input_command(node_config[:input_command], timeout: node_config[:input_timeout] || 60)
|
|
560
|
+
end
|
|
254
561
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
258
567
|
end
|
|
259
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
|
|
260
573
|
def resolve_agent_file_path(file_path)
|
|
261
574
|
return file_path if Pathname.new(file_path).absolute?
|
|
262
575
|
|
|
263
|
-
@
|
|
576
|
+
@base_dir.join(file_path).to_s
|
|
264
577
|
end
|
|
265
578
|
|
|
266
579
|
def detect_circular_dependencies
|
|
@@ -283,8 +596,12 @@ module SwarmSDK
|
|
|
283
596
|
path.push(agent_name)
|
|
284
597
|
connections_for(agent_name).each do |connection|
|
|
285
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
|
+
|
|
286
603
|
unless @agents.key?(connection_sym)
|
|
287
|
-
raise ConfigurationError, "Agent '#{agent_name}'
|
|
604
|
+
raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{connection}' (not a local agent or registered swarm)"
|
|
288
605
|
end
|
|
289
606
|
|
|
290
607
|
detect_cycle_from(connection_sym, visited, path)
|