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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/builder.rb +118 -21
- data/lib/swarm_sdk/agent/definition.rb +121 -12
- data/lib/swarm_sdk/configuration.rb +44 -11
- data/lib/swarm_sdk/hooks/context.rb +34 -0
- data/lib/swarm_sdk/hooks/registry.rb +4 -0
- data/lib/swarm_sdk/log_collector.rb +3 -35
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +32 -3
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +81 -3
- data/lib/swarm_sdk/swarm/builder.rb +286 -21
- data/lib/swarm_sdk/swarm/tool_configurator.rb +1 -0
- data/lib/swarm_sdk/swarm.rb +71 -6
- data/lib/swarm_sdk/tools/delegate.rb +15 -3
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +57 -0
- metadata +9 -4
@@ -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
|
-
#
|
16
|
-
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
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
|
-
#
|
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
|
-
|
131
|
-
|
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
|
-
|
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
|
-
@
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
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
|
|
data/lib/swarm_sdk/swarm.rb
CHANGED
@@ -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
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
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
|
-
|
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?
|
data/lib/swarm_sdk/version.rb
CHANGED