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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +2 -15
  5. data/lib/claude_swarm/mcp_generator.rb +1 -0
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/run.rb +2 -2
  11. data/lib/swarm_cli/config_loader.rb +11 -11
  12. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  13. data/lib/swarm_cli/interactive_repl.rb +11 -5
  14. data/lib/swarm_cli/ui/icons.rb +0 -23
  15. data/lib/swarm_cli/version.rb +1 -1
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  17. data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
  18. data/lib/swarm_memory/version.rb +1 -1
  19. data/lib/swarm_memory.rb +1 -1
  20. data/lib/swarm_sdk/agent/builder.rb +58 -0
  21. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  22. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  23. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  24. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  25. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  26. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  27. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  28. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  30. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  31. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  32. data/lib/swarm_sdk/agent/context.rb +2 -2
  33. data/lib/swarm_sdk/agent/definition.rb +66 -154
  34. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  35. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  36. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  37. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  38. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  39. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  40. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  41. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  42. data/lib/swarm_sdk/configuration.rb +65 -543
  43. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  44. data/lib/swarm_sdk/context_compactor.rb +6 -11
  45. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  46. data/lib/swarm_sdk/context_management/context.rb +328 -0
  47. data/lib/swarm_sdk/defaults.rb +196 -0
  48. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  49. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  50. data/lib/swarm_sdk/log_collector.rb +179 -29
  51. data/lib/swarm_sdk/log_stream.rb +29 -0
  52. data/lib/swarm_sdk/node_context.rb +1 -1
  53. data/lib/swarm_sdk/observer/builder.rb +81 -0
  54. data/lib/swarm_sdk/observer/config.rb +45 -0
  55. data/lib/swarm_sdk/observer/manager.rb +236 -0
  56. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  57. data/lib/swarm_sdk/plugin.rb +93 -3
  58. data/lib/swarm_sdk/snapshot.rb +6 -6
  59. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  60. data/lib/swarm_sdk/state_restorer.rb +136 -151
  61. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  62. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  63. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  64. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  65. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  66. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  67. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  68. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  69. data/lib/swarm_sdk/swarm.rb +137 -679
  70. data/lib/swarm_sdk/tools/bash.rb +11 -3
  71. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  72. data/lib/swarm_sdk/tools/edit.rb +8 -13
  73. data/lib/swarm_sdk/tools/glob.rb +9 -1
  74. data/lib/swarm_sdk/tools/grep.rb +7 -0
  75. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  76. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  77. data/lib/swarm_sdk/tools/read.rb +11 -13
  78. data/lib/swarm_sdk/tools/registry.rb +122 -10
  79. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  80. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  81. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  82. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  83. data/lib/swarm_sdk/tools/write.rb +8 -13
  84. data/lib/swarm_sdk/version.rb +1 -1
  85. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  86. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  87. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  88. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  89. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  90. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  91. data/lib/swarm_sdk.rb +33 -3
  92. metadata +37 -14
  93. data/lib/swarm_memory/chat_extension.rb +0 -34
  94. 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: 8422c40d79fb0c2adcd9e1de9a4cfdf289ced49ef27507969ea797c4187a0ee1
4
- data.tar.gz: 875f1429c37f2485d32641a7d96c9cae5db226a5bc2fc3e2f8c1443946e021b9
3
+ metadata.gz: ebb25752a42e174408e197c5b24d94ebfbd2a57cfd5a26101e839f2375e54b8f
4
+ data.tar.gz: 9558c9e19cfb486872d6cee1b445506e9c599f8d4a767670743f3a3f1481fb49
5
5
  SHA512:
6
- metadata.gz: 298c0506a2f486aab1ad29c9f6e4605fe3fbb91fb17f7e3ee913b046f97a9d347312afd686e4bf26d28d6329c8974d421d39ee488e04bf2bc4cde22fea8cdc8c
7
- data.tar.gz: c472ec8754b18a2a8d0ab1ae1a0dc391b2f4fffc2de670831272938d81e51a6716c81343b174afa8833d256e28a728fdd198e0bb2b6d92e13adc085c786d2abc
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
@@ -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
- # Only add temperature for non-o-series models
71
- # O-series models don't support temperature parameter
72
- if @temperature && !@model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
73
- parameters[:temperature] = @temperature
74
- end
75
-
76
- # Only add reasoning_effort for o-series models
77
- # reasoning_effort is only supported by o-series models: o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.
78
- if @reasoning_effort && @model.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
79
- parameters[:reasoning_effort] = @reasoning_effort
80
- end
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
- # Only add temperature for non-o-series models
50
- # O-series models don't support temperature parameter
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] = previous_response_id if 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) && !output.empty?
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
- # Check if we already have a conversation going
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "1.0.6"
4
+ VERSION = "1.0.9"
5
5
  end
@@ -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.on_log do |log_entry|
138
- formatter.on_log(log_entry) if log_entry[:type] == "model_lookup_warning"
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::NodeOrchestrator instance
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::NodeOrchestrator instance
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::NodeOrchestrator] Configured swarm or orchestrator instance
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::NodeOrchestrator instance. The file should
63
- # use SwarmSDK.build or create a Swarm/NodeOrchestrator instance directly.
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::NodeOrchestrator] Configured swarm or orchestrator instance
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 NodeOrchestrator instance
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::NodeOrchestrator)
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::NodeOrchestrator instance. " \
80
+ "Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::Workflow instance. " \
81
81
  "Got: #{result.class}. " \
82
- "Use: SwarmSDK.build { ... } or Swarm.new(...)"
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.reset_messages!
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 /^DelegateTaskTo/
578
+ when /^WorkWith/
573
579
  delegation_tools << tool_name
574
580
  when /^mcp__/
575
581
  mcp_tools << tool_name
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.1.2"
4
+ VERSION = "2.1.4"
5
5
  end
@@ -93,10 +93,10 @@ module SwarmMemory
93
93
  "Clear old entries or use smaller content."
94
94
  end
95
95
 
96
- # Strip .md extension and flatten path for disk storage
97
- # "concepts/ruby/classes.md" → "concepts--ruby--classes"
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 = flatten_path(base_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 and flatten path
165
+ # Strip .md extension
166
166
  base_path = file_path.sub(/\.md\z/, "")
167
- disk_path = flatten_path(base_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 and flatten path
192
+ # Strip .md extension
193
193
  base_path = file_path.sub(/\.md\z/, "")
194
- disk_path = flatten_path(base_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 and flatten path
233
+ # Strip .md extension
234
234
  base_path = file_path.sub(/\.md\z/, "")
235
- disk_path = flatten_path(base_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 = flatten_path(base_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 = flatten_path(base_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)