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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +58 -0
  3. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  4. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  5. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  6. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  7. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  8. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  9. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  10. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  11. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  12. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  13. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  14. data/lib/swarm_sdk/agent/context.rb +2 -2
  15. data/lib/swarm_sdk/agent/definition.rb +66 -154
  16. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  17. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  18. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  19. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  20. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  21. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  22. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  23. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  24. data/lib/swarm_sdk/configuration.rb +65 -543
  25. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  26. data/lib/swarm_sdk/context_compactor.rb +6 -11
  27. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  28. data/lib/swarm_sdk/context_management/context.rb +328 -0
  29. data/lib/swarm_sdk/defaults.rb +196 -0
  30. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  31. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  32. data/lib/swarm_sdk/log_collector.rb +179 -29
  33. data/lib/swarm_sdk/log_stream.rb +29 -0
  34. data/lib/swarm_sdk/node_context.rb +1 -1
  35. data/lib/swarm_sdk/observer/builder.rb +81 -0
  36. data/lib/swarm_sdk/observer/config.rb +45 -0
  37. data/lib/swarm_sdk/observer/manager.rb +236 -0
  38. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  39. data/lib/swarm_sdk/plugin.rb +93 -3
  40. data/lib/swarm_sdk/snapshot.rb +6 -6
  41. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  42. data/lib/swarm_sdk/state_restorer.rb +136 -151
  43. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  44. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  45. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  46. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  47. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  48. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  49. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  50. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  51. data/lib/swarm_sdk/swarm.rb +137 -679
  52. data/lib/swarm_sdk/tools/bash.rb +11 -3
  53. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  54. data/lib/swarm_sdk/tools/edit.rb +8 -13
  55. data/lib/swarm_sdk/tools/glob.rb +9 -1
  56. data/lib/swarm_sdk/tools/grep.rb +7 -0
  57. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  58. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  59. data/lib/swarm_sdk/tools/read.rb +11 -13
  60. data/lib/swarm_sdk/tools/registry.rb +122 -10
  61. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  62. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  63. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  64. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  65. data/lib/swarm_sdk/tools/write.rb +8 -13
  66. data/lib/swarm_sdk/version.rb +1 -1
  67. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  68. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  69. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  70. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  71. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  72. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  73. data/lib/swarm_sdk.rb +33 -3
  74. metadata +67 -15
  75. 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