swarm_memory 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/cli.rb +9 -11
  3. data/lib/claude_swarm/commands/ps.rb +1 -2
  4. data/lib/claude_swarm/configuration.rb +30 -7
  5. data/lib/claude_swarm/mcp_generator.rb +4 -10
  6. data/lib/claude_swarm/orchestrator.rb +43 -44
  7. data/lib/claude_swarm/system_utils.rb +4 -4
  8. data/lib/claude_swarm/version.rb +1 -1
  9. data/lib/claude_swarm.rb +5 -9
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
  12. data/lib/swarm_cli/config_loader.rb +14 -13
  13. data/lib/swarm_cli/version.rb +1 -1
  14. data/lib/swarm_cli.rb +2 -0
  15. data/lib/swarm_memory/adapters/base.rb +4 -4
  16. data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
  17. data/lib/swarm_memory/core/storage.rb +66 -6
  18. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  19. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  20. data/lib/swarm_memory/integration/sdk_plugin.rb +24 -4
  21. data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
  22. data/lib/swarm_memory/tools/memory_edit.rb +3 -2
  23. data/lib/swarm_memory/tools/memory_glob.rb +24 -1
  24. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  25. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  26. data/lib/swarm_memory/tools/memory_write.rb +2 -2
  27. data/lib/swarm_memory/version.rb +1 -1
  28. data/lib/swarm_memory.rb +7 -0
  29. data/lib/swarm_sdk/agent/builder.rb +33 -0
  30. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  31. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  32. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  33. data/lib/swarm_sdk/agent/chat.rb +199 -52
  34. data/lib/swarm_sdk/agent/context.rb +6 -2
  35. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  36. data/lib/swarm_sdk/agent/definition.rb +32 -23
  37. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  38. data/lib/swarm_sdk/configuration.rb +420 -103
  39. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  40. data/lib/swarm_sdk/log_collector.rb +31 -5
  41. data/lib/swarm_sdk/log_stream.rb +37 -8
  42. data/lib/swarm_sdk/model_aliases.json +4 -1
  43. data/lib/swarm_sdk/node/agent_config.rb +39 -9
  44. data/lib/swarm_sdk/node/builder.rb +158 -42
  45. data/lib/swarm_sdk/node_context.rb +75 -0
  46. data/lib/swarm_sdk/node_orchestrator.rb +492 -18
  47. data/lib/swarm_sdk/plugin.rb +73 -1
  48. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  49. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  50. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  51. data/lib/swarm_sdk/restore_result.rb +65 -0
  52. data/lib/swarm_sdk/result.rb +32 -6
  53. data/lib/swarm_sdk/snapshot.rb +156 -0
  54. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  55. data/lib/swarm_sdk/state_restorer.rb +491 -0
  56. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  57. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  58. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  59. data/lib/swarm_sdk/swarm/builder.rb +208 -11
  60. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  61. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  62. data/lib/swarm_sdk/swarm.rb +367 -90
  63. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  64. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  65. data/lib/swarm_sdk/tools/delegate.rb +94 -9
  66. data/lib/swarm_sdk/tools/read.rb +17 -5
  67. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  68. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  69. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  70. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  71. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  72. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  73. data/lib/swarm_sdk/tools/think.rb +4 -1
  74. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  75. data/lib/swarm_sdk/utils.rb +18 -0
  76. data/lib/swarm_sdk/validation_result.rb +33 -0
  77. data/lib/swarm_sdk/version.rb +1 -1
  78. data/lib/swarm_sdk.rb +365 -28
  79. metadata +17 -5
@@ -43,10 +43,14 @@ module SwarmSDK
43
43
 
44
44
  # Configure an agent for this node
45
45
  #
46
- # Returns an AgentConfig object that supports fluent delegation syntax.
47
- # If delegates_to is not called, the agent is registered with no delegation.
46
+ # Returns an AgentConfig object that supports fluent delegation and tool override syntax.
47
+ # If delegates_to/tools are not called, the agent uses global configuration.
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.
48
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,21 @@ module SwarmSDK
54
58
  #
55
59
  # @example Without delegation
56
60
  # agent(:planner)
57
- def agent(name)
58
- config = AgentConfig.new(name, self)
61
+ #
62
+ # @example Preserve context across nodes
63
+ # agent(:architect, reset_context: false)
64
+ #
65
+ # @example Override tools for this node
66
+ # agent(:backend).tools(:Read, :Think)
67
+ #
68
+ # @example Combine delegation and tools
69
+ # agent(:backend).delegates_to(:tester).tools(:Read, :Edit, :Write)
70
+ def agent(name, reset_context: true)
71
+ config = AgentConfig.new(name, self, reset_context: reset_context)
59
72
 
60
- # Register immediately with empty delegation
61
- # If delegates_to is called later, it will update this
62
- register_agent(name, [])
73
+ # Register immediately with empty delegation and no tool override
74
+ # If delegates_to/tools are called later, they will update this
75
+ register_agent(name, [], reset_context, nil)
63
76
 
64
77
  config
65
78
  end
@@ -68,17 +81,26 @@ module SwarmSDK
68
81
  #
69
82
  # @param agent_name [Symbol] Agent name
70
83
  # @param delegates_to [Array<Symbol>] Delegation targets
84
+ # @param reset_context [Boolean] Whether to reset agent context
85
+ # @param tools [Array<Symbol>, nil] Tool override for this node (nil = use global)
71
86
  # @return [void]
72
- def register_agent(agent_name, delegates_to)
87
+ def register_agent(agent_name, delegates_to, reset_context = true, tools = nil)
73
88
  # Check if agent already registered
74
89
  existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
75
90
 
76
91
  if existing
77
- # Update delegation (happens when delegates_to is called after agent())
92
+ # Update delegation, reset_context, and tools (happens when methods are called after agent())
78
93
  existing[:delegates_to] = delegates_to
94
+ existing[:reset_context] = reset_context
95
+ existing[:tools] = tools unless tools.nil?
79
96
  else
80
97
  # Add new agent configuration
81
- @agent_configs << { agent: agent_name, delegates_to: delegates_to }
98
+ @agent_configs << {
99
+ agent: agent_name,
100
+ delegates_to: delegates_to,
101
+ reset_context: reset_context,
102
+ tools: tools,
103
+ }
82
104
  end
83
105
  end
84
106
 
@@ -120,12 +142,13 @@ module SwarmSDK
120
142
  # Can also be used for side effects (logging, file I/O) since the block
121
143
  # runs at execution time, not declaration time.
122
144
  #
123
- # **Skip Execution**: Return a hash with `skip_execution: true` to skip
124
- # the node's swarm execution and immediately return the provided content.
125
- # Useful for caching, validation, or conditional execution.
145
+ # **Control Flow**: Return a hash with special keys to control execution:
146
+ # - `skip_execution: true` - Skip node's LLM execution, return content immediately
147
+ # - `halt_workflow: true` - Halt entire workflow with content as final result
148
+ # - `goto_node: :node_name` - Jump to different node with content as input
126
149
  #
127
150
  # @yield [NodeContext] Context with previous results and metadata
128
- # @return [String, Hash] Transformed input OR skip hash
151
+ # @return [String, Hash] Transformed input OR control hash
129
152
  #
130
153
  # @example Access previous result and original prompt
131
154
  # input do |ctx|
@@ -144,28 +167,36 @@ module SwarmSDK
144
167
  # "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
145
168
  # end
146
169
  #
147
- # @example Skip execution (caching)
170
+ # @example Skip execution (caching) - using return
148
171
  # input do |ctx|
149
172
  # cached = check_cache(ctx.content)
150
- # if cached
151
- # # Skip LLM call, return cached result
152
- # { skip_execution: true, content: cached }
153
- # else
154
- # ctx.content
155
- # end
173
+ # return ctx.skip_execution(content: cached) if cached
174
+ # ctx.content
156
175
  # end
157
176
  #
158
- # @example Skip execution (validation)
177
+ # @example Halt workflow (validation) - using return
159
178
  # input do |ctx|
160
179
  # if ctx.content.length > 10000
161
- # # Fail early without LLM call
162
- # { skip_execution: true, content: "ERROR: Input too long" }
163
- # else
164
- # ctx.content
180
+ # # Halt entire workflow - return works safely!
181
+ # return ctx.halt_workflow(content: "ERROR: Input too long")
165
182
  # end
183
+ # ctx.content
166
184
  # end
185
+ #
186
+ # @example Jump to different node (conditional routing) - using return
187
+ # input do |ctx|
188
+ # if ctx.content.include?("NEEDS_REVIEW")
189
+ # # Jump to review node instead - return works safely!
190
+ # return ctx.goto_node(:review, content: ctx.content)
191
+ # end
192
+ # ctx.content
193
+ # end
194
+ #
195
+ # @note The input block is automatically converted to a lambda, which means
196
+ # return statements work safely and only exit the transformer, not the
197
+ # entire program. This allows natural control flow patterns.
167
198
  def input(&block)
168
- @input_transformer = block
199
+ @input_transformer = ProcHelpers.to_lambda(block)
169
200
  end
170
201
 
171
202
  # Set input transformer as bash command (YAML API)
@@ -198,8 +229,12 @@ module SwarmSDK
198
229
  # Can also be used for side effects (logging, file I/O) since the block
199
230
  # runs at execution time, not declaration time.
200
231
  #
232
+ # **Control Flow**: Return a hash with special keys to control execution:
233
+ # - `halt_workflow: true` - Halt entire workflow with content as final result
234
+ # - `goto_node: :node_name` - Jump to different node with content as input
235
+ #
201
236
  # @yield [NodeContext] Context with current result and metadata
202
- # @return [String] Transformed output
237
+ # @return [String, Hash] Transformed output OR control hash
203
238
  #
204
239
  # @example Transform and save to file
205
240
  # output do |ctx|
@@ -216,15 +251,26 @@ module SwarmSDK
216
251
  # "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
217
252
  # end
218
253
  #
219
- # @example Access multiple node results
254
+ # @example Halt workflow (convergence check) - using return
220
255
  # output do |ctx|
221
- # plan = ctx.all_results[:planning].content
222
- # impl = ctx.content
256
+ # return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
257
+ # ctx.content
258
+ # end
223
259
  #
224
- # "Completed:\nPlan: #{plan}\nImpl: #{impl}"
260
+ # @example Jump to different node (conditional routing) - using return
261
+ # output do |ctx|
262
+ # if needs_revision?(ctx.content)
263
+ # # Go back to revision node - return works safely!
264
+ # return ctx.goto_node(:revision, content: ctx.content)
265
+ # end
266
+ # ctx.content
225
267
  # end
268
+ #
269
+ # @note The output block is automatically converted to a lambda, which means
270
+ # return statements work safely and only exit the transformer, not the
271
+ # entire program. This allows natural control flow patterns.
226
272
  def output(&block)
227
- @output_transformer = block
273
+ @output_transformer = ProcHelpers.to_lambda(block)
228
274
  end
229
275
 
230
276
  # Set output transformer as bash command (YAML API)
@@ -264,6 +310,12 @@ module SwarmSDK
264
310
  #
265
311
  # Executes either Ruby block or bash command transformer.
266
312
  #
313
+ # **Ruby block return values:**
314
+ # - String: Transformed content
315
+ # - Hash with `skip_execution: true`: Skip node execution
316
+ # - Hash with `halt_workflow: true`: Halt entire workflow
317
+ # - Hash with `goto_node: :name`: Jump to different node
318
+ #
267
319
  # **Exit code behavior (bash commands only):**
268
320
  # - Exit 0: Use STDOUT as transformed content
269
321
  # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
@@ -271,16 +323,23 @@ module SwarmSDK
271
323
  #
272
324
  # @param context [NodeContext] Context with previous results and metadata
273
325
  # @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
274
- # @return [String, Hash] Transformed input OR skip hash `{ skip_execution: true, content: "..." }`
326
+ # @return [String, Hash] Transformed input OR control hash (skip_execution, halt_workflow, goto_node)
275
327
  # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
276
328
  def transform_input(context, current_input:)
277
329
  # No transformer configured: return content as-is
278
330
  return context.content unless @input_transformer || @input_transformer_command
279
331
 
280
332
  # Ruby block transformer
281
- # Ruby blocks can return String (transformed content) OR Hash (skip_execution)
333
+ # Ruby blocks can return String (transformed content) OR Hash (control flow)
282
334
  if @input_transformer
283
- return @input_transformer.call(context)
335
+ result = @input_transformer.call(context)
336
+
337
+ # If hash, validate control flow keys
338
+ if result.is_a?(Hash)
339
+ validate_transformer_hash(result, :input)
340
+ end
341
+
342
+ return result
284
343
  end
285
344
 
286
345
  # Bash command transformer
@@ -318,22 +377,34 @@ module SwarmSDK
318
377
  #
319
378
  # Executes either Ruby block or bash command transformer.
320
379
  #
380
+ # **Ruby block return values:**
381
+ # - String: Transformed content
382
+ # - Hash with `halt_workflow: true`: Halt entire workflow
383
+ # - Hash with `goto_node: :name`: Jump to different node
384
+ #
321
385
  # **Exit code behavior (bash commands only):**
322
386
  # - Exit 0: Use STDOUT as transformed content
323
387
  # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
324
388
  # - Exit 2: Halt workflow with error (STDOUT ignored)
325
389
  #
326
390
  # @param context [NodeContext] Context with current result and metadata
327
- # @return [String] Transformed output
391
+ # @return [String, Hash] Transformed output OR control hash (halt_workflow, goto_node)
328
392
  # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
329
393
  def transform_output(context)
330
394
  # No transformer configured: return content as-is
331
395
  return context.content unless @output_transformer || @output_transformer_command
332
396
 
333
397
  # Ruby block transformer
334
- # Simply calls the block with context and returns result
398
+ # Ruby blocks can return String (transformed content) OR Hash (control flow)
335
399
  if @output_transformer
336
- return @output_transformer.call(context)
400
+ result = @output_transformer.call(context)
401
+
402
+ # If hash, validate control flow keys
403
+ if result.is_a?(Hash)
404
+ validate_transformer_hash(result, :output)
405
+ end
406
+
407
+ return result
337
408
  end
338
409
 
339
410
  # Bash command transformer
@@ -411,6 +482,50 @@ module SwarmSDK
411
482
 
412
483
  private
413
484
 
485
+ # Validate transformer hash return value
486
+ #
487
+ # Ensures hash has valid control flow keys and required content field.
488
+ #
489
+ # @param hash [Hash] Hash returned from transformer
490
+ # @param transformer_type [Symbol] :input or :output
491
+ # @return [void]
492
+ # @raise [ConfigurationError] If hash is invalid
493
+ def validate_transformer_hash(hash, transformer_type)
494
+ # Valid control keys
495
+ valid_keys = if transformer_type == :input
496
+ [:skip_execution, :halt_workflow, :goto_node, :content]
497
+ else
498
+ [:halt_workflow, :goto_node, :content]
499
+ end
500
+
501
+ # Check for invalid keys
502
+ invalid_keys = hash.keys - valid_keys
503
+ if invalid_keys.any?
504
+ raise ConfigurationError,
505
+ "Invalid #{transformer_type} transformer hash keys: #{invalid_keys.join(", ")}. " \
506
+ "Valid keys: #{valid_keys.join(", ")}"
507
+ end
508
+
509
+ # Ensure content is present
510
+ unless hash.key?(:content)
511
+ raise ConfigurationError,
512
+ "#{transformer_type.capitalize} transformer hash must include :content key"
513
+ end
514
+
515
+ # Ensure only one control key
516
+ control_keys = hash.keys & [:skip_execution, :halt_workflow, :goto_node]
517
+ if control_keys.size > 1
518
+ raise ConfigurationError,
519
+ "#{transformer_type.capitalize} transformer hash can only have one control key, got: #{control_keys.join(", ")}"
520
+ end
521
+
522
+ # Validate goto_node has valid node name
523
+ if hash[:goto_node] && !hash[:goto_node].is_a?(Symbol)
524
+ raise ConfigurationError,
525
+ "goto_node value must be a Symbol, got: #{hash[:goto_node].class}"
526
+ end
527
+ end
528
+
414
529
  # Auto-add agents that are mentioned in delegates_to but not explicitly declared
415
530
  #
416
531
  # This allows:
@@ -418,7 +533,8 @@ module SwarmSDK
418
533
  # Without needing:
419
534
  # agent(:tester)
420
535
  #
421
- # The tester agent is automatically added to the node with no delegation.
536
+ # The tester agent is automatically added to the node with no delegation
537
+ # and reset_context: true (fresh context by default).
422
538
  #
423
539
  # @return [void]
424
540
  def auto_add_delegate_agents
@@ -429,9 +545,9 @@ module SwarmSDK
429
545
  declared_agents = @agent_configs.map { |ac| ac[:agent] }
430
546
  missing_delegates = all_delegates - declared_agents
431
547
 
432
- # Auto-add missing delegates with empty delegation
548
+ # Auto-add missing delegates with empty delegation and default reset_context
433
549
  missing_delegates.each do |delegate_name|
434
- @agent_configs << { agent: delegate_name, delegates_to: [] }
550
+ @agent_configs << { agent: delegate_name, delegates_to: [], reset_context: true }
435
551
  end
436
552
  end
437
553
  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