swarm_memory 2.1.2 → 2.1.4

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
@@ -14,16 +14,11 @@ module SwarmSDK
14
14
  # This encapsulates the complex initialization logic that was previously
15
15
  # embedded in Swarm#initialize_agents.
16
16
  class AgentInitializer
17
- # rubocop:disable Metrics/ParameterLists
18
- def initialize(swarm, agent_definitions, global_semaphore, hook_registry, scratchpad_storage, plugin_storages, config_for_hooks: nil)
19
- # rubocop:enable Metrics/ParameterLists
17
+ # Initialize with swarm reference (all data accessible via swarm)
18
+ #
19
+ # @param swarm [Swarm] The parent swarm instance
20
+ def initialize(swarm)
20
21
  @swarm = swarm
21
- @agent_definitions = agent_definitions
22
- @global_semaphore = global_semaphore
23
- @hook_registry = hook_registry
24
- @scratchpad_storage = scratchpad_storage
25
- @plugin_storages = plugin_storages
26
- @config_for_hooks = config_for_hooks
27
22
  @agents = {}
28
23
  @agent_contexts = {}
29
24
  end
@@ -51,6 +46,39 @@ module SwarmSDK
51
46
  # Provide access to agent contexts for Swarm
52
47
  attr_reader :agent_contexts
53
48
 
49
+ # Initialize a single agent in isolation (for observer agents)
50
+ #
51
+ # Creates an isolated agent chat without delegation tools,
52
+ # suitable for observer agents that don't need to delegate.
53
+ # Reuses existing create_agent_chat infrastructure.
54
+ #
55
+ # @param agent_name [Symbol] Name of agent to initialize
56
+ # @return [Agent::Chat] Isolated agent chat instance
57
+ # @raise [ConfigurationError] If agent not found
58
+ #
59
+ # @example
60
+ # initializer = AgentInitializer.new(swarm)
61
+ # chat = initializer.initialize_isolated_agent(:profiler)
62
+ # chat.ask("Analyze this prompt")
63
+ def initialize_isolated_agent(agent_name)
64
+ agent_def = @swarm.agent_definitions[agent_name]
65
+ raise ConfigurationError, "Agent '#{agent_name}' not found" unless agent_def
66
+
67
+ # Ensure plugin storages are created (needed by ToolConfigurator)
68
+ create_plugin_storages if @swarm.plugin_storages.empty?
69
+
70
+ # Reuse existing create_agent_chat infrastructure
71
+ tool_configurator = ToolConfigurator.new(
72
+ @swarm,
73
+ @swarm.scratchpad_storage,
74
+ @swarm.plugin_storages,
75
+ )
76
+
77
+ # Create chat using same method as pass_1_create_agents
78
+ # This gives us full tool setup, MCP servers, etc.
79
+ create_agent_chat(agent_name, agent_def, tool_configurator)
80
+ end
81
+
54
82
  # Create a tool that delegates work to another agent
55
83
  #
56
84
  # This method is public for testing delegation from Swarm.
@@ -68,24 +96,31 @@ module SwarmSDK
68
96
  delegate_chat: delegate_chat,
69
97
  agent_name: agent_name,
70
98
  swarm: @swarm,
71
- hook_registry: @hook_registry,
72
99
  delegating_chat: delegating_chat,
73
100
  )
74
101
  end
75
102
 
76
103
  private
77
104
 
78
- # Pass 1: Create all agent chat instances
105
+ # Pass 1: Create primary agent chat instances
79
106
  #
80
- # This creates the Agent::Chat instances but doesn't wire them together yet.
81
- # Each agent gets its own chat instance with configured tools.
107
+ # Only creates agents that will actually be used as primaries:
108
+ # - The lead agent
109
+ # - Agents with shared_across_delegations: true (shared delegates)
110
+ # - Agents not used as delegates (standalone agents)
111
+ #
112
+ # Agents that are ONLY delegates with shared_across_delegations: false
113
+ # are NOT created here - they'll be created as delegation instances in pass 2a.
82
114
  def pass_1_create_agents
83
115
  # Create plugin storages for agents
84
116
  create_plugin_storages
85
117
 
86
- tool_configurator = ToolConfigurator.new(@swarm, @scratchpad_storage, @plugin_storages)
118
+ tool_configurator = ToolConfigurator.new(@swarm, @swarm.scratchpad_storage, @swarm.plugin_storages)
119
+
120
+ @swarm.agent_definitions.each do |name, agent_definition|
121
+ # Skip if this agent will only exist as delegation instances
122
+ next if should_skip_primary_creation?(name, agent_definition)
87
123
 
88
- @agent_definitions.each do |name, agent_definition|
89
124
  chat = create_agent_chat(name, agent_definition, tool_configurator)
90
125
  @agents[name] = chat
91
126
 
@@ -94,46 +129,228 @@ module SwarmSDK
94
129
  end
95
130
  end
96
131
 
97
- # Pass 2: Register agent delegation tools
132
+ # Pass 2: Create delegation instances and wire delegation tools
98
133
  #
99
- # Now that all agents exist, we can create delegation tools
100
- # that allow agents to call each other.
134
+ # This pass has three sub-steps that must happen in order:
135
+ # 2a. Create delegation instances (ONLY for agents with shared_across_delegations: false)
136
+ # 2b. Wire primary agents to delegation instances OR shared primaries
137
+ # 2c. Wire delegation instances to their delegates (nested delegation support)
101
138
  def pass_2_register_delegation_tools
102
- @agent_definitions.each do |name, agent_definition|
103
- register_delegation_tools(@agents[name], agent_definition.delegates_to, agent_name: name)
139
+ tool_configurator = ToolConfigurator.new(@swarm, @swarm.scratchpad_storage, @swarm.plugin_storages)
140
+
141
+ # Sub-pass 2a: Create delegation instances for isolated agents
142
+ @swarm.agent_definitions.each do |delegator_name, delegator_def|
143
+ delegator_def.delegates_to.each do |delegate_base_name|
144
+ delegate_base_name = delegate_base_name.to_sym
145
+
146
+ unless @swarm.agent_definitions.key?(delegate_base_name)
147
+ raise ConfigurationError,
148
+ "Agent '#{delegator_name}' delegates to unknown agent '#{delegate_base_name}'"
149
+ end
150
+
151
+ delegate_definition = @swarm.agent_definitions[delegate_base_name]
152
+
153
+ # Check isolation mode of the DELEGATE agent
154
+ # If delegate wants to be shared, skip instance creation (use primary)
155
+ next if delegate_definition.shared_across_delegations
156
+
157
+ # Create unique delegation instance (isolated mode)
158
+ instance_name = "#{delegate_base_name}@#{delegator_name}"
159
+
160
+ # V7.0: Use existing register_all_tools (no new method needed!)
161
+ delegation_chat = create_agent_chat_for_delegation(
162
+ instance_name: instance_name,
163
+ base_name: delegate_base_name,
164
+ agent_definition: delegate_definition,
165
+ tool_configurator: tool_configurator,
166
+ )
167
+
168
+ # Store in delegation_instances hash
169
+ @swarm.delegation_instances[instance_name] = delegation_chat
170
+ end
171
+ end
172
+
173
+ # Sub-pass 2b: Wire primary agents to delegation instances OR shared primaries OR registered swarms
174
+ @swarm.agent_definitions.each do |delegator_name, delegator_def|
175
+ delegator_chat = @agents[delegator_name]
176
+
177
+ # Skip if delegator doesn't exist as primary (wasn't created in pass_1)
178
+ next unless delegator_chat
179
+
180
+ delegator_def.delegates_to.each do |delegate_name|
181
+ wire_delegation(
182
+ delegator_name: delegator_name,
183
+ delegator_chat: delegator_chat,
184
+ delegate_name: delegate_name,
185
+ tool_configurator: tool_configurator,
186
+ create_nested_instances: false,
187
+ )
188
+ end
189
+ end
190
+
191
+ # Sub-pass 2c: Wire delegation instances to their delegates (nested delegation)
192
+ # Convert to array first to avoid "can't add key during iteration" error
193
+ @swarm.delegation_instances.to_a.each do |instance_name, delegation_chat|
194
+ base_name = extract_base_name(instance_name)
195
+ delegate_definition = @swarm.agent_definitions[base_name]
196
+
197
+ # Register delegation tools for THIS instance's delegates_to
198
+ delegate_definition.delegates_to.each do |nested_delegate_name|
199
+ wire_delegation(
200
+ delegator_name: instance_name.to_sym,
201
+ delegator_chat: delegation_chat,
202
+ delegate_name: nested_delegate_name,
203
+ tool_configurator: tool_configurator,
204
+ create_nested_instances: true,
205
+ )
206
+ end
104
207
  end
105
208
  end
106
209
 
210
+ # Wire a single delegation from one agent/instance to a delegate
211
+ #
212
+ # This is the unified logic for delegation wiring used by both:
213
+ # - Sub-pass 2b: Primary agents → delegates
214
+ # - Sub-pass 2c: Delegation instances → nested delegates
215
+ #
216
+ # @param delegator_name [Symbol, String] Name of the agent doing the delegating
217
+ # @param delegator_chat [Agent::Chat] Chat instance of the delegator
218
+ # @param delegate_name [Symbol, String] Name of the delegate target
219
+ # @param tool_configurator [ToolConfigurator] Tool configuration helper
220
+ # @param create_nested_instances [Boolean] Whether to create new instances for nested delegation
221
+ # @return [void]
222
+ def wire_delegation(delegator_name:, delegator_chat:, delegate_name:, tool_configurator:, create_nested_instances:)
223
+ delegate_name_str = delegate_name.to_s
224
+ delegate_name_sym = delegate_name.to_sym
225
+
226
+ # Check if target is a registered swarm
227
+ if @swarm.swarm_registry&.registered?(delegate_name_str)
228
+ wire_swarm_delegation(delegator_name, delegator_chat, delegate_name_str)
229
+ elsif @swarm.agent_definitions.key?(delegate_name_sym)
230
+ wire_agent_delegation(
231
+ delegator_name: delegator_name,
232
+ delegator_chat: delegator_chat,
233
+ delegate_name_sym: delegate_name_sym,
234
+ tool_configurator: tool_configurator,
235
+ create_nested_instances: create_nested_instances,
236
+ )
237
+ else
238
+ raise ConfigurationError,
239
+ "Agent '#{delegator_name}' delegates to unknown target '#{delegate_name_str}' (not a local agent or registered swarm)"
240
+ end
241
+ end
242
+
243
+ # Wire delegation to an external swarm
244
+ #
245
+ # @param delegator_name [Symbol, String] Name of the delegating agent
246
+ # @param delegator_chat [Agent::Chat] Chat instance of the delegator
247
+ # @param swarm_name [String] Name of the registered swarm
248
+ # @return [void]
249
+ def wire_swarm_delegation(delegator_name, delegator_chat, swarm_name)
250
+ tool = create_delegation_tool(
251
+ name: swarm_name,
252
+ description: "External swarm: #{swarm_name}",
253
+ delegate_chat: nil, # Swarm delegation - no direct chat
254
+ agent_name: delegator_name,
255
+ delegating_chat: delegator_chat,
256
+ )
257
+
258
+ delegator_chat.add_tool(tool)
259
+ end
260
+
261
+ # Wire delegation to a local agent
262
+ #
263
+ # Determines whether to use shared primary or isolated instance based on
264
+ # the delegate's shared_across_delegations setting.
265
+ #
266
+ # @param delegator_name [Symbol, String] Name of the delegating agent
267
+ # @param delegator_chat [Agent::Chat] Chat instance of the delegator
268
+ # @param delegate_name_sym [Symbol] Name of the delegate agent
269
+ # @param tool_configurator [ToolConfigurator] Tool configuration helper
270
+ # @param create_nested_instances [Boolean] Whether to create new instances if not found
271
+ # @return [void]
272
+ def wire_agent_delegation(delegator_name:, delegator_chat:, delegate_name_sym:, tool_configurator:, create_nested_instances:)
273
+ delegate_definition = @swarm.agent_definitions[delegate_name_sym]
274
+
275
+ # Determine which chat instance to use
276
+ target_chat = if delegate_definition.shared_across_delegations
277
+ # Shared mode: use primary agent (semaphore-protected)
278
+ @agents[delegate_name_sym]
279
+ else
280
+ # Isolated mode: use delegation instance
281
+ instance_name = "#{delegate_name_sym}@#{delegator_name}"
282
+
283
+ if create_nested_instances
284
+ # For nested delegation: create if not exists
285
+ @swarm.delegation_instances[instance_name] ||= create_agent_chat_for_delegation(
286
+ instance_name: instance_name,
287
+ base_name: delegate_name_sym,
288
+ agent_definition: delegate_definition,
289
+ tool_configurator: tool_configurator,
290
+ )
291
+ else
292
+ # For primary delegation: instance was pre-created in 2a
293
+ @swarm.delegation_instances[instance_name]
294
+ end
295
+ end
296
+
297
+ # Create delegation tool pointing to chosen instance
298
+ tool = create_delegation_tool(
299
+ name: delegate_name_sym.to_s,
300
+ description: delegate_definition.description,
301
+ delegate_chat: target_chat,
302
+ agent_name: delegator_name,
303
+ delegating_chat: delegator_chat,
304
+ )
305
+
306
+ delegator_chat.add_tool(tool)
307
+ end
308
+
107
309
  # Pass 3: Setup agent contexts
108
310
  #
109
311
  # Create Agent::Context for each agent to track delegations and metadata.
110
312
  # This is needed regardless of whether logging is enabled.
111
313
  def pass_3_setup_contexts
314
+ # Setup contexts for PRIMARY agents
112
315
  @agents.each do |agent_name, chat|
113
- agent_definition = @agent_definitions[agent_name]
114
- delegate_tool_names = agent_definition.delegates_to.map do |delegate_name|
115
- "DelegateTaskTo#{delegate_name.to_s.capitalize}"
116
- end
316
+ setup_agent_context(agent_name, @swarm.agent_definitions[agent_name], chat, is_delegation: false)
317
+ end
117
318
 
118
- # Create agent context
119
- context = Agent::Context.new(
120
- name: agent_name,
121
- delegation_tools: delegate_tool_names,
122
- metadata: {},
123
- )
124
- @agent_contexts[agent_name] = context
319
+ # Setup contexts for DELEGATION instances
320
+ @swarm.delegation_instances.each do |instance_name, chat|
321
+ base_name = extract_base_name(instance_name)
322
+ agent_definition = @swarm.agent_definitions[base_name]
323
+ setup_agent_context(instance_name.to_sym, agent_definition, chat, is_delegation: true)
324
+ end
325
+ end
326
+
327
+ # Setup context for an agent (primary or delegation instance)
328
+ def setup_agent_context(agent_name, agent_definition, chat, is_delegation: false)
329
+ delegate_tool_names = agent_definition.delegates_to.map do |delegate_name|
330
+ Tools::Delegate.tool_name_for(delegate_name)
331
+ end
125
332
 
126
- # Always set agent context (needed for delegation tracking)
127
- chat.setup_context(context) if chat.respond_to?(:setup_context)
333
+ context = Agent::Context.new(
334
+ name: agent_name,
335
+ swarm_id: @swarm.swarm_id,
336
+ parent_swarm_id: @swarm.parent_swarm_id,
337
+ delegation_tools: delegate_tool_names,
338
+ metadata: { is_delegation_instance: is_delegation },
339
+ )
128
340
 
129
- # Configure logging callbacks if logging is enabled
130
- next unless LogStream.emitter
341
+ # Store context (only for primaries)
342
+ @agent_contexts[agent_name] = context unless is_delegation
131
343
 
132
- chat.setup_logging if chat.respond_to?(:setup_logging)
344
+ # Always set agent context on chat
345
+ chat.setup_context(context) if chat.respond_to?(:setup_context)
133
346
 
134
- # Emit validation warnings for this agent
135
- emit_validation_warnings(agent_name, agent_definition)
136
- end
347
+ # Configure logging if enabled
348
+ return unless LogStream.emitter
349
+
350
+ chat.setup_logging if chat.respond_to?(:setup_logging)
351
+
352
+ # Emit validation warnings (only for primaries, not each delegation instance)
353
+ emit_validation_warnings(agent_name, agent_definition) unless is_delegation
137
354
  end
138
355
 
139
356
  # Emit validation warnings as log events
@@ -166,34 +383,60 @@ module SwarmSDK
166
383
  #
167
384
  # Setup the callback system for each agent, integrating with RubyLLM callbacks.
168
385
  def pass_4_configure_hooks
386
+ # Configure hooks for PRIMARY agents
169
387
  @agents.each do |agent_name, chat|
170
- agent_definition = @agent_definitions[agent_name]
388
+ configure_hooks_for_agent(agent_name, chat)
389
+ end
171
390
 
172
- # Configure callback system (integrates with RubyLLM callbacks)
173
- chat.setup_hooks(
174
- registry: @hook_registry,
175
- agent_definition: agent_definition,
176
- swarm: @swarm,
177
- ) if chat.respond_to?(:setup_hooks)
391
+ # Configure hooks for DELEGATION instances
392
+ @swarm.delegation_instances.each do |instance_name, chat|
393
+ configure_hooks_for_agent(instance_name.to_sym, chat)
178
394
  end
179
395
  end
180
396
 
397
+ # Configure hooks for an agent (primary or delegation instance)
398
+ def configure_hooks_for_agent(agent_name, chat)
399
+ base_name = extract_base_name(agent_name)
400
+ agent_definition = @swarm.agent_definitions[base_name]
401
+
402
+ chat.setup_hooks(
403
+ registry: @swarm.hook_registry,
404
+ agent_definition: agent_definition,
405
+ swarm: @swarm,
406
+ ) if chat.respond_to?(:setup_hooks)
407
+ end
408
+
181
409
  # Pass 5: Apply YAML hooks
182
410
  #
183
411
  # If the swarm was loaded from YAML with agent-specific hooks,
184
412
  # apply them now via HooksAdapter.
185
413
  def pass_5_apply_yaml_hooks
186
- return unless @config_for_hooks
414
+ return unless @swarm.config_for_hooks
187
415
 
416
+ # Apply YAML hooks to PRIMARY agents
188
417
  @agents.each do |agent_name, chat|
189
- agent_def = @config_for_hooks.agents[agent_name]
190
- next unless agent_def&.hooks
418
+ apply_yaml_hooks_for_agent(agent_name, chat)
419
+ end
191
420
 
192
- # Apply agent-specific hooks via Hooks::Adapter
193
- Hooks::Adapter.apply_agent_hooks(chat, agent_name, agent_def.hooks, @swarm.name)
421
+ # Apply YAML hooks to DELEGATION instances
422
+ @swarm.delegation_instances.each do |instance_name, chat|
423
+ apply_yaml_hooks_for_agent(instance_name.to_sym, chat)
194
424
  end
195
425
  end
196
426
 
427
+ # Apply YAML hooks for an agent (primary or delegation instance)
428
+ def apply_yaml_hooks_for_agent(agent_name, chat)
429
+ base_name = extract_base_name(agent_name)
430
+ agent_config = @swarm.config_for_hooks.agents[base_name]
431
+ return unless agent_config
432
+
433
+ # Configuration.agents now returns hashes, not Definitions
434
+ hooks = agent_config.is_a?(Hash) ? agent_config[:hooks] : agent_config.hooks
435
+ return unless hooks&.any?
436
+
437
+ Hooks::Adapter.apply_agent_hooks(chat, agent_name, hooks, @swarm.name)
438
+ end
439
+
197
440
  # Create Agent::Chat instance with rate limiting
198
441
  #
199
442
  # @param agent_name [Symbol] Agent name
@@ -204,7 +447,7 @@ module SwarmSDK
204
447
  chat = Agent::Chat.new(
205
448
  definition: agent_definition.to_h,
206
449
  agent_name: agent_name,
207
- global_semaphore: @global_semaphore,
450
+ global_semaphore: @swarm.global_semaphore,
208
451
  )
209
452
 
210
453
  # Set agent name on provider for logging (if provider supports it)
@@ -226,6 +469,50 @@ module SwarmSDK
226
469
  chat
227
470
  end
228
471
 
472
+ # Create a delegation-specific instance of an agent
473
+ #
474
+ # V7.0: Simplified - just calls register_all_tools with instance_name
475
+ #
476
+ # @param instance_name [String] Unique instance name ("base@delegator")
477
+ # @param base_name [Symbol] Base agent name (for definition lookup)
478
+ # @param agent_definition [Agent::Definition] Base agent definition
479
+ # @param tool_configurator [ToolConfigurator] Shared tool configurator
480
+ # @return [Agent::Chat] Delegation-specific chat instance
481
+ def create_agent_chat_for_delegation(instance_name:, base_name:, agent_definition:, tool_configurator:)
482
+ # Create chat with instance_name for isolated conversation + tool state
483
+ chat = Agent::Chat.new(
484
+ definition: agent_definition.to_h,
485
+ agent_name: instance_name.to_sym, # Full instance name for isolation
486
+ global_semaphore: @swarm.global_semaphore,
487
+ )
488
+
489
+ # Set provider agent name for logging
490
+ chat.provider.agent_name = instance_name if chat.provider.respond_to?(:agent_name=)
491
+
492
+ # V7.0 SIMPLIFIED: Just call register_all_tools with instance_name!
493
+ # Base name extraction happens automatically in create_plugin_tool
494
+ tool_configurator.register_all_tools(
495
+ chat: chat,
496
+ agent_name: instance_name.to_sym,
497
+ agent_definition: agent_definition,
498
+ )
499
+
500
+ # Register MCP servers (tracked by instance_name automatically)
501
+ if agent_definition.mcp_servers.any?
502
+ mcp_configurator = McpConfigurator.new(@swarm)
503
+ mcp_configurator.register_mcp_servers(
504
+ chat,
505
+ agent_definition.mcp_servers,
506
+ agent_name: instance_name,
507
+ )
508
+ end
509
+
510
+ # Notify plugins (use instance_name, plugins extract base_name if needed)
511
+ notify_plugins_agent_initialized(instance_name.to_sym, chat, agent_definition, tool_configurator)
512
+
513
+ chat
514
+ end
515
+
229
516
  # Register agent delegation tools
230
517
  #
231
518
  # Creates delegation tools that allow one agent to call another.
@@ -237,25 +524,38 @@ module SwarmSDK
237
524
  return if delegate_names.empty?
238
525
 
239
526
  delegate_names.each do |delegate_name|
240
- delegate_name = delegate_name.to_sym
241
-
242
- unless @agents.key?(delegate_name)
243
- raise ConfigurationError, "Agent delegates to unknown agent '#{delegate_name}'"
244
- end
245
-
246
- # Create a tool that delegates to the specified agent
247
- delegate_agent = @agents[delegate_name]
248
- delegate_definition = @agent_definitions[delegate_name]
527
+ delegate_name_sym = delegate_name.to_sym
528
+ delegate_name_str = delegate_name.to_s
529
+
530
+ # Check if target is a local agent
531
+ if @agents.key?(delegate_name_sym)
532
+ # Delegate to local agent
533
+ delegate_agent = @agents[delegate_name_sym]
534
+ delegate_definition = @swarm.agent_definitions[delegate_name_sym]
535
+
536
+ tool = create_delegation_tool(
537
+ name: delegate_name_str,
538
+ description: delegate_definition.description,
539
+ delegate_chat: delegate_agent,
540
+ agent_name: agent_name,
541
+ delegating_chat: chat,
542
+ )
249
543
 
250
- tool = create_delegation_tool(
251
- name: delegate_name.to_s,
252
- description: delegate_definition.description,
253
- delegate_chat: delegate_agent,
254
- agent_name: agent_name,
255
- delegating_chat: chat,
256
- )
544
+ chat.add_tool(tool)
545
+ elsif @swarm.swarm_registry&.registered?(delegate_name_str)
546
+ # Delegate to registered swarm
547
+ tool = create_delegation_tool(
548
+ name: delegate_name_str,
549
+ description: "External swarm: #{delegate_name_str}",
550
+ delegate_chat: nil, # Swarm delegation - no direct chat
551
+ agent_name: agent_name,
552
+ delegating_chat: chat,
553
+ )
257
554
 
258
- chat.with_tool(tool)
555
+ chat.add_tool(tool)
556
+ else
557
+ raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{delegate_name_str}' (not a local agent or registered swarm)"
558
+ end
259
559
  end
260
560
  end
261
561
 
@@ -267,7 +567,7 @@ module SwarmSDK
267
567
  # @return [void]
268
568
  def create_plugin_storages
269
569
  PluginRegistry.all.each do |plugin|
270
- @agent_definitions.each do |agent_name, agent_definition|
570
+ @swarm.agent_definitions.each do |agent_name, agent_definition|
271
571
  # Check if this plugin needs storage for this agent
272
572
  next unless plugin.storage_enabled?(agent_definition)
273
573
 
@@ -282,25 +582,23 @@ module SwarmSDK
282
582
  storage = plugin.create_storage(agent_name: agent_name, config: parsed_config)
283
583
 
284
584
  # Store in plugin_storages: { plugin_name => { agent_name => storage } }
285
- @plugin_storages[plugin.name] ||= {}
286
- @plugin_storages[plugin.name][agent_name] = storage
585
+ @swarm.plugin_storages[plugin.name] ||= {}
586
+ @swarm.plugin_storages[plugin.name][agent_name] = storage
287
587
  end
288
588
  end
289
589
  end
290
590
 
291
591
  # Get plugin-specific config from agent definition
292
592
  #
293
- # Plugins can store their config in agent definition under their plugin name.
294
- # E.g., memory plugin looks for `agent_definition.memory`
593
+ # Uses the generic plugin_configs accessor to retrieve plugin-specific config.
594
+ # E.g., memory plugin config is accessed via `agent_definition.plugin_config(:memory)`
295
595
  #
296
596
  # @param agent_definition [Agent::Definition] Agent definition
297
597
  # @param plugin_name [Symbol] Plugin name
298
598
  # @return [Object, nil] Plugin config or nil
299
599
  def get_plugin_config(agent_definition, plugin_name)
300
- # Try to call method named after plugin (e.g., .memory for :memory plugin)
301
- if agent_definition.respond_to?(plugin_name)
302
- agent_definition.public_send(plugin_name)
303
- end
600
+ # Use generic plugin config accessor
601
+ agent_definition.plugin_config(plugin_name)
304
602
  end
305
603
 
306
604
  # Notify all plugins that an agent was initialized
@@ -315,7 +613,7 @@ module SwarmSDK
315
613
  def notify_plugins_agent_initialized(agent_name, chat, agent_definition, tool_configurator)
316
614
  PluginRegistry.all.each do |plugin|
317
615
  # Get plugin storage for this agent (if any)
318
- plugin_storages = @plugin_storages[plugin.name] || {}
616
+ plugin_storages = @swarm.plugin_storages[plugin.name] || {}
319
617
  storage = plugin_storages[agent_name]
320
618
 
321
619
  # Build context for plugin
@@ -329,6 +627,57 @@ module SwarmSDK
329
627
  plugin.on_agent_initialized(agent_name: agent_name, agent: chat, context: context)
330
628
  end
331
629
  end
630
+
631
+ # Determine if we should skip creating a primary agent
632
+ #
633
+ # Skip if:
634
+ # - NOT the lead agent, AND
635
+ # - Has shared_across_delegations: false (isolated mode), AND
636
+ # - Is only referenced as a delegate (not used standalone)
637
+ #
638
+ # @param name [Symbol] Agent name
639
+ # @param agent_definition [Agent::Definition] Agent definition
640
+ # @return [Boolean] True if should skip primary creation
641
+ def should_skip_primary_creation?(name, agent_definition)
642
+ # Always create lead agent
643
+ return false if name == @swarm.lead_agent
644
+
645
+ # If shared mode, create primary (delegates will use it)
646
+ return false if agent_definition.shared_across_delegations
647
+
648
+ # Skip if only used as a delegate
649
+ only_referenced_as_delegate?(name)
650
+ end
651
+
652
+ # Check if an agent is only referenced as a delegate
653
+ #
654
+ # @param name [Symbol] Agent name
655
+ # @return [Boolean] True if only referenced as delegate
656
+ def only_referenced_as_delegate?(name)
657
+ # Check if any agent delegates to this one
658
+ referenced_as_delegate = @swarm.agent_definitions.any? do |_agent_name, definition|
659
+ definition.delegates_to.include?(name)
660
+ end
661
+
662
+ # Skip if referenced as delegate (and not lead, already checked above)
663
+ referenced_as_delegate
664
+ end
665
+
666
+ # Extract base agent name from instance name
667
+ #
668
+ # @param instance_name [Symbol, String] Instance name (may be delegation instance)
669
+ # @return [Symbol] Base agent name
670
+ def extract_base_name(instance_name)
671
+ instance_name.to_s.split("@").first.to_sym
672
+ end
673
+
674
+ # Check if instance name is a delegation instance
675
+ #
676
+ # @param instance_name [Symbol, String] Instance name
677
+ # @return [Boolean] True if delegation instance (contains '@')
678
+ def delegation_instance?(instance_name)
679
+ instance_name.to_s.include?("@")
680
+ end
332
681
  end
333
682
  end
334
683
  end