claude_swarm 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +3 -0
- data/CLAUDE.md +0 -1
- data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +12 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +139 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +249 -1
- data/docs/v2/README.md +15 -5
- data/docs/v2/guides/complete-tutorial.md +93 -7
- data/docs/v2/guides/getting-started.md +3 -1
- data/docs/v2/guides/memory-adapters.md +41 -0
- data/docs/v2/guides/{migrating-to-2.3.md → migrating-to-2.x.md} +213 -8
- data/docs/v2/guides/plugins.md +52 -5
- data/docs/v2/guides/rails-integration.md +6 -0
- data/docs/v2/guides/swarm-memory.md +2 -13
- data/docs/v2/reference/cli.md +0 -1
- data/docs/v2/reference/configuration_reference.md +300 -0
- data/docs/v2/reference/event_payload_structures.md +26 -4
- data/docs/v2/reference/ruby-dsl.md +457 -4
- data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
- data/docs/v2/reference/yaml.md +2 -2
- data/lib/claude_swarm/mcp_generator.rb +1 -1
- data/lib/claude_swarm/orchestrator.rb +8 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/core/semantic_index.rb +10 -2
- data/lib/swarm_memory/core/storage.rb +7 -2
- data/lib/swarm_memory/dsl/memory_config.rb +37 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +120 -27
- data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
- data/lib/swarm_memory/tools/load_skill.rb +0 -1
- data/lib/swarm_memory/tools/memory_edit.rb +2 -1
- data/lib/swarm_memory/tools/memory_read.rb +1 -1
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +7 -5
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +4 -0
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +38 -4
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +2 -2
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +48 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +3 -3
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +1 -1
- data/lib/swarm_sdk/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +91 -12
- data/lib/swarm_sdk/config.rb +302 -0
- data/lib/swarm_sdk/configuration/parser.rb +22 -2
- data/lib/swarm_sdk/configuration.rb +13 -4
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -3
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/models.rb +43 -2
- data/lib/swarm_sdk/plugin.rb +2 -2
- data/lib/swarm_sdk/result.rb +52 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +1 -1
- data/lib/swarm_sdk/swarm/hook_triggers.rb +1 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +18 -4
- data/lib/swarm_sdk/swarm.rb +76 -13
- data/lib/swarm_sdk/tools/bash.rb +7 -9
- data/lib/swarm_sdk/tools/glob.rb +5 -5
- data/lib/swarm_sdk/tools/read.rb +8 -8
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +4 -3
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -18
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +49 -0
- data/lib/swarm_sdk/workflow/node_builder.rb +4 -2
- data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
- data/lib/swarm_sdk.rb +261 -105
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +8 -3
- data/swarm_sdk.gemspec +4 -4
- data/team_full.yml +104 -300
- metadata +9 -5
- data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
- /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Global registry for reusable agent definitions
|
|
5
|
+
#
|
|
6
|
+
# AgentRegistry allows declaring agents in separate files that can be
|
|
7
|
+
# referenced by name in swarm definitions. This promotes code reuse and
|
|
8
|
+
# separation of concerns - agent definitions can live in dedicated files
|
|
9
|
+
# while swarm configurations compose them together.
|
|
10
|
+
#
|
|
11
|
+
# ## Usage
|
|
12
|
+
#
|
|
13
|
+
# Register agents globally (typically in separate files):
|
|
14
|
+
#
|
|
15
|
+
# # agents/backend.rb
|
|
16
|
+
# SwarmSDK.agent :backend do
|
|
17
|
+
# model "claude-sonnet-4"
|
|
18
|
+
# description "Backend API developer"
|
|
19
|
+
# system_prompt "You build REST APIs"
|
|
20
|
+
# tools :Read, :Edit, :Bash
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Reference registered agents in swarm definitions:
|
|
24
|
+
#
|
|
25
|
+
# # swarm.rb
|
|
26
|
+
# SwarmSDK.build do
|
|
27
|
+
# name "Dev Team"
|
|
28
|
+
# lead :backend
|
|
29
|
+
#
|
|
30
|
+
# agent :backend # Pulls from registry
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# ## Override Support
|
|
34
|
+
#
|
|
35
|
+
# Registered agents can be extended with additional configuration:
|
|
36
|
+
#
|
|
37
|
+
# SwarmSDK.build do
|
|
38
|
+
# name "Dev Team"
|
|
39
|
+
# lead :backend
|
|
40
|
+
#
|
|
41
|
+
# agent :backend do
|
|
42
|
+
# # Registry config is applied first, then this block
|
|
43
|
+
# tools :CustomTool # Adds to tools from registry
|
|
44
|
+
# delegates_to :database
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# @note This registry is not thread-safe. In multi-threaded environments,
|
|
49
|
+
# register all agents before spawning threads, or synchronize access
|
|
50
|
+
# externally. For typical fiber-based async usage (the default in SwarmSDK),
|
|
51
|
+
# this is not a concern.
|
|
52
|
+
#
|
|
53
|
+
class AgentRegistry
|
|
54
|
+
@agents = {}
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
# Register an agent definition block
|
|
58
|
+
#
|
|
59
|
+
# Stores a configuration block that will be executed when the agent
|
|
60
|
+
# is referenced in a swarm definition. The block receives an
|
|
61
|
+
# Agent::Builder context and can use all builder DSL methods.
|
|
62
|
+
#
|
|
63
|
+
# @param name [Symbol, String] Agent name (will be symbolized)
|
|
64
|
+
# @yield Agent configuration block using Agent::Builder DSL
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [ArgumentError] If no block is provided
|
|
67
|
+
# @raise [ArgumentError] If agent with same name is already registered
|
|
68
|
+
#
|
|
69
|
+
# @example Register a backend agent
|
|
70
|
+
# SwarmSDK::AgentRegistry.register(:backend) do
|
|
71
|
+
# model "claude-sonnet-4"
|
|
72
|
+
# description "Backend developer"
|
|
73
|
+
# tools :Read, :Edit, :Bash
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @example Register with MCP servers
|
|
77
|
+
# SwarmSDK::AgentRegistry.register(:filesystem_agent) do
|
|
78
|
+
# model "gpt-4"
|
|
79
|
+
# description "File manager"
|
|
80
|
+
# mcp_server :fs, type: :stdio, command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
|
81
|
+
# end
|
|
82
|
+
def register(name, &block)
|
|
83
|
+
raise ArgumentError, "Block required for agent registration" unless block_given?
|
|
84
|
+
|
|
85
|
+
sym_name = name.to_sym
|
|
86
|
+
if @agents.key?(sym_name)
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
"Agent '#{sym_name}' is already registered. " \
|
|
89
|
+
"Use SwarmSDK.clear_agent_registry! to reset, or choose a different name."
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@agents[sym_name] = block
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Retrieve a registered agent block
|
|
96
|
+
#
|
|
97
|
+
# @param name [Symbol, String] Agent name
|
|
98
|
+
# @return [Proc, nil] The registration block or nil if not found
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# block = SwarmSDK::AgentRegistry.get(:backend)
|
|
102
|
+
# builder.instance_eval(&block) if block
|
|
103
|
+
def get(name)
|
|
104
|
+
@agents[name.to_sym]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if an agent is registered
|
|
108
|
+
#
|
|
109
|
+
# @param name [Symbol, String] Agent name
|
|
110
|
+
# @return [Boolean] true if agent is registered
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# if SwarmSDK::AgentRegistry.registered?(:backend)
|
|
114
|
+
# puts "Backend agent is available"
|
|
115
|
+
# end
|
|
116
|
+
def registered?(name)
|
|
117
|
+
@agents.key?(name.to_sym)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# List all registered agent names
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<Symbol>] Names of all registered agents
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# SwarmSDK::AgentRegistry.names
|
|
126
|
+
# # => [:backend, :frontend, :database]
|
|
127
|
+
def names
|
|
128
|
+
@agents.keys
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Clear all registrations
|
|
132
|
+
#
|
|
133
|
+
# Primarily useful for testing to ensure clean state between tests.
|
|
134
|
+
#
|
|
135
|
+
# @return [void]
|
|
136
|
+
#
|
|
137
|
+
# @example In test setup/teardown
|
|
138
|
+
# def teardown
|
|
139
|
+
# SwarmSDK::AgentRegistry.clear
|
|
140
|
+
# end
|
|
141
|
+
def clear
|
|
142
|
+
@agents.clear
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -64,11 +64,14 @@ module SwarmSDK
|
|
|
64
64
|
@swarm_registry_config = builder.registrations
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
# Define an agent with fluent API
|
|
67
|
+
# Define an agent with fluent API, load from markdown, or reference registry
|
|
68
68
|
#
|
|
69
|
-
# Supports
|
|
70
|
-
# 1.
|
|
71
|
-
# 2.
|
|
69
|
+
# Supports multiple forms:
|
|
70
|
+
# 1. Registry lookup: agent :name (pulls from global registry)
|
|
71
|
+
# 2. Registry + overrides: agent :name do ... end (when registered)
|
|
72
|
+
# 3. Inline DSL: agent :name do ... end (when not registered)
|
|
73
|
+
# 4. Markdown content: agent :name, <<~MD ... MD
|
|
74
|
+
# 5. Markdown + overrides: agent :name, <<~MD do ... end
|
|
72
75
|
#
|
|
73
76
|
# @example Inline DSL
|
|
74
77
|
# agent :backend do
|
|
@@ -77,6 +80,15 @@ module SwarmSDK
|
|
|
77
80
|
# tools :Read, :Write
|
|
78
81
|
# end
|
|
79
82
|
#
|
|
83
|
+
# @example Registry lookup (agent must be registered with SwarmSDK.agent)
|
|
84
|
+
# agent :backend # Pulls configuration from registry
|
|
85
|
+
#
|
|
86
|
+
# @example Registry + overrides
|
|
87
|
+
# agent :backend do
|
|
88
|
+
# # Base config from registry, then apply overrides
|
|
89
|
+
# tools :CustomTool # Adds to registry-defined tools
|
|
90
|
+
# end
|
|
91
|
+
#
|
|
80
92
|
# @example Markdown content
|
|
81
93
|
# agent :backend, <<~MD
|
|
82
94
|
# ---
|
|
@@ -87,19 +99,38 @@ module SwarmSDK
|
|
|
87
99
|
# You build APIs.
|
|
88
100
|
# MD
|
|
89
101
|
def agent(name, content = nil, &block)
|
|
102
|
+
name = name.to_sym
|
|
103
|
+
|
|
90
104
|
# Case 1: agent :name, <<~MD do ... end (markdown + overrides)
|
|
91
105
|
if content.is_a?(String) && block_given? && markdown_content?(content)
|
|
92
106
|
load_agent_from_markdown_with_overrides(content, name, &block)
|
|
107
|
+
|
|
93
108
|
# Case 2: agent :name, <<~MD (markdown only)
|
|
94
109
|
elsif content.is_a?(String) && !block_given? && markdown_content?(content)
|
|
95
110
|
load_agent_from_markdown(content, name)
|
|
96
|
-
|
|
111
|
+
|
|
112
|
+
# Case 3: agent :name (registry lookup only - no content, no block)
|
|
113
|
+
elsif content.nil? && !block_given?
|
|
114
|
+
load_agent_from_registry(name)
|
|
115
|
+
|
|
116
|
+
# Case 4: agent :name do ... end (with registered agent - registry + overrides)
|
|
117
|
+
elsif content.nil? && block_given? && AgentRegistry.registered?(name)
|
|
118
|
+
load_agent_from_registry_with_overrides(name, &block)
|
|
119
|
+
|
|
120
|
+
# Case 5: agent :name do ... end (inline DSL - not registered)
|
|
97
121
|
elsif block_given?
|
|
98
122
|
builder = Agent::Builder.new(name)
|
|
99
123
|
builder.instance_eval(&block)
|
|
100
124
|
@agents[name] = builder
|
|
125
|
+
|
|
101
126
|
else
|
|
102
|
-
raise ArgumentError,
|
|
127
|
+
raise ArgumentError,
|
|
128
|
+
"Invalid agent definition for '#{name}'. Use:\n " \
|
|
129
|
+
"agent :#{name} { ... } # Inline DSL\n " \
|
|
130
|
+
"agent :#{name} # Registry lookup\n " \
|
|
131
|
+
"agent :#{name} { ... } # Registry + overrides (if registered)\n " \
|
|
132
|
+
"agent :#{name}, <<~MD ... MD # Markdown\n " \
|
|
133
|
+
"agent :#{name}, <<~MD do ... end # Markdown + overrides"
|
|
103
134
|
end
|
|
104
135
|
end
|
|
105
136
|
|
|
@@ -138,6 +169,54 @@ module SwarmSDK
|
|
|
138
169
|
str.start_with?("---") || str.include?("\n---\n")
|
|
139
170
|
end
|
|
140
171
|
|
|
172
|
+
# Load an agent from the global registry
|
|
173
|
+
#
|
|
174
|
+
# Retrieves the registered agent block and executes it in the context
|
|
175
|
+
# of a new Agent::Builder.
|
|
176
|
+
#
|
|
177
|
+
# @param name [Symbol] Agent name
|
|
178
|
+
# @return [void]
|
|
179
|
+
# @raise [ConfigurationError] If agent is not registered
|
|
180
|
+
#
|
|
181
|
+
# @example
|
|
182
|
+
# load_agent_from_registry(:backend)
|
|
183
|
+
def load_agent_from_registry(name)
|
|
184
|
+
registered_proc = AgentRegistry.get(name)
|
|
185
|
+
unless registered_proc
|
|
186
|
+
raise ConfigurationError,
|
|
187
|
+
"Agent '#{name}' not found in registry. " \
|
|
188
|
+
"Either define inline with `agent :#{name} do ... end` or " \
|
|
189
|
+
"register globally with `SwarmSDK.agent :#{name} do ... end`"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
builder = Agent::Builder.new(name)
|
|
193
|
+
builder.instance_eval(®istered_proc)
|
|
194
|
+
@agents[name] = builder
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Load an agent from the registry with additional overrides
|
|
198
|
+
#
|
|
199
|
+
# Applies the registered configuration first, then executes the
|
|
200
|
+
# override block to customize the agent.
|
|
201
|
+
#
|
|
202
|
+
# @param name [Symbol] Agent name
|
|
203
|
+
# @yield Override block with additional configuration
|
|
204
|
+
# @return [void]
|
|
205
|
+
#
|
|
206
|
+
# @example
|
|
207
|
+
# load_agent_from_registry_with_overrides(:backend) do
|
|
208
|
+
# tools :CustomTool # Adds to registry-defined tools
|
|
209
|
+
# end
|
|
210
|
+
def load_agent_from_registry_with_overrides(name, &override_block)
|
|
211
|
+
registered_proc = AgentRegistry.get(name)
|
|
212
|
+
# Guaranteed to exist since we checked in the condition
|
|
213
|
+
|
|
214
|
+
builder = Agent::Builder.new(name)
|
|
215
|
+
builder.instance_eval(®istered_proc) # Base config from registry
|
|
216
|
+
builder.instance_eval(&override_block) # Apply overrides
|
|
217
|
+
@agents[name] = builder
|
|
218
|
+
end
|
|
219
|
+
|
|
141
220
|
# Load an agent from markdown content
|
|
142
221
|
#
|
|
143
222
|
# Returns a hash of the agent config (not a Definition yet) so that
|
|
@@ -318,7 +397,7 @@ module SwarmSDK
|
|
|
318
397
|
# @return [void]
|
|
319
398
|
def validate_all_agents_filesystem_tools
|
|
320
399
|
resolved_setting = if @allow_filesystem_tools.nil?
|
|
321
|
-
SwarmSDK.
|
|
400
|
+
SwarmSDK.config.allow_filesystem_tools
|
|
322
401
|
else
|
|
323
402
|
@allow_filesystem_tools
|
|
324
403
|
end
|
|
@@ -333,10 +412,10 @@ module SwarmSDK
|
|
|
333
412
|
return if forbidden.empty?
|
|
334
413
|
|
|
335
414
|
raise ConfigurationError,
|
|
336
|
-
"Filesystem tools are globally disabled (SwarmSDK.
|
|
415
|
+
"Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
|
|
337
416
|
"but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
|
|
338
417
|
"This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
|
|
339
|
-
"To use filesystem tools, set SwarmSDK.
|
|
418
|
+
"To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
|
|
340
419
|
end
|
|
341
420
|
|
|
342
421
|
# Validate individual agent filesystem tools
|
|
@@ -345,7 +424,7 @@ module SwarmSDK
|
|
|
345
424
|
# @return [void]
|
|
346
425
|
def validate_agent_filesystem_tools
|
|
347
426
|
resolved_setting = if @allow_filesystem_tools.nil?
|
|
348
|
-
SwarmSDK.
|
|
427
|
+
SwarmSDK.config.allow_filesystem_tools
|
|
349
428
|
else
|
|
350
429
|
@allow_filesystem_tools
|
|
351
430
|
end
|
|
@@ -373,10 +452,10 @@ module SwarmSDK
|
|
|
373
452
|
next if forbidden.empty?
|
|
374
453
|
|
|
375
454
|
raise ConfigurationError,
|
|
376
|
-
"Filesystem tools are globally disabled (SwarmSDK.
|
|
455
|
+
"Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
|
|
377
456
|
"but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
|
|
378
457
|
"This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
|
|
379
|
-
"To use filesystem tools, set SwarmSDK.
|
|
458
|
+
"To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
|
|
380
459
|
end
|
|
381
460
|
end
|
|
382
461
|
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Centralized configuration for SwarmSDK
|
|
5
|
+
#
|
|
6
|
+
# Config provides a single entry point for all SwarmSDK configuration,
|
|
7
|
+
# including API keys (proxied to RubyLLM), defaults override, and
|
|
8
|
+
# WebFetch settings.
|
|
9
|
+
#
|
|
10
|
+
# ## Priority Order
|
|
11
|
+
#
|
|
12
|
+
# Configuration values are resolved in this order:
|
|
13
|
+
# 1. Explicit value (set via SwarmSDK.configure)
|
|
14
|
+
# 2. Environment variable
|
|
15
|
+
# 3. Module default (from SwarmSDK::Defaults)
|
|
16
|
+
#
|
|
17
|
+
# ## API Key Proxying
|
|
18
|
+
#
|
|
19
|
+
# API keys are automatically proxied to RubyLLM.config when set,
|
|
20
|
+
# ensuring RubyLLM always has the correct credentials.
|
|
21
|
+
#
|
|
22
|
+
# @example Basic configuration
|
|
23
|
+
# SwarmSDK.configure do |config|
|
|
24
|
+
# config.openai_api_key = "sk-..."
|
|
25
|
+
# config.default_model = "claude-sonnet-4"
|
|
26
|
+
# config.agent_request_timeout = 600
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @example Testing setup
|
|
30
|
+
# def setup
|
|
31
|
+
# SwarmSDK.reset_config!
|
|
32
|
+
# end
|
|
33
|
+
class Config
|
|
34
|
+
# API keys that proxy to RubyLLM.config
|
|
35
|
+
# Maps SwarmSDK config key => [RubyLLM config key, ENV variable]
|
|
36
|
+
API_KEY_MAPPINGS = {
|
|
37
|
+
openai_api_key: [:openai_api_key, "OPENAI_API_KEY"],
|
|
38
|
+
openai_api_base: [:openai_api_base, "OPENAI_API_BASE"],
|
|
39
|
+
openai_organization_id: [:openai_organization_id, "OPENAI_ORG_ID"],
|
|
40
|
+
openai_project_id: [:openai_project_id, "OPENAI_PROJECT_ID"],
|
|
41
|
+
anthropic_api_key: [:anthropic_api_key, "ANTHROPIC_API_KEY"],
|
|
42
|
+
gemini_api_key: [:gemini_api_key, "GEMINI_API_KEY"],
|
|
43
|
+
gemini_api_base: [:gemini_api_base, "GEMINI_API_BASE"],
|
|
44
|
+
vertexai_project_id: [:vertexai_project_id, "GOOGLE_CLOUD_PROJECT"],
|
|
45
|
+
vertexai_location: [:vertexai_location, "GOOGLE_CLOUD_LOCATION"],
|
|
46
|
+
deepseek_api_key: [:deepseek_api_key, "DEEPSEEK_API_KEY"],
|
|
47
|
+
mistral_api_key: [:mistral_api_key, "MISTRAL_API_KEY"],
|
|
48
|
+
perplexity_api_key: [:perplexity_api_key, "PERPLEXITY_API_KEY"],
|
|
49
|
+
openrouter_api_key: [:openrouter_api_key, "OPENROUTER_API_KEY"],
|
|
50
|
+
bedrock_api_key: [:bedrock_api_key, "AWS_ACCESS_KEY_ID"],
|
|
51
|
+
bedrock_secret_key: [:bedrock_secret_key, "AWS_SECRET_ACCESS_KEY"],
|
|
52
|
+
bedrock_region: [:bedrock_region, "AWS_REGION"],
|
|
53
|
+
bedrock_session_token: [:bedrock_session_token, "AWS_SESSION_TOKEN"],
|
|
54
|
+
ollama_api_base: [:ollama_api_base, "OLLAMA_API_BASE"],
|
|
55
|
+
gpustack_api_base: [:gpustack_api_base, "GPUSTACK_API_BASE"],
|
|
56
|
+
gpustack_api_key: [:gpustack_api_key, "GPUSTACK_API_KEY"],
|
|
57
|
+
}.freeze
|
|
58
|
+
|
|
59
|
+
# SwarmSDK defaults that can be overridden
|
|
60
|
+
# Maps config key => [ENV variable, default proc]
|
|
61
|
+
DEFAULTS_MAPPINGS = {
|
|
62
|
+
default_model: ["SWARM_SDK_DEFAULT_MODEL", -> { Defaults::Agent::MODEL }],
|
|
63
|
+
default_provider: ["SWARM_SDK_DEFAULT_PROVIDER", -> { Defaults::Agent::PROVIDER }],
|
|
64
|
+
agent_request_timeout: ["SWARM_SDK_AGENT_REQUEST_TIMEOUT", -> { Defaults::Timeouts::AGENT_REQUEST_SECONDS }],
|
|
65
|
+
bash_command_timeout: ["SWARM_SDK_BASH_COMMAND_TIMEOUT", -> { Defaults::Timeouts::BASH_COMMAND_MS }],
|
|
66
|
+
bash_command_max_timeout: ["SWARM_SDK_BASH_COMMAND_MAX_TIMEOUT", -> { Defaults::Timeouts::BASH_COMMAND_MAX_MS }],
|
|
67
|
+
web_fetch_timeout: ["SWARM_SDK_WEB_FETCH_TIMEOUT", -> { Defaults::Timeouts::WEB_FETCH_SECONDS }],
|
|
68
|
+
hook_shell_timeout: ["SWARM_SDK_HOOK_SHELL_TIMEOUT", -> { Defaults::Timeouts::HOOK_SHELL_SECONDS }],
|
|
69
|
+
transformer_command_timeout: ["SWARM_SDK_TRANSFORMER_COMMAND_TIMEOUT", -> { Defaults::Timeouts::TRANSFORMER_COMMAND_SECONDS }],
|
|
70
|
+
global_concurrency_limit: ["SWARM_SDK_GLOBAL_CONCURRENCY_LIMIT", -> { Defaults::Concurrency::GLOBAL_LIMIT }],
|
|
71
|
+
local_concurrency_limit: ["SWARM_SDK_LOCAL_CONCURRENCY_LIMIT", -> { Defaults::Concurrency::LOCAL_LIMIT }],
|
|
72
|
+
output_character_limit: ["SWARM_SDK_OUTPUT_CHARACTER_LIMIT", -> { Defaults::Limits::OUTPUT_CHARACTERS }],
|
|
73
|
+
read_line_limit: ["SWARM_SDK_READ_LINE_LIMIT", -> { Defaults::Limits::READ_LINES }],
|
|
74
|
+
line_character_limit: ["SWARM_SDK_LINE_CHARACTER_LIMIT", -> { Defaults::Limits::LINE_CHARACTERS }],
|
|
75
|
+
web_fetch_character_limit: ["SWARM_SDK_WEB_FETCH_CHARACTER_LIMIT", -> { Defaults::Limits::WEB_FETCH_CHARACTERS }],
|
|
76
|
+
glob_result_limit: ["SWARM_SDK_GLOB_RESULT_LIMIT", -> { Defaults::Limits::GLOB_RESULTS }],
|
|
77
|
+
scratchpad_entry_size_limit: ["SWARM_SDK_SCRATCHPAD_ENTRY_SIZE_LIMIT", -> { Defaults::Storage::ENTRY_SIZE_BYTES }],
|
|
78
|
+
scratchpad_total_size_limit: ["SWARM_SDK_SCRATCHPAD_TOTAL_SIZE_LIMIT", -> { Defaults::Storage::TOTAL_SIZE_BYTES }],
|
|
79
|
+
context_compression_threshold: ["SWARM_SDK_CONTEXT_COMPRESSION_THRESHOLD", -> { Defaults::Context::COMPRESSION_THRESHOLD_PERCENT }],
|
|
80
|
+
todowrite_reminder_interval: ["SWARM_SDK_TODOWRITE_REMINDER_INTERVAL", -> { Defaults::Context::TODOWRITE_REMINDER_INTERVAL }],
|
|
81
|
+
chars_per_token_prose: ["SWARM_SDK_CHARS_PER_TOKEN_PROSE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE }],
|
|
82
|
+
chars_per_token_code: ["SWARM_SDK_CHARS_PER_TOKEN_CODE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE }],
|
|
83
|
+
mcp_log_level: ["SWARM_SDK_MCP_LOG_LEVEL", -> { Defaults::Logging::MCP_LOG_LEVEL }],
|
|
84
|
+
}.freeze
|
|
85
|
+
|
|
86
|
+
# WebFetch and control settings
|
|
87
|
+
# Maps config key => [ENV variable, default value]
|
|
88
|
+
SETTINGS_MAPPINGS = {
|
|
89
|
+
webfetch_provider: ["SWARM_SDK_WEBFETCH_PROVIDER", nil],
|
|
90
|
+
webfetch_model: ["SWARM_SDK_WEBFETCH_MODEL", nil],
|
|
91
|
+
webfetch_base_url: ["SWARM_SDK_WEBFETCH_BASE_URL", nil],
|
|
92
|
+
webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
|
|
93
|
+
allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
|
|
94
|
+
env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
|
|
95
|
+
}.freeze
|
|
96
|
+
|
|
97
|
+
class << self
|
|
98
|
+
# Get the singleton Config instance
|
|
99
|
+
#
|
|
100
|
+
# @return [Config] The singleton instance
|
|
101
|
+
def instance
|
|
102
|
+
@instance ||= new
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reset the Config instance
|
|
106
|
+
#
|
|
107
|
+
# Clears all configuration including explicit values and cached ENV values.
|
|
108
|
+
# Use in tests to ensure clean state.
|
|
109
|
+
#
|
|
110
|
+
# @return [void]
|
|
111
|
+
def reset!
|
|
112
|
+
@instance = nil
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Initialize a new Config instance
|
|
117
|
+
#
|
|
118
|
+
# @note Use Config.instance instead of new for the singleton pattern
|
|
119
|
+
def initialize
|
|
120
|
+
@explicit_values = {}
|
|
121
|
+
@env_values = {}
|
|
122
|
+
@env_loaded = false
|
|
123
|
+
@env_mutex = Mutex.new
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# ========== API Key Accessors (with RubyLLM proxying) ==========
|
|
127
|
+
|
|
128
|
+
# @!method openai_api_key
|
|
129
|
+
# Get the OpenAI API key
|
|
130
|
+
# @return [String, nil] The API key
|
|
131
|
+
#
|
|
132
|
+
# @!method openai_api_key=(value)
|
|
133
|
+
# Set the OpenAI API key (proxied to RubyLLM)
|
|
134
|
+
# @param value [String] The API key
|
|
135
|
+
|
|
136
|
+
API_KEY_MAPPINGS.each_key do |config_key|
|
|
137
|
+
ruby_llm_key, _ = API_KEY_MAPPINGS[config_key]
|
|
138
|
+
|
|
139
|
+
# Getter
|
|
140
|
+
define_method(config_key) do
|
|
141
|
+
ensure_env_loaded!
|
|
142
|
+
@explicit_values[config_key] || @env_values[config_key]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Setter with RubyLLM proxying
|
|
146
|
+
define_method("#{config_key}=") do |value|
|
|
147
|
+
@explicit_values[config_key] = value
|
|
148
|
+
RubyLLM.config.public_send("#{ruby_llm_key}=", value) if value
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# ========== Defaults Accessors (with module constant fallback) ==========
|
|
153
|
+
|
|
154
|
+
# @!method default_model
|
|
155
|
+
# Get the default model
|
|
156
|
+
# @return [String] The default model (falls back to Defaults::Agent::MODEL)
|
|
157
|
+
#
|
|
158
|
+
# @!method default_model=(value)
|
|
159
|
+
# Set the default model
|
|
160
|
+
# @param value [String] The default model
|
|
161
|
+
|
|
162
|
+
DEFAULTS_MAPPINGS.each_key do |config_key|
|
|
163
|
+
_env_key, default_proc = DEFAULTS_MAPPINGS[config_key]
|
|
164
|
+
|
|
165
|
+
# Getter with default fallback
|
|
166
|
+
define_method(config_key) do
|
|
167
|
+
ensure_env_loaded!
|
|
168
|
+
@explicit_values[config_key] || @env_values[config_key] || default_proc.call
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Setter
|
|
172
|
+
define_method("#{config_key}=") do |value|
|
|
173
|
+
@explicit_values[config_key] = value
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# ========== Settings Accessors (WebFetch and control) ==========
|
|
178
|
+
|
|
179
|
+
# @!method webfetch_provider
|
|
180
|
+
# Get the WebFetch LLM provider
|
|
181
|
+
# @return [String, nil] The provider
|
|
182
|
+
#
|
|
183
|
+
# @!method allow_filesystem_tools
|
|
184
|
+
# Get whether filesystem tools are allowed
|
|
185
|
+
# @return [Boolean] true if allowed
|
|
186
|
+
|
|
187
|
+
SETTINGS_MAPPINGS.each_key do |config_key|
|
|
188
|
+
_env_key, default_value = SETTINGS_MAPPINGS[config_key]
|
|
189
|
+
|
|
190
|
+
# Getter with default fallback
|
|
191
|
+
define_method(config_key) do
|
|
192
|
+
ensure_env_loaded!
|
|
193
|
+
if @explicit_values.key?(config_key)
|
|
194
|
+
@explicit_values[config_key]
|
|
195
|
+
elsif @env_values.key?(config_key)
|
|
196
|
+
@env_values[config_key]
|
|
197
|
+
else
|
|
198
|
+
default_value
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Setter
|
|
203
|
+
define_method("#{config_key}=") do |value|
|
|
204
|
+
@explicit_values[config_key] = value
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# ========== Convenience Methods ==========
|
|
209
|
+
|
|
210
|
+
# Check if WebFetch LLM processing is enabled
|
|
211
|
+
#
|
|
212
|
+
# WebFetch uses LLM processing when both provider and model are configured.
|
|
213
|
+
#
|
|
214
|
+
# @return [Boolean] true if WebFetch LLM is configured
|
|
215
|
+
def webfetch_llm_enabled?
|
|
216
|
+
!webfetch_provider.nil? && !webfetch_model.nil?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
private
|
|
220
|
+
|
|
221
|
+
# Ensure ENV values are loaded (lazy loading with double-check locking)
|
|
222
|
+
#
|
|
223
|
+
# Thread-safe lazy loading of ENV values. Only loads once per Config instance.
|
|
224
|
+
#
|
|
225
|
+
# @return [void]
|
|
226
|
+
def ensure_env_loaded!
|
|
227
|
+
return if @env_loaded
|
|
228
|
+
|
|
229
|
+
@env_mutex.synchronize do
|
|
230
|
+
return if @env_loaded
|
|
231
|
+
|
|
232
|
+
load_env_values!
|
|
233
|
+
@env_loaded = true
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Load environment variable values
|
|
238
|
+
#
|
|
239
|
+
# Loads API keys (with RubyLLM proxying), defaults, and settings from ENV.
|
|
240
|
+
# Only loads values that haven't been explicitly set.
|
|
241
|
+
#
|
|
242
|
+
# @return [void]
|
|
243
|
+
def load_env_values!
|
|
244
|
+
# Load API keys and proxy to RubyLLM
|
|
245
|
+
API_KEY_MAPPINGS.each do |config_key, (ruby_llm_key, env_key)|
|
|
246
|
+
next if @explicit_values.key?(config_key)
|
|
247
|
+
next unless ENV.key?(env_key)
|
|
248
|
+
|
|
249
|
+
value = ENV[env_key]
|
|
250
|
+
@env_values[config_key] = value
|
|
251
|
+
|
|
252
|
+
# Proxy to RubyLLM
|
|
253
|
+
RubyLLM.config.public_send("#{ruby_llm_key}=", value)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Load defaults (no RubyLLM proxy)
|
|
257
|
+
DEFAULTS_MAPPINGS.each do |config_key, (env_key, _default_proc)|
|
|
258
|
+
next if @explicit_values.key?(config_key)
|
|
259
|
+
next unless ENV.key?(env_key)
|
|
260
|
+
|
|
261
|
+
@env_values[config_key] = parse_env_value(ENV[env_key], config_key)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Load settings (no RubyLLM proxy)
|
|
265
|
+
SETTINGS_MAPPINGS.each do |config_key, (env_key, _default_value)|
|
|
266
|
+
next if @explicit_values.key?(config_key)
|
|
267
|
+
next unless ENV.key?(env_key)
|
|
268
|
+
|
|
269
|
+
@env_values[config_key] = parse_env_value(ENV[env_key], config_key)
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Parse environment variable value to appropriate type
|
|
274
|
+
#
|
|
275
|
+
# Converts string ENV values to integers, floats, or booleans based on
|
|
276
|
+
# the configuration key pattern.
|
|
277
|
+
#
|
|
278
|
+
# @param value [String] The ENV value string
|
|
279
|
+
# @param key [Symbol] The configuration key
|
|
280
|
+
# @return [Integer, Float, Boolean, String] The parsed value
|
|
281
|
+
def parse_env_value(value, key)
|
|
282
|
+
case key
|
|
283
|
+
when :allow_filesystem_tools, :env_interpolation
|
|
284
|
+
# Convert string to boolean
|
|
285
|
+
case value.to_s.downcase
|
|
286
|
+
when "true", "yes", "1", "on", "enabled"
|
|
287
|
+
true
|
|
288
|
+
when "false", "no", "0", "off", "disabled"
|
|
289
|
+
false
|
|
290
|
+
else
|
|
291
|
+
true # Default to true if unrecognized
|
|
292
|
+
end
|
|
293
|
+
when /_timeout$/, /_limit$/, /_interval$/, /_threshold$/, :mcp_log_level, :webfetch_max_tokens
|
|
294
|
+
value.to_i
|
|
295
|
+
when /^chars_per_token/
|
|
296
|
+
value.to_f
|
|
297
|
+
else
|
|
298
|
+
value
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
@@ -30,9 +30,18 @@ module SwarmSDK
|
|
|
30
30
|
:nodes,
|
|
31
31
|
:external_swarms
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# Initialize parser with YAML content and options
|
|
34
|
+
#
|
|
35
|
+
# @param yaml_content [String] YAML configuration content
|
|
36
|
+
# @param base_dir [String, Pathname] Base directory for resolving paths
|
|
37
|
+
# @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
|
|
38
|
+
# When nil, uses the global SwarmSDK.config.env_interpolation setting.
|
|
39
|
+
# When true, interpolates ${VAR} and ${VAR:=default} patterns.
|
|
40
|
+
# When false, skips interpolation entirely.
|
|
41
|
+
def initialize(yaml_content, base_dir:, env_interpolation: nil)
|
|
34
42
|
@yaml_content = yaml_content
|
|
35
43
|
@base_dir = Pathname.new(base_dir).expand_path
|
|
44
|
+
@env_interpolation = env_interpolation
|
|
36
45
|
@config_type = nil
|
|
37
46
|
@swarm_id = nil
|
|
38
47
|
@swarm_name = nil
|
|
@@ -55,7 +64,7 @@ module SwarmSDK
|
|
|
55
64
|
end
|
|
56
65
|
|
|
57
66
|
@config = Utils.symbolize_keys(@config)
|
|
58
|
-
interpolate_env_vars!(@config)
|
|
67
|
+
interpolate_env_vars!(@config) if env_interpolation_enabled?
|
|
59
68
|
|
|
60
69
|
validate_version
|
|
61
70
|
detect_and_validate_type
|
|
@@ -86,6 +95,17 @@ module SwarmSDK
|
|
|
86
95
|
|
|
87
96
|
private
|
|
88
97
|
|
|
98
|
+
# Check if environment variable interpolation is enabled
|
|
99
|
+
#
|
|
100
|
+
# Uses the local setting if explicitly set, otherwise falls back to global config.
|
|
101
|
+
#
|
|
102
|
+
# @return [Boolean] true if interpolation should be performed
|
|
103
|
+
def env_interpolation_enabled?
|
|
104
|
+
return @env_interpolation unless @env_interpolation.nil?
|
|
105
|
+
|
|
106
|
+
SwarmSDK.config.env_interpolation
|
|
107
|
+
end
|
|
108
|
+
|
|
89
109
|
def validate_version
|
|
90
110
|
version = @config[:version]
|
|
91
111
|
raise ConfigurationError, "Missing 'version' field in configuration" unless version
|