swarm_sdk 2.4.2 → 2.4.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/swarm_sdk/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +85 -6
- data/lib/swarm_sdk/config.rb +2 -1
- data/lib/swarm_sdk/configuration/parser.rb +22 -2
- data/lib/swarm_sdk/configuration.rb +13 -4
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +49 -0
- data/lib/swarm_sdk.rb +87 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2357a0a9317288666f8e26f001e2d3b42402870d0e7e03560b6ff14109b36f56
|
|
4
|
+
data.tar.gz: 0c11addc8f0578cfd796cda4795dfb51f2f4b134280eab2601e600b0b90670c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '029817745549dfcd04234c2b4e9f809e139fc222cf717f366d09c5cca6d7b7e2ae50a6f2c52058380a2a1e860e86526618ca06de464589f1d10eb21bebe1bb04'
|
|
7
|
+
data.tar.gz: 2455fab120ec48edeb80adfa0bfa5ede016f09a49eb4ab2dc2e48490432d4b282255999f5540cb494267d4fe46c8d2f72e94ea7fcc37059db58f533ebd0bd834
|
|
@@ -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
|
data/lib/swarm_sdk/config.rb
CHANGED
|
@@ -91,6 +91,7 @@ module SwarmSDK
|
|
|
91
91
|
webfetch_base_url: ["SWARM_SDK_WEBFETCH_BASE_URL", nil],
|
|
92
92
|
webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
|
|
93
93
|
allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
|
|
94
|
+
env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
|
|
94
95
|
}.freeze
|
|
95
96
|
|
|
96
97
|
class << self
|
|
@@ -279,7 +280,7 @@ module SwarmSDK
|
|
|
279
280
|
# @return [Integer, Float, Boolean, String] The parsed value
|
|
280
281
|
def parse_env_value(value, key)
|
|
281
282
|
case key
|
|
282
|
-
when :allow_filesystem_tools
|
|
283
|
+
when :allow_filesystem_tools, :env_interpolation
|
|
283
284
|
# Convert string to boolean
|
|
284
285
|
case value.to_s.downcase
|
|
285
286
|
when "true", "yes", "1", "on", "enabled"
|
|
@@ -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
|
|
@@ -38,9 +38,13 @@ module SwarmSDK
|
|
|
38
38
|
# Load configuration from YAML file
|
|
39
39
|
#
|
|
40
40
|
# @param path [String, Pathname] Path to YAML configuration file
|
|
41
|
+
# @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
|
|
42
|
+
# When nil, uses the global SwarmSDK.config.env_interpolation setting.
|
|
43
|
+
# When true, interpolates ${VAR} and ${VAR:=default} patterns.
|
|
44
|
+
# When false, skips interpolation entirely.
|
|
41
45
|
# @return [Configuration] Validated configuration instance
|
|
42
46
|
# @raise [ConfigurationError] If file not found or invalid
|
|
43
|
-
def load_file(path)
|
|
47
|
+
def load_file(path, env_interpolation: nil)
|
|
44
48
|
path = Pathname.new(path).expand_path
|
|
45
49
|
|
|
46
50
|
unless path.exist?
|
|
@@ -50,7 +54,7 @@ module SwarmSDK
|
|
|
50
54
|
yaml_content = File.read(path)
|
|
51
55
|
base_dir = path.dirname
|
|
52
56
|
|
|
53
|
-
new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
|
|
57
|
+
new(yaml_content, base_dir: base_dir, env_interpolation: env_interpolation).tap(&:load_and_validate)
|
|
54
58
|
rescue Errno::ENOENT
|
|
55
59
|
raise ConfigurationError, "Configuration file not found: #{path}"
|
|
56
60
|
end
|
|
@@ -60,12 +64,17 @@ module SwarmSDK
|
|
|
60
64
|
#
|
|
61
65
|
# @param yaml_content [String] YAML configuration content
|
|
62
66
|
# @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
|
|
63
|
-
|
|
67
|
+
# @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
|
|
68
|
+
# When nil, uses the global SwarmSDK.config.env_interpolation setting.
|
|
69
|
+
# When true, interpolates ${VAR} and ${VAR:=default} patterns.
|
|
70
|
+
# When false, skips interpolation entirely.
|
|
71
|
+
def initialize(yaml_content, base_dir: Dir.pwd, env_interpolation: nil)
|
|
64
72
|
raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
|
|
65
73
|
raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
|
|
66
74
|
|
|
67
75
|
@yaml_content = yaml_content
|
|
68
76
|
@base_dir = Pathname.new(base_dir).expand_path
|
|
77
|
+
@env_interpolation = env_interpolation
|
|
69
78
|
@parser = nil
|
|
70
79
|
@translator = nil
|
|
71
80
|
end
|
|
@@ -77,7 +86,7 @@ module SwarmSDK
|
|
|
77
86
|
#
|
|
78
87
|
# @return [self]
|
|
79
88
|
def load_and_validate
|
|
80
|
-
@parser = Parser.new(@yaml_content, base_dir: @base_dir)
|
|
89
|
+
@parser = Parser.new(@yaml_content, base_dir: @base_dir, env_interpolation: @env_interpolation)
|
|
81
90
|
@parser.parse
|
|
82
91
|
|
|
83
92
|
# Sync parsed data to instance variables for backward compatibility
|
data/lib/swarm_sdk/version.rb
CHANGED
|
@@ -119,6 +119,9 @@ module SwarmSDK
|
|
|
119
119
|
#
|
|
120
120
|
# @return [Workflow] Configured workflow instance
|
|
121
121
|
def build_workflow
|
|
122
|
+
# Resolve any missing agents from global registry before building definitions
|
|
123
|
+
resolve_missing_agents_from_registry
|
|
124
|
+
|
|
122
125
|
# Build agent definitions
|
|
123
126
|
agent_definitions = build_agent_definitions
|
|
124
127
|
|
|
@@ -138,6 +141,52 @@ module SwarmSDK
|
|
|
138
141
|
|
|
139
142
|
workflow
|
|
140
143
|
end
|
|
144
|
+
|
|
145
|
+
# Resolve agents referenced in nodes that aren't defined at workflow level
|
|
146
|
+
#
|
|
147
|
+
# For each agent referenced in a node but not defined with `agent :name do ... end`,
|
|
148
|
+
# checks the global AgentRegistry and loads the agent if found.
|
|
149
|
+
#
|
|
150
|
+
# This allows workflows to reference globally registered agents without
|
|
151
|
+
# explicitly re-declaring them at the workflow level.
|
|
152
|
+
#
|
|
153
|
+
# @return [void]
|
|
154
|
+
# @raise [ConfigurationError] If agent not found in workflow or registry
|
|
155
|
+
def resolve_missing_agents_from_registry
|
|
156
|
+
# Collect all agents referenced in nodes
|
|
157
|
+
referenced_agents = collect_referenced_agents
|
|
158
|
+
|
|
159
|
+
# Find agents that aren't defined at workflow level
|
|
160
|
+
defined_agents = @agents.keys
|
|
161
|
+
missing_agents = referenced_agents - defined_agents
|
|
162
|
+
|
|
163
|
+
# Try to resolve each missing agent from the global registry
|
|
164
|
+
missing_agents.each do |agent_name|
|
|
165
|
+
if AgentRegistry.registered?(agent_name)
|
|
166
|
+
# Load from registry (uses same method as BaseBuilder)
|
|
167
|
+
load_agent_from_registry(agent_name)
|
|
168
|
+
else
|
|
169
|
+
raise ConfigurationError,
|
|
170
|
+
"Agent '#{agent_name}' referenced in node but not found. " \
|
|
171
|
+
"Either define at workflow level with `agent :#{agent_name} do ... end` " \
|
|
172
|
+
"or register globally with `SwarmSDK.agent :#{agent_name} do ... end`"
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Collect all agent names referenced across all nodes
|
|
178
|
+
#
|
|
179
|
+
# Includes both directly referenced agents and delegation targets.
|
|
180
|
+
#
|
|
181
|
+
# @return [Array<Symbol>] Unique agent names referenced in nodes
|
|
182
|
+
def collect_referenced_agents
|
|
183
|
+
@nodes.values.flat_map do |node_builder|
|
|
184
|
+
# Collect both direct agents and their delegation targets
|
|
185
|
+
node_builder.agent_configs.flat_map do |config|
|
|
186
|
+
[config[:agent]] + Array(config[:delegates_to])
|
|
187
|
+
end
|
|
188
|
+
end.uniq
|
|
189
|
+
end
|
|
141
190
|
end
|
|
142
191
|
end
|
|
143
192
|
end
|
data/lib/swarm_sdk.rb
CHANGED
|
@@ -95,6 +95,73 @@ module SwarmSDK
|
|
|
95
95
|
Config.reset!
|
|
96
96
|
end
|
|
97
97
|
|
|
98
|
+
# Register a global agent definition
|
|
99
|
+
#
|
|
100
|
+
# Declares an agent configuration that can be referenced by name in any
|
|
101
|
+
# swarm definition. This allows defining agents in separate files and
|
|
102
|
+
# composing them into swarms without duplication.
|
|
103
|
+
#
|
|
104
|
+
# The registered block uses the Agent::Builder DSL and is executed when
|
|
105
|
+
# the agent is referenced in a swarm definition.
|
|
106
|
+
#
|
|
107
|
+
# @param name [Symbol, String] Agent name (will be symbolized)
|
|
108
|
+
# @yield Agent configuration block using Agent::Builder DSL
|
|
109
|
+
# @return [void]
|
|
110
|
+
# @raise [ArgumentError] If no block is provided
|
|
111
|
+
#
|
|
112
|
+
# @example Register agent in separate file
|
|
113
|
+
# # agents/backend.rb
|
|
114
|
+
# SwarmSDK.agent :backend do
|
|
115
|
+
# model "claude-sonnet-4"
|
|
116
|
+
# description "Backend API developer"
|
|
117
|
+
# system_prompt "You build REST APIs"
|
|
118
|
+
# tools :Read, :Edit, :Bash
|
|
119
|
+
# delegates_to :database
|
|
120
|
+
# end
|
|
121
|
+
#
|
|
122
|
+
# @example Reference in swarm definition
|
|
123
|
+
# # swarm.rb
|
|
124
|
+
# require_relative "agents/backend"
|
|
125
|
+
#
|
|
126
|
+
# SwarmSDK.build do
|
|
127
|
+
# name "Dev Team"
|
|
128
|
+
# lead :backend
|
|
129
|
+
#
|
|
130
|
+
# agent :backend # Pulls from registry
|
|
131
|
+
# end
|
|
132
|
+
#
|
|
133
|
+
# @example Extend registered agent with overrides
|
|
134
|
+
# SwarmSDK.build do
|
|
135
|
+
# name "Extended Team"
|
|
136
|
+
# lead :backend
|
|
137
|
+
#
|
|
138
|
+
# agent :backend do
|
|
139
|
+
# # Registry config applied first, then this block
|
|
140
|
+
# tools :CustomTool # Adds to existing tools
|
|
141
|
+
# delegates_to :cache # Adds delegation target
|
|
142
|
+
# end
|
|
143
|
+
# end
|
|
144
|
+
#
|
|
145
|
+
# @see AgentRegistry
|
|
146
|
+
def agent(name, &block)
|
|
147
|
+
AgentRegistry.register(name, &block)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Clear the global agent registry
|
|
151
|
+
#
|
|
152
|
+
# Removes all registered agent definitions. Primarily useful for testing
|
|
153
|
+
# to ensure clean state between tests.
|
|
154
|
+
#
|
|
155
|
+
# @return [void]
|
|
156
|
+
#
|
|
157
|
+
# @example In test teardown
|
|
158
|
+
# def teardown
|
|
159
|
+
# SwarmSDK.clear_agent_registry!
|
|
160
|
+
# end
|
|
161
|
+
def clear_agent_registry!
|
|
162
|
+
AgentRegistry.clear
|
|
163
|
+
end
|
|
164
|
+
|
|
98
165
|
# Main entry point for DSL - builds simple multi-agent swarms
|
|
99
166
|
#
|
|
100
167
|
# @return [Swarm] Always returns a Swarm instance
|
|
@@ -208,6 +275,11 @@ module SwarmSDK
|
|
|
208
275
|
#
|
|
209
276
|
# @param yaml_content [String] YAML configuration content
|
|
210
277
|
# @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
|
|
278
|
+
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
279
|
+
# @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
|
|
280
|
+
# When nil, uses the global SwarmSDK.config.env_interpolation setting.
|
|
281
|
+
# When true, interpolates ${VAR} and ${VAR:=default} patterns.
|
|
282
|
+
# When false, skips interpolation entirely.
|
|
211
283
|
# @return [Swarm, Workflow] Configured swarm or workflow instance
|
|
212
284
|
# @raise [ConfigurationError] If YAML is invalid or configuration is incorrect
|
|
213
285
|
#
|
|
@@ -230,8 +302,11 @@ module SwarmSDK
|
|
|
230
302
|
# @example Load with default base_dir (Dir.pwd)
|
|
231
303
|
# yaml = File.read("config.yml")
|
|
232
304
|
# swarm = SwarmSDK.load(yaml) # base_dir defaults to Dir.pwd
|
|
233
|
-
|
|
234
|
-
|
|
305
|
+
#
|
|
306
|
+
# @example Load without environment variable interpolation
|
|
307
|
+
# swarm = SwarmSDK.load(yaml, env_interpolation: false)
|
|
308
|
+
def load(yaml_content, base_dir: Dir.pwd, allow_filesystem_tools: nil, env_interpolation: nil)
|
|
309
|
+
config = Configuration.new(yaml_content, base_dir: base_dir, env_interpolation: env_interpolation)
|
|
235
310
|
config.load_and_validate
|
|
236
311
|
swarm = config.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
|
|
237
312
|
|
|
@@ -253,6 +328,11 @@ module SwarmSDK
|
|
|
253
328
|
# loading swarms from configuration files.
|
|
254
329
|
#
|
|
255
330
|
# @param path [String, Pathname] Path to YAML configuration file
|
|
331
|
+
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
332
|
+
# @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
|
|
333
|
+
# When nil, uses the global SwarmSDK.config.env_interpolation setting.
|
|
334
|
+
# When true, interpolates ${VAR} and ${VAR:=default} patterns.
|
|
335
|
+
# When false, skips interpolation entirely.
|
|
256
336
|
# @return [Swarm, Workflow] Configured swarm or workflow instance
|
|
257
337
|
# @raise [ConfigurationError] If file not found or configuration invalid
|
|
258
338
|
#
|
|
@@ -262,8 +342,11 @@ module SwarmSDK
|
|
|
262
342
|
#
|
|
263
343
|
# @example With absolute path
|
|
264
344
|
# swarm = SwarmSDK.load_file("/absolute/path/config.yml")
|
|
265
|
-
|
|
266
|
-
|
|
345
|
+
#
|
|
346
|
+
# @example Load without environment variable interpolation
|
|
347
|
+
# swarm = SwarmSDK.load_file("config.yml", env_interpolation: false)
|
|
348
|
+
def load_file(path, allow_filesystem_tools: nil, env_interpolation: nil)
|
|
349
|
+
config = Configuration.load_file(path, env_interpolation: env_interpolation)
|
|
267
350
|
swarm = config.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
|
|
268
351
|
|
|
269
352
|
# Apply hooks if any are configured (YAML-only feature)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: swarm_sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.4.
|
|
4
|
+
version: 2.4.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Arruda
|
|
@@ -139,6 +139,7 @@ files:
|
|
|
139
139
|
- lib/swarm_sdk/agent/definition.rb
|
|
140
140
|
- lib/swarm_sdk/agent/llm_instrumentation_middleware.rb
|
|
141
141
|
- lib/swarm_sdk/agent/system_prompt_builder.rb
|
|
142
|
+
- lib/swarm_sdk/agent_registry.rb
|
|
142
143
|
- lib/swarm_sdk/builders/base_builder.rb
|
|
143
144
|
- lib/swarm_sdk/claude_code_agent_adapter.rb
|
|
144
145
|
- lib/swarm_sdk/concerns/cleanupable.rb
|