swarm_sdk 2.7.10 → 2.7.11
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/ruby_llm_patches/chat_callbacks_patch.rb +284 -0
- data/lib/swarm_sdk/ruby_llm_patches/configuration_patch.rb +41 -0
- data/lib/swarm_sdk/ruby_llm_patches/connection_patch.rb +47 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +41 -0
- data/lib/swarm_sdk/ruby_llm_patches/io_endpoint_patch.rb +40 -0
- data/lib/swarm_sdk/ruby_llm_patches/message_management_patch.rb +23 -0
- data/lib/swarm_sdk/ruby_llm_patches/responses_api_patch.rb +599 -0
- data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +218 -0
- data/lib/swarm_sdk/tools/delegate.rb +2 -2
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +22 -2
- metadata +12 -18
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Adds concurrent tool execution support to RubyLLM::Chat
|
|
4
|
+
# Supports :async and :threads executors with configurable concurrency limits
|
|
5
|
+
#
|
|
6
|
+
# Fork Reference: Commit d0912c7, file lib/ruby_llm/tool_executors.rb
|
|
7
|
+
|
|
8
|
+
module RubyLLM
|
|
9
|
+
# Tool executor registry
|
|
10
|
+
class << self
|
|
11
|
+
def tool_executors
|
|
12
|
+
@tool_executors ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def register_tool_executor(name, &block)
|
|
16
|
+
tool_executors[name] = block
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def get_tool_executor(name)
|
|
20
|
+
tool_executors[name] || raise(ArgumentError, "Unknown tool executor: #{name}")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Built-in tool executors
|
|
25
|
+
module ToolExecutors
|
|
26
|
+
class << self
|
|
27
|
+
def register_defaults
|
|
28
|
+
register_threads_executor
|
|
29
|
+
register_async_executor
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def register_threads_executor
|
|
35
|
+
RubyLLM.register_tool_executor(:threads) do |tool_calls, max_concurrency:, &execute|
|
|
36
|
+
results = {}
|
|
37
|
+
mutex = Mutex.new
|
|
38
|
+
semaphore = max_concurrency ? Thread::SizedQueue.new(max_concurrency) : nil
|
|
39
|
+
|
|
40
|
+
# Fill semaphore with permits
|
|
41
|
+
max_concurrency&.times { semaphore << :permit }
|
|
42
|
+
|
|
43
|
+
threads = tool_calls.map do |tool_call|
|
|
44
|
+
Thread.new do
|
|
45
|
+
permit = semaphore&.pop
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
result = execute.call(tool_call)
|
|
49
|
+
mutex.synchronize { results[tool_call.id] = result }
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
error_result = "Error: #{e.class}: #{e.message}"
|
|
52
|
+
mutex.synchronize { results[tool_call.id] = error_result }
|
|
53
|
+
RubyLLM.logger.warn("[RubyLLM] Tool #{tool_call.id} failed: #{e.message}")
|
|
54
|
+
ensure
|
|
55
|
+
semaphore&.push(permit) if permit
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
threads.each(&:join)
|
|
61
|
+
results
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def register_async_executor
|
|
66
|
+
RubyLLM.register_tool_executor(:async) do |tool_calls, max_concurrency:, &execute|
|
|
67
|
+
AsyncExecutor.execute(tool_calls, max_concurrency: max_concurrency, &execute)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
module AsyncExecutor
|
|
73
|
+
class << self
|
|
74
|
+
def execute(tool_calls, max_concurrency:, &block)
|
|
75
|
+
load_async_gem
|
|
76
|
+
run_with_sync { execute_tools(tool_calls, max_concurrency, &block) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def load_async_gem
|
|
82
|
+
require "async"
|
|
83
|
+
require "async/barrier"
|
|
84
|
+
require "async/semaphore"
|
|
85
|
+
rescue LoadError => e
|
|
86
|
+
raise LoadError,
|
|
87
|
+
"The async gem is required for :async tool executor. " \
|
|
88
|
+
"Add `gem 'async'` to your Gemfile. Original error: #{e.message}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_with_sync(&)
|
|
92
|
+
if defined?(Sync)
|
|
93
|
+
Sync(&)
|
|
94
|
+
else
|
|
95
|
+
Async(&).wait
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def execute_tools(tool_calls, max_concurrency)
|
|
100
|
+
semaphore = max_concurrency ? Async::Semaphore.new(max_concurrency) : nil
|
|
101
|
+
barrier = Async::Barrier.new
|
|
102
|
+
results = {}
|
|
103
|
+
|
|
104
|
+
tool_calls.each do |tool_call|
|
|
105
|
+
barrier.async do
|
|
106
|
+
results[tool_call.id] = execute_single_tool(tool_call, semaphore) { yield tool_call }
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
results[tool_call.id] = "Error: #{e.class}: #{e.message}"
|
|
109
|
+
RubyLLM.logger.warn("[RubyLLM] Tool #{tool_call.id} failed: #{e.message}")
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
barrier.wait
|
|
114
|
+
results
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def execute_single_tool(_tool_call, semaphore, &)
|
|
118
|
+
if semaphore
|
|
119
|
+
semaphore.acquire(&)
|
|
120
|
+
else
|
|
121
|
+
yield
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
class Chat
|
|
129
|
+
attr_reader :tool_concurrency, :max_concurrency
|
|
130
|
+
|
|
131
|
+
# Module to prepend for concurrent tool execution
|
|
132
|
+
module ConcurrentToolExecution
|
|
133
|
+
def initialize(tool_concurrency: nil, max_concurrency: nil, **kwargs)
|
|
134
|
+
@tool_concurrency = tool_concurrency
|
|
135
|
+
@max_concurrency = max_concurrency
|
|
136
|
+
super(**kwargs)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Configure tool concurrency
|
|
140
|
+
def with_tool_concurrency(executor, max: nil)
|
|
141
|
+
@tool_concurrency = executor
|
|
142
|
+
@max_concurrency = max
|
|
143
|
+
self
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
# Override handle_tool_calls to support concurrent execution
|
|
149
|
+
# This method is called when tool_concurrency is set
|
|
150
|
+
def handle_tool_calls(response, &block)
|
|
151
|
+
return super unless @tool_concurrency
|
|
152
|
+
|
|
153
|
+
tool_calls = response.tool_calls
|
|
154
|
+
halt_result = execute_tools_concurrently(tool_calls)
|
|
155
|
+
halt_result || complete(&block)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def execute_tools_concurrently(tool_calls)
|
|
159
|
+
executor = RubyLLM.get_tool_executor(@tool_concurrency)
|
|
160
|
+
tool_calls_array = tool_calls.values
|
|
161
|
+
|
|
162
|
+
# Execute tools concurrently, emitting events per-tool
|
|
163
|
+
results = executor.call(tool_calls_array, max_concurrency: @max_concurrency) do |tool_call|
|
|
164
|
+
execute_single_tool_with_events(tool_call)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Add all tool result messages atomically
|
|
168
|
+
add_tool_results_atomically(tool_calls, results)
|
|
169
|
+
|
|
170
|
+
# Find first halt result by original order
|
|
171
|
+
find_first_halt(tool_calls, results)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Execute a single tool with events (for concurrent execution)
|
|
175
|
+
# Emits new_message, tool_call, and tool_result events per-tool
|
|
176
|
+
def execute_single_tool_with_events(tool_call)
|
|
177
|
+
emit(:new_message)
|
|
178
|
+
emit(:tool_call, tool_call)
|
|
179
|
+
result = execute_tool_with_hook(tool_call)
|
|
180
|
+
emit(:tool_result, tool_call, result)
|
|
181
|
+
result
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Add all tool result messages atomically to ensure consistent state
|
|
185
|
+
def add_tool_results_atomically(tool_calls, results)
|
|
186
|
+
messages = []
|
|
187
|
+
|
|
188
|
+
tool_calls.each_key do |id|
|
|
189
|
+
tool_call = tool_calls[id]
|
|
190
|
+
result = results[id]
|
|
191
|
+
|
|
192
|
+
tool_payload = result.is_a?(Tool::Halt) ? result.content : result
|
|
193
|
+
content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
|
|
194
|
+
message = add_message(role: :tool, content: content, tool_call_id: tool_call.id)
|
|
195
|
+
messages << message
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Fire end_message events for all messages
|
|
199
|
+
messages.each { |msg| emit(:end_message, msg) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Find the first halt result by request order
|
|
203
|
+
def find_first_halt(tool_calls, results)
|
|
204
|
+
tool_calls.each_key do |id|
|
|
205
|
+
result = results[id]
|
|
206
|
+
return result if result.is_a?(Tool::Halt)
|
|
207
|
+
end
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Prepend after MultiSubscriberCallbacks so we can call its methods
|
|
213
|
+
prepend ConcurrentToolExecution
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Register built-in executors
|
|
218
|
+
RubyLLM::ToolExecutors.register_defaults
|
|
@@ -309,8 +309,8 @@ module SwarmSDK
|
|
|
309
309
|
# Reset swarm if reset_context is true
|
|
310
310
|
swarm_registry.reset(@delegate_target) if reset_context
|
|
311
311
|
|
|
312
|
-
# Execute sub-swarm's lead agent
|
|
313
|
-
lead_agent = subswarm.
|
|
312
|
+
# Execute sub-swarm's lead agent (uses agent() to trigger lazy initialization)
|
|
313
|
+
lead_agent = subswarm.agent(subswarm.lead_agent)
|
|
314
314
|
response = lead_agent.ask(message, source: "delegation")
|
|
315
315
|
result = response.content
|
|
316
316
|
|
data/lib/swarm_sdk/version.rb
CHANGED
data/lib/swarm_sdk.rb
CHANGED
|
@@ -18,14 +18,32 @@ require "async/semaphore"
|
|
|
18
18
|
require "ruby_llm"
|
|
19
19
|
require "ruby_llm/mcp"
|
|
20
20
|
|
|
21
|
-
#
|
|
21
|
+
# Load ruby_llm compatibility patches
|
|
22
|
+
# These patches extend upstream ruby_llm to match fork functionality used by SwarmSDK
|
|
23
|
+
require_relative "swarm_sdk/ruby_llm_patches/init"
|
|
24
|
+
|
|
25
|
+
# Patch Zeitwerk loaders to ignore Rails-dependent files when Rails is not present
|
|
22
26
|
# This prevents NameError when eager loading outside of Rails applications
|
|
23
27
|
unless defined?(Rails)
|
|
24
28
|
require "zeitwerk"
|
|
29
|
+
|
|
30
|
+
# Ignore ruby_llm's ActiveRecord integration (requires ActiveSupport)
|
|
31
|
+
ruby_llm_loader = nil
|
|
32
|
+
Zeitwerk::Registry.loaders.each { |l| ruby_llm_loader = l if l.tag == "ruby_llm" }
|
|
33
|
+
if ruby_llm_loader
|
|
34
|
+
ruby_llm_gem_dir = Gem.loaded_specs["ruby_llm"]&.gem_dir
|
|
35
|
+
if ruby_llm_gem_dir
|
|
36
|
+
active_record_dir = File.join(ruby_llm_gem_dir, "lib", "ruby_llm", "active_record")
|
|
37
|
+
ruby_llm_loader.ignore(active_record_dir)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Ignore ruby_llm-mcp's railtie
|
|
25
42
|
mcp_loader = nil
|
|
26
43
|
Zeitwerk::Registry.loaders.each { |l| mcp_loader = l if l.tag == "RubyLLM-mcp" }
|
|
27
44
|
if mcp_loader
|
|
28
|
-
mcp_gem_dir = Gem.loaded_specs["
|
|
45
|
+
mcp_gem_dir = Gem.loaded_specs["ruby_llm-mcp"]&.gem_dir ||
|
|
46
|
+
Gem.loaded_specs["ruby_llm_swarm-mcp"]&.gem_dir
|
|
29
47
|
if mcp_gem_dir
|
|
30
48
|
railtie_path = File.join(mcp_gem_dir, "lib", "ruby_llm", "mcp", "railtie.rb")
|
|
31
49
|
mcp_loader.ignore(railtie_path)
|
|
@@ -51,6 +69,8 @@ loader.inflector.inflect(
|
|
|
51
69
|
"mcp" => "MCP",
|
|
52
70
|
"openai_with_responses" => "OpenAIWithResponses",
|
|
53
71
|
)
|
|
72
|
+
# Ignore ruby_llm_patches - these are manually required monkey patches, not autoloaded modules
|
|
73
|
+
loader.ignore("#{__dir__}/swarm_sdk/ruby_llm_patches")
|
|
54
74
|
loader.setup
|
|
55
75
|
|
|
56
76
|
module SwarmSDK
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: swarm_sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.7.
|
|
4
|
+
version: 2.7.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Arruda
|
|
@@ -52,33 +52,19 @@ dependencies:
|
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
53
|
version: '0.4'
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
55
|
+
name: ruby_llm
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - "~>"
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version:
|
|
60
|
+
version: '1.11'
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version:
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: ruby_llm_swarm
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: 1.9.7
|
|
75
|
-
type: :runtime
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: 1.9.7
|
|
67
|
+
version: '1.11'
|
|
82
68
|
- !ruby/object:Gem::Dependency
|
|
83
69
|
name: ruby_llm_swarm-mcp
|
|
84
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -190,6 +176,14 @@ files:
|
|
|
190
176
|
- lib/swarm_sdk/prompts/base_system_prompt.md.erb
|
|
191
177
|
- lib/swarm_sdk/restore_result.rb
|
|
192
178
|
- lib/swarm_sdk/result.rb
|
|
179
|
+
- lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb
|
|
180
|
+
- lib/swarm_sdk/ruby_llm_patches/configuration_patch.rb
|
|
181
|
+
- lib/swarm_sdk/ruby_llm_patches/connection_patch.rb
|
|
182
|
+
- lib/swarm_sdk/ruby_llm_patches/init.rb
|
|
183
|
+
- lib/swarm_sdk/ruby_llm_patches/io_endpoint_patch.rb
|
|
184
|
+
- lib/swarm_sdk/ruby_llm_patches/message_management_patch.rb
|
|
185
|
+
- lib/swarm_sdk/ruby_llm_patches/responses_api_patch.rb
|
|
186
|
+
- lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb
|
|
193
187
|
- lib/swarm_sdk/snapshot.rb
|
|
194
188
|
- lib/swarm_sdk/snapshot_from_events.rb
|
|
195
189
|
- lib/swarm_sdk/state_restorer.rb
|