swarm_sdk 2.2.0 → 2.3.0
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/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 +67 -15
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
|
+
# Configuration facade that delegates to Parser and Translator
|
|
5
|
+
#
|
|
6
|
+
# This class maintains the public API while internally delegating to:
|
|
7
|
+
# - Configuration::Parser - YAML parsing, validation, and normalization
|
|
8
|
+
# - Configuration::Translator - Translation to Swarm/Workflow DSL builders
|
|
9
|
+
#
|
|
10
|
+
# ## Public API (unchanged)
|
|
11
|
+
# - Configuration.load_file(path) - Load from file
|
|
12
|
+
# - Configuration.new(yaml_content, base_dir:) - Load from string
|
|
13
|
+
# - config.load_and_validate - Parse and validate
|
|
14
|
+
# - config.to_swarm(allow_filesystem_tools:) - Convert to Swarm/Workflow
|
|
15
|
+
# - config.agent_names - Get list of agent names
|
|
16
|
+
# - config.connections_for(agent_name) - Get delegation targets
|
|
17
|
+
#
|
|
18
|
+
# ## Architecture
|
|
19
|
+
# The facade pattern keeps backward compatibility while separating concerns:
|
|
20
|
+
# - Parser handles all YAML parsing and validation logic
|
|
21
|
+
# - Translator handles all DSL builder translation logic
|
|
22
|
+
# - Configuration delegates to both, exposing parsed data via attr_readers
|
|
4
23
|
class Configuration
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
24
|
+
attr_reader :config_type,
|
|
25
|
+
:swarm_name,
|
|
26
|
+
:swarm_id,
|
|
27
|
+
:lead_agent,
|
|
28
|
+
:start_node,
|
|
29
|
+
:agents,
|
|
30
|
+
:all_agents_config,
|
|
31
|
+
:swarm_hooks,
|
|
32
|
+
:all_agents_hooks,
|
|
33
|
+
:scratchpad_enabled,
|
|
34
|
+
:nodes,
|
|
35
|
+
:external_swarms
|
|
8
36
|
|
|
9
37
|
class << self
|
|
10
38
|
# Load configuration from YAML file
|
|
11
39
|
#
|
|
12
|
-
# Convenience method that reads the file and uses the file's directory
|
|
13
|
-
# as the base directory for resolving agent file paths.
|
|
14
|
-
#
|
|
15
40
|
# @param path [String, Pathname] Path to YAML configuration file
|
|
16
41
|
# @return [Configuration] Validated configuration instance
|
|
17
42
|
# @raise [ConfigurationError] If file not found or invalid
|
|
@@ -41,35 +66,24 @@ module SwarmSDK
|
|
|
41
66
|
|
|
42
67
|
@yaml_content = yaml_content
|
|
43
68
|
@base_dir = Pathname.new(base_dir).expand_path
|
|
44
|
-
@
|
|
45
|
-
@
|
|
46
|
-
@all_agents_config = {} # Settings applied to all agents
|
|
47
|
-
@swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
|
|
48
|
-
@all_agents_hooks = {} # Hooks applied to all agents
|
|
49
|
-
@external_swarms = {} # External swarms for composable swarms
|
|
50
|
-
@nodes = {} # Parsed node configs (hashes)
|
|
51
|
-
@start_node = nil # Starting node for workflows
|
|
69
|
+
@parser = nil
|
|
70
|
+
@translator = nil
|
|
52
71
|
end
|
|
53
72
|
|
|
73
|
+
# Parse and validate YAML configuration
|
|
74
|
+
#
|
|
75
|
+
# Delegates to Parser for all parsing logic, then syncs parsed data
|
|
76
|
+
# to instance variables for backward compatibility.
|
|
77
|
+
#
|
|
78
|
+
# @return [self]
|
|
54
79
|
def load_and_validate
|
|
55
|
-
@
|
|
80
|
+
@parser = Parser.new(@yaml_content, base_dir: @base_dir)
|
|
81
|
+
@parser.parse
|
|
56
82
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
end
|
|
83
|
+
# Sync parsed data to instance variables for backward compatibility
|
|
84
|
+
sync_from_parser
|
|
60
85
|
|
|
61
|
-
@config = Utils.symbolize_keys(@config)
|
|
62
|
-
interpolate_env_vars!(@config)
|
|
63
|
-
validate_version
|
|
64
|
-
load_all_agents_config
|
|
65
|
-
load_hooks_config
|
|
66
|
-
validate_swarm
|
|
67
|
-
load_agents
|
|
68
|
-
load_nodes
|
|
69
|
-
detect_circular_dependencies
|
|
70
86
|
self
|
|
71
|
-
rescue Psych::SyntaxError => e
|
|
72
|
-
raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
|
|
73
87
|
end
|
|
74
88
|
|
|
75
89
|
def agent_names
|
|
@@ -80,534 +94,42 @@ module SwarmSDK
|
|
|
80
94
|
agent_config = @agents[agent_name]
|
|
81
95
|
return [] unless agent_config
|
|
82
96
|
|
|
83
|
-
# Extract delegates_to from hash and convert to symbols
|
|
84
97
|
delegates = agent_config[:delegates_to] || []
|
|
85
98
|
Array(delegates).map(&:to_sym)
|
|
86
99
|
end
|
|
87
100
|
|
|
88
|
-
# Convert configuration to Swarm or
|
|
101
|
+
# Convert configuration to Swarm or Workflow using appropriate builder
|
|
89
102
|
#
|
|
90
|
-
#
|
|
91
|
-
# The DSL (Swarm::Builder) handles all validation, merging, and construction.
|
|
103
|
+
# Delegates to Translator for all DSL translation logic.
|
|
92
104
|
#
|
|
93
105
|
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
94
|
-
# @return [Swarm,
|
|
106
|
+
# @return [Swarm, Workflow] Configured swarm or workflow
|
|
95
107
|
def to_swarm(allow_filesystem_tools: nil)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
# Translate basic swarm config to DSL
|
|
99
|
-
builder.id(@swarm_id) if @swarm_id
|
|
100
|
-
builder.name(@swarm_name)
|
|
101
|
-
builder.lead(@lead_agent)
|
|
102
|
-
builder.scratchpad(@scratchpad_mode)
|
|
103
|
-
|
|
104
|
-
# Translate external swarms
|
|
105
|
-
if @external_swarms&.any?
|
|
106
|
-
builder.swarms do
|
|
107
|
-
@external_swarms.each do |name, config|
|
|
108
|
-
source = config[:source]
|
|
109
|
-
case source[:type]
|
|
110
|
-
when :file
|
|
111
|
-
register(name, file: source[:value], keep_context: config[:keep_context])
|
|
112
|
-
when :yaml
|
|
113
|
-
register(name, yaml: source[:value], keep_context: config[:keep_context])
|
|
114
|
-
else
|
|
115
|
-
raise ConfigurationError, "Unknown source type: #{source[:type]}"
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Translate all_agents config to DSL (if present)
|
|
122
|
-
translate_all_agents(builder) if @all_agents_config.any?
|
|
108
|
+
raise ConfigurationError, "Configuration not loaded. Call load_and_validate first." unless @parser
|
|
123
109
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
# Translate swarm-level hooks to DSL (if present)
|
|
128
|
-
translate_swarm_hooks(builder) if @swarm_hooks.any?
|
|
129
|
-
|
|
130
|
-
# Translate nodes to DSL (if present)
|
|
131
|
-
if @nodes.any?
|
|
132
|
-
translate_nodes(builder)
|
|
133
|
-
builder.start_node(@start_node)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Build the swarm or orchestrator (DSL decides based on presence of nodes)
|
|
137
|
-
builder.build_swarm
|
|
110
|
+
@translator = Translator.new(@parser)
|
|
111
|
+
@translator.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
|
|
138
112
|
end
|
|
139
113
|
|
|
140
114
|
private
|
|
141
115
|
|
|
142
|
-
|
|
143
|
-
return :disabled if value.nil? # Default
|
|
144
|
-
|
|
145
|
-
# Convert strings from YAML to symbols
|
|
146
|
-
value = value.to_sym if value.is_a?(String)
|
|
147
|
-
|
|
148
|
-
# Validate symbols
|
|
149
|
-
case value
|
|
150
|
-
when :enabled, :disabled, :per_node
|
|
151
|
-
value
|
|
152
|
-
else
|
|
153
|
-
raise ConfigurationError,
|
|
154
|
-
"Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
|
|
158
|
-
def interpolate_env_vars!(obj)
|
|
159
|
-
case obj
|
|
160
|
-
when String
|
|
161
|
-
interpolate_env_string(obj)
|
|
162
|
-
when Hash
|
|
163
|
-
obj.transform_values! { |v| interpolate_env_vars!(v) }
|
|
164
|
-
when Array
|
|
165
|
-
obj.map! { |v| interpolate_env_vars!(v) }
|
|
166
|
-
else
|
|
167
|
-
obj
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def interpolate_env_string(str)
|
|
172
|
-
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
173
|
-
env_var = Regexp.last_match(1)
|
|
174
|
-
has_default = Regexp.last_match(2)
|
|
175
|
-
default_value = Regexp.last_match(3)
|
|
176
|
-
|
|
177
|
-
if ENV.key?(env_var)
|
|
178
|
-
ENV[env_var]
|
|
179
|
-
elsif has_default
|
|
180
|
-
default_value || ""
|
|
181
|
-
else
|
|
182
|
-
raise ConfigurationError, "Environment variable '#{env_var}' is not set"
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
def validate_version
|
|
188
|
-
version = @config[:version]
|
|
189
|
-
raise ConfigurationError, "Missing 'version' field in configuration" unless version
|
|
190
|
-
raise ConfigurationError, "SwarmSDK requires version: 2 configuration. Got version: #{version}" unless version == 2
|
|
191
|
-
end
|
|
192
|
-
|
|
193
|
-
def load_all_agents_config
|
|
194
|
-
return unless @config[:swarm]
|
|
195
|
-
|
|
196
|
-
@all_agents_config = @config[:swarm][:all_agents] || {}
|
|
197
|
-
|
|
198
|
-
# Convert disable_default_tools array elements to symbols
|
|
199
|
-
if @all_agents_config[:disable_default_tools].is_a?(Array)
|
|
200
|
-
@all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
|
|
201
|
-
end
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def load_hooks_config
|
|
205
|
-
return unless @config[:swarm]
|
|
206
|
-
|
|
207
|
-
# Load swarm-level hooks (only swarm_start, swarm_stop allowed)
|
|
208
|
-
@swarm_hooks = Utils.symbolize_keys(@config[:swarm][:hooks] || {})
|
|
209
|
-
|
|
210
|
-
# Load all_agents hooks (applied as swarm defaults)
|
|
211
|
-
if @config[:swarm][:all_agents]
|
|
212
|
-
@all_agents_hooks = Utils.symbolize_keys(@config[:swarm][:all_agents][:hooks] || {})
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def validate_swarm
|
|
217
|
-
raise ConfigurationError, "Missing 'swarm' field in configuration" unless @config[:swarm]
|
|
218
|
-
|
|
219
|
-
swarm = @config[:swarm]
|
|
220
|
-
raise ConfigurationError, "Missing 'name' field in swarm configuration" unless swarm[:name]
|
|
221
|
-
raise ConfigurationError, "Missing 'agents' field in swarm configuration" unless swarm[:agents]
|
|
222
|
-
raise ConfigurationError, "Missing 'lead' field in swarm configuration" unless swarm[:lead]
|
|
223
|
-
raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
|
|
224
|
-
|
|
225
|
-
@swarm_name = swarm[:name]
|
|
226
|
-
@swarm_id = swarm[:id] # Optional - will auto-generate if missing
|
|
227
|
-
@lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
|
|
228
|
-
@scratchpad_mode = parse_scratchpad_mode(swarm[:scratchpad])
|
|
229
|
-
|
|
230
|
-
# Load external swarms for composable swarms
|
|
231
|
-
load_external_swarms(swarm[:swarms]) if swarm[:swarms]
|
|
232
|
-
end
|
|
233
|
-
|
|
234
|
-
def load_external_swarms(swarms_config)
|
|
235
|
-
@external_swarms = {}
|
|
236
|
-
swarms_config.each do |name, config|
|
|
237
|
-
# Determine source type: file, yaml string, or inline swarm definition
|
|
238
|
-
source = if config[:file]
|
|
239
|
-
# File path - resolve relative to base_dir
|
|
240
|
-
file_path = if config[:file].start_with?("/")
|
|
241
|
-
config[:file]
|
|
242
|
-
else
|
|
243
|
-
(@base_dir / config[:file]).to_s
|
|
244
|
-
end
|
|
245
|
-
{ type: :file, value: file_path }
|
|
246
|
-
elsif config[:yaml]
|
|
247
|
-
# YAML string provided directly
|
|
248
|
-
{ type: :yaml, value: config[:yaml] }
|
|
249
|
-
elsif config[:swarm]
|
|
250
|
-
# Inline swarm definition - convert to YAML string
|
|
251
|
-
inline_config = {
|
|
252
|
-
version: 2,
|
|
253
|
-
swarm: config[:swarm],
|
|
254
|
-
}
|
|
255
|
-
yaml_string = Utils.hash_to_yaml(inline_config)
|
|
256
|
-
{ type: :yaml, value: yaml_string }
|
|
257
|
-
else
|
|
258
|
-
raise ConfigurationError, "Swarm '#{name}' must specify either 'file:', 'yaml:', or 'swarm:' (inline definition)"
|
|
259
|
-
end
|
|
260
|
-
|
|
261
|
-
@external_swarms[name.to_sym] = {
|
|
262
|
-
source: source,
|
|
263
|
-
keep_context: config.fetch(:keep_context, true),
|
|
264
|
-
}
|
|
265
|
-
end
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def load_agents
|
|
269
|
-
swarm_agents = @config[:swarm][:agents]
|
|
270
|
-
|
|
271
|
-
swarm_agents.each do |name, agent_config|
|
|
272
|
-
# Support three formats:
|
|
273
|
-
# 1. String: assistant: "agents/assistant.md" (file path)
|
|
274
|
-
# 2. Hash with agent_file: assistant: { agent_file: "..." }
|
|
275
|
-
# 3. Hash with inline definition: assistant: { description: "...", model: "..." }
|
|
276
|
-
# 4. nil: Invalid (will be caught when building swarm)
|
|
277
|
-
|
|
278
|
-
parsed_config = if agent_config.nil?
|
|
279
|
-
# Null config - store empty hash, will fail during swarm building
|
|
280
|
-
{}
|
|
281
|
-
elsif agent_config.is_a?(String)
|
|
282
|
-
# Format 1: Direct file path as string
|
|
283
|
-
{ agent_file: agent_config }
|
|
284
|
-
elsif agent_config.is_a?(Hash) && agent_config[:agent_file]
|
|
285
|
-
# Format 2: Hash with agent_file key
|
|
286
|
-
agent_config
|
|
287
|
-
else
|
|
288
|
-
# Format 3: Inline definition
|
|
289
|
-
agent_config || {}
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
# Validate required fields for inline definitions (strict validation for YAML)
|
|
293
|
-
# File-based agents are validated when loaded
|
|
294
|
-
if parsed_config[:agent_file].nil? && parsed_config[:description].nil?
|
|
295
|
-
raise ConfigurationError,
|
|
296
|
-
"Agent '#{name}' missing required 'description' field"
|
|
297
|
-
end
|
|
298
|
-
|
|
299
|
-
@agents[name] = parsed_config
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
unless @agents.key?(@lead_agent)
|
|
303
|
-
raise ConfigurationError, "Lead agent '#{@lead_agent}' not found in agents"
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
def load_nodes
|
|
308
|
-
return unless @config[:swarm][:nodes]
|
|
309
|
-
|
|
310
|
-
@nodes = Utils.symbolize_keys(@config[:swarm][:nodes])
|
|
311
|
-
@start_node = @config[:swarm][:start_node]&.to_sym
|
|
312
|
-
|
|
313
|
-
# Validate start_node is required if nodes defined
|
|
314
|
-
if @nodes.any? && !@start_node
|
|
315
|
-
raise ConfigurationError, "start_node required when nodes are defined"
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# Validate start_node exists
|
|
319
|
-
if @start_node && !@nodes.key?(@start_node)
|
|
320
|
-
raise ConfigurationError, "start_node '#{@start_node}' not found in nodes"
|
|
321
|
-
end
|
|
322
|
-
|
|
323
|
-
# Basic node structure validation
|
|
324
|
-
@nodes.each do |node_name, node_config|
|
|
325
|
-
unless node_config.is_a?(Hash)
|
|
326
|
-
raise ConfigurationError, "Node '#{node_name}' must be a hash"
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
# Validate agents if present (optional for agent-less nodes)
|
|
330
|
-
if node_config[:agents]
|
|
331
|
-
unless node_config[:agents].is_a?(Array)
|
|
332
|
-
raise ConfigurationError, "Node '#{node_name}' agents must be an array"
|
|
333
|
-
end
|
|
334
|
-
|
|
335
|
-
# Validate each agent config
|
|
336
|
-
node_config[:agents].each do |agent_config|
|
|
337
|
-
unless agent_config.is_a?(Hash) && agent_config[:agent]
|
|
338
|
-
raise ConfigurationError,
|
|
339
|
-
"Node '#{node_name}' agents must be hashes with 'agent' key"
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
# Validate agent exists in swarm agents
|
|
343
|
-
agent_sym = agent_config[:agent].to_sym
|
|
344
|
-
unless @agents.key?(agent_sym)
|
|
345
|
-
raise ConfigurationError,
|
|
346
|
-
"Node '#{node_name}' references undefined agent '#{agent_config[:agent]}'"
|
|
347
|
-
end
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# Validate dependencies if present
|
|
352
|
-
next unless node_config[:dependencies]
|
|
353
|
-
unless node_config[:dependencies].is_a?(Array)
|
|
354
|
-
raise ConfigurationError, "Node '#{node_name}' dependencies must be an array"
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
# Validate each dependency exists
|
|
358
|
-
node_config[:dependencies].each do |dep|
|
|
359
|
-
dep_sym = dep.to_sym
|
|
360
|
-
unless @nodes.key?(dep_sym)
|
|
361
|
-
raise ConfigurationError,
|
|
362
|
-
"Node '#{node_name}' depends on undefined node '#{dep}'"
|
|
363
|
-
end
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
# Translate all_agents configuration to DSL
|
|
116
|
+
# Sync parsed data from Parser to instance variables
|
|
369
117
|
#
|
|
370
|
-
#
|
|
371
|
-
# @
|
|
372
|
-
def
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
parameters(all_agents_cfg[:parameters]) if all_agents_cfg[:parameters]
|
|
386
|
-
headers(all_agents_cfg[:headers]) if all_agents_cfg[:headers]
|
|
387
|
-
coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
|
|
388
|
-
disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
|
|
389
|
-
|
|
390
|
-
# Translate all_agents hooks
|
|
391
|
-
if all_agents_hks.any?
|
|
392
|
-
all_agents_hks.each do |event, hook_specs|
|
|
393
|
-
Array(hook_specs).each do |spec|
|
|
394
|
-
matcher = spec[:matcher]
|
|
395
|
-
hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
|
|
396
|
-
end
|
|
397
|
-
end
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
# Permissions - set directly as hash (YAML doesn't use DSL block syntax)
|
|
401
|
-
self.permissions_hash = all_agents_cfg[:permissions] if all_agents_cfg[:permissions]
|
|
402
|
-
end
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
# Translate agents to DSL
|
|
406
|
-
#
|
|
407
|
-
# @param builder [Swarm::Builder] DSL builder instance
|
|
408
|
-
# @return [void]
|
|
409
|
-
def translate_agents(builder)
|
|
410
|
-
@agents.each do |name, agent_config|
|
|
411
|
-
translate_agent(builder, name, agent_config)
|
|
412
|
-
rescue ConfigurationError => e
|
|
413
|
-
# Re-raise with agent context for better error messages
|
|
414
|
-
raise ConfigurationError, "Error in swarm.agents.#{name}: #{e.message}"
|
|
415
|
-
end
|
|
416
|
-
end
|
|
417
|
-
|
|
418
|
-
# Translate single agent to DSL
|
|
419
|
-
#
|
|
420
|
-
# @param builder [Swarm::Builder] DSL builder instance
|
|
421
|
-
# @param name [Symbol] Agent name
|
|
422
|
-
# @param config [Hash] Agent configuration
|
|
423
|
-
# @return [void]
|
|
424
|
-
def translate_agent(builder, name, config)
|
|
425
|
-
if config[:agent_file]
|
|
426
|
-
# Load from file
|
|
427
|
-
agent_file_path = resolve_agent_file_path(config[:agent_file])
|
|
428
|
-
|
|
429
|
-
unless File.exist?(agent_file_path)
|
|
430
|
-
raise ConfigurationError, "Agent file not found: #{agent_file_path}"
|
|
431
|
-
end
|
|
432
|
-
|
|
433
|
-
content = File.read(agent_file_path)
|
|
434
|
-
|
|
435
|
-
# Check if there are overrides besides agent_file
|
|
436
|
-
overrides = config.except(:agent_file)
|
|
437
|
-
|
|
438
|
-
if overrides.any?
|
|
439
|
-
# Load from markdown with DSL overrides
|
|
440
|
-
builder.agent(name, content, &create_agent_config_block(overrides))
|
|
441
|
-
else
|
|
442
|
-
# Load from markdown only
|
|
443
|
-
builder.agent(name, content)
|
|
444
|
-
end
|
|
445
|
-
else
|
|
446
|
-
# Inline definition - translate to DSL
|
|
447
|
-
builder.agent(name, &create_agent_config_block(config))
|
|
448
|
-
end
|
|
449
|
-
rescue StandardError => e
|
|
450
|
-
raise ConfigurationError, "Error loading agent '#{name}': #{e.message}"
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
# Create a block that configures an agent builder with the given config
|
|
454
|
-
#
|
|
455
|
-
# Returns a proc that can be passed to builder.agent
|
|
456
|
-
#
|
|
457
|
-
# @param config [Hash] Agent configuration hash
|
|
458
|
-
# @return [Proc] Block that configures agent builder
|
|
459
|
-
def create_agent_config_block(config)
|
|
460
|
-
proc do
|
|
461
|
-
description(config[:description]) if config[:description]
|
|
462
|
-
model(config[:model]) if config[:model]
|
|
463
|
-
provider(config[:provider]) if config[:provider]
|
|
464
|
-
base_url(config[:base_url]) if config[:base_url]
|
|
465
|
-
api_version(config[:api_version]) if config[:api_version]
|
|
466
|
-
context_window(config[:context_window]) if config[:context_window]
|
|
467
|
-
system_prompt(config[:system_prompt]) if config[:system_prompt]
|
|
468
|
-
directory(config[:directory]) if config[:directory]
|
|
469
|
-
timeout(config[:timeout]) if config[:timeout]
|
|
470
|
-
parameters(config[:parameters]) if config[:parameters]
|
|
471
|
-
headers(config[:headers]) if config[:headers]
|
|
472
|
-
coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
|
|
473
|
-
bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
|
|
474
|
-
disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
|
|
475
|
-
shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
|
|
476
|
-
|
|
477
|
-
# Tools
|
|
478
|
-
if config[:tools]&.any?
|
|
479
|
-
tool_names = config[:tools].map { |t| t.is_a?(Hash) ? t[:name] : t }
|
|
480
|
-
tools(*tool_names)
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
# Delegation
|
|
484
|
-
delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
|
|
485
|
-
|
|
486
|
-
# MCP servers
|
|
487
|
-
config[:mcp_servers]&.each do |server|
|
|
488
|
-
mcp_server(server[:name], **server.except(:name))
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
# Hooks (YAML-style command hooks)
|
|
492
|
-
config[:hooks]&.each do |event, hook_specs|
|
|
493
|
-
Array(hook_specs).each do |spec|
|
|
494
|
-
matcher = spec[:matcher]
|
|
495
|
-
hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
|
|
496
|
-
end
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
# Memory
|
|
500
|
-
if config[:memory]
|
|
501
|
-
memory do
|
|
502
|
-
directory(config[:memory][:directory]) if config[:memory][:directory]
|
|
503
|
-
adapter(config[:memory][:adapter]) if config[:memory][:adapter]
|
|
504
|
-
mode(config[:memory][:mode]) if config[:memory][:mode]
|
|
505
|
-
end
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
# Permissions - set directly as hash (YAML doesn't use DSL block syntax)
|
|
509
|
-
self.permissions_hash = config[:permissions] if config[:permissions]
|
|
510
|
-
end
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
# Translate swarm-level hooks to DSL
|
|
514
|
-
#
|
|
515
|
-
# @param builder [Swarm::Builder] Swarm builder instance
|
|
516
|
-
# @return [void]
|
|
517
|
-
def translate_swarm_hooks(builder)
|
|
518
|
-
@swarm_hooks.each do |event, hook_specs|
|
|
519
|
-
Array(hook_specs).each do |spec|
|
|
520
|
-
if spec[:type] == "command"
|
|
521
|
-
builder.hook(event, command: spec[:command], timeout: spec[:timeout])
|
|
522
|
-
end
|
|
523
|
-
end
|
|
524
|
-
end
|
|
525
|
-
end
|
|
526
|
-
|
|
527
|
-
# Translate nodes to DSL
|
|
528
|
-
#
|
|
529
|
-
# @param builder [Swarm::Builder] Swarm builder instance
|
|
530
|
-
# @return [void]
|
|
531
|
-
def translate_nodes(builder)
|
|
532
|
-
@nodes.each do |node_name, node_config|
|
|
533
|
-
builder.node(node_name) do
|
|
534
|
-
# Translate agents
|
|
535
|
-
node_config[:agents]&.each do |agent_config|
|
|
536
|
-
agent_name = agent_config[:agent].to_sym
|
|
537
|
-
delegates = agent_config[:delegates_to] || []
|
|
538
|
-
reset_ctx = agent_config.key?(:reset_context) ? agent_config[:reset_context] : true
|
|
539
|
-
tools_override = agent_config[:tools]
|
|
540
|
-
|
|
541
|
-
# Build agent config with fluent API
|
|
542
|
-
agent_cfg = agent(agent_name, reset_context: reset_ctx)
|
|
543
|
-
|
|
544
|
-
# Apply delegation if present
|
|
545
|
-
agent_cfg = agent_cfg.delegates_to(*delegates) if delegates.any?
|
|
546
|
-
|
|
547
|
-
# Apply tools override if present
|
|
548
|
-
agent_cfg.tools(*tools_override) if tools_override # Return config (finalize will be called automatically)
|
|
549
|
-
end
|
|
550
|
-
|
|
551
|
-
# Translate dependencies
|
|
552
|
-
depends_on(*node_config[:dependencies]) if node_config[:dependencies]&.any?
|
|
553
|
-
|
|
554
|
-
# Translate lead override
|
|
555
|
-
lead(node_config[:lead].to_sym) if node_config[:lead]
|
|
556
|
-
|
|
557
|
-
# Translate transformers
|
|
558
|
-
if node_config[:input_command]
|
|
559
|
-
input_command(node_config[:input_command], timeout: node_config[:input_timeout] || 60)
|
|
560
|
-
end
|
|
561
|
-
|
|
562
|
-
if node_config[:output_command]
|
|
563
|
-
output_command(node_config[:output_command], timeout: node_config[:output_timeout] || 60)
|
|
564
|
-
end
|
|
565
|
-
end
|
|
566
|
-
end
|
|
567
|
-
end
|
|
568
|
-
|
|
569
|
-
# Resolve agent file path relative to base_dir
|
|
570
|
-
#
|
|
571
|
-
# @param file_path [String] Relative or absolute file path
|
|
572
|
-
# @return [String] Resolved absolute path
|
|
573
|
-
def resolve_agent_file_path(file_path)
|
|
574
|
-
return file_path if Pathname.new(file_path).absolute?
|
|
575
|
-
|
|
576
|
-
@base_dir.join(file_path).to_s
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
def detect_circular_dependencies
|
|
580
|
-
@agents.each_key do |agent_name|
|
|
581
|
-
visited = Set.new
|
|
582
|
-
path = []
|
|
583
|
-
detect_cycle_from(agent_name, visited, path)
|
|
584
|
-
end
|
|
585
|
-
end
|
|
586
|
-
|
|
587
|
-
def detect_cycle_from(agent_name, visited, path)
|
|
588
|
-
return if visited.include?(agent_name)
|
|
589
|
-
|
|
590
|
-
if path.include?(agent_name)
|
|
591
|
-
cycle_start = path.index(agent_name)
|
|
592
|
-
cycle = path[cycle_start..] + [agent_name]
|
|
593
|
-
raise CircularDependencyError, "Circular dependency detected: #{cycle.join(" -> ")}"
|
|
594
|
-
end
|
|
595
|
-
|
|
596
|
-
path.push(agent_name)
|
|
597
|
-
connections_for(agent_name).each do |connection|
|
|
598
|
-
connection_sym = connection.to_sym # Convert to symbol for lookup
|
|
599
|
-
|
|
600
|
-
# Skip external swarms - they are not local agents and don't have circular dependency issues
|
|
601
|
-
next if @external_swarms.key?(connection_sym)
|
|
602
|
-
|
|
603
|
-
unless @agents.key?(connection_sym)
|
|
604
|
-
raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{connection}' (not a local agent or registered swarm)"
|
|
605
|
-
end
|
|
606
|
-
|
|
607
|
-
detect_cycle_from(connection_sym, visited, path)
|
|
608
|
-
end
|
|
609
|
-
path.pop
|
|
610
|
-
visited.add(agent_name)
|
|
118
|
+
# This maintains backward compatibility with code that accesses
|
|
119
|
+
# @config_type, @agents, etc. directly via attr_readers.
|
|
120
|
+
def sync_from_parser
|
|
121
|
+
@config_type = @parser.config_type
|
|
122
|
+
@swarm_name = @parser.swarm_name
|
|
123
|
+
@swarm_id = @parser.swarm_id
|
|
124
|
+
@lead_agent = @parser.lead_agent
|
|
125
|
+
@start_node = @parser.start_node
|
|
126
|
+
@agents = @parser.agents
|
|
127
|
+
@all_agents_config = @parser.all_agents_config
|
|
128
|
+
@swarm_hooks = @parser.swarm_hooks
|
|
129
|
+
@all_agents_hooks = @parser.all_agents_hooks
|
|
130
|
+
@external_swarms = @parser.external_swarms
|
|
131
|
+
@nodes = @parser.nodes
|
|
132
|
+
@scratchpad_enabled = @parser.scratchpad_mode # NOTE: attr_reader says scratchpad_enabled
|
|
611
133
|
end
|
|
612
134
|
end
|
|
613
135
|
end
|
|
@@ -17,9 +17,9 @@ module SwarmSDK
|
|
|
17
17
|
# total_tokens = TokenCounter.estimate_messages(messages)
|
|
18
18
|
#
|
|
19
19
|
class TokenCounter
|
|
20
|
-
#
|
|
21
|
-
CHARS_PER_TOKEN_PROSE =
|
|
22
|
-
CHARS_PER_TOKEN_CODE =
|
|
20
|
+
# Backward compatibility aliases - use Defaults module for new code
|
|
21
|
+
CHARS_PER_TOKEN_PROSE = Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE
|
|
22
|
+
CHARS_PER_TOKEN_CODE = Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE
|
|
23
23
|
|
|
24
24
|
class << self
|
|
25
25
|
# Estimate tokens for a single message
|