swarm_sdk 2.4.2 → 2.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e0a32d6cd3def4eb9f0df716e7f10076023f8c1efd5442bdd5f6501af95d542
4
- data.tar.gz: e243a4b009844dcf19c7e425ff632a9cf19fe8d7d0e9325107c8ef50efb35f8f
3
+ metadata.gz: 370423fc8b77c4d3cc159123a64795b04bcea24ed6b96090a194329bb1d5af73
4
+ data.tar.gz: 05fa10de4f50a64e3bdb919fbea65a4e480cf0eb0b054b2c1e32ef6f5a84c65d
5
5
  SHA512:
6
- metadata.gz: e8493eac228cb132c969476d884c2e2ae3d7008bbceef134c411549545e8c879e6a4aaa75c9c04835555b28193740633163142c2696ba42f6a85d8ea26ed79b3
7
- data.tar.gz: 104440a7754b2fa74bc74eade37cf6ea76cbef2b334d0f5eb0f54bb0f1a38091d3c403294baccbf5a74a44c10468dd5fff89f89abe6c7b5cbba06c36b365e6d0
6
+ metadata.gz: 064afcb3024b9a3006ba398f1237f5b7c189ae307a7603ea744312ec885f8ea307b1e9cb186d0b0a9da6b9908c46711b58a50c849dfdefce089ec82c989e5b26
7
+ data.tar.gz: eaded0c55ebc089acd20ad14a73a97ae24fe8cd5acf0cc2c557733ac03d224b96291e0717025998b9ba1c703e96465178b1dfab02c563c1d206790729daf3d21
@@ -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 or load from markdown content
67
+ # Define an agent with fluent API, load from markdown, or reference registry
68
68
  #
69
- # Supports two forms:
70
- # 1. Inline DSL: agent :name do ... end
71
- # 2. Markdown content: agent :name, <<~MD ... MD
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
- # Case 3: agent :name do ... end (inline DSL)
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, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
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(&registered_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(&registered_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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.4.2"
4
+ VERSION = "2.4.3"
5
5
  end
@@ -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
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.2
4
+ version: 2.4.3
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