claude_swarm 0.3.5 → 0.3.7
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/CHANGELOG.md +21 -0
- data/README.md +3 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +19 -137
- data/lib/claude_swarm/claude_mcp_server.rb +2 -1
- data/lib/claude_swarm/cli.rb +1 -0
- data/lib/claude_swarm/mcp_generator.rb +3 -1
- data/lib/claude_swarm/openai/chat_completion.rb +15 -15
- data/lib/claude_swarm/openai/executor.rb +69 -161
- data/lib/claude_swarm/openai/responses.rb +27 -27
- data/lib/claude_swarm/orchestrator.rb +153 -171
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +1 -0
- metadata +16 -1
@@ -2,25 +2,28 @@
|
|
2
2
|
|
3
3
|
module ClaudeSwarm
|
4
4
|
module OpenAI
|
5
|
-
class Executor
|
6
|
-
attr_reader :session_id, :last_response, :working_directory, :logger, :session_path
|
7
|
-
|
5
|
+
class Executor < BaseExecutor
|
8
6
|
def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
|
9
7
|
instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
|
10
|
-
claude_session_id: nil, additional_directories: [],
|
8
|
+
claude_session_id: nil, additional_directories: [], debug: false,
|
11
9
|
temperature: nil, api_version: "chat_completion", openai_token_env: "OPENAI_API_KEY",
|
12
10
|
base_url: nil, reasoning_effort: nil)
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
11
|
+
# Call parent initializer for common attributes
|
12
|
+
super(
|
13
|
+
working_directory: working_directory,
|
14
|
+
model: model,
|
15
|
+
mcp_config: mcp_config,
|
16
|
+
vibe: vibe,
|
17
|
+
instance_name: instance_name,
|
18
|
+
instance_id: instance_id,
|
19
|
+
calling_instance: calling_instance,
|
20
|
+
calling_instance_id: calling_instance_id,
|
21
|
+
claude_session_id: claude_session_id,
|
22
|
+
additional_directories: additional_directories,
|
23
|
+
debug: debug
|
24
|
+
)
|
25
|
+
|
26
|
+
# OpenAI-specific attributes
|
24
27
|
@temperature = temperature
|
25
28
|
@api_version = api_version
|
26
29
|
@base_url = base_url
|
@@ -30,37 +33,14 @@ module ClaudeSwarm
|
|
30
33
|
@conversation_messages = []
|
31
34
|
@previous_response_id = nil
|
32
35
|
|
33
|
-
# Setup logging first
|
34
|
-
setup_logging
|
35
|
-
|
36
36
|
# Setup OpenAI client
|
37
37
|
setup_openai_client(openai_token_env)
|
38
38
|
|
39
39
|
# Setup MCP client for tools
|
40
40
|
setup_mcp_client
|
41
41
|
|
42
|
-
# Create API
|
43
|
-
@
|
44
|
-
openai_client: @openai_client,
|
45
|
-
mcp_client: @mcp_client,
|
46
|
-
available_tools: @available_tools,
|
47
|
-
logger: self,
|
48
|
-
instance_name: @instance_name,
|
49
|
-
model: @model,
|
50
|
-
temperature: @temperature,
|
51
|
-
reasoning_effort: @reasoning_effort,
|
52
|
-
)
|
53
|
-
|
54
|
-
@responses_handler = OpenAI::Responses.new(
|
55
|
-
openai_client: @openai_client,
|
56
|
-
mcp_client: @mcp_client,
|
57
|
-
available_tools: @available_tools,
|
58
|
-
logger: self,
|
59
|
-
instance_name: @instance_name,
|
60
|
-
model: @model,
|
61
|
-
temperature: @temperature,
|
62
|
-
reasoning_effort: @reasoning_effort,
|
63
|
-
)
|
42
|
+
# Create API handler based on api_version
|
43
|
+
@api_handler = create_api_handler
|
64
44
|
end
|
65
45
|
|
66
46
|
def execute(prompt, options = {})
|
@@ -70,12 +50,8 @@ module ClaudeSwarm
|
|
70
50
|
# Start timing
|
71
51
|
start_time = Time.now
|
72
52
|
|
73
|
-
# Execute
|
74
|
-
result =
|
75
|
-
@responses_handler.execute(prompt, options)
|
76
|
-
else
|
77
|
-
@chat_completion_handler.execute(prompt, options)
|
78
|
-
end
|
53
|
+
# Execute using the appropriate handler
|
54
|
+
result = @api_handler.execute(prompt, options)
|
79
55
|
|
80
56
|
# Calculate duration
|
81
57
|
duration_ms = ((Time.now - start_time) * 1000).round
|
@@ -94,37 +70,14 @@ module ClaudeSwarm
|
|
94
70
|
@last_response = response
|
95
71
|
response
|
96
72
|
rescue StandardError => e
|
97
|
-
|
98
|
-
|
73
|
+
logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
|
74
|
+
logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
|
99
75
|
raise
|
100
76
|
end
|
101
77
|
|
102
78
|
def reset_session
|
103
|
-
|
104
|
-
@
|
105
|
-
@chat_completion_handler&.reset_session
|
106
|
-
@responses_handler&.reset_session
|
107
|
-
end
|
108
|
-
|
109
|
-
def has_session?
|
110
|
-
!@session_id.nil?
|
111
|
-
end
|
112
|
-
|
113
|
-
# Delegate logger methods for the API handlers
|
114
|
-
def info(message)
|
115
|
-
@logger.info(message)
|
116
|
-
end
|
117
|
-
|
118
|
-
def error(message)
|
119
|
-
@logger.error(message)
|
120
|
-
end
|
121
|
-
|
122
|
-
def warn(message)
|
123
|
-
@logger.warn(message)
|
124
|
-
end
|
125
|
-
|
126
|
-
def debug(message)
|
127
|
-
@logger.debug(message)
|
79
|
+
super
|
80
|
+
@api_handler&.reset_session
|
128
81
|
end
|
129
82
|
|
130
83
|
# Session JSON logger for the API handlers
|
@@ -146,7 +99,29 @@ module ClaudeSwarm
|
|
146
99
|
}
|
147
100
|
config[:uri_base] = @base_url if @base_url
|
148
101
|
|
149
|
-
@openai_client = ::OpenAI::Client.new(config)
|
102
|
+
@openai_client = ::OpenAI::Client.new(config) do |faraday|
|
103
|
+
# Add retry middleware with custom configuration
|
104
|
+
faraday.request(
|
105
|
+
:retry,
|
106
|
+
max: 3, # Maximum number of retries
|
107
|
+
interval: 0.5, # Initial delay between retries (in seconds)
|
108
|
+
interval_randomness: 0.5, # Randomness factor for retry intervals
|
109
|
+
backoff_factor: 2, # Exponential backoff factor
|
110
|
+
exceptions: [
|
111
|
+
Faraday::TimeoutError,
|
112
|
+
Faraday::ConnectionFailed,
|
113
|
+
Faraday::ServerError, # Retry on 5xx errors
|
114
|
+
],
|
115
|
+
retry_statuses: [429, 500, 502, 503, 504], # HTTP status codes to retry
|
116
|
+
retry_block: lambda do |env:, options:, retry_count:, exception:, will_retry:|
|
117
|
+
if will_retry
|
118
|
+
@logger.warn("Request failed (attempt #{retry_count}/#{options.max}): #{exception&.message || "HTTP #{env.status}"}. Retrying in #{options.interval * (options.backoff_factor**(retry_count - 1))} seconds...")
|
119
|
+
else
|
120
|
+
@logger.warn("Request failed after #{retry_count} attempts: #{exception&.message || "HTTP #{env.status}"}. Giving up.")
|
121
|
+
end
|
122
|
+
end,
|
123
|
+
)
|
124
|
+
end
|
150
125
|
rescue KeyError
|
151
126
|
raise ExecutionError, "OpenAI API key not found in environment variable: #{token_env}"
|
152
127
|
end
|
@@ -175,7 +150,7 @@ module ClaudeSwarm
|
|
175
150
|
stdio_config[:read_timeout] = 1800
|
176
151
|
mcp_configs << stdio_config
|
177
152
|
when "sse"
|
178
|
-
|
153
|
+
logger.warn { "SSE MCP servers not yet supported for OpenAI instances: #{name}" }
|
179
154
|
# TODO: Add SSE support when available in ruby-mcp-client
|
180
155
|
end
|
181
156
|
end
|
@@ -193,16 +168,16 @@ module ClaudeSwarm
|
|
193
168
|
# List available tools from all MCP servers
|
194
169
|
begin
|
195
170
|
@available_tools = @mcp_client.list_tools
|
196
|
-
|
171
|
+
logger.info { "Loaded #{@available_tools.size} tools from #{mcp_configs.size} MCP server(s)" }
|
197
172
|
rescue StandardError => e
|
198
|
-
|
173
|
+
logger.error { "Failed to load MCP tools: #{e.message}" }
|
199
174
|
@available_tools = []
|
200
175
|
end
|
201
176
|
end
|
202
177
|
end
|
203
178
|
end
|
204
179
|
rescue StandardError => e
|
205
|
-
|
180
|
+
logger.error { "Failed to setup MCP client: #{e.message}" }
|
206
181
|
@mcp_client = nil
|
207
182
|
@available_tools = []
|
208
183
|
end
|
@@ -213,96 +188,29 @@ module ClaudeSwarm
|
|
213
188
|
"$0.00"
|
214
189
|
end
|
215
190
|
|
216
|
-
def
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
# Custom formatter for better readability
|
228
|
-
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
229
|
-
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
|
230
|
-
end
|
231
|
-
|
232
|
-
return unless @instance_name
|
233
|
-
|
234
|
-
instance_info = @instance_name
|
235
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
236
|
-
@logger.info("Started OpenAI executor for instance: #{instance_info}")
|
237
|
-
end
|
238
|
-
|
239
|
-
def log_request(prompt)
|
240
|
-
caller_info = @calling_instance
|
241
|
-
caller_info += " (#{@calling_instance_id})" if @calling_instance_id
|
242
|
-
instance_info = @instance_name
|
243
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
244
|
-
@logger.info("#{caller_info} -> #{instance_info}: \n---\n#{prompt}\n---")
|
245
|
-
|
246
|
-
# Build event hash for JSON logging
|
247
|
-
event = {
|
248
|
-
type: "request",
|
249
|
-
from_instance: @calling_instance,
|
250
|
-
from_instance_id: @calling_instance_id,
|
251
|
-
to_instance: @instance_name,
|
252
|
-
to_instance_id: @instance_id,
|
253
|
-
prompt: prompt,
|
254
|
-
timestamp: Time.now.iso8601,
|
191
|
+
def create_api_handler
|
192
|
+
handler_params = {
|
193
|
+
openai_client: @openai_client,
|
194
|
+
mcp_client: @mcp_client,
|
195
|
+
available_tools: @available_tools,
|
196
|
+
executor: self,
|
197
|
+
instance_name: @instance_name,
|
198
|
+
model: @model,
|
199
|
+
temperature: @temperature,
|
200
|
+
reasoning_effort: @reasoning_effort,
|
255
201
|
}
|
256
202
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
caller_info += " (#{@calling_instance_id})" if @calling_instance_id
|
263
|
-
instance_info = @instance_name
|
264
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
265
|
-
@logger.info(
|
266
|
-
"(#{response["total_cost"]} - #{response["duration_ms"]}ms) #{instance_info} -> #{caller_info}: \n---\n#{response["result"]}\n---",
|
267
|
-
)
|
203
|
+
if @api_version == "responses"
|
204
|
+
OpenAI::Responses.new(**handler_params)
|
205
|
+
else
|
206
|
+
OpenAI::ChatCompletion.new(**handler_params)
|
207
|
+
end
|
268
208
|
end
|
269
209
|
|
270
210
|
def log_streaming_content(content)
|
271
211
|
# Log streaming content similar to ClaudeCodeExecutor
|
272
|
-
instance_info
|
273
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
274
|
-
@logger.debug("#{instance_info} streaming: #{content}")
|
275
|
-
end
|
276
|
-
|
277
|
-
def append_to_session_json(event)
|
278
|
-
json_filename = "session.log.json"
|
279
|
-
json_path = File.join(@session_path, json_filename)
|
280
|
-
|
281
|
-
# Use file locking to ensure thread-safe writes
|
282
|
-
File.open(json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
|
283
|
-
file.flock(File::LOCK_EX)
|
284
|
-
|
285
|
-
# Create entry with metadata
|
286
|
-
entry = {
|
287
|
-
instance: @instance_name,
|
288
|
-
instance_id: @instance_id,
|
289
|
-
calling_instance: @calling_instance,
|
290
|
-
calling_instance_id: @calling_instance_id,
|
291
|
-
timestamp: Time.now.iso8601,
|
292
|
-
event: event,
|
293
|
-
}
|
294
|
-
|
295
|
-
# Write as single line JSON (JSONL format)
|
296
|
-
file.puts(entry.to_json)
|
297
|
-
|
298
|
-
file.flock(File::LOCK_UN)
|
299
|
-
end
|
300
|
-
rescue StandardError => e
|
301
|
-
@logger.error("Failed to append to session JSON: #{e.message}")
|
302
|
-
raise
|
212
|
+
logger.debug { "#{instance_info} streaming: #{content}" }
|
303
213
|
end
|
304
|
-
|
305
|
-
class ExecutionError < StandardError; end
|
306
214
|
end
|
307
215
|
end
|
308
216
|
end
|
@@ -5,11 +5,11 @@ module ClaudeSwarm
|
|
5
5
|
class Responses
|
6
6
|
MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
|
7
7
|
|
8
|
-
def initialize(openai_client:, mcp_client:, available_tools:,
|
8
|
+
def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
|
9
9
|
@openai_client = openai_client
|
10
10
|
@mcp_client = mcp_client
|
11
11
|
@available_tools = available_tools
|
12
|
-
@executor =
|
12
|
+
@executor = executor
|
13
13
|
@instance_name = instance_name
|
14
14
|
@model = model
|
15
15
|
@temperature = temperature
|
@@ -37,7 +37,7 @@ module ClaudeSwarm
|
|
37
37
|
def process_responses_api(input, conversation_array, previous_response_id, depth = 0)
|
38
38
|
# Prevent infinite recursion
|
39
39
|
if depth > MAX_TURNS_WITH_TOOLS
|
40
|
-
@executor.error
|
40
|
+
@executor.logger.error { "Maximum recursion depth reached in tool execution" }
|
41
41
|
return "Error: Maximum tool call depth exceeded"
|
42
42
|
end
|
43
43
|
|
@@ -72,11 +72,11 @@ module ClaudeSwarm
|
|
72
72
|
parameters[:input] = conversation_array
|
73
73
|
|
74
74
|
# Log conversation array to debug duplicates
|
75
|
-
@executor.info
|
75
|
+
@executor.logger.info { "Conversation array size: #{conversation_array.size}" }
|
76
76
|
conversation_ids = conversation_array.map do |item|
|
77
77
|
item["call_id"] || item["id"] || "no-id-#{item["type"]}"
|
78
78
|
end.compact
|
79
|
-
@executor.info
|
79
|
+
@executor.logger.info { "Conversation item IDs: #{conversation_ids.inspect}" }
|
80
80
|
end
|
81
81
|
|
82
82
|
# Add previous response ID for conversation continuity
|
@@ -93,11 +93,11 @@ module ClaudeSwarm
|
|
93
93
|
"parameters" => tool.schema || {},
|
94
94
|
}
|
95
95
|
end
|
96
|
-
@executor.info
|
96
|
+
@executor.logger.info { "Available tools for responses API: #{parameters[:tools].map { |t| t["name"] }.join(", ")}" }
|
97
97
|
end
|
98
98
|
|
99
99
|
# Log the request parameters
|
100
|
-
@executor.info
|
100
|
+
@executor.logger.info { "Responses API Request (depth=#{depth}): #{JSON.pretty_generate(parameters)}" }
|
101
101
|
|
102
102
|
# Append to session JSON
|
103
103
|
append_to_session_json({
|
@@ -111,16 +111,16 @@ module ClaudeSwarm
|
|
111
111
|
begin
|
112
112
|
response = @openai_client.responses.create(parameters: parameters)
|
113
113
|
rescue StandardError => e
|
114
|
-
@executor.error
|
115
|
-
@executor.error
|
114
|
+
@executor.logger.error { "Responses API error: #{e.class} - #{e.message}" }
|
115
|
+
@executor.logger.error { "Request parameters: #{JSON.pretty_generate(parameters)}" }
|
116
116
|
|
117
117
|
# Try to extract and log the response body for better debugging
|
118
118
|
if e.respond_to?(:response)
|
119
119
|
begin
|
120
120
|
error_body = e.response[:body]
|
121
|
-
@executor.error
|
121
|
+
@executor.logger.error { "Error response body: #{error_body}" }
|
122
122
|
rescue StandardError => parse_error
|
123
|
-
@executor.error
|
123
|
+
@executor.logger.error { "Could not parse error response: #{parse_error.message}" }
|
124
124
|
end
|
125
125
|
end
|
126
126
|
|
@@ -140,7 +140,7 @@ module ClaudeSwarm
|
|
140
140
|
end
|
141
141
|
|
142
142
|
# Log the full response
|
143
|
-
@executor.info
|
143
|
+
@executor.logger.info { "Responses API Full Response (depth=#{depth}): #{JSON.pretty_generate(response)}" }
|
144
144
|
|
145
145
|
# Append to session JSON
|
146
146
|
append_to_session_json({
|
@@ -157,7 +157,7 @@ module ClaudeSwarm
|
|
157
157
|
output = response["output"]
|
158
158
|
|
159
159
|
if output.nil?
|
160
|
-
@executor.error
|
160
|
+
@executor.logger.error { "No output in response" }
|
161
161
|
return "Error: No output in OpenAI response"
|
162
162
|
end
|
163
163
|
|
@@ -185,7 +185,7 @@ module ClaudeSwarm
|
|
185
185
|
extract_text_response(output)
|
186
186
|
end
|
187
187
|
else
|
188
|
-
@executor.error
|
188
|
+
@executor.logger.error { "Unexpected output format: #{output.inspect}" }
|
189
189
|
"Error: Unexpected response format"
|
190
190
|
end
|
191
191
|
end
|
@@ -200,12 +200,12 @@ module ClaudeSwarm
|
|
200
200
|
|
201
201
|
def build_conversation_with_outputs(function_calls)
|
202
202
|
# Log tool calls
|
203
|
-
@executor.info
|
203
|
+
@executor.logger.info { "Responses API - Handling #{function_calls.size} function calls" }
|
204
204
|
|
205
205
|
# Log IDs to check for duplicates
|
206
206
|
call_ids = function_calls.map { |fc| fc["call_id"] || fc["id"] }
|
207
|
-
@executor.info
|
208
|
-
@executor.warn
|
207
|
+
@executor.logger.info { "Function call IDs: #{call_ids.inspect}" }
|
208
|
+
@executor.logger.warn { "WARNING: Duplicate function call IDs detected!" } if call_ids.size != call_ids.uniq.size
|
209
209
|
|
210
210
|
# Append to session JSON
|
211
211
|
append_to_session_json({
|
@@ -226,20 +226,20 @@ module ClaudeSwarm
|
|
226
226
|
call_id = function_call["call_id"]
|
227
227
|
|
228
228
|
# Log both IDs to debug
|
229
|
-
@executor.info
|
229
|
+
@executor.logger.info { "Function call has id=#{function_call["id"]}, call_id=#{function_call["call_id"]}" }
|
230
230
|
|
231
231
|
begin
|
232
232
|
# Parse arguments
|
233
233
|
tool_args = JSON.parse(tool_args_str)
|
234
234
|
|
235
235
|
# Log tool execution
|
236
|
-
@executor.info
|
236
|
+
@executor.logger.info { "Responses API - Executing tool: #{tool_name} with args: #{JSON.pretty_generate(tool_args)}" }
|
237
237
|
|
238
238
|
# Execute tool via MCP
|
239
239
|
result = @mcp_client.call_tool(tool_name, tool_args)
|
240
240
|
|
241
241
|
# Log result
|
242
|
-
@executor.info
|
242
|
+
@executor.logger.info { "Responses API - Tool result for #{tool_name}: #{result}" }
|
243
243
|
|
244
244
|
# Append to session JSON
|
245
245
|
append_to_session_json({
|
@@ -257,8 +257,8 @@ module ClaudeSwarm
|
|
257
257
|
output: result.to_json, # Must be JSON string
|
258
258
|
}
|
259
259
|
rescue StandardError => e
|
260
|
-
@executor.error
|
261
|
-
@executor.error
|
260
|
+
@executor.logger.error { "Responses API - Tool execution failed for #{tool_name}: #{e.message}" }
|
261
|
+
@executor.logger.error { e.backtrace.join("\n") }
|
262
262
|
|
263
263
|
# Append error to session JSON
|
264
264
|
append_to_session_json({
|
@@ -282,8 +282,8 @@ module ClaudeSwarm
|
|
282
282
|
end
|
283
283
|
end
|
284
284
|
|
285
|
-
@executor.info
|
286
|
-
@executor.debug
|
285
|
+
@executor.logger.info { "Responses API - Built conversation with #{conversation.size} function outputs" }
|
286
|
+
@executor.logger.debug { "Final conversation structure: #{JSON.pretty_generate(conversation)}" }
|
287
287
|
conversation
|
288
288
|
end
|
289
289
|
|
@@ -302,13 +302,13 @@ module ClaudeSwarm
|
|
302
302
|
tool_args = JSON.parse(tool_args_str)
|
303
303
|
|
304
304
|
# Log tool execution
|
305
|
-
@executor.info
|
305
|
+
@executor.logger.info { "Responses API - Executing tool: #{tool_name} with args: #{JSON.pretty_generate(tool_args)}" }
|
306
306
|
|
307
307
|
# Execute tool via MCP
|
308
308
|
result = @mcp_client.call_tool(tool_name, tool_args)
|
309
309
|
|
310
310
|
# Log result
|
311
|
-
@executor.info
|
311
|
+
@executor.logger.info { "Responses API - Tool result for #{tool_name}: #{result}" }
|
312
312
|
|
313
313
|
# Add function output to conversation
|
314
314
|
conversation << {
|
@@ -317,7 +317,7 @@ module ClaudeSwarm
|
|
317
317
|
output: result.to_json, # Must be JSON string
|
318
318
|
}
|
319
319
|
rescue StandardError => e
|
320
|
-
@executor.error
|
320
|
+
@executor.logger.error { "Responses API - Tool execution failed for #{tool_name}: #{e.message}" }
|
321
321
|
|
322
322
|
# Add error output to conversation
|
323
323
|
conversation << {
|