swarm_sdk 2.0.0.pre.2 → 2.0.1
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 +118 -21
- data/lib/swarm_sdk/agent/definition.rb +121 -12
- data/lib/swarm_sdk/configuration.rb +44 -11
- data/lib/swarm_sdk/hooks/context.rb +34 -0
- data/lib/swarm_sdk/hooks/registry.rb +4 -0
- data/lib/swarm_sdk/log_collector.rb +3 -35
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +32 -3
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +81 -3
- data/lib/swarm_sdk/swarm/builder.rb +286 -21
- data/lib/swarm_sdk/swarm/tool_configurator.rb +1 -0
- data/lib/swarm_sdk/swarm.rb +71 -6
- data/lib/swarm_sdk/tools/delegate.rb +15 -3
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +73 -0
- metadata +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c45fe12dc7f0cc16c5e82d539f941d22cfc847919f108000b73a6ff259819d24
|
4
|
+
data.tar.gz: cd18d30692c2686c60c3e20e48081ecab97ab81f7bc9051da8bb9a3bf862a979
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c1d02a1552463ab920c2e851a47449e6ed6eec82cd8874781cbc37e526937094a0d4b4625ae68596abb1825224fd7021c918f68f32f9d5d900406bf381c883e
|
7
|
+
data.tar.gz: 85ba1af56c5fa6726565a8b65a43a5268aa01ee9381e1c12afc0d0ae75544f82a689bf2939b4b0a9213046c3e8efda84fa83c30a2f06f5bcbc4cd3c1b43fbbeb
|
@@ -46,50 +46,66 @@ module SwarmSDK
|
|
46
46
|
@mcp_servers = []
|
47
47
|
@include_default_tools = true
|
48
48
|
@bypass_permissions = false
|
49
|
-
@
|
49
|
+
@coding_agent = nil # nil = not set (will default to false in Definition)
|
50
50
|
@assume_model_exists = nil
|
51
51
|
@hooks = []
|
52
52
|
@permissions_config = {}
|
53
53
|
@default_permissions = {} # Set by SwarmBuilder from all_agents
|
54
54
|
end
|
55
55
|
|
56
|
-
# Set agent model
|
57
|
-
def model(model_name)
|
56
|
+
# Set/get agent model
|
57
|
+
def model(model_name = :__not_provided__)
|
58
|
+
return @model if model_name == :__not_provided__
|
59
|
+
|
58
60
|
@model = model_name
|
59
61
|
end
|
60
62
|
|
61
|
-
# Set provider
|
62
|
-
def provider(provider_name)
|
63
|
+
# Set/get provider
|
64
|
+
def provider(provider_name = :__not_provided__)
|
65
|
+
return @provider if provider_name == :__not_provided__
|
66
|
+
|
63
67
|
@provider = provider_name
|
64
68
|
end
|
65
69
|
|
66
|
-
# Set base URL
|
67
|
-
def base_url(url)
|
70
|
+
# Set/get base URL
|
71
|
+
def base_url(url = :__not_provided__)
|
72
|
+
return @base_url if url == :__not_provided__
|
73
|
+
|
68
74
|
@base_url = url
|
69
75
|
end
|
70
76
|
|
71
|
-
# Set API version (OpenAI-compatible providers only)
|
72
|
-
def api_version(version)
|
77
|
+
# Set/get API version (OpenAI-compatible providers only)
|
78
|
+
def api_version(version = :__not_provided__)
|
79
|
+
return @api_version if version == :__not_provided__
|
80
|
+
|
73
81
|
@api_version = version
|
74
82
|
end
|
75
83
|
|
76
|
-
# Set explicit context window override
|
77
|
-
def context_window(tokens)
|
84
|
+
# Set/get explicit context window override
|
85
|
+
def context_window(tokens = :__not_provided__)
|
86
|
+
return @context_window if tokens == :__not_provided__
|
87
|
+
|
78
88
|
@context_window = tokens
|
79
89
|
end
|
80
90
|
|
81
|
-
# Set LLM parameters
|
82
|
-
def parameters(params)
|
91
|
+
# Set/get LLM parameters
|
92
|
+
def parameters(params = :__not_provided__)
|
93
|
+
return @parameters if params == :__not_provided__
|
94
|
+
|
83
95
|
@parameters = params
|
84
96
|
end
|
85
97
|
|
86
|
-
# Set custom HTTP headers
|
87
|
-
def headers(header_hash)
|
98
|
+
# Set/get custom HTTP headers
|
99
|
+
def headers(header_hash = :__not_provided__)
|
100
|
+
return @headers if header_hash == :__not_provided__
|
101
|
+
|
88
102
|
@headers = header_hash
|
89
103
|
end
|
90
104
|
|
91
|
-
# Set timeout
|
92
|
-
def timeout(seconds)
|
105
|
+
# Set/get timeout
|
106
|
+
def timeout(seconds = :__not_provided__)
|
107
|
+
return @timeout if seconds == :__not_provided__
|
108
|
+
|
93
109
|
@timeout = seconds
|
94
110
|
end
|
95
111
|
|
@@ -118,9 +134,18 @@ module SwarmSDK
|
|
118
134
|
@bypass_permissions = enabled
|
119
135
|
end
|
120
136
|
|
121
|
-
# Set
|
122
|
-
|
123
|
-
|
137
|
+
# Set coding_agent flag
|
138
|
+
#
|
139
|
+
# When true, includes the base system prompt for coding tasks.
|
140
|
+
# When false (default), uses only the custom system prompt.
|
141
|
+
#
|
142
|
+
# @param enabled [Boolean] Whether to include base coding prompt
|
143
|
+
# @return [void]
|
144
|
+
#
|
145
|
+
# @example
|
146
|
+
# coding_agent true # Include base prompt for coding tasks
|
147
|
+
def coding_agent(enabled)
|
148
|
+
@coding_agent = enabled
|
124
149
|
end
|
125
150
|
|
126
151
|
# Set assume_model_exists flag
|
@@ -212,6 +237,78 @@ module SwarmSDK
|
|
212
237
|
@permissions_config = PermissionsBuilder.build(&block)
|
213
238
|
end
|
214
239
|
|
240
|
+
# Check if model has been explicitly set (not default)
|
241
|
+
#
|
242
|
+
# Used by Swarm::Builder to determine if all_agents model should apply.
|
243
|
+
#
|
244
|
+
# @return [Boolean] true if model was explicitly set
|
245
|
+
def model_set?
|
246
|
+
@model != "gpt-5"
|
247
|
+
end
|
248
|
+
|
249
|
+
# Check if provider has been explicitly set
|
250
|
+
#
|
251
|
+
# Used by Swarm::Builder to determine if all_agents provider should apply.
|
252
|
+
#
|
253
|
+
# @return [Boolean] true if provider was explicitly set
|
254
|
+
def provider_set?
|
255
|
+
!@provider.nil?
|
256
|
+
end
|
257
|
+
|
258
|
+
# Check if base_url has been explicitly set
|
259
|
+
#
|
260
|
+
# Used by Swarm::Builder to determine if all_agents base_url should apply.
|
261
|
+
#
|
262
|
+
# @return [Boolean] true if base_url was explicitly set
|
263
|
+
def base_url_set?
|
264
|
+
!@base_url.nil?
|
265
|
+
end
|
266
|
+
|
267
|
+
# Check if api_version has been explicitly set
|
268
|
+
#
|
269
|
+
# Used by Swarm::Builder to determine if all_agents api_version should apply.
|
270
|
+
#
|
271
|
+
# @return [Boolean] true if api_version was explicitly set
|
272
|
+
def api_version_set?
|
273
|
+
!@api_version.nil?
|
274
|
+
end
|
275
|
+
|
276
|
+
# Check if timeout has been explicitly set
|
277
|
+
#
|
278
|
+
# Used by Swarm::Builder to determine if all_agents timeout should apply.
|
279
|
+
#
|
280
|
+
# @return [Boolean] true if timeout was explicitly set
|
281
|
+
def timeout_set?
|
282
|
+
!@timeout.nil?
|
283
|
+
end
|
284
|
+
|
285
|
+
# Check if coding_agent has been explicitly set
|
286
|
+
#
|
287
|
+
# Used by Swarm::Builder to determine if all_agents coding_agent should apply.
|
288
|
+
#
|
289
|
+
# @return [Boolean] true if coding_agent was explicitly set
|
290
|
+
def coding_agent_set?
|
291
|
+
!@coding_agent.nil?
|
292
|
+
end
|
293
|
+
|
294
|
+
# Check if parameters have been set
|
295
|
+
#
|
296
|
+
# Used by Swarm::Builder for merging all_agents parameters.
|
297
|
+
#
|
298
|
+
# @return [Boolean] true if parameters were set
|
299
|
+
def parameters_set?
|
300
|
+
@parameters.any?
|
301
|
+
end
|
302
|
+
|
303
|
+
# Check if headers have been set
|
304
|
+
#
|
305
|
+
# Used by Swarm::Builder for merging all_agents headers.
|
306
|
+
#
|
307
|
+
# @return [Boolean] true if headers were set
|
308
|
+
def headers_set?
|
309
|
+
@headers.any?
|
310
|
+
end
|
311
|
+
|
215
312
|
# Build and return an Agent::Definition
|
216
313
|
#
|
217
314
|
# This method converts the builder's configuration into a validated
|
@@ -242,7 +339,7 @@ module SwarmSDK
|
|
242
339
|
agent_config[:mcp_servers] = @mcp_servers if @mcp_servers.any?
|
243
340
|
agent_config[:include_default_tools] = @include_default_tools
|
244
341
|
agent_config[:bypass_permissions] = @bypass_permissions
|
245
|
-
agent_config[:
|
342
|
+
agent_config[:coding_agent] = @coding_agent
|
246
343
|
agent_config[:assume_model_exists] = @assume_model_exists unless @assume_model_exists.nil?
|
247
344
|
agent_config[:permissions] = @permissions_config if @permissions_config.any?
|
248
345
|
agent_config[:default_permissions] = @default_permissions if @default_permissions.any?
|
@@ -39,7 +39,7 @@ module SwarmSDK
|
|
39
39
|
:headers,
|
40
40
|
:timeout,
|
41
41
|
:include_default_tools,
|
42
|
-
:
|
42
|
+
:coding_agent,
|
43
43
|
:default_permissions,
|
44
44
|
:agent_permissions,
|
45
45
|
:assume_model_exists,
|
@@ -83,8 +83,10 @@ module SwarmSDK
|
|
83
83
|
# include_default_tools defaults to true if not specified
|
84
84
|
@include_default_tools = config.key?(:include_default_tools) ? config[:include_default_tools] : true
|
85
85
|
|
86
|
-
#
|
87
|
-
|
86
|
+
# coding_agent defaults to false if not specified
|
87
|
+
# When true, includes the base system prompt for coding tasks
|
88
|
+
# When false, uses only the custom system prompt (no base prompt)
|
89
|
+
@coding_agent = config.key?(:coding_agent) ? config[:coding_agent] : false
|
88
90
|
|
89
91
|
# Parse directory first so it can be used in system prompt rendering
|
90
92
|
@directory = parse_directory(config[:directory])
|
@@ -132,26 +134,107 @@ module SwarmSDK
|
|
132
134
|
timeout: @timeout,
|
133
135
|
bypass_permissions: @bypass_permissions,
|
134
136
|
include_default_tools: @include_default_tools,
|
135
|
-
|
137
|
+
coding_agent: @coding_agent,
|
136
138
|
assume_model_exists: @assume_model_exists,
|
137
139
|
max_concurrent_tools: @max_concurrent_tools,
|
138
140
|
hooks: @hooks,
|
139
141
|
}.compact
|
140
142
|
end
|
141
143
|
|
144
|
+
# Validate agent configuration and return warnings (non-fatal issues)
|
145
|
+
#
|
146
|
+
# Unlike validate! which raises exceptions for critical errors, this method
|
147
|
+
# returns an array of warning hashes for non-fatal issues like:
|
148
|
+
# - Model not found in registry (informs user, suggests alternatives)
|
149
|
+
# - Context tracking unavailable (useful even with assume_model_exists)
|
150
|
+
#
|
151
|
+
# Note: Validation ALWAYS runs, even with assume_model_exists: true or base_url set.
|
152
|
+
# The purpose is to inform the user about potential issues and suggest corrections,
|
153
|
+
# not to block execution.
|
154
|
+
#
|
155
|
+
# @return [Array<Hash>] Array of warning hashes
|
156
|
+
def validate
|
157
|
+
warnings = []
|
158
|
+
|
159
|
+
# Always validate model (even with assume_model_exists)
|
160
|
+
# Warnings inform user about typos and context tracking limitations
|
161
|
+
model_warning = validate_model
|
162
|
+
warnings << model_warning if model_warning
|
163
|
+
|
164
|
+
# Future: could add tool validation, delegate validation, etc.
|
165
|
+
|
166
|
+
warnings
|
167
|
+
end
|
168
|
+
|
142
169
|
private
|
143
170
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
171
|
+
# Validate that model exists in RubyLLM registry
|
172
|
+
#
|
173
|
+
# @return [Hash, nil] Warning hash if model not found, nil otherwise
|
174
|
+
def validate_model
|
175
|
+
# Try to find model in registry (searches all providers)
|
176
|
+
RubyLLM.models.find(@model)
|
177
|
+
nil # Model exists
|
178
|
+
rescue StandardError => e
|
179
|
+
# Model not found - return warning with suggestions
|
180
|
+
{
|
181
|
+
type: :model_not_found,
|
182
|
+
agent: @name,
|
183
|
+
model: @model,
|
184
|
+
error_message: e.message,
|
185
|
+
suggestions: suggest_similar_models,
|
186
|
+
}
|
187
|
+
end
|
149
188
|
|
150
|
-
|
189
|
+
# Suggest similar models when a model is not found
|
190
|
+
#
|
191
|
+
# @return [Array<Hash>] Up to 3 similar models with their info
|
192
|
+
def suggest_similar_models
|
193
|
+
normalized_query = @model.to_s.downcase.gsub(/[.\-_]/, "")
|
194
|
+
|
195
|
+
RubyLLM.models.all.select do |model_info|
|
196
|
+
normalized_id = model_info.id.downcase.gsub(/[.\-_]/, "")
|
197
|
+
normalized_id.include?(normalized_query) ||
|
198
|
+
model_info.name&.downcase&.gsub(/[.\-_]/, "")&.include?(normalized_query)
|
199
|
+
end.first(3).map do |model_info|
|
200
|
+
{
|
201
|
+
id: model_info.id,
|
202
|
+
name: model_info.name,
|
203
|
+
context_window: model_info.context_window,
|
204
|
+
}
|
205
|
+
end
|
206
|
+
rescue StandardError
|
207
|
+
[]
|
208
|
+
end
|
151
209
|
|
152
|
-
|
210
|
+
def build_full_system_prompt(custom_prompt)
|
211
|
+
# If coding_agent is false (default), return custom prompt with optional TODO/Scratchpad info
|
212
|
+
# If coding_agent is true, include full base prompt for coding tasks
|
213
|
+
if @coding_agent
|
214
|
+
# Coding agent: include full base prompt
|
215
|
+
rendered_base = render_base_system_prompt
|
216
|
+
|
217
|
+
if custom_prompt && !custom_prompt.strip.empty?
|
218
|
+
"#{rendered_base}\n\n#{custom_prompt}"
|
219
|
+
else
|
220
|
+
rendered_base
|
221
|
+
end
|
222
|
+
elsif @include_default_tools
|
223
|
+
# Non-coding agent: optionally include TODO/Scratchpad sections if default tools available
|
224
|
+
non_coding_base = render_non_coding_base_prompt
|
153
225
|
|
154
|
-
|
226
|
+
if custom_prompt && !custom_prompt.strip.empty?
|
227
|
+
# Prepend TODO/Scratchpad info before custom prompt
|
228
|
+
"#{non_coding_base}\n\n#{custom_prompt}"
|
229
|
+
else
|
230
|
+
# No custom prompt: just return TODO/Scratchpad info
|
231
|
+
non_coding_base
|
232
|
+
end
|
233
|
+
# Default tools available: include TODO/Scratchpad instructions
|
234
|
+
else
|
235
|
+
# No default tools: return only custom prompt
|
236
|
+
(custom_prompt || "").to_s
|
237
|
+
end
|
155
238
|
end
|
156
239
|
|
157
240
|
def render_base_system_prompt
|
@@ -168,6 +251,32 @@ module SwarmSDK
|
|
168
251
|
ERB.new(template_content).result(binding)
|
169
252
|
end
|
170
253
|
|
254
|
+
def render_non_coding_base_prompt
|
255
|
+
# Simplified base prompt for non-coding agents
|
256
|
+
# Only includes TODO and Scratchpad tool information
|
257
|
+
# Does not steer towards coding tasks
|
258
|
+
<<~PROMPT.strip
|
259
|
+
# Task Management
|
260
|
+
|
261
|
+
You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool to track your progress and give visibility into your work.
|
262
|
+
|
263
|
+
When working on multi-step tasks:
|
264
|
+
1. Create a todo list with all known tasks before starting work
|
265
|
+
2. Mark each task as in_progress when you start it
|
266
|
+
3. Mark each task as completed IMMEDIATELY after finishing it
|
267
|
+
4. Complete ALL pending todos before finishing your response
|
268
|
+
|
269
|
+
# Scratchpad Storage
|
270
|
+
|
271
|
+
You have access to Scratchpad tools for storing and retrieving information:
|
272
|
+
- **ScratchpadWrite**: Store detailed outputs, analysis, or results that are too long for direct responses
|
273
|
+
- **ScratchpadRead**: Retrieve previously stored content
|
274
|
+
- **ScratchpadList**: List available scratchpad entries
|
275
|
+
|
276
|
+
Use the scratchpad to share information that would otherwise clutter your responses.
|
277
|
+
PROMPT
|
278
|
+
end
|
279
|
+
|
171
280
|
def parse_directory(directory_config)
|
172
281
|
directory_config ||= "."
|
173
282
|
File.expand_path(directory_config.to_s)
|
@@ -147,16 +147,31 @@ module SwarmSDK
|
|
147
147
|
swarm_agents = @config[:swarm][:agents]
|
148
148
|
|
149
149
|
swarm_agents.each do |name, agent_config|
|
150
|
-
|
151
|
-
|
152
|
-
#
|
153
|
-
#
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
150
|
+
# Support three formats:
|
151
|
+
# 1. String: assistant: "agents/assistant.md" (file path)
|
152
|
+
# 2. Hash with agent_file: assistant: { agent_file: "..." }
|
153
|
+
# 3. Hash with inline definition: assistant: { description: "...", model: "..." }
|
154
|
+
|
155
|
+
if agent_config.is_a?(String)
|
156
|
+
# Format 1: Direct file path as string
|
157
|
+
file_path = agent_config
|
158
|
+
merged_config = merge_all_agents_config({})
|
159
|
+
@agents[name] = load_agent_from_file(name, file_path, merged_config)
|
158
160
|
else
|
159
|
-
|
161
|
+
# Format 2 or 3: Hash configuration
|
162
|
+
agent_config ||= {}
|
163
|
+
|
164
|
+
# Merge all_agents_config into agent config
|
165
|
+
# Agent-specific config overrides all_agents config
|
166
|
+
merged_config = merge_all_agents_config(agent_config)
|
167
|
+
|
168
|
+
@agents[name] = if agent_config[:agent_file]
|
169
|
+
# Format 2: Hash with agent_file key
|
170
|
+
load_agent_from_file(name, agent_config[:agent_file], merged_config)
|
171
|
+
else
|
172
|
+
# Format 3: Inline definition
|
173
|
+
Agent::Definition.new(name, merged_config)
|
174
|
+
end
|
160
175
|
end
|
161
176
|
end
|
162
177
|
|
@@ -167,10 +182,19 @@ module SwarmSDK
|
|
167
182
|
|
168
183
|
# Merge all_agents config with agent-specific config
|
169
184
|
# Agent config takes precedence over all_agents config
|
185
|
+
#
|
186
|
+
# Merge strategy:
|
187
|
+
# - Arrays (tools, delegates_to): Concatenate
|
188
|
+
# - Hashes (parameters, headers): Merge (agent values override)
|
189
|
+
# - Scalars (model, provider, base_url, timeout, coding_agent): Agent overrides
|
190
|
+
#
|
191
|
+
# @param agent_config [Hash] Agent-specific configuration
|
192
|
+
# @return [Hash] Merged configuration
|
170
193
|
def merge_all_agents_config(agent_config)
|
171
194
|
merged = @all_agents_config.dup
|
172
195
|
|
173
|
-
# For arrays
|
196
|
+
# For arrays, concatenate
|
197
|
+
# For hashes, merge (agent values override)
|
174
198
|
# For scalars, agent value overrides
|
175
199
|
agent_config.each do |key, value|
|
176
200
|
case key
|
@@ -180,8 +204,17 @@ module SwarmSDK
|
|
180
204
|
when :delegates_to
|
181
205
|
# Concatenate delegates_to
|
182
206
|
merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
|
207
|
+
when :parameters
|
208
|
+
# Merge parameters: all_agents.parameters + agent.parameters
|
209
|
+
# Agent values override all_agents values for same keys
|
210
|
+
merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
|
211
|
+
when :headers
|
212
|
+
# Merge headers: all_agents.headers + agent.headers
|
213
|
+
# Agent values override all_agents values for same keys
|
214
|
+
merged[:headers] = (merged[:headers] || {}).merge(value || {})
|
183
215
|
else
|
184
|
-
# For everything else,
|
216
|
+
# For everything else (model, provider, base_url, timeout, coding_agent, etc.),
|
217
|
+
# agent value overrides all_agents value
|
185
218
|
merged[key] = value
|
186
219
|
end
|
187
220
|
end
|
@@ -158,6 +158,40 @@ module SwarmSDK
|
|
158
158
|
def finish_swarm(message)
|
159
159
|
Result.finish_swarm(message)
|
160
160
|
end
|
161
|
+
|
162
|
+
# Enter an interactive debugging breakpoint
|
163
|
+
#
|
164
|
+
# This method:
|
165
|
+
# 1. Emits a breakpoint_enter event (formatters can pause spinners)
|
166
|
+
# 2. Opens binding.irb for interactive debugging
|
167
|
+
# 3. Emits a breakpoint_exit event (formatters can resume spinners)
|
168
|
+
#
|
169
|
+
# @example Use in a hook
|
170
|
+
# hook(:pre_delegation) do |ctx|
|
171
|
+
# ctx.breakpoint # Pause execution and inspect context
|
172
|
+
# end
|
173
|
+
#
|
174
|
+
# @return [void]
|
175
|
+
def breakpoint
|
176
|
+
# Emit breakpoint_enter event
|
177
|
+
LogStream.emit(
|
178
|
+
type: "breakpoint_enter",
|
179
|
+
agent: @agent_name,
|
180
|
+
event: @event,
|
181
|
+
timestamp: Time.now.utc.iso8601,
|
182
|
+
)
|
183
|
+
|
184
|
+
# Enter interactive debugging
|
185
|
+
binding.irb # rubocop:disable Lint/Debugger
|
186
|
+
|
187
|
+
# Emit breakpoint_exit event
|
188
|
+
LogStream.emit(
|
189
|
+
type: "breakpoint_exit",
|
190
|
+
agent: @agent_name,
|
191
|
+
event: @event,
|
192
|
+
timestamp: Time.now.utc.iso8601,
|
193
|
+
)
|
194
|
+
end
|
161
195
|
end
|
162
196
|
end
|
163
197
|
end
|
@@ -41,6 +41,10 @@ module SwarmSDK
|
|
41
41
|
|
42
42
|
# Context events
|
43
43
|
:context_warning, # When context usage crosses threshold
|
44
|
+
|
45
|
+
# Debug events
|
46
|
+
:breakpoint_enter, # When entering interactive debugging (binding.irb)
|
47
|
+
:breakpoint_exit, # When exiting interactive debugging
|
44
48
|
].freeze
|
45
49
|
|
46
50
|
def initialize
|
@@ -14,30 +14,18 @@ module SwarmSDK
|
|
14
14
|
# puts JSON.generate(event)
|
15
15
|
# end
|
16
16
|
#
|
17
|
-
# # Freeze callbacks (after all registrations, before Async execution)
|
18
|
-
# LogCollector.freeze!
|
19
|
-
#
|
20
17
|
# # During execution, LogStream calls emit
|
21
18
|
# LogCollector.emit(type: "user_prompt", agent: :backend)
|
22
19
|
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# LogCollector is fiber-safe because:
|
26
|
-
# - All callbacks registered before Async execution starts
|
27
|
-
# - freeze! makes @callbacks immutable
|
28
|
-
# - emit() only reads the frozen array (no mutations)
|
20
|
+
# # After execution, reset for next use
|
21
|
+
# LogCollector.reset!
|
29
22
|
#
|
30
23
|
module LogCollector
|
31
24
|
class << self
|
32
25
|
# Register a callback to receive log events
|
33
26
|
#
|
34
|
-
# Must be called before freeze! is called.
|
35
|
-
#
|
36
27
|
# @yield [Hash] Log event entry
|
37
|
-
# @raise [StateError] If called after freeze!
|
38
28
|
def on_log(&block)
|
39
|
-
raise StateError, "Cannot register callbacks after LogCollector is frozen" if @frozen
|
40
|
-
|
41
29
|
@callbacks ||= []
|
42
30
|
@callbacks << block
|
43
31
|
end
|
@@ -47,36 +35,16 @@ module SwarmSDK
|
|
47
35
|
# @param entry [Hash] Log event entry
|
48
36
|
# @return [void]
|
49
37
|
def emit(entry)
|
50
|
-
# Use defensive copy for fiber safety
|
51
38
|
Array(@callbacks).each do |callback|
|
52
39
|
callback.call(entry)
|
53
40
|
end
|
54
41
|
end
|
55
42
|
|
56
|
-
#
|
57
|
-
#
|
58
|
-
# This prevents new callbacks from being registered and makes
|
59
|
-
# the array immutable for fiber safety.
|
60
|
-
#
|
61
|
-
# @return [void]
|
62
|
-
def freeze!
|
63
|
-
@callbacks&.freeze
|
64
|
-
@frozen = true
|
65
|
-
end
|
66
|
-
|
67
|
-
# Reset the collector (for test cleanup)
|
43
|
+
# Reset the collector (clears callbacks for next execution)
|
68
44
|
#
|
69
45
|
# @return [void]
|
70
46
|
def reset!
|
71
47
|
@callbacks = []
|
72
|
-
@frozen = false
|
73
|
-
end
|
74
|
-
|
75
|
-
# Check if collector is frozen
|
76
|
-
#
|
77
|
-
# @return [Boolean]
|
78
|
-
def frozen?
|
79
|
-
@frozen
|
80
48
|
end
|
81
49
|
end
|
82
50
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SwarmSDK
|
4
|
+
module Node
|
5
|
+
# AgentConfig provides fluent API for configuring agents within a node
|
6
|
+
#
|
7
|
+
# This class enables the chainable syntax:
|
8
|
+
# agent(:backend).delegates_to(:tester, :database)
|
9
|
+
#
|
10
|
+
# @example Basic delegation
|
11
|
+
# agent(:backend).delegates_to(:tester)
|
12
|
+
#
|
13
|
+
# @example No delegation (solo agent)
|
14
|
+
# agent(:planner)
|
15
|
+
class AgentConfig
|
16
|
+
attr_reader :agent_name
|
17
|
+
|
18
|
+
def initialize(agent_name, node_builder)
|
19
|
+
@agent_name = agent_name
|
20
|
+
@node_builder = node_builder
|
21
|
+
@delegates_to = []
|
22
|
+
@finalized = false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set delegation targets for this agent
|
26
|
+
#
|
27
|
+
# @param agent_names [Array<Symbol>] Names of agents to delegate to
|
28
|
+
# @return [self] For method chaining
|
29
|
+
def delegates_to(*agent_names)
|
30
|
+
@delegates_to = agent_names.map(&:to_sym)
|
31
|
+
finalize
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
# Finalize agent configuration (called automatically)
|
36
|
+
#
|
37
|
+
# Registers this agent configuration with the parent node builder.
|
38
|
+
# If delegates_to was never called, registers with empty delegation.
|
39
|
+
#
|
40
|
+
# @return [void]
|
41
|
+
def finalize
|
42
|
+
return if @finalized
|
43
|
+
|
44
|
+
@node_builder.register_agent(@agent_name, @delegates_to)
|
45
|
+
@finalized = true
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|