swarm_memory 2.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 +7 -0
- data/LICENSE +21 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +349 -0
- data/lib/claude_swarm/claude_mcp_server.rb +77 -0
- data/lib/claude_swarm/cli.rb +712 -0
- data/lib/claude_swarm/commands/ps.rb +216 -0
- data/lib/claude_swarm/commands/show.rb +139 -0
- data/lib/claude_swarm/configuration.rb +363 -0
- 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 +248 -0
- data/lib/claude_swarm/openai/chat_completion.rb +264 -0
- data/lib/claude_swarm/openai/executor.rb +254 -0
- data/lib/claude_swarm/openai/responses.rb +338 -0
- data/lib/claude_swarm/orchestrator.rb +879 -0
- data/lib/claude_swarm/process_tracker.rb +78 -0
- data/lib/claude_swarm/session_cost_calculator.rb +209 -0
- data/lib/claude_swarm/session_path.rb +42 -0
- data/lib/claude_swarm/settings_generator.rb +77 -0
- data/lib/claude_swarm/system_utils.rb +46 -0
- data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
- data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
- data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
- data/lib/claude_swarm/tools/task_tool.rb +63 -0
- data/lib/claude_swarm/version.rb +5 -0
- data/lib/claude_swarm/worktree_manager.rb +475 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +69 -0
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +45 -0
- data/lib/swarm_memory/adapters/base.rb +140 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +789 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +286 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +139 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +201 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +76 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +40 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +145 -0
- data/lib/swarm_memory/tools/memory_grep.rb +209 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +215 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1144 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +416 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +167 -0
- metadata +313 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
class McpGenerator
|
|
5
|
+
def initialize(configuration, vibe: false, restore_session_path: nil)
|
|
6
|
+
@config = configuration
|
|
7
|
+
@vibe = vibe
|
|
8
|
+
@restore_session_path = restore_session_path
|
|
9
|
+
@session_path = nil # Will be set when needed
|
|
10
|
+
@instance_ids = {} # Store instance IDs for all instances
|
|
11
|
+
@restore_states = {} # Store loaded state data during restoration
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate_all
|
|
15
|
+
ensure_swarm_directory
|
|
16
|
+
|
|
17
|
+
if @restore_session_path
|
|
18
|
+
# Load existing instance IDs and states from state files
|
|
19
|
+
load_instance_states
|
|
20
|
+
else
|
|
21
|
+
# Generate new instance IDs
|
|
22
|
+
@config.instances.each_key do |name|
|
|
23
|
+
@instance_ids[name] = "#{name}_#{SecureRandom.hex(4)}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@config.instances.each do |name, instance|
|
|
28
|
+
generate_mcp_config(name, instance)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def mcp_config_path(instance_name)
|
|
33
|
+
File.join(session_path, "#{instance_name}.mcp.json")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def session_path
|
|
39
|
+
@session_path ||= SessionPath.from_env
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def ensure_swarm_directory
|
|
43
|
+
# Session directory is already created by orchestrator
|
|
44
|
+
# Just ensure it exists
|
|
45
|
+
SessionPath.ensure_directory(session_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def generate_mcp_config(name, instance)
|
|
49
|
+
mcp_servers = {}
|
|
50
|
+
|
|
51
|
+
# Add configured MCP servers
|
|
52
|
+
instance[:mcps].each do |mcp|
|
|
53
|
+
mcp_servers[mcp["name"]] = build_mcp_server_config(mcp)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Add connection MCPs for other instances
|
|
57
|
+
instance[:connections].each do |connection_name|
|
|
58
|
+
connected_instance = @config.instances[connection_name]
|
|
59
|
+
mcp_servers[connection_name] = build_instance_mcp_config(
|
|
60
|
+
connection_name,
|
|
61
|
+
connected_instance,
|
|
62
|
+
calling_instance: name,
|
|
63
|
+
calling_instance_id: @instance_ids[name],
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add Claude tools MCP server for OpenAI instances
|
|
68
|
+
mcp_servers["claude_tools"] = build_claude_tools_mcp_config if instance[:provider] == "openai"
|
|
69
|
+
|
|
70
|
+
config = {
|
|
71
|
+
"instance_id" => @instance_ids[name],
|
|
72
|
+
"instance_name" => name,
|
|
73
|
+
"mcpServers" => mcp_servers,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
JsonHandler.write_file!(mcp_config_path(name), config)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_mcp_server_config(mcp)
|
|
80
|
+
case mcp["type"]
|
|
81
|
+
when "stdio"
|
|
82
|
+
{
|
|
83
|
+
"type" => "stdio",
|
|
84
|
+
"command" => mcp["command"],
|
|
85
|
+
"args" => mcp["args"] || [],
|
|
86
|
+
}.tap do |config|
|
|
87
|
+
config["env"] = mcp["env"] if mcp["env"]
|
|
88
|
+
end
|
|
89
|
+
when "sse", "http"
|
|
90
|
+
{
|
|
91
|
+
"type" => mcp["type"],
|
|
92
|
+
"url" => mcp["url"],
|
|
93
|
+
}.tap do |config|
|
|
94
|
+
config["headers"] = mcp["headers"] if mcp["headers"]
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_claude_tools_mcp_config
|
|
100
|
+
# Build environment for claude mcp serve by excluding Ruby/Bundler-specific variables
|
|
101
|
+
# This preserves all system variables while removing Ruby contamination
|
|
102
|
+
clean_env = ENV.to_h.reject do |key, _|
|
|
103
|
+
key.start_with?("BUNDLE_") ||
|
|
104
|
+
key.start_with?("RUBY") ||
|
|
105
|
+
key.start_with?("GEM_") ||
|
|
106
|
+
key == "RUBYOPT" ||
|
|
107
|
+
key == "RUBYLIB"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
"type" => "stdio",
|
|
112
|
+
"command" => "claude",
|
|
113
|
+
"args" => ["mcp", "serve"],
|
|
114
|
+
"env" => clean_env,
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
|
|
119
|
+
# Get the path to the claude-swarm executable
|
|
120
|
+
exe_path = "claude-swarm"
|
|
121
|
+
|
|
122
|
+
# Build command-line arguments for Thor
|
|
123
|
+
args = [
|
|
124
|
+
"mcp-serve",
|
|
125
|
+
"--name",
|
|
126
|
+
name,
|
|
127
|
+
"--directory",
|
|
128
|
+
instance[:directory],
|
|
129
|
+
"--model",
|
|
130
|
+
instance[:model],
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
# Add directories array if we have multiple directories
|
|
134
|
+
args.push("--directories", *instance[:directories]) if instance[:directories] && instance[:directories].size > 1
|
|
135
|
+
|
|
136
|
+
# Add optional arguments
|
|
137
|
+
# Handle prompt_file by reading the file contents
|
|
138
|
+
if instance[:prompt_file]
|
|
139
|
+
prompt_file_path = File.join(@config.root_directory, instance[:prompt_file])
|
|
140
|
+
if File.exist?(prompt_file_path)
|
|
141
|
+
prompt_content = File.read(prompt_file_path)
|
|
142
|
+
args.push("--prompt", prompt_content)
|
|
143
|
+
end
|
|
144
|
+
elsif instance[:prompt]
|
|
145
|
+
args.push("--prompt", instance[:prompt])
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
args.push("--description", instance[:description]) if instance[:description]
|
|
149
|
+
|
|
150
|
+
args.push("--allowed-tools", instance[:allowed_tools].join(",")) if instance[:allowed_tools] && !instance[:allowed_tools].empty?
|
|
151
|
+
|
|
152
|
+
args.push("--disallowed-tools", instance[:disallowed_tools].join(",")) if instance[:disallowed_tools] && !instance[:disallowed_tools].empty?
|
|
153
|
+
|
|
154
|
+
args.push("--connections", instance[:connections].join(",")) if instance[:connections] && !instance[:connections].empty?
|
|
155
|
+
|
|
156
|
+
args.push("--mcp-config-path", mcp_config_path(name))
|
|
157
|
+
|
|
158
|
+
args.push("--calling-instance", calling_instance) if calling_instance
|
|
159
|
+
|
|
160
|
+
args.push("--calling-instance-id", calling_instance_id) if calling_instance_id
|
|
161
|
+
|
|
162
|
+
args.push("--instance-id", @instance_ids[name]) if @instance_ids[name]
|
|
163
|
+
|
|
164
|
+
args.push("--vibe") if @vibe || instance[:vibe]
|
|
165
|
+
|
|
166
|
+
# Add provider-specific parameters
|
|
167
|
+
if instance[:provider]
|
|
168
|
+
args.push("--provider", instance[:provider])
|
|
169
|
+
|
|
170
|
+
# Add OpenAI-specific parameters
|
|
171
|
+
if instance[:provider] == "openai"
|
|
172
|
+
args.push("--reasoning-effort", instance[:reasoning_effort]) if instance[:reasoning_effort]
|
|
173
|
+
args.push("--temperature", instance[:temperature].to_s) if instance[:temperature]
|
|
174
|
+
args.push("--api-version", instance[:api_version]) if instance[:api_version]
|
|
175
|
+
args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
|
|
176
|
+
args.push("--base-url", instance[:base_url]) if instance[:base_url]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Add claude session ID if restoring
|
|
181
|
+
if @restore_states[name.to_s]
|
|
182
|
+
claude_session_id = @restore_states[name.to_s]["claude_session_id"]
|
|
183
|
+
args.push("--claude-session-id", claude_session_id) if claude_session_id
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Capture environment variables needed for Ruby and Bundler to work properly
|
|
187
|
+
# This includes both BUNDLE_* variables and Ruby-specific variables
|
|
188
|
+
required_env = {}
|
|
189
|
+
|
|
190
|
+
# Bundle-specific variables
|
|
191
|
+
ENV.each do |k, v|
|
|
192
|
+
required_env[k] = v if k.start_with?("BUNDLE_")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Claude Swarm-specific variables
|
|
196
|
+
ENV.each do |k, v|
|
|
197
|
+
required_env[k] = v if k.start_with?("CLAUDE_SWARM_")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Ruby-specific variables that MCP servers need
|
|
201
|
+
[
|
|
202
|
+
"RUBY_ROOT",
|
|
203
|
+
"RUBY_ENGINE",
|
|
204
|
+
"RUBY_VERSION",
|
|
205
|
+
"GEM_ROOT",
|
|
206
|
+
"GEM_HOME",
|
|
207
|
+
"GEM_PATH",
|
|
208
|
+
"RUBYOPT",
|
|
209
|
+
"RUBYLIB",
|
|
210
|
+
"PATH",
|
|
211
|
+
].each do |key|
|
|
212
|
+
required_env[key] = ENV[key] if ENV[key]
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
config = {
|
|
216
|
+
"type" => "stdio",
|
|
217
|
+
"command" => exe_path,
|
|
218
|
+
"args" => args,
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# Add required environment variables if any exist
|
|
222
|
+
config["env"] = required_env unless required_env.empty?
|
|
223
|
+
|
|
224
|
+
config
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def load_instance_states
|
|
228
|
+
state_dir = File.join(@restore_session_path, "state")
|
|
229
|
+
return unless Dir.exist?(state_dir)
|
|
230
|
+
|
|
231
|
+
Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
|
|
232
|
+
data = JsonHandler.parse_file!(state_file)
|
|
233
|
+
instance_name = data["instance_name"]
|
|
234
|
+
instance_id = data["instance_id"]
|
|
235
|
+
|
|
236
|
+
# Check both string and symbol keys since config instances might have either
|
|
237
|
+
if instance_name && (@config.instances.key?(instance_name) || @config.instances.key?(instance_name.to_sym))
|
|
238
|
+
# Store with the same key type as in @config.instances
|
|
239
|
+
key = @config.instances.key?(instance_name) ? instance_name : instance_name.to_sym
|
|
240
|
+
@instance_ids[key] = instance_id
|
|
241
|
+
@restore_states[instance_name] = data
|
|
242
|
+
end
|
|
243
|
+
rescue StandardError
|
|
244
|
+
# Skip invalid state files
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
module OpenAI
|
|
5
|
+
class ChatCompletion
|
|
6
|
+
MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
|
|
7
|
+
|
|
8
|
+
def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
|
|
9
|
+
@openai_client = openai_client
|
|
10
|
+
@mcp_client = mcp_client
|
|
11
|
+
@available_tools = available_tools
|
|
12
|
+
@executor = executor
|
|
13
|
+
@instance_name = instance_name
|
|
14
|
+
@model = model
|
|
15
|
+
@temperature = temperature
|
|
16
|
+
@reasoning_effort = reasoning_effort
|
|
17
|
+
@conversation_messages = []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def execute(prompt, options = {})
|
|
21
|
+
# Build messages array
|
|
22
|
+
messages = build_messages(prompt, options)
|
|
23
|
+
|
|
24
|
+
# Process chat with recursive tool handling
|
|
25
|
+
result = process_chat_completion(messages)
|
|
26
|
+
|
|
27
|
+
# Update conversation state
|
|
28
|
+
@conversation_messages = messages
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reset_session
|
|
34
|
+
@conversation_messages = []
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_messages(prompt, options)
|
|
40
|
+
messages = []
|
|
41
|
+
|
|
42
|
+
# Add system prompt if provided
|
|
43
|
+
system_prompt = options[:system_prompt]
|
|
44
|
+
if system_prompt && @conversation_messages.empty?
|
|
45
|
+
messages << { role: "system", content: system_prompt }
|
|
46
|
+
elsif !@conversation_messages.empty?
|
|
47
|
+
# Use existing conversation
|
|
48
|
+
messages = @conversation_messages.dup
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Add user message
|
|
52
|
+
messages << { role: "user", content: prompt }
|
|
53
|
+
|
|
54
|
+
messages
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def process_chat_completion(messages, depth = 0)
|
|
58
|
+
# Prevent infinite recursion
|
|
59
|
+
if depth > MAX_TURNS_WITH_TOOLS
|
|
60
|
+
@executor.logger.error { "Maximum recursion depth reached in tool execution" }
|
|
61
|
+
return "Error: Maximum tool call depth exceeded"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Build parameters
|
|
65
|
+
parameters = {
|
|
66
|
+
model: @model,
|
|
67
|
+
messages: messages,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Only add temperature for non-o-series models
|
|
71
|
+
# O-series models don't support temperature parameter
|
|
72
|
+
if @temperature && !@model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
|
73
|
+
parameters[:temperature] = @temperature
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Only add reasoning_effort for o-series models
|
|
77
|
+
# reasoning_effort is only supported by o-series models: o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.
|
|
78
|
+
if @reasoning_effort && @model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
|
79
|
+
parameters[:reasoning_effort] = @reasoning_effort
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Add tools if available
|
|
83
|
+
parameters[:tools] = @mcp_client.to_openai_tools if @available_tools&.any? && @mcp_client
|
|
84
|
+
|
|
85
|
+
# Log the request parameters
|
|
86
|
+
@executor.logger.info { "Chat API Request (depth=#{depth}): #{JsonHandler.pretty_generate!(parameters)}" }
|
|
87
|
+
|
|
88
|
+
# Append to session JSON
|
|
89
|
+
append_to_session_json({
|
|
90
|
+
type: "openai_request",
|
|
91
|
+
api: "chat",
|
|
92
|
+
depth: depth,
|
|
93
|
+
parameters: parameters,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# Make the API call without streaming
|
|
97
|
+
begin
|
|
98
|
+
response = @openai_client.chat(parameters: parameters)
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
@executor.logger.error { "Chat API error: #{e.class} - #{e.message}" }
|
|
101
|
+
@executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
|
|
102
|
+
|
|
103
|
+
# Try to extract and log the response body for better debugging
|
|
104
|
+
if e.respond_to?(:response)
|
|
105
|
+
begin
|
|
106
|
+
error_body = e.response[:body]
|
|
107
|
+
@executor.logger.error { "Error response body: #{error_body}" }
|
|
108
|
+
rescue StandardError => parse_error
|
|
109
|
+
@executor.logger.error { "Could not parse error response: #{parse_error.message}" }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Log error to session JSON
|
|
114
|
+
append_to_session_json({
|
|
115
|
+
type: "openai_error",
|
|
116
|
+
api: "chat",
|
|
117
|
+
depth: depth,
|
|
118
|
+
error: {
|
|
119
|
+
class: e.class.to_s,
|
|
120
|
+
message: e.message,
|
|
121
|
+
response_body: e.respond_to?(:response) ? e.response[:body] : nil,
|
|
122
|
+
backtrace: e.backtrace.first(5),
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
return "Error calling OpenAI chat API: #{e.message}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Log the response
|
|
130
|
+
@executor.logger.info { "Chat API Response (depth=#{depth}): #{JsonHandler.pretty_generate!(response)}" }
|
|
131
|
+
|
|
132
|
+
# Append to session JSON
|
|
133
|
+
append_to_session_json({
|
|
134
|
+
type: "openai_response",
|
|
135
|
+
api: "chat",
|
|
136
|
+
depth: depth,
|
|
137
|
+
response: response,
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
# Extract the message from the response
|
|
141
|
+
message = response.dig("choices", 0, "message")
|
|
142
|
+
|
|
143
|
+
if message.nil?
|
|
144
|
+
@executor.logger.error { "No message in response: #{response.inspect}" }
|
|
145
|
+
return "Error: No response from OpenAI"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if there are tool calls
|
|
149
|
+
if message["tool_calls"]
|
|
150
|
+
# Add the assistant message with tool calls
|
|
151
|
+
messages << {
|
|
152
|
+
role: "assistant",
|
|
153
|
+
content: nil,
|
|
154
|
+
tool_calls: message["tool_calls"],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
# Execute tools and collect results
|
|
158
|
+
execute_and_append_tool_results(message["tool_calls"], messages)
|
|
159
|
+
|
|
160
|
+
# Recursively process the next response
|
|
161
|
+
process_chat_completion(messages, depth + 1)
|
|
162
|
+
else
|
|
163
|
+
# Regular text response - this is the final response
|
|
164
|
+
response_text = message["content"] || ""
|
|
165
|
+
messages << { role: "assistant", content: response_text }
|
|
166
|
+
response_text
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def execute_and_append_tool_results(tool_calls, messages)
|
|
171
|
+
# Log tool calls
|
|
172
|
+
@executor.logger.info { "Executing tool calls: #{JsonHandler.pretty_generate!(tool_calls)}" }
|
|
173
|
+
|
|
174
|
+
# Append to session JSON
|
|
175
|
+
append_to_session_json({
|
|
176
|
+
type: "tool_calls",
|
|
177
|
+
api: "chat",
|
|
178
|
+
tool_calls: tool_calls,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
# Execute tool calls in parallel threads
|
|
182
|
+
threads = tool_calls.map do |tool_call|
|
|
183
|
+
Thread.new do
|
|
184
|
+
tool_name = tool_call.dig("function", "name")
|
|
185
|
+
tool_args_str = tool_call.dig("function", "arguments")
|
|
186
|
+
|
|
187
|
+
begin
|
|
188
|
+
# Parse arguments
|
|
189
|
+
tool_args = tool_args_str.is_a?(String) ? JsonHandler.parse!(tool_args_str) : tool_args_str
|
|
190
|
+
|
|
191
|
+
# Log tool execution
|
|
192
|
+
@executor.logger.info { "Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
|
|
193
|
+
|
|
194
|
+
# Execute tool via MCP
|
|
195
|
+
result = @mcp_client.call_tool(tool_name, tool_args)
|
|
196
|
+
|
|
197
|
+
# Log result
|
|
198
|
+
@executor.logger.info { "Tool result for #{tool_name}: #{result}" }
|
|
199
|
+
|
|
200
|
+
# Append to session JSON
|
|
201
|
+
append_to_session_json({
|
|
202
|
+
type: "tool_execution",
|
|
203
|
+
tool_name: tool_name,
|
|
204
|
+
arguments: tool_args,
|
|
205
|
+
result: result.to_s,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
# Return success result
|
|
209
|
+
{
|
|
210
|
+
success: true,
|
|
211
|
+
tool_call_id: tool_call["id"],
|
|
212
|
+
role: "tool",
|
|
213
|
+
name: tool_name,
|
|
214
|
+
content: result.to_s,
|
|
215
|
+
}
|
|
216
|
+
rescue StandardError => e
|
|
217
|
+
@executor.logger.error { "Tool execution failed for #{tool_name}: #{e.message}" }
|
|
218
|
+
@executor.logger.error { e.backtrace.join("\n") }
|
|
219
|
+
|
|
220
|
+
# Append error to session JSON
|
|
221
|
+
append_to_session_json({
|
|
222
|
+
type: "tool_error",
|
|
223
|
+
tool_name: tool_name,
|
|
224
|
+
arguments: tool_args,
|
|
225
|
+
error: {
|
|
226
|
+
class: e.class.to_s,
|
|
227
|
+
message: e.message,
|
|
228
|
+
backtrace: e.backtrace.first(5),
|
|
229
|
+
},
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
# Return error result
|
|
233
|
+
{
|
|
234
|
+
success: false,
|
|
235
|
+
tool_call_id: tool_call["id"],
|
|
236
|
+
role: "tool",
|
|
237
|
+
name: tool_name,
|
|
238
|
+
content: "Error: #{e.message}",
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Collect results from all threads
|
|
245
|
+
tool_results = threads.map(&:value)
|
|
246
|
+
|
|
247
|
+
# Add all tool results to messages
|
|
248
|
+
tool_results.each do |result|
|
|
249
|
+
messages << {
|
|
250
|
+
tool_call_id: result[:tool_call_id],
|
|
251
|
+
role: result[:role],
|
|
252
|
+
name: result[:name],
|
|
253
|
+
content: result[:content],
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def append_to_session_json(event)
|
|
259
|
+
# Delegate to the executor's log method
|
|
260
|
+
@executor.log(event) if @executor.respond_to?(:log)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|