claude_swarm 1.0.4 → 1.0.6
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/CHANGELOG.md +24 -0
- data/Rakefile +4 -4
- data/docs/v2/CHANGELOG.swarm_cli.md +19 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +19 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +92 -0
- data/docs/v2/README.md +56 -22
- data/docs/v2/guides/MEMORY_DEFRAG_GUIDE.md +811 -0
- data/docs/v2/guides/complete-tutorial.md +115 -3
- data/docs/v2/guides/getting-started.md +6 -6
- data/docs/v2/guides/rails-integration.md +6 -6
- data/docs/v2/reference/architecture-flow.md +407 -0
- data/docs/v2/reference/event_payload_structures.md +471 -0
- data/docs/v2/reference/execution-flow.md +600 -0
- data/docs/v2/reference/ruby-dsl.md +138 -5
- data/docs/v2/reference/swarm_memory_technical_details.md +2090 -0
- data/examples/v2/swarm_with_hooks.yml +1 -1
- data/lib/claude_swarm/cli.rb +9 -11
- data/lib/claude_swarm/commands/ps.rb +1 -2
- data/lib/claude_swarm/configuration.rb +2 -3
- data/lib/claude_swarm/mcp_generator.rb +4 -10
- data/lib/claude_swarm/orchestrator.rb +43 -44
- data/lib/claude_swarm/system_utils.rb +4 -4
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +4 -9
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
- data/lib/swarm_cli/config_loader.rb +14 -13
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_cli.rb +2 -0
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
- data/lib/swarm_memory/core/storage.rb +66 -6
- data/lib/swarm_memory/integration/sdk_plugin.rb +14 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
- data/lib/swarm_memory/tools/memory_edit.rb +1 -0
- data/lib/swarm_memory/tools/memory_glob.rb +24 -1
- data/lib/swarm_memory/tools/memory_write.rb +2 -2
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +2 -0
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/definition.rb +18 -21
- data/lib/swarm_sdk/configuration.rb +34 -10
- data/lib/swarm_sdk/mcp.rb +16 -0
- 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/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/result.rb +32 -6
- data/lib/swarm_sdk/swarm/builder.rb +1 -0
- data/lib/swarm_sdk/swarm.rb +32 -50
- data/lib/swarm_sdk/tools/delegate.rb +2 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +20 -8
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +332 -27
- data/swarm_sdk.gemspec +1 -1
- metadata +9 -3
|
@@ -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
|
|
@@ -69,139 +69,15 @@ When making changes to files, first understand the file's conventions. Mimic exi
|
|
|
69
69
|
- When you edit something, first look at the surrounding context (especially imports/requires) to understand the choice of frameworks and libraries. Then consider how to make the given change in a way that is most consistent with existing patterns.
|
|
70
70
|
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to repositories.
|
|
71
71
|
|
|
72
|
-
# Task Management
|
|
73
|
-
|
|
74
|
-
You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
|
|
75
|
-
This tool is also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
|
|
76
|
-
|
|
77
|
-
**CRITICAL WORKFLOW**: When starting a multi-step task:
|
|
78
|
-
1. **FIRST**: Analyze what needs to be done (search, read files, understand scope)
|
|
79
|
-
2. **SECOND**: Create a COMPLETE todo list with ALL known tasks before starting work
|
|
80
|
-
3. **THIRD**: Begin executing tasks, marking them in_progress → completed as you work
|
|
81
|
-
4. **ONLY add new todos** if you discover unexpected work during implementation
|
|
82
|
-
|
|
83
|
-
**CRITICAL RULES FOR TODO COMPLETION**:
|
|
84
|
-
- Mark EACH task as completed IMMEDIATELY after finishing it (do not batch updates)
|
|
85
|
-
- You MUST complete ALL pending todos before giving your final answer to the user
|
|
86
|
-
- If a task becomes irrelevant, remove it from the list or mark it completed with a note
|
|
87
|
-
- NEVER leave in_progress or pending tasks when you finish responding to the user
|
|
88
|
-
- Before giving your final response, verify all todos are marked completed
|
|
89
|
-
|
|
90
|
-
Examples:
|
|
91
|
-
|
|
92
|
-
<example>
|
|
93
|
-
user: Run the build and fix any type errors
|
|
94
|
-
assistant: I'll run the build first to identify all type errors, then create a complete todo list.
|
|
95
|
-
|
|
96
|
-
[Runs build and finds 3 type errors in 3 different files]
|
|
97
|
-
|
|
98
|
-
Now I'll create a complete todo list with all the work:
|
|
99
|
-
|
|
100
|
-
[Uses TodoWrite to create full list:]
|
|
101
|
-
1. Fix type error in auth.ts:45 (in_progress)
|
|
102
|
-
2. Fix type error in user.ts:23 (pending)
|
|
103
|
-
3. Fix type error in api.ts:67 (pending)
|
|
104
|
-
4. Run build again to verify all fixes (pending)
|
|
105
|
-
|
|
106
|
-
Starting with the first error in auth.ts...
|
|
107
|
-
[Fixes auth.ts error]
|
|
108
|
-
|
|
109
|
-
[Updates TodoWrite - marks task 1 completed, task 2 in_progress]
|
|
110
|
-
|
|
111
|
-
Now fixing user.ts...
|
|
112
|
-
[Fixes user.ts error]
|
|
113
|
-
|
|
114
|
-
[Updates TodoWrite - marks task 2 completed, task 3 in_progress]
|
|
115
|
-
|
|
116
|
-
Now fixing api.ts...
|
|
117
|
-
[Fixes api.ts error]
|
|
118
|
-
|
|
119
|
-
[Updates TodoWrite - marks task 3 completed, task 4 in_progress]
|
|
120
|
-
|
|
121
|
-
Running build to verify...
|
|
122
|
-
[Runs build - all errors fixed!]
|
|
123
|
-
|
|
124
|
-
[Updates TodoWrite - marks task 4 completed]
|
|
125
|
-
|
|
126
|
-
All type errors have been fixed and the build passes successfully.
|
|
127
|
-
</example>
|
|
128
|
-
In this example, the assistant: (1) analyzed scope first, (2) created COMPLETE todo list, (3) marked EACH task completed immediately after finishing it, (4) completed ALL tasks before giving final response.
|
|
129
|
-
|
|
130
|
-
<example>
|
|
131
|
-
user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
|
|
132
|
-
|
|
133
|
-
A: I'll help you implement a usage metrics tracking and export feature. Let me first research the codebase to understand the scope, then create a complete todo list.
|
|
134
|
-
|
|
135
|
-
[Searches for existing metrics/telemetry code, examines architecture, identifies integration points]
|
|
136
|
-
|
|
137
|
-
Based on my analysis, here's the complete implementation plan:
|
|
138
|
-
|
|
139
|
-
[Uses TodoWrite to create full list:]
|
|
140
|
-
1. Create MetricsCollector class in src/metrics/ (in_progress)
|
|
141
|
-
2. Add database schema for metrics storage (pending)
|
|
142
|
-
3. Implement CSV export format (pending)
|
|
143
|
-
4. Implement JSON export format (pending)
|
|
144
|
-
5. Create MetricsService API endpoints (pending)
|
|
145
|
-
6. Add frontend UI for export (pending)
|
|
146
|
-
7. Write unit tests for collectors and exporters (pending)
|
|
147
|
-
8. Update documentation (pending)
|
|
148
|
-
|
|
149
|
-
Now starting with the MetricsCollector class...
|
|
150
|
-
[Implements MetricsCollector]
|
|
151
|
-
|
|
152
|
-
[Updates TodoWrite - marks task 1 completed, task 2 in_progress]
|
|
153
|
-
|
|
154
|
-
Adding database schema...
|
|
155
|
-
[Creates schema]
|
|
156
|
-
|
|
157
|
-
[Updates TodoWrite - marks task 2 completed, task 3 in_progress]
|
|
158
|
-
|
|
159
|
-
Implementing CSV export...
|
|
160
|
-
[Implements CSV exporter]
|
|
161
|
-
|
|
162
|
-
[Updates TodoWrite - marks task 3 completed, task 4 in_progress]
|
|
163
|
-
|
|
164
|
-
Implementing JSON export...
|
|
165
|
-
[Implements JSON exporter]
|
|
166
|
-
|
|
167
|
-
[Updates TodoWrite - marks task 4 completed, task 5 in_progress]
|
|
168
|
-
|
|
169
|
-
Creating MetricsService API endpoints...
|
|
170
|
-
[Implements API endpoints]
|
|
171
|
-
|
|
172
|
-
[Updates TodoWrite - marks task 5 completed, task 6 in_progress]
|
|
173
|
-
|
|
174
|
-
Adding frontend UI for export...
|
|
175
|
-
[Implements UI components]
|
|
176
|
-
|
|
177
|
-
[Updates TodoWrite - marks task 6 completed, task 7 in_progress]
|
|
178
|
-
|
|
179
|
-
Writing unit tests...
|
|
180
|
-
[Writes and runs tests - all pass]
|
|
181
|
-
|
|
182
|
-
[Updates TodoWrite - marks task 7 completed, task 8 in_progress]
|
|
183
|
-
|
|
184
|
-
Updating documentation...
|
|
185
|
-
[Updates docs with usage examples]
|
|
186
|
-
|
|
187
|
-
[Updates TodoWrite - marks task 8 completed]
|
|
188
|
-
|
|
189
|
-
The metrics tracking and export feature is now complete. Users can collect metrics and export them to CSV or JSON formats through both the API and the frontend UI.
|
|
190
|
-
</example>
|
|
191
|
-
|
|
192
72
|
# Doing tasks
|
|
193
73
|
|
|
194
74
|
The user will primarily request you perform tasks. This includes solving problems, adding new functionality, refactoring, explaining content, and more. For these tasks the following steps are recommended:
|
|
195
75
|
|
|
196
|
-
- Use the TodoWrite tool to plan the task if required
|
|
197
76
|
- Use the available search tools to understand the context and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
|
|
198
77
|
- Implement the solution using all tools available to you
|
|
199
|
-
- Mark each todo completed IMMEDIATELY after finishing it
|
|
200
78
|
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the project documentation or search to determine the testing approach.
|
|
201
79
|
- When you have completed a task, if there are linting or validation commands available to you, run them to ensure your work is correct. NEVER assume what these commands are - check the project documentation first.
|
|
202
80
|
NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
|
|
203
|
-
- Before giving your final response: Ensure ALL todos are marked completed. NEVER leave pending or in_progress tasks.
|
|
204
|
-
- IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
|
|
205
81
|
|
|
206
82
|
# Tool usage policy
|
|
207
83
|
|
|
@@ -211,8 +87,6 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
|
|
|
211
87
|
- If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to delegate a task to multiple agents in parallel, send a single message with multiple DelegateTask tool calls.
|
|
212
88
|
- Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit/MultiEdit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
|
|
213
89
|
|
|
214
|
-
IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
|
|
215
|
-
|
|
216
90
|
You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
|
|
217
91
|
|
|
218
92
|
|
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,
|