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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9cc16f06662bd282f8c296aeb6600f53dd80d14f27afad26b9da6f724916d20a
|
|
4
|
+
data.tar.gz: 6acf6425bdc544f125826ac5ba1f6b127f9c3d96e7ba067712cd15f165ac5af5
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91b645763ed45723d82080fb2321ca5e9c0d61310c3f3907843c9086980f5b27e0176315b2ac399f837191c5559b6e36563e2f71bb18adaf2a93a957f186a2e9
|
|
7
|
+
data.tar.gz: 9b019e85c71c3f7c9f1c1ac5358c53566b5c5934fb1e983087e9c6ebdd81c79b1e32c100d762161bda4aff2227c2be24bfa3d9f0784edc7cbd227a9a21d724af
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Paulo Arruda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -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
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
class ClaudeCodeExecutor < BaseExecutor
|
|
5
|
+
def execute(prompt, options = {})
|
|
6
|
+
# Log the request
|
|
7
|
+
log_request(prompt)
|
|
8
|
+
|
|
9
|
+
# Build SDK options
|
|
10
|
+
sdk_options = build_sdk_options(prompt, options)
|
|
11
|
+
|
|
12
|
+
# Variables to collect output
|
|
13
|
+
all_messages = []
|
|
14
|
+
result_response = nil
|
|
15
|
+
|
|
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
|
|
22
|
+
|
|
23
|
+
# Log streaming event BEFORE we modify anything
|
|
24
|
+
log_streaming_event(message_hash)
|
|
25
|
+
|
|
26
|
+
# Process specific message types
|
|
27
|
+
case message
|
|
28
|
+
when ClaudeSDK::Messages::System
|
|
29
|
+
# Capture session_id from system init
|
|
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."
|
|
46
|
+
end
|
|
47
|
+
|
|
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
|
+
}
|
|
59
|
+
end
|
|
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}"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Ensure we got a result
|
|
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
|
|
74
|
+
|
|
75
|
+
result_response
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
|
|
78
|
+
logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
|
|
79
|
+
raise
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def write_instance_state
|
|
85
|
+
return unless @instance_id && @session_id
|
|
86
|
+
|
|
87
|
+
state_dir = File.join(@session_path, "state")
|
|
88
|
+
FileUtils.mkdir_p(state_dir)
|
|
89
|
+
|
|
90
|
+
state_file = File.join(state_dir, "#{@instance_id}.json")
|
|
91
|
+
state_data = {
|
|
92
|
+
instance_name: @instance_name,
|
|
93
|
+
instance_id: @instance_id,
|
|
94
|
+
claude_session_id: @session_id,
|
|
95
|
+
status: "active",
|
|
96
|
+
updated_at: Time.now.iso8601,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
JsonHandler.write_file!(state_file, state_data)
|
|
100
|
+
logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def log_streaming_event(event)
|
|
106
|
+
append_to_session_json(event)
|
|
107
|
+
|
|
108
|
+
return log_system_message(event) if event["type"] == "system"
|
|
109
|
+
|
|
110
|
+
# Add specific details based on event type
|
|
111
|
+
case event["type"]
|
|
112
|
+
when "assistant"
|
|
113
|
+
log_assistant_message(event["message"])
|
|
114
|
+
when "user"
|
|
115
|
+
log_user_message(event["message"]["content"])
|
|
116
|
+
when "result"
|
|
117
|
+
log_response(event)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def log_system_message(event)
|
|
122
|
+
logger.debug { "SYSTEM: #{JsonHandler.pretty_generate!(event)}" }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def log_assistant_message(msg)
|
|
126
|
+
# Assistant messages don't have stop_reason in SDK - they only have content
|
|
127
|
+
content = msg["content"]
|
|
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" } || []
|
|
132
|
+
tool_calls.each do |tool_call|
|
|
133
|
+
arguments = tool_call["input"].to_json
|
|
134
|
+
arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
|
|
135
|
+
|
|
136
|
+
logger.info do
|
|
137
|
+
"Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Log thinking text
|
|
142
|
+
text = content&.select { |c| c["type"] == "text" } || []
|
|
143
|
+
text.each do |t|
|
|
144
|
+
logger.info { "#{instance_info} is thinking:\n---\n#{t["text"]}\n---" }
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def log_user_message(content)
|
|
149
|
+
logger.debug { "USER: #{JsonHandler.pretty_generate!(content)}" }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def build_sdk_options(prompt, options)
|
|
153
|
+
# Map CLI options to SDK options
|
|
154
|
+
sdk_options = ClaudeSDK::ClaudeCodeOptions.new
|
|
155
|
+
|
|
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]
|
|
161
|
+
|
|
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 : []
|
|
168
|
+
|
|
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
|
|
173
|
+
|
|
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
|
|
178
|
+
|
|
179
|
+
# System prompt
|
|
180
|
+
sdk_options.append_system_prompt = options[:system_prompt] if options[:system_prompt]
|
|
181
|
+
|
|
182
|
+
# MCP configuration
|
|
183
|
+
if @mcp_config
|
|
184
|
+
sdk_options.mcp_servers = parse_mcp_config(@mcp_config)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Handle additional directories by adding them to MCP servers
|
|
188
|
+
if @additional_directories.any?
|
|
189
|
+
setup_additional_directories_mcp(sdk_options)
|
|
190
|
+
end
|
|
191
|
+
|
|
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)
|
|
195
|
+
|
|
196
|
+
sdk_options
|
|
197
|
+
end
|
|
198
|
+
|
|
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
|
|
229
|
+
|
|
230
|
+
mcp_servers.compact
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
logger.error { "Failed to parse MCP config: #{e.message}" }
|
|
233
|
+
{}
|
|
234
|
+
end
|
|
235
|
+
|
|
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 ||= {}
|
|
239
|
+
|
|
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
|
+
}
|
|
257
|
+
|
|
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
|
|
266
|
+
end
|
|
267
|
+
|
|
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 }
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
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
|
|
348
|
+
end
|
|
349
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeSwarm
|
|
4
|
+
class ClaudeMcpServer
|
|
5
|
+
# Class variables to share state with tool classes
|
|
6
|
+
class << self
|
|
7
|
+
attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance, :calling_instance_id
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(instance_config, calling_instance:, calling_instance_id: nil, debug: false)
|
|
11
|
+
@instance_config = instance_config
|
|
12
|
+
@calling_instance = calling_instance
|
|
13
|
+
@calling_instance_id = calling_instance_id
|
|
14
|
+
|
|
15
|
+
# Create appropriate executor based on provider
|
|
16
|
+
common_params = {
|
|
17
|
+
working_directory: instance_config[:directory],
|
|
18
|
+
model: instance_config[:model],
|
|
19
|
+
mcp_config: instance_config[:mcp_config_path],
|
|
20
|
+
vibe: instance_config[:vibe],
|
|
21
|
+
instance_name: instance_config[:name],
|
|
22
|
+
instance_id: instance_config[:instance_id],
|
|
23
|
+
calling_instance: calling_instance,
|
|
24
|
+
calling_instance_id: calling_instance_id,
|
|
25
|
+
claude_session_id: instance_config[:claude_session_id],
|
|
26
|
+
additional_directories: instance_config[:directories][1..] || [],
|
|
27
|
+
debug: debug,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@executor = if instance_config[:provider] == "openai"
|
|
31
|
+
OpenAI::Executor.new(
|
|
32
|
+
**common_params,
|
|
33
|
+
# OpenAI-specific parameters
|
|
34
|
+
temperature: instance_config[:temperature],
|
|
35
|
+
api_version: instance_config[:api_version],
|
|
36
|
+
openai_token_env: instance_config[:openai_token_env],
|
|
37
|
+
base_url: instance_config[:base_url],
|
|
38
|
+
reasoning_effort: instance_config[:reasoning_effort],
|
|
39
|
+
)
|
|
40
|
+
else
|
|
41
|
+
# Default Claude behavior - always use SDK
|
|
42
|
+
ClaudeCodeExecutor.new(**common_params)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Set class variables so tools can access them
|
|
46
|
+
self.class.executor = @executor
|
|
47
|
+
self.class.instance_config = @instance_config
|
|
48
|
+
self.class.logger = @executor.logger
|
|
49
|
+
self.class.session_path = @executor.session_path
|
|
50
|
+
self.class.calling_instance = @calling_instance
|
|
51
|
+
self.class.calling_instance_id = @calling_instance_id
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def start
|
|
55
|
+
server = FastMcp::Server.new(
|
|
56
|
+
name: @instance_config[:name],
|
|
57
|
+
version: "1.0.0",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Set dynamic description for TaskTool based on instance config
|
|
61
|
+
thinking_info = " Thinking budget levels: \"think\" < \"think hard\" < \"think harder\" < \"ultrathink\"."
|
|
62
|
+
if @instance_config[:description]
|
|
63
|
+
Tools::TaskTool.description("Execute a task using Agent #{@instance_config[:name]}. #{@instance_config[:description]} #{thinking_info}")
|
|
64
|
+
else
|
|
65
|
+
Tools::TaskTool.description("Execute a task using Agent #{@instance_config[:name]}. #{thinking_info}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Register tool classes (not instances)
|
|
69
|
+
server.register_tool(Tools::TaskTool)
|
|
70
|
+
server.register_tool(Tools::SessionInfoTool)
|
|
71
|
+
server.register_tool(Tools::ResetSessionTool)
|
|
72
|
+
|
|
73
|
+
# Start the stdio server
|
|
74
|
+
server.start
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|