swarm_memory 2.1.3 → 2.1.4
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/lib/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +2 -15
- data/lib/claude_swarm/mcp_generator.rb +1 -0
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- 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 +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- 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 -1059
- 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 +111 -44
- 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 -679
- 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
- metadata +37 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebb25752a42e174408e197c5b24d94ebfbd2a57cfd5a26101e839f2375e54b8f
|
|
4
|
+
data.tar.gz: 9558c9e19cfb486872d6cee1b445506e9c599f8d4a767670743f3a3f1481fb49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a5d16f99c9aeb5fca71dfed900a8e44337d2fa5a1ee58fbec71e25f6825091973c0ef849a9c28cd88eb3bfdac9a481bbe743de2fd034fe3f124c8ea0aa23c5f1
|
|
7
|
+
data.tar.gz: b2ce4c330d45730c723a51786301d39dcc8c3b57eb19f65f6510b5b3027620d3ab214df201e8237b2b0e36f82c726b04e233cbb0266187aa7743446e1215b854
|
|
@@ -36,6 +36,7 @@ module ClaudeSwarm
|
|
|
36
36
|
openai_token_env: instance_config[:openai_token_env],
|
|
37
37
|
base_url: instance_config[:base_url],
|
|
38
38
|
reasoning_effort: instance_config[:reasoning_effort],
|
|
39
|
+
zdr: instance_config[:zdr],
|
|
39
40
|
)
|
|
40
41
|
else
|
|
41
42
|
# Default Claude behavior - always use SDK
|
data/lib/claude_swarm/cli.rb
CHANGED
|
@@ -172,6 +172,10 @@ module ClaudeSwarm
|
|
|
172
172
|
method_option :reasoning_effort,
|
|
173
173
|
type: :string,
|
|
174
174
|
desc: "Reasoning effort for OpenAI models"
|
|
175
|
+
method_option :zdr,
|
|
176
|
+
type: :boolean,
|
|
177
|
+
default: false,
|
|
178
|
+
desc: "Enable ZDR for OpenAI models"
|
|
175
179
|
def mcp_serve
|
|
176
180
|
# Validate reasoning_effort if provided
|
|
177
181
|
if options[:reasoning_effort]
|
|
@@ -181,14 +185,6 @@ module ClaudeSwarm
|
|
|
181
185
|
exit(1)
|
|
182
186
|
end
|
|
183
187
|
|
|
184
|
-
# Validate it's used with an o-series model
|
|
185
|
-
model = options[:model]
|
|
186
|
-
unless model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
|
187
|
-
error("reasoning_effort is only supported for 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.)")
|
|
188
|
-
error("Current model: #{model}")
|
|
189
|
-
exit(1)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
188
|
# Validate the value
|
|
193
189
|
unless ClaudeSwarm::Configuration::VALID_REASONING_EFFORTS.include?(options[:reasoning_effort])
|
|
194
190
|
error("reasoning_effort must be 'low', 'medium', or 'high'")
|
|
@@ -196,16 +192,6 @@ module ClaudeSwarm
|
|
|
196
192
|
end
|
|
197
193
|
end
|
|
198
194
|
|
|
199
|
-
# Validate temperature is not used with o-series models
|
|
200
|
-
if options[:temperature] && options[:provider] == "openai"
|
|
201
|
-
model = options[:model]
|
|
202
|
-
if model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
|
203
|
-
error("temperature parameter is not supported for o-series models (#{model})")
|
|
204
|
-
error("O-series models use deterministic reasoning and don't accept temperature settings")
|
|
205
|
-
exit(1)
|
|
206
|
-
end
|
|
207
|
-
end
|
|
208
|
-
|
|
209
195
|
instance_config = {
|
|
210
196
|
name: options[:name],
|
|
211
197
|
directory: options[:directory],
|
|
@@ -226,6 +212,7 @@ module ClaudeSwarm
|
|
|
226
212
|
openai_token_env: options[:openai_token_env],
|
|
227
213
|
base_url: options[:base_url],
|
|
228
214
|
reasoning_effort: options[:reasoning_effort],
|
|
215
|
+
zdr: options[:zdr],
|
|
229
216
|
}
|
|
230
217
|
|
|
231
218
|
begin
|
|
@@ -4,15 +4,13 @@ module ClaudeSwarm
|
|
|
4
4
|
class Configuration
|
|
5
5
|
# Frozen constants for validation
|
|
6
6
|
VALID_PROVIDERS = ["claude", "openai"].freeze
|
|
7
|
-
OPENAI_SPECIFIC_FIELDS = ["temperature", "api_version", "openai_token_env", "base_url", "reasoning_effort"].freeze
|
|
7
|
+
OPENAI_SPECIFIC_FIELDS = ["temperature", "api_version", "openai_token_env", "base_url", "reasoning_effort", "zdr"].freeze
|
|
8
8
|
VALID_API_VERSIONS = ["chat_completion", "responses"].freeze
|
|
9
9
|
VALID_REASONING_EFFORTS = ["low", "medium", "high"].freeze
|
|
10
10
|
|
|
11
11
|
# Regex patterns
|
|
12
12
|
ENV_VAR_PATTERN = /\$\{([^}]+)\}/
|
|
13
13
|
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
14
|
-
O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
|
|
15
|
-
|
|
16
14
|
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :base_dir
|
|
17
15
|
|
|
18
16
|
def initialize(config_path, base_dir: nil, options: {})
|
|
@@ -165,7 +163,6 @@ module ClaudeSwarm
|
|
|
165
163
|
|
|
166
164
|
# Parse provider (optional, defaults to claude)
|
|
167
165
|
provider = config["provider"]
|
|
168
|
-
model = config["model"]
|
|
169
166
|
|
|
170
167
|
# Validate provider value if specified
|
|
171
168
|
if provider && !VALID_PROVIDERS.include?(provider)
|
|
@@ -183,17 +180,6 @@ module ClaudeSwarm
|
|
|
183
180
|
unless VALID_REASONING_EFFORTS.include?(config["reasoning_effort"])
|
|
184
181
|
raise Error, "Instance '#{name}' has invalid reasoning_effort '#{config["reasoning_effort"]}'. Must be 'low', 'medium', or 'high'"
|
|
185
182
|
end
|
|
186
|
-
|
|
187
|
-
# Validate it's only used with o-series or gpt-5 models
|
|
188
|
-
# Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.
|
|
189
|
-
unless model&.match?(O_SERIES_MODEL_PATTERN)
|
|
190
|
-
raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series or gpt-5 model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, gpt-5, gpt-5-mini, gpt-5-nano, etc.)"
|
|
191
|
-
end
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
# Validate temperature is not used with o-series or gpt-5 models when provider is openai
|
|
195
|
-
if provider == "openai" && config["temperature"] && model&.match?(O_SERIES_MODEL_PATTERN)
|
|
196
|
-
raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series or gpt-5 model. O-series and gpt-5 models use deterministic reasoning and don't accept temperature settings"
|
|
197
183
|
end
|
|
198
184
|
|
|
199
185
|
# Validate OpenAI-specific fields only when provider is not "openai"
|
|
@@ -246,6 +232,7 @@ module ClaudeSwarm
|
|
|
246
232
|
instance_config[:openai_token_env] = config["openai_token_env"] || "OPENAI_API_KEY"
|
|
247
233
|
instance_config[:base_url] = config["base_url"]
|
|
248
234
|
instance_config[:reasoning_effort] = config["reasoning_effort"] if config["reasoning_effort"]
|
|
235
|
+
instance_config[:zdr] = config["zdr"] if config.key?("zdr")
|
|
249
236
|
# Default vibe to true for OpenAI instances if not specified
|
|
250
237
|
instance_config[:vibe] = true if config["vibe"].nil?
|
|
251
238
|
elsif config["vibe"].nil?
|
|
@@ -174,6 +174,7 @@ module ClaudeSwarm
|
|
|
174
174
|
args.push("--api-version", instance[:api_version]) if instance[:api_version]
|
|
175
175
|
args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
|
|
176
176
|
args.push("--base-url", instance[:base_url]) if instance[:base_url]
|
|
177
|
+
args.push("--zdr", instance[:zdr].to_s) if instance.key?(:zdr)
|
|
177
178
|
end
|
|
178
179
|
end
|
|
179
180
|
|
|
@@ -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
|
|
|
@@ -67,17 +68,8 @@ module ClaudeSwarm
|
|
|
67
68
|
messages: messages,
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
71
|
+
parameters[:temperature] = @temperature if @temperature
|
|
72
|
+
parameters[:reasoning_effort] = @reasoning_effort if @reasoning_effort
|
|
81
73
|
|
|
82
74
|
# Add tools if available
|
|
83
75
|
parameters[:tools] = @mcp_client.to_openai_tools if @available_tools&.any? && @mcp_client
|
|
@@ -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
|
|
|
@@ -46,17 +47,8 @@ module ClaudeSwarm
|
|
|
46
47
|
model: @model,
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
unless @model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
|
52
|
-
parameters[:temperature] = @temperature
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Only add reasoning effort for o-series models
|
|
56
|
-
# reasoning 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.
|
|
57
|
-
if @reasoning_effort && @model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
|
|
58
|
-
parameters[:reasoning] = { effort: @reasoning_effort }
|
|
59
|
-
end
|
|
50
|
+
parameters[:temperature] = @temperature if @temperature
|
|
51
|
+
parameters[:reasoning] = { effort: @reasoning_effort } if @reasoning_effort
|
|
60
52
|
|
|
61
53
|
# On first call, use string input (can include system prompt)
|
|
62
54
|
# On subsequent calls with function results, use array input
|
|
@@ -67,6 +59,7 @@ module ClaudeSwarm
|
|
|
67
59
|
else
|
|
68
60
|
input
|
|
69
61
|
end
|
|
62
|
+
conversation_array << { role: "user", content: parameters[:input] }
|
|
70
63
|
else
|
|
71
64
|
# Follow-up call with conversation array (function calls + outputs)
|
|
72
65
|
parameters[:input] = conversation_array
|
|
@@ -79,8 +72,8 @@ module ClaudeSwarm
|
|
|
79
72
|
@executor.logger.info { "Conversation item IDs: #{conversation_ids.inspect}" }
|
|
80
73
|
end
|
|
81
74
|
|
|
82
|
-
# Add previous response ID for conversation continuity
|
|
83
|
-
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
|
|
84
77
|
|
|
85
78
|
# Add tools if available
|
|
86
79
|
if @available_tools&.any?
|
|
@@ -115,7 +108,7 @@ module ClaudeSwarm
|
|
|
115
108
|
@executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
|
|
116
109
|
|
|
117
110
|
# Try to extract and log the response body for better debugging
|
|
118
|
-
if e.respond_to?(:response)
|
|
111
|
+
if e.respond_to?(:response) && e.response
|
|
119
112
|
begin
|
|
120
113
|
error_body = e.response[:body]
|
|
121
114
|
@executor.logger.error { "Error response body: #{error_body}" }
|
|
@@ -131,7 +124,7 @@ module ClaudeSwarm
|
|
|
131
124
|
error: {
|
|
132
125
|
class: e.class.to_s,
|
|
133
126
|
message: e.message,
|
|
134
|
-
response_body: e.respond_to?(:response) ? e.response[:body] : nil,
|
|
127
|
+
response_body: e.respond_to?(:response) && e.response ? e.response[:body] : nil,
|
|
135
128
|
backtrace: e.backtrace.first(5),
|
|
136
129
|
},
|
|
137
130
|
})
|
|
@@ -155,33 +148,21 @@ module ClaudeSwarm
|
|
|
155
148
|
|
|
156
149
|
# Handle response based on output structure
|
|
157
150
|
output = response["output"]
|
|
158
|
-
|
|
159
151
|
if output.nil?
|
|
160
152
|
@executor.logger.error { "No output in response" }
|
|
161
153
|
return "Error: No output in OpenAI response"
|
|
162
154
|
end
|
|
163
155
|
|
|
164
156
|
# Check if output is an array (as per documentation)
|
|
165
|
-
if output.is_a?(Array) &&
|
|
157
|
+
if output.is_a?(Array) && output.any?
|
|
158
|
+
new_conversation = conversation_array.dup
|
|
159
|
+
new_conversation.concat(output)
|
|
166
160
|
# Check if there are function calls
|
|
167
161
|
function_calls = output.select { |item| item["type"] == "function_call" }
|
|
168
|
-
|
|
169
162
|
if function_calls.any?
|
|
170
|
-
|
|
171
|
-
if conversation_array.empty?
|
|
172
|
-
# First depth - build new conversation
|
|
173
|
-
new_conversation = build_conversation_with_outputs(function_calls)
|
|
174
|
-
else
|
|
175
|
-
# Subsequent depth - append to existing conversation
|
|
176
|
-
# Don't re-add function calls, just add the new ones and their outputs
|
|
177
|
-
new_conversation = conversation_array.dup
|
|
178
|
-
append_new_outputs(function_calls, new_conversation)
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
# Recursively process with updated conversation
|
|
163
|
+
append_new_outputs(function_calls, new_conversation)
|
|
182
164
|
process_responses_api(nil, new_conversation, response_id, depth + 1)
|
|
183
165
|
else
|
|
184
|
-
# Look for text response
|
|
185
166
|
extract_text_response(output)
|
|
186
167
|
end
|
|
187
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
|
|
@@ -95,11 +95,18 @@ module SwarmCLI
|
|
|
95
95
|
handle_breakpoint_enter(entry)
|
|
96
96
|
when "breakpoint_exit"
|
|
97
97
|
handle_breakpoint_exit(entry)
|
|
98
|
+
when "llm_retry_attempt"
|
|
99
|
+
handle_llm_retry_attempt(entry)
|
|
100
|
+
when "llm_retry_exhausted"
|
|
101
|
+
handle_llm_retry_exhausted(entry)
|
|
98
102
|
end
|
|
99
103
|
end
|
|
100
104
|
|
|
101
105
|
# Called when swarm execution completes successfully
|
|
102
106
|
def on_success(result:)
|
|
107
|
+
# Defensive: ensure all spinners are stopped before showing result
|
|
108
|
+
@spinner_manager.stop_all
|
|
109
|
+
|
|
103
110
|
if @mode == :non_interactive
|
|
104
111
|
# Full result display with summary
|
|
105
112
|
@output.puts
|
|
@@ -115,6 +122,9 @@ module SwarmCLI
|
|
|
115
122
|
|
|
116
123
|
# Called when swarm execution fails
|
|
117
124
|
def on_error(error:, duration: nil)
|
|
125
|
+
# Defensive: ensure all spinners are stopped before showing error
|
|
126
|
+
@spinner_manager.stop_all
|
|
127
|
+
|
|
118
128
|
@output.puts
|
|
119
129
|
@output.puts @divider.full
|
|
120
130
|
print_error(error)
|
|
@@ -575,6 +585,66 @@ module SwarmCLI
|
|
|
575
585
|
@output.puts
|
|
576
586
|
end
|
|
577
587
|
|
|
588
|
+
def handle_llm_retry_attempt(entry)
|
|
589
|
+
agent = entry[:agent]
|
|
590
|
+
attempt = entry[:attempt]
|
|
591
|
+
max_retries = entry[:max_retries]
|
|
592
|
+
error_class = entry[:error_class]
|
|
593
|
+
error_message = entry[:error_message]
|
|
594
|
+
retry_delay = entry[:retry_delay]
|
|
595
|
+
|
|
596
|
+
# Stop agent thinking spinner (if active)
|
|
597
|
+
unless @quiet
|
|
598
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
599
|
+
@spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
lines = [
|
|
603
|
+
@pastel.yellow("LLM API request failed (attempt #{attempt}/#{max_retries})"),
|
|
604
|
+
@pastel.dim("Error: #{error_class}: #{error_message}"),
|
|
605
|
+
@pastel.dim("Retrying in #{retry_delay}s..."),
|
|
606
|
+
]
|
|
607
|
+
|
|
608
|
+
@output.puts @panel.render(
|
|
609
|
+
type: :warning,
|
|
610
|
+
title: "RETRY #{@agent_badge.render(agent)}",
|
|
611
|
+
lines: lines,
|
|
612
|
+
indent: @depth_tracker.get(agent),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
# Restart spinner for next attempt
|
|
616
|
+
unless @quiet
|
|
617
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
618
|
+
@spinner_manager.start(spinner_key, "#{agent} is retrying...")
|
|
619
|
+
end
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def handle_llm_retry_exhausted(entry)
|
|
623
|
+
agent = entry[:agent]
|
|
624
|
+
attempts = entry[:attempts]
|
|
625
|
+
error_class = entry[:error_class]
|
|
626
|
+
error_message = entry[:error_message]
|
|
627
|
+
|
|
628
|
+
# Stop agent thinking spinner (if active)
|
|
629
|
+
unless @quiet
|
|
630
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
631
|
+
@spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
lines = [
|
|
635
|
+
@pastel.red("LLM API request failed after #{attempts} attempts"),
|
|
636
|
+
@pastel.dim("Error: #{error_class}: #{error_message}"),
|
|
637
|
+
@pastel.dim("No more retries available"),
|
|
638
|
+
]
|
|
639
|
+
|
|
640
|
+
@output.puts @panel.render(
|
|
641
|
+
type: :error,
|
|
642
|
+
title: "RETRY EXHAUSTED #{@agent_badge.render(agent)}",
|
|
643
|
+
lines: lines,
|
|
644
|
+
indent: @depth_tracker.get(agent),
|
|
645
|
+
)
|
|
646
|
+
end
|
|
647
|
+
|
|
578
648
|
def display_todo_list(agent, timestamp)
|
|
579
649
|
todos = SwarmSDK::Tools::Stores::TodoManager.get_todos(agent.to_sym)
|
|
580
650
|
indent = @depth_tracker.indent(agent)
|
|
@@ -81,6 +81,9 @@ module SwarmCLI
|
|
|
81
81
|
display_session_summary
|
|
82
82
|
exit(130)
|
|
83
83
|
ensure
|
|
84
|
+
# Defensive: ensure all spinners are stopped on exit
|
|
85
|
+
@formatter&.spinner_manager&.stop_all
|
|
86
|
+
|
|
84
87
|
# Save history on exit
|
|
85
88
|
save_persistent_history
|
|
86
89
|
end
|
|
@@ -432,11 +435,12 @@ module SwarmCLI
|
|
|
432
435
|
end
|
|
433
436
|
end
|
|
434
437
|
|
|
438
|
+
# CRITICAL: Stop all spinners after execution completes
|
|
439
|
+
# This ensures spinner doesn't interfere with error/success display or REPL prompt
|
|
440
|
+
@formatter.spinner_manager.stop_all
|
|
441
|
+
|
|
435
442
|
# Handle cancellation (result is nil when cancelled)
|
|
436
443
|
if result.nil?
|
|
437
|
-
# Stop all active spinners
|
|
438
|
-
@formatter.spinner_manager.stop_all
|
|
439
|
-
|
|
440
444
|
puts ""
|
|
441
445
|
puts @colors[:warning].call("✗ Request cancelled by user")
|
|
442
446
|
puts ""
|
|
@@ -459,6 +463,8 @@ module SwarmCLI
|
|
|
459
463
|
# Add response to history
|
|
460
464
|
@conversation_history << { role: "agent", content: result.content }
|
|
461
465
|
rescue StandardError => e
|
|
466
|
+
# Defensive: ensure spinners are stopped on exception
|
|
467
|
+
@formatter.spinner_manager.stop_all
|
|
462
468
|
@formatter.on_error(error: e)
|
|
463
469
|
end
|
|
464
470
|
|
|
@@ -528,7 +534,7 @@ module SwarmCLI
|
|
|
528
534
|
lead = @swarm.agent(@swarm.lead_agent)
|
|
529
535
|
|
|
530
536
|
# Clear the agent's conversation history
|
|
531
|
-
lead.
|
|
537
|
+
lead.replace_messages([])
|
|
532
538
|
|
|
533
539
|
# Clear REPL conversation history
|
|
534
540
|
@conversation_history.clear
|
|
@@ -569,7 +575,7 @@ module SwarmCLI
|
|
|
569
575
|
case tool_name
|
|
570
576
|
when /^Memory/, "LoadSkill"
|
|
571
577
|
memory_tools << tool_name
|
|
572
|
-
when /^
|
|
578
|
+
when /^WorkWith/
|
|
573
579
|
delegation_tools << tool_name
|
|
574
580
|
when /^mcp__/
|
|
575
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)
|