swarm_sdk 2.1.1 → 2.1.2
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/chat.rb +1 -1
- data/lib/swarm_sdk/agent/definition.rb +17 -1
- data/lib/swarm_sdk/node/agent_config.rb +7 -2
- data/lib/swarm_sdk/node/builder.rb +130 -35
- data/lib/swarm_sdk/node_context.rb +75 -0
- data/lib/swarm_sdk/node_orchestrator.rb +219 -12
- data/lib/swarm_sdk/plugin.rb +73 -1
- data/lib/swarm_sdk/result.rb +32 -6
- data/lib/swarm_sdk/swarm/builder.rb +1 -0
- data/lib/swarm_sdk/tools/delegate.rb +2 -2
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +3 -7
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a77daa5d8abcd0cae1b1edad441d93421d25ecbf15a0713697d9eabb6c4dc00e
|
|
4
|
+
data.tar.gz: 97c037f892d03992b12292a31bc3ab9359ccacead39d37466af84ef5660e4933
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7620b869014c9f1b889795310ec1d648cbcc55fc53a3cbb7023c37b403f83c57e5f3645b4f8ffdd38d60fa4877fa24d9ab3f0ccf0de3b2be6a45420516dbf50
|
|
7
|
+
data.tar.gz: bc081f3a444bb410a22dc2e5971f258dd43dadaa69d53558e799d036149412bfecf8d95396aa0f6e1f0c39cf6716b986722669265b32cc2b550f7daa0a6e9c2c
|
data/lib/swarm_sdk/agent/chat.rb
CHANGED
|
@@ -415,7 +415,7 @@ module SwarmSDK
|
|
|
415
415
|
|
|
416
416
|
# Handle nil response from provider (malformed API response)
|
|
417
417
|
if response.nil?
|
|
418
|
-
raise
|
|
418
|
+
raise StandardError, "Provider returned nil response. This usually indicates a malformed API response " \
|
|
419
419
|
"that couldn't be parsed.\n\n" \
|
|
420
420
|
"Provider: #{@provider.class.name}\n" \
|
|
421
421
|
"API Base: #{@provider.api_base}\n" \
|
|
@@ -158,10 +158,12 @@ module SwarmSDK
|
|
|
158
158
|
end
|
|
159
159
|
|
|
160
160
|
def to_h
|
|
161
|
-
|
|
161
|
+
# Core SDK configuration (always serialized)
|
|
162
|
+
base_config = {
|
|
162
163
|
name: @name,
|
|
163
164
|
description: @description,
|
|
164
165
|
model: SwarmSDK::Models.resolve_alias(@model), # Resolve model aliases
|
|
166
|
+
context_window: @context_window,
|
|
165
167
|
directory: @directory,
|
|
166
168
|
tools: @tools,
|
|
167
169
|
delegates_to: @delegates_to,
|
|
@@ -179,7 +181,21 @@ module SwarmSDK
|
|
|
179
181
|
assume_model_exists: @assume_model_exists,
|
|
180
182
|
max_concurrent_tools: @max_concurrent_tools,
|
|
181
183
|
hooks: @hooks,
|
|
184
|
+
# Permissions are core SDK functionality (not plugin-specific)
|
|
185
|
+
default_permissions: @default_permissions,
|
|
186
|
+
permissions: @agent_permissions,
|
|
182
187
|
}.compact
|
|
188
|
+
|
|
189
|
+
# Allow plugins to contribute their config for serialization
|
|
190
|
+
# This enables plugin features (memory, skills, etc.) to be preserved
|
|
191
|
+
# when cloning agents without SwarmSDK knowing about plugin-specific fields
|
|
192
|
+
plugin_configs = SwarmSDK::PluginRegistry.all.map do |plugin|
|
|
193
|
+
plugin.serialize_config(agent_definition: self)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Merge plugin configs into base config
|
|
197
|
+
# Later plugins override earlier ones if they have conflicting keys
|
|
198
|
+
plugin_configs.reduce(base_config) { |acc, config| acc.merge(config) }
|
|
183
199
|
end
|
|
184
200
|
|
|
185
201
|
# Validate agent configuration and return warnings (non-fatal issues)
|
|
@@ -6,19 +6,24 @@ module SwarmSDK
|
|
|
6
6
|
#
|
|
7
7
|
# This class enables the chainable syntax:
|
|
8
8
|
# agent(:backend).delegates_to(:tester, :database)
|
|
9
|
+
# agent(:backend, reset_context: false) # Preserve context across nodes
|
|
9
10
|
#
|
|
10
11
|
# @example Basic delegation
|
|
11
12
|
# agent(:backend).delegates_to(:tester)
|
|
12
13
|
#
|
|
13
14
|
# @example No delegation (solo agent)
|
|
14
15
|
# agent(:planner)
|
|
16
|
+
#
|
|
17
|
+
# @example Preserve agent context
|
|
18
|
+
# agent(:architect, reset_context: false)
|
|
15
19
|
class AgentConfig
|
|
16
20
|
attr_reader :agent_name
|
|
17
21
|
|
|
18
|
-
def initialize(agent_name, node_builder)
|
|
22
|
+
def initialize(agent_name, node_builder, reset_context: true)
|
|
19
23
|
@agent_name = agent_name
|
|
20
24
|
@node_builder = node_builder
|
|
21
25
|
@delegates_to = []
|
|
26
|
+
@reset_context = reset_context
|
|
22
27
|
@finalized = false
|
|
23
28
|
end
|
|
24
29
|
|
|
@@ -41,7 +46,7 @@ module SwarmSDK
|
|
|
41
46
|
def finalize
|
|
42
47
|
return if @finalized
|
|
43
48
|
|
|
44
|
-
@node_builder.register_agent(@agent_name, @delegates_to)
|
|
49
|
+
@node_builder.register_agent(@agent_name, @delegates_to, @reset_context)
|
|
45
50
|
@finalized = true
|
|
46
51
|
end
|
|
47
52
|
end
|
|
@@ -46,7 +46,11 @@ module SwarmSDK
|
|
|
46
46
|
# Returns an AgentConfig object that supports fluent delegation syntax.
|
|
47
47
|
# If delegates_to is not called, the agent is registered with no delegation.
|
|
48
48
|
#
|
|
49
|
+
# By default, agents get fresh context in each node (reset_context: true).
|
|
50
|
+
# Set reset_context: false to preserve conversation history across nodes.
|
|
51
|
+
#
|
|
49
52
|
# @param name [Symbol] Agent name
|
|
53
|
+
# @param reset_context [Boolean] Whether to reset agent context (default: true)
|
|
50
54
|
# @return [AgentConfig] Fluent configuration object
|
|
51
55
|
#
|
|
52
56
|
# @example With delegation
|
|
@@ -54,12 +58,15 @@ module SwarmSDK
|
|
|
54
58
|
#
|
|
55
59
|
# @example Without delegation
|
|
56
60
|
# agent(:planner)
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
#
|
|
62
|
+
# @example Preserve context across nodes
|
|
63
|
+
# agent(:architect, reset_context: false)
|
|
64
|
+
def agent(name, reset_context: true)
|
|
65
|
+
config = AgentConfig.new(name, self, reset_context: reset_context)
|
|
59
66
|
|
|
60
67
|
# Register immediately with empty delegation
|
|
61
68
|
# If delegates_to is called later, it will update this
|
|
62
|
-
register_agent(name, [])
|
|
69
|
+
register_agent(name, [], reset_context)
|
|
63
70
|
|
|
64
71
|
config
|
|
65
72
|
end
|
|
@@ -68,17 +75,19 @@ module SwarmSDK
|
|
|
68
75
|
#
|
|
69
76
|
# @param agent_name [Symbol] Agent name
|
|
70
77
|
# @param delegates_to [Array<Symbol>] Delegation targets
|
|
78
|
+
# @param reset_context [Boolean] Whether to reset agent context
|
|
71
79
|
# @return [void]
|
|
72
|
-
def register_agent(agent_name, delegates_to)
|
|
80
|
+
def register_agent(agent_name, delegates_to, reset_context = true)
|
|
73
81
|
# Check if agent already registered
|
|
74
82
|
existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
|
|
75
83
|
|
|
76
84
|
if existing
|
|
77
|
-
# Update delegation (happens when delegates_to is called after agent())
|
|
85
|
+
# Update delegation and reset_context (happens when delegates_to is called after agent())
|
|
78
86
|
existing[:delegates_to] = delegates_to
|
|
87
|
+
existing[:reset_context] = reset_context
|
|
79
88
|
else
|
|
80
89
|
# Add new agent configuration
|
|
81
|
-
@agent_configs << { agent: agent_name, delegates_to: delegates_to }
|
|
90
|
+
@agent_configs << { agent: agent_name, delegates_to: delegates_to, reset_context: reset_context }
|
|
82
91
|
end
|
|
83
92
|
end
|
|
84
93
|
|
|
@@ -120,12 +129,13 @@ module SwarmSDK
|
|
|
120
129
|
# Can also be used for side effects (logging, file I/O) since the block
|
|
121
130
|
# runs at execution time, not declaration time.
|
|
122
131
|
#
|
|
123
|
-
# **
|
|
124
|
-
#
|
|
125
|
-
#
|
|
132
|
+
# **Control Flow**: Return a hash with special keys to control execution:
|
|
133
|
+
# - `skip_execution: true` - Skip node's LLM execution, return content immediately
|
|
134
|
+
# - `halt_workflow: true` - Halt entire workflow with content as final result
|
|
135
|
+
# - `goto_node: :node_name` - Jump to different node with content as input
|
|
126
136
|
#
|
|
127
137
|
# @yield [NodeContext] Context with previous results and metadata
|
|
128
|
-
# @return [String, Hash] Transformed input OR
|
|
138
|
+
# @return [String, Hash] Transformed input OR control hash
|
|
129
139
|
#
|
|
130
140
|
# @example Access previous result and original prompt
|
|
131
141
|
# input do |ctx|
|
|
@@ -147,22 +157,26 @@ module SwarmSDK
|
|
|
147
157
|
# @example Skip execution (caching)
|
|
148
158
|
# input do |ctx|
|
|
149
159
|
# cached = check_cache(ctx.content)
|
|
150
|
-
# if cached
|
|
151
|
-
#
|
|
152
|
-
# { skip_execution: true, content: cached }
|
|
153
|
-
# else
|
|
154
|
-
# ctx.content
|
|
155
|
-
# end
|
|
160
|
+
# return ctx.skip_execution(content: cached) if cached
|
|
161
|
+
# ctx.content
|
|
156
162
|
# end
|
|
157
163
|
#
|
|
158
|
-
# @example
|
|
164
|
+
# @example Halt workflow (validation)
|
|
159
165
|
# input do |ctx|
|
|
160
166
|
# if ctx.content.length > 10000
|
|
161
|
-
# #
|
|
162
|
-
#
|
|
163
|
-
#
|
|
164
|
-
#
|
|
167
|
+
# # Halt entire workflow
|
|
168
|
+
# return ctx.halt_workflow(content: "ERROR: Input too long")
|
|
169
|
+
# end
|
|
170
|
+
# ctx.content
|
|
171
|
+
# end
|
|
172
|
+
#
|
|
173
|
+
# @example Jump to different node (conditional routing)
|
|
174
|
+
# input do |ctx|
|
|
175
|
+
# if ctx.content.include?("NEEDS_REVIEW")
|
|
176
|
+
# # Jump to review node instead
|
|
177
|
+
# return ctx.goto_node(:review, content: ctx.content)
|
|
165
178
|
# end
|
|
179
|
+
# ctx.content
|
|
166
180
|
# end
|
|
167
181
|
def input(&block)
|
|
168
182
|
@input_transformer = block
|
|
@@ -198,8 +212,12 @@ module SwarmSDK
|
|
|
198
212
|
# Can also be used for side effects (logging, file I/O) since the block
|
|
199
213
|
# runs at execution time, not declaration time.
|
|
200
214
|
#
|
|
215
|
+
# **Control Flow**: Return a hash with special keys to control execution:
|
|
216
|
+
# - `halt_workflow: true` - Halt entire workflow with content as final result
|
|
217
|
+
# - `goto_node: :node_name` - Jump to different node with content as input
|
|
218
|
+
#
|
|
201
219
|
# @yield [NodeContext] Context with current result and metadata
|
|
202
|
-
# @return [String] Transformed output
|
|
220
|
+
# @return [String, Hash] Transformed output OR control hash
|
|
203
221
|
#
|
|
204
222
|
# @example Transform and save to file
|
|
205
223
|
# output do |ctx|
|
|
@@ -216,12 +234,19 @@ module SwarmSDK
|
|
|
216
234
|
# "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
|
|
217
235
|
# end
|
|
218
236
|
#
|
|
219
|
-
# @example
|
|
237
|
+
# @example Halt workflow (convergence check)
|
|
220
238
|
# output do |ctx|
|
|
221
|
-
#
|
|
222
|
-
#
|
|
239
|
+
# return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
|
|
240
|
+
# ctx.content
|
|
241
|
+
# end
|
|
223
242
|
#
|
|
224
|
-
#
|
|
243
|
+
# @example Jump to different node (conditional routing)
|
|
244
|
+
# output do |ctx|
|
|
245
|
+
# if needs_revision?(ctx.content)
|
|
246
|
+
# # Go back to revision node
|
|
247
|
+
# return ctx.goto_node(:revision, content: ctx.content)
|
|
248
|
+
# end
|
|
249
|
+
# ctx.content
|
|
225
250
|
# end
|
|
226
251
|
def output(&block)
|
|
227
252
|
@output_transformer = block
|
|
@@ -264,6 +289,12 @@ module SwarmSDK
|
|
|
264
289
|
#
|
|
265
290
|
# Executes either Ruby block or bash command transformer.
|
|
266
291
|
#
|
|
292
|
+
# **Ruby block return values:**
|
|
293
|
+
# - String: Transformed content
|
|
294
|
+
# - Hash with `skip_execution: true`: Skip node execution
|
|
295
|
+
# - Hash with `halt_workflow: true`: Halt entire workflow
|
|
296
|
+
# - Hash with `goto_node: :name`: Jump to different node
|
|
297
|
+
#
|
|
267
298
|
# **Exit code behavior (bash commands only):**
|
|
268
299
|
# - Exit 0: Use STDOUT as transformed content
|
|
269
300
|
# - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
|
|
@@ -271,16 +302,23 @@ module SwarmSDK
|
|
|
271
302
|
#
|
|
272
303
|
# @param context [NodeContext] Context with previous results and metadata
|
|
273
304
|
# @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
|
|
274
|
-
# @return [String, Hash] Transformed input OR
|
|
305
|
+
# @return [String, Hash] Transformed input OR control hash (skip_execution, halt_workflow, goto_node)
|
|
275
306
|
# @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
|
|
276
307
|
def transform_input(context, current_input:)
|
|
277
308
|
# No transformer configured: return content as-is
|
|
278
309
|
return context.content unless @input_transformer || @input_transformer_command
|
|
279
310
|
|
|
280
311
|
# Ruby block transformer
|
|
281
|
-
# Ruby blocks can return String (transformed content) OR Hash (
|
|
312
|
+
# Ruby blocks can return String (transformed content) OR Hash (control flow)
|
|
282
313
|
if @input_transformer
|
|
283
|
-
|
|
314
|
+
result = @input_transformer.call(context)
|
|
315
|
+
|
|
316
|
+
# If hash, validate control flow keys
|
|
317
|
+
if result.is_a?(Hash)
|
|
318
|
+
validate_transformer_hash(result, :input)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
return result
|
|
284
322
|
end
|
|
285
323
|
|
|
286
324
|
# Bash command transformer
|
|
@@ -318,22 +356,34 @@ module SwarmSDK
|
|
|
318
356
|
#
|
|
319
357
|
# Executes either Ruby block or bash command transformer.
|
|
320
358
|
#
|
|
359
|
+
# **Ruby block return values:**
|
|
360
|
+
# - String: Transformed content
|
|
361
|
+
# - Hash with `halt_workflow: true`: Halt entire workflow
|
|
362
|
+
# - Hash with `goto_node: :name`: Jump to different node
|
|
363
|
+
#
|
|
321
364
|
# **Exit code behavior (bash commands only):**
|
|
322
365
|
# - Exit 0: Use STDOUT as transformed content
|
|
323
366
|
# - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
|
|
324
367
|
# - Exit 2: Halt workflow with error (STDOUT ignored)
|
|
325
368
|
#
|
|
326
369
|
# @param context [NodeContext] Context with current result and metadata
|
|
327
|
-
# @return [String] Transformed output
|
|
370
|
+
# @return [String, Hash] Transformed output OR control hash (halt_workflow, goto_node)
|
|
328
371
|
# @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
|
|
329
372
|
def transform_output(context)
|
|
330
373
|
# No transformer configured: return content as-is
|
|
331
374
|
return context.content unless @output_transformer || @output_transformer_command
|
|
332
375
|
|
|
333
376
|
# Ruby block transformer
|
|
334
|
-
#
|
|
377
|
+
# Ruby blocks can return String (transformed content) OR Hash (control flow)
|
|
335
378
|
if @output_transformer
|
|
336
|
-
|
|
379
|
+
result = @output_transformer.call(context)
|
|
380
|
+
|
|
381
|
+
# If hash, validate control flow keys
|
|
382
|
+
if result.is_a?(Hash)
|
|
383
|
+
validate_transformer_hash(result, :output)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
return result
|
|
337
387
|
end
|
|
338
388
|
|
|
339
389
|
# Bash command transformer
|
|
@@ -411,6 +461,50 @@ module SwarmSDK
|
|
|
411
461
|
|
|
412
462
|
private
|
|
413
463
|
|
|
464
|
+
# Validate transformer hash return value
|
|
465
|
+
#
|
|
466
|
+
# Ensures hash has valid control flow keys and required content field.
|
|
467
|
+
#
|
|
468
|
+
# @param hash [Hash] Hash returned from transformer
|
|
469
|
+
# @param transformer_type [Symbol] :input or :output
|
|
470
|
+
# @return [void]
|
|
471
|
+
# @raise [ConfigurationError] If hash is invalid
|
|
472
|
+
def validate_transformer_hash(hash, transformer_type)
|
|
473
|
+
# Valid control keys
|
|
474
|
+
valid_keys = if transformer_type == :input
|
|
475
|
+
[:skip_execution, :halt_workflow, :goto_node, :content]
|
|
476
|
+
else
|
|
477
|
+
[:halt_workflow, :goto_node, :content]
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# Check for invalid keys
|
|
481
|
+
invalid_keys = hash.keys - valid_keys
|
|
482
|
+
if invalid_keys.any?
|
|
483
|
+
raise ConfigurationError,
|
|
484
|
+
"Invalid #{transformer_type} transformer hash keys: #{invalid_keys.join(", ")}. " \
|
|
485
|
+
"Valid keys: #{valid_keys.join(", ")}"
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Ensure content is present
|
|
489
|
+
unless hash.key?(:content)
|
|
490
|
+
raise ConfigurationError,
|
|
491
|
+
"#{transformer_type.capitalize} transformer hash must include :content key"
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Ensure only one control key
|
|
495
|
+
control_keys = hash.keys & [:skip_execution, :halt_workflow, :goto_node]
|
|
496
|
+
if control_keys.size > 1
|
|
497
|
+
raise ConfigurationError,
|
|
498
|
+
"#{transformer_type.capitalize} transformer hash can only have one control key, got: #{control_keys.join(", ")}"
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Validate goto_node has valid node name
|
|
502
|
+
if hash[:goto_node] && !hash[:goto_node].is_a?(Symbol)
|
|
503
|
+
raise ConfigurationError,
|
|
504
|
+
"goto_node value must be a Symbol, got: #{hash[:goto_node].class}"
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
414
508
|
# Auto-add agents that are mentioned in delegates_to but not explicitly declared
|
|
415
509
|
#
|
|
416
510
|
# This allows:
|
|
@@ -418,7 +512,8 @@ module SwarmSDK
|
|
|
418
512
|
# Without needing:
|
|
419
513
|
# agent(:tester)
|
|
420
514
|
#
|
|
421
|
-
# The tester agent is automatically added to the node with no delegation
|
|
515
|
+
# The tester agent is automatically added to the node with no delegation
|
|
516
|
+
# and reset_context: true (fresh context by default).
|
|
422
517
|
#
|
|
423
518
|
# @return [void]
|
|
424
519
|
def auto_add_delegate_agents
|
|
@@ -429,9 +524,9 @@ module SwarmSDK
|
|
|
429
524
|
declared_agents = @agent_configs.map { |ac| ac[:agent] }
|
|
430
525
|
missing_delegates = all_delegates - declared_agents
|
|
431
526
|
|
|
432
|
-
# Auto-add missing delegates with empty delegation
|
|
527
|
+
# Auto-add missing delegates with empty delegation and default reset_context
|
|
433
528
|
missing_delegates.each do |delegate_name|
|
|
434
|
-
@agent_configs << { agent: delegate_name, delegates_to: [] }
|
|
529
|
+
@agent_configs << { agent: delegate_name, delegates_to: [], reset_context: true }
|
|
435
530
|
end
|
|
436
531
|
end
|
|
437
532
|
end
|
|
@@ -166,5 +166,80 @@ module SwarmSDK
|
|
|
166
166
|
@previous_result.success?
|
|
167
167
|
end
|
|
168
168
|
end
|
|
169
|
+
|
|
170
|
+
# Control flow methods for transformers
|
|
171
|
+
# These return special hashes that NodeOrchestrator recognizes
|
|
172
|
+
|
|
173
|
+
# Skip current node's LLM execution and return content immediately
|
|
174
|
+
#
|
|
175
|
+
# Only valid for input transformers.
|
|
176
|
+
#
|
|
177
|
+
# @param content [String] Content to return (skips LLM call)
|
|
178
|
+
# @return [Hash] Control hash for skip_execution
|
|
179
|
+
# @raise [ArgumentError] If content is nil
|
|
180
|
+
#
|
|
181
|
+
# @example
|
|
182
|
+
# input do |ctx|
|
|
183
|
+
# cached = check_cache(ctx.content)
|
|
184
|
+
# return ctx.skip_execution(content: cached) if cached
|
|
185
|
+
# ctx.content
|
|
186
|
+
# end
|
|
187
|
+
def skip_execution(content:)
|
|
188
|
+
if content.nil?
|
|
189
|
+
raise ArgumentError,
|
|
190
|
+
"skip_execution requires content (got nil). " \
|
|
191
|
+
"Check that ctx.content or your content source is not nil. " \
|
|
192
|
+
"Node: #{@node_name}"
|
|
193
|
+
end
|
|
194
|
+
{ skip_execution: true, content: content }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Halt entire workflow and return content as final result
|
|
198
|
+
#
|
|
199
|
+
# Valid for both input and output transformers.
|
|
200
|
+
#
|
|
201
|
+
# @param content [String] Final content to return
|
|
202
|
+
# @return [Hash] Control hash for halt_workflow
|
|
203
|
+
# @raise [ArgumentError] If content is nil
|
|
204
|
+
#
|
|
205
|
+
# @example
|
|
206
|
+
# output do |ctx|
|
|
207
|
+
# return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
|
|
208
|
+
# ctx.content
|
|
209
|
+
# end
|
|
210
|
+
def halt_workflow(content:)
|
|
211
|
+
if content.nil?
|
|
212
|
+
raise ArgumentError,
|
|
213
|
+
"halt_workflow requires content (got nil). " \
|
|
214
|
+
"Check that ctx.content or your content source is not nil. " \
|
|
215
|
+
"Node: #{@node_name}"
|
|
216
|
+
end
|
|
217
|
+
{ halt_workflow: true, content: content }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Jump to a different node with provided content as input
|
|
221
|
+
#
|
|
222
|
+
# Valid for both input and output transformers.
|
|
223
|
+
#
|
|
224
|
+
# @param node [Symbol] Node name to jump to
|
|
225
|
+
# @param content [String] Content to pass to target node
|
|
226
|
+
# @return [Hash] Control hash for goto_node
|
|
227
|
+
# @raise [ArgumentError] If content is nil
|
|
228
|
+
#
|
|
229
|
+
# @example
|
|
230
|
+
# input do |ctx|
|
|
231
|
+
# return ctx.goto_node(:review, content: ctx.content) if needs_review?(ctx.content)
|
|
232
|
+
# ctx.content
|
|
233
|
+
# end
|
|
234
|
+
def goto_node(node, content:)
|
|
235
|
+
if content.nil?
|
|
236
|
+
raise ArgumentError,
|
|
237
|
+
"goto_node requires content (got nil). " \
|
|
238
|
+
"Check that ctx.content or your content source is not nil. " \
|
|
239
|
+
"This often happens when the previous node failed with an error. " \
|
|
240
|
+
"Node: #{@node_name}, Target: #{node}"
|
|
241
|
+
end
|
|
242
|
+
{ goto_node: node.to_sym, content: content }
|
|
243
|
+
end
|
|
169
244
|
end
|
|
170
245
|
end
|
|
@@ -20,16 +20,28 @@ module SwarmSDK
|
|
|
20
20
|
class NodeOrchestrator
|
|
21
21
|
attr_reader :swarm_name, :nodes, :start_node
|
|
22
22
|
|
|
23
|
-
def initialize(swarm_name:, agent_definitions:, nodes:, start_node:)
|
|
23
|
+
def initialize(swarm_name:, agent_definitions:, nodes:, start_node:, scratchpad_enabled: true)
|
|
24
24
|
@swarm_name = swarm_name
|
|
25
25
|
@agent_definitions = agent_definitions
|
|
26
26
|
@nodes = nodes
|
|
27
27
|
@start_node = start_node
|
|
28
|
+
@scratchpad_enabled = scratchpad_enabled
|
|
29
|
+
@agent_instance_cache = {} # Cache for preserving agent context across nodes
|
|
28
30
|
|
|
29
31
|
validate!
|
|
30
32
|
@execution_order = build_execution_order
|
|
31
33
|
end
|
|
32
34
|
|
|
35
|
+
# Alias for compatibility with Swarm interface
|
|
36
|
+
alias_method :name, :swarm_name
|
|
37
|
+
|
|
38
|
+
# Return the lead agent of the start node for CLI compatibility
|
|
39
|
+
#
|
|
40
|
+
# @return [Symbol] Lead agent of the start node
|
|
41
|
+
def lead_agent
|
|
42
|
+
@nodes[@start_node].lead_agent
|
|
43
|
+
end
|
|
44
|
+
|
|
33
45
|
# Execute the node workflow
|
|
34
46
|
#
|
|
35
47
|
# Executes nodes in topological order, passing output from each node
|
|
@@ -56,7 +68,12 @@ module SwarmSDK
|
|
|
56
68
|
LogStream.emitter = LogCollector
|
|
57
69
|
end
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
# Dynamic execution with support for goto_node
|
|
72
|
+
execution_index = 0
|
|
73
|
+
last_result = nil
|
|
74
|
+
|
|
75
|
+
while execution_index < @execution_order.size
|
|
76
|
+
node_name = @execution_order[execution_index]
|
|
60
77
|
node = @nodes[node_name]
|
|
61
78
|
node_start_time = Time.now
|
|
62
79
|
|
|
@@ -102,13 +119,26 @@ module SwarmSDK
|
|
|
102
119
|
# - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
|
|
103
120
|
transformed = node.transform_input(input_context, current_input: current_input)
|
|
104
121
|
|
|
105
|
-
# Check
|
|
106
|
-
|
|
107
|
-
|
|
122
|
+
# Check for control flow from transformer
|
|
123
|
+
control_result = handle_transformer_control_flow(
|
|
124
|
+
transformed: transformed,
|
|
125
|
+
node_name: node_name,
|
|
126
|
+
node: node,
|
|
127
|
+
node_start_time: node_start_time,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
case control_result[:action]
|
|
131
|
+
when :halt
|
|
132
|
+
return control_result[:result]
|
|
133
|
+
when :goto
|
|
134
|
+
execution_index = find_node_index(control_result[:target])
|
|
135
|
+
current_input = control_result[:content]
|
|
136
|
+
next
|
|
137
|
+
when :skip
|
|
108
138
|
skip_execution = true
|
|
109
|
-
skip_content =
|
|
110
|
-
|
|
111
|
-
current_input =
|
|
139
|
+
skip_content = control_result[:content]
|
|
140
|
+
when :continue
|
|
141
|
+
current_input = control_result[:content]
|
|
112
142
|
end
|
|
113
143
|
end
|
|
114
144
|
|
|
@@ -130,6 +160,9 @@ module SwarmSDK
|
|
|
130
160
|
mini_swarm = build_swarm_for_node(node)
|
|
131
161
|
result = mini_swarm.execute(current_input)
|
|
132
162
|
|
|
163
|
+
# Cache agent instances for context preservation
|
|
164
|
+
cache_agent_instances(mini_swarm, node)
|
|
165
|
+
|
|
133
166
|
# If result has error, log it with backtrace
|
|
134
167
|
if result.error
|
|
135
168
|
RubyLLM.logger.error("NodeOrchestrator: Node '#{node_name}' failed: #{result.error.message}")
|
|
@@ -138,6 +171,7 @@ module SwarmSDK
|
|
|
138
171
|
end
|
|
139
172
|
|
|
140
173
|
results[node_name] = result
|
|
174
|
+
last_result = result
|
|
141
175
|
|
|
142
176
|
# Transform output for next node using NodeContext
|
|
143
177
|
output_context = NodeContext.for_output(
|
|
@@ -146,7 +180,29 @@ module SwarmSDK
|
|
|
146
180
|
original_prompt: @original_prompt,
|
|
147
181
|
node_name: node_name,
|
|
148
182
|
)
|
|
149
|
-
|
|
183
|
+
transformed_output = node.transform_output(output_context)
|
|
184
|
+
|
|
185
|
+
# Check for control flow from output transformer
|
|
186
|
+
control_result = handle_output_transformer_control_flow(
|
|
187
|
+
transformed: transformed_output,
|
|
188
|
+
node_name: node_name,
|
|
189
|
+
node: node,
|
|
190
|
+
node_start_time: node_start_time,
|
|
191
|
+
skip_execution: skip_execution,
|
|
192
|
+
result: result,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
case control_result[:action]
|
|
196
|
+
when :halt
|
|
197
|
+
return control_result[:result]
|
|
198
|
+
when :goto
|
|
199
|
+
execution_index = find_node_index(control_result[:target])
|
|
200
|
+
current_input = control_result[:content]
|
|
201
|
+
emit_node_stop(node_name, node, result, Time.now - node_start_time, skip_execution)
|
|
202
|
+
next
|
|
203
|
+
when :continue
|
|
204
|
+
current_input = control_result[:content]
|
|
205
|
+
end
|
|
150
206
|
|
|
151
207
|
# For agent-less nodes, update the result with transformed content
|
|
152
208
|
# This ensures all_results contains the actual output, not the input
|
|
@@ -158,14 +214,17 @@ module SwarmSDK
|
|
|
158
214
|
duration: result.duration,
|
|
159
215
|
error: result.error,
|
|
160
216
|
)
|
|
217
|
+
last_result = results[node_name]
|
|
161
218
|
end
|
|
162
219
|
|
|
163
220
|
# Emit node_stop event
|
|
164
221
|
node_duration = Time.now - node_start_time
|
|
165
222
|
emit_node_stop(node_name, node, result, node_duration, skip_execution)
|
|
223
|
+
|
|
224
|
+
execution_index += 1
|
|
166
225
|
end
|
|
167
226
|
|
|
168
|
-
|
|
227
|
+
last_result
|
|
169
228
|
ensure
|
|
170
229
|
# Reset logging state for next execution
|
|
171
230
|
LogCollector.reset!
|
|
@@ -284,10 +343,18 @@ module SwarmSDK
|
|
|
284
343
|
# Creates a new Swarm with only the agents specified in the node,
|
|
285
344
|
# configured with the node's delegation topology.
|
|
286
345
|
#
|
|
346
|
+
# For agents with reset_context: false, injects cached instances
|
|
347
|
+
# to preserve conversation history across nodes.
|
|
348
|
+
#
|
|
349
|
+
# Inherits scratchpad_enabled setting from NodeOrchestrator.
|
|
350
|
+
#
|
|
287
351
|
# @param node [Node::Builder] Node configuration
|
|
288
352
|
# @return [Swarm] Configured swarm instance
|
|
289
353
|
def build_swarm_for_node(node)
|
|
290
|
-
swarm = Swarm.new(
|
|
354
|
+
swarm = Swarm.new(
|
|
355
|
+
name: "#{@swarm_name}:#{node.name}",
|
|
356
|
+
scratchpad_enabled: @scratchpad_enabled,
|
|
357
|
+
)
|
|
291
358
|
|
|
292
359
|
# Add each agent specified in this node
|
|
293
360
|
node.agent_configs.each do |config|
|
|
@@ -306,6 +373,9 @@ module SwarmSDK
|
|
|
306
373
|
# Set lead agent
|
|
307
374
|
swarm.lead = node.lead_agent
|
|
308
375
|
|
|
376
|
+
# Inject cached agent instances for context preservation
|
|
377
|
+
inject_cached_agents(swarm, node)
|
|
378
|
+
|
|
309
379
|
swarm
|
|
310
380
|
end
|
|
311
381
|
|
|
@@ -359,7 +429,8 @@ module SwarmSDK
|
|
|
359
429
|
if order.size < @nodes.size
|
|
360
430
|
unprocessed = @nodes.keys - order
|
|
361
431
|
raise CircularDependencyError,
|
|
362
|
-
"Circular dependency detected. Unprocessed nodes: #{unprocessed.join(", ")}"
|
|
432
|
+
"Circular dependency detected. Unprocessed nodes: #{unprocessed.join(", ")}. " \
|
|
433
|
+
"Use goto_node in transformers to create loops instead of circular depends_on."
|
|
363
434
|
end
|
|
364
435
|
|
|
365
436
|
# Verify start_node is in the execution order
|
|
@@ -380,5 +451,141 @@ module SwarmSDK
|
|
|
380
451
|
|
|
381
452
|
order
|
|
382
453
|
end
|
|
454
|
+
|
|
455
|
+
# Handle control flow from input transformer
|
|
456
|
+
#
|
|
457
|
+
# @param transformed [String, Hash] Result from transformer
|
|
458
|
+
# @param node_name [Symbol] Current node name
|
|
459
|
+
# @param node [Node::Builder] Node configuration
|
|
460
|
+
# @param node_start_time [Time] Node execution start time
|
|
461
|
+
# @return [Hash] Control result with :action and relevant data
|
|
462
|
+
def handle_transformer_control_flow(transformed:, node_name:, node:, node_start_time:)
|
|
463
|
+
return { action: :continue, content: transformed } unless transformed.is_a?(Hash)
|
|
464
|
+
|
|
465
|
+
if transformed[:halt_workflow]
|
|
466
|
+
# Halt entire workflow
|
|
467
|
+
halt_result = Result.new(
|
|
468
|
+
content: transformed[:content],
|
|
469
|
+
agent: "halted:#{node_name}",
|
|
470
|
+
logs: [],
|
|
471
|
+
duration: Time.now - node_start_time,
|
|
472
|
+
)
|
|
473
|
+
emit_node_stop(node_name, node, halt_result, Time.now - node_start_time, false)
|
|
474
|
+
{ action: :halt, result: halt_result }
|
|
475
|
+
elsif transformed[:goto_node]
|
|
476
|
+
# Jump to different node
|
|
477
|
+
{ action: :goto, target: transformed[:goto_node], content: transformed[:content] }
|
|
478
|
+
elsif transformed[:skip_execution]
|
|
479
|
+
# Skip node execution
|
|
480
|
+
{ action: :skip, content: transformed[:content] }
|
|
481
|
+
else
|
|
482
|
+
# No control flow - continue normally
|
|
483
|
+
{ action: :continue, content: transformed[:content] }
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
# Handle control flow from output transformer
|
|
488
|
+
#
|
|
489
|
+
# @param transformed [String, Hash] Result from transformer
|
|
490
|
+
# @param node_name [Symbol] Current node name
|
|
491
|
+
# @param node [Node::Builder] Node configuration
|
|
492
|
+
# @param node_start_time [Time] Node execution start time
|
|
493
|
+
# @param skip_execution [Boolean] Whether node execution was skipped
|
|
494
|
+
# @param result [Result] Node execution result
|
|
495
|
+
# @return [Hash] Control result with :action and relevant data
|
|
496
|
+
def handle_output_transformer_control_flow(transformed:, node_name:, node:, node_start_time:, skip_execution:, result:)
|
|
497
|
+
# If not a hash, it's just transformed content - continue normally
|
|
498
|
+
return { action: :continue, content: transformed } unless transformed.is_a?(Hash)
|
|
499
|
+
|
|
500
|
+
if transformed[:halt_workflow]
|
|
501
|
+
# Halt entire workflow
|
|
502
|
+
halt_result = Result.new(
|
|
503
|
+
content: transformed[:content],
|
|
504
|
+
agent: result.agent,
|
|
505
|
+
logs: result.logs,
|
|
506
|
+
duration: result.duration,
|
|
507
|
+
)
|
|
508
|
+
emit_node_stop(node_name, node, halt_result, Time.now - node_start_time, skip_execution)
|
|
509
|
+
{ action: :halt, result: halt_result }
|
|
510
|
+
elsif transformed[:goto_node]
|
|
511
|
+
# Jump to different node
|
|
512
|
+
{ action: :goto, target: transformed[:goto_node], content: transformed[:content] }
|
|
513
|
+
else
|
|
514
|
+
# Hash without control flow keys - treat as regular hash with :content key
|
|
515
|
+
# This handles the case where transformer returns a hash that's not for control flow
|
|
516
|
+
{ action: :continue, content: transformed[:content] || transformed }
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Find the index of a node in the execution order
|
|
521
|
+
#
|
|
522
|
+
# @param node_name [Symbol] Node name to find
|
|
523
|
+
# @return [Integer] Index in execution order
|
|
524
|
+
# @raise [ConfigurationError] If node not found
|
|
525
|
+
def find_node_index(node_name)
|
|
526
|
+
index = @execution_order.index(node_name)
|
|
527
|
+
unless index
|
|
528
|
+
raise ConfigurationError,
|
|
529
|
+
"goto_node target '#{node_name}' not found. Available nodes: #{@execution_order.join(", ")}"
|
|
530
|
+
end
|
|
531
|
+
index
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
# Cache agent instances from a swarm for potential reuse
|
|
535
|
+
#
|
|
536
|
+
# Only caches agents that have reset_context: false in this node.
|
|
537
|
+
# This allows preserving conversation history across nodes.
|
|
538
|
+
#
|
|
539
|
+
# @param swarm [Swarm] Swarm instance that just executed
|
|
540
|
+
# @param node [Node::Builder] Node configuration
|
|
541
|
+
# @return [void]
|
|
542
|
+
def cache_agent_instances(swarm, node)
|
|
543
|
+
return unless swarm.agents # Only cache if agents were initialized
|
|
544
|
+
|
|
545
|
+
node.agent_configs.each do |config|
|
|
546
|
+
agent_name = config[:agent]
|
|
547
|
+
reset_context = config[:reset_context]
|
|
548
|
+
|
|
549
|
+
# Only cache if reset_context is false
|
|
550
|
+
next if reset_context
|
|
551
|
+
|
|
552
|
+
# Cache the agent instance
|
|
553
|
+
agent_instance = swarm.agents[agent_name]
|
|
554
|
+
@agent_instance_cache[agent_name] = agent_instance if agent_instance
|
|
555
|
+
end
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Inject cached agent instances into a swarm
|
|
559
|
+
#
|
|
560
|
+
# For agents with reset_context: false, reuses cached instances to preserve context.
|
|
561
|
+
# Forces agent initialization first (by accessing .agents), then swaps in cached instances.
|
|
562
|
+
#
|
|
563
|
+
# @param swarm [Swarm] Swarm instance to inject into
|
|
564
|
+
# @param node [Node::Builder] Node configuration
|
|
565
|
+
# @return [void]
|
|
566
|
+
def inject_cached_agents(swarm, node)
|
|
567
|
+
# Check if any agents need context preservation
|
|
568
|
+
has_preserved_agents = node.agent_configs.any? { |c| !c[:reset_context] && @agent_instance_cache[c[:agent]] }
|
|
569
|
+
return unless has_preserved_agents
|
|
570
|
+
|
|
571
|
+
# Force agent initialization by accessing .agents (triggers lazy init)
|
|
572
|
+
# Then inject cached instances
|
|
573
|
+
agents_hash = swarm.agents
|
|
574
|
+
|
|
575
|
+
node.agent_configs.each do |config|
|
|
576
|
+
agent_name = config[:agent]
|
|
577
|
+
reset_context = config[:reset_context]
|
|
578
|
+
|
|
579
|
+
# Skip if reset_context is true (want fresh instance)
|
|
580
|
+
next if reset_context
|
|
581
|
+
|
|
582
|
+
# Check if we have a cached instance
|
|
583
|
+
cached_agent = @agent_instance_cache[agent_name]
|
|
584
|
+
next unless cached_agent
|
|
585
|
+
|
|
586
|
+
# Inject the cached instance (replace the freshly initialized one)
|
|
587
|
+
agents_hash[agent_name] = cached_agent
|
|
588
|
+
end
|
|
589
|
+
end
|
|
383
590
|
end
|
|
384
591
|
end
|
data/lib/swarm_sdk/plugin.rb
CHANGED
|
@@ -7,7 +7,32 @@ module SwarmSDK
|
|
|
7
7
|
# Plugins are self-registering - they call SwarmSDK::PluginRegistry.register
|
|
8
8
|
# when the gem is loaded.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
10
|
+
# ## Adding Custom Attributes to Agents
|
|
11
|
+
#
|
|
12
|
+
# Plugins can add custom attributes to Agent::Definition that are preserved
|
|
13
|
+
# when agents are cloned (e.g., in NodeOrchestrator). To do this:
|
|
14
|
+
#
|
|
15
|
+
# 1. Add attr_reader to Agent::Definition for your attribute
|
|
16
|
+
# 2. Parse the attribute in Agent::Definition#initialize
|
|
17
|
+
# 3. Implement serialize_config to preserve it during serialization
|
|
18
|
+
#
|
|
19
|
+
# @example Plugin with custom agent attributes
|
|
20
|
+
# # 1. Extend Agent::Definition (in your plugin gem)
|
|
21
|
+
# module SwarmSDK
|
|
22
|
+
# module Agent
|
|
23
|
+
# class Definition
|
|
24
|
+
# attr_reader :my_custom_config
|
|
25
|
+
#
|
|
26
|
+
# alias_method :original_initialize, :initialize
|
|
27
|
+
# def initialize(name, config = {})
|
|
28
|
+
# @my_custom_config = config[:my_custom_config]
|
|
29
|
+
# original_initialize(name, config)
|
|
30
|
+
# end
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # 2. Implement plugin with serialize_config
|
|
11
36
|
# class MyPlugin < SwarmSDK::Plugin
|
|
12
37
|
# def name
|
|
13
38
|
# :my_plugin
|
|
@@ -20,9 +45,34 @@ module SwarmSDK
|
|
|
20
45
|
# def create_tool(tool_name, context)
|
|
21
46
|
# # Create and return tool instance
|
|
22
47
|
# end
|
|
48
|
+
#
|
|
49
|
+
# # Preserve custom config when agents are cloned
|
|
50
|
+
# def serialize_config(agent_definition:)
|
|
51
|
+
# return {} unless agent_definition.my_custom_config
|
|
52
|
+
#
|
|
53
|
+
# { my_custom_config: agent_definition.my_custom_config }
|
|
54
|
+
# end
|
|
23
55
|
# end
|
|
24
56
|
#
|
|
25
57
|
# SwarmSDK::PluginRegistry.register(MyPlugin.new)
|
|
58
|
+
#
|
|
59
|
+
# Now agents can use your custom config:
|
|
60
|
+
#
|
|
61
|
+
# agent :researcher do
|
|
62
|
+
# my_custom_config { option: "value" }
|
|
63
|
+
# end
|
|
64
|
+
#
|
|
65
|
+
# And it will be preserved when NodeOrchestrator clones the agent!
|
|
66
|
+
#
|
|
67
|
+
# @example Real-world: SwarmMemory plugin
|
|
68
|
+
# # SwarmMemory adds 'memory' attribute to agents
|
|
69
|
+
# class SDKPlugin < SwarmSDK::Plugin
|
|
70
|
+
# def serialize_config(agent_definition:)
|
|
71
|
+
# return {} unless agent_definition.memory
|
|
72
|
+
# { memory: agent_definition.memory }
|
|
73
|
+
# end
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
26
76
|
class Plugin
|
|
27
77
|
# Plugin name (must be unique)
|
|
28
78
|
#
|
|
@@ -143,5 +193,27 @@ module SwarmSDK
|
|
|
143
193
|
def on_user_message(agent_name:, prompt:, is_first_message:)
|
|
144
194
|
[]
|
|
145
195
|
end
|
|
196
|
+
|
|
197
|
+
# Contribute to agent serialization (optional)
|
|
198
|
+
#
|
|
199
|
+
# Called when Agent::Definition.to_h is invoked (e.g., for cloning agents
|
|
200
|
+
# in NodeOrchestrator). Plugins can return config keys that should be
|
|
201
|
+
# included in the serialized hash to preserve their state.
|
|
202
|
+
#
|
|
203
|
+
# This allows plugins to maintain their configuration when agents are
|
|
204
|
+
# cloned or serialized, without SwarmSDK needing to know about plugin-specific fields.
|
|
205
|
+
#
|
|
206
|
+
# @param agent_definition [Agent::Definition] Agent definition
|
|
207
|
+
# @return [Hash] Config keys to include in to_h (e.g., { memory: config })
|
|
208
|
+
#
|
|
209
|
+
# @example Memory plugin serialization
|
|
210
|
+
# def serialize_config(agent_definition:)
|
|
211
|
+
# return {} unless agent_definition.memory
|
|
212
|
+
#
|
|
213
|
+
# { memory: agent_definition.memory }
|
|
214
|
+
# end
|
|
215
|
+
def serialize_config(agent_definition:)
|
|
216
|
+
{}
|
|
217
|
+
end
|
|
146
218
|
end
|
|
147
219
|
end
|
data/lib/swarm_sdk/result.rb
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmSDK
|
|
4
4
|
class Result
|
|
5
|
-
attr_reader :content, :agent, :
|
|
5
|
+
attr_reader :content, :agent, :duration, :logs, :error, :metadata
|
|
6
6
|
|
|
7
|
-
def initialize(content: nil, agent:, cost:
|
|
7
|
+
def initialize(content: nil, agent:, cost: nil, tokens: nil, duration: 0.0, logs: [], error: nil, metadata: {})
|
|
8
8
|
@content = content
|
|
9
9
|
@agent = agent
|
|
10
|
-
@cost = cost
|
|
11
|
-
@tokens = tokens
|
|
12
10
|
@duration = duration
|
|
13
11
|
@logs = logs
|
|
14
12
|
@error = error
|
|
15
13
|
@metadata = metadata
|
|
14
|
+
# Legacy parameters kept for backward compatibility but not stored
|
|
15
|
+
# Use total_cost and tokens methods instead which calculate from logs
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def success?
|
|
@@ -23,12 +23,38 @@ module SwarmSDK
|
|
|
23
23
|
!success?
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Calculate total cost from logs
|
|
27
|
+
#
|
|
28
|
+
# Delegates to total_cost for consistency. This attribute is calculated
|
|
29
|
+
# dynamically rather than stored.
|
|
30
|
+
#
|
|
31
|
+
# @return [Float] Total cost in dollars
|
|
32
|
+
def cost
|
|
33
|
+
total_cost
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Get token breakdown from logs
|
|
37
|
+
#
|
|
38
|
+
# Returns input and output tokens from the last log entry with usage data.
|
|
39
|
+
# This attribute is calculated dynamically rather than stored.
|
|
40
|
+
#
|
|
41
|
+
# @return [Hash] Token breakdown with :input and :output keys, or empty hash if no usage data
|
|
42
|
+
def tokens
|
|
43
|
+
last_entry = @logs.reverse.find { |entry| entry.dig(:usage, :cumulative_input_tokens) }
|
|
44
|
+
return {} unless last_entry
|
|
45
|
+
|
|
46
|
+
{
|
|
47
|
+
input: last_entry.dig(:usage, :cumulative_input_tokens) || 0,
|
|
48
|
+
output: last_entry.dig(:usage, :cumulative_output_tokens) || 0,
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
|
|
26
52
|
def to_h
|
|
27
53
|
{
|
|
28
54
|
content: @content,
|
|
29
55
|
agent: @agent,
|
|
30
|
-
cost:
|
|
31
|
-
tokens:
|
|
56
|
+
cost: cost,
|
|
57
|
+
tokens: tokens,
|
|
32
58
|
duration: @duration,
|
|
33
59
|
success: success?,
|
|
34
60
|
error: @error&.message,
|
|
@@ -43,8 +43,8 @@ module SwarmSDK
|
|
|
43
43
|
@delegate_target = delegate_name.to_s
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
-
#
|
|
47
|
-
description
|
|
46
|
+
# Override description to return dynamic string based on delegate
|
|
47
|
+
def description
|
|
48
48
|
"Delegate tasks to #{@delegate_name}. #{@delegate_description}"
|
|
49
49
|
end
|
|
50
50
|
|
data/lib/swarm_sdk/version.rb
CHANGED
data/lib/swarm_sdk.rb
CHANGED
|
@@ -21,19 +21,15 @@ require_relative "swarm_sdk/version"
|
|
|
21
21
|
|
|
22
22
|
require "zeitwerk"
|
|
23
23
|
loader = Zeitwerk::Loader.new
|
|
24
|
+
loader.tag = File.basename(__FILE__, ".rb")
|
|
24
25
|
loader.push_dir("#{__dir__}/swarm_sdk", namespace: SwarmSDK)
|
|
26
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
25
27
|
loader.inflector.inflect(
|
|
26
28
|
"cli" => "CLI",
|
|
29
|
+
"openai_with_responses" => "OpenAIWithResponses",
|
|
27
30
|
)
|
|
28
31
|
loader.setup
|
|
29
32
|
|
|
30
|
-
# Load plugin system explicitly (core infrastructure)
|
|
31
|
-
require_relative "swarm_sdk/plugin"
|
|
32
|
-
require_relative "swarm_sdk/plugin_registry"
|
|
33
|
-
|
|
34
|
-
# Load custom providers explicitly (Zeitwerk doesn't eager load by default)
|
|
35
|
-
require_relative "swarm_sdk/providers/openai_with_responses"
|
|
36
|
-
|
|
37
33
|
module SwarmSDK
|
|
38
34
|
class Error < StandardError; end
|
|
39
35
|
class ConfigurationError < Error; end
|