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,353 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Configuration
|
|
5
|
+
# Handles YAML parsing, validation, and normalization
|
|
6
|
+
#
|
|
7
|
+
# This class is responsible for:
|
|
8
|
+
# - Loading and parsing YAML content
|
|
9
|
+
# - Validating configuration structure
|
|
10
|
+
# - Normalizing data (symbolizing keys, env interpolation)
|
|
11
|
+
# - Detecting configuration type (swarm vs workflow)
|
|
12
|
+
# - Loading agents and nodes
|
|
13
|
+
# - Detecting circular dependencies
|
|
14
|
+
#
|
|
15
|
+
# After parsing, the parsed data can be translated to a Swarm/Workflow
|
|
16
|
+
# using the Translator class.
|
|
17
|
+
class Parser
|
|
18
|
+
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
19
|
+
|
|
20
|
+
attr_reader :config_type,
|
|
21
|
+
:swarm_name,
|
|
22
|
+
:swarm_id,
|
|
23
|
+
:lead_agent,
|
|
24
|
+
:start_node,
|
|
25
|
+
:agents,
|
|
26
|
+
:all_agents_config,
|
|
27
|
+
:swarm_hooks,
|
|
28
|
+
:all_agents_hooks,
|
|
29
|
+
:scratchpad_mode,
|
|
30
|
+
:nodes,
|
|
31
|
+
:external_swarms
|
|
32
|
+
|
|
33
|
+
def initialize(yaml_content, base_dir:)
|
|
34
|
+
@yaml_content = yaml_content
|
|
35
|
+
@base_dir = Pathname.new(base_dir).expand_path
|
|
36
|
+
@config_type = nil
|
|
37
|
+
@swarm_id = nil
|
|
38
|
+
@swarm_name = nil
|
|
39
|
+
@lead_agent = nil
|
|
40
|
+
@start_node = nil
|
|
41
|
+
@agents = {}
|
|
42
|
+
@all_agents_config = {}
|
|
43
|
+
@swarm_hooks = {}
|
|
44
|
+
@all_agents_hooks = {}
|
|
45
|
+
@external_swarms = {}
|
|
46
|
+
@nodes = {}
|
|
47
|
+
@scratchpad_mode = :disabled
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def parse
|
|
51
|
+
@config = YAML.safe_load(@yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
52
|
+
|
|
53
|
+
unless @config.is_a?(Hash)
|
|
54
|
+
raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@config = Utils.symbolize_keys(@config)
|
|
58
|
+
interpolate_env_vars!(@config)
|
|
59
|
+
|
|
60
|
+
validate_version
|
|
61
|
+
detect_and_validate_type
|
|
62
|
+
load_common_config
|
|
63
|
+
load_type_specific_config
|
|
64
|
+
load_agents
|
|
65
|
+
load_nodes if @config_type == :workflow
|
|
66
|
+
detect_circular_dependencies
|
|
67
|
+
|
|
68
|
+
self
|
|
69
|
+
rescue Psych::SyntaxError => e
|
|
70
|
+
raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def agent_names
|
|
74
|
+
@agents.keys
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def connections_for(agent_name)
|
|
78
|
+
agent_config = @agents[agent_name]
|
|
79
|
+
return [] unless agent_config
|
|
80
|
+
|
|
81
|
+
delegates = agent_config[:delegates_to] || []
|
|
82
|
+
Array(delegates).map(&:to_sym)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
attr_reader :base_dir
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def validate_version
|
|
90
|
+
version = @config[:version]
|
|
91
|
+
raise ConfigurationError, "Missing 'version' field in configuration" unless version
|
|
92
|
+
raise ConfigurationError, "SwarmSDK requires version: 2 configuration. Got version: #{version}" unless version == 2
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def detect_and_validate_type
|
|
96
|
+
has_swarm = @config.key?(:swarm)
|
|
97
|
+
has_workflow = @config.key?(:workflow)
|
|
98
|
+
|
|
99
|
+
if has_swarm && has_workflow
|
|
100
|
+
raise ConfigurationError, "Cannot have both 'swarm:' and 'workflow:' keys. Use one or the other."
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
unless has_swarm || has_workflow
|
|
104
|
+
raise ConfigurationError, "Missing 'swarm:' or 'workflow:' key in configuration"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
@config_type = has_swarm ? :swarm : :workflow
|
|
108
|
+
@root_config = @config[@config_type]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def load_common_config
|
|
112
|
+
raise ConfigurationError, "Missing 'name' field in #{@config_type} configuration" unless @root_config[:name]
|
|
113
|
+
|
|
114
|
+
@swarm_name = @root_config[:name]
|
|
115
|
+
@swarm_id = @root_config[:id]
|
|
116
|
+
@scratchpad_mode = parse_scratchpad_mode(@root_config[:scratchpad])
|
|
117
|
+
|
|
118
|
+
load_all_agents_config
|
|
119
|
+
load_hooks_config
|
|
120
|
+
load_external_swarms(@root_config[:swarms]) if @root_config[:swarms]
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def load_type_specific_config
|
|
124
|
+
if @config_type == :swarm
|
|
125
|
+
load_swarm_config
|
|
126
|
+
else
|
|
127
|
+
load_workflow_config
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def load_swarm_config
|
|
132
|
+
raise ConfigurationError, "Missing 'lead' field in swarm configuration" unless @root_config[:lead]
|
|
133
|
+
raise ConfigurationError, "Missing 'agents' field in swarm configuration" unless @root_config[:agents]
|
|
134
|
+
|
|
135
|
+
@lead_agent = @root_config[:lead].to_sym
|
|
136
|
+
|
|
137
|
+
if @root_config[:nodes] || @root_config[:start_node]
|
|
138
|
+
raise ConfigurationError, "Swarm configuration cannot have 'nodes' or 'start_node'. Use 'workflow:' key instead."
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def load_workflow_config
|
|
143
|
+
raise ConfigurationError, "Missing 'start_node' field in workflow configuration" unless @root_config[:start_node]
|
|
144
|
+
raise ConfigurationError, "Missing 'nodes' field in workflow configuration" unless @root_config[:nodes]
|
|
145
|
+
raise ConfigurationError, "Missing 'agents' field in workflow configuration" unless @root_config[:agents]
|
|
146
|
+
|
|
147
|
+
@start_node = @root_config[:start_node].to_sym
|
|
148
|
+
|
|
149
|
+
if @root_config[:lead]
|
|
150
|
+
raise ConfigurationError, "Workflow configuration cannot have 'lead'. Use 'start_node' instead."
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def load_all_agents_config
|
|
155
|
+
@all_agents_config = @root_config[:all_agents] || {}
|
|
156
|
+
|
|
157
|
+
if @all_agents_config[:disable_default_tools].is_a?(Array)
|
|
158
|
+
@all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def load_hooks_config
|
|
163
|
+
@swarm_hooks = Utils.symbolize_keys(@root_config[:hooks] || {})
|
|
164
|
+
|
|
165
|
+
if @root_config[:all_agents]
|
|
166
|
+
@all_agents_hooks = Utils.symbolize_keys(@root_config[:all_agents][:hooks] || {})
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def load_external_swarms(swarms_config)
|
|
171
|
+
@external_swarms = {}
|
|
172
|
+
swarms_config.each do |name, config|
|
|
173
|
+
source = if config[:file]
|
|
174
|
+
file_path = if config[:file].start_with?("/")
|
|
175
|
+
config[:file]
|
|
176
|
+
else
|
|
177
|
+
(@base_dir / config[:file]).to_s
|
|
178
|
+
end
|
|
179
|
+
{ type: :file, value: file_path }
|
|
180
|
+
elsif config[:yaml]
|
|
181
|
+
{ type: :yaml, value: config[:yaml] }
|
|
182
|
+
elsif config[:swarm]
|
|
183
|
+
inline_config = {
|
|
184
|
+
version: 2,
|
|
185
|
+
swarm: config[:swarm],
|
|
186
|
+
}
|
|
187
|
+
yaml_string = Utils.hash_to_yaml(inline_config)
|
|
188
|
+
{ type: :yaml, value: yaml_string }
|
|
189
|
+
else
|
|
190
|
+
raise ConfigurationError, "Swarm '#{name}' must specify either 'file:', 'yaml:', or 'swarm:' (inline definition)"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
@external_swarms[name.to_sym] = {
|
|
194
|
+
source: source,
|
|
195
|
+
keep_context: config.fetch(:keep_context, true),
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def load_agents
|
|
201
|
+
swarm_agents = @root_config[:agents]
|
|
202
|
+
raise ConfigurationError, "No agents defined" if swarm_agents.empty?
|
|
203
|
+
|
|
204
|
+
swarm_agents.each do |name, agent_config|
|
|
205
|
+
parsed_config = if agent_config.nil?
|
|
206
|
+
{}
|
|
207
|
+
elsif agent_config.is_a?(String)
|
|
208
|
+
{ agent_file: agent_config }
|
|
209
|
+
elsif agent_config.is_a?(Hash) && agent_config[:agent_file]
|
|
210
|
+
agent_config
|
|
211
|
+
else
|
|
212
|
+
agent_config || {}
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
if parsed_config[:agent_file].nil? && parsed_config[:description].nil?
|
|
216
|
+
raise ConfigurationError,
|
|
217
|
+
"Agent '#{name}' missing required 'description' field"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
@agents[name] = parsed_config
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
if @config_type == :swarm
|
|
224
|
+
unless @agents.key?(@lead_agent)
|
|
225
|
+
raise ConfigurationError, "Lead agent '#{@lead_agent}' not found in agents"
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def load_nodes
|
|
231
|
+
@nodes = Utils.symbolize_keys(@root_config[:nodes])
|
|
232
|
+
|
|
233
|
+
unless @nodes.key?(@start_node)
|
|
234
|
+
raise ConfigurationError, "start_node '#{@start_node}' not found in nodes"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
@nodes.each do |node_name, node_config|
|
|
238
|
+
unless node_config.is_a?(Hash)
|
|
239
|
+
raise ConfigurationError, "Node '#{node_name}' must be a hash"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
if node_config[:agents]
|
|
243
|
+
unless node_config[:agents].is_a?(Array)
|
|
244
|
+
raise ConfigurationError, "Node '#{node_name}' agents must be an array"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
node_config[:agents].each do |agent_config|
|
|
248
|
+
unless agent_config.is_a?(Hash) && agent_config[:agent]
|
|
249
|
+
raise ConfigurationError,
|
|
250
|
+
"Node '#{node_name}' agents must be hashes with 'agent' key"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
agent_sym = agent_config[:agent].to_sym
|
|
254
|
+
unless @agents.key?(agent_sym)
|
|
255
|
+
raise ConfigurationError,
|
|
256
|
+
"Node '#{node_name}' references undefined agent '#{agent_config[:agent]}'"
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
next unless node_config[:dependencies]
|
|
262
|
+
unless node_config[:dependencies].is_a?(Array)
|
|
263
|
+
raise ConfigurationError, "Node '#{node_name}' dependencies must be an array"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
node_config[:dependencies].each do |dep|
|
|
267
|
+
dep_sym = dep.to_sym
|
|
268
|
+
unless @nodes.key?(dep_sym)
|
|
269
|
+
raise ConfigurationError,
|
|
270
|
+
"Node '#{node_name}' depends on undefined node '#{dep}'"
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def parse_scratchpad_mode(value)
|
|
277
|
+
return :disabled if value.nil?
|
|
278
|
+
|
|
279
|
+
value = value.to_sym if value.is_a?(String)
|
|
280
|
+
|
|
281
|
+
case value
|
|
282
|
+
when :enabled, :disabled, :per_node
|
|
283
|
+
value
|
|
284
|
+
else
|
|
285
|
+
raise ConfigurationError,
|
|
286
|
+
"Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def interpolate_env_vars!(obj)
|
|
291
|
+
case obj
|
|
292
|
+
when String
|
|
293
|
+
interpolate_env_string(obj)
|
|
294
|
+
when Hash
|
|
295
|
+
obj.transform_values! { |v| interpolate_env_vars!(v) }
|
|
296
|
+
when Array
|
|
297
|
+
obj.map! { |v| interpolate_env_vars!(v) }
|
|
298
|
+
else
|
|
299
|
+
obj
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def interpolate_env_string(str)
|
|
304
|
+
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
305
|
+
env_var = Regexp.last_match(1)
|
|
306
|
+
has_default = Regexp.last_match(2)
|
|
307
|
+
default_value = Regexp.last_match(3)
|
|
308
|
+
|
|
309
|
+
if ENV.key?(env_var)
|
|
310
|
+
ENV[env_var]
|
|
311
|
+
elsif has_default
|
|
312
|
+
default_value || ""
|
|
313
|
+
else
|
|
314
|
+
raise ConfigurationError, "Environment variable '#{env_var}' is not set"
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def detect_circular_dependencies
|
|
320
|
+
@agents.each_key do |agent_name|
|
|
321
|
+
visited = Set.new
|
|
322
|
+
path = []
|
|
323
|
+
detect_cycle_from(agent_name, visited, path)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def detect_cycle_from(agent_name, visited, path)
|
|
328
|
+
return if visited.include?(agent_name)
|
|
329
|
+
|
|
330
|
+
if path.include?(agent_name)
|
|
331
|
+
cycle_start = path.index(agent_name)
|
|
332
|
+
cycle = path[cycle_start..] + [agent_name]
|
|
333
|
+
raise CircularDependencyError, "Circular dependency detected: #{cycle.join(" -> ")}"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
path.push(agent_name)
|
|
337
|
+
connections_for(agent_name).each do |connection|
|
|
338
|
+
connection_sym = connection.to_sym
|
|
339
|
+
|
|
340
|
+
next if @external_swarms.key?(connection_sym)
|
|
341
|
+
|
|
342
|
+
unless @agents.key?(connection_sym)
|
|
343
|
+
raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{connection}' (not a local agent or registered swarm)"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
detect_cycle_from(connection_sym, visited, path)
|
|
347
|
+
end
|
|
348
|
+
path.pop
|
|
349
|
+
visited.add(agent_name)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
class Configuration
|
|
5
|
+
# Translates parsed configuration to Swarm/Workflow using DSL builders
|
|
6
|
+
#
|
|
7
|
+
# This class is responsible for:
|
|
8
|
+
# - Creating the appropriate builder (Swarm::Builder or Workflow::Builder)
|
|
9
|
+
# - Translating parsed configuration into DSL method calls
|
|
10
|
+
# - Building the final Swarm or Workflow instance
|
|
11
|
+
#
|
|
12
|
+
# Receives a parsed Configuration::Parser and converts it to runtime objects.
|
|
13
|
+
class Translator
|
|
14
|
+
def initialize(parser)
|
|
15
|
+
@parser = parser
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_swarm(allow_filesystem_tools: nil)
|
|
19
|
+
builder = create_builder(allow_filesystem_tools)
|
|
20
|
+
|
|
21
|
+
translate_common_config(builder)
|
|
22
|
+
translate_type_specific_config(builder)
|
|
23
|
+
translate_agents(builder)
|
|
24
|
+
translate_hooks(builder)
|
|
25
|
+
|
|
26
|
+
builder.build_swarm
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def create_builder(allow_filesystem_tools)
|
|
32
|
+
if @parser.config_type == :swarm
|
|
33
|
+
Swarm::Builder.new(allow_filesystem_tools: allow_filesystem_tools)
|
|
34
|
+
else
|
|
35
|
+
Workflow::Builder.new(allow_filesystem_tools: allow_filesystem_tools)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def translate_common_config(builder)
|
|
40
|
+
builder.id(@parser.swarm_id) if @parser.swarm_id
|
|
41
|
+
builder.name(@parser.swarm_name)
|
|
42
|
+
builder.scratchpad(@parser.scratchpad_mode)
|
|
43
|
+
|
|
44
|
+
if @parser.external_swarms&.any?
|
|
45
|
+
external_swarms = @parser.external_swarms
|
|
46
|
+
builder.swarms do
|
|
47
|
+
external_swarms.each do |name, config|
|
|
48
|
+
source = config[:source]
|
|
49
|
+
case source[:type]
|
|
50
|
+
when :file
|
|
51
|
+
register(name, file: source[:value], keep_context: config[:keep_context])
|
|
52
|
+
when :yaml
|
|
53
|
+
register(name, yaml: source[:value], keep_context: config[:keep_context])
|
|
54
|
+
else
|
|
55
|
+
raise ConfigurationError, "Unknown source type: #{source[:type]}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
translate_all_agents(builder) if @parser.all_agents_config.any?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def translate_type_specific_config(builder)
|
|
65
|
+
if @parser.config_type == :swarm
|
|
66
|
+
builder.lead(@parser.lead_agent)
|
|
67
|
+
else
|
|
68
|
+
builder.start_node(@parser.start_node)
|
|
69
|
+
translate_nodes(builder)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def translate_hooks(builder)
|
|
74
|
+
return if @parser.swarm_hooks.none?
|
|
75
|
+
|
|
76
|
+
@parser.swarm_hooks.each do |event, hook_specs|
|
|
77
|
+
Array(hook_specs).each do |spec|
|
|
78
|
+
if spec[:type] == "command"
|
|
79
|
+
builder.hook(event, command: spec[:command], timeout: spec[:timeout])
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def translate_all_agents(builder)
|
|
86
|
+
all_agents_cfg = @parser.all_agents_config
|
|
87
|
+
all_agents_hks = @parser.all_agents_hooks
|
|
88
|
+
|
|
89
|
+
builder.all_agents do
|
|
90
|
+
tools(*all_agents_cfg[:tools]) if all_agents_cfg[:tools]&.any?
|
|
91
|
+
model(all_agents_cfg[:model]) if all_agents_cfg[:model]
|
|
92
|
+
provider(all_agents_cfg[:provider]) if all_agents_cfg[:provider]
|
|
93
|
+
base_url(all_agents_cfg[:base_url]) if all_agents_cfg[:base_url]
|
|
94
|
+
api_version(all_agents_cfg[:api_version]) if all_agents_cfg[:api_version]
|
|
95
|
+
timeout(all_agents_cfg[:timeout]) if all_agents_cfg[:timeout]
|
|
96
|
+
parameters(all_agents_cfg[:parameters]) if all_agents_cfg[:parameters]
|
|
97
|
+
headers(all_agents_cfg[:headers]) if all_agents_cfg[:headers]
|
|
98
|
+
coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
|
|
99
|
+
disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
|
|
100
|
+
|
|
101
|
+
if all_agents_hks.any?
|
|
102
|
+
all_agents_hks.each do |event, hook_specs|
|
|
103
|
+
Array(hook_specs).each do |spec|
|
|
104
|
+
matcher = spec[:matcher]
|
|
105
|
+
hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
self.permissions_hash = all_agents_cfg[:permissions] if all_agents_cfg[:permissions]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def translate_agents(builder)
|
|
115
|
+
@parser.agents.each do |name, agent_config|
|
|
116
|
+
translate_agent(builder, name, agent_config)
|
|
117
|
+
rescue ConfigurationError => e
|
|
118
|
+
raise ConfigurationError, "Error in #{@parser.config_type}.agents.#{name}: #{e.message}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def translate_agent(builder, name, config)
|
|
123
|
+
if config[:agent_file]
|
|
124
|
+
agent_file_path = resolve_agent_file_path(config[:agent_file])
|
|
125
|
+
|
|
126
|
+
unless File.exist?(agent_file_path)
|
|
127
|
+
raise ConfigurationError, "Agent file not found: #{agent_file_path}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
content = File.read(agent_file_path)
|
|
131
|
+
overrides = config.except(:agent_file)
|
|
132
|
+
|
|
133
|
+
if overrides.any?
|
|
134
|
+
builder.agent(name, content, &create_agent_config_block(overrides))
|
|
135
|
+
else
|
|
136
|
+
builder.agent(name, content)
|
|
137
|
+
end
|
|
138
|
+
else
|
|
139
|
+
builder.agent(name, &create_agent_config_block(config))
|
|
140
|
+
end
|
|
141
|
+
rescue StandardError => e
|
|
142
|
+
raise ConfigurationError, "Error loading agent '#{name}': #{e.message}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def create_agent_config_block(config)
|
|
146
|
+
proc do
|
|
147
|
+
description(config[:description]) if config[:description]
|
|
148
|
+
model(config[:model]) if config[:model]
|
|
149
|
+
provider(config[:provider]) if config[:provider]
|
|
150
|
+
base_url(config[:base_url]) if config[:base_url]
|
|
151
|
+
api_version(config[:api_version]) if config[:api_version]
|
|
152
|
+
context_window(config[:context_window]) if config[:context_window]
|
|
153
|
+
system_prompt(config[:system_prompt]) if config[:system_prompt]
|
|
154
|
+
directory(config[:directory]) if config[:directory]
|
|
155
|
+
timeout(config[:timeout]) if config[:timeout]
|
|
156
|
+
parameters(config[:parameters]) if config[:parameters]
|
|
157
|
+
headers(config[:headers]) if config[:headers]
|
|
158
|
+
coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
|
|
159
|
+
bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
|
160
|
+
disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
161
|
+
shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
|
|
162
|
+
|
|
163
|
+
if config[:tools]&.any?
|
|
164
|
+
tool_names = config[:tools].map { |t| t.is_a?(Hash) ? t[:name] : t }
|
|
165
|
+
tools(*tool_names)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
|
|
169
|
+
|
|
170
|
+
config[:mcp_servers]&.each do |server|
|
|
171
|
+
mcp_server(server[:name], **server.except(:name))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
config[:hooks]&.each do |event, hook_specs|
|
|
175
|
+
Array(hook_specs).each do |spec|
|
|
176
|
+
matcher = spec[:matcher]
|
|
177
|
+
hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Translate context_management YAML config to DSL
|
|
182
|
+
if config[:context_management]
|
|
183
|
+
ctx_mgmt_config = config[:context_management]
|
|
184
|
+
context_management do
|
|
185
|
+
ctx_mgmt_config.each do |event_name, handler_cfg|
|
|
186
|
+
# Capture handler_cfg in closure for each threshold
|
|
187
|
+
captured_config = handler_cfg
|
|
188
|
+
on(event_name.to_sym) do |ctx|
|
|
189
|
+
action = captured_config[:action]&.to_s
|
|
190
|
+
|
|
191
|
+
case action
|
|
192
|
+
when "compress_tool_results"
|
|
193
|
+
ctx.compress_tool_results(
|
|
194
|
+
keep_recent: captured_config[:keep_recent] || 10,
|
|
195
|
+
truncate_to: captured_config[:truncate_to] || 200,
|
|
196
|
+
)
|
|
197
|
+
when "prune_old_messages"
|
|
198
|
+
ctx.prune_old_messages(keep_recent: captured_config[:keep_recent] || 20)
|
|
199
|
+
when "log_warning"
|
|
200
|
+
ctx.log_action("threshold_warning", threshold: ctx.threshold)
|
|
201
|
+
else
|
|
202
|
+
raise ConfigurationError, "Unknown context_management action: #{action}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Let plugins handle their YAML config translation
|
|
210
|
+
# This removes SDK knowledge of plugin-specific configuration
|
|
211
|
+
PluginRegistry.all.each do |plugin|
|
|
212
|
+
plugin.translate_yaml_config(self, config)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
self.permissions_hash = config[:permissions] if config[:permissions]
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def translate_nodes(builder)
|
|
220
|
+
@parser.nodes.each do |node_name, node_config|
|
|
221
|
+
builder.node(node_name) do
|
|
222
|
+
node_config[:agents]&.each do |agent_config|
|
|
223
|
+
agent_name = agent_config[:agent].to_sym
|
|
224
|
+
delegates = agent_config[:delegates_to] || []
|
|
225
|
+
reset_ctx = agent_config.key?(:reset_context) ? agent_config[:reset_context] : true
|
|
226
|
+
tools_override = agent_config[:tools]
|
|
227
|
+
|
|
228
|
+
agent_cfg = agent(agent_name, reset_context: reset_ctx)
|
|
229
|
+
agent_cfg = agent_cfg.delegates_to(*delegates) if delegates.any?
|
|
230
|
+
agent_cfg.tools(*tools_override) if tools_override
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
depends_on(*node_config[:dependencies]) if node_config[:dependencies]&.any?
|
|
234
|
+
|
|
235
|
+
lead(node_config[:lead].to_sym) if node_config[:lead]
|
|
236
|
+
|
|
237
|
+
if node_config[:input_command]
|
|
238
|
+
input_command(node_config[:input_command], timeout: node_config[:input_timeout] || 60)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
if node_config[:output_command]
|
|
242
|
+
output_command(node_config[:output_command], timeout: node_config[:output_timeout] || 60)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def resolve_agent_file_path(file_path)
|
|
249
|
+
return file_path if Pathname.new(file_path).absolute?
|
|
250
|
+
|
|
251
|
+
@parser.base_dir.join(file_path).to_s
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|