swarm_sdk 2.0.0.pre.2 → 2.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dfc7f8797b72503c6f69cf0a524bf278c503f59379ff063ad720d34eac2e0244
4
- data.tar.gz: b8add20a799625d7c4a067b43eb98bfac926a0ecc56e33c3c5ad0ee02243552b
3
+ metadata.gz: c45fe12dc7f0cc16c5e82d539f941d22cfc847919f108000b73a6ff259819d24
4
+ data.tar.gz: cd18d30692c2686c60c3e20e48081ecab97ab81f7bc9051da8bb9a3bf862a979
5
5
  SHA512:
6
- metadata.gz: 5bf6007ca02a43c843f1f15486a7a5fdca8e269aed37d5ae102c1ee99ce055bb6a4dc34a427da72ce0c8fe84f7e8782b4d4d3ad8ed8010e64741fbfeb5ac0269
7
- data.tar.gz: b89b9cef461bd4546f498b6f0f693cc56c4c8c006b6f2e864df4743e3af18aeaf7976ce440c364c031c2764d1ccdae65b7b38d32b5801739ba1a6346e7773a45
6
+ metadata.gz: 9c1d02a1552463ab920c2e851a47449e6ed6eec82cd8874781cbc37e526937094a0d4b4625ae68596abb1825224fd7021c918f68f32f9d5d900406bf381c883e
7
+ data.tar.gz: 85ba1af56c5fa6726565a8b65a43a5268aa01ee9381e1c12afc0d0ae75544f82a689bf2939b4b0a9213046c3e8efda84fa83c30a2f06f5bcbc4cd3c1b43fbbeb
@@ -46,50 +46,66 @@ module SwarmSDK
46
46
  @mcp_servers = []
47
47
  @include_default_tools = true
48
48
  @bypass_permissions = false
49
- @skip_base_prompt = false
49
+ @coding_agent = nil # nil = not set (will default to false in Definition)
50
50
  @assume_model_exists = nil
51
51
  @hooks = []
52
52
  @permissions_config = {}
53
53
  @default_permissions = {} # Set by SwarmBuilder from all_agents
54
54
  end
55
55
 
56
- # Set agent model
57
- def model(model_name)
56
+ # Set/get agent model
57
+ def model(model_name = :__not_provided__)
58
+ return @model if model_name == :__not_provided__
59
+
58
60
  @model = model_name
59
61
  end
60
62
 
61
- # Set provider
62
- def provider(provider_name)
63
+ # Set/get provider
64
+ def provider(provider_name = :__not_provided__)
65
+ return @provider if provider_name == :__not_provided__
66
+
63
67
  @provider = provider_name
64
68
  end
65
69
 
66
- # Set base URL
67
- def base_url(url)
70
+ # Set/get base URL
71
+ def base_url(url = :__not_provided__)
72
+ return @base_url if url == :__not_provided__
73
+
68
74
  @base_url = url
69
75
  end
70
76
 
71
- # Set API version (OpenAI-compatible providers only)
72
- def api_version(version)
77
+ # Set/get API version (OpenAI-compatible providers only)
78
+ def api_version(version = :__not_provided__)
79
+ return @api_version if version == :__not_provided__
80
+
73
81
  @api_version = version
74
82
  end
75
83
 
76
- # Set explicit context window override
77
- def context_window(tokens)
84
+ # Set/get explicit context window override
85
+ def context_window(tokens = :__not_provided__)
86
+ return @context_window if tokens == :__not_provided__
87
+
78
88
  @context_window = tokens
79
89
  end
80
90
 
81
- # Set LLM parameters
82
- def parameters(params)
91
+ # Set/get LLM parameters
92
+ def parameters(params = :__not_provided__)
93
+ return @parameters if params == :__not_provided__
94
+
83
95
  @parameters = params
84
96
  end
85
97
 
86
- # Set custom HTTP headers
87
- def headers(header_hash)
98
+ # Set/get custom HTTP headers
99
+ def headers(header_hash = :__not_provided__)
100
+ return @headers if header_hash == :__not_provided__
101
+
88
102
  @headers = header_hash
89
103
  end
90
104
 
91
- # Set timeout
92
- def timeout(seconds)
105
+ # Set/get timeout
106
+ def timeout(seconds = :__not_provided__)
107
+ return @timeout if seconds == :__not_provided__
108
+
93
109
  @timeout = seconds
94
110
  end
95
111
 
@@ -118,9 +134,18 @@ module SwarmSDK
118
134
  @bypass_permissions = enabled
119
135
  end
120
136
 
121
- # Set skip_base_prompt flag
122
- def skip_base_prompt(enabled)
123
- @skip_base_prompt = enabled
137
+ # Set coding_agent flag
138
+ #
139
+ # When true, includes the base system prompt for coding tasks.
140
+ # When false (default), uses only the custom system prompt.
141
+ #
142
+ # @param enabled [Boolean] Whether to include base coding prompt
143
+ # @return [void]
144
+ #
145
+ # @example
146
+ # coding_agent true # Include base prompt for coding tasks
147
+ def coding_agent(enabled)
148
+ @coding_agent = enabled
124
149
  end
125
150
 
126
151
  # Set assume_model_exists flag
@@ -212,6 +237,78 @@ module SwarmSDK
212
237
  @permissions_config = PermissionsBuilder.build(&block)
213
238
  end
214
239
 
240
+ # Check if model has been explicitly set (not default)
241
+ #
242
+ # Used by Swarm::Builder to determine if all_agents model should apply.
243
+ #
244
+ # @return [Boolean] true if model was explicitly set
245
+ def model_set?
246
+ @model != "gpt-5"
247
+ end
248
+
249
+ # Check if provider has been explicitly set
250
+ #
251
+ # Used by Swarm::Builder to determine if all_agents provider should apply.
252
+ #
253
+ # @return [Boolean] true if provider was explicitly set
254
+ def provider_set?
255
+ !@provider.nil?
256
+ end
257
+
258
+ # Check if base_url has been explicitly set
259
+ #
260
+ # Used by Swarm::Builder to determine if all_agents base_url should apply.
261
+ #
262
+ # @return [Boolean] true if base_url was explicitly set
263
+ def base_url_set?
264
+ !@base_url.nil?
265
+ end
266
+
267
+ # Check if api_version has been explicitly set
268
+ #
269
+ # Used by Swarm::Builder to determine if all_agents api_version should apply.
270
+ #
271
+ # @return [Boolean] true if api_version was explicitly set
272
+ def api_version_set?
273
+ !@api_version.nil?
274
+ end
275
+
276
+ # Check if timeout has been explicitly set
277
+ #
278
+ # Used by Swarm::Builder to determine if all_agents timeout should apply.
279
+ #
280
+ # @return [Boolean] true if timeout was explicitly set
281
+ def timeout_set?
282
+ !@timeout.nil?
283
+ end
284
+
285
+ # Check if coding_agent has been explicitly set
286
+ #
287
+ # Used by Swarm::Builder to determine if all_agents coding_agent should apply.
288
+ #
289
+ # @return [Boolean] true if coding_agent was explicitly set
290
+ def coding_agent_set?
291
+ !@coding_agent.nil?
292
+ end
293
+
294
+ # Check if parameters have been set
295
+ #
296
+ # Used by Swarm::Builder for merging all_agents parameters.
297
+ #
298
+ # @return [Boolean] true if parameters were set
299
+ def parameters_set?
300
+ @parameters.any?
301
+ end
302
+
303
+ # Check if headers have been set
304
+ #
305
+ # Used by Swarm::Builder for merging all_agents headers.
306
+ #
307
+ # @return [Boolean] true if headers were set
308
+ def headers_set?
309
+ @headers.any?
310
+ end
311
+
215
312
  # Build and return an Agent::Definition
216
313
  #
217
314
  # This method converts the builder's configuration into a validated
@@ -242,7 +339,7 @@ module SwarmSDK
242
339
  agent_config[:mcp_servers] = @mcp_servers if @mcp_servers.any?
243
340
  agent_config[:include_default_tools] = @include_default_tools
244
341
  agent_config[:bypass_permissions] = @bypass_permissions
245
- agent_config[:skip_base_prompt] = @skip_base_prompt
342
+ agent_config[:coding_agent] = @coding_agent
246
343
  agent_config[:assume_model_exists] = @assume_model_exists unless @assume_model_exists.nil?
247
344
  agent_config[:permissions] = @permissions_config if @permissions_config.any?
248
345
  agent_config[:default_permissions] = @default_permissions if @default_permissions.any?
@@ -39,7 +39,7 @@ module SwarmSDK
39
39
  :headers,
40
40
  :timeout,
41
41
  :include_default_tools,
42
- :skip_base_prompt,
42
+ :coding_agent,
43
43
  :default_permissions,
44
44
  :agent_permissions,
45
45
  :assume_model_exists,
@@ -83,8 +83,10 @@ module SwarmSDK
83
83
  # include_default_tools defaults to true if not specified
84
84
  @include_default_tools = config.key?(:include_default_tools) ? config[:include_default_tools] : true
85
85
 
86
- # skip_base_prompt defaults to false if not specified
87
- @skip_base_prompt = config.key?(:skip_base_prompt) ? config[:skip_base_prompt] : false
86
+ # coding_agent defaults to false if not specified
87
+ # When true, includes the base system prompt for coding tasks
88
+ # When false, uses only the custom system prompt (no base prompt)
89
+ @coding_agent = config.key?(:coding_agent) ? config[:coding_agent] : false
88
90
 
89
91
  # Parse directory first so it can be used in system prompt rendering
90
92
  @directory = parse_directory(config[:directory])
@@ -132,26 +134,107 @@ module SwarmSDK
132
134
  timeout: @timeout,
133
135
  bypass_permissions: @bypass_permissions,
134
136
  include_default_tools: @include_default_tools,
135
- skip_base_prompt: @skip_base_prompt,
137
+ coding_agent: @coding_agent,
136
138
  assume_model_exists: @assume_model_exists,
137
139
  max_concurrent_tools: @max_concurrent_tools,
138
140
  hooks: @hooks,
139
141
  }.compact
140
142
  end
141
143
 
144
+ # Validate agent configuration and return warnings (non-fatal issues)
145
+ #
146
+ # Unlike validate! which raises exceptions for critical errors, this method
147
+ # returns an array of warning hashes for non-fatal issues like:
148
+ # - Model not found in registry (informs user, suggests alternatives)
149
+ # - Context tracking unavailable (useful even with assume_model_exists)
150
+ #
151
+ # Note: Validation ALWAYS runs, even with assume_model_exists: true or base_url set.
152
+ # The purpose is to inform the user about potential issues and suggest corrections,
153
+ # not to block execution.
154
+ #
155
+ # @return [Array<Hash>] Array of warning hashes
156
+ def validate
157
+ warnings = []
158
+
159
+ # Always validate model (even with assume_model_exists)
160
+ # Warnings inform user about typos and context tracking limitations
161
+ model_warning = validate_model
162
+ warnings << model_warning if model_warning
163
+
164
+ # Future: could add tool validation, delegate validation, etc.
165
+
166
+ warnings
167
+ end
168
+
142
169
  private
143
170
 
144
- def build_full_system_prompt(custom_prompt)
145
- # If skip_base_prompt is true, return only the custom prompt (or empty string if nil)
146
- if @skip_base_prompt
147
- return (custom_prompt || "").to_s
148
- end
171
+ # Validate that model exists in RubyLLM registry
172
+ #
173
+ # @return [Hash, nil] Warning hash if model not found, nil otherwise
174
+ def validate_model
175
+ # Try to find model in registry (searches all providers)
176
+ RubyLLM.models.find(@model)
177
+ nil # Model exists
178
+ rescue StandardError => e
179
+ # Model not found - return warning with suggestions
180
+ {
181
+ type: :model_not_found,
182
+ agent: @name,
183
+ model: @model,
184
+ error_message: e.message,
185
+ suggestions: suggest_similar_models,
186
+ }
187
+ end
149
188
 
150
- rendered_base = render_base_system_prompt
189
+ # Suggest similar models when a model is not found
190
+ #
191
+ # @return [Array<Hash>] Up to 3 similar models with their info
192
+ def suggest_similar_models
193
+ normalized_query = @model.to_s.downcase.gsub(/[.\-_]/, "")
194
+
195
+ RubyLLM.models.all.select do |model_info|
196
+ normalized_id = model_info.id.downcase.gsub(/[.\-_]/, "")
197
+ normalized_id.include?(normalized_query) ||
198
+ model_info.name&.downcase&.gsub(/[.\-_]/, "")&.include?(normalized_query)
199
+ end.first(3).map do |model_info|
200
+ {
201
+ id: model_info.id,
202
+ name: model_info.name,
203
+ context_window: model_info.context_window,
204
+ }
205
+ end
206
+ rescue StandardError
207
+ []
208
+ end
151
209
 
152
- return rendered_base if custom_prompt.nil? || custom_prompt.strip.empty?
210
+ def build_full_system_prompt(custom_prompt)
211
+ # If coding_agent is false (default), return custom prompt with optional TODO/Scratchpad info
212
+ # If coding_agent is true, include full base prompt for coding tasks
213
+ if @coding_agent
214
+ # Coding agent: include full base prompt
215
+ rendered_base = render_base_system_prompt
216
+
217
+ if custom_prompt && !custom_prompt.strip.empty?
218
+ "#{rendered_base}\n\n#{custom_prompt}"
219
+ else
220
+ rendered_base
221
+ end
222
+ elsif @include_default_tools
223
+ # Non-coding agent: optionally include TODO/Scratchpad sections if default tools available
224
+ non_coding_base = render_non_coding_base_prompt
153
225
 
154
- "#{rendered_base}\n\n#{custom_prompt}"
226
+ if custom_prompt && !custom_prompt.strip.empty?
227
+ # Prepend TODO/Scratchpad info before custom prompt
228
+ "#{non_coding_base}\n\n#{custom_prompt}"
229
+ else
230
+ # No custom prompt: just return TODO/Scratchpad info
231
+ non_coding_base
232
+ end
233
+ # Default tools available: include TODO/Scratchpad instructions
234
+ else
235
+ # No default tools: return only custom prompt
236
+ (custom_prompt || "").to_s
237
+ end
155
238
  end
156
239
 
157
240
  def render_base_system_prompt
@@ -168,6 +251,32 @@ module SwarmSDK
168
251
  ERB.new(template_content).result(binding)
169
252
  end
170
253
 
254
+ def render_non_coding_base_prompt
255
+ # Simplified base prompt for non-coding agents
256
+ # Only includes TODO and Scratchpad tool information
257
+ # Does not steer towards coding tasks
258
+ <<~PROMPT.strip
259
+ # Task Management
260
+
261
+ You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool to track your progress and give visibility into your work.
262
+
263
+ When working on multi-step tasks:
264
+ 1. Create a todo list with all known tasks before starting work
265
+ 2. Mark each task as in_progress when you start it
266
+ 3. Mark each task as completed IMMEDIATELY after finishing it
267
+ 4. Complete ALL pending todos before finishing your response
268
+
269
+ # Scratchpad Storage
270
+
271
+ You have access to Scratchpad tools for storing and retrieving information:
272
+ - **ScratchpadWrite**: Store detailed outputs, analysis, or results that are too long for direct responses
273
+ - **ScratchpadRead**: Retrieve previously stored content
274
+ - **ScratchpadList**: List available scratchpad entries
275
+
276
+ Use the scratchpad to share information that would otherwise clutter your responses.
277
+ PROMPT
278
+ end
279
+
171
280
  def parse_directory(directory_config)
172
281
  directory_config ||= "."
173
282
  File.expand_path(directory_config.to_s)
@@ -147,16 +147,31 @@ module SwarmSDK
147
147
  swarm_agents = @config[:swarm][:agents]
148
148
 
149
149
  swarm_agents.each do |name, agent_config|
150
- agent_config ||= {}
151
-
152
- # Merge all_agents_config into agent config
153
- # Agent-specific config overrides all_agents config
154
- merged_config = merge_all_agents_config(agent_config)
155
-
156
- @agents[name] = if agent_config[:agent_file]
157
- load_agent_from_file(name, agent_config[:agent_file], merged_config)
150
+ # Support three formats:
151
+ # 1. String: assistant: "agents/assistant.md" (file path)
152
+ # 2. Hash with agent_file: assistant: { agent_file: "..." }
153
+ # 3. Hash with inline definition: assistant: { description: "...", model: "..." }
154
+
155
+ if agent_config.is_a?(String)
156
+ # Format 1: Direct file path as string
157
+ file_path = agent_config
158
+ merged_config = merge_all_agents_config({})
159
+ @agents[name] = load_agent_from_file(name, file_path, merged_config)
158
160
  else
159
- Agent::Definition.new(name, merged_config)
161
+ # Format 2 or 3: Hash configuration
162
+ agent_config ||= {}
163
+
164
+ # Merge all_agents_config into agent config
165
+ # Agent-specific config overrides all_agents config
166
+ merged_config = merge_all_agents_config(agent_config)
167
+
168
+ @agents[name] = if agent_config[:agent_file]
169
+ # Format 2: Hash with agent_file key
170
+ load_agent_from_file(name, agent_config[:agent_file], merged_config)
171
+ else
172
+ # Format 3: Inline definition
173
+ Agent::Definition.new(name, merged_config)
174
+ end
160
175
  end
161
176
  end
162
177
 
@@ -167,10 +182,19 @@ module SwarmSDK
167
182
 
168
183
  # Merge all_agents config with agent-specific config
169
184
  # Agent config takes precedence over all_agents config
185
+ #
186
+ # Merge strategy:
187
+ # - Arrays (tools, delegates_to): Concatenate
188
+ # - Hashes (parameters, headers): Merge (agent values override)
189
+ # - Scalars (model, provider, base_url, timeout, coding_agent): Agent overrides
190
+ #
191
+ # @param agent_config [Hash] Agent-specific configuration
192
+ # @return [Hash] Merged configuration
170
193
  def merge_all_agents_config(agent_config)
171
194
  merged = @all_agents_config.dup
172
195
 
173
- # For arrays (like tools, delegates_to), concatenate instead of replace
196
+ # For arrays, concatenate
197
+ # For hashes, merge (agent values override)
174
198
  # For scalars, agent value overrides
175
199
  agent_config.each do |key, value|
176
200
  case key
@@ -180,8 +204,17 @@ module SwarmSDK
180
204
  when :delegates_to
181
205
  # Concatenate delegates_to
182
206
  merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
207
+ when :parameters
208
+ # Merge parameters: all_agents.parameters + agent.parameters
209
+ # Agent values override all_agents values for same keys
210
+ merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
211
+ when :headers
212
+ # Merge headers: all_agents.headers + agent.headers
213
+ # Agent values override all_agents values for same keys
214
+ merged[:headers] = (merged[:headers] || {}).merge(value || {})
183
215
  else
184
- # For everything else, agent value overrides all_agents value
216
+ # For everything else (model, provider, base_url, timeout, coding_agent, etc.),
217
+ # agent value overrides all_agents value
185
218
  merged[key] = value
186
219
  end
187
220
  end
@@ -158,6 +158,40 @@ module SwarmSDK
158
158
  def finish_swarm(message)
159
159
  Result.finish_swarm(message)
160
160
  end
161
+
162
+ # Enter an interactive debugging breakpoint
163
+ #
164
+ # This method:
165
+ # 1. Emits a breakpoint_enter event (formatters can pause spinners)
166
+ # 2. Opens binding.irb for interactive debugging
167
+ # 3. Emits a breakpoint_exit event (formatters can resume spinners)
168
+ #
169
+ # @example Use in a hook
170
+ # hook(:pre_delegation) do |ctx|
171
+ # ctx.breakpoint # Pause execution and inspect context
172
+ # end
173
+ #
174
+ # @return [void]
175
+ def breakpoint
176
+ # Emit breakpoint_enter event
177
+ LogStream.emit(
178
+ type: "breakpoint_enter",
179
+ agent: @agent_name,
180
+ event: @event,
181
+ timestamp: Time.now.utc.iso8601,
182
+ )
183
+
184
+ # Enter interactive debugging
185
+ binding.irb # rubocop:disable Lint/Debugger
186
+
187
+ # Emit breakpoint_exit event
188
+ LogStream.emit(
189
+ type: "breakpoint_exit",
190
+ agent: @agent_name,
191
+ event: @event,
192
+ timestamp: Time.now.utc.iso8601,
193
+ )
194
+ end
161
195
  end
162
196
  end
163
197
  end
@@ -41,6 +41,10 @@ module SwarmSDK
41
41
 
42
42
  # Context events
43
43
  :context_warning, # When context usage crosses threshold
44
+
45
+ # Debug events
46
+ :breakpoint_enter, # When entering interactive debugging (binding.irb)
47
+ :breakpoint_exit, # When exiting interactive debugging
44
48
  ].freeze
45
49
 
46
50
  def initialize
@@ -14,30 +14,18 @@ module SwarmSDK
14
14
  # puts JSON.generate(event)
15
15
  # end
16
16
  #
17
- # # Freeze callbacks (after all registrations, before Async execution)
18
- # LogCollector.freeze!
19
- #
20
17
  # # During execution, LogStream calls emit
21
18
  # LogCollector.emit(type: "user_prompt", agent: :backend)
22
19
  #
23
- # ## Fiber Safety
24
- #
25
- # LogCollector is fiber-safe because:
26
- # - All callbacks registered before Async execution starts
27
- # - freeze! makes @callbacks immutable
28
- # - emit() only reads the frozen array (no mutations)
20
+ # # After execution, reset for next use
21
+ # LogCollector.reset!
29
22
  #
30
23
  module LogCollector
31
24
  class << self
32
25
  # Register a callback to receive log events
33
26
  #
34
- # Must be called before freeze! is called.
35
- #
36
27
  # @yield [Hash] Log event entry
37
- # @raise [StateError] If called after freeze!
38
28
  def on_log(&block)
39
- raise StateError, "Cannot register callbacks after LogCollector is frozen" if @frozen
40
-
41
29
  @callbacks ||= []
42
30
  @callbacks << block
43
31
  end
@@ -47,36 +35,16 @@ module SwarmSDK
47
35
  # @param entry [Hash] Log event entry
48
36
  # @return [void]
49
37
  def emit(entry)
50
- # Use defensive copy for fiber safety
51
38
  Array(@callbacks).each do |callback|
52
39
  callback.call(entry)
53
40
  end
54
41
  end
55
42
 
56
- # Freeze the callbacks array (call before Async execution)
57
- #
58
- # This prevents new callbacks from being registered and makes
59
- # the array immutable for fiber safety.
60
- #
61
- # @return [void]
62
- def freeze!
63
- @callbacks&.freeze
64
- @frozen = true
65
- end
66
-
67
- # Reset the collector (for test cleanup)
43
+ # Reset the collector (clears callbacks for next execution)
68
44
  #
69
45
  # @return [void]
70
46
  def reset!
71
47
  @callbacks = []
72
- @frozen = false
73
- end
74
-
75
- # Check if collector is frozen
76
- #
77
- # @return [Boolean]
78
- def frozen?
79
- @frozen
80
48
  end
81
49
  end
82
50
  end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Node
5
+ # AgentConfig provides fluent API for configuring agents within a node
6
+ #
7
+ # This class enables the chainable syntax:
8
+ # agent(:backend).delegates_to(:tester, :database)
9
+ #
10
+ # @example Basic delegation
11
+ # agent(:backend).delegates_to(:tester)
12
+ #
13
+ # @example No delegation (solo agent)
14
+ # agent(:planner)
15
+ class AgentConfig
16
+ attr_reader :agent_name
17
+
18
+ def initialize(agent_name, node_builder)
19
+ @agent_name = agent_name
20
+ @node_builder = node_builder
21
+ @delegates_to = []
22
+ @finalized = false
23
+ end
24
+
25
+ # Set delegation targets for this agent
26
+ #
27
+ # @param agent_names [Array<Symbol>] Names of agents to delegate to
28
+ # @return [self] For method chaining
29
+ def delegates_to(*agent_names)
30
+ @delegates_to = agent_names.map(&:to_sym)
31
+ finalize
32
+ self
33
+ end
34
+
35
+ # Finalize agent configuration (called automatically)
36
+ #
37
+ # Registers this agent configuration with the parent node builder.
38
+ # If delegates_to was never called, registers with empty delegation.
39
+ #
40
+ # @return [void]
41
+ def finalize
42
+ return if @finalized
43
+
44
+ @node_builder.register_agent(@agent_name, @delegates_to)
45
+ @finalized = true
46
+ end
47
+ end
48
+ end
49
+ end