claude_swarm 0.3.2 → 1.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/.claude/commands/release.md +27 -0
- data/CHANGELOG.md +174 -0
- data/CLAUDE.md +62 -3
- data/README.md +131 -5
- data/examples/simple-session-hook-swarm.yml +37 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +245 -210
- data/lib/claude_swarm/claude_mcp_server.rb +3 -2
- data/lib/claude_swarm/cli.rb +27 -20
- data/lib/claude_swarm/commands/ps.rb +29 -10
- data/lib/claude_swarm/commands/show.rb +4 -5
- data/lib/claude_swarm/configuration.rb +19 -12
- data/lib/claude_swarm/hooks/session_start_hook.rb +42 -0
- data/lib/claude_swarm/json_handler.rb +91 -0
- data/lib/claude_swarm/mcp_generator.rb +7 -5
- data/lib/claude_swarm/openai/chat_completion.rb +16 -16
- data/lib/claude_swarm/openai/executor.rb +155 -209
- data/lib/claude_swarm/openai/responses.rb +29 -29
- data/lib/claude_swarm/orchestrator.rb +452 -257
- data/lib/claude_swarm/session_cost_calculator.rb +130 -14
- data/lib/claude_swarm/session_path.rb +2 -8
- data/lib/claude_swarm/settings_generator.rb +77 -0
- data/lib/claude_swarm/system_utils.rb +6 -2
- data/lib/claude_swarm/templates/generation_prompt.md.erb +6 -6
- data/lib/claude_swarm/tools/task_tool.rb +13 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +2 -2
- data/lib/claude_swarm.rb +23 -2
- data/team.yml +75 -3
- data/team_v2.yml +367 -0
- metadata +54 -5
|
@@ -1,102 +1,84 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ClaudeSwarm
|
|
4
|
-
class ClaudeCodeExecutor
|
|
5
|
-
attr_reader :session_id, :last_response, :working_directory, :logger, :session_path
|
|
6
|
-
|
|
7
|
-
def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
|
|
8
|
-
instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
|
|
9
|
-
claude_session_id: nil, additional_directories: [])
|
|
10
|
-
@working_directory = working_directory
|
|
11
|
-
@additional_directories = additional_directories
|
|
12
|
-
@model = model
|
|
13
|
-
@mcp_config = mcp_config
|
|
14
|
-
@vibe = vibe
|
|
15
|
-
@session_id = claude_session_id
|
|
16
|
-
@last_response = nil
|
|
17
|
-
@instance_name = instance_name
|
|
18
|
-
@instance_id = instance_id
|
|
19
|
-
@calling_instance = calling_instance
|
|
20
|
-
@calling_instance_id = calling_instance_id
|
|
21
|
-
|
|
22
|
-
# Setup logging
|
|
23
|
-
setup_logging
|
|
24
|
-
end
|
|
25
|
-
|
|
4
|
+
class ClaudeCodeExecutor < BaseExecutor
|
|
26
5
|
def execute(prompt, options = {})
|
|
27
6
|
# Log the request
|
|
28
7
|
log_request(prompt)
|
|
29
8
|
|
|
30
|
-
|
|
9
|
+
# Build SDK options
|
|
10
|
+
sdk_options = build_sdk_options(prompt, options)
|
|
31
11
|
|
|
32
12
|
# Variables to collect output
|
|
33
|
-
|
|
13
|
+
all_messages = []
|
|
34
14
|
result_response = nil
|
|
35
15
|
|
|
36
|
-
# Execute
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
stdin.close
|
|
43
|
-
|
|
44
|
-
# Read stderr in a separate thread
|
|
45
|
-
stderr_thread = Thread.new do
|
|
46
|
-
stderr.each_line { |line| stderr_output << line }
|
|
47
|
-
end
|
|
16
|
+
# Execute with streaming
|
|
17
|
+
begin
|
|
18
|
+
ClaudeSDK.query(prompt, options: sdk_options) do |message|
|
|
19
|
+
# Convert message to hash for logging
|
|
20
|
+
message_hash = message_to_hash(message)
|
|
21
|
+
all_messages << message_hash
|
|
48
22
|
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
json_data = JSON.parse(line.strip)
|
|
52
|
-
|
|
53
|
-
# Log each JSON event
|
|
54
|
-
log_streaming_event(json_data)
|
|
23
|
+
# Log streaming event BEFORE we modify anything
|
|
24
|
+
log_streaming_event(message_hash)
|
|
55
25
|
|
|
26
|
+
# Process specific message types
|
|
27
|
+
case message
|
|
28
|
+
when ClaudeSDK::Messages::System
|
|
56
29
|
# Capture session_id from system init
|
|
57
|
-
if
|
|
58
|
-
|
|
59
|
-
|
|
30
|
+
if message.subtype == "init" && message.data.is_a?(Hash)
|
|
31
|
+
# For init messages, session_id is in the data hash
|
|
32
|
+
session_id = message.data[:session_id] || message.data["session_id"]
|
|
33
|
+
|
|
34
|
+
if session_id
|
|
35
|
+
@session_id = session_id
|
|
36
|
+
write_instance_state
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
when ClaudeSDK::Messages::Assistant
|
|
40
|
+
# Assistant messages only contain content blocks
|
|
41
|
+
# No need to track for result extraction - result comes from Result message
|
|
42
|
+
when ClaudeSDK::Messages::Result
|
|
43
|
+
# Validate that we have actual result content
|
|
44
|
+
if message.result.nil? || (message.result.is_a?(String) && message.result.strip.empty?)
|
|
45
|
+
raise ExecutionError, "Claude SDK returned an empty result. The agent completed execution but provided no response content."
|
|
60
46
|
end
|
|
61
47
|
|
|
62
|
-
#
|
|
63
|
-
result_response =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
unless exit_status.success?
|
|
74
|
-
error_msg = stderr_output.join
|
|
75
|
-
@logger.error("Execution error for #{@instance_name}: #{error_msg}")
|
|
76
|
-
raise ExecutionError, "Claude Code execution failed: #{error_msg}"
|
|
48
|
+
# Build result response in expected format
|
|
49
|
+
result_response = {
|
|
50
|
+
"type" => "result",
|
|
51
|
+
"subtype" => message.subtype || "success",
|
|
52
|
+
"cost_usd" => message.total_cost_usd,
|
|
53
|
+
"is_error" => message.is_error || false,
|
|
54
|
+
"duration_ms" => message.duration_ms,
|
|
55
|
+
"result" => message.result, # Result text is directly in message.result
|
|
56
|
+
"total_cost" => message.total_cost_usd,
|
|
57
|
+
"session_id" => message.session_id,
|
|
58
|
+
}
|
|
77
59
|
end
|
|
78
60
|
end
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
logger.error { "Execution error for #{@instance_name}: #{e.class} - #{e.message}" }
|
|
63
|
+
logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
|
|
64
|
+
raise ExecutionError, "Claude Code execution failed: #{e.message}"
|
|
79
65
|
end
|
|
80
66
|
|
|
81
67
|
# Ensure we got a result
|
|
82
|
-
raise ParseError, "No result found in
|
|
68
|
+
raise ParseError, "No result found in SDK response" unless result_response
|
|
69
|
+
|
|
70
|
+
# Write session JSON log
|
|
71
|
+
all_messages.each do |msg|
|
|
72
|
+
append_to_session_json(msg)
|
|
73
|
+
end
|
|
83
74
|
|
|
84
75
|
result_response
|
|
85
76
|
rescue StandardError => e
|
|
86
|
-
|
|
87
|
-
|
|
77
|
+
logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
|
|
78
|
+
logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
|
|
88
79
|
raise
|
|
89
80
|
end
|
|
90
81
|
|
|
91
|
-
def reset_session
|
|
92
|
-
@session_id = nil
|
|
93
|
-
@last_response = nil
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def has_session?
|
|
97
|
-
!@session_id.nil?
|
|
98
|
-
end
|
|
99
|
-
|
|
100
82
|
private
|
|
101
83
|
|
|
102
84
|
def write_instance_state
|
|
@@ -114,64 +96,10 @@ module ClaudeSwarm
|
|
|
114
96
|
updated_at: Time.now.iso8601,
|
|
115
97
|
}
|
|
116
98
|
|
|
117
|
-
|
|
118
|
-
|
|
99
|
+
JsonHandler.write_file!(state_file, state_data)
|
|
100
|
+
logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
|
|
119
101
|
rescue StandardError => e
|
|
120
|
-
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def setup_logging
|
|
124
|
-
# Use session path from environment (required)
|
|
125
|
-
@session_path = SessionPath.from_env
|
|
126
|
-
SessionPath.ensure_directory(@session_path)
|
|
127
|
-
|
|
128
|
-
# Create logger with session.log filename
|
|
129
|
-
log_filename = "session.log"
|
|
130
|
-
log_path = File.join(@session_path, log_filename)
|
|
131
|
-
@logger = Logger.new(log_path)
|
|
132
|
-
@logger.level = Logger::INFO
|
|
133
|
-
|
|
134
|
-
# Custom formatter for better readability
|
|
135
|
-
@logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
136
|
-
"[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
return unless @instance_name
|
|
140
|
-
|
|
141
|
-
instance_info = @instance_name
|
|
142
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
|
143
|
-
@logger.info("Started Claude Code executor for instance: #{instance_info}")
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def log_request(prompt)
|
|
147
|
-
caller_info = @calling_instance
|
|
148
|
-
caller_info += " (#{@calling_instance_id})" if @calling_instance_id
|
|
149
|
-
instance_info = @instance_name
|
|
150
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
|
151
|
-
@logger.info("#{caller_info} -> #{instance_info}: \n---\n#{prompt}\n---")
|
|
152
|
-
|
|
153
|
-
# Build event hash for JSON logging
|
|
154
|
-
event = {
|
|
155
|
-
type: "request",
|
|
156
|
-
from_instance: @calling_instance,
|
|
157
|
-
from_instance_id: @calling_instance_id,
|
|
158
|
-
to_instance: @instance_name,
|
|
159
|
-
to_instance_id: @instance_id,
|
|
160
|
-
prompt: prompt,
|
|
161
|
-
timestamp: Time.now.iso8601,
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
append_to_session_json(event)
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def log_response(response)
|
|
168
|
-
caller_info = @calling_instance
|
|
169
|
-
caller_info += " (#{@calling_instance_id})" if @calling_instance_id
|
|
170
|
-
instance_info = @instance_name
|
|
171
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
|
172
|
-
@logger.info(
|
|
173
|
-
"($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{instance_info} -> #{caller_info}: \n---\n#{response["result"]}\n---",
|
|
174
|
-
)
|
|
102
|
+
logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
|
|
175
103
|
end
|
|
176
104
|
|
|
177
105
|
def log_streaming_event(event)
|
|
@@ -191,124 +119,231 @@ module ClaudeSwarm
|
|
|
191
119
|
end
|
|
192
120
|
|
|
193
121
|
def log_system_message(event)
|
|
194
|
-
|
|
122
|
+
logger.debug { "SYSTEM: #{JsonHandler.pretty_generate!(event)}" }
|
|
195
123
|
end
|
|
196
124
|
|
|
197
125
|
def log_assistant_message(msg)
|
|
198
|
-
|
|
199
|
-
|
|
126
|
+
# Assistant messages don't have stop_reason in SDK - they only have content
|
|
200
127
|
content = msg["content"]
|
|
201
|
-
|
|
202
|
-
|
|
128
|
+
logger.debug { "ASSISTANT: #{JsonHandler.pretty_generate!(content)}" } if content
|
|
129
|
+
|
|
130
|
+
# Log tool calls
|
|
131
|
+
tool_calls = content&.select { |c| c["type"] == "tool_use" } || []
|
|
203
132
|
tool_calls.each do |tool_call|
|
|
204
133
|
arguments = tool_call["input"].to_json
|
|
205
134
|
arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
|
|
206
135
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
"Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}",
|
|
211
|
-
)
|
|
136
|
+
logger.info do
|
|
137
|
+
"Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
|
|
138
|
+
end
|
|
212
139
|
end
|
|
213
140
|
|
|
214
|
-
|
|
141
|
+
# Log thinking text
|
|
142
|
+
text = content&.select { |c| c["type"] == "text" } || []
|
|
215
143
|
text.each do |t|
|
|
216
|
-
instance_info
|
|
217
|
-
instance_info += " (#{@instance_id})" if @instance_id
|
|
218
|
-
@logger.info("#{instance_info} is thinking:\n---\n#{t["text"]}\n---")
|
|
144
|
+
logger.info { "#{instance_info} is thinking:\n---\n#{t["text"]}\n---" }
|
|
219
145
|
end
|
|
220
146
|
end
|
|
221
147
|
|
|
222
148
|
def log_user_message(content)
|
|
223
|
-
|
|
149
|
+
logger.debug { "USER: #{JsonHandler.pretty_generate!(content)}" }
|
|
224
150
|
end
|
|
225
151
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
# Use file locking to ensure thread-safe writes
|
|
231
|
-
File.open(json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
|
|
232
|
-
file.flock(File::LOCK_EX)
|
|
233
|
-
|
|
234
|
-
# Create entry with metadata
|
|
235
|
-
entry = {
|
|
236
|
-
instance: @instance_name,
|
|
237
|
-
instance_id: @instance_id,
|
|
238
|
-
calling_instance: @calling_instance,
|
|
239
|
-
calling_instance_id: @calling_instance_id,
|
|
240
|
-
timestamp: Time.now.iso8601,
|
|
241
|
-
event: event,
|
|
242
|
-
}
|
|
152
|
+
def build_sdk_options(prompt, options)
|
|
153
|
+
# Map CLI options to SDK options
|
|
154
|
+
sdk_options = ClaudeSDK::ClaudeCodeOptions.new
|
|
243
155
|
|
|
244
|
-
|
|
245
|
-
|
|
156
|
+
# Basic options
|
|
157
|
+
# Only set model if ANTHROPIC_MODEL env var is not set
|
|
158
|
+
sdk_options.model = @model if @model && !ENV["ANTHROPIC_MODEL"]
|
|
159
|
+
sdk_options.cwd = @working_directory
|
|
160
|
+
sdk_options.resume = @session_id if @session_id && !options[:new_session]
|
|
246
161
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
162
|
+
# Permission mode
|
|
163
|
+
if @vibe
|
|
164
|
+
sdk_options.permission_mode = ClaudeSDK::PermissionMode::BYPASS_PERMISSIONS
|
|
165
|
+
else
|
|
166
|
+
# Build allowed tools list including MCP connections
|
|
167
|
+
allowed_tools = options[:allowed_tools] ? Array(options[:allowed_tools]).dup : []
|
|
253
168
|
|
|
254
|
-
|
|
255
|
-
|
|
169
|
+
# Add mcp__instance_name for each connection if we have instance info
|
|
170
|
+
options[:connections]&.each do |connection_name|
|
|
171
|
+
allowed_tools << "mcp__#{connection_name}"
|
|
172
|
+
end
|
|
256
173
|
|
|
257
|
-
|
|
258
|
-
|
|
174
|
+
# Set allowed and disallowed tools
|
|
175
|
+
sdk_options.allowed_tools = allowed_tools if allowed_tools.any?
|
|
176
|
+
sdk_options.disallowed_tools = Array(options[:disallowed_tools]) if options[:disallowed_tools]
|
|
177
|
+
end
|
|
259
178
|
|
|
260
|
-
|
|
179
|
+
# System prompt
|
|
180
|
+
sdk_options.append_system_prompt = options[:system_prompt] if options[:system_prompt]
|
|
261
181
|
|
|
262
|
-
#
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
cmd_array << additional_dir
|
|
182
|
+
# MCP configuration
|
|
183
|
+
if @mcp_config
|
|
184
|
+
sdk_options.mcp_servers = parse_mcp_config(@mcp_config)
|
|
266
185
|
end
|
|
267
186
|
|
|
268
|
-
#
|
|
269
|
-
|
|
187
|
+
# Handle additional directories by adding them to MCP servers
|
|
188
|
+
if @additional_directories.any?
|
|
189
|
+
setup_additional_directories_mcp(sdk_options)
|
|
190
|
+
end
|
|
270
191
|
|
|
271
|
-
#
|
|
272
|
-
|
|
192
|
+
# Add settings file path if it exists
|
|
193
|
+
settings_file = File.join(@session_path, "#{@instance_name}_settings.json")
|
|
194
|
+
sdk_options.settings = settings_file if File.exist?(settings_file)
|
|
273
195
|
|
|
274
|
-
|
|
275
|
-
|
|
196
|
+
sdk_options
|
|
197
|
+
end
|
|
276
198
|
|
|
277
|
-
|
|
278
|
-
|
|
199
|
+
def parse_mcp_config(config_path)
|
|
200
|
+
# Parse MCP JSON config file and convert to SDK format
|
|
201
|
+
config = JsonHandler.parse_file!(config_path)
|
|
202
|
+
mcp_servers = {}
|
|
203
|
+
|
|
204
|
+
config["mcpServers"]&.each do |name, server_config|
|
|
205
|
+
server_type = server_config["type"] || "stdio"
|
|
206
|
+
|
|
207
|
+
mcp_servers[name] = case server_type
|
|
208
|
+
when "stdio"
|
|
209
|
+
ClaudeSDK::McpServerConfig::StdioServer.new(
|
|
210
|
+
command: server_config["command"],
|
|
211
|
+
args: server_config["args"] || [],
|
|
212
|
+
env: server_config["env"] || {},
|
|
213
|
+
)
|
|
214
|
+
when "sse"
|
|
215
|
+
ClaudeSDK::McpServerConfig::SSEServer.new(
|
|
216
|
+
url: server_config["url"],
|
|
217
|
+
headers: server_config["headers"] || {},
|
|
218
|
+
)
|
|
219
|
+
when "http"
|
|
220
|
+
ClaudeSDK::McpServerConfig::HttpServer.new(
|
|
221
|
+
url: server_config["url"],
|
|
222
|
+
headers: server_config["headers"] || {},
|
|
223
|
+
)
|
|
224
|
+
else
|
|
225
|
+
logger.warn { "Unsupported MCP server type: #{server_type} for server: #{name}" }
|
|
226
|
+
nil
|
|
227
|
+
end
|
|
228
|
+
end
|
|
279
229
|
|
|
280
|
-
|
|
281
|
-
|
|
230
|
+
mcp_servers.compact
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
logger.error { "Failed to parse MCP config: #{e.message}" }
|
|
233
|
+
{}
|
|
234
|
+
end
|
|
282
235
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
else
|
|
287
|
-
# Build allowed tools list including MCP connections
|
|
288
|
-
allowed_tools = options[:allowed_tools] ? Array(options[:allowed_tools]).dup : []
|
|
236
|
+
def setup_additional_directories_mcp(sdk_options)
|
|
237
|
+
# Workaround for --add-dir: add file system MCP servers for additional directories
|
|
238
|
+
sdk_options.mcp_servers ||= {}
|
|
289
239
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
240
|
+
@additional_directories.each do |dir|
|
|
241
|
+
# This is a placeholder - the SDK doesn't directly support file system servers
|
|
242
|
+
# You would need to implement a proper MCP server that provides file access
|
|
243
|
+
logger.warn { "Additional directories not fully supported: #{dir}" }
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def message_to_hash(message)
|
|
248
|
+
# Convert SDK message objects to hash format matching CLI JSON output
|
|
249
|
+
case message
|
|
250
|
+
when ClaudeSDK::Messages::System
|
|
251
|
+
# System messages have subtype and data attributes
|
|
252
|
+
# The data hash contains the actual information from the CLI
|
|
253
|
+
hash = {
|
|
254
|
+
"type" => "system",
|
|
255
|
+
"subtype" => message.subtype,
|
|
256
|
+
}
|
|
294
257
|
|
|
295
|
-
#
|
|
296
|
-
if
|
|
297
|
-
|
|
298
|
-
|
|
258
|
+
# Include the data hash if it exists - this is where CLI puts info like session_id, tools, etc.
|
|
259
|
+
if message.data.is_a?(Hash)
|
|
260
|
+
# For "init" subtype, extract session_id and tools from data
|
|
261
|
+
if message.subtype == "init"
|
|
262
|
+
hash["session_id"] = message.data[:session_id] || message.data["session_id"]
|
|
263
|
+
hash["tools"] = message.data[:tools] || message.data["tools"]
|
|
264
|
+
end
|
|
265
|
+
# You can add other relevant data fields as needed
|
|
299
266
|
end
|
|
300
267
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
268
|
+
hash.compact
|
|
269
|
+
when ClaudeSDK::Messages::Assistant
|
|
270
|
+
# Assistant messages only have content attribute
|
|
271
|
+
{
|
|
272
|
+
"type" => "assistant",
|
|
273
|
+
"message" => {
|
|
274
|
+
"type" => "message",
|
|
275
|
+
"role" => "assistant",
|
|
276
|
+
"content" => content_blocks_to_hash(message.content),
|
|
277
|
+
},
|
|
278
|
+
"session_id" => @session_id,
|
|
279
|
+
}
|
|
280
|
+
when ClaudeSDK::Messages::User
|
|
281
|
+
# User messages only have content attribute (a string)
|
|
282
|
+
{
|
|
283
|
+
"type" => "user",
|
|
284
|
+
"message" => {
|
|
285
|
+
"type" => "message",
|
|
286
|
+
"role" => "user",
|
|
287
|
+
"content" => message.content,
|
|
288
|
+
},
|
|
289
|
+
"session_id" => @session_id,
|
|
290
|
+
}
|
|
291
|
+
when ClaudeSDK::Messages::Result
|
|
292
|
+
# Result messages have multiple attributes
|
|
293
|
+
{
|
|
294
|
+
"type" => "result",
|
|
295
|
+
"subtype" => message.subtype || "success",
|
|
296
|
+
"cost_usd" => message.total_cost_usd,
|
|
297
|
+
"is_error" => message.is_error || false,
|
|
298
|
+
"duration_ms" => message.duration_ms,
|
|
299
|
+
"duration_api_ms" => message.duration_api_ms,
|
|
300
|
+
"num_turns" => message.num_turns,
|
|
301
|
+
"result" => message.result, # Result text is in message.result, not from content
|
|
302
|
+
"total_cost" => message.total_cost_usd,
|
|
303
|
+
"total_cost_usd" => message.total_cost_usd,
|
|
304
|
+
"session_id" => message.session_id,
|
|
305
|
+
"usage" => message.usage,
|
|
306
|
+
}.compact
|
|
307
|
+
else
|
|
308
|
+
# Fallback for unknown message types
|
|
309
|
+
begin
|
|
310
|
+
message.to_h
|
|
311
|
+
rescue
|
|
312
|
+
{ "type" => "unknown", "data" => message.to_s }
|
|
305
313
|
end
|
|
306
314
|
end
|
|
307
|
-
|
|
308
|
-
cmd_array
|
|
309
315
|
end
|
|
310
316
|
|
|
311
|
-
|
|
312
|
-
|
|
317
|
+
def content_blocks_to_hash(content)
|
|
318
|
+
return [] unless content
|
|
319
|
+
|
|
320
|
+
content.map do |block|
|
|
321
|
+
case block
|
|
322
|
+
when ClaudeSDK::ContentBlock::Text
|
|
323
|
+
{ "type" => "text", "text" => block.text }
|
|
324
|
+
when ClaudeSDK::ContentBlock::ToolUse
|
|
325
|
+
{
|
|
326
|
+
"type" => "tool_use",
|
|
327
|
+
"id" => block.id,
|
|
328
|
+
"name" => block.name,
|
|
329
|
+
"input" => block.input,
|
|
330
|
+
}
|
|
331
|
+
when ClaudeSDK::ContentBlock::ToolResult
|
|
332
|
+
{
|
|
333
|
+
"type" => "tool_result",
|
|
334
|
+
"tool_use_id" => block.tool_use_id,
|
|
335
|
+
"content" => block.content,
|
|
336
|
+
"is_error" => block.is_error,
|
|
337
|
+
}
|
|
338
|
+
else
|
|
339
|
+
# Fallback
|
|
340
|
+
begin
|
|
341
|
+
block.to_h
|
|
342
|
+
rescue
|
|
343
|
+
{ "type" => "unknown", "data" => block.to_s }
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
313
348
|
end
|
|
314
349
|
end
|
|
@@ -7,7 +7,7 @@ module ClaudeSwarm
|
|
|
7
7
|
attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance, :calling_instance_id
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def initialize(instance_config, calling_instance:, calling_instance_id: nil)
|
|
10
|
+
def initialize(instance_config, calling_instance:, calling_instance_id: nil, debug: false)
|
|
11
11
|
@instance_config = instance_config
|
|
12
12
|
@calling_instance = calling_instance
|
|
13
13
|
@calling_instance_id = calling_instance_id
|
|
@@ -24,6 +24,7 @@ module ClaudeSwarm
|
|
|
24
24
|
calling_instance_id: calling_instance_id,
|
|
25
25
|
claude_session_id: instance_config[:claude_session_id],
|
|
26
26
|
additional_directories: instance_config[:directories][1..] || [],
|
|
27
|
+
debug: debug,
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
@executor = if instance_config[:provider] == "openai"
|
|
@@ -37,7 +38,7 @@ module ClaudeSwarm
|
|
|
37
38
|
reasoning_effort: instance_config[:reasoning_effort],
|
|
38
39
|
)
|
|
39
40
|
else
|
|
40
|
-
# Default Claude behavior
|
|
41
|
+
# Default Claude behavior - always use SDK
|
|
41
42
|
ClaudeCodeExecutor.new(**common_params)
|
|
42
43
|
end
|
|
43
44
|
|