swarm_sdk 2.7.9 → 2.7.11

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.
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds concurrent tool execution support to RubyLLM::Chat
4
+ # Supports :async and :threads executors with configurable concurrency limits
5
+ #
6
+ # Fork Reference: Commit d0912c7, file lib/ruby_llm/tool_executors.rb
7
+
8
+ module RubyLLM
9
+ # Tool executor registry
10
+ class << self
11
+ def tool_executors
12
+ @tool_executors ||= {}
13
+ end
14
+
15
+ def register_tool_executor(name, &block)
16
+ tool_executors[name] = block
17
+ end
18
+
19
+ def get_tool_executor(name)
20
+ tool_executors[name] || raise(ArgumentError, "Unknown tool executor: #{name}")
21
+ end
22
+ end
23
+
24
+ # Built-in tool executors
25
+ module ToolExecutors
26
+ class << self
27
+ def register_defaults
28
+ register_threads_executor
29
+ register_async_executor
30
+ end
31
+
32
+ private
33
+
34
+ def register_threads_executor
35
+ RubyLLM.register_tool_executor(:threads) do |tool_calls, max_concurrency:, &execute|
36
+ results = {}
37
+ mutex = Mutex.new
38
+ semaphore = max_concurrency ? Thread::SizedQueue.new(max_concurrency) : nil
39
+
40
+ # Fill semaphore with permits
41
+ max_concurrency&.times { semaphore << :permit }
42
+
43
+ threads = tool_calls.map do |tool_call|
44
+ Thread.new do
45
+ permit = semaphore&.pop
46
+
47
+ begin
48
+ result = execute.call(tool_call)
49
+ mutex.synchronize { results[tool_call.id] = result }
50
+ rescue StandardError => e
51
+ error_result = "Error: #{e.class}: #{e.message}"
52
+ mutex.synchronize { results[tool_call.id] = error_result }
53
+ RubyLLM.logger.warn("[RubyLLM] Tool #{tool_call.id} failed: #{e.message}")
54
+ ensure
55
+ semaphore&.push(permit) if permit
56
+ end
57
+ end
58
+ end
59
+
60
+ threads.each(&:join)
61
+ results
62
+ end
63
+ end
64
+
65
+ def register_async_executor
66
+ RubyLLM.register_tool_executor(:async) do |tool_calls, max_concurrency:, &execute|
67
+ AsyncExecutor.execute(tool_calls, max_concurrency: max_concurrency, &execute)
68
+ end
69
+ end
70
+ end
71
+
72
+ module AsyncExecutor
73
+ class << self
74
+ def execute(tool_calls, max_concurrency:, &block)
75
+ load_async_gem
76
+ run_with_sync { execute_tools(tool_calls, max_concurrency, &block) }
77
+ end
78
+
79
+ private
80
+
81
+ def load_async_gem
82
+ require "async"
83
+ require "async/barrier"
84
+ require "async/semaphore"
85
+ rescue LoadError => e
86
+ raise LoadError,
87
+ "The async gem is required for :async tool executor. " \
88
+ "Add `gem 'async'` to your Gemfile. Original error: #{e.message}"
89
+ end
90
+
91
+ def run_with_sync(&)
92
+ if defined?(Sync)
93
+ Sync(&)
94
+ else
95
+ Async(&).wait
96
+ end
97
+ end
98
+
99
+ def execute_tools(tool_calls, max_concurrency)
100
+ semaphore = max_concurrency ? Async::Semaphore.new(max_concurrency) : nil
101
+ barrier = Async::Barrier.new
102
+ results = {}
103
+
104
+ tool_calls.each do |tool_call|
105
+ barrier.async do
106
+ results[tool_call.id] = execute_single_tool(tool_call, semaphore) { yield tool_call }
107
+ rescue StandardError => e
108
+ results[tool_call.id] = "Error: #{e.class}: #{e.message}"
109
+ RubyLLM.logger.warn("[RubyLLM] Tool #{tool_call.id} failed: #{e.message}")
110
+ end
111
+ end
112
+
113
+ barrier.wait
114
+ results
115
+ end
116
+
117
+ def execute_single_tool(_tool_call, semaphore, &)
118
+ if semaphore
119
+ semaphore.acquire(&)
120
+ else
121
+ yield
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ class Chat
129
+ attr_reader :tool_concurrency, :max_concurrency
130
+
131
+ # Module to prepend for concurrent tool execution
132
+ module ConcurrentToolExecution
133
+ def initialize(tool_concurrency: nil, max_concurrency: nil, **kwargs)
134
+ @tool_concurrency = tool_concurrency
135
+ @max_concurrency = max_concurrency
136
+ super(**kwargs)
137
+ end
138
+
139
+ # Configure tool concurrency
140
+ def with_tool_concurrency(executor, max: nil)
141
+ @tool_concurrency = executor
142
+ @max_concurrency = max
143
+ self
144
+ end
145
+
146
+ private
147
+
148
+ # Override handle_tool_calls to support concurrent execution
149
+ # This method is called when tool_concurrency is set
150
+ def handle_tool_calls(response, &block)
151
+ return super unless @tool_concurrency
152
+
153
+ tool_calls = response.tool_calls
154
+ halt_result = execute_tools_concurrently(tool_calls)
155
+ halt_result || complete(&block)
156
+ end
157
+
158
+ def execute_tools_concurrently(tool_calls)
159
+ executor = RubyLLM.get_tool_executor(@tool_concurrency)
160
+ tool_calls_array = tool_calls.values
161
+
162
+ # Execute tools concurrently, emitting events per-tool
163
+ results = executor.call(tool_calls_array, max_concurrency: @max_concurrency) do |tool_call|
164
+ execute_single_tool_with_events(tool_call)
165
+ end
166
+
167
+ # Add all tool result messages atomically
168
+ add_tool_results_atomically(tool_calls, results)
169
+
170
+ # Find first halt result by original order
171
+ find_first_halt(tool_calls, results)
172
+ end
173
+
174
+ # Execute a single tool with events (for concurrent execution)
175
+ # Emits new_message, tool_call, and tool_result events per-tool
176
+ def execute_single_tool_with_events(tool_call)
177
+ emit(:new_message)
178
+ emit(:tool_call, tool_call)
179
+ result = execute_tool_with_hook(tool_call)
180
+ emit(:tool_result, tool_call, result)
181
+ result
182
+ end
183
+
184
+ # Add all tool result messages atomically to ensure consistent state
185
+ def add_tool_results_atomically(tool_calls, results)
186
+ messages = []
187
+
188
+ tool_calls.each_key do |id|
189
+ tool_call = tool_calls[id]
190
+ result = results[id]
191
+
192
+ tool_payload = result.is_a?(Tool::Halt) ? result.content : result
193
+ content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
194
+ message = add_message(role: :tool, content: content, tool_call_id: tool_call.id)
195
+ messages << message
196
+ end
197
+
198
+ # Fire end_message events for all messages
199
+ messages.each { |msg| emit(:end_message, msg) }
200
+ end
201
+
202
+ # Find the first halt result by request order
203
+ def find_first_halt(tool_calls, results)
204
+ tool_calls.each_key do |id|
205
+ result = results[id]
206
+ return result if result.is_a?(Tool::Halt)
207
+ end
208
+ nil
209
+ end
210
+ end
211
+
212
+ # Prepend after MultiSubscriberCallbacks so we can call its methods
213
+ prepend ConcurrentToolExecution
214
+ end
215
+ end
216
+
217
+ # Register built-in executors
218
+ RubyLLM::ToolExecutors.register_defaults
@@ -189,119 +189,18 @@ module SwarmSDK
189
189
  results
190
190
  end
191
191
 
192
- # Collect all delegation instances that need to be created
192
+ # Pass 2: Wire delegation tools (lazy loading for isolated delegates)
193
193
  #
194
- # Validates delegation configs and returns a list of instances to create.
195
- # This is done sequentially to fail fast on configuration errors.
194
+ # This pass wires delegation tools for primary agents:
195
+ # - Shared delegates use the primary agent instance
196
+ # - Isolated delegates use LazyDelegateChat (created on first use)
196
197
  #
197
- # @return [Array<Hash>] Array of { instance_name:, base_name:, definition: }
198
- def collect_delegation_instances_to_create
199
- instances = []
200
-
201
- @swarm.agent_definitions.each do |delegator_name, delegator_def|
202
- delegator_def.delegation_configs.each do |delegation_config|
203
- delegate_base_name = delegation_config[:agent]
204
-
205
- # Validate delegate exists
206
- unless @swarm.agent_definitions.key?(delegate_base_name)
207
- raise ConfigurationError,
208
- "Agent '#{delegator_name}' delegates to unknown agent '#{delegate_base_name}'"
209
- end
210
-
211
- delegate_definition = @swarm.agent_definitions[delegate_base_name]
212
-
213
- # Skip if delegate wants to be shared (use primary instead)
214
- next if delegate_definition.shared_across_delegations
215
-
216
- instance_name = "#{delegate_base_name}@#{delegator_name}"
217
-
218
- instances << {
219
- instance_name: instance_name,
220
- base_name: delegate_base_name,
221
- definition: delegate_definition,
222
- }
223
- end
224
- end
225
-
226
- instances
227
- end
228
-
229
- # Create multiple delegation instances in parallel using Async fibers
230
- #
231
- # @param instances_to_create [Array<Hash>] Array of instance configs
232
- # @param tool_configurator [ToolConfigurator] Shared tool configurator
233
- # @return [Array<Array>] Array of [instance_name, chat] tuples
234
- def create_delegation_instances_in_parallel(instances_to_create, tool_configurator)
235
- return [] if instances_to_create.empty?
236
-
237
- results = []
238
- errors = []
239
- mutex = Mutex.new
240
-
241
- Sync do
242
- barrier = Async::Barrier.new
243
-
244
- instances_to_create.each do |config|
245
- barrier.async do
246
- delegation_chat = create_agent_chat_for_delegation(
247
- instance_name: config[:instance_name],
248
- base_name: config[:base_name],
249
- agent_definition: config[:definition],
250
- tool_configurator: tool_configurator,
251
- )
252
- mutex.synchronize { results << [config[:instance_name], delegation_chat] }
253
- rescue StandardError => e
254
- # Catch errors to avoid Async warning logs (which fail in tests with StringIO)
255
- mutex.synchronize { errors << [config[:instance_name], e] }
256
- end
257
- end
258
-
259
- barrier.wait
260
- end
261
-
262
- # Re-raise first error if any occurred
263
- unless errors.empty?
264
- # Emit events for all errors (not just the first)
265
- errors.each do |inst_name, err|
266
- LogStream.emit(
267
- type: "delegation_instance_initialization_error",
268
- instance_name: inst_name,
269
- error_class: err.class.name,
270
- error_message: err.message,
271
- timestamp: Time.now.utc.iso8601,
272
- )
273
- end
274
-
275
- # Re-raise first error with context
276
- instance_name, error = errors.first
277
- raise error.class, "Delegation instance '#{instance_name}' initialization failed: #{error.message}", error.backtrace
278
- end
279
-
280
- results
281
- end
282
-
283
- # Pass 2: Create delegation instances and wire delegation tools
284
- #
285
- # This pass has three sub-steps that must happen in order:
286
- # 2a. Create delegation instances (ONLY for agents with shared_across_delegations: false)
287
- # 2b. Wire primary agents to delegation instances OR shared primaries
288
- # 2c. Wire delegation instances to their delegates (nested delegation support)
289
- #
290
- # Sub-pass 2a is parallelized using Async::Barrier for faster initialization.
198
+ # Sub-pass 2a (eager creation) is REMOVED - delegation instances are now lazy.
199
+ # Sub-pass 2c (nested delegation) is handled by LazyDelegateChat when initialized.
291
200
  def pass_2_register_delegation_tools
292
201
  tool_configurator = ToolConfigurator.new(@swarm, @swarm.scratchpad_storage, @swarm.plugin_storages)
293
202
 
294
- # Sub-pass 2a: Create delegation instances for isolated agents (parallelized)
295
- delegation_instances_to_create = collect_delegation_instances_to_create
296
-
297
- results = create_delegation_instances_in_parallel(delegation_instances_to_create, tool_configurator)
298
-
299
- # Store results after all parallel creation completes
300
- results.each do |instance_name, delegation_chat|
301
- @swarm.delegation_instances[instance_name] = delegation_chat
302
- end
303
-
304
- # Sub-pass 2b: Wire primary agents to delegation instances OR shared primaries OR registered swarms
203
+ # Wire primary agents to delegates (shared primaries or lazy loaders)
305
204
  @swarm.agent_definitions.each do |delegator_name, delegator_def|
306
205
  delegator_chat = @agents[delegator_name]
307
206
 
@@ -314,43 +213,25 @@ module SwarmSDK
314
213
  delegator_chat: delegator_chat,
315
214
  delegation_config: delegation_config,
316
215
  tool_configurator: tool_configurator,
317
- create_nested_instances: false,
318
216
  )
319
217
  end
320
218
  end
321
219
 
322
- # Sub-pass 2c: Wire delegation instances to their delegates (nested delegation)
323
- # Convert to array first to avoid "can't add key during iteration" error
324
- @swarm.delegation_instances.to_a.each do |instance_name, delegation_chat|
325
- base_name = extract_base_name(instance_name)
326
- delegate_definition = @swarm.agent_definitions[base_name]
327
-
328
- # Register delegation tools for THIS instance's delegates_to
329
- delegate_definition.delegation_configs.each do |delegation_config|
330
- wire_delegation(
331
- delegator_name: instance_name.to_sym,
332
- delegator_chat: delegation_chat,
333
- delegation_config: delegation_config,
334
- tool_configurator: tool_configurator,
335
- create_nested_instances: true,
336
- )
337
- end
338
- end
220
+ # NOTE: Nested delegation wiring is now handled by LazyDelegateChat#wire_delegation_tools
221
+ # when the lazy delegate is first accessed.
339
222
  end
340
223
 
341
- # Wire a single delegation from one agent/instance to a delegate
224
+ # Wire a single delegation from one agent to a delegate
342
225
  #
343
- # This is the unified logic for delegation wiring used by both:
344
- # - Sub-pass 2b: Primary agents → delegates
345
- # - Sub-pass 2c: Delegation instances → nested delegates
226
+ # For isolated delegates, creates a LazyDelegateChat wrapper instead of
227
+ # eagerly creating the chat instance.
346
228
  #
347
229
  # @param delegator_name [Symbol, String] Name of the agent doing the delegating
348
230
  # @param delegator_chat [Agent::Chat] Chat instance of the delegator
349
231
  # @param delegation_config [Hash] Delegation configuration with :agent, :tool_name, and :preserve_context keys
350
232
  # @param tool_configurator [ToolConfigurator] Tool configuration helper
351
- # @param create_nested_instances [Boolean] Whether to create new instances for nested delegation
352
233
  # @return [void]
353
- def wire_delegation(delegator_name:, delegator_chat:, delegation_config:, tool_configurator:, create_nested_instances:)
234
+ def wire_delegation(delegator_name:, delegator_chat:, delegation_config:, tool_configurator:)
354
235
  delegate_name_sym = delegation_config[:agent]
355
236
  delegate_name_str = delegate_name_sym.to_s
356
237
  custom_tool_name = delegation_config[:tool_name]
@@ -365,8 +246,6 @@ module SwarmSDK
365
246
  delegator_chat: delegator_chat,
366
247
  delegate_name_sym: delegate_name_sym,
367
248
  custom_tool_name: custom_tool_name,
368
- tool_configurator: tool_configurator,
369
- create_nested_instances: create_nested_instances,
370
249
  preserve_context: preserve_context,
371
250
  )
372
251
  else
@@ -404,18 +283,17 @@ module SwarmSDK
404
283
 
405
284
  # Wire delegation to a local agent
406
285
  #
407
- # Determines whether to use shared primary or isolated instance based on
408
- # the delegate's shared_across_delegations setting.
286
+ # For shared delegates, uses the primary agent instance.
287
+ # For isolated delegates, creates a LazyDelegateChat wrapper that
288
+ # defers creation until first use.
409
289
  #
410
290
  # @param delegator_name [Symbol, String] Name of the delegating agent
411
291
  # @param delegator_chat [Agent::Chat] Chat instance of the delegator
412
292
  # @param delegate_name_sym [Symbol] Name of the delegate agent
413
293
  # @param custom_tool_name [String, nil] Optional custom tool name
414
- # @param tool_configurator [ToolConfigurator] Tool configuration helper
415
- # @param create_nested_instances [Boolean] Whether to create new instances if not found
416
294
  # @param preserve_context [Boolean] Whether to preserve context between delegations
417
295
  # @return [void]
418
- def wire_agent_delegation(delegator_name:, delegator_chat:, delegate_name_sym:, custom_tool_name:, tool_configurator:, create_nested_instances:, preserve_context:)
296
+ def wire_agent_delegation(delegator_name:, delegator_chat:, delegate_name_sym:, custom_tool_name:, preserve_context:)
419
297
  delegate_definition = @swarm.agent_definitions[delegate_name_sym]
420
298
 
421
299
  # Determine which chat instance to use
@@ -423,24 +301,18 @@ module SwarmSDK
423
301
  # Shared mode: use primary agent (semaphore-protected)
424
302
  @agents[delegate_name_sym]
425
303
  else
426
- # Isolated mode: use delegation instance
304
+ # Isolated mode: use lazy loader (created on first delegation)
427
305
  instance_name = "#{delegate_name_sym}@#{delegator_name}"
428
306
 
429
- if create_nested_instances
430
- # For nested delegation: create if not exists
431
- @swarm.delegation_instances[instance_name] ||= create_agent_chat_for_delegation(
432
- instance_name: instance_name,
433
- base_name: delegate_name_sym,
434
- agent_definition: delegate_definition,
435
- tool_configurator: tool_configurator,
436
- )
437
- else
438
- # For primary delegation: instance was pre-created in 2a
439
- @swarm.delegation_instances[instance_name]
440
- end
307
+ LazyDelegateChat.new(
308
+ instance_name: instance_name,
309
+ base_name: delegate_name_sym,
310
+ agent_definition: delegate_definition,
311
+ swarm: @swarm,
312
+ )
441
313
  end
442
314
 
443
- # Create delegation tool pointing to chosen instance
315
+ # Create delegation tool pointing to chosen instance (or lazy loader)
444
316
  tool = create_delegation_tool(
445
317
  name: delegate_name_sym.to_s,
446
318
  description: delegate_definition.description,
@@ -467,18 +339,15 @@ module SwarmSDK
467
339
  #
468
340
  # Create Agent::Context for each agent to track delegations and metadata.
469
341
  # This is needed regardless of whether logging is enabled.
342
+ #
343
+ # NOTE: Delegation instances are now lazy-loaded, so their contexts are
344
+ # set up in LazyDelegateChat#setup_context when first accessed.
470
345
  def pass_3_setup_contexts
471
- # Setup contexts for PRIMARY agents
346
+ # Setup contexts for PRIMARY agents only
347
+ # (Delegation instances handle their own context setup via LazyDelegateChat)
472
348
  @agents.each do |agent_name, chat|
473
349
  setup_agent_context(agent_name, @swarm.agent_definitions[agent_name], chat, is_delegation: false)
474
350
  end
475
-
476
- # Setup contexts for DELEGATION instances
477
- @swarm.delegation_instances.each do |instance_name, chat|
478
- base_name = extract_base_name(instance_name)
479
- agent_definition = @swarm.agent_definitions[base_name]
480
- setup_agent_context(instance_name.to_sym, agent_definition, chat, is_delegation: true)
481
- end
482
351
  end
483
352
 
484
353
  # Setup context for an agent (primary or delegation instance)
@@ -541,16 +410,15 @@ module SwarmSDK
541
410
  # Pass 4: Configure hook system
542
411
  #
543
412
  # Setup the callback system for each agent, integrating with RubyLLM callbacks.
413
+ #
414
+ # NOTE: Delegation instances are now lazy-loaded, so their hooks are
415
+ # configured in LazyDelegateChat#configure_hooks when first accessed.
544
416
  def pass_4_configure_hooks
545
- # Configure hooks for PRIMARY agents
417
+ # Configure hooks for PRIMARY agents only
418
+ # (Delegation instances handle their own hook setup via LazyDelegateChat)
546
419
  @agents.each do |agent_name, chat|
547
420
  configure_hooks_for_agent(agent_name, chat)
548
421
  end
549
-
550
- # Configure hooks for DELEGATION instances
551
- @swarm.delegation_instances.each do |instance_name, chat|
552
- configure_hooks_for_agent(instance_name.to_sym, chat)
553
- end
554
422
  end
555
423
 
556
424
  # Configure hooks for an agent (primary or delegation instance)
@@ -569,18 +437,17 @@ module SwarmSDK
569
437
  #
570
438
  # If the swarm was loaded from YAML with agent-specific hooks,
571
439
  # apply them now via HooksAdapter.
440
+ #
441
+ # NOTE: Delegation instances are now lazy-loaded, so their YAML hooks are
442
+ # applied in LazyDelegateChat#apply_yaml_hooks when first accessed.
572
443
  def pass_5_apply_yaml_hooks
573
444
  return unless @swarm.config_for_hooks
574
445
 
575
- # Apply YAML hooks to PRIMARY agents
446
+ # Apply YAML hooks to PRIMARY agents only
447
+ # (Delegation instances handle their own YAML hooks via LazyDelegateChat)
576
448
  @agents.each do |agent_name, chat|
577
449
  apply_yaml_hooks_for_agent(agent_name, chat)
578
450
  end
579
-
580
- # Apply YAML hooks to DELEGATION instances
581
- @swarm.delegation_instances.each do |instance_name, chat|
582
- apply_yaml_hooks_for_agent(instance_name.to_sym, chat)
583
- end
584
451
  end
585
452
 
586
453
  # Apply YAML hooks for an agent (primary or delegation instance)
@@ -603,13 +470,14 @@ module SwarmSDK
603
470
  # - Tools must be activated AFTER all registration is complete
604
471
  # - This populates @llm_chat.tools from the registry
605
472
  #
473
+ # NOTE: Delegation instances are now lazy-loaded, so their tools are
474
+ # activated in LazyDelegateChat#initialize_chat when first accessed.
475
+ #
606
476
  # @return [void]
607
477
  def pass_6_activate_tools
608
- # Activate tools for PRIMARY agents
478
+ # Activate tools for PRIMARY agents only
479
+ # (Delegation instances handle their own tool activation via LazyDelegateChat)
609
480
  @agents.each_value(&:activate_tools_for_prompt)
610
-
611
- # Activate tools for DELEGATION instances
612
- @swarm.delegation_instances.each_value(&:activate_tools_for_prompt)
613
481
  end
614
482
 
615
483
  # Create Agent::Chat instance with rate limiting
@@ -653,103 +521,8 @@ module SwarmSDK
653
521
  chat
654
522
  end
655
523
 
656
- # Create a delegation-specific instance of an agent
657
- #
658
- # V7.0: Simplified - just calls register_all_tools with instance_name
659
- #
660
- # @param instance_name [String] Unique instance name ("base@delegator")
661
- # @param base_name [Symbol] Base agent name (for definition lookup)
662
- # @param agent_definition [Agent::Definition] Base agent definition
663
- # @param tool_configurator [ToolConfigurator] Shared tool configurator
664
- # @return [Agent::Chat] Delegation-specific chat instance
665
- def create_agent_chat_for_delegation(instance_name:, base_name:, agent_definition:, tool_configurator:)
666
- # Create chat with instance_name for isolated conversation + tool state
667
- chat = Agent::Chat.new(
668
- definition: agent_definition.to_h,
669
- agent_name: instance_name.to_sym, # Full instance name for isolation
670
- global_semaphore: @swarm.global_semaphore,
671
- )
672
-
673
- # Set provider agent name for logging
674
- chat.provider.agent_name = instance_name if chat.provider.respond_to?(:agent_name=)
675
-
676
- # V7.0 SIMPLIFIED: Just call register_all_tools with instance_name!
677
- # Base name extraction happens automatically in create_plugin_tool
678
- tool_configurator.register_all_tools(
679
- chat: chat,
680
- agent_name: instance_name.to_sym,
681
- agent_definition: agent_definition,
682
- )
683
-
684
- # Register MCP servers (tracked by instance_name automatically)
685
- if agent_definition.mcp_servers.any?
686
- mcp_configurator = McpConfigurator.new(@swarm)
687
- mcp_configurator.register_mcp_servers(
688
- chat,
689
- agent_definition.mcp_servers,
690
- agent_name: instance_name,
691
- )
692
- end
693
-
694
- # Setup tool activation dependencies (Plan 025)
695
- chat.setup_tool_activation(
696
- tool_configurator: tool_configurator,
697
- agent_definition: agent_definition,
698
- )
699
-
700
- # Notify plugins (use instance_name, plugins extract base_name if needed)
701
- notify_plugins_agent_initialized(instance_name.to_sym, chat, agent_definition, tool_configurator)
702
-
703
- # NOTE: activate_tools_for_prompt is called in Pass 6 after all plugins
704
-
705
- chat
706
- end
707
-
708
- # Register agent delegation tools
709
- #
710
- # Creates delegation tools that allow one agent to call another.
711
- #
712
- # @param chat [Agent::Chat] The chat instance
713
- # @param delegate_names [Array<Symbol>] Names of agents to delegate to
714
- # @param agent_name [Symbol] Name of the agent doing the delegating
715
- def register_delegation_tools(chat, delegate_names, agent_name:)
716
- return if delegate_names.empty?
717
-
718
- delegate_names.each do |delegate_name|
719
- delegate_name_sym = delegate_name.to_sym
720
- delegate_name_str = delegate_name.to_s
721
-
722
- # Check if target is a local agent
723
- if @agents.key?(delegate_name_sym)
724
- # Delegate to local agent
725
- delegate_agent = @agents[delegate_name_sym]
726
- delegate_definition = @swarm.agent_definitions[delegate_name_sym]
727
-
728
- tool = create_delegation_tool(
729
- name: delegate_name_str,
730
- description: delegate_definition.description,
731
- delegate_chat: delegate_agent,
732
- agent_name: agent_name,
733
- delegating_chat: chat,
734
- )
735
-
736
- chat.add_tool(tool)
737
- elsif @swarm.swarm_registry&.registered?(delegate_name_str)
738
- # Delegate to registered swarm
739
- tool = create_delegation_tool(
740
- name: delegate_name_str,
741
- description: "External swarm: #{delegate_name_str}",
742
- delegate_chat: nil, # Swarm delegation - no direct chat
743
- agent_name: agent_name,
744
- delegating_chat: chat,
745
- )
746
-
747
- chat.add_tool(tool)
748
- else
749
- raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{delegate_name_str}' (not a local agent or registered swarm)"
750
- end
751
- end
752
- end
524
+ # NOTE: create_agent_chat_for_delegation and register_delegation_tools were removed.
525
+ # Delegation instances are now lazy-loaded via LazyDelegateChat.
753
526
 
754
527
  # Create plugin storages for all agents
755
528
  #