swarm_sdk 2.5.5 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/agent/builder.rb +62 -13
- data/lib/swarm_sdk/agent/chat.rb +106 -43
- data/lib/swarm_sdk/agent/definition.rb +83 -8
- data/lib/swarm_sdk/builders/base_builder.rb +63 -6
- data/lib/swarm_sdk/config.rb +2 -0
- data/lib/swarm_sdk/configuration/parser.rb +26 -2
- data/lib/swarm_sdk/configuration/translator.rb +28 -4
- data/lib/swarm_sdk/configuration.rb +22 -1
- data/lib/swarm_sdk/defaults.rb +14 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +26 -16
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +12 -5
- data/lib/swarm_sdk/swarm/builder.rb +7 -0
- data/lib/swarm_sdk/swarm/executor.rb +84 -7
- data/lib/swarm_sdk/swarm.rb +14 -2
- data/lib/swarm_sdk/tools/delegate.rb +5 -3
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/workflow/agent_config.rb +19 -3
- data/lib/swarm_sdk/workflow/builder.rb +36 -1
- data/lib/swarm_sdk/workflow/node_builder.rb +37 -1
- data/lib/swarm_sdk/workflow.rb +36 -1
- data/lib/swarm_sdk.rb +10 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ce2f1c2ddb88cfb9a4fe7505fc0417494c2baf22a41678cbe7eb9085d96f136f
|
|
4
|
+
data.tar.gz: 5d5c8174d6f96d52f6855636d5fc7c485db6a0470e11d18ee2b5866fcd91431a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5e503eeda171b1840cabc10fccc5f98d1c4bcd75eba1782f885fd19cc09219b39850662c901793ff9e9d0cbfbdfcc9759071c782476bb793ab87f9ddd94c3ae9
|
|
7
|
+
data.tar.gz: 054e3a4db5ead9c96e2d332a1a85e9102bdaabe8e50c1e3163776789709289b5fbc73fce5f73b4af856c6e1ca0a3e6f5b3cbf4918f10f6a946c9ef6223384b12
|
|
@@ -49,7 +49,8 @@ module SwarmSDK
|
|
|
49
49
|
@directory = "."
|
|
50
50
|
@parameters = {}
|
|
51
51
|
@headers = {}
|
|
52
|
-
@
|
|
52
|
+
@request_timeout = nil
|
|
53
|
+
@turn_timeout = nil
|
|
53
54
|
@mcp_servers = []
|
|
54
55
|
@disable_default_tools = nil # nil = include all default tools
|
|
55
56
|
@bypass_permissions = false
|
|
@@ -112,11 +113,18 @@ module SwarmSDK
|
|
|
112
113
|
@headers = header_hash
|
|
113
114
|
end
|
|
114
115
|
|
|
115
|
-
# Set/get timeout
|
|
116
|
-
def
|
|
117
|
-
return @
|
|
116
|
+
# Set/get request timeout
|
|
117
|
+
def request_timeout(seconds = :__not_provided__)
|
|
118
|
+
return @request_timeout if seconds == :__not_provided__
|
|
118
119
|
|
|
119
|
-
@
|
|
120
|
+
@request_timeout = seconds
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Set/get turn timeout
|
|
124
|
+
def turn_timeout(seconds = :__not_provided__)
|
|
125
|
+
return @turn_timeout if seconds == :__not_provided__
|
|
126
|
+
|
|
127
|
+
@turn_timeout = seconds
|
|
120
128
|
end
|
|
121
129
|
|
|
122
130
|
# Add an MCP server configuration
|
|
@@ -242,8 +250,39 @@ module SwarmSDK
|
|
|
242
250
|
end
|
|
243
251
|
|
|
244
252
|
# Set delegation targets
|
|
245
|
-
|
|
246
|
-
|
|
253
|
+
#
|
|
254
|
+
# Supports multiple formats for flexibility:
|
|
255
|
+
#
|
|
256
|
+
# @example Simple array (backwards compatible)
|
|
257
|
+
# delegates_to :frontend, :backend, :qa
|
|
258
|
+
#
|
|
259
|
+
# @example Hash with custom tool names
|
|
260
|
+
# delegates_to frontend: "AskFrontend",
|
|
261
|
+
# backend: "GetBackendHelp",
|
|
262
|
+
# qa: "RequestReview"
|
|
263
|
+
#
|
|
264
|
+
# @example Mixed - some auto, some custom
|
|
265
|
+
# delegates_to :frontend,
|
|
266
|
+
# backend: "GetBackendHelp",
|
|
267
|
+
# :qa
|
|
268
|
+
#
|
|
269
|
+
# @param agent_names_and_options [Array<Symbol, Hash>] Agent names and/or hash with custom tool names
|
|
270
|
+
# @return [void]
|
|
271
|
+
def delegates_to(*agent_names_and_options)
|
|
272
|
+
agent_names_and_options.each do |item|
|
|
273
|
+
case item
|
|
274
|
+
when Symbol, String
|
|
275
|
+
# Simple format: :frontend
|
|
276
|
+
@delegates_to << { agent: item.to_sym, tool_name: nil }
|
|
277
|
+
when Hash
|
|
278
|
+
# Hash format: { frontend: "AskFrontend", backend: nil }
|
|
279
|
+
item.each do |agent, tool_name|
|
|
280
|
+
@delegates_to << { agent: agent.to_sym, tool_name: tool_name }
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
raise ConfigurationError, "delegates_to accepts Symbols or Hashes, got #{item.class}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
247
286
|
end
|
|
248
287
|
|
|
249
288
|
# Add a hook (Ruby block OR shell command)
|
|
@@ -386,13 +425,22 @@ module SwarmSDK
|
|
|
386
425
|
!@api_version.nil?
|
|
387
426
|
end
|
|
388
427
|
|
|
389
|
-
# Check if
|
|
428
|
+
# Check if request_timeout has been explicitly set
|
|
429
|
+
#
|
|
430
|
+
# Used by Swarm::Builder to determine if all_agents request_timeout should apply.
|
|
431
|
+
#
|
|
432
|
+
# @return [Boolean] true if request_timeout was explicitly set
|
|
433
|
+
def request_timeout_set?
|
|
434
|
+
!@request_timeout.nil?
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Check if turn_timeout has been explicitly set
|
|
390
438
|
#
|
|
391
|
-
# Used by Swarm::Builder to determine if all_agents
|
|
439
|
+
# Used by Swarm::Builder to determine if all_agents turn_timeout should apply.
|
|
392
440
|
#
|
|
393
|
-
# @return [Boolean] true if
|
|
394
|
-
def
|
|
395
|
-
!@
|
|
441
|
+
# @return [Boolean] true if turn_timeout was explicitly set
|
|
442
|
+
def turn_timeout_set?
|
|
443
|
+
!@turn_timeout.nil?
|
|
396
444
|
end
|
|
397
445
|
|
|
398
446
|
# Check if coding_agent has been explicitly set
|
|
@@ -448,7 +496,8 @@ module SwarmSDK
|
|
|
448
496
|
agent_config[:context_window] = @context_window if @context_window
|
|
449
497
|
agent_config[:parameters] = @parameters if @parameters.any?
|
|
450
498
|
agent_config[:headers] = @headers if @headers.any?
|
|
451
|
-
agent_config[:
|
|
499
|
+
agent_config[:request_timeout] = @request_timeout if @request_timeout
|
|
500
|
+
agent_config[:turn_timeout] = @turn_timeout if @turn_timeout
|
|
452
501
|
agent_config[:mcp_servers] = @mcp_servers if @mcp_servers.any?
|
|
453
502
|
agent_config[:disable_default_tools] = @disable_default_tools unless @disable_default_tools.nil?
|
|
454
503
|
agent_config[:bypass_permissions] = @bypass_permissions
|
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -122,7 +122,7 @@ module SwarmSDK
|
|
|
122
122
|
max_concurrent_tools = definition[:max_concurrent_tools]
|
|
123
123
|
base_url = definition[:base_url]
|
|
124
124
|
api_version = definition[:api_version]
|
|
125
|
-
|
|
125
|
+
request_timeout = definition[:request_timeout] || SwarmSDK.config.agent_request_timeout
|
|
126
126
|
assume_model_exists = definition[:assume_model_exists]
|
|
127
127
|
system_prompt = definition[:system_prompt]
|
|
128
128
|
parameters = definition[:parameters]
|
|
@@ -131,6 +131,9 @@ module SwarmSDK
|
|
|
131
131
|
# Agent identifier (for plugin callbacks)
|
|
132
132
|
@agent_name = agent_name
|
|
133
133
|
|
|
134
|
+
# Turn timeout (external timeout for entire ask() call)
|
|
135
|
+
@turn_timeout = definition[:turn_timeout]
|
|
136
|
+
|
|
134
137
|
# Context manager for ephemeral messages
|
|
135
138
|
@context_manager = ContextManager.new
|
|
136
139
|
|
|
@@ -162,7 +165,7 @@ module SwarmSDK
|
|
|
162
165
|
provider_name: provider_name,
|
|
163
166
|
base_url: base_url,
|
|
164
167
|
api_version: api_version,
|
|
165
|
-
timeout:
|
|
168
|
+
timeout: request_timeout,
|
|
166
169
|
assume_model_exists: assume_model_exists,
|
|
167
170
|
max_concurrent_tools: max_concurrent_tools,
|
|
168
171
|
)
|
|
@@ -461,48 +464,11 @@ module SwarmSDK
|
|
|
461
464
|
# @return [RubyLLM::Message] LLM response
|
|
462
465
|
def ask(prompt, **options)
|
|
463
466
|
@ask_semaphore.acquire do
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
# Trigger user_prompt hook (with clean prompt, not reminders)
|
|
470
|
-
source = options.delete(:source) || "user"
|
|
471
|
-
final_prompt = prompt
|
|
472
|
-
if @hook_executor
|
|
473
|
-
hook_result = trigger_user_prompt(prompt, source: source)
|
|
474
|
-
|
|
475
|
-
if hook_result[:halted]
|
|
476
|
-
return RubyLLM::Message.new(
|
|
477
|
-
role: :assistant,
|
|
478
|
-
content: hook_result[:halt_message],
|
|
479
|
-
model_id: model_id,
|
|
480
|
-
)
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
final_prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt]
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
# Add CLEAN user message to history (no reminders embedded)
|
|
487
|
-
@llm_chat.add_message(role: :user, content: final_prompt)
|
|
488
|
-
|
|
489
|
-
# Track reminders as ephemeral content for this LLM call only
|
|
490
|
-
# They'll be injected by around_llm_request hook but not stored
|
|
491
|
-
reminders.each do |reminder|
|
|
492
|
-
@context_manager.add_ephemeral_reminder(reminder, messages_array: @llm_chat.messages)
|
|
493
|
-
end
|
|
494
|
-
|
|
495
|
-
# Execute complete() which handles tool loop and ephemeral injection
|
|
496
|
-
response = execute_with_global_semaphore do
|
|
497
|
-
catch(:finish_agent) do
|
|
498
|
-
catch(:finish_swarm) do
|
|
499
|
-
@llm_chat.complete(**options)
|
|
500
|
-
end
|
|
501
|
-
end
|
|
467
|
+
if @turn_timeout
|
|
468
|
+
execute_with_turn_timeout(prompt, options)
|
|
469
|
+
else
|
|
470
|
+
execute_ask(prompt, options)
|
|
502
471
|
end
|
|
503
|
-
|
|
504
|
-
# Handle finish markers from hooks
|
|
505
|
-
handle_finish_marker(response)
|
|
506
472
|
end
|
|
507
473
|
end
|
|
508
474
|
|
|
@@ -559,6 +525,103 @@ module SwarmSDK
|
|
|
559
525
|
|
|
560
526
|
private
|
|
561
527
|
|
|
528
|
+
# Execute ask with turn timeout wrapper
|
|
529
|
+
def execute_with_turn_timeout(prompt, options)
|
|
530
|
+
task = Async::Task.current
|
|
531
|
+
|
|
532
|
+
# Use barrier to track child tasks spawned during this turn
|
|
533
|
+
# (includes RubyLLM's async tool execution when max_concurrent_tools is set)
|
|
534
|
+
barrier = Async::Barrier.new
|
|
535
|
+
|
|
536
|
+
begin
|
|
537
|
+
task.with_timeout(
|
|
538
|
+
@turn_timeout,
|
|
539
|
+
TurnTimeoutError,
|
|
540
|
+
"Agent turn timed out after #{@turn_timeout}s",
|
|
541
|
+
) do
|
|
542
|
+
# Execute inside barrier to track child tasks
|
|
543
|
+
barrier.async do
|
|
544
|
+
execute_ask(prompt, options)
|
|
545
|
+
end.wait
|
|
546
|
+
end
|
|
547
|
+
rescue TurnTimeoutError
|
|
548
|
+
# Stop all child tasks
|
|
549
|
+
barrier.stop
|
|
550
|
+
|
|
551
|
+
emit_turn_timeout_event
|
|
552
|
+
|
|
553
|
+
# Return error message as response so caller can handle gracefully
|
|
554
|
+
# Format like other tool/delegation errors for natural flow
|
|
555
|
+
# This message goes to the swarm/caller, NOT added to agent's conversation history
|
|
556
|
+
RubyLLM::Message.new(
|
|
557
|
+
role: :assistant,
|
|
558
|
+
content: "Error: Request timed out after #{@turn_timeout}s. The agent did not complete its response within the time limit. Please try a simpler request or increase the turn timeout.",
|
|
559
|
+
model_id: model_id,
|
|
560
|
+
)
|
|
561
|
+
ensure
|
|
562
|
+
# Cleanup barrier if not already stopped
|
|
563
|
+
barrier.stop unless barrier.empty?
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Emit turn timeout event
|
|
568
|
+
def emit_turn_timeout_event
|
|
569
|
+
LogStream.emit(
|
|
570
|
+
type: "turn_timeout",
|
|
571
|
+
agent: @agent_name,
|
|
572
|
+
swarm_id: @agent_context&.swarm_id,
|
|
573
|
+
parent_swarm_id: @agent_context&.parent_swarm_id,
|
|
574
|
+
limit: @turn_timeout,
|
|
575
|
+
message: "Agent turn timed out after #{@turn_timeout}s",
|
|
576
|
+
)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
# Execute ask without timeout (original ask implementation)
|
|
580
|
+
def execute_ask(prompt, options)
|
|
581
|
+
is_first = first_message?
|
|
582
|
+
|
|
583
|
+
# Collect system reminders to inject as ephemeral content
|
|
584
|
+
reminders = collect_system_reminders(prompt, is_first)
|
|
585
|
+
|
|
586
|
+
# Trigger user_prompt hook (with clean prompt, not reminders)
|
|
587
|
+
source = options.delete(:source) || "user"
|
|
588
|
+
final_prompt = prompt
|
|
589
|
+
if @hook_executor
|
|
590
|
+
hook_result = trigger_user_prompt(prompt, source: source)
|
|
591
|
+
|
|
592
|
+
if hook_result[:halted]
|
|
593
|
+
return RubyLLM::Message.new(
|
|
594
|
+
role: :assistant,
|
|
595
|
+
content: hook_result[:halt_message],
|
|
596
|
+
model_id: model_id,
|
|
597
|
+
)
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
final_prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt]
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
# Add CLEAN user message to history (no reminders embedded)
|
|
604
|
+
@llm_chat.add_message(role: :user, content: final_prompt)
|
|
605
|
+
|
|
606
|
+
# Track reminders as ephemeral content for this LLM call only
|
|
607
|
+
# They'll be injected by around_llm_request hook but not stored
|
|
608
|
+
reminders.each do |reminder|
|
|
609
|
+
@context_manager.add_ephemeral_reminder(reminder, messages_array: @llm_chat.messages)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
# Execute complete() which handles tool loop and ephemeral injection
|
|
613
|
+
response = execute_with_global_semaphore do
|
|
614
|
+
catch(:finish_agent) do
|
|
615
|
+
catch(:finish_swarm) do
|
|
616
|
+
@llm_chat.complete(**options)
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# Handle finish markers from hooks
|
|
622
|
+
handle_finish_marker(response)
|
|
623
|
+
end
|
|
624
|
+
|
|
562
625
|
# --- Tool Execution Hook ---
|
|
563
626
|
|
|
564
627
|
# Setup around_tool_execution hook for SwarmSDK orchestration
|
|
@@ -24,7 +24,7 @@ module SwarmSDK
|
|
|
24
24
|
:context_window,
|
|
25
25
|
:directory,
|
|
26
26
|
:tools,
|
|
27
|
-
:
|
|
27
|
+
:delegation_configs, # Full delegation config with tool names
|
|
28
28
|
:system_prompt,
|
|
29
29
|
:provider,
|
|
30
30
|
:base_url,
|
|
@@ -32,7 +32,8 @@ module SwarmSDK
|
|
|
32
32
|
:mcp_servers,
|
|
33
33
|
:parameters,
|
|
34
34
|
:headers,
|
|
35
|
-
:
|
|
35
|
+
:request_timeout,
|
|
36
|
+
:turn_timeout,
|
|
36
37
|
:disable_default_tools,
|
|
37
38
|
:coding_agent,
|
|
38
39
|
:default_permissions,
|
|
@@ -74,9 +75,16 @@ module SwarmSDK
|
|
|
74
75
|
@context_window = coerce_to_integer(config[:context_window]) # Explicit context window override
|
|
75
76
|
@parameters = config[:parameters] || {}
|
|
76
77
|
@headers = Utils.stringify_keys(config[:headers] || {})
|
|
77
|
-
@
|
|
78
|
+
@request_timeout = config[:request_timeout] || SwarmSDK.config.agent_request_timeout
|
|
78
79
|
@bypass_permissions = config[:bypass_permissions] || false
|
|
79
80
|
@max_concurrent_tools = config[:max_concurrent_tools]
|
|
81
|
+
|
|
82
|
+
# Use default from config unless explicitly set (including nil to disable)
|
|
83
|
+
@turn_timeout = if config.key?(:turn_timeout)
|
|
84
|
+
config[:turn_timeout] # Could be a number OR nil (to disable)
|
|
85
|
+
else
|
|
86
|
+
SwarmSDK.config.default_turn_timeout
|
|
87
|
+
end
|
|
80
88
|
# Always assume model exists - SwarmSDK validates models separately using models.json
|
|
81
89
|
# This prevents RubyLLM from trying to validate models in its registry
|
|
82
90
|
@assume_model_exists = true
|
|
@@ -117,7 +125,8 @@ module SwarmSDK
|
|
|
117
125
|
# Inject default write restrictions for security
|
|
118
126
|
@tools = inject_default_write_permissions(@tools)
|
|
119
127
|
|
|
120
|
-
|
|
128
|
+
# Parse delegation configuration (supports both simple arrays and custom tool names)
|
|
129
|
+
@delegation_configs = parse_delegation_config(config[:delegates_to])
|
|
121
130
|
@mcp_servers = Array(config[:mcp_servers] || [])
|
|
122
131
|
|
|
123
132
|
# Parse hooks configuration
|
|
@@ -127,6 +136,20 @@ module SwarmSDK
|
|
|
127
136
|
validate!
|
|
128
137
|
end
|
|
129
138
|
|
|
139
|
+
# Get agent names that this agent delegates to (backwards compatible)
|
|
140
|
+
#
|
|
141
|
+
# Returns an array of agent name symbols. This maintains backwards compatibility
|
|
142
|
+
# with existing code that expects delegates_to to be a simple array.
|
|
143
|
+
#
|
|
144
|
+
# @return [Array<Symbol>] Delegate agent names
|
|
145
|
+
#
|
|
146
|
+
# @example
|
|
147
|
+
# agent_definition.delegates_to
|
|
148
|
+
# # => [:frontend, :backend, :qa]
|
|
149
|
+
def delegates_to
|
|
150
|
+
@delegation_configs.map { |config| config[:agent] }
|
|
151
|
+
end
|
|
152
|
+
|
|
130
153
|
# Get plugin-specific configuration
|
|
131
154
|
#
|
|
132
155
|
# Plugins store their configuration in the generic plugin_configs hash.
|
|
@@ -138,7 +161,7 @@ module SwarmSDK
|
|
|
138
161
|
#
|
|
139
162
|
# @example
|
|
140
163
|
# agent_definition.plugin_config(:memory)
|
|
141
|
-
# # => { directory: "tmp/memory", mode: :
|
|
164
|
+
# # => { directory: "tmp/memory", mode: :full_access }
|
|
142
165
|
def plugin_config(plugin_name)
|
|
143
166
|
@plugin_configs[plugin_name.to_sym] || @plugin_configs[plugin_name.to_s]
|
|
144
167
|
end
|
|
@@ -152,7 +175,7 @@ module SwarmSDK
|
|
|
152
175
|
context_window: @context_window,
|
|
153
176
|
directory: @directory,
|
|
154
177
|
tools: @tools,
|
|
155
|
-
delegates_to: @
|
|
178
|
+
delegates_to: @delegation_configs, # Serialize full config
|
|
156
179
|
system_prompt: @system_prompt,
|
|
157
180
|
provider: @provider,
|
|
158
181
|
base_url: @base_url,
|
|
@@ -160,7 +183,8 @@ module SwarmSDK
|
|
|
160
183
|
mcp_servers: @mcp_servers,
|
|
161
184
|
parameters: @parameters,
|
|
162
185
|
headers: @headers,
|
|
163
|
-
|
|
186
|
+
request_timeout: @request_timeout,
|
|
187
|
+
turn_timeout: @turn_timeout,
|
|
164
188
|
bypass_permissions: @bypass_permissions,
|
|
165
189
|
disable_default_tools: @disable_default_tools,
|
|
166
190
|
coding_agent: @coding_agent,
|
|
@@ -276,6 +300,51 @@ module SwarmSDK
|
|
|
276
300
|
File.expand_path(directory_config.to_s)
|
|
277
301
|
end
|
|
278
302
|
|
|
303
|
+
# Parse delegation configuration
|
|
304
|
+
#
|
|
305
|
+
# Supports multiple formats for backwards compatibility and new features:
|
|
306
|
+
# 1. Simple array (backwards compatible): [:frontend, :backend]
|
|
307
|
+
# 2. Hash with custom tool names: { frontend: "AskFrontend", backend: nil }
|
|
308
|
+
# 3. Array of hashes: [{ agent: :frontend, tool_name: "AskFrontend" }]
|
|
309
|
+
#
|
|
310
|
+
# Returns normalized format: [{agent: :name, tool_name: "Custom" or nil}]
|
|
311
|
+
#
|
|
312
|
+
# @param delegation_config [nil, Array, Hash] Delegation configuration
|
|
313
|
+
# @return [Array<Hash>] Normalized delegation config
|
|
314
|
+
def parse_delegation_config(delegation_config)
|
|
315
|
+
return [] if delegation_config.nil?
|
|
316
|
+
return [] if delegation_config.respond_to?(:empty?) && delegation_config.empty?
|
|
317
|
+
|
|
318
|
+
# Handle array format (could be symbols or hashes)
|
|
319
|
+
if delegation_config.is_a?(Array)
|
|
320
|
+
delegation_config.flat_map do |item|
|
|
321
|
+
case item
|
|
322
|
+
when Symbol, String
|
|
323
|
+
# Simple format: :frontend → {agent: :frontend, tool_name: nil}
|
|
324
|
+
[{ agent: item.to_sym, tool_name: nil }]
|
|
325
|
+
when Hash
|
|
326
|
+
# Could be already normalized or hash format
|
|
327
|
+
if item.key?(:agent)
|
|
328
|
+
# Already normalized: {agent: :frontend, tool_name: "Custom"}
|
|
329
|
+
[item]
|
|
330
|
+
else
|
|
331
|
+
# Hash format in array: {frontend: "AskFrontend"}
|
|
332
|
+
item.map { |agent, tool_name| { agent: agent.to_sym, tool_name: tool_name } }
|
|
333
|
+
end
|
|
334
|
+
else
|
|
335
|
+
raise ConfigurationError, "Invalid delegation config format: #{item.inspect}"
|
|
336
|
+
end
|
|
337
|
+
end.uniq { |config| config[:agent] } # Remove duplicates by agent name
|
|
338
|
+
elsif delegation_config.is_a?(Hash)
|
|
339
|
+
# Hash format: {frontend: "AskFrontend", backend: nil}
|
|
340
|
+
delegation_config.map do |agent, tool_name|
|
|
341
|
+
{ agent: agent.to_sym, tool_name: tool_name }
|
|
342
|
+
end
|
|
343
|
+
else
|
|
344
|
+
raise ConfigurationError, "delegates_to must be an Array or Hash, got #{delegation_config.class}"
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
279
348
|
# Extract plugin-specific configuration keys from the config hash
|
|
280
349
|
#
|
|
281
350
|
# Standard SDK keys are filtered out, leaving only plugin-specific keys.
|
|
@@ -294,7 +363,8 @@ module SwarmSDK
|
|
|
294
363
|
:context_window,
|
|
295
364
|
:parameters,
|
|
296
365
|
:headers,
|
|
297
|
-
:
|
|
366
|
+
:request_timeout,
|
|
367
|
+
:turn_timeout,
|
|
298
368
|
:bypass_permissions,
|
|
299
369
|
:max_concurrent_tools,
|
|
300
370
|
:assume_model_exists,
|
|
@@ -465,6 +535,11 @@ module SwarmSDK
|
|
|
465
535
|
def validate!
|
|
466
536
|
raise ConfigurationError, "Agent '#{@name}' missing required 'description' field" unless @description
|
|
467
537
|
|
|
538
|
+
# Validate turn_timeout is positive if set
|
|
539
|
+
if @turn_timeout && @turn_timeout <= 0
|
|
540
|
+
raise ConfigurationError, "Agent '#{@name}' turn_timeout must be positive (got #{@turn_timeout})"
|
|
541
|
+
end
|
|
542
|
+
|
|
468
543
|
# Validate api_version can only be set for OpenAI-compatible providers
|
|
469
544
|
if @api_version
|
|
470
545
|
openai_compatible = ["openai", "deepseek", "perplexity", "mistral", "openrouter"]
|
|
@@ -260,7 +260,8 @@ module SwarmSDK
|
|
|
260
260
|
builder.context_window(config[:context_window]) if config[:context_window]
|
|
261
261
|
builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
|
|
262
262
|
builder.directory(config[:directory]) if config[:directory]
|
|
263
|
-
builder.
|
|
263
|
+
builder.request_timeout(config[:request_timeout]) if config[:request_timeout]
|
|
264
|
+
builder.turn_timeout(config[:turn_timeout]) if config[:turn_timeout]
|
|
264
265
|
builder.parameters(config[:parameters]) if config[:parameters]
|
|
265
266
|
builder.headers(config[:headers]) if config[:headers]
|
|
266
267
|
builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
|
|
@@ -275,8 +276,17 @@ module SwarmSDK
|
|
|
275
276
|
builder.tools(*tool_names)
|
|
276
277
|
end
|
|
277
278
|
|
|
278
|
-
# Add delegates_to
|
|
279
|
-
|
|
279
|
+
# Add delegates_to (handle both array and hash formats)
|
|
280
|
+
if config[:delegates_to]&.any?
|
|
281
|
+
delegation_config = config[:delegates_to]
|
|
282
|
+
if delegation_config.is_a?(Hash)
|
|
283
|
+
# Hash format: pass as single argument
|
|
284
|
+
builder.delegates_to(delegation_config)
|
|
285
|
+
elsif delegation_config.is_a?(Array)
|
|
286
|
+
# Array format: splat the array
|
|
287
|
+
builder.delegates_to(*delegation_config)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
280
290
|
|
|
281
291
|
# Add MCP servers
|
|
282
292
|
config[:mcp_servers]&.each do |server|
|
|
@@ -332,11 +342,25 @@ module SwarmSDK
|
|
|
332
342
|
when :tools
|
|
333
343
|
merged[:tools] = Array(merged[:tools]) + Array(value)
|
|
334
344
|
when :delegates_to
|
|
335
|
-
|
|
345
|
+
# Handle merging delegation configs (can be array or hash)
|
|
346
|
+
existing = merged[:delegates_to] || []
|
|
347
|
+
new_value = value || []
|
|
348
|
+
|
|
349
|
+
# Convert both to array of delegation configs for merging
|
|
350
|
+
existing_array = normalize_delegation_array(existing)
|
|
351
|
+
new_array = normalize_delegation_array(new_value)
|
|
352
|
+
|
|
353
|
+
merged[:delegates_to] = existing_array + new_array
|
|
336
354
|
when :parameters
|
|
337
355
|
merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
|
|
338
356
|
when :headers
|
|
339
357
|
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
|
358
|
+
when :turn_timeout
|
|
359
|
+
# Agent-specific turn_timeout overrides all_agents
|
|
360
|
+
merged[key] = value
|
|
361
|
+
when :request_timeout
|
|
362
|
+
# Agent-specific request_timeout overrides all_agents
|
|
363
|
+
merged[key] = value
|
|
340
364
|
else
|
|
341
365
|
merged[key] = value
|
|
342
366
|
end
|
|
@@ -350,6 +374,35 @@ module SwarmSDK
|
|
|
350
374
|
merged
|
|
351
375
|
end
|
|
352
376
|
|
|
377
|
+
# Normalize delegation config to array of hashes format
|
|
378
|
+
#
|
|
379
|
+
# Converts various delegation formats to normalized array for merging:
|
|
380
|
+
# - Array of symbols: [:frontend, :backend] → [{agent: :frontend, tool_name: nil}, ...]
|
|
381
|
+
# - Hash: {frontend: "Custom"} → [{agent: :frontend, tool_name: "Custom"}, ...]
|
|
382
|
+
# - Array of hashes: [{agent: :frontend, tool_name: "Custom"}] → unchanged
|
|
383
|
+
#
|
|
384
|
+
# @param delegation_config [Array, Hash] Delegation configuration
|
|
385
|
+
# @return [Array<Hash>] Normalized array of {agent:, tool_name:} hashes
|
|
386
|
+
def normalize_delegation_array(delegation_config)
|
|
387
|
+
return [] if delegation_config.nil? || (delegation_config.respond_to?(:empty?) && delegation_config.empty?)
|
|
388
|
+
|
|
389
|
+
case delegation_config
|
|
390
|
+
when Array
|
|
391
|
+
delegation_config.map do |item|
|
|
392
|
+
case item
|
|
393
|
+
when Symbol, String
|
|
394
|
+
{ agent: item.to_sym, tool_name: nil }
|
|
395
|
+
when Hash
|
|
396
|
+
item.key?(:agent) ? item : item.map { |agent, tool_name| { agent: agent.to_sym, tool_name: tool_name } }
|
|
397
|
+
end
|
|
398
|
+
end.flatten
|
|
399
|
+
when Hash
|
|
400
|
+
delegation_config.map { |agent, tool_name| { agent: agent.to_sym, tool_name: tool_name } }
|
|
401
|
+
else
|
|
402
|
+
[]
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
353
406
|
# Apply all_agents defaults to an agent builder
|
|
354
407
|
#
|
|
355
408
|
# @param agent_builder [Agent::Builder] The agent builder to configure
|
|
@@ -372,8 +425,12 @@ module SwarmSDK
|
|
|
372
425
|
agent_builder.api_version(all_agents_hash[:api_version])
|
|
373
426
|
end
|
|
374
427
|
|
|
375
|
-
if all_agents_hash[:
|
|
376
|
-
agent_builder.
|
|
428
|
+
if all_agents_hash[:request_timeout] && !agent_builder.request_timeout_set?
|
|
429
|
+
agent_builder.request_timeout(all_agents_hash[:request_timeout])
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
if all_agents_hash[:turn_timeout] && !agent_builder.turn_timeout_set?
|
|
433
|
+
agent_builder.turn_timeout(all_agents_hash[:turn_timeout])
|
|
377
434
|
end
|
|
378
435
|
|
|
379
436
|
if all_agents_hash[:parameters]
|
data/lib/swarm_sdk/config.rb
CHANGED
|
@@ -81,6 +81,8 @@ module SwarmSDK
|
|
|
81
81
|
chars_per_token_prose: ["SWARM_SDK_CHARS_PER_TOKEN_PROSE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE }],
|
|
82
82
|
chars_per_token_code: ["SWARM_SDK_CHARS_PER_TOKEN_CODE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE }],
|
|
83
83
|
mcp_log_level: ["SWARM_SDK_MCP_LOG_LEVEL", -> { Defaults::Logging::MCP_LOG_LEVEL }],
|
|
84
|
+
default_execution_timeout: ["SWARM_SDK_DEFAULT_EXECUTION_TIMEOUT", -> { Defaults::Timeouts::EXECUTION_TIMEOUT_SECONDS }],
|
|
85
|
+
default_turn_timeout: ["SWARM_SDK_DEFAULT_TURN_TIMEOUT", -> { Defaults::Timeouts::TURN_TIMEOUT_SECONDS }],
|
|
84
86
|
}.freeze
|
|
85
87
|
|
|
86
88
|
# WebFetch and control settings
|
|
@@ -28,7 +28,8 @@ module SwarmSDK
|
|
|
28
28
|
:all_agents_hooks,
|
|
29
29
|
:scratchpad_mode,
|
|
30
30
|
:nodes,
|
|
31
|
-
:external_swarms
|
|
31
|
+
:external_swarms,
|
|
32
|
+
:execution_timeout
|
|
32
33
|
|
|
33
34
|
# Initialize parser with YAML content and options
|
|
34
35
|
#
|
|
@@ -54,6 +55,7 @@ module SwarmSDK
|
|
|
54
55
|
@external_swarms = {}
|
|
55
56
|
@nodes = {}
|
|
56
57
|
@scratchpad_mode = :disabled
|
|
58
|
+
@execution_timeout = nil
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
def parse
|
|
@@ -88,7 +90,28 @@ module SwarmSDK
|
|
|
88
90
|
return [] unless agent_config
|
|
89
91
|
|
|
90
92
|
delegates = agent_config[:delegates_to] || []
|
|
91
|
-
|
|
93
|
+
|
|
94
|
+
# Handle both array and hash formats for delegates_to
|
|
95
|
+
case delegates
|
|
96
|
+
when Array
|
|
97
|
+
# Array of symbols: [:frontend, :backend]
|
|
98
|
+
# OR array of hashes: [{agent: :frontend, tool_name: "Custom"}]
|
|
99
|
+
delegates.map do |item|
|
|
100
|
+
case item
|
|
101
|
+
when Symbol, String
|
|
102
|
+
item.to_sym
|
|
103
|
+
when Hash
|
|
104
|
+
# Extract agent name from hash format
|
|
105
|
+
agent_name = item[:agent] || item["agent"]
|
|
106
|
+
agent_name&.to_sym
|
|
107
|
+
end
|
|
108
|
+
end.compact # Remove nils from malformed hashes
|
|
109
|
+
when Hash
|
|
110
|
+
# Hash format: {frontend: "Custom", backend: nil}
|
|
111
|
+
delegates.keys.map(&:to_sym)
|
|
112
|
+
else
|
|
113
|
+
[]
|
|
114
|
+
end
|
|
92
115
|
end
|
|
93
116
|
|
|
94
117
|
attr_reader :base_dir
|
|
@@ -134,6 +157,7 @@ module SwarmSDK
|
|
|
134
157
|
@swarm_name = @root_config[:name]
|
|
135
158
|
@swarm_id = @root_config[:id]
|
|
136
159
|
@scratchpad_mode = parse_scratchpad_mode(@root_config[:scratchpad])
|
|
160
|
+
@execution_timeout = @root_config[:execution_timeout]
|
|
137
161
|
|
|
138
162
|
load_all_agents_config
|
|
139
163
|
load_hooks_config
|