claude_swarm 0.3.6 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b13f053c52c3e4ff662746f5027fcc6005be35ab6621c827b5fac205bb69b917
4
- data.tar.gz: 70d1fbedfa25a81060259f37b4de2d47c740d8719171ce956a5462ce022359d2
3
+ metadata.gz: bc0d22524516d5e0f2a76715c2a80b36d1a974a3ff7ba451c103c86cb4a36fd1
4
+ data.tar.gz: 4cebdba8032fc3dfe223033ba9b2890b5849dd5516a539ffdf57231bcec66c38
5
5
  SHA512:
6
- metadata.gz: 60b9dbc2a41ae296a4a4d844c5dbf1195e26a38a4ce90aeb72a23f86467e94bc803e5b7488f8189829ad5a898a0887d1a639dc888adf24af87c4d391346709ac
7
- data.tar.gz: 4935a7b7001b78d0a8ae1f4f732f61aed8d4fc35859478615f035edd2411ea5f9b5df3be1e26f910ad8ce8943c750d3b51f22adf3c105bbd97c5a36b17b072e9
6
+ metadata.gz: e5ec9ed928d5e73d12eb1257571e3c2b45fc9db64dbd64e8f3186ea2df5f187927d4cc0a6e8eb9901cb8dbaf4679f442f340c85f6f6f728ca01f1d8b82db8bf8
7
+ data.tar.gz: 8fce151a6a47d68736fc62b6453515f46e12d293d2eee2f3e951f99510351941faf9f0b9c5f0497de37847fac282cc997ccebdbb11f477fc71b21b8514dd753d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.3.7]
2
+
3
+ ### Added
4
+ - **Main instance logging**: Captures main Claude instance output in `session.log` with prettified JSON format
5
+
6
+ ### Changed
7
+ - **Updated claude-code-sdk-ruby dependency**: Bumped from 0.1.4 to 0.1.6
8
+ - Includes latest SDK improvements and bug fixes
9
+
1
10
  ## [0.3.6]
2
11
 
3
12
  ### Added
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class BaseExecutor
5
+ attr_reader :session_id, :last_response, :working_directory, :logger, :session_path, :session_json_path, :instance_info
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: [], debug: false)
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
+ @debug = debug
22
+
23
+ # Setup static info strings for logging
24
+ @instance_info = build_info(@instance_name, @instance_id)
25
+ @caller_info = build_info(@calling_instance, @calling_instance_id)
26
+ @caller_to_instance = "#{@caller_info} -> #{instance_info}:"
27
+ @instance_to_caller = "#{instance_info} -> #{@caller_info}:"
28
+
29
+ # Setup logging
30
+ setup_logging
31
+
32
+ # Setup static event templates
33
+ setup_event_templates
34
+ end
35
+
36
+ def execute(_prompt, _options = {})
37
+ raise NotImplementedError, "Subclasses must implement the execute method"
38
+ end
39
+
40
+ def reset_session
41
+ @session_id = nil
42
+ @last_response = nil
43
+ end
44
+
45
+ def has_session?
46
+ !@session_id.nil?
47
+ end
48
+
49
+ protected
50
+
51
+ def build_info(name, id)
52
+ return name unless id
53
+
54
+ "#{name} (#{id})"
55
+ end
56
+
57
+ def setup_logging
58
+ # Use session path from environment (required)
59
+ @session_path = SessionPath.from_env
60
+ SessionPath.ensure_directory(@session_path)
61
+
62
+ # Initialize session JSON path
63
+ @session_json_path = File.join(@session_path, "session.log.json")
64
+
65
+ # Create logger with session.log filename
66
+ log_filename = "session.log"
67
+ log_path = File.join(@session_path, log_filename)
68
+ log_level = @debug ? :debug : :info
69
+ @logger = Logger.new(log_path, level: log_level, progname: @instance_name)
70
+
71
+ logger.info { "Started #{self.class.name} for instance: #{instance_info}" }
72
+ end
73
+
74
+ def setup_event_templates
75
+ @log_request_event_template = {
76
+ type: "request",
77
+ from_instance: @calling_instance,
78
+ from_instance_id: @calling_instance_id,
79
+ to_instance: @instance_name,
80
+ to_instance_id: @instance_id,
81
+ }.freeze
82
+
83
+ @session_json_entry_template = {
84
+ instance: @instance_name,
85
+ instance_id: @instance_id,
86
+ calling_instance: @calling_instance,
87
+ calling_instance_id: @calling_instance_id,
88
+ }.freeze
89
+ end
90
+
91
+ def log_request(prompt)
92
+ logger.info { "#{@caller_to_instance} \n---\n#{prompt}\n---" }
93
+
94
+ # Merge dynamic data with static template
95
+ event = @log_request_event_template.merge(
96
+ prompt: prompt,
97
+ timestamp: Time.now.iso8601,
98
+ )
99
+
100
+ append_to_session_json(event)
101
+ end
102
+
103
+ def log_response(response)
104
+ logger.info do
105
+ "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{@instance_to_caller} \n---\n#{response["result"]}\n---"
106
+ end
107
+ end
108
+
109
+ def append_to_session_json(event)
110
+ # Use file locking to ensure thread-safe writes
111
+ File.open(@session_json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
112
+ file.flock(File::LOCK_EX)
113
+
114
+ # Merge dynamic data with static template
115
+ entry = @session_json_entry_template.merge(
116
+ timestamp: Time.now.iso8601,
117
+ event: event,
118
+ )
119
+
120
+ # Write as single line JSON (JSONL format)
121
+ file.puts(entry.to_json)
122
+
123
+ file.flock(File::LOCK_UN)
124
+ end
125
+ rescue StandardError => e
126
+ logger.error { "Failed to append to session JSON: #{e.message}" }
127
+ raise
128
+ end
129
+
130
+ class ExecutionError < StandardError; end
131
+ class ParseError < StandardError; end
132
+ end
133
+ end
@@ -1,28 +1,7 @@
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)
@@ -75,8 +54,8 @@ module ClaudeSwarm
75
54
  end
76
55
  end
77
56
  rescue StandardError => e
78
- @logger.error("Execution error for #{@instance_name}: #{e.class} - #{e.message}")
79
- @logger.error("Backtrace: #{e.backtrace.join("\n")}")
57
+ logger.error { "Execution error for #{@instance_name}: #{e.class} - #{e.message}" }
58
+ logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
80
59
  raise ExecutionError, "Claude Code execution failed: #{e.message}"
81
60
  end
82
61
 
@@ -90,20 +69,11 @@ module ClaudeSwarm
90
69
 
91
70
  result_response
92
71
  rescue StandardError => e
93
- @logger.error("Unexpected error for #{@instance_name}: #{e.class} - #{e.message}")
94
- @logger.error("Backtrace: #{e.backtrace.join("\n")}")
72
+ logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
73
+ logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
95
74
  raise
96
75
  end
97
76
 
98
- def reset_session
99
- @session_id = nil
100
- @last_response = nil
101
- end
102
-
103
- def has_session?
104
- !@session_id.nil?
105
- end
106
-
107
77
  private
108
78
 
109
79
  def write_instance_state
@@ -122,63 +92,9 @@ module ClaudeSwarm
122
92
  }
123
93
 
124
94
  File.write(state_file, JSON.pretty_generate(state_data))
125
- @logger.info("Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}")
95
+ logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
126
96
  rescue StandardError => e
127
- @logger.error("Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}")
128
- end
129
-
130
- def setup_logging
131
- # Use session path from environment (required)
132
- @session_path = SessionPath.from_env
133
- SessionPath.ensure_directory(@session_path)
134
-
135
- # Create logger with session.log filename
136
- log_filename = "session.log"
137
- log_path = File.join(@session_path, log_filename)
138
- @logger = Logger.new(log_path)
139
- @logger.level = Logger::INFO
140
-
141
- # Custom formatter for better readability
142
- @logger.formatter = proc do |severity, datetime, _progname, msg|
143
- "[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
144
- end
145
-
146
- return unless @instance_name
147
-
148
- instance_info = @instance_name
149
- instance_info += " (#{@instance_id})" if @instance_id
150
- @logger.info("Started Claude Code executor for instance: #{instance_info}")
151
- end
152
-
153
- def log_request(prompt)
154
- caller_info = @calling_instance
155
- caller_info += " (#{@calling_instance_id})" if @calling_instance_id
156
- instance_info = @instance_name
157
- instance_info += " (#{@instance_id})" if @instance_id
158
- @logger.info("#{caller_info} -> #{instance_info}: \n---\n#{prompt}\n---")
159
-
160
- # Build event hash for JSON logging
161
- event = {
162
- type: "request",
163
- from_instance: @calling_instance,
164
- from_instance_id: @calling_instance_id,
165
- to_instance: @instance_name,
166
- to_instance_id: @instance_id,
167
- prompt: prompt,
168
- timestamp: Time.now.iso8601,
169
- }
170
-
171
- append_to_session_json(event)
172
- end
173
-
174
- def log_response(response)
175
- caller_info = @calling_instance
176
- caller_info += " (#{@calling_instance_id})" if @calling_instance_id
177
- instance_info = @instance_name
178
- instance_info += " (#{@instance_id})" if @instance_id
179
- @logger.info(
180
- "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{instance_info} -> #{caller_info}: \n---\n#{response["result"]}\n---",
181
- )
97
+ logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
182
98
  end
183
99
 
184
100
  def log_streaming_event(event)
@@ -198,13 +114,13 @@ module ClaudeSwarm
198
114
  end
199
115
 
200
116
  def log_system_message(event)
201
- @logger.debug("SYSTEM: #{JSON.pretty_generate(event)}")
117
+ logger.debug { "SYSTEM: #{JSON.pretty_generate(event)}" }
202
118
  end
203
119
 
204
120
  def log_assistant_message(msg)
205
121
  # Assistant messages don't have stop_reason in SDK - they only have content
206
122
  content = msg["content"]
207
- @logger.debug("ASSISTANT: #{JSON.pretty_generate(content)}") if content
123
+ logger.debug { "ASSISTANT: #{JSON.pretty_generate(content)}" } if content
208
124
 
209
125
  # Log tool calls
210
126
  tool_calls = content&.select { |c| c["type"] == "tool_use" } || []
@@ -212,52 +128,20 @@ module ClaudeSwarm
212
128
  arguments = tool_call["input"].to_json
213
129
  arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
214
130
 
215
- instance_info = @instance_name
216
- instance_info += " (#{@instance_id})" if @instance_id
217
- @logger.info(
218
- "Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}",
219
- )
131
+ logger.info do
132
+ "Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
133
+ end
220
134
  end
221
135
 
222
136
  # Log thinking text
223
137
  text = content&.select { |c| c["type"] == "text" } || []
224
138
  text.each do |t|
225
- instance_info = @instance_name
226
- instance_info += " (#{@instance_id})" if @instance_id
227
- @logger.info("#{instance_info} is thinking:\n---\n#{t["text"]}\n---")
139
+ logger.info { "#{instance_info} is thinking:\n---\n#{t["text"]}\n---" }
228
140
  end
229
141
  end
230
142
 
231
143
  def log_user_message(content)
232
- @logger.debug("USER: #{JSON.pretty_generate(content)}")
233
- end
234
-
235
- def append_to_session_json(event)
236
- json_filename = "session.log.json"
237
- json_path = File.join(@session_path, json_filename)
238
-
239
- # Use file locking to ensure thread-safe writes
240
- File.open(json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
241
- file.flock(File::LOCK_EX)
242
-
243
- # Create entry with metadata
244
- entry = {
245
- instance: @instance_name,
246
- instance_id: @instance_id,
247
- calling_instance: @calling_instance,
248
- calling_instance_id: @calling_instance_id,
249
- timestamp: Time.now.iso8601,
250
- event: event,
251
- }
252
-
253
- # Write as single line JSON (JSONL format)
254
- file.puts(entry.to_json)
255
-
256
- file.flock(File::LOCK_UN)
257
- end
258
- rescue StandardError => e
259
- @logger.error("Failed to append to session JSON: #{e.message}")
260
- raise
144
+ logger.debug { "USER: #{JSON.pretty_generate(content)}" }
261
145
  end
262
146
 
263
147
  def build_sdk_options(prompt, options)
@@ -329,14 +213,14 @@ module ClaudeSwarm
329
213
  headers: server_config["headers"] || {},
330
214
  )
331
215
  else
332
- @logger.warn("Unsupported MCP server type: #{server_type} for server: #{name}")
216
+ logger.warn { "Unsupported MCP server type: #{server_type} for server: #{name}" }
333
217
  nil
334
218
  end
335
219
  end
336
220
 
337
221
  mcp_servers.compact
338
222
  rescue StandardError => e
339
- @logger.error("Failed to parse MCP config: #{e.message}")
223
+ logger.error { "Failed to parse MCP config: #{e.message}" }
340
224
  {}
341
225
  end
342
226
 
@@ -347,7 +231,7 @@ module ClaudeSwarm
347
231
  @additional_directories.each do |dir|
348
232
  # This is a placeholder - the SDK doesn't directly support file system servers
349
233
  # You would need to implement a proper MCP server that provides file access
350
- @logger.warn("Additional directories not fully supported: #{dir}")
234
+ logger.warn { "Additional directories not fully supported: #{dir}" }
351
235
  end
352
236
  end
353
237
 
@@ -452,8 +336,5 @@ module ClaudeSwarm
452
336
  end
453
337
  end
454
338
  end
455
-
456
- class ExecutionError < StandardError; end
457
- class ParseError < StandardError; end
458
339
  end
459
340
  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"
@@ -230,6 +230,7 @@ module ClaudeSwarm
230
230
  instance_config,
231
231
  calling_instance: options[:calling_instance],
232
232
  calling_instance_id: options[:calling_instance_id],
233
+ debug: options[:debug],
233
234
  )
234
235
  server.start
235
236
  rescue StandardError => e
@@ -5,11 +5,11 @@ module ClaudeSwarm
5
5
  class ChatCompletion
6
6
  MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
7
7
 
8
- def initialize(openai_client:, mcp_client:, available_tools:, logger:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
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 = logger # This is actually the executor, not a logger
12
+ @executor = executor
13
13
  @instance_name = instance_name
14
14
  @model = model
15
15
  @temperature = temperature
@@ -57,7 +57,7 @@ module ClaudeSwarm
57
57
  def process_chat_completion(messages, depth = 0)
58
58
  # Prevent infinite recursion
59
59
  if depth > MAX_TURNS_WITH_TOOLS
60
- @executor.error("Maximum recursion depth reached in tool execution")
60
+ @executor.logger.error { "Maximum recursion depth reached in tool execution" }
61
61
  return "Error: Maximum tool call depth exceeded"
62
62
  end
63
63
 
@@ -83,7 +83,7 @@ module ClaudeSwarm
83
83
  parameters[:tools] = @mcp_client.to_openai_tools if @available_tools&.any? && @mcp_client
84
84
 
85
85
  # Log the request parameters
86
- @executor.info("Chat API Request (depth=#{depth}): #{JSON.pretty_generate(parameters)}")
86
+ @executor.logger.info { "Chat API Request (depth=#{depth}): #{JSON.pretty_generate(parameters)}" }
87
87
 
88
88
  # Append to session JSON
89
89
  append_to_session_json({
@@ -97,16 +97,16 @@ module ClaudeSwarm
97
97
  begin
98
98
  response = @openai_client.chat(parameters: parameters)
99
99
  rescue StandardError => e
100
- @executor.error("Chat API error: #{e.class} - #{e.message}")
101
- @executor.error("Request parameters: #{JSON.pretty_generate(parameters)}")
100
+ @executor.logger.error { "Chat API error: #{e.class} - #{e.message}" }
101
+ @executor.logger.error { "Request parameters: #{JSON.pretty_generate(parameters)}" }
102
102
 
103
103
  # Try to extract and log the response body for better debugging
104
104
  if e.respond_to?(:response)
105
105
  begin
106
106
  error_body = e.response[:body]
107
- @executor.error("Error response body: #{error_body}")
107
+ @executor.logger.error { "Error response body: #{error_body}" }
108
108
  rescue StandardError => parse_error
109
- @executor.error("Could not parse error response: #{parse_error.message}")
109
+ @executor.logger.error { "Could not parse error response: #{parse_error.message}" }
110
110
  end
111
111
  end
112
112
 
@@ -127,7 +127,7 @@ module ClaudeSwarm
127
127
  end
128
128
 
129
129
  # Log the response
130
- @executor.info("Chat API Response (depth=#{depth}): #{JSON.pretty_generate(response)}")
130
+ @executor.logger.info { "Chat API Response (depth=#{depth}): #{JSON.pretty_generate(response)}" }
131
131
 
132
132
  # Append to session JSON
133
133
  append_to_session_json({
@@ -141,7 +141,7 @@ module ClaudeSwarm
141
141
  message = response.dig("choices", 0, "message")
142
142
 
143
143
  if message.nil?
144
- @executor.error("No message in response: #{response.inspect}")
144
+ @executor.logger.error { "No message in response: #{response.inspect}" }
145
145
  return "Error: No response from OpenAI"
146
146
  end
147
147
 
@@ -169,7 +169,7 @@ module ClaudeSwarm
169
169
 
170
170
  def execute_and_append_tool_results(tool_calls, messages)
171
171
  # Log tool calls
172
- @executor.info("Executing tool calls: #{JSON.pretty_generate(tool_calls)}")
172
+ @executor.logger.info { "Executing tool calls: #{JSON.pretty_generate(tool_calls)}" }
173
173
 
174
174
  # Append to session JSON
175
175
  append_to_session_json({
@@ -189,13 +189,13 @@ module ClaudeSwarm
189
189
  tool_args = tool_args_str.is_a?(String) ? JSON.parse(tool_args_str) : tool_args_str
190
190
 
191
191
  # Log tool execution
192
- @executor.info("Executing tool: #{tool_name} with args: #{JSON.pretty_generate(tool_args)}")
192
+ @executor.logger.info { "Executing tool: #{tool_name} with args: #{JSON.pretty_generate(tool_args)}" }
193
193
 
194
194
  # Execute tool via MCP
195
195
  result = @mcp_client.call_tool(tool_name, tool_args)
196
196
 
197
197
  # Log result
198
- @executor.info("Tool result for #{tool_name}: #{result}")
198
+ @executor.logger.info { "Tool result for #{tool_name}: #{result}" }
199
199
 
200
200
  # Append to session JSON
201
201
  append_to_session_json({
@@ -214,8 +214,8 @@ module ClaudeSwarm
214
214
  content: result.to_s,
215
215
  }
216
216
  rescue StandardError => e
217
- @executor.error("Tool execution failed for #{tool_name}: #{e.message}")
218
- @executor.error(e.backtrace.join("\n"))
217
+ @executor.logger.error { "Tool execution failed for #{tool_name}: #{e.message}" }
218
+ @executor.logger.error { e.backtrace.join("\n") }
219
219
 
220
220
  # Append error to session JSON
221
221
  append_to_session_json({