claude_swarm 0.3.11 → 1.0.1
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 +48 -0
- data/CLAUDE.md +2 -0
- data/README.md +58 -0
- data/lib/claude_swarm/claude_code_executor.rb +10 -5
- data/lib/claude_swarm/cli.rb +11 -13
- data/lib/claude_swarm/commands/ps.rb +8 -8
- data/lib/claude_swarm/commands/show.rb +4 -5
- data/lib/claude_swarm/configuration.rb +2 -2
- data/lib/claude_swarm/json_handler.rb +91 -0
- data/lib/claude_swarm/mcp_generator.rb +4 -4
- data/lib/claude_swarm/openai/chat_completion.rb +6 -6
- data/lib/claude_swarm/openai/executor.rb +1 -1
- data/lib/claude_swarm/openai/responses.rb +8 -8
- data/lib/claude_swarm/orchestrator.rb +70 -46
- data/lib/claude_swarm/session_cost_calculator.rb +6 -6
- data/lib/claude_swarm/session_path.rb +2 -8
- data/lib/claude_swarm/settings_generator.rb +1 -1
- data/lib/claude_swarm/templates/generation_prompt.md.erb +3 -0
- 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 +22 -2
- data/team_v2.yml +367 -0
- metadata +8 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 10448e995f0dfb02668037c803ef03a7c9078c35deff18643c3479ec2ff14aee
|
4
|
+
data.tar.gz: f362d0f8c63e91c09ee1e36405aa79aa8c3abae2f2b19a4cbfb626e686da15dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 88ef64675a48dc613f12b50701164050bf2fcc6dc433f0b976858737b07ccb4c167b3c9e5c1775535d89b157155aa8cb95942ff468c5258b3b03cbb802db988f
|
7
|
+
data.tar.gz: 3d2e4dc12833979001347f97fd3e6b9c914b8f1723799104eac1e4d0f48af3e4d92edc9bb0f02b8f59ab72ee7e9f9608e9d0b78a5386e1b5ada59b54d7913128
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,53 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [1.0.1]
|
4
|
+
|
5
|
+
### Fixed
|
6
|
+
- **Fixed require statement for fast-mcp gem**: Updated `require "fast_mcp_annotations"` to `require "fast_mcp"` to match the gem dependency change in 1.0.0
|
7
|
+
- The gemspec was correctly updated to use fast-mcp (~> 1.6) in 1.0.0, but the require statement was not updated
|
8
|
+
- This fix ensures the correct gem is loaded at runtime
|
9
|
+
|
10
|
+
## [1.0.0]
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- **HTTP MCP server configuration support**: Added support for HTTP-type MCP servers alongside existing stdio and SSE types
|
14
|
+
- HTTP servers can be configured with a `url` field
|
15
|
+
- Properly preserves server type (http/sse) in generated configurations
|
16
|
+
- Full compatibility with Claude Code's HTTP MCP server implementation
|
17
|
+
|
18
|
+
### Changed
|
19
|
+
- **Updated dependency from fast-mcp-annotations to fast-mcp gem**: Migrated to the consolidated fast-mcp gem (~> 1.6)
|
20
|
+
- Consolidates MCP functionality into a single, more maintainable gem
|
21
|
+
- Maintains all existing functionality with improved performance
|
22
|
+
- **Updated claude-code-sdk-ruby dependency**: Minimum version requirement increased to 0.1.6
|
23
|
+
- Includes latest SDK improvements and bug fixes
|
24
|
+
- Better error handling and stability
|
25
|
+
- **Swarm generation improvements**: Generator now avoids creating swarms with circular dependencies
|
26
|
+
- Prevents generation of invalid configurations
|
27
|
+
- Improves reliability of generated swarm templates
|
28
|
+
|
29
|
+
### Fixed
|
30
|
+
- **Empty response validation**: Added validation to prevent empty or nil responses from being passed to MCP callers
|
31
|
+
- ClaudeCodeExecutor now validates that Claude SDK returns non-empty result content
|
32
|
+
- TaskTool validates response structure and content before returning to MCP caller
|
33
|
+
- Clear error messages indicate when agent completes execution but provides no response
|
34
|
+
- Prevents silent failures when Claude SDK returns empty results
|
35
|
+
- **Before commands directory handling**: Fixed error when before commands need to create the main instance directory
|
36
|
+
- Smart directory detection: if the main instance directory exists, commands run inside it (for `npm install`, etc.)
|
37
|
+
- If the directory doesn't exist, commands run in the parent directory (allowing `mkdir` commands to create it)
|
38
|
+
- Works correctly with both regular directories and Git worktrees
|
39
|
+
- After commands follow the same logic for consistency
|
40
|
+
- Fixes "No such file or directory @ dir_chdir" errors when before commands create directories
|
41
|
+
|
42
|
+
### Internal
|
43
|
+
- **Centralized JSON handling**: Added JsonHandler class for consistent JSON parsing and generation
|
44
|
+
- Improved error handling for malformed JSON files
|
45
|
+
- Standardized JSON output formatting across all modules
|
46
|
+
- **Centralized CLAUDE_SWARM_HOME handling**: Refactored environment variable management for cleaner code
|
47
|
+
- **Enhanced test coverage**: Added comprehensive configuration tests and enabled branch coverage in SimpleCov
|
48
|
+
- Extensive validation of edge cases including circular dependencies
|
49
|
+
- Better coverage metrics with branch coverage enabled
|
50
|
+
|
3
51
|
## [0.3.11]
|
4
52
|
|
5
53
|
### Added
|
data/CLAUDE.md
CHANGED
@@ -6,6 +6,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|
6
6
|
|
7
7
|
Claude Swarm is a Ruby gem that orchestrates multiple Claude Code instances as a collaborative AI development team. It enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol).
|
8
8
|
|
9
|
+
SwarmCore is a complete reimagining of Claude Swarm that decouples from Claude Code and runs everything in a single process using RubyLLM for all LLM interactions. It is being developed in `lib/swarm_core`, and using the gemspec swarm-core.gemspec.
|
10
|
+
|
9
11
|
## Development Commands
|
10
12
|
|
11
13
|
### Testing
|
data/README.md
CHANGED
@@ -228,6 +228,53 @@ swarm:
|
|
228
228
|
# Instance definitions...
|
229
229
|
```
|
230
230
|
|
231
|
+
#### YAML Aliases
|
232
|
+
|
233
|
+
Claude Swarm supports [YAML aliases](https://yaml.org/spec/1.2.2/#71-alias-nodes) to reduce duplication in your configuration. This is particularly useful for sharing common values like prompts, tool lists, or MCP configurations across multiple instances:
|
234
|
+
|
235
|
+
```yaml
|
236
|
+
version: 1
|
237
|
+
swarm:
|
238
|
+
name: "Development Team"
|
239
|
+
main: lead
|
240
|
+
instances:
|
241
|
+
lead:
|
242
|
+
description: "Lead developer"
|
243
|
+
prompt: &shared_prompt "You are an expert developer following best practices"
|
244
|
+
allowed_tools: &standard_tools
|
245
|
+
- Read
|
246
|
+
- Edit
|
247
|
+
- Bash
|
248
|
+
- WebSearch
|
249
|
+
mcps: &common_mcps
|
250
|
+
- name: github
|
251
|
+
type: stdio
|
252
|
+
command: gh
|
253
|
+
args: ["mcp"]
|
254
|
+
|
255
|
+
frontend:
|
256
|
+
description: "Frontend developer"
|
257
|
+
prompt: *shared_prompt # Reuses the same prompt
|
258
|
+
allowed_tools: *standard_tools # Reuses the same tool list
|
259
|
+
mcps: *common_mcps # Reuses the same MCP servers
|
260
|
+
directory: ./frontend
|
261
|
+
|
262
|
+
backend:
|
263
|
+
description: "Backend developer"
|
264
|
+
prompt: *shared_prompt # Reuses the same prompt
|
265
|
+
allowed_tools: *standard_tools # Reuses the same tool list
|
266
|
+
mcps: *common_mcps # Reuses the same MCP servers
|
267
|
+
directory: ./backend
|
268
|
+
```
|
269
|
+
|
270
|
+
In this example:
|
271
|
+
- `&shared_prompt` defines an anchor for the prompt string
|
272
|
+
- `&standard_tools` defines an anchor for the tool array
|
273
|
+
- `&common_mcps` defines an anchor for the MCP configuration
|
274
|
+
- `*shared_prompt`, `*standard_tools`, and `*common_mcps` reference those anchors
|
275
|
+
|
276
|
+
This helps maintain consistency across instances and makes configuration updates easier.
|
277
|
+
|
231
278
|
#### Environment Variable Interpolation
|
232
279
|
|
233
280
|
Claude Swarm supports environment variable interpolation in all configuration values using the `${ENV_VAR_NAME}` syntax:
|
@@ -395,6 +442,17 @@ mcps:
|
|
395
442
|
X-Custom-Header: "value"
|
396
443
|
```
|
397
444
|
|
445
|
+
#### http (HTTP-based MCP)
|
446
|
+
```yaml
|
447
|
+
mcps:
|
448
|
+
- name: http_service
|
449
|
+
type: http
|
450
|
+
url: "https://api.example.com/mcp-endpoint"
|
451
|
+
headers: # Optional: custom headers for authentication
|
452
|
+
Authorization: "Bearer ${API_TOKEN}"
|
453
|
+
X-API-Key: "${API_KEY}"
|
454
|
+
```
|
455
|
+
|
398
456
|
### Hooks Configuration
|
399
457
|
|
400
458
|
Claude Swarm supports configuring [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) for each instance. Hooks allow you to run custom scripts before/after tools, on prompt submission, and more. Each instance can have its own hooks configuration.
|
@@ -40,6 +40,11 @@ module ClaudeSwarm
|
|
40
40
|
# Assistant messages only contain content blocks
|
41
41
|
# No need to track for result extraction - result comes from Result message
|
42
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."
|
46
|
+
end
|
47
|
+
|
43
48
|
# Build result response in expected format
|
44
49
|
result_response = {
|
45
50
|
"type" => "result",
|
@@ -91,7 +96,7 @@ module ClaudeSwarm
|
|
91
96
|
updated_at: Time.now.iso8601,
|
92
97
|
}
|
93
98
|
|
94
|
-
|
99
|
+
JsonHandler.write_file!(state_file, state_data)
|
95
100
|
logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
|
96
101
|
rescue StandardError => e
|
97
102
|
logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
|
@@ -114,13 +119,13 @@ module ClaudeSwarm
|
|
114
119
|
end
|
115
120
|
|
116
121
|
def log_system_message(event)
|
117
|
-
logger.debug { "SYSTEM: #{
|
122
|
+
logger.debug { "SYSTEM: #{JsonHandler.pretty_generate!(event)}" }
|
118
123
|
end
|
119
124
|
|
120
125
|
def log_assistant_message(msg)
|
121
126
|
# Assistant messages don't have stop_reason in SDK - they only have content
|
122
127
|
content = msg["content"]
|
123
|
-
logger.debug { "ASSISTANT: #{
|
128
|
+
logger.debug { "ASSISTANT: #{JsonHandler.pretty_generate!(content)}" } if content
|
124
129
|
|
125
130
|
# Log tool calls
|
126
131
|
tool_calls = content&.select { |c| c["type"] == "tool_use" } || []
|
@@ -141,7 +146,7 @@ module ClaudeSwarm
|
|
141
146
|
end
|
142
147
|
|
143
148
|
def log_user_message(content)
|
144
|
-
logger.debug { "USER: #{
|
149
|
+
logger.debug { "USER: #{JsonHandler.pretty_generate!(content)}" }
|
145
150
|
end
|
146
151
|
|
147
152
|
def build_sdk_options(prompt, options)
|
@@ -193,7 +198,7 @@ module ClaudeSwarm
|
|
193
198
|
|
194
199
|
def parse_mcp_config(config_path)
|
195
200
|
# Parse MCP JSON config file and convert to SDK format
|
196
|
-
config =
|
201
|
+
config = JsonHandler.parse_file!(config_path)
|
197
202
|
mcp_servers = {}
|
198
203
|
|
199
204
|
config["mcpServers"]&.each do |name, server_config|
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -406,12 +406,12 @@ module ClaudeSwarm
|
|
406
406
|
desc: "Number of lines to show initially"
|
407
407
|
def watch(session_id)
|
408
408
|
# Find session path
|
409
|
-
run_symlink =
|
409
|
+
run_symlink = ClaudeSwarm.joined_run_dir(session_id)
|
410
410
|
session_path = if File.symlink?(run_symlink)
|
411
411
|
File.readlink(run_symlink)
|
412
412
|
else
|
413
413
|
# Search in sessions directory
|
414
|
-
Dir.glob(
|
414
|
+
Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
|
415
415
|
File.basename(path) == session_id
|
416
416
|
end
|
417
417
|
end
|
@@ -437,7 +437,7 @@ module ClaudeSwarm
|
|
437
437
|
default: 10,
|
438
438
|
desc: "Maximum number of sessions to display"
|
439
439
|
def list_sessions
|
440
|
-
sessions_dir =
|
440
|
+
sessions_dir = ClaudeSwarm.joined_sessions_dir
|
441
441
|
unless Dir.exist?(sessions_dir)
|
442
442
|
say("No sessions found", :yellow)
|
443
443
|
return
|
@@ -562,12 +562,10 @@ module ClaudeSwarm
|
|
562
562
|
# Load session metadata if it exists to check for worktree info
|
563
563
|
session_metadata_file = File.join(session_path, "session_metadata.json")
|
564
564
|
worktree_name = nil
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
|
570
|
-
end
|
565
|
+
metadata = JsonHandler.parse_file(session_metadata_file)
|
566
|
+
if metadata && metadata["worktree"] && metadata["worktree"]["enabled"]
|
567
|
+
worktree_name = metadata["worktree"]["name"]
|
568
|
+
say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
|
571
569
|
end
|
572
570
|
|
573
571
|
# Create orchestrator with restoration mode
|
@@ -592,7 +590,7 @@ module ClaudeSwarm
|
|
592
590
|
end
|
593
591
|
|
594
592
|
def find_session_path(session_id)
|
595
|
-
sessions_dir =
|
593
|
+
sessions_dir = ClaudeSwarm.joined_sessions_dir
|
596
594
|
|
597
595
|
# Search for the session ID in all projects
|
598
596
|
Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
|
@@ -604,7 +602,7 @@ module ClaudeSwarm
|
|
604
602
|
end
|
605
603
|
|
606
604
|
def clean_stale_symlinks(days)
|
607
|
-
run_dir =
|
605
|
+
run_dir = ClaudeSwarm.joined_run_dir
|
608
606
|
return 0 unless Dir.exist?(run_dir)
|
609
607
|
|
610
608
|
cleaned = 0
|
@@ -633,10 +631,10 @@ module ClaudeSwarm
|
|
633
631
|
end
|
634
632
|
|
635
633
|
def clean_orphaned_worktrees(days)
|
636
|
-
worktrees_dir =
|
634
|
+
worktrees_dir = ClaudeSwarm.joined_worktrees_dir
|
637
635
|
return 0 unless Dir.exist?(worktrees_dir)
|
638
636
|
|
639
|
-
sessions_dir =
|
637
|
+
sessions_dir = ClaudeSwarm.joined_sessions_dir
|
640
638
|
cleaned = 0
|
641
639
|
|
642
640
|
Dir.glob("#{worktrees_dir}/*").each do |session_worktree_dir|
|
@@ -3,10 +3,9 @@
|
|
3
3
|
module ClaudeSwarm
|
4
4
|
module Commands
|
5
5
|
class Ps
|
6
|
-
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
7
|
-
|
8
6
|
def execute
|
9
|
-
|
7
|
+
run_dir = ClaudeSwarm.joined_run_dir
|
8
|
+
unless Dir.exist?(run_dir)
|
10
9
|
puts "No active sessions"
|
11
10
|
return
|
12
11
|
end
|
@@ -14,7 +13,7 @@ module ClaudeSwarm
|
|
14
13
|
sessions = []
|
15
14
|
|
16
15
|
# Read all symlinks in run directory
|
17
|
-
Dir.glob("#{
|
16
|
+
Dir.glob("#{run_dir}/*").each do |symlink|
|
18
17
|
next unless File.symlink?(symlink)
|
19
18
|
|
20
19
|
begin
|
@@ -147,9 +146,10 @@ module ClaudeSwarm
|
|
147
146
|
def get_start_time(session_dir)
|
148
147
|
# Try to get from session metadata first
|
149
148
|
metadata_file = File.join(session_dir, "session_metadata.json")
|
150
|
-
|
151
|
-
|
152
|
-
|
149
|
+
metadata = JsonHandler.parse_file(metadata_file)
|
150
|
+
|
151
|
+
if metadata && metadata["start_time"]
|
152
|
+
return Time.parse(metadata["start_time"])
|
153
153
|
end
|
154
154
|
|
155
155
|
# Fallback to directory creation time
|
@@ -179,7 +179,7 @@ module ClaudeSwarm
|
|
179
179
|
session_metadata_file = File.join(session_dir, "session_metadata.json")
|
180
180
|
return directories unless File.exist?(session_metadata_file)
|
181
181
|
|
182
|
-
metadata =
|
182
|
+
metadata = JsonHandler.parse_file!(session_metadata_file)
|
183
183
|
worktree_info = metadata["worktree"]
|
184
184
|
return directories unless worktree_info && worktree_info["enabled"]
|
185
185
|
|
@@ -63,23 +63,22 @@ module ClaudeSwarm
|
|
63
63
|
|
64
64
|
def find_session_path(session_id)
|
65
65
|
# First check the run directory
|
66
|
-
run_symlink =
|
66
|
+
run_symlink = ClaudeSwarm.joined_run_dir(session_id)
|
67
67
|
if File.symlink?(run_symlink)
|
68
68
|
target = File.readlink(run_symlink)
|
69
69
|
return target if Dir.exist?(target)
|
70
70
|
end
|
71
71
|
|
72
72
|
# Fall back to searching all sessions
|
73
|
-
Dir.glob(
|
73
|
+
Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
|
74
74
|
File.basename(path) == session_id
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
78
|
def get_runtime_info(session_path)
|
79
79
|
metadata_file = File.join(session_path, "session_metadata.json")
|
80
|
-
|
81
|
-
|
82
|
-
metadata = JSON.parse(File.read(metadata_file))
|
80
|
+
metadata = JsonHandler.parse_file(metadata_file)
|
81
|
+
return unless metadata
|
83
82
|
|
84
83
|
if metadata["duration_seconds"]
|
85
84
|
# Session has completed
|
@@ -59,7 +59,7 @@ module ClaudeSwarm
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def load_and_validate
|
62
|
-
@config = YAML.load_file(@config_path)
|
62
|
+
@config = YAML.load_file(@config_path, aliases: true)
|
63
63
|
interpolate_env_vars!(@config)
|
64
64
|
validate_version
|
65
65
|
validate_swarm
|
@@ -248,7 +248,7 @@ module ClaudeSwarm
|
|
248
248
|
case mcp["type"]
|
249
249
|
when "stdio"
|
250
250
|
raise Error, "MCP '#{mcp["name"]}' missing 'command'" unless mcp["command"]
|
251
|
-
when "sse"
|
251
|
+
when "sse", "http"
|
252
252
|
raise Error, "MCP '#{mcp["name"]}' missing 'url'" unless mcp["url"]
|
253
253
|
else
|
254
254
|
raise Error, "Unknown MCP type '#{mcp["type"]}' for '#{mcp["name"]}'"
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSwarm
|
4
|
+
# Centralized JSON handling for the Claude Swarm codebase
|
5
|
+
class JsonHandler
|
6
|
+
class << self
|
7
|
+
# Parse JSON string into Ruby object
|
8
|
+
# @param json_string [String] The JSON string to parse
|
9
|
+
# @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
|
10
|
+
# @return [Object] The parsed Ruby object, or original string if parsing fails and raise_on_error is false
|
11
|
+
# @raise [JSON::ParserError] If the JSON is invalid and raise_on_error is true
|
12
|
+
def parse(json_string, raise_on_error: false)
|
13
|
+
JSON.parse(json_string)
|
14
|
+
rescue JSON::ParserError => e
|
15
|
+
raise e if raise_on_error
|
16
|
+
|
17
|
+
json_string
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parse JSON string with exception raising
|
21
|
+
# @param json_string [String] The JSON string to parse
|
22
|
+
# @return [Object] The parsed Ruby object
|
23
|
+
# @raise [JSON::ParserError] If the JSON is invalid
|
24
|
+
def parse!(json_string)
|
25
|
+
parse(json_string, raise_on_error: true)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parse JSON from a file with exception raising
|
29
|
+
# @param file_path [String] Path to the JSON file
|
30
|
+
# @return [Object] The parsed Ruby object
|
31
|
+
# @raise [Errno::ENOENT] If the file does not exist
|
32
|
+
# @raise [JSON::ParserError] If the file contains invalid JSON
|
33
|
+
def parse_file!(file_path)
|
34
|
+
content = File.read(file_path)
|
35
|
+
parse!(content)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Parse JSON from a file, returning nil on error
|
39
|
+
# @param file_path [String] Path to the JSON file
|
40
|
+
# @return [Object, nil] The parsed Ruby object or nil if file doesn't exist or contains invalid JSON
|
41
|
+
def parse_file(file_path)
|
42
|
+
parse_file!(file_path)
|
43
|
+
rescue Errno::ENOENT, JSON::ParserError
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Generate pretty-formatted JSON string
|
48
|
+
# @param object [Object] The Ruby object to convert to JSON
|
49
|
+
# @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
|
50
|
+
# @return [String, nil] The pretty-formatted JSON string, or nil if generation fails and raise_on_error is false
|
51
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON and raise_on_error is true
|
52
|
+
def pretty_generate(object, raise_on_error: false)
|
53
|
+
JSON.pretty_generate(object)
|
54
|
+
rescue JSON::GeneratorError, JSON::NestingError => e
|
55
|
+
raise e if raise_on_error
|
56
|
+
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate pretty-formatted JSON string with exception raising
|
61
|
+
# @param object [Object] The Ruby object to convert to JSON
|
62
|
+
# @return [String] The pretty-formatted JSON string
|
63
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON
|
64
|
+
def pretty_generate!(object)
|
65
|
+
pretty_generate(object, raise_on_error: true)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Write Ruby object to a JSON file with pretty formatting
|
69
|
+
# @param file_path [String] Path to the JSON file
|
70
|
+
# @param object [Object] The Ruby object to write
|
71
|
+
# @return [Boolean] True if successful, false if generation or write fails
|
72
|
+
def write_file(file_path, object)
|
73
|
+
json_string = pretty_generate!(object)
|
74
|
+
File.write(file_path, json_string)
|
75
|
+
true
|
76
|
+
rescue JSON::GeneratorError, JSON::NestingError, SystemCallError
|
77
|
+
false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Write Ruby object to a JSON file with exception raising
|
81
|
+
# @param file_path [String] Path to the JSON file
|
82
|
+
# @param object [Object] The Ruby object to write
|
83
|
+
# @raise [JSON::GeneratorError] If the object cannot be converted to JSON
|
84
|
+
# @raise [SystemCallError] If the file cannot be written
|
85
|
+
def write_file!(file_path, object)
|
86
|
+
json_string = pretty_generate!(object)
|
87
|
+
File.write(file_path, json_string)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -73,7 +73,7 @@ module ClaudeSwarm
|
|
73
73
|
"mcpServers" => mcp_servers,
|
74
74
|
}
|
75
75
|
|
76
|
-
|
76
|
+
JsonHandler.write_file!(mcp_config_path(name), config)
|
77
77
|
end
|
78
78
|
|
79
79
|
def build_mcp_server_config(mcp)
|
@@ -86,9 +86,9 @@ module ClaudeSwarm
|
|
86
86
|
}.tap do |config|
|
87
87
|
config["env"] = mcp["env"] if mcp["env"]
|
88
88
|
end
|
89
|
-
when "sse"
|
89
|
+
when "sse", "http"
|
90
90
|
{
|
91
|
-
"type" => "
|
91
|
+
"type" => mcp["type"],
|
92
92
|
"url" => mcp["url"],
|
93
93
|
}.tap do |config|
|
94
94
|
config["headers"] = mcp["headers"] if mcp["headers"]
|
@@ -220,7 +220,7 @@ module ClaudeSwarm
|
|
220
220
|
return unless Dir.exist?(state_dir)
|
221
221
|
|
222
222
|
Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
|
223
|
-
data =
|
223
|
+
data = JsonHandler.parse_file!(state_file)
|
224
224
|
instance_name = data["instance_name"]
|
225
225
|
instance_id = data["instance_id"]
|
226
226
|
|
@@ -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.logger.info { "Chat API Request (depth=#{depth}): #{
|
86
|
+
@executor.logger.info { "Chat API Request (depth=#{depth}): #{JsonHandler.pretty_generate!(parameters)}" }
|
87
87
|
|
88
88
|
# Append to session JSON
|
89
89
|
append_to_session_json({
|
@@ -98,7 +98,7 @@ module ClaudeSwarm
|
|
98
98
|
response = @openai_client.chat(parameters: parameters)
|
99
99
|
rescue StandardError => e
|
100
100
|
@executor.logger.error { "Chat API error: #{e.class} - #{e.message}" }
|
101
|
-
@executor.logger.error { "Request parameters: #{
|
101
|
+
@executor.logger.error { "Request parameters: #{JsonHandler.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)
|
@@ -127,7 +127,7 @@ module ClaudeSwarm
|
|
127
127
|
end
|
128
128
|
|
129
129
|
# Log the response
|
130
|
-
@executor.logger.info { "Chat API Response (depth=#{depth}): #{
|
130
|
+
@executor.logger.info { "Chat API Response (depth=#{depth}): #{JsonHandler.pretty_generate!(response)}" }
|
131
131
|
|
132
132
|
# Append to session JSON
|
133
133
|
append_to_session_json({
|
@@ -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.logger.info { "Executing tool calls: #{
|
172
|
+
@executor.logger.info { "Executing tool calls: #{JsonHandler.pretty_generate!(tool_calls)}" }
|
173
173
|
|
174
174
|
# Append to session JSON
|
175
175
|
append_to_session_json({
|
@@ -186,10 +186,10 @@ module ClaudeSwarm
|
|
186
186
|
|
187
187
|
begin
|
188
188
|
# Parse arguments
|
189
|
-
tool_args = tool_args_str.is_a?(String) ?
|
189
|
+
tool_args = tool_args_str.is_a?(String) ? JsonHandler.parse!(tool_args_str) : tool_args_str
|
190
190
|
|
191
191
|
# Log tool execution
|
192
|
-
@executor.logger.info { "Executing tool: #{tool_name} with args: #{
|
192
|
+
@executor.logger.info { "Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
|
193
193
|
|
194
194
|
# Execute tool via MCP
|
195
195
|
result = @mcp_client.call_tool(tool_name, tool_args)
|
@@ -122,7 +122,7 @@ module ClaudeSwarm
|
|
122
122
|
return unless @mcp_config && File.exist?(@mcp_config)
|
123
123
|
|
124
124
|
# Read MCP config to find MCP servers
|
125
|
-
mcp_data =
|
125
|
+
mcp_data = JsonHandler.parse_file!(@mcp_config)
|
126
126
|
|
127
127
|
# Build MCP configurations from servers
|
128
128
|
mcp_configs = build_mcp_configs(mcp_data["mcpServers"])
|
@@ -97,7 +97,7 @@ module ClaudeSwarm
|
|
97
97
|
end
|
98
98
|
|
99
99
|
# Log the request parameters
|
100
|
-
@executor.logger.info { "Responses API Request (depth=#{depth}): #{
|
100
|
+
@executor.logger.info { "Responses API Request (depth=#{depth}): #{JsonHandler.pretty_generate!(parameters)}" }
|
101
101
|
|
102
102
|
# Append to session JSON
|
103
103
|
append_to_session_json({
|
@@ -112,7 +112,7 @@ module ClaudeSwarm
|
|
112
112
|
response = @openai_client.responses.create(parameters: parameters)
|
113
113
|
rescue StandardError => e
|
114
114
|
@executor.logger.error { "Responses API error: #{e.class} - #{e.message}" }
|
115
|
-
@executor.logger.error { "Request parameters: #{
|
115
|
+
@executor.logger.error { "Request parameters: #{JsonHandler.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)
|
@@ -140,7 +140,7 @@ module ClaudeSwarm
|
|
140
140
|
end
|
141
141
|
|
142
142
|
# Log the full response
|
143
|
-
@executor.logger.info { "Responses API Full Response (depth=#{depth}): #{
|
143
|
+
@executor.logger.info { "Responses API Full Response (depth=#{depth}): #{JsonHandler.pretty_generate!(response)}" }
|
144
144
|
|
145
145
|
# Append to session JSON
|
146
146
|
append_to_session_json({
|
@@ -230,10 +230,10 @@ module ClaudeSwarm
|
|
230
230
|
|
231
231
|
begin
|
232
232
|
# Parse arguments
|
233
|
-
tool_args =
|
233
|
+
tool_args = JsonHandler.parse!(tool_args_str)
|
234
234
|
|
235
235
|
# Log tool execution
|
236
|
-
@executor.logger.info { "Responses API - Executing tool: #{tool_name} with args: #{
|
236
|
+
@executor.logger.info { "Responses API - Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
|
237
237
|
|
238
238
|
# Execute tool via MCP
|
239
239
|
result = @mcp_client.call_tool(tool_name, tool_args)
|
@@ -283,7 +283,7 @@ module ClaudeSwarm
|
|
283
283
|
end
|
284
284
|
|
285
285
|
@executor.logger.info { "Responses API - Built conversation with #{conversation.size} function outputs" }
|
286
|
-
@executor.logger.debug { "Final conversation structure: #{
|
286
|
+
@executor.logger.debug { "Final conversation structure: #{JsonHandler.pretty_generate!(conversation)}" }
|
287
287
|
conversation
|
288
288
|
end
|
289
289
|
|
@@ -299,10 +299,10 @@ module ClaudeSwarm
|
|
299
299
|
|
300
300
|
begin
|
301
301
|
# Parse arguments
|
302
|
-
tool_args =
|
302
|
+
tool_args = JsonHandler.parse!(tool_args_str)
|
303
303
|
|
304
304
|
# Log tool execution
|
305
|
-
@executor.logger.info { "Responses API - Executing tool: #{tool_name} with args: #{
|
305
|
+
@executor.logger.info { "Responses API - Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
|
306
306
|
|
307
307
|
# Execute tool via MCP
|
308
308
|
result = @mcp_client.call_tool(tool_name, tool_args)
|