claude_swarm 1.0.8 → 1.0.10
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 +15 -0
- data/CLAUDE.md +347 -191
- data/docs/v1/README.md +10 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +8 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +7 -1
- data/docs/v2/CHANGELOG.swarm_sdk.md +184 -9
- data/docs/v2/README.md +6 -1
- data/docs/v2/guides/complete-tutorial.md +2 -2
- data/docs/v2/guides/getting-started.md +7 -7
- data/docs/v2/guides/migrating-to-2.3.md +541 -0
- data/docs/v2/guides/snapshots.md +14 -14
- data/docs/v2/reference/architecture-flow.md +3 -3
- data/docs/v2/reference/event_payload_structures.md +1 -1
- data/docs/v2/reference/ruby-dsl.md +157 -14
- data/docs/v2/reference/yaml.md +170 -52
- data/examples/snapshot_demo.rb +2 -2
- data/lib/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -0
- data/lib/claude_swarm/configuration.rb +2 -1
- data/lib/claude_swarm/mcp_generator.rb +8 -20
- data/lib/claude_swarm/openai/chat_completion.rb +2 -1
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +11 -21
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +11 -11
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
- data/lib/swarm_cli/interactive_repl.rb +2 -2
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +1 -1
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1061
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +2 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +65 -543
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
- data/lib/swarm_sdk/swarm.rb +137 -680
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +11 -13
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +33 -3
- data/rubocop/cop/security/no_reflection_methods.rb +1 -1
- data/swarm_memory.gemspec +1 -1
- data/swarm_sdk.gemspec +4 -2
- data/team_full.yml +24 -24
- metadata +35 -11
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module ClaudeSwarm
|
|
4
4
|
class McpGenerator
|
|
5
|
+
CLAUDE_MCP_SERVER_CONFIG = {
|
|
6
|
+
"type" => "stdio",
|
|
7
|
+
"command" => "claude",
|
|
8
|
+
"args" => ["mcp", "serve"],
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
5
11
|
def initialize(configuration, vibe: false, restore_session_path: nil)
|
|
6
12
|
@config = configuration
|
|
7
13
|
@vibe = vibe
|
|
@@ -65,7 +71,7 @@ module ClaudeSwarm
|
|
|
65
71
|
end
|
|
66
72
|
|
|
67
73
|
# Add Claude tools MCP server for OpenAI instances
|
|
68
|
-
mcp_servers["claude_tools"] =
|
|
74
|
+
mcp_servers["claude_tools"] = CLAUDE_MCP_SERVER_CONFIG.dup if instance[:provider] == "openai"
|
|
69
75
|
|
|
70
76
|
config = {
|
|
71
77
|
"instance_id" => @instance_ids[name],
|
|
@@ -96,25 +102,6 @@ module ClaudeSwarm
|
|
|
96
102
|
end
|
|
97
103
|
end
|
|
98
104
|
|
|
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
105
|
def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
|
|
119
106
|
# Get the path to the claude-swarm executable
|
|
120
107
|
exe_path = "claude-swarm"
|
|
@@ -174,6 +161,7 @@ module ClaudeSwarm
|
|
|
174
161
|
args.push("--api-version", instance[:api_version]) if instance[:api_version]
|
|
175
162
|
args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
|
|
176
163
|
args.push("--base-url", instance[:base_url]) if instance[:base_url]
|
|
164
|
+
args.push("--zdr", instance[:zdr].to_s) if instance.key?(:zdr)
|
|
177
165
|
end
|
|
178
166
|
end
|
|
179
167
|
|
|
@@ -5,7 +5,7 @@ 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:, executor:, 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, zdr: false)
|
|
9
9
|
@openai_client = openai_client
|
|
10
10
|
@mcp_client = mcp_client
|
|
11
11
|
@available_tools = available_tools
|
|
@@ -14,6 +14,7 @@ module ClaudeSwarm
|
|
|
14
14
|
@model = model
|
|
15
15
|
@temperature = temperature
|
|
16
16
|
@reasoning_effort = reasoning_effort
|
|
17
|
+
@zdr = zdr # Not used in chat_completion API, but kept for compatibility
|
|
17
18
|
@conversation_messages = []
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -31,7 +31,7 @@ module ClaudeSwarm
|
|
|
31
31
|
instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
|
|
32
32
|
claude_session_id: nil, additional_directories: [], debug: false,
|
|
33
33
|
temperature: nil, api_version: "chat_completion", openai_token_env: "OPENAI_API_KEY",
|
|
34
|
-
base_url: nil, reasoning_effort: nil)
|
|
34
|
+
base_url: nil, reasoning_effort: nil, zdr: false)
|
|
35
35
|
# Call parent initializer for common attributes
|
|
36
36
|
super(
|
|
37
37
|
working_directory: working_directory,
|
|
@@ -52,6 +52,7 @@ module ClaudeSwarm
|
|
|
52
52
|
@api_version = api_version
|
|
53
53
|
@base_url = base_url
|
|
54
54
|
@reasoning_effort = reasoning_effort
|
|
55
|
+
@zdr = zdr
|
|
55
56
|
|
|
56
57
|
# Conversation state for maintaining context
|
|
57
58
|
@conversation_messages = []
|
|
@@ -162,6 +163,7 @@ module ClaudeSwarm
|
|
|
162
163
|
model: @model,
|
|
163
164
|
temperature: @temperature,
|
|
164
165
|
reasoning_effort: @reasoning_effort,
|
|
166
|
+
zdr: @zdr,
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
if @api_version == "responses"
|
|
@@ -5,7 +5,7 @@ module ClaudeSwarm
|
|
|
5
5
|
class Responses
|
|
6
6
|
MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
|
|
7
7
|
|
|
8
|
-
def initialize(openai_client:, mcp_client:, available_tools:, executor:, 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, zdr: false)
|
|
9
9
|
@openai_client = openai_client
|
|
10
10
|
@mcp_client = mcp_client
|
|
11
11
|
@available_tools = available_tools
|
|
@@ -14,6 +14,7 @@ module ClaudeSwarm
|
|
|
14
14
|
@model = model
|
|
15
15
|
@temperature = temperature
|
|
16
16
|
@reasoning_effort = reasoning_effort
|
|
17
|
+
@zdr = zdr
|
|
17
18
|
@system_prompt = nil
|
|
18
19
|
end
|
|
19
20
|
|
|
@@ -58,6 +59,7 @@ module ClaudeSwarm
|
|
|
58
59
|
else
|
|
59
60
|
input
|
|
60
61
|
end
|
|
62
|
+
conversation_array << { role: "user", content: parameters[:input] }
|
|
61
63
|
else
|
|
62
64
|
# Follow-up call with conversation array (function calls + outputs)
|
|
63
65
|
parameters[:input] = conversation_array
|
|
@@ -70,8 +72,8 @@ module ClaudeSwarm
|
|
|
70
72
|
@executor.logger.info { "Conversation item IDs: #{conversation_ids.inspect}" }
|
|
71
73
|
end
|
|
72
74
|
|
|
73
|
-
# Add previous response ID for conversation continuity
|
|
74
|
-
parameters[:previous_response_id] =
|
|
75
|
+
# Add previous response ID for conversation continuity (unless zdr is enabled)
|
|
76
|
+
parameters[:previous_response_id] = @zdr ? nil : previous_response_id
|
|
75
77
|
|
|
76
78
|
# Add tools if available
|
|
77
79
|
if @available_tools&.any?
|
|
@@ -106,7 +108,7 @@ module ClaudeSwarm
|
|
|
106
108
|
@executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
|
|
107
109
|
|
|
108
110
|
# Try to extract and log the response body for better debugging
|
|
109
|
-
if e.respond_to?(:response)
|
|
111
|
+
if e.respond_to?(:response) && e.response
|
|
110
112
|
begin
|
|
111
113
|
error_body = e.response[:body]
|
|
112
114
|
@executor.logger.error { "Error response body: #{error_body}" }
|
|
@@ -122,7 +124,7 @@ module ClaudeSwarm
|
|
|
122
124
|
error: {
|
|
123
125
|
class: e.class.to_s,
|
|
124
126
|
message: e.message,
|
|
125
|
-
response_body: e.respond_to?(:response) ? e.response[:body] : nil,
|
|
127
|
+
response_body: e.respond_to?(:response) && e.response ? e.response[:body] : nil,
|
|
126
128
|
backtrace: e.backtrace.first(5),
|
|
127
129
|
},
|
|
128
130
|
})
|
|
@@ -146,33 +148,21 @@ module ClaudeSwarm
|
|
|
146
148
|
|
|
147
149
|
# Handle response based on output structure
|
|
148
150
|
output = response["output"]
|
|
149
|
-
|
|
150
151
|
if output.nil?
|
|
151
152
|
@executor.logger.error { "No output in response" }
|
|
152
153
|
return "Error: No output in OpenAI response"
|
|
153
154
|
end
|
|
154
155
|
|
|
155
156
|
# Check if output is an array (as per documentation)
|
|
156
|
-
if output.is_a?(Array) &&
|
|
157
|
+
if output.is_a?(Array) && output.any?
|
|
158
|
+
new_conversation = conversation_array.dup
|
|
159
|
+
new_conversation.concat(output)
|
|
157
160
|
# Check if there are function calls
|
|
158
161
|
function_calls = output.select { |item| item["type"] == "function_call" }
|
|
159
|
-
|
|
160
162
|
if function_calls.any?
|
|
161
|
-
|
|
162
|
-
if conversation_array.empty?
|
|
163
|
-
# First depth - build new conversation
|
|
164
|
-
new_conversation = build_conversation_with_outputs(function_calls)
|
|
165
|
-
else
|
|
166
|
-
# Subsequent depth - append to existing conversation
|
|
167
|
-
# Don't re-add function calls, just add the new ones and their outputs
|
|
168
|
-
new_conversation = conversation_array.dup
|
|
169
|
-
append_new_outputs(function_calls, new_conversation)
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
# Recursively process with updated conversation
|
|
163
|
+
append_new_outputs(function_calls, new_conversation)
|
|
173
164
|
process_responses_api(nil, new_conversation, response_id, depth + 1)
|
|
174
165
|
else
|
|
175
|
-
# Look for text response
|
|
176
166
|
extract_text_response(output)
|
|
177
167
|
end
|
|
178
168
|
else
|
data/lib/claude_swarm/version.rb
CHANGED
|
@@ -134,8 +134,8 @@ module SwarmCLI
|
|
|
134
134
|
|
|
135
135
|
def emit_validation_warnings(swarm, formatter)
|
|
136
136
|
# Setup temporary logging to capture and emit warnings
|
|
137
|
-
SwarmSDK::LogCollector.
|
|
138
|
-
formatter.on_log(log_entry)
|
|
137
|
+
SwarmSDK::LogCollector.subscribe(filter: { type: "model_lookup_warning" }) do |log_entry|
|
|
138
|
+
formatter.on_log(log_entry)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
SwarmSDK::LogStream.emitter = SwarmSDK::LogCollector
|
|
@@ -5,7 +5,7 @@ module SwarmCLI
|
|
|
5
5
|
#
|
|
6
6
|
# Supports:
|
|
7
7
|
# - YAML files (.yml, .yaml) - loaded via SwarmSDK.load_file
|
|
8
|
-
# - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::
|
|
8
|
+
# - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::Workflow instance
|
|
9
9
|
#
|
|
10
10
|
# @example Load YAML config
|
|
11
11
|
# swarm = ConfigLoader.load("config.yml")
|
|
@@ -19,10 +19,10 @@ module SwarmCLI
|
|
|
19
19
|
#
|
|
20
20
|
# Detects file type by extension:
|
|
21
21
|
# - .yml, .yaml -> Load as YAML using SwarmSDK.load_file
|
|
22
|
-
# - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::
|
|
22
|
+
# - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::Workflow instance
|
|
23
23
|
#
|
|
24
24
|
# @param path [String, Pathname] Path to configuration file
|
|
25
|
-
# @return [SwarmSDK::Swarm, SwarmSDK::
|
|
25
|
+
# @return [SwarmSDK::Swarm, SwarmSDK::Workflow] Configured swarm or workflow instance
|
|
26
26
|
# @raise [SwarmCLI::ConfigurationError] If file not found or invalid format
|
|
27
27
|
def load(path)
|
|
28
28
|
path = Pathname.new(path).expand_path
|
|
@@ -59,27 +59,27 @@ module SwarmCLI
|
|
|
59
59
|
# Load Ruby DSL configuration file
|
|
60
60
|
#
|
|
61
61
|
# Executes the Ruby file in a clean binding and expects it to return
|
|
62
|
-
# a SwarmSDK::Swarm or SwarmSDK::
|
|
63
|
-
# use SwarmSDK.build or create a Swarm/
|
|
62
|
+
# a SwarmSDK::Swarm or SwarmSDK::Workflow instance. The file should
|
|
63
|
+
# use SwarmSDK.build or SwarmSDK.workflow or create a Swarm/Workflow instance directly.
|
|
64
64
|
#
|
|
65
65
|
# @param path [Pathname] Path to Ruby DSL file
|
|
66
|
-
# @return [SwarmSDK::Swarm, SwarmSDK::
|
|
66
|
+
# @return [SwarmSDK::Swarm, SwarmSDK::Workflow] Configured swarm or workflow instance
|
|
67
67
|
# @raise [ConfigurationError] If file doesn't return a valid instance
|
|
68
68
|
def load_ruby_dsl(path)
|
|
69
69
|
# Read the file content
|
|
70
70
|
content = path.read
|
|
71
71
|
|
|
72
72
|
# Execute in a clean binding with SwarmSDK available
|
|
73
|
-
# This allows the DSL file to use SwarmSDK.build directly
|
|
73
|
+
# This allows the DSL file to use SwarmSDK.build or SwarmSDK.workflow directly
|
|
74
74
|
result = eval(content, binding, path.to_s, 1) # rubocop:disable Security/Eval
|
|
75
75
|
|
|
76
|
-
# Validate result is a Swarm or
|
|
76
|
+
# Validate result is a Swarm or Workflow instance
|
|
77
77
|
# Both have the same execute(prompt) interface
|
|
78
|
-
unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::
|
|
78
|
+
unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::Workflow)
|
|
79
79
|
raise ConfigurationError,
|
|
80
|
-
"Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::
|
|
80
|
+
"Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::Workflow instance. " \
|
|
81
81
|
"Got: #{result.class}. " \
|
|
82
|
-
"Use: SwarmSDK.build { ... } or
|
|
82
|
+
"Use: SwarmSDK.build { ... } or SwarmSDK.workflow { ... }"
|
|
83
83
|
end
|
|
84
84
|
|
|
85
85
|
result
|
|
@@ -99,8 +99,6 @@ module SwarmCLI
|
|
|
99
99
|
handle_llm_retry_attempt(entry)
|
|
100
100
|
when "llm_retry_exhausted"
|
|
101
101
|
handle_llm_retry_exhausted(entry)
|
|
102
|
-
when "response_parse_error"
|
|
103
|
-
handle_response_parse_error(entry)
|
|
104
102
|
end
|
|
105
103
|
end
|
|
106
104
|
|
|
@@ -647,37 +645,6 @@ module SwarmCLI
|
|
|
647
645
|
)
|
|
648
646
|
end
|
|
649
647
|
|
|
650
|
-
def handle_response_parse_error(entry)
|
|
651
|
-
agent = entry[:agent]
|
|
652
|
-
error_class = entry[:error_class]
|
|
653
|
-
error_message = entry[:error_message]
|
|
654
|
-
|
|
655
|
-
# Stop agent thinking spinner (if active)
|
|
656
|
-
unless @quiet
|
|
657
|
-
spinner_key = "agent_#{agent}".to_sym
|
|
658
|
-
@spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
|
|
659
|
-
end
|
|
660
|
-
|
|
661
|
-
lines = [
|
|
662
|
-
@pastel.red("Failed to parse LLM API response"),
|
|
663
|
-
@pastel.dim("Error: #{error_class}: #{error_message}"),
|
|
664
|
-
]
|
|
665
|
-
|
|
666
|
-
# Add response body preview if available (truncated)
|
|
667
|
-
if entry[:response_body]
|
|
668
|
-
body_preview = entry[:response_body].to_s[0..200]
|
|
669
|
-
body_preview += "..." if entry[:response_body].to_s.length > 200
|
|
670
|
-
lines << @pastel.dim("Response: #{body_preview}")
|
|
671
|
-
end
|
|
672
|
-
|
|
673
|
-
@output.puts @panel.render(
|
|
674
|
-
type: :error,
|
|
675
|
-
title: "PARSE ERROR #{@agent_badge.render(agent)}",
|
|
676
|
-
lines: lines,
|
|
677
|
-
indent: @depth_tracker.get(agent),
|
|
678
|
-
)
|
|
679
|
-
end
|
|
680
|
-
|
|
681
648
|
def display_todo_list(agent, timestamp)
|
|
682
649
|
todos = SwarmSDK::Tools::Stores::TodoManager.get_todos(agent.to_sym)
|
|
683
650
|
indent = @depth_tracker.indent(agent)
|
|
@@ -534,7 +534,7 @@ module SwarmCLI
|
|
|
534
534
|
lead = @swarm.agent(@swarm.lead_agent)
|
|
535
535
|
|
|
536
536
|
# Clear the agent's conversation history
|
|
537
|
-
lead.
|
|
537
|
+
lead.replace_messages([])
|
|
538
538
|
|
|
539
539
|
# Clear REPL conversation history
|
|
540
540
|
@conversation_history.clear
|
|
@@ -575,7 +575,7 @@ module SwarmCLI
|
|
|
575
575
|
case tool_name
|
|
576
576
|
when /^Memory/, "LoadSkill"
|
|
577
577
|
memory_tools << tool_name
|
|
578
|
-
when /^
|
|
578
|
+
when /^WorkWith/
|
|
579
579
|
delegation_tools << tool_name
|
|
580
580
|
when /^mcp__/
|
|
581
581
|
mcp_tools << tool_name
|
data/lib/swarm_cli/ui/icons.rb
CHANGED
|
@@ -31,29 +31,6 @@ module SwarmCLI
|
|
|
31
31
|
ARROW_RIGHT = "→"
|
|
32
32
|
BULLET = "•"
|
|
33
33
|
COMPRESS = "🗜️"
|
|
34
|
-
|
|
35
|
-
# All icons as hash for backward compatibility
|
|
36
|
-
ALL = {
|
|
37
|
-
thinking: THINKING,
|
|
38
|
-
response: RESPONSE,
|
|
39
|
-
success: SUCCESS,
|
|
40
|
-
error: ERROR,
|
|
41
|
-
info: INFO,
|
|
42
|
-
warning: WARNING,
|
|
43
|
-
agent: AGENT,
|
|
44
|
-
tool: TOOL,
|
|
45
|
-
delegate: DELEGATE,
|
|
46
|
-
result: RESULT,
|
|
47
|
-
hook: HOOK,
|
|
48
|
-
llm: LLM,
|
|
49
|
-
tokens: TOKENS,
|
|
50
|
-
cost: COST,
|
|
51
|
-
time: TIME,
|
|
52
|
-
sparkles: SPARKLES,
|
|
53
|
-
arrow_right: ARROW_RIGHT,
|
|
54
|
-
bullet: BULLET,
|
|
55
|
-
compress: COMPRESS,
|
|
56
|
-
}.freeze
|
|
57
34
|
end
|
|
58
35
|
end
|
|
59
36
|
end
|
data/lib/swarm_cli/version.rb
CHANGED
|
@@ -93,10 +93,10 @@ module SwarmMemory
|
|
|
93
93
|
"Clear old entries or use smaller content."
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
-
# Strip .md extension
|
|
97
|
-
# "concepts/ruby/classes.md" → "concepts
|
|
96
|
+
# Strip .md extension for disk storage
|
|
97
|
+
# "concepts/ruby/classes.md" → "concepts/ruby/classes"
|
|
98
98
|
base_path = file_path.sub(/\.md\z/, "")
|
|
99
|
-
disk_path =
|
|
99
|
+
disk_path = base_path
|
|
100
100
|
|
|
101
101
|
# 1. Write content to .md file (stored exactly as provided)
|
|
102
102
|
md_file = File.join(@directory, "#{disk_path}.md")
|
|
@@ -162,9 +162,9 @@ module SwarmMemory
|
|
|
162
162
|
return entry.content
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
-
# Strip .md extension
|
|
165
|
+
# Strip .md extension
|
|
166
166
|
base_path = file_path.sub(/\.md\z/, "")
|
|
167
|
-
disk_path =
|
|
167
|
+
disk_path = base_path
|
|
168
168
|
md_file = File.join(@directory, "#{disk_path}.md")
|
|
169
169
|
|
|
170
170
|
raise ArgumentError, "memory://#{file_path} not found" unless File.exist?(md_file)
|
|
@@ -189,9 +189,9 @@ module SwarmMemory
|
|
|
189
189
|
return load_virtual_entry(file_path)
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
-
# Strip .md extension
|
|
192
|
+
# Strip .md extension
|
|
193
193
|
base_path = file_path.sub(/\.md\z/, "")
|
|
194
|
-
disk_path =
|
|
194
|
+
disk_path = base_path
|
|
195
195
|
md_file = File.join(@directory, "#{disk_path}.md")
|
|
196
196
|
yaml_file = File.join(@directory, "#{disk_path}.yml")
|
|
197
197
|
|
|
@@ -230,9 +230,9 @@ module SwarmMemory
|
|
|
230
230
|
@semaphore.acquire do
|
|
231
231
|
raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
|
|
232
232
|
|
|
233
|
-
# Strip .md extension
|
|
233
|
+
# Strip .md extension
|
|
234
234
|
base_path = file_path.sub(/\.md\z/, "")
|
|
235
|
-
disk_path =
|
|
235
|
+
disk_path = base_path
|
|
236
236
|
md_file = File.join(@directory, "#{disk_path}.md")
|
|
237
237
|
|
|
238
238
|
raise ArgumentError, "memory://#{file_path} not found" unless File.exist?(md_file)
|
|
@@ -500,29 +500,6 @@ module SwarmMemory
|
|
|
500
500
|
)
|
|
501
501
|
end
|
|
502
502
|
|
|
503
|
-
# Flatten path for disk storage
|
|
504
|
-
# "concepts/ruby/classes" → "concepts--ruby--classes"
|
|
505
|
-
#
|
|
506
|
-
# @param logical_path [String] Logical path with slashes
|
|
507
|
-
# @return [String] Flattened path with --
|
|
508
|
-
# Identity function - paths are now stored hierarchically
|
|
509
|
-
# Kept for backward compatibility during transition
|
|
510
|
-
#
|
|
511
|
-
# @param logical_path [String] Logical path
|
|
512
|
-
# @return [String] Same path (no flattening)
|
|
513
|
-
def flatten_path(logical_path)
|
|
514
|
-
logical_path
|
|
515
|
-
end
|
|
516
|
-
|
|
517
|
-
# Identity function - paths are now stored hierarchically
|
|
518
|
-
# Kept for backward compatibility during transition
|
|
519
|
-
#
|
|
520
|
-
# @param disk_path [String] Disk path
|
|
521
|
-
# @return [String] Same path (no unflattening)
|
|
522
|
-
def unflatten_path(disk_path)
|
|
523
|
-
disk_path
|
|
524
|
-
end
|
|
525
|
-
|
|
526
503
|
# Check if content is a stub (redirect)
|
|
527
504
|
#
|
|
528
505
|
# @param content [String] File content
|
|
@@ -566,7 +543,7 @@ module SwarmMemory
|
|
|
566
543
|
# @return [void]
|
|
567
544
|
def increment_hits(file_path)
|
|
568
545
|
base_path = file_path.sub(/\.md\z/, "")
|
|
569
|
-
disk_path =
|
|
546
|
+
disk_path = base_path
|
|
570
547
|
yaml_file = File.join(@directory, "#{disk_path}.yml")
|
|
571
548
|
return unless File.exist?(yaml_file)
|
|
572
549
|
|
|
@@ -587,7 +564,7 @@ module SwarmMemory
|
|
|
587
564
|
# @return [Integer] Size in bytes
|
|
588
565
|
def get_entry_size(file_path)
|
|
589
566
|
base_path = file_path.sub(/\.md\z/, "")
|
|
590
|
-
disk_path =
|
|
567
|
+
disk_path = base_path
|
|
591
568
|
yaml_file = File.join(@directory, "#{disk_path}.yml")
|
|
592
569
|
|
|
593
570
|
if File.exist?(yaml_file)
|
|
@@ -156,7 +156,7 @@ module SwarmMemory
|
|
|
156
156
|
# @return [String] Memory prompt contribution
|
|
157
157
|
def system_prompt_contribution(agent_definition:, storage:)
|
|
158
158
|
# Extract mode from memory config
|
|
159
|
-
memory_config = agent_definition.memory
|
|
159
|
+
memory_config = agent_definition.plugin_config(:memory)
|
|
160
160
|
mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
|
|
161
161
|
memory_config.mode # MemoryConfig object from DSL
|
|
162
162
|
elsif memory_config.respond_to?(:mode)
|
|
@@ -204,20 +204,100 @@ module SwarmMemory
|
|
|
204
204
|
# @param agent_definition [Agent::Definition] Agent definition
|
|
205
205
|
# @return [Boolean] True if agent has memory configuration
|
|
206
206
|
def storage_enabled?(agent_definition)
|
|
207
|
-
agent_definition.
|
|
207
|
+
memory_config = agent_definition.plugin_config(:memory)
|
|
208
|
+
return false if memory_config.nil?
|
|
209
|
+
|
|
210
|
+
# MemoryConfig object (from DSL)
|
|
211
|
+
return memory_config.enabled? if memory_config.respond_to?(:enabled?)
|
|
212
|
+
|
|
213
|
+
# Hash (from YAML) - check for directory key
|
|
214
|
+
if memory_config.is_a?(Hash)
|
|
215
|
+
directory = memory_config[:directory] || memory_config["directory"]
|
|
216
|
+
return !directory.nil? && !directory.to_s.strip.empty?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
false
|
|
208
220
|
end
|
|
209
221
|
|
|
210
222
|
# Contribute to agent serialization
|
|
211
223
|
#
|
|
212
|
-
# Preserves memory configuration when agents are cloned (e.g., in
|
|
224
|
+
# Preserves memory configuration when agents are cloned (e.g., in Workflow).
|
|
213
225
|
# This allows memory configuration to persist across node transitions.
|
|
214
226
|
#
|
|
215
227
|
# @param agent_definition [Agent::Definition] Agent definition
|
|
216
228
|
# @return [Hash] Memory config to include in to_h
|
|
217
229
|
def serialize_config(agent_definition:)
|
|
218
|
-
|
|
230
|
+
memory_config = agent_definition.plugin_config(:memory)
|
|
231
|
+
return {} unless memory_config
|
|
232
|
+
|
|
233
|
+
{ memory: memory_config }
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Snapshot plugin-specific state for an agent
|
|
237
|
+
#
|
|
238
|
+
# Captures memory read tracking state for session persistence.
|
|
239
|
+
# This allows agents to remember which memory entries they've read
|
|
240
|
+
# across sessions.
|
|
241
|
+
#
|
|
242
|
+
# @param agent_name [Symbol] Agent identifier
|
|
243
|
+
# @return [Hash] Plugin-specific state
|
|
244
|
+
def snapshot_agent_state(agent_name)
|
|
245
|
+
entries_with_digests = Core::StorageReadTracker.get_read_entries(agent_name)
|
|
246
|
+
return {} if entries_with_digests.empty?
|
|
247
|
+
|
|
248
|
+
{ read_entries: entries_with_digests }
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Restore plugin-specific state for an agent
|
|
252
|
+
#
|
|
253
|
+
# Restores memory read tracking state from snapshot.
|
|
254
|
+
# This is idempotent - calling multiple times with same state
|
|
255
|
+
# produces the same result.
|
|
256
|
+
#
|
|
257
|
+
# @param agent_name [Symbol] Agent identifier
|
|
258
|
+
# @param state [Hash] Previously snapshotted state (with symbol keys)
|
|
259
|
+
# @return [void]
|
|
260
|
+
def restore_agent_state(agent_name, state)
|
|
261
|
+
entries = state[:read_entries] || state["read_entries"]
|
|
262
|
+
return unless entries
|
|
263
|
+
|
|
264
|
+
Core::StorageReadTracker.restore_read_entries(agent_name, entries)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Get digest for a memory tool result
|
|
268
|
+
#
|
|
269
|
+
# Returns the digest for a MemoryRead tool call, enabling change detection
|
|
270
|
+
# hooks to know if a memory entry has been modified since last read.
|
|
271
|
+
#
|
|
272
|
+
# @param agent_name [Symbol] Agent identifier
|
|
273
|
+
# @param tool_name [String] Name of the tool
|
|
274
|
+
# @param path [String] Path of the memory entry
|
|
275
|
+
# @return [String, nil] Digest string or nil if not a memory tool
|
|
276
|
+
def get_tool_result_digest(agent_name:, tool_name:, path:)
|
|
277
|
+
return unless tool_name == "MemoryRead"
|
|
219
278
|
|
|
220
|
-
|
|
279
|
+
Core::StorageReadTracker.get_read_entries(agent_name)[path]
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Translate YAML configuration into DSL calls
|
|
283
|
+
#
|
|
284
|
+
# Called during YAML-to-DSL translation. Handles memory-specific YAML
|
|
285
|
+
# configuration and translates it into DSL method calls on the builder.
|
|
286
|
+
#
|
|
287
|
+
# @param builder [Agent::Builder] Builder instance (self in DSL context)
|
|
288
|
+
# @param agent_config [Hash] Full agent config from YAML
|
|
289
|
+
# @return [void]
|
|
290
|
+
def translate_yaml_config(builder, agent_config)
|
|
291
|
+
memory_config = agent_config[:memory]
|
|
292
|
+
return unless memory_config
|
|
293
|
+
|
|
294
|
+
builder.instance_eval do
|
|
295
|
+
memory do
|
|
296
|
+
directory(memory_config[:directory]) if memory_config[:directory]
|
|
297
|
+
adapter(memory_config[:adapter]) if memory_config[:adapter]
|
|
298
|
+
mode(memory_config[:mode]) if memory_config[:mode]
|
|
299
|
+
end
|
|
300
|
+
end
|
|
221
301
|
end
|
|
222
302
|
|
|
223
303
|
# Lifecycle: Agent initialized
|
|
@@ -239,7 +319,7 @@ module SwarmMemory
|
|
|
239
319
|
return unless storage # Only proceed if memory is enabled for this agent
|
|
240
320
|
|
|
241
321
|
# Extract mode from memory config
|
|
242
|
-
memory_config = agent_definition.memory
|
|
322
|
+
memory_config = agent_definition.plugin_config(:memory)
|
|
243
323
|
mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
|
|
244
324
|
memory_config.mode # MemoryConfig object from DSL
|
|
245
325
|
elsif memory_config.respond_to?(:mode)
|
|
@@ -281,7 +361,7 @@ module SwarmMemory
|
|
|
281
361
|
agent_definition: agent_definition,
|
|
282
362
|
)
|
|
283
363
|
|
|
284
|
-
agent.
|
|
364
|
+
agent.add_tool(load_skill_tool)
|
|
285
365
|
end
|
|
286
366
|
|
|
287
367
|
# Mark mode-specific memory tools + LoadSkill as immutable
|
data/lib/swarm_memory/version.rb
CHANGED
data/lib/swarm_memory.rb
CHANGED
|
@@ -42,7 +42,7 @@ loader.setup
|
|
|
42
42
|
# These must be loaded after Zeitwerk but before anything uses them
|
|
43
43
|
require_relative "swarm_memory/dsl/memory_config"
|
|
44
44
|
require_relative "swarm_memory/dsl/builder_extension"
|
|
45
|
-
|
|
45
|
+
# NOTE: ChatExtension was removed in favor of SDK's built-in remove_tool method
|
|
46
46
|
|
|
47
47
|
module SwarmMemory
|
|
48
48
|
class << self
|