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
|
@@ -28,7 +28,7 @@ module SwarmSDK
|
|
|
28
28
|
# end
|
|
29
29
|
#
|
|
30
30
|
# swarm.execute("Build auth API")
|
|
31
|
-
class Builder
|
|
31
|
+
class Builder < Builders::BaseBuilder
|
|
32
32
|
# Main entry point for DSL
|
|
33
33
|
#
|
|
34
34
|
# @example
|
|
@@ -45,29 +45,10 @@ module SwarmSDK
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def initialize(allow_filesystem_tools: nil)
|
|
48
|
-
|
|
49
|
-
@swarm_name = nil
|
|
48
|
+
super
|
|
50
49
|
@lead_agent = nil
|
|
51
|
-
@agents = {}
|
|
52
|
-
@all_agents_config = nil
|
|
53
50
|
@swarm_hooks = []
|
|
54
|
-
@
|
|
55
|
-
@nodes = {}
|
|
56
|
-
@start_node = nil
|
|
57
|
-
@scratchpad = :disabled # Default: disabled
|
|
58
|
-
@allow_filesystem_tools = allow_filesystem_tools
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# Set swarm ID
|
|
62
|
-
#
|
|
63
|
-
# @param swarm_id [String] Unique identifier for this swarm
|
|
64
|
-
def id(swarm_id)
|
|
65
|
-
@swarm_id = swarm_id
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Set swarm name
|
|
69
|
-
def name(swarm_name)
|
|
70
|
-
@swarm_name = swarm_name
|
|
51
|
+
@observer_configs = []
|
|
71
52
|
end
|
|
72
53
|
|
|
73
54
|
# Set lead agent
|
|
@@ -75,75 +56,42 @@ module SwarmSDK
|
|
|
75
56
|
@lead_agent = agent_name
|
|
76
57
|
end
|
|
77
58
|
|
|
78
|
-
#
|
|
59
|
+
# Define observer agent behavior
|
|
79
60
|
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
61
|
+
# Configures an agent to run in parallel with main execution,
|
|
62
|
+
# triggered by specific events. The block defines event handlers.
|
|
82
63
|
#
|
|
83
|
-
# @param
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Register external swarms for composable swarms
|
|
64
|
+
# @param agent_name [Symbol] Name of agent to use as observer (must be defined)
|
|
65
|
+
# @param options [Hash] Optional observer settings (timeout, max_concurrent, etc.)
|
|
66
|
+
# @yield Observer configuration block
|
|
89
67
|
#
|
|
90
|
-
# @example
|
|
91
|
-
#
|
|
92
|
-
#
|
|
93
|
-
#
|
|
68
|
+
# @example Basic observer
|
|
69
|
+
# observer :profiler do
|
|
70
|
+
# on :swarm_start do |event|
|
|
71
|
+
# "Analyze this prompt: #{event[:prompt]}"
|
|
72
|
+
# end
|
|
94
73
|
# end
|
|
95
74
|
#
|
|
96
|
-
# @
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Define an agent with fluent API or load from markdown content
|
|
104
|
-
#
|
|
105
|
-
# Supports two forms:
|
|
106
|
-
# 1. Inline DSL: agent :name do ... end
|
|
107
|
-
# 2. Markdown content: agent :name, <<~MD ... MD
|
|
108
|
-
#
|
|
109
|
-
# The name parameter is always required. If the markdown has a name field
|
|
110
|
-
# in frontmatter, it will be replaced by the name parameter.
|
|
111
|
-
#
|
|
112
|
-
# @example Inline DSL
|
|
113
|
-
# agent :backend do
|
|
114
|
-
# model "gpt-5"
|
|
115
|
-
# system_prompt "You build APIs"
|
|
116
|
-
# tools :Read, :Write
|
|
117
|
-
#
|
|
118
|
-
# hook :pre_tool_use, matcher: "Bash" do |ctx|
|
|
119
|
-
# # Inline validation logic!
|
|
75
|
+
# @example Observer with options
|
|
76
|
+
# observer :monitor, timeout: 120 do
|
|
77
|
+
# on :tool_call do |event|
|
|
78
|
+
# next unless event[:tool_name] == "Bash"
|
|
79
|
+
# "Check command: #{event[:arguments][:command]}"
|
|
120
80
|
# end
|
|
121
81
|
# end
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# model: "gpt-4"
|
|
128
|
-
# ---
|
|
129
|
-
#
|
|
130
|
-
# You build APIs.
|
|
131
|
-
# MD
|
|
132
|
-
def agent(name, content = nil, &block)
|
|
133
|
-
# Case 1: agent :name, <<~MD do ... end (markdown + overrides)
|
|
134
|
-
if content.is_a?(String) && block_given? && markdown_content?(content)
|
|
135
|
-
load_agent_from_markdown_with_overrides(content, name, &block)
|
|
136
|
-
# Case 2: agent :name, <<~MD (markdown only)
|
|
137
|
-
elsif content.is_a?(String) && !block_given? && markdown_content?(content)
|
|
138
|
-
load_agent_from_markdown(content, name)
|
|
139
|
-
# Case 3: agent :name do ... end (inline DSL)
|
|
140
|
-
elsif block_given?
|
|
141
|
-
builder = Agent::Builder.new(name)
|
|
142
|
-
builder.instance_eval(&block)
|
|
143
|
-
@agents[name] = builder
|
|
144
|
-
else
|
|
145
|
-
raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
|
|
82
|
+
def observer(agent_name, **options, &block)
|
|
83
|
+
unless @agents.key?(agent_name)
|
|
84
|
+
raise ConfigurationError,
|
|
85
|
+
"Observer agent '#{agent_name}' not defined. " \
|
|
86
|
+
"Define the agent first with `agent :#{agent_name} do ... end`"
|
|
146
87
|
end
|
|
88
|
+
|
|
89
|
+
config = Observer::Config.new(agent_name)
|
|
90
|
+
config.options.merge!(options) if options.any?
|
|
91
|
+
builder = Observer::Builder.new(agent_name, config)
|
|
92
|
+
builder.instance_eval(&block)
|
|
93
|
+
|
|
94
|
+
@observer_configs << config
|
|
147
95
|
end
|
|
148
96
|
|
|
149
97
|
# Add swarm-level hook (swarm_start, swarm_stop only)
|
|
@@ -164,202 +112,21 @@ module SwarmSDK
|
|
|
164
112
|
@swarm_hooks << { event: event, command: command, timeout: timeout, block: block }
|
|
165
113
|
end
|
|
166
114
|
|
|
167
|
-
#
|
|
168
|
-
#
|
|
169
|
-
# @example
|
|
170
|
-
# all_agents do
|
|
171
|
-
# tools :Read, :Write
|
|
172
|
-
#
|
|
173
|
-
# hook :pre_tool_use, matcher: "Write" do |ctx|
|
|
174
|
-
# # Validation for all agents
|
|
175
|
-
# end
|
|
176
|
-
# end
|
|
177
|
-
def all_agents(&block)
|
|
178
|
-
builder = AllAgentsBuilder.new
|
|
179
|
-
builder.instance_eval(&block)
|
|
180
|
-
@all_agents_config = builder
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Define a node (mini-swarm execution stage)
|
|
184
|
-
#
|
|
185
|
-
# Nodes enable multi-stage workflows where different agent teams
|
|
186
|
-
# collaborate in sequence. Each node is an independent swarm execution.
|
|
187
|
-
#
|
|
188
|
-
# @param name [Symbol] Node name
|
|
189
|
-
# @yield Block for node configuration
|
|
190
|
-
# @return [void]
|
|
191
|
-
#
|
|
192
|
-
# @example Solo agent node
|
|
193
|
-
# node :planning do
|
|
194
|
-
# agent(:architect)
|
|
195
|
-
# end
|
|
196
|
-
#
|
|
197
|
-
# @example Multi-agent node with delegation
|
|
198
|
-
# node :implementation do
|
|
199
|
-
# agent(:backend).delegates_to(:tester, :database)
|
|
200
|
-
# agent(:tester).delegates_to(:database)
|
|
201
|
-
# agent(:database)
|
|
202
|
-
# after :planning
|
|
203
|
-
# end
|
|
204
|
-
def node(name, &block)
|
|
205
|
-
builder = Node::Builder.new(name)
|
|
206
|
-
builder.instance_eval(&block)
|
|
207
|
-
@nodes[name] = builder
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
# Set the starting node for workflow execution
|
|
211
|
-
#
|
|
212
|
-
# Required when nodes are defined. Specifies which node to execute first.
|
|
213
|
-
#
|
|
214
|
-
# @param name [Symbol] Name of starting node
|
|
215
|
-
# @return [void]
|
|
216
|
-
#
|
|
217
|
-
# @example
|
|
218
|
-
# start_node :planning
|
|
219
|
-
def start_node(name)
|
|
220
|
-
@start_node = name.to_sym
|
|
221
|
-
end
|
|
222
|
-
|
|
223
|
-
# Build the actual Swarm instance or NodeOrchestrator
|
|
115
|
+
# Build the actual Swarm instance
|
|
224
116
|
def build_swarm
|
|
225
117
|
raise ConfigurationError, "Swarm name not set. Use: name 'My Swarm'" unless @swarm_name
|
|
118
|
+
raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
|
|
119
|
+
raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
|
|
226
120
|
|
|
227
|
-
# Validate
|
|
121
|
+
# Validate filesystem tools BEFORE building
|
|
228
122
|
validate_all_agents_filesystem_tools if @all_agents_config
|
|
229
|
-
|
|
230
|
-
# Validate individual agent filesystem tools BEFORE building
|
|
231
123
|
validate_agent_filesystem_tools
|
|
232
124
|
|
|
233
|
-
|
|
234
|
-
if @nodes.any?
|
|
235
|
-
# Node-based workflow (agents optional for agent-less workflows)
|
|
236
|
-
build_node_orchestrator
|
|
237
|
-
else
|
|
238
|
-
# Traditional single-swarm execution (requires agents and lead)
|
|
239
|
-
raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
|
|
240
|
-
raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
|
|
241
|
-
|
|
242
|
-
build_single_swarm
|
|
243
|
-
end
|
|
125
|
+
build_single_swarm
|
|
244
126
|
end
|
|
245
127
|
|
|
246
128
|
private
|
|
247
129
|
|
|
248
|
-
# Normalize scratchpad mode parameter
|
|
249
|
-
#
|
|
250
|
-
# Accepts symbols: :enabled, :per_node, or :disabled
|
|
251
|
-
#
|
|
252
|
-
# @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
|
|
253
|
-
# @return [Symbol] Normalized mode (:enabled, :per_node, or :disabled)
|
|
254
|
-
# @raise [ConfigurationError] If value is invalid
|
|
255
|
-
def normalize_scratchpad_mode(value)
|
|
256
|
-
# Convert strings from YAML to symbols
|
|
257
|
-
value = value.to_sym if value.is_a?(String)
|
|
258
|
-
|
|
259
|
-
case value
|
|
260
|
-
when :enabled, :per_node, :disabled
|
|
261
|
-
value
|
|
262
|
-
else
|
|
263
|
-
raise ConfigurationError,
|
|
264
|
-
"Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
# Check if a string is markdown content (has frontmatter)
|
|
269
|
-
#
|
|
270
|
-
# @param str [String] String to check
|
|
271
|
-
# @return [Boolean] true if string contains markdown frontmatter
|
|
272
|
-
def markdown_content?(str)
|
|
273
|
-
str.start_with?("---") || str.include?("\n---\n")
|
|
274
|
-
end
|
|
275
|
-
|
|
276
|
-
# Load an agent from markdown content
|
|
277
|
-
#
|
|
278
|
-
# Returns a hash of the agent config (not a Definition yet) so that
|
|
279
|
-
# all_agents config can be applied later in the build process.
|
|
280
|
-
#
|
|
281
|
-
# @param content [String] Markdown content with frontmatter
|
|
282
|
-
# @param name_override [Symbol, nil] Optional name to override frontmatter name
|
|
283
|
-
# @return [void]
|
|
284
|
-
def load_agent_from_markdown(content, name_override = nil)
|
|
285
|
-
# Parse markdown content - will extract name from frontmatter if not overridden
|
|
286
|
-
definition = MarkdownParser.parse(content, name_override)
|
|
287
|
-
|
|
288
|
-
# Store the config hash (not Definition) so all_agents can be applied
|
|
289
|
-
# We'll wrap this in a special marker so we know it came from markdown
|
|
290
|
-
@agents[definition.name] = { __file_config__: definition.to_h }
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
# Load an agent from markdown content with DSL overrides
|
|
294
|
-
#
|
|
295
|
-
# This allows loading from a file and then overriding specific settings:
|
|
296
|
-
# agent :reviewer, File.read("reviewer.md") do
|
|
297
|
-
# provider :openai
|
|
298
|
-
# model "gpt-4o"
|
|
299
|
-
# end
|
|
300
|
-
#
|
|
301
|
-
# @param content [String] Markdown content with frontmatter
|
|
302
|
-
# @param name_override [Symbol, nil] Optional name to override frontmatter name
|
|
303
|
-
# @yield Block with DSL overrides
|
|
304
|
-
# @return [void]
|
|
305
|
-
def load_agent_from_markdown_with_overrides(content, name_override = nil, &block)
|
|
306
|
-
# Parse markdown content first
|
|
307
|
-
definition = MarkdownParser.parse(content, name_override)
|
|
308
|
-
|
|
309
|
-
# Create a builder with the markdown config
|
|
310
|
-
builder = Agent::Builder.new(definition.name)
|
|
311
|
-
|
|
312
|
-
# Apply markdown settings to builder (these become the base)
|
|
313
|
-
apply_definition_to_builder(builder, definition.to_h)
|
|
314
|
-
|
|
315
|
-
# Apply DSL overrides (these override the markdown settings)
|
|
316
|
-
builder.instance_eval(&block)
|
|
317
|
-
|
|
318
|
-
# Store the builder (not file config) so overrides are preserved
|
|
319
|
-
@agents[definition.name] = builder
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
# Apply agent definition hash to a builder
|
|
323
|
-
#
|
|
324
|
-
# @param builder [Agent::Builder] Builder to configure
|
|
325
|
-
# @param config [Hash] Configuration hash from definition
|
|
326
|
-
# @return [void]
|
|
327
|
-
def apply_definition_to_builder(builder, config)
|
|
328
|
-
builder.description(config[:description]) if config[:description]
|
|
329
|
-
builder.model(config[:model]) if config[:model]
|
|
330
|
-
builder.provider(config[:provider]) if config[:provider]
|
|
331
|
-
builder.base_url(config[:base_url]) if config[:base_url]
|
|
332
|
-
builder.api_version(config[:api_version]) if config[:api_version]
|
|
333
|
-
builder.context_window(config[:context_window]) if config[:context_window]
|
|
334
|
-
builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
|
|
335
|
-
builder.directory(config[:directory]) if config[:directory]
|
|
336
|
-
builder.timeout(config[:timeout]) if config[:timeout]
|
|
337
|
-
builder.parameters(config[:parameters]) if config[:parameters]
|
|
338
|
-
builder.headers(config[:headers]) if config[:headers]
|
|
339
|
-
builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
|
|
340
|
-
# Don't apply assume_model_exists from markdown - let DSL overrides or auto-enable handle it
|
|
341
|
-
# builder.assume_model_exists(config[:assume_model_exists]) unless config[:assume_model_exists].nil?
|
|
342
|
-
builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
|
343
|
-
builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
344
|
-
|
|
345
|
-
# Add tools from markdown
|
|
346
|
-
if config[:tools]&.any?
|
|
347
|
-
# Extract tool names from the tools array (which may be hashes with permissions)
|
|
348
|
-
tool_names = config[:tools].map do |tool|
|
|
349
|
-
tool.is_a?(Hash) ? tool[:name] : tool
|
|
350
|
-
end
|
|
351
|
-
builder.tools(*tool_names)
|
|
352
|
-
end
|
|
353
|
-
|
|
354
|
-
# Add delegates_to
|
|
355
|
-
builder.delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
|
|
356
|
-
|
|
357
|
-
# Add MCP servers
|
|
358
|
-
config[:mcp_servers]&.each do |server|
|
|
359
|
-
builder.mcp_server(server[:name], **server.except(:name))
|
|
360
|
-
end
|
|
361
|
-
end
|
|
362
|
-
|
|
363
130
|
# Build a traditional single-swarm execution
|
|
364
131
|
#
|
|
365
132
|
# @return [Swarm] Configured swarm instance
|
|
@@ -386,20 +153,9 @@ module SwarmSDK
|
|
|
386
153
|
swarm.swarm_registry = registry
|
|
387
154
|
end
|
|
388
155
|
|
|
389
|
-
#
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
# Build definitions and add to swarm
|
|
393
|
-
# Handle both Agent::Builder (inline DSL) and file configs (from files)
|
|
394
|
-
@agents.each do |agent_name, agent_builder_or_config|
|
|
395
|
-
definition = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
|
|
396
|
-
# File-loaded agent config (with all_agents merged)
|
|
397
|
-
Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
|
|
398
|
-
else
|
|
399
|
-
# Builder object (from inline DSL) - convert to definition
|
|
400
|
-
agent_builder_or_config.to_definition
|
|
401
|
-
end
|
|
402
|
-
|
|
156
|
+
# Build agent definitions and add to swarm
|
|
157
|
+
agent_definitions = build_agent_definitions
|
|
158
|
+
agent_definitions.each_value do |definition|
|
|
403
159
|
swarm.add_agent(definition)
|
|
404
160
|
end
|
|
405
161
|
|
|
@@ -407,198 +163,21 @@ module SwarmSDK
|
|
|
407
163
|
swarm.lead = @lead_agent
|
|
408
164
|
|
|
409
165
|
# Apply swarm hooks (Ruby blocks)
|
|
410
|
-
# These are swarm-level hooks (swarm_start, swarm_stop)
|
|
411
166
|
@swarm_hooks.each do |hook_config|
|
|
412
167
|
apply_swarm_hook(swarm, hook_config)
|
|
413
168
|
end
|
|
414
169
|
|
|
415
170
|
# Apply all_agents hooks (Ruby blocks)
|
|
416
|
-
# These become swarm-level default callbacks that apply to all agents
|
|
417
171
|
@all_agents_config&.hooks&.each do |hook_config|
|
|
418
172
|
apply_all_agents_hook(swarm, hook_config)
|
|
419
173
|
end
|
|
420
174
|
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
# This ensures they're applied at the right time, after LogStream is set up
|
|
175
|
+
# Add observer configurations to swarm
|
|
176
|
+
@observer_configs.each { |c| swarm.add_observer_config(c) }
|
|
424
177
|
|
|
425
178
|
swarm
|
|
426
179
|
end
|
|
427
180
|
|
|
428
|
-
# Build a node-based workflow orchestrator
|
|
429
|
-
#
|
|
430
|
-
# @return [NodeOrchestrator] Configured orchestrator
|
|
431
|
-
def build_node_orchestrator
|
|
432
|
-
raise ConfigurationError, "start_node required when nodes are defined. Use: start_node :name" unless @start_node
|
|
433
|
-
|
|
434
|
-
# Merge all_agents config into each agent (applies to all nodes)
|
|
435
|
-
merge_all_agents_config_into_agents if @all_agents_config
|
|
436
|
-
|
|
437
|
-
# Build agent definitions
|
|
438
|
-
# Handle both Agent::Builder (inline DSL) and file configs (from files)
|
|
439
|
-
agent_definitions = {}
|
|
440
|
-
@agents.each do |agent_name, agent_builder_or_config|
|
|
441
|
-
agent_definitions[agent_name] = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
|
|
442
|
-
# File-loaded agent config (with all_agents merged)
|
|
443
|
-
Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
|
|
444
|
-
else
|
|
445
|
-
# Builder object (from inline DSL) - convert to definition
|
|
446
|
-
agent_builder_or_config.to_definition
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
# Create node orchestrator
|
|
451
|
-
orchestrator = NodeOrchestrator.new(
|
|
452
|
-
swarm_name: @swarm_name,
|
|
453
|
-
swarm_id: @swarm_id,
|
|
454
|
-
agent_definitions: agent_definitions,
|
|
455
|
-
nodes: @nodes,
|
|
456
|
-
start_node: @start_node,
|
|
457
|
-
scratchpad: @scratchpad,
|
|
458
|
-
allow_filesystem_tools: @allow_filesystem_tools,
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
# Pass swarm registry config to orchestrator if external swarms registered
|
|
462
|
-
orchestrator.swarm_registry_config = @swarm_registry_config if @swarm_registry_config.any?
|
|
463
|
-
|
|
464
|
-
orchestrator
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
# Merge all_agents configuration into each agent
|
|
468
|
-
#
|
|
469
|
-
# All_agents values are used as defaults - agent-specific values override.
|
|
470
|
-
# This applies to both inline DSL agents (Builder) and file-loaded agents (config hash).
|
|
471
|
-
#
|
|
472
|
-
# @return [void]
|
|
473
|
-
def merge_all_agents_config_into_agents
|
|
474
|
-
return unless @all_agents_config
|
|
475
|
-
|
|
476
|
-
all_agents_hash = @all_agents_config.to_h
|
|
477
|
-
|
|
478
|
-
@agents.each_value do |agent_builder_or_config|
|
|
479
|
-
if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
|
|
480
|
-
# File-loaded agent - merge into the config hash
|
|
481
|
-
file_config = agent_builder_or_config[:__file_config__]
|
|
482
|
-
|
|
483
|
-
# Merge all_agents into file config (file config overrides)
|
|
484
|
-
# Use same merge strategy as Configuration class
|
|
485
|
-
merged_config = merge_all_agents_into_config(all_agents_hash, file_config)
|
|
486
|
-
|
|
487
|
-
# Update the stored config
|
|
488
|
-
agent_builder_or_config[:__file_config__] = merged_config
|
|
489
|
-
else
|
|
490
|
-
# Builder object (inline DSL agent)
|
|
491
|
-
agent_builder = agent_builder_or_config
|
|
492
|
-
|
|
493
|
-
# Apply all_agents defaults that haven't been set at agent level
|
|
494
|
-
# Agent values override all_agents values
|
|
495
|
-
apply_all_agents_defaults(agent_builder, all_agents_hash)
|
|
496
|
-
|
|
497
|
-
# Merge tools (prepend all_agents tools)
|
|
498
|
-
all_agents_tools = @all_agents_config.tools_list
|
|
499
|
-
agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
|
|
500
|
-
|
|
501
|
-
# Pass all_agents permissions as default_permissions
|
|
502
|
-
if @all_agents_config.permissions_config.any?
|
|
503
|
-
agent_builder.default_permissions = @all_agents_config.permissions_config
|
|
504
|
-
end
|
|
505
|
-
end
|
|
506
|
-
end
|
|
507
|
-
end
|
|
508
|
-
|
|
509
|
-
# Merge all_agents config into file-loaded agent config
|
|
510
|
-
#
|
|
511
|
-
# Follows same merge strategy as Configuration class:
|
|
512
|
-
# - Arrays (tools, delegates_to): Concatenate (all_agents + file)
|
|
513
|
-
# - Hashes (parameters, headers): Merge (file values override)
|
|
514
|
-
# - Scalars (model, provider, etc.): File overrides
|
|
515
|
-
#
|
|
516
|
-
# @param all_agents_hash [Hash] All_agents configuration
|
|
517
|
-
# @param file_config [Hash] File-loaded agent configuration
|
|
518
|
-
# @return [Hash] Merged configuration
|
|
519
|
-
def merge_all_agents_into_config(all_agents_hash, file_config)
|
|
520
|
-
merged = all_agents_hash.dup
|
|
521
|
-
|
|
522
|
-
file_config.each do |key, value|
|
|
523
|
-
case key
|
|
524
|
-
when :tools
|
|
525
|
-
# Concatenate tools: all_agents.tools + file.tools
|
|
526
|
-
merged[:tools] = Array(merged[:tools]) + Array(value)
|
|
527
|
-
when :delegates_to
|
|
528
|
-
# Concatenate delegates_to
|
|
529
|
-
merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
|
|
530
|
-
when :parameters
|
|
531
|
-
# Merge parameters: file values override all_agents
|
|
532
|
-
merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
|
|
533
|
-
when :headers
|
|
534
|
-
# Merge headers: file values override all_agents
|
|
535
|
-
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
|
536
|
-
else
|
|
537
|
-
# For everything else, file value overrides all_agents value
|
|
538
|
-
merged[key] = value
|
|
539
|
-
end
|
|
540
|
-
end
|
|
541
|
-
|
|
542
|
-
# Pass all_agents permissions as default_permissions
|
|
543
|
-
if all_agents_hash[:permissions] && !merged[:default_permissions]
|
|
544
|
-
merged[:default_permissions] = all_agents_hash[:permissions]
|
|
545
|
-
end
|
|
546
|
-
|
|
547
|
-
merged
|
|
548
|
-
end
|
|
549
|
-
|
|
550
|
-
# Apply all_agents defaults to an agent builder
|
|
551
|
-
#
|
|
552
|
-
# Only sets values that haven't been explicitly set at the agent level.
|
|
553
|
-
# This implements the override semantics: agent values take precedence.
|
|
554
|
-
#
|
|
555
|
-
# @param agent_builder [Agent::Builder] The agent builder to configure
|
|
556
|
-
# @param all_agents_hash [Hash] All_agents configuration
|
|
557
|
-
# @return [void]
|
|
558
|
-
def apply_all_agents_defaults(agent_builder, all_agents_hash)
|
|
559
|
-
# Model: only set if agent hasn't explicitly set it
|
|
560
|
-
if all_agents_hash[:model] && !agent_builder.model_set?
|
|
561
|
-
agent_builder.model(all_agents_hash[:model])
|
|
562
|
-
end
|
|
563
|
-
|
|
564
|
-
# Provider: only set if agent hasn't set it
|
|
565
|
-
if all_agents_hash[:provider] && !agent_builder.provider_set?
|
|
566
|
-
agent_builder.provider(all_agents_hash[:provider])
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
# Base URL: only set if agent hasn't set it
|
|
570
|
-
if all_agents_hash[:base_url] && !agent_builder.base_url_set?
|
|
571
|
-
agent_builder.base_url(all_agents_hash[:base_url])
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
# API Version: only set if agent hasn't set it
|
|
575
|
-
if all_agents_hash[:api_version] && !agent_builder.api_version_set?
|
|
576
|
-
agent_builder.api_version(all_agents_hash[:api_version])
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
# Timeout: only set if agent hasn't set it
|
|
580
|
-
if all_agents_hash[:timeout] && !agent_builder.timeout_set?
|
|
581
|
-
agent_builder.timeout(all_agents_hash[:timeout])
|
|
582
|
-
end
|
|
583
|
-
|
|
584
|
-
# Parameters: merge (all_agents + agent, agent values override)
|
|
585
|
-
if all_agents_hash[:parameters]
|
|
586
|
-
merged_params = all_agents_hash[:parameters].merge(agent_builder.parameters)
|
|
587
|
-
agent_builder.parameters(merged_params)
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
# Headers: merge (all_agents + agent, agent values override)
|
|
591
|
-
if all_agents_hash[:headers]
|
|
592
|
-
merged_headers = all_agents_hash[:headers].merge(agent_builder.headers)
|
|
593
|
-
agent_builder.headers(merged_headers)
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
# Coding_agent: only set if agent hasn't set it
|
|
597
|
-
if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
|
|
598
|
-
agent_builder.coding_agent(all_agents_hash[:coding_agent])
|
|
599
|
-
end
|
|
600
|
-
end
|
|
601
|
-
|
|
602
181
|
def apply_swarm_hook(swarm, config)
|
|
603
182
|
event = config[:event]
|
|
604
183
|
|
|
@@ -644,7 +223,7 @@ module SwarmSDK
|
|
|
644
223
|
end
|
|
645
224
|
|
|
646
225
|
def build_hook_input(context, event)
|
|
647
|
-
# Build JSON input for shell hooks
|
|
226
|
+
# Build JSON input for shell hooks
|
|
648
227
|
base = { event: event.to_s }
|
|
649
228
|
|
|
650
229
|
case event
|
|
@@ -662,122 +241,9 @@ module SwarmSDK
|
|
|
662
241
|
base
|
|
663
242
|
end
|
|
664
243
|
end
|
|
665
|
-
|
|
666
|
-
# Validate all_agents filesystem tools
|
|
667
|
-
#
|
|
668
|
-
# Raises ConfigurationError if filesystem tools are globally disabled
|
|
669
|
-
# but all_agents configuration includes them.
|
|
670
|
-
#
|
|
671
|
-
# @raise [ConfigurationError] If filesystem tools are disabled and all_agents has them
|
|
672
|
-
# @return [void]
|
|
673
|
-
def validate_all_agents_filesystem_tools
|
|
674
|
-
# Resolve the effective setting
|
|
675
|
-
resolved_setting = if @allow_filesystem_tools.nil?
|
|
676
|
-
SwarmSDK.settings.allow_filesystem_tools
|
|
677
|
-
else
|
|
678
|
-
@allow_filesystem_tools
|
|
679
|
-
end
|
|
680
|
-
|
|
681
|
-
return if resolved_setting # If true, allow everything
|
|
682
|
-
return unless @all_agents_config&.tools_list&.any?
|
|
683
|
-
|
|
684
|
-
forbidden = @all_agents_config.tools_list.select do |tool|
|
|
685
|
-
SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool.to_sym)
|
|
686
|
-
end
|
|
687
|
-
|
|
688
|
-
return if forbidden.empty?
|
|
689
|
-
|
|
690
|
-
raise ConfigurationError,
|
|
691
|
-
"Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
|
|
692
|
-
"but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
|
|
693
|
-
"This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
|
|
694
|
-
"To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
|
|
695
|
-
end
|
|
696
|
-
|
|
697
|
-
# Validate individual agent filesystem tools
|
|
698
|
-
#
|
|
699
|
-
# Raises ConfigurationError if filesystem tools are globally disabled
|
|
700
|
-
# but any agent attempts to use them.
|
|
701
|
-
#
|
|
702
|
-
# @raise [ConfigurationError] If filesystem tools are disabled and any agent has them
|
|
703
|
-
# @return [void]
|
|
704
|
-
def validate_agent_filesystem_tools
|
|
705
|
-
# Resolve the effective setting
|
|
706
|
-
resolved_setting = if @allow_filesystem_tools.nil?
|
|
707
|
-
SwarmSDK.settings.allow_filesystem_tools
|
|
708
|
-
else
|
|
709
|
-
@allow_filesystem_tools
|
|
710
|
-
end
|
|
711
|
-
|
|
712
|
-
return if resolved_setting # If true, allow everything
|
|
713
|
-
|
|
714
|
-
# Check each agent for forbidden tools
|
|
715
|
-
@agents.each do |agent_name, agent_builder_or_config|
|
|
716
|
-
# Extract tool list from either Builder or file config
|
|
717
|
-
tools_list = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
|
|
718
|
-
# File-loaded agent
|
|
719
|
-
agent_builder_or_config[:__file_config__][:tools] || []
|
|
720
|
-
elsif agent_builder_or_config.is_a?(Agent::Builder)
|
|
721
|
-
# Builder object - use tools_list method
|
|
722
|
-
agent_builder_or_config.tools_list
|
|
723
|
-
else
|
|
724
|
-
[]
|
|
725
|
-
end
|
|
726
|
-
|
|
727
|
-
# Extract tool names (they might be hashes with permissions) and convert to symbols
|
|
728
|
-
tool_names = tools_list.map do |tool|
|
|
729
|
-
name = tool.is_a?(Hash) ? tool[:name] : tool
|
|
730
|
-
name.to_sym
|
|
731
|
-
end
|
|
732
|
-
|
|
733
|
-
# Find forbidden tools
|
|
734
|
-
forbidden = tool_names.select do |tool|
|
|
735
|
-
SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool)
|
|
736
|
-
end
|
|
737
|
-
|
|
738
|
-
next if forbidden.empty?
|
|
739
|
-
|
|
740
|
-
raise ConfigurationError,
|
|
741
|
-
"Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
|
|
742
|
-
"but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
|
|
743
|
-
"This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
|
|
744
|
-
"To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
|
|
745
|
-
end
|
|
746
|
-
end
|
|
747
244
|
end
|
|
748
245
|
|
|
749
|
-
# Helper class for swarms block in DSL
|
|
750
|
-
#
|
|
751
|
-
# Provides a clean API for registering external swarms within the swarms { } block.
|
|
752
|
-
# Supports three registration methods:
|
|
753
|
-
# 1. File path: register "name", file: "./swarm.rb"
|
|
754
|
-
# 2. YAML string: register "name", yaml: "version: 2\n..."
|
|
755
|
-
# 3. Inline block: register "name" do ... end
|
|
756
|
-
#
|
|
757
|
-
# @example From file
|
|
758
|
-
# swarms do
|
|
759
|
-
# register "code_review", file: "./swarms/code_review.rb"
|
|
760
|
-
# end
|
|
761
|
-
#
|
|
762
|
-
# @example From YAML string
|
|
763
|
-
# swarms do
|
|
764
|
-
# yaml_content = File.read("testing.yml")
|
|
765
|
-
# register "testing", yaml: yaml_content, keep_context: false
|
|
766
|
-
# end
|
|
767
|
-
#
|
|
768
|
-
# @example Inline block
|
|
769
|
-
# swarms do
|
|
770
|
-
# register "testing", keep_context: false do
|
|
771
|
-
# id "testing_team"
|
|
772
|
-
# name "Testing Team"
|
|
773
|
-
# lead :tester
|
|
774
|
-
# agent :tester do
|
|
775
|
-
# model "gpt-4o-mini"
|
|
776
|
-
# system "You test code"
|
|
777
|
-
# end
|
|
778
|
-
# end
|
|
779
|
-
# end
|
|
780
|
-
#
|
|
781
|
-
# NOTE: SwarmRegistryBuilder is now in swarm_registry_builder.rb for Zeitwerk
|
|
246
|
+
# Helper class for swarms block in DSL (kept in this file for reference)
|
|
247
|
+
# Actual implementation is in swarm_registry_builder.rb for Zeitwerk
|
|
782
248
|
end
|
|
783
249
|
end
|