swarm_sdk 2.7.12 → 2.7.14
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 +25 -0
- data/lib/swarm_sdk/agent/chat.rb +59 -46
- data/lib/swarm_sdk/agent/definition.rb +8 -1
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +1 -0
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +15 -3
- data/lib/swarm_sdk/builders/base_builder.rb +5 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +3 -0
- data/lib/swarm_sdk/config.rb +2 -1
- data/lib/swarm_sdk/configuration/translator.rb +2 -0
- data/lib/swarm_sdk/observer/manager.rb +12 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +2 -0
- data/lib/swarm_sdk/result.rb +29 -0
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +27 -22
- data/lib/swarm_sdk/ruby_llm_patches/configuration_patch.rb +14 -1
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +3 -0
- data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
- data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +11 -8
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +9 -0
- data/lib/swarm_sdk/swarm/executor.rb +176 -20
- data/lib/swarm_sdk/swarm/hook_triggers.rb +11 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +20 -0
- data/lib/swarm_sdk/swarm.rb +132 -6
- data/lib/swarm_sdk/tools/delegate.rb +92 -47
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +3 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 22ea4e05517149f4cb6f6f18e896e9493e9ed75f69e82bd6891a9b0b471e314e
|
|
4
|
+
data.tar.gz: 94f2f1b7c28fe626889aa4f1fbb611bb36ac4d73ab7de778f76e553538589cbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2360f50b71a7c2342f2301a8dc5362b0e92402f16bdaee19dc7932b395a33f37e12a385fe752b69411e481ed10418396b7245a5d61669f7d41d12b6f7d0c3f40
|
|
7
|
+
data.tar.gz: 076b1ca993b017aeabfe5e35daa08be81e4cd815e314b2e28b594f298ae2b5b430101a0c9ea01d5263689b21ad888e4fb17d06c4a59904aa38734308a21ad130
|
|
@@ -63,6 +63,7 @@ module SwarmSDK
|
|
|
63
63
|
@shared_across_delegations = nil # nil = not set (will default to false in Definition)
|
|
64
64
|
@streaming = nil # nil = not set (will use global config default)
|
|
65
65
|
@thinking = nil # nil = not set (extended thinking disabled)
|
|
66
|
+
@disable_environment_info = nil # nil = not set (will default to false in Definition)
|
|
66
67
|
@context_management_config = nil # Context management DSL hooks
|
|
67
68
|
end
|
|
68
69
|
|
|
@@ -529,6 +530,29 @@ module SwarmSDK
|
|
|
529
530
|
!@coding_agent.nil?
|
|
530
531
|
end
|
|
531
532
|
|
|
533
|
+
# Disable environment info (date, platform, OS, working directory) in system prompt
|
|
534
|
+
#
|
|
535
|
+
# When true, omits the environment information section from the agent's system prompt.
|
|
536
|
+
# Defaults to false if not set.
|
|
537
|
+
#
|
|
538
|
+
# @param enabled [Boolean] Whether to disable environment info
|
|
539
|
+
# @return [void]
|
|
540
|
+
#
|
|
541
|
+
# @example
|
|
542
|
+
# disable_environment_info true # Omit environment info from prompt
|
|
543
|
+
def disable_environment_info(enabled)
|
|
544
|
+
@disable_environment_info = enabled
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
# Check if disable_environment_info has been explicitly set
|
|
548
|
+
#
|
|
549
|
+
# Used by Swarm::Builder to determine if all_agents disable_environment_info should apply.
|
|
550
|
+
#
|
|
551
|
+
# @return [Boolean] true if disable_environment_info was explicitly set
|
|
552
|
+
def disable_environment_info_set?
|
|
553
|
+
!@disable_environment_info.nil?
|
|
554
|
+
end
|
|
555
|
+
|
|
532
556
|
# Check if parameters have been set
|
|
533
557
|
#
|
|
534
558
|
# Used by Swarm::Builder for merging all_agents parameters.
|
|
@@ -586,6 +610,7 @@ module SwarmSDK
|
|
|
586
610
|
agent_config[:shared_across_delegations] = @shared_across_delegations unless @shared_across_delegations.nil?
|
|
587
611
|
agent_config[:streaming] = @streaming unless @streaming.nil?
|
|
588
612
|
agent_config[:thinking] = @thinking if @thinking
|
|
613
|
+
agent_config[:disable_environment_info] = @disable_environment_info unless @disable_environment_info.nil?
|
|
589
614
|
|
|
590
615
|
# Convert DSL hooks to HookDefinition format
|
|
591
616
|
agent_config[:hooks] = convert_hooks_to_definitions if @hooks.any?
|
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -526,17 +526,24 @@ module SwarmSDK
|
|
|
526
526
|
#
|
|
527
527
|
# This method:
|
|
528
528
|
# 1. Serializes concurrent asks via @ask_semaphore
|
|
529
|
-
# 2.
|
|
530
|
-
# 3.
|
|
531
|
-
# 4.
|
|
532
|
-
# 5.
|
|
533
|
-
# 6.
|
|
529
|
+
# 2. Optionally clears conversation context (inside semaphore for safety)
|
|
530
|
+
# 3. Adds CLEAN user message to history (no reminders)
|
|
531
|
+
# 4. Injects system reminders as ephemeral content (sent to LLM but not stored)
|
|
532
|
+
# 5. Triggers user_prompt hooks
|
|
533
|
+
# 6. Acquires global semaphore for LLM call
|
|
534
|
+
# 7. Delegates to RubyLLM::Chat for actual execution
|
|
534
535
|
#
|
|
535
536
|
# @param prompt [String] User prompt
|
|
537
|
+
# @param clear_context [Boolean] When true, clears conversation history before
|
|
538
|
+
# processing. Clearing happens inside the ask_semaphore, making it safe for
|
|
539
|
+
# concurrent callers (e.g., parallel delegations to the same agent).
|
|
536
540
|
# @param options [Hash] Additional options (source: for hooks)
|
|
537
541
|
# @return [RubyLLM::Message] LLM response
|
|
538
|
-
def ask(prompt, **options)
|
|
542
|
+
def ask(prompt, clear_context: false, **options)
|
|
539
543
|
@ask_semaphore.acquire do
|
|
544
|
+
# Clear inside semaphore so concurrent callers don't corrupt each other's messages
|
|
545
|
+
clear_conversation if clear_context
|
|
546
|
+
|
|
540
547
|
if @turn_timeout
|
|
541
548
|
execute_with_turn_timeout(prompt, options)
|
|
542
549
|
else
|
|
@@ -651,57 +658,63 @@ module SwarmSDK
|
|
|
651
658
|
|
|
652
659
|
# Execute ask without timeout (original ask implementation)
|
|
653
660
|
def execute_ask(prompt, options)
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
# Collect system reminders to inject as ephemeral content
|
|
657
|
-
reminders = collect_system_reminders(prompt, is_first)
|
|
661
|
+
@hook_swarm&.mark_agent_active(@agent_name, self)
|
|
658
662
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
663
|
+
begin
|
|
664
|
+
is_first = first_message?
|
|
665
|
+
|
|
666
|
+
# Collect system reminders to inject as ephemeral content
|
|
667
|
+
reminders = collect_system_reminders(prompt, is_first)
|
|
668
|
+
|
|
669
|
+
# Trigger user_prompt hook (with clean prompt, not reminders)
|
|
670
|
+
source = options.delete(:source) || "user"
|
|
671
|
+
final_prompt = prompt
|
|
672
|
+
if @hook_executor
|
|
673
|
+
hook_result = trigger_user_prompt(prompt, source: source)
|
|
674
|
+
|
|
675
|
+
if hook_result[:halted]
|
|
676
|
+
return RubyLLM::Message.new(
|
|
677
|
+
role: :assistant,
|
|
678
|
+
content: hook_result[:halt_message],
|
|
679
|
+
model_id: model_id,
|
|
680
|
+
)
|
|
681
|
+
end
|
|
664
682
|
|
|
665
|
-
|
|
666
|
-
return RubyLLM::Message.new(
|
|
667
|
-
role: :assistant,
|
|
668
|
-
content: hook_result[:halt_message],
|
|
669
|
-
model_id: model_id,
|
|
670
|
-
)
|
|
683
|
+
final_prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt]
|
|
671
684
|
end
|
|
672
685
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
# Add CLEAN user message to history (no reminders embedded)
|
|
677
|
-
@llm_chat.add_message(role: :user, content: final_prompt)
|
|
686
|
+
# Add CLEAN user message to history (no reminders embedded)
|
|
687
|
+
@llm_chat.add_message(role: :user, content: final_prompt)
|
|
678
688
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
# Execute complete() which handles tool loop and ephemeral injection
|
|
686
|
-
response = execute_with_global_semaphore do
|
|
687
|
-
catch(:finish_agent) do
|
|
688
|
-
catch(:finish_swarm) do
|
|
689
|
-
if @streaming_enabled
|
|
690
|
-
# Reset chunk type tracking for new streaming request
|
|
691
|
-
@last_chunk_type = nil
|
|
689
|
+
# Track reminders as ephemeral content for this LLM call only
|
|
690
|
+
# They'll be injected by around_llm_request hook but not stored
|
|
691
|
+
reminders.each do |reminder|
|
|
692
|
+
@context_manager.add_ephemeral_reminder(reminder, messages_array: @llm_chat.messages)
|
|
693
|
+
end
|
|
692
694
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
+
# Execute complete() which handles tool loop and ephemeral injection
|
|
696
|
+
response = execute_with_global_semaphore do
|
|
697
|
+
catch(:finish_agent) do
|
|
698
|
+
catch(:finish_swarm) do
|
|
699
|
+
if @streaming_enabled
|
|
700
|
+
# Reset chunk type tracking for new streaming request
|
|
701
|
+
@last_chunk_type = nil
|
|
702
|
+
|
|
703
|
+
@llm_chat.complete(**options) do |chunk|
|
|
704
|
+
emit_content_chunk(chunk)
|
|
705
|
+
end
|
|
706
|
+
else
|
|
707
|
+
@llm_chat.complete(**options)
|
|
695
708
|
end
|
|
696
|
-
else
|
|
697
|
-
@llm_chat.complete(**options)
|
|
698
709
|
end
|
|
699
710
|
end
|
|
700
711
|
end
|
|
701
|
-
end
|
|
702
712
|
|
|
703
|
-
|
|
704
|
-
|
|
713
|
+
# Handle finish markers from hooks
|
|
714
|
+
handle_finish_marker(response)
|
|
715
|
+
ensure
|
|
716
|
+
@hook_swarm&.mark_agent_inactive(@agent_name)
|
|
717
|
+
end
|
|
705
718
|
end
|
|
706
719
|
|
|
707
720
|
# --- Tool Execution Hook ---
|
|
@@ -43,7 +43,8 @@ module SwarmSDK
|
|
|
43
43
|
:plugin_configs,
|
|
44
44
|
:shared_across_delegations,
|
|
45
45
|
:streaming,
|
|
46
|
-
:thinking
|
|
46
|
+
:thinking,
|
|
47
|
+
:disable_environment_info
|
|
47
48
|
|
|
48
49
|
attr_accessor :bypass_permissions, :max_concurrent_tools
|
|
49
50
|
|
|
@@ -118,6 +119,9 @@ module SwarmSDK
|
|
|
118
119
|
# Extended thinking configuration (nil = disabled)
|
|
119
120
|
@thinking = config[:thinking]
|
|
120
121
|
|
|
122
|
+
# When true, omits date/platform/OS/working directory from system prompts
|
|
123
|
+
@disable_environment_info = config.fetch(:disable_environment_info, false)
|
|
124
|
+
|
|
121
125
|
# Build system prompt after directory and memory are set
|
|
122
126
|
@system_prompt = build_full_system_prompt(config[:system_prompt])
|
|
123
127
|
|
|
@@ -201,6 +205,7 @@ module SwarmSDK
|
|
|
201
205
|
hooks: @hooks,
|
|
202
206
|
shared_across_delegations: @shared_across_delegations,
|
|
203
207
|
streaming: @streaming,
|
|
208
|
+
disable_environment_info: @disable_environment_info,
|
|
204
209
|
# Permissions are core SDK functionality (not plugin-specific)
|
|
205
210
|
default_permissions: @default_permissions,
|
|
206
211
|
permissions: @agent_permissions,
|
|
@@ -301,6 +306,7 @@ module SwarmSDK
|
|
|
301
306
|
disable_default_tools: @disable_default_tools,
|
|
302
307
|
directory: @directory,
|
|
303
308
|
definition: self,
|
|
309
|
+
disable_environment_info: @disable_environment_info,
|
|
304
310
|
)
|
|
305
311
|
end
|
|
306
312
|
|
|
@@ -394,6 +400,7 @@ module SwarmSDK
|
|
|
394
400
|
:shared_across_delegations,
|
|
395
401
|
:streaming,
|
|
396
402
|
:directories,
|
|
403
|
+
:disable_environment_info,
|
|
397
404
|
]
|
|
398
405
|
|
|
399
406
|
config.reject { |k, _| standard_keys.include?(k.to_sym) }
|
|
@@ -28,24 +28,29 @@ module SwarmSDK
|
|
|
28
28
|
# @param disable_default_tools [Boolean, Array, nil] Default tools disable configuration
|
|
29
29
|
# @param directory [String] Agent's working directory
|
|
30
30
|
# @param definition [Definition] Full definition for plugin contributions
|
|
31
|
+
# @param disable_environment_info [Boolean] Whether to omit environment info from prompt
|
|
31
32
|
# @return [String] Complete system prompt
|
|
32
|
-
def build(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition
|
|
33
|
+
def build(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:,
|
|
34
|
+
disable_environment_info: false)
|
|
33
35
|
new(
|
|
34
36
|
custom_prompt: custom_prompt,
|
|
35
37
|
coding_agent: coding_agent,
|
|
36
38
|
disable_default_tools: disable_default_tools,
|
|
37
39
|
directory: directory,
|
|
38
40
|
definition: definition,
|
|
41
|
+
disable_environment_info: disable_environment_info,
|
|
39
42
|
).build
|
|
40
43
|
end
|
|
41
44
|
end
|
|
42
45
|
|
|
43
|
-
def initialize(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition
|
|
46
|
+
def initialize(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:,
|
|
47
|
+
disable_environment_info: false)
|
|
44
48
|
@custom_prompt = custom_prompt
|
|
45
49
|
@coding_agent = coding_agent
|
|
46
50
|
@disable_default_tools = disable_default_tools
|
|
47
51
|
@directory = directory
|
|
48
52
|
@definition = definition
|
|
53
|
+
@disable_environment_info = disable_environment_info
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def build
|
|
@@ -80,7 +85,11 @@ module SwarmSDK
|
|
|
80
85
|
non_coding_base = render_non_coding_base_prompt
|
|
81
86
|
|
|
82
87
|
if @custom_prompt && !@custom_prompt.strip.empty?
|
|
83
|
-
|
|
88
|
+
if non_coding_base.empty?
|
|
89
|
+
@custom_prompt.to_s
|
|
90
|
+
else
|
|
91
|
+
"#{non_coding_base}\n\n#{@custom_prompt}"
|
|
92
|
+
end
|
|
84
93
|
else
|
|
85
94
|
non_coding_base
|
|
86
95
|
end
|
|
@@ -91,6 +100,7 @@ module SwarmSDK
|
|
|
91
100
|
end
|
|
92
101
|
|
|
93
102
|
def render_base_system_prompt
|
|
103
|
+
disable_environment_info = @disable_environment_info
|
|
94
104
|
cwd = @directory || Dir.pwd
|
|
95
105
|
platform = RUBY_PLATFORM
|
|
96
106
|
os_version = begin
|
|
@@ -105,6 +115,8 @@ module SwarmSDK
|
|
|
105
115
|
end
|
|
106
116
|
|
|
107
117
|
def render_non_coding_base_prompt
|
|
118
|
+
return "" if @disable_environment_info
|
|
119
|
+
|
|
108
120
|
cwd = @directory || Dir.pwd
|
|
109
121
|
platform = RUBY_PLATFORM
|
|
110
122
|
os_version = begin
|
|
@@ -265,6 +265,7 @@ module SwarmSDK
|
|
|
265
265
|
builder.parameters(config[:parameters]) if config[:parameters]
|
|
266
266
|
builder.headers(config[:headers]) if config[:headers]
|
|
267
267
|
builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
|
|
268
|
+
builder.disable_environment_info(config[:disable_environment_info]) unless config[:disable_environment_info].nil?
|
|
268
269
|
builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
|
269
270
|
builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
270
271
|
|
|
@@ -451,6 +452,10 @@ module SwarmSDK
|
|
|
451
452
|
agent_builder.streaming(all_agents_hash[:streaming])
|
|
452
453
|
end
|
|
453
454
|
|
|
455
|
+
if !all_agents_hash[:disable_environment_info].nil? && !agent_builder.disable_environment_info_set?
|
|
456
|
+
agent_builder.disable_environment_info(all_agents_hash[:disable_environment_info])
|
|
457
|
+
end
|
|
458
|
+
|
|
454
459
|
if all_agents_hash[:thinking] && !agent_builder.thinking_set?
|
|
455
460
|
agent_builder.thinking(**all_agents_hash[:thinking])
|
|
456
461
|
end
|
data/lib/swarm_sdk/config.rb
CHANGED
|
@@ -105,6 +105,7 @@ module SwarmSDK
|
|
|
105
105
|
allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
|
|
106
106
|
env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
|
|
107
107
|
streaming: ["SWARM_SDK_STREAMING", true],
|
|
108
|
+
mcp_ssl_verify: ["SWARM_SDK_MCP_SSL_VERIFY", true],
|
|
108
109
|
}.freeze
|
|
109
110
|
|
|
110
111
|
class << self
|
|
@@ -345,7 +346,7 @@ module SwarmSDK
|
|
|
345
346
|
# @return [Integer, Float, Boolean, String] The parsed value
|
|
346
347
|
def parse_env_value(value, key)
|
|
347
348
|
case key
|
|
348
|
-
when :allow_filesystem_tools, :env_interpolation, :streaming
|
|
349
|
+
when :allow_filesystem_tools, :env_interpolation, :streaming, :mcp_ssl_verify
|
|
349
350
|
# Convert string to boolean
|
|
350
351
|
case value.to_s.downcase
|
|
351
352
|
when "true", "yes", "1", "on", "enabled"
|
|
@@ -100,6 +100,7 @@ module SwarmSDK
|
|
|
100
100
|
coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
|
|
101
101
|
disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
|
|
102
102
|
streaming(all_agents_cfg[:streaming]) unless all_agents_cfg[:streaming].nil?
|
|
103
|
+
disable_environment_info(all_agents_cfg[:disable_environment_info]) unless all_agents_cfg[:disable_environment_info].nil?
|
|
103
104
|
thinking(**all_agents_cfg[:thinking]) if all_agents_cfg[:thinking]
|
|
104
105
|
|
|
105
106
|
if all_agents_hks.any?
|
|
@@ -165,6 +166,7 @@ module SwarmSDK
|
|
|
165
166
|
disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
166
167
|
shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
|
|
167
168
|
streaming(config[:streaming]) unless config[:streaming].nil?
|
|
169
|
+
disable_environment_info(config[:disable_environment_info]) unless config[:disable_environment_info].nil?
|
|
168
170
|
thinking(**config[:thinking]) if config[:thinking]
|
|
169
171
|
|
|
170
172
|
if config[:tools]&.any?
|
|
@@ -75,6 +75,18 @@ module SwarmSDK
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Stop all observer tasks immediately
|
|
79
|
+
#
|
|
80
|
+
# Interrupts in-flight observer LLM calls by stopping the barrier.
|
|
81
|
+
# Called during swarm interruption instead of wait_for_completion.
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
def stop
|
|
85
|
+
@barrier&.stop
|
|
86
|
+
rescue StandardError => e
|
|
87
|
+
RubyLLM.logger.debug("SwarmSDK: Error stopping observer barrier: #{e.message}")
|
|
88
|
+
end
|
|
89
|
+
|
|
78
90
|
# Cleanup all subscriptions
|
|
79
91
|
#
|
|
80
92
|
# Unsubscribes from LogCollector to prevent memory leaks.
|
|
@@ -90,6 +90,7 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
|
|
|
90
90
|
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
<% unless disable_environment_info %>
|
|
93
94
|
# Today's date
|
|
94
95
|
|
|
95
96
|
<today-date>
|
|
@@ -104,6 +105,7 @@ Working directory: <%= cwd %>
|
|
|
104
105
|
Platform: <%= platform %>
|
|
105
106
|
OS Version: <%= os_version %>
|
|
106
107
|
</env>
|
|
108
|
+
<% end %>
|
|
107
109
|
|
|
108
110
|
IMPORTANT: Assist with defensive security tasks only. Refuse to create, modify, or improve code that may be used maliciously. Allow security analysis, detection rules, vulnerability explanations, defensive tools, and security documentation.
|
|
109
111
|
|
data/lib/swarm_sdk/result.rb
CHANGED
|
@@ -23,6 +23,35 @@ module SwarmSDK
|
|
|
23
23
|
!success?
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Check if execution was interrupted via swarm.stop
|
|
27
|
+
#
|
|
28
|
+
# @return [Boolean] true if execution was interrupted
|
|
29
|
+
#
|
|
30
|
+
# @example
|
|
31
|
+
# result = swarm.execute("Build auth")
|
|
32
|
+
# result.interrupted? # => false
|
|
33
|
+
#
|
|
34
|
+
# # After calling swarm.stop during execution:
|
|
35
|
+
# result.interrupted? # => true
|
|
36
|
+
def interrupted?
|
|
37
|
+
@metadata[:interrupted] == true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Get the reason execution finished
|
|
41
|
+
#
|
|
42
|
+
# Possible values:
|
|
43
|
+
# - "finished" - Normal completion
|
|
44
|
+
# - "interrupted" - Stopped via swarm.stop
|
|
45
|
+
# - "error" - Execution failed with an error
|
|
46
|
+
#
|
|
47
|
+
# @return [String] The finish reason
|
|
48
|
+
#
|
|
49
|
+
# @example
|
|
50
|
+
# result.finish_reason # => "finished"
|
|
51
|
+
def finish_reason
|
|
52
|
+
@metadata[:finish_reason] || (success? ? "finished" : "error")
|
|
53
|
+
end
|
|
54
|
+
|
|
26
55
|
# Calculate total cost from logs
|
|
27
56
|
#
|
|
28
57
|
# Delegates to total_cost for consistency. This attribute is calculated
|
|
@@ -153,30 +153,34 @@ module RubyLLM
|
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
|
|
156
|
-
# Override complete to use emit() and support around_llm_request hook
|
|
157
|
-
#
|
|
156
|
+
# Override complete to use emit() and support around_llm_request hook.
|
|
157
|
+
# Uses a trampoline loop instead of mutual recursion with handle_tool_calls
|
|
158
|
+
# to avoid stack growth during multi-round tool-call conversations.
|
|
158
159
|
def complete(&block)
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
loop do
|
|
161
|
+
response = execute_llm_request(&block)
|
|
161
162
|
|
|
162
|
-
|
|
163
|
+
emit(:new_message) unless block_given?
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
if @schema && response.content.is_a?(String)
|
|
166
|
+
begin
|
|
167
|
+
response.content = JSON.parse(response.content)
|
|
168
|
+
rescue JSON::ParserError
|
|
169
|
+
# If parsing fails, keep content as string
|
|
170
|
+
end
|
|
169
171
|
end
|
|
170
|
-
end
|
|
171
172
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
173
|
+
add_message(response)
|
|
174
|
+
emit(:end_message, response)
|
|
175
|
+
|
|
176
|
+
if response.tool_call?
|
|
177
|
+
halt_result = handle_tool_calls(response, &block)
|
|
178
|
+
return halt_result if halt_result
|
|
179
|
+
|
|
180
|
+
# Loop continues: next LLM call with zero stack growth
|
|
181
|
+
else
|
|
182
|
+
return response
|
|
183
|
+
end
|
|
180
184
|
end
|
|
181
185
|
end
|
|
182
186
|
|
|
@@ -238,8 +242,9 @@ module RubyLLM
|
|
|
238
242
|
end
|
|
239
243
|
end
|
|
240
244
|
|
|
241
|
-
#
|
|
242
|
-
|
|
245
|
+
# Execute tool calls and return halt result (or nil to continue the loop).
|
|
246
|
+
# Does NOT recurse back into complete() — the trampoline loop handles that.
|
|
247
|
+
def handle_tool_calls(response, &_block)
|
|
243
248
|
halt_result = nil
|
|
244
249
|
|
|
245
250
|
response.tool_calls.each_value do |tool_call|
|
|
@@ -259,7 +264,7 @@ module RubyLLM
|
|
|
259
264
|
halt_result = result if result.is_a?(Tool::Halt)
|
|
260
265
|
end
|
|
261
266
|
|
|
262
|
-
halt_result
|
|
267
|
+
halt_result
|
|
263
268
|
end
|
|
264
269
|
|
|
265
270
|
# Execute tool with around_tool_execution hook if set
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# Extends RubyLLM::Configuration with additional options:
|
|
4
4
|
# - anthropic_api_base: Configurable Anthropic API base URL
|
|
5
5
|
# - read_timeout, open_timeout, write_timeout: Granular timeout configuration
|
|
6
|
+
# - Fixes Anthropic completion_url leading slash that breaks proxy base URLs
|
|
6
7
|
#
|
|
7
8
|
# Fork Reference: Commits da6144b, 3daa4fb
|
|
8
9
|
|
|
@@ -29,13 +30,25 @@ module RubyLLM
|
|
|
29
30
|
end
|
|
30
31
|
end
|
|
31
32
|
|
|
32
|
-
# Patch Anthropic provider to use configurable base URL
|
|
33
|
+
# Patch Anthropic provider to use configurable base URL and fix completion_url
|
|
33
34
|
module Providers
|
|
34
35
|
class Anthropic
|
|
35
36
|
# Override api_base to use configurable base URL
|
|
36
37
|
def api_base
|
|
37
38
|
@config.anthropic_api_base || "https://api.anthropic.com"
|
|
38
39
|
end
|
|
40
|
+
|
|
41
|
+
# Fix completion_url to use relative path (no leading slash).
|
|
42
|
+
# The leading slash causes Faraday to discard the base URL path component,
|
|
43
|
+
# breaking proxy configurations where api_base includes a path segment
|
|
44
|
+
# (e.g., https://proxy.dev/apis/anthropic/v1/messages → https://proxy.dev/v1/messages).
|
|
45
|
+
# stream_url delegates to completion_url, so this fixes both sync and streaming.
|
|
46
|
+
# Can be removed once RubyLLM releases a version including upstream fix (commit da6144b).
|
|
47
|
+
module Chat
|
|
48
|
+
def completion_url
|
|
49
|
+
"v1/messages"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
39
52
|
end
|
|
40
53
|
end
|
|
41
54
|
end
|
|
@@ -39,3 +39,6 @@ require_relative "message_management_patch"
|
|
|
39
39
|
|
|
40
40
|
# 7. Responses API patch (depends on configuration, uses error classes)
|
|
41
41
|
require_relative "responses_api_patch"
|
|
42
|
+
|
|
43
|
+
# 8. MCP SSL patch (configures SSL for HTTPX connections in ruby_llm-mcp)
|
|
44
|
+
require_relative "mcp_ssl_patch"
|