swarm_sdk 2.0.0.pre.2 → 2.0.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.
@@ -3,17 +3,77 @@
3
3
  module SwarmSDK
4
4
  class Swarm
5
5
  # AllAgentsBuilder for configuring settings that apply to all agents
6
+ #
7
+ # Settings configured here are applied to ALL agents, but can be overridden
8
+ # at the agent level. This is useful for shared configuration like:
9
+ # - Common provider/base_url (all agents use same proxy)
10
+ # - Shared timeout settings
11
+ # - Global permissions
12
+ #
13
+ # @example
14
+ # all_agents do
15
+ # provider :openai
16
+ # base_url "http://proxy.com/v1"
17
+ # timeout 180
18
+ # tools :Read, :Write
19
+ # coding_agent false
20
+ # end
6
21
  class AllAgentsBuilder
7
- attr_reader :hooks, :permissions_config
22
+ attr_reader :hooks, :permissions_config, :tools_list
8
23
 
9
24
  def initialize
10
25
  @tools_list = []
11
26
  @hooks = []
12
27
  @permissions_config = {}
28
+ @model = nil
29
+ @provider = nil
30
+ @base_url = nil
31
+ @api_version = nil
32
+ @timeout = nil
33
+ @parameters = nil
34
+ @headers = nil
35
+ @coding_agent = nil
13
36
  end
14
37
 
15
- # Get tools list
16
- attr_reader :tools_list
38
+ # Set model for all agents
39
+ def model(model_name)
40
+ @model = model_name
41
+ end
42
+
43
+ # Set provider for all agents
44
+ def provider(provider_name)
45
+ @provider = provider_name
46
+ end
47
+
48
+ # Set base URL for all agents
49
+ def base_url(url)
50
+ @base_url = url
51
+ end
52
+
53
+ # Set API version for all agents
54
+ def api_version(version)
55
+ @api_version = version
56
+ end
57
+
58
+ # Set timeout for all agents
59
+ def timeout(seconds)
60
+ @timeout = seconds
61
+ end
62
+
63
+ # Set parameters for all agents
64
+ def parameters(params)
65
+ @parameters = params
66
+ end
67
+
68
+ # Set headers for all agents
69
+ def headers(header_hash)
70
+ @headers = header_hash
71
+ end
72
+
73
+ # Set coding_agent flag for all agents
74
+ def coding_agent(enabled)
75
+ @coding_agent = enabled
76
+ end
17
77
 
18
78
  # Add tools that all agents will have
19
79
  def tools(*tool_names)
@@ -57,6 +117,24 @@ module SwarmSDK
57
117
  def permissions(&block)
58
118
  @permissions_config = PermissionsBuilder.build(&block)
59
119
  end
120
+
121
+ # Convert to hash for merging with agent configs
122
+ #
123
+ # @return [Hash] Configuration hash
124
+ def to_h
125
+ {
126
+ model: @model,
127
+ provider: @provider,
128
+ base_url: @base_url,
129
+ api_version: @api_version,
130
+ timeout: @timeout,
131
+ parameters: @parameters,
132
+ headers: @headers,
133
+ coding_agent: @coding_agent,
134
+ tools: @tools_list,
135
+ permissions: @permissions_config,
136
+ }.compact
137
+ end
60
138
  end
61
139
  end
62
140
  end
@@ -50,6 +50,8 @@ module SwarmSDK
50
50
  @agents = {}
51
51
  @all_agents_config = nil
52
52
  @swarm_hooks = []
53
+ @nodes = {}
54
+ @start_node = nil
53
55
  end
54
56
 
55
57
  # Set swarm name
@@ -62,22 +64,47 @@ module SwarmSDK
62
64
  @lead_agent = agent_name
63
65
  end
64
66
 
65
- # Define an agent with fluent API
67
+ # Define an agent with fluent API or load from markdown content
66
68
  #
67
- # @example
69
+ # Supports two forms:
70
+ # 1. Inline DSL: agent :name do ... end
71
+ # 2. Markdown content: agent :name, <<~MD ... MD
72
+ #
73
+ # The name parameter is always required. If the markdown has a name field
74
+ # in frontmatter, it will be replaced by the name parameter.
75
+ #
76
+ # @example Inline DSL
68
77
  # agent :backend do
69
78
  # model "gpt-5"
70
- # prompt "You build APIs"
79
+ # system_prompt "You build APIs"
71
80
  # tools :Read, :Write
72
81
  #
73
82
  # hook :pre_tool_use, matcher: "Bash" do |ctx|
74
83
  # # Inline validation logic!
75
84
  # end
76
85
  # end
77
- def agent(name, &block)
78
- builder = Agent::Builder.new(name)
79
- builder.instance_eval(&block)
80
- @agents[name] = builder
86
+ #
87
+ # @example Markdown content
88
+ # agent :backend, <<~MD
89
+ # ---
90
+ # description: "Backend developer"
91
+ # model: "gpt-4"
92
+ # ---
93
+ #
94
+ # You build APIs.
95
+ # MD
96
+ def agent(name, content = nil, &block)
97
+ # Case 1: agent :name, <<~MD (name + markdown content)
98
+ if content.is_a?(String) && !block_given? && markdown_content?(content)
99
+ load_agent_from_markdown(content, name)
100
+ # Case 2: agent :name do ... end (inline DSL)
101
+ elsif block_given?
102
+ builder = Agent::Builder.new(name)
103
+ builder.instance_eval(&block)
104
+ @agents[name] = builder
105
+ else
106
+ raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD"
107
+ end
81
108
  end
82
109
 
83
110
  # Add swarm-level hook (swarm_start, swarm_stop only)
@@ -114,21 +141,111 @@ module SwarmSDK
114
141
  @all_agents_config = builder
115
142
  end
116
143
 
117
- # Build the actual Swarm instance
144
+ # Define a node (mini-swarm execution stage)
145
+ #
146
+ # Nodes enable multi-stage workflows where different agent teams
147
+ # collaborate in sequence. Each node is an independent swarm execution.
148
+ #
149
+ # @param name [Symbol] Node name
150
+ # @yield Block for node configuration
151
+ # @return [void]
152
+ #
153
+ # @example Solo agent node
154
+ # node :planning do
155
+ # agent(:architect)
156
+ # end
157
+ #
158
+ # @example Multi-agent node with delegation
159
+ # node :implementation do
160
+ # agent(:backend).delegates_to(:tester, :database)
161
+ # agent(:tester).delegates_to(:database)
162
+ # agent(:database)
163
+ # after :planning
164
+ # end
165
+ def node(name, &block)
166
+ builder = Node::Builder.new(name)
167
+ builder.instance_eval(&block)
168
+ @nodes[name] = builder
169
+ end
170
+
171
+ # Set the starting node for workflow execution
172
+ #
173
+ # Required when nodes are defined. Specifies which node to execute first.
174
+ #
175
+ # @param name [Symbol] Name of starting node
176
+ # @return [void]
177
+ #
178
+ # @example
179
+ # start_node :planning
180
+ def start_node(name)
181
+ @start_node = name.to_sym
182
+ end
183
+
184
+ # Build the actual Swarm instance or NodeOrchestrator
118
185
  def build_swarm
119
186
  raise ConfigurationError, "Swarm name not set. Use: name 'My Swarm'" unless @swarm_name
120
- raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
121
- raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
122
187
 
188
+ # Check if nodes are defined
189
+ if @nodes.any?
190
+ # Node-based workflow (agents optional for agent-less workflows)
191
+ build_node_orchestrator
192
+ else
193
+ # Traditional single-swarm execution (requires agents and lead)
194
+ raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
195
+ raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
196
+
197
+ build_single_swarm
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ # Check if a string is markdown content (has frontmatter)
204
+ #
205
+ # @param str [String] String to check
206
+ # @return [Boolean] true if string contains markdown frontmatter
207
+ def markdown_content?(str)
208
+ str.start_with?("---") || str.include?("\n---\n")
209
+ end
210
+
211
+ # Load an agent from markdown content
212
+ #
213
+ # Returns a hash of the agent config (not a Definition yet) so that
214
+ # all_agents config can be applied later in the build process.
215
+ #
216
+ # @param content [String] Markdown content with frontmatter
217
+ # @param name_override [Symbol, nil] Optional name to override frontmatter name
218
+ # @return [void]
219
+ def load_agent_from_markdown(content, name_override = nil)
220
+ # Parse markdown content - will extract name from frontmatter if not overridden
221
+ definition = MarkdownParser.parse(content, name_override)
222
+
223
+ # Store the config hash (not Definition) so all_agents can be applied
224
+ # We'll wrap this in a special marker so we know it came from markdown
225
+ @agents[definition.name] = { __file_config__: definition.to_h }
226
+ end
227
+
228
+ # Build a traditional single-swarm execution
229
+ #
230
+ # @return [Swarm] Configured swarm instance
231
+ def build_single_swarm
123
232
  # Create swarm using SDK
124
233
  swarm = Swarm.new(name: @swarm_name)
125
234
 
126
- # Merge all_agents config into each agent
235
+ # Merge all_agents config into each agent (including file-loaded ones)
127
236
  merge_all_agents_config_into_agents if @all_agents_config
128
237
 
129
238
  # Build definitions and add to swarm
130
- @agents.each do |_agent_name, agent_builder|
131
- definition = agent_builder.to_definition
239
+ # Handle both Agent::Builder (inline DSL) and file configs (from files)
240
+ @agents.each do |agent_name, agent_builder_or_config|
241
+ definition = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
242
+ # File-loaded agent config (with all_agents merged)
243
+ Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
244
+ else
245
+ # Builder object (from inline DSL) - convert to definition
246
+ agent_builder_or_config.to_definition
247
+ end
248
+
132
249
  swarm.add_agent(definition)
133
250
  end
134
251
 
@@ -154,24 +271,172 @@ module SwarmSDK
154
271
  swarm
155
272
  end
156
273
 
157
- private
274
+ # Build a node-based workflow orchestrator
275
+ #
276
+ # @return [NodeOrchestrator] Configured orchestrator
277
+ def build_node_orchestrator
278
+ raise ConfigurationError, "start_node required when nodes are defined. Use: start_node :name" unless @start_node
279
+
280
+ # Merge all_agents config into each agent (applies to all nodes)
281
+ merge_all_agents_config_into_agents if @all_agents_config
282
+
283
+ # Build agent definitions
284
+ # Handle both Agent::Builder (inline DSL) and file configs (from files)
285
+ agent_definitions = {}
286
+ @agents.each do |agent_name, agent_builder_or_config|
287
+ agent_definitions[agent_name] = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
288
+ # File-loaded agent config (with all_agents merged)
289
+ Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
290
+ else
291
+ # Builder object (from inline DSL) - convert to definition
292
+ agent_builder_or_config.to_definition
293
+ end
294
+ end
295
+
296
+ # Create node orchestrator
297
+ NodeOrchestrator.new(
298
+ swarm_name: @swarm_name,
299
+ agent_definitions: agent_definitions,
300
+ nodes: @nodes,
301
+ start_node: @start_node,
302
+ )
303
+ end
158
304
 
159
305
  # Merge all_agents configuration into each agent
306
+ #
307
+ # All_agents values are used as defaults - agent-specific values override.
308
+ # This applies to both inline DSL agents (Builder) and file-loaded agents (config hash).
309
+ #
310
+ # @return [void]
160
311
  def merge_all_agents_config_into_agents
161
312
  return unless @all_agents_config
162
313
 
163
- @agents.each_value do |agent_builder|
164
- # Merge tools (prepend all_agents tools)
165
- all_agents_tools = @all_agents_config.tools_list
166
- agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
314
+ all_agents_hash = @all_agents_config.to_h
315
+
316
+ @agents.each_value do |agent_builder_or_config|
317
+ if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
318
+ # File-loaded agent - merge into the config hash
319
+ file_config = agent_builder_or_config[:__file_config__]
320
+
321
+ # Merge all_agents into file config (file config overrides)
322
+ # Use same merge strategy as Configuration class
323
+ merged_config = merge_all_agents_into_config(all_agents_hash, file_config)
167
324
 
168
- # Pass all_agents permissions as default_permissions
169
- if @all_agents_config.permissions_config.any?
170
- agent_builder.default_permissions = @all_agents_config.permissions_config
325
+ # Update the stored config
326
+ agent_builder_or_config[:__file_config__] = merged_config
327
+ else
328
+ # Builder object (inline DSL agent)
329
+ agent_builder = agent_builder_or_config
330
+
331
+ # Apply all_agents defaults that haven't been set at agent level
332
+ # Agent values override all_agents values
333
+ apply_all_agents_defaults(agent_builder, all_agents_hash)
334
+
335
+ # Merge tools (prepend all_agents tools)
336
+ all_agents_tools = @all_agents_config.tools_list
337
+ agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
338
+
339
+ # Pass all_agents permissions as default_permissions
340
+ if @all_agents_config.permissions_config.any?
341
+ agent_builder.default_permissions = @all_agents_config.permissions_config
342
+ end
171
343
  end
172
344
  end
173
345
  end
174
346
 
347
+ # Merge all_agents config into file-loaded agent config
348
+ #
349
+ # Follows same merge strategy as Configuration class:
350
+ # - Arrays (tools, delegates_to): Concatenate (all_agents + file)
351
+ # - Hashes (parameters, headers): Merge (file values override)
352
+ # - Scalars (model, provider, etc.): File overrides
353
+ #
354
+ # @param all_agents_hash [Hash] All_agents configuration
355
+ # @param file_config [Hash] File-loaded agent configuration
356
+ # @return [Hash] Merged configuration
357
+ def merge_all_agents_into_config(all_agents_hash, file_config)
358
+ merged = all_agents_hash.dup
359
+
360
+ file_config.each do |key, value|
361
+ case key
362
+ when :tools
363
+ # Concatenate tools: all_agents.tools + file.tools
364
+ merged[:tools] = Array(merged[:tools]) + Array(value)
365
+ when :delegates_to
366
+ # Concatenate delegates_to
367
+ merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
368
+ when :parameters
369
+ # Merge parameters: file values override all_agents
370
+ merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
371
+ when :headers
372
+ # Merge headers: file values override all_agents
373
+ merged[:headers] = (merged[:headers] || {}).merge(value || {})
374
+ else
375
+ # For everything else, file value overrides all_agents value
376
+ merged[key] = value
377
+ end
378
+ end
379
+
380
+ # Pass all_agents permissions as default_permissions
381
+ if all_agents_hash[:permissions] && !merged[:default_permissions]
382
+ merged[:default_permissions] = all_agents_hash[:permissions]
383
+ end
384
+
385
+ merged
386
+ end
387
+
388
+ # Apply all_agents defaults to an agent builder
389
+ #
390
+ # Only sets values that haven't been explicitly set at the agent level.
391
+ # This implements the override semantics: agent values take precedence.
392
+ #
393
+ # @param agent_builder [Agent::Builder] The agent builder to configure
394
+ # @param all_agents_hash [Hash] All_agents configuration
395
+ # @return [void]
396
+ def apply_all_agents_defaults(agent_builder, all_agents_hash)
397
+ # Model: only set if agent hasn't explicitly set it
398
+ if all_agents_hash[:model] && !agent_builder.model_set?
399
+ agent_builder.model(all_agents_hash[:model])
400
+ end
401
+
402
+ # Provider: only set if agent hasn't set it
403
+ if all_agents_hash[:provider] && !agent_builder.provider_set?
404
+ agent_builder.provider(all_agents_hash[:provider])
405
+ end
406
+
407
+ # Base URL: only set if agent hasn't set it
408
+ if all_agents_hash[:base_url] && !agent_builder.base_url_set?
409
+ agent_builder.base_url(all_agents_hash[:base_url])
410
+ end
411
+
412
+ # API Version: only set if agent hasn't set it
413
+ if all_agents_hash[:api_version] && !agent_builder.api_version_set?
414
+ agent_builder.api_version(all_agents_hash[:api_version])
415
+ end
416
+
417
+ # Timeout: only set if agent hasn't set it
418
+ if all_agents_hash[:timeout] && !agent_builder.timeout_set?
419
+ agent_builder.timeout(all_agents_hash[:timeout])
420
+ end
421
+
422
+ # Parameters: merge (all_agents + agent, agent values override)
423
+ if all_agents_hash[:parameters]
424
+ merged_params = all_agents_hash[:parameters].merge(agent_builder.parameters)
425
+ agent_builder.parameters(merged_params)
426
+ end
427
+
428
+ # Headers: merge (all_agents + agent, agent values override)
429
+ if all_agents_hash[:headers]
430
+ merged_headers = all_agents_hash[:headers].merge(agent_builder.headers)
431
+ agent_builder.headers(merged_headers)
432
+ end
433
+
434
+ # Coding_agent: only set if agent hasn't set it
435
+ if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
436
+ agent_builder.coding_agent(all_agents_hash[:coding_agent])
437
+ end
438
+ end
439
+
175
440
  def apply_swarm_hook(swarm, config)
176
441
  event = config[:event]
177
442
 
@@ -194,6 +194,7 @@ module SwarmSDK
194
194
  agent_name: agent_name,
195
195
  swarm: @swarm,
196
196
  hook_registry: @hook_registry,
197
+ delegating_chat: chat,
197
198
  )
198
199
 
199
200
  chat.with_tool(tool)
@@ -254,9 +254,6 @@ module SwarmSDK
254
254
  # Lazy initialization of agents (with optional logging)
255
255
  initialize_agents unless @agents_initialized
256
256
 
257
- # Freeze log collector to make it fiber-safe before Async execution
258
- LogCollector.freeze! if block_given?
259
-
260
257
  # Execution loop (supports reprompting)
261
258
  result = nil
262
259
  swarm_stop_triggered = false
@@ -359,9 +356,26 @@ module SwarmSDK
359
356
 
360
357
  # Cleanup MCP clients after execution
361
358
  cleanup
362
- # Reset logging state for next execution
363
- LogCollector.reset!
364
- LogStream.reset!
359
+
360
+ # Reset logging state for next execution if we set it up
361
+ #
362
+ # IMPORTANT: Only reset if we set up logging (block_given? == true).
363
+ # When this swarm is a mini-swarm within a NodeOrchestrator workflow,
364
+ # the orchestrator manages LogCollector and we don't set up logging.
365
+ #
366
+ # Flow in NodeOrchestrator:
367
+ # 1. NodeOrchestrator sets up LogCollector + LogStream (no block given to mini-swarms)
368
+ # 2. Each mini-swarm executes without logging block (block_given? == false)
369
+ # 3. Each mini-swarm skips reset (didn't set up logging)
370
+ # 4. NodeOrchestrator resets once at the very end
371
+ #
372
+ # Flow in standalone swarm / interactive REPL:
373
+ # 1. Swarm.execute sets up LogCollector + LogStream (block given)
374
+ # 2. Swarm.execute resets in ensure block (cleanup for next call)
375
+ if block_given?
376
+ LogCollector.reset!
377
+ LogStream.reset!
378
+ end
365
379
  end
366
380
 
367
381
  # Get an agent chat instance by name
@@ -395,6 +409,57 @@ module SwarmSDK
395
409
  @agent_definitions.keys
396
410
  end
397
411
 
412
+ # Validate swarm configuration and return warnings
413
+ #
414
+ # This performs lightweight validation checks without creating agents.
415
+ # Useful for displaying configuration warnings before execution.
416
+ #
417
+ # @return [Array<Hash>] Array of warning hashes from all agent definitions
418
+ #
419
+ # @example
420
+ # swarm = Swarm.load("config.yml")
421
+ # warnings = swarm.validate
422
+ # warnings.each do |warning|
423
+ # puts "⚠️ #{warning[:agent]}: #{warning[:model]} not found"
424
+ # end
425
+ def validate
426
+ @agent_definitions.flat_map { |_name, definition| definition.validate }
427
+ end
428
+
429
+ # Emit validation warnings as log events
430
+ #
431
+ # This validates all agent definitions and emits any warnings as
432
+ # model_lookup_warning events through LogStream. Useful for emitting
433
+ # warnings before execution starts (e.g., in REPL after welcome screen).
434
+ #
435
+ # Requires LogStream.emitter to be set.
436
+ #
437
+ # @return [Array<Hash>] The validation warnings that were emitted
438
+ #
439
+ # @example
440
+ # LogCollector.on_log { |event| puts event }
441
+ # LogStream.emitter = LogCollector
442
+ # swarm.emit_validation_warnings
443
+ def emit_validation_warnings
444
+ warnings = validate
445
+
446
+ warnings.each do |warning|
447
+ case warning[:type]
448
+ when :model_not_found
449
+ LogStream.emit(
450
+ type: "model_lookup_warning",
451
+ agent: warning[:agent],
452
+ model: warning[:model],
453
+ error_message: warning[:error_message],
454
+ suggestions: warning[:suggestions],
455
+ timestamp: Time.now.utc.iso8601,
456
+ )
457
+ end
458
+ end
459
+
460
+ warnings
461
+ end
462
+
398
463
  # Cleanup all MCP clients
399
464
  #
400
465
  # Stops all MCP client connections gracefully.
@@ -18,13 +18,15 @@ module SwarmSDK
18
18
  # @param agent_name [Symbol, String] Name of the agent using this tool
19
19
  # @param swarm [Swarm] The swarm instance
20
20
  # @param hook_registry [Hooks::Registry] Registry for callbacks
21
+ # @param delegating_chat [Agent::Chat, nil] The chat instance of the agent doing the delegating (for accessing hooks)
21
22
  def initialize(
22
23
  delegate_name:,
23
24
  delegate_description:,
24
25
  delegate_chat:,
25
26
  agent_name:,
26
27
  swarm:,
27
- hook_registry:
28
+ hook_registry:,
29
+ delegating_chat: nil
28
30
  )
29
31
  super()
30
32
 
@@ -34,6 +36,7 @@ module SwarmSDK
34
36
  @agent_name = agent_name
35
37
  @swarm = swarm
36
38
  @hook_registry = hook_registry
39
+ @delegating_chat = delegating_chat
37
40
 
38
41
  # Generate tool name in the expected format: DelegateTaskTo[AgentName]
39
42
  @tool_name = "DelegateTaskTo#{delegate_name.to_s.capitalize}"
@@ -60,6 +63,13 @@ module SwarmSDK
60
63
  # @param task [String] Task to delegate
61
64
  # @return [String] Result from delegate agent or error message
62
65
  def execute(task:)
66
+ # Get agent-specific hooks from the delegating chat instance
67
+ agent_hooks = if @delegating_chat&.respond_to?(:hook_agent_hooks)
68
+ @delegating_chat.hook_agent_hooks || {}
69
+ else
70
+ {}
71
+ end
72
+
63
73
  # Trigger pre_delegation callback
64
74
  context = Hooks::Context.new(
65
75
  event: :pre_delegation,
@@ -74,7 +84,8 @@ module SwarmSDK
74
84
  )
75
85
 
76
86
  executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
77
- result = executor.execute_safe(event: :pre_delegation, context: context, callbacks: [])
87
+ pre_agent_hooks = agent_hooks[:pre_delegation] || []
88
+ result = executor.execute_safe(event: :pre_delegation, context: context, callbacks: pre_agent_hooks)
78
89
 
79
90
  # Check if callback halted or replaced the delegation
80
91
  if result.halt?
@@ -102,7 +113,8 @@ module SwarmSDK
102
113
  },
103
114
  )
104
115
 
105
- post_result = executor.execute_safe(event: :post_delegation, context: post_context, callbacks: [])
116
+ post_agent_hooks = agent_hooks[:post_delegation] || []
117
+ post_result = executor.execute_safe(event: :post_delegation, context: post_context, callbacks: post_agent_hooks)
106
118
 
107
119
  # Return modified result if callback replaces it
108
120
  if post_result.replace?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.0.0-2"
4
+ VERSION = "2.0.0"
5
5
  end