swarm_memory 2.1.2 → 2.1.4

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 (118) hide show
  1. checksums.yaml +4 -4
  2. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  3. data/lib/claude_swarm/cli.rb +5 -18
  4. data/lib/claude_swarm/configuration.rb +30 -19
  5. data/lib/claude_swarm/mcp_generator.rb +5 -10
  6. data/lib/claude_swarm/openai/chat_completion.rb +4 -12
  7. data/lib/claude_swarm/openai/executor.rb +3 -1
  8. data/lib/claude_swarm/openai/responses.rb +13 -32
  9. data/lib/claude_swarm/version.rb +1 -1
  10. data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
  11. data/lib/swarm_cli/commands/run.rb +2 -2
  12. data/lib/swarm_cli/config_loader.rb +14 -14
  13. data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
  14. data/lib/swarm_cli/interactive_repl.rb +11 -5
  15. data/lib/swarm_cli/ui/icons.rb +0 -23
  16. data/lib/swarm_cli/version.rb +1 -1
  17. data/lib/swarm_memory/adapters/base.rb +4 -4
  18. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  19. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  20. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  21. data/lib/swarm_memory/integration/sdk_plugin.rb +98 -12
  22. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  23. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  24. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  25. data/lib/swarm_memory/version.rb +1 -1
  26. data/lib/swarm_memory.rb +6 -1
  27. data/lib/swarm_sdk/agent/builder.rb +91 -0
  28. data/lib/swarm_sdk/agent/chat.rb +540 -925
  29. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +33 -79
  30. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  31. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +147 -39
  32. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  33. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  34. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  35. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  36. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +22 -38
  37. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  38. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  39. data/lib/swarm_sdk/agent/context.rb +8 -4
  40. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  41. data/lib/swarm_sdk/agent/definition.rb +79 -174
  42. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +182 -0
  43. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  44. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  45. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  46. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  47. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  48. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  49. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  50. data/lib/swarm_sdk/configuration.rb +100 -261
  51. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  52. data/lib/swarm_sdk/context_compactor.rb +6 -11
  53. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  54. data/lib/swarm_sdk/context_management/context.rb +328 -0
  55. data/lib/swarm_sdk/defaults.rb +196 -0
  56. data/lib/swarm_sdk/events_to_messages.rb +199 -0
  57. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  58. data/lib/swarm_sdk/log_collector.rb +192 -16
  59. data/lib/swarm_sdk/log_stream.rb +66 -8
  60. data/lib/swarm_sdk/model_aliases.json +4 -1
  61. data/lib/swarm_sdk/node_context.rb +1 -1
  62. data/lib/swarm_sdk/observer/builder.rb +81 -0
  63. data/lib/swarm_sdk/observer/config.rb +45 -0
  64. data/lib/swarm_sdk/observer/manager.rb +236 -0
  65. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  66. data/lib/swarm_sdk/plugin.rb +93 -3
  67. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  68. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  69. data/lib/swarm_sdk/restore_result.rb +65 -0
  70. data/lib/swarm_sdk/snapshot.rb +156 -0
  71. data/lib/swarm_sdk/snapshot_from_events.rb +397 -0
  72. data/lib/swarm_sdk/state_restorer.rb +476 -0
  73. data/lib/swarm_sdk/state_snapshot.rb +334 -0
  74. data/lib/swarm_sdk/swarm/agent_initializer.rb +428 -79
  75. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  76. data/lib/swarm_sdk/swarm/builder.rb +69 -407
  77. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  78. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  79. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  80. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  81. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  82. data/lib/swarm_sdk/swarm/tool_configurator.rb +88 -149
  83. data/lib/swarm_sdk/swarm.rb +366 -631
  84. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  85. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +127 -24
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +28 -18
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  96. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  97. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  98. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  99. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +53 -5
  100. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  101. data/lib/swarm_sdk/tools/think.rb +4 -1
  102. data/lib/swarm_sdk/tools/todo_write.rb +27 -8
  103. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  104. data/lib/swarm_sdk/tools/write.rb +8 -13
  105. data/lib/swarm_sdk/utils.rb +18 -0
  106. data/lib/swarm_sdk/validation_result.rb +33 -0
  107. data/lib/swarm_sdk/version.rb +1 -1
  108. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +34 -9
  109. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  110. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  111. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +42 -21
  112. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  113. data/lib/swarm_sdk/workflow.rb +554 -0
  114. data/lib/swarm_sdk.rb +393 -22
  115. metadata +51 -16
  116. data/lib/swarm_memory/chat_extension.rb +0 -34
  117. data/lib/swarm_sdk/node_orchestrator.rb +0 -591
  118. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -582
@@ -1,591 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- # NodeOrchestrator executes a multi-node workflow
5
- #
6
- # Each node represents a mini-swarm execution stage. The orchestrator:
7
- # - Builds execution order from node dependencies (topological sort)
8
- # - Creates a separate swarm instance for each node
9
- # - Passes output from one node as input to dependent nodes
10
- # - Supports input/output transformers for data flow customization
11
- #
12
- # @example
13
- # orchestrator = NodeOrchestrator.new(
14
- # swarm_name: "Dev Team",
15
- # agent_definitions: { backend: def1, tester: def2 },
16
- # nodes: { planning: node1, implementation: node2 },
17
- # start_node: :planning
18
- # )
19
- # result = orchestrator.execute("Build auth system")
20
- class NodeOrchestrator
21
- attr_reader :swarm_name, :nodes, :start_node
22
-
23
- def initialize(swarm_name:, agent_definitions:, nodes:, start_node:, scratchpad_enabled: true)
24
- @swarm_name = swarm_name
25
- @agent_definitions = agent_definitions
26
- @nodes = nodes
27
- @start_node = start_node
28
- @scratchpad_enabled = scratchpad_enabled
29
- @agent_instance_cache = {} # Cache for preserving agent context across nodes
30
-
31
- validate!
32
- @execution_order = build_execution_order
33
- end
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
-
45
- # Execute the node workflow
46
- #
47
- # Executes nodes in topological order, passing output from each node
48
- # to its dependents. Supports streaming logs if block given.
49
- #
50
- # @param prompt [String] Initial prompt for the workflow
51
- # @yield [Hash] Log entry if block given (for streaming)
52
- # @return [Result] Final result from last node execution
53
- def execute(prompt, &block)
54
- logs = []
55
- current_input = prompt
56
- results = {}
57
- @original_prompt = prompt # Store original prompt for NodeContext
58
-
59
- # Setup logging if block given
60
- if block_given?
61
- # Register callback to collect logs and forward to user's block
62
- LogCollector.on_log do |entry|
63
- logs << entry
64
- block.call(entry)
65
- end
66
-
67
- # Set LogStream to use LogCollector as emitter
68
- LogStream.emitter = LogCollector
69
- end
70
-
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]
77
- node = @nodes[node_name]
78
- node_start_time = Time.now
79
-
80
- # Emit node_start event
81
- emit_node_start(node_name, node)
82
-
83
- # Transform input if node has transformer (Ruby block or bash command)
84
- skip_execution = false
85
- skip_content = nil
86
-
87
- if node.has_input_transformer?
88
- # Build NodeContext based on dependencies
89
- #
90
- # For single dependency: previous_result has original Result metadata,
91
- # transformed_content has output from previous transformer
92
- # For multiple dependencies: previous_result is hash of Results
93
- # For no dependencies: previous_result is initial prompt string
94
- previous_result = if node.dependencies.size > 1
95
- # Multiple dependencies: pass hash of original results
96
- node.dependencies.to_h { |dep| [dep, results[dep]] }
97
- elsif node.dependencies.size == 1
98
- # Single dependency: pass the original result
99
- results[node.dependencies.first]
100
- else
101
- # No dependencies: initial prompt
102
- current_input
103
- end
104
-
105
- # Create NodeContext for input transformer
106
- input_context = NodeContext.for_input(
107
- previous_result: previous_result,
108
- all_results: results,
109
- original_prompt: @original_prompt,
110
- node_name: node_name,
111
- dependencies: node.dependencies,
112
- transformed_content: node.dependencies.size == 1 ? current_input : nil,
113
- )
114
-
115
- # Apply input transformer (passes current_input for bash command fallback)
116
- # Bash transformer exit codes:
117
- # - Exit 0: Use STDOUT as transformed content
118
- # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
119
- # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
120
- transformed = node.transform_input(input_context, current_input: current_input)
121
-
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
138
- skip_execution = true
139
- skip_content = control_result[:content]
140
- when :continue
141
- current_input = control_result[:content]
142
- end
143
- end
144
-
145
- # Execute node (or skip if requested)
146
- if skip_execution
147
- # Skip execution: return result immediately with provided content
148
- result = Result.new(
149
- content: skip_content,
150
- agent: "skipped:#{node_name}",
151
- logs: [],
152
- duration: 0.0,
153
- )
154
- elsif node.agent_less?
155
- # Agent-less node: run pure computation without LLM
156
- result = execute_agent_less_node(node, current_input)
157
- else
158
- # Normal node: build mini-swarm and execute with LLM
159
- # NOTE: Don't pass block to mini-swarm - LogCollector already captures all logs
160
- mini_swarm = build_swarm_for_node(node)
161
- result = mini_swarm.execute(current_input)
162
-
163
- # Cache agent instances for context preservation
164
- cache_agent_instances(mini_swarm, node)
165
-
166
- # If result has error, log it with backtrace
167
- if result.error
168
- RubyLLM.logger.error("NodeOrchestrator: Node '#{node_name}' failed: #{result.error.message}")
169
- RubyLLM.logger.error(" Backtrace: #{result.error.backtrace&.first(5)&.join("\n ")}")
170
- end
171
- end
172
-
173
- results[node_name] = result
174
- last_result = result
175
-
176
- # Transform output for next node using NodeContext
177
- output_context = NodeContext.for_output(
178
- result: result,
179
- all_results: results,
180
- original_prompt: @original_prompt,
181
- node_name: node_name,
182
- )
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
206
-
207
- # For agent-less nodes, update the result with transformed content
208
- # This ensures all_results contains the actual output, not the input
209
- if node.agent_less? && current_input != result.content
210
- results[node_name] = Result.new(
211
- content: current_input,
212
- agent: result.agent,
213
- logs: result.logs,
214
- duration: result.duration,
215
- error: result.error,
216
- )
217
- last_result = results[node_name]
218
- end
219
-
220
- # Emit node_stop event
221
- node_duration = Time.now - node_start_time
222
- emit_node_stop(node_name, node, result, node_duration, skip_execution)
223
-
224
- execution_index += 1
225
- end
226
-
227
- last_result
228
- ensure
229
- # Reset logging state for next execution
230
- LogCollector.reset!
231
- LogStream.reset!
232
- end
233
-
234
- private
235
-
236
- # Emit node_start event
237
- #
238
- # @param node_name [Symbol] Name of the node
239
- # @param node [Node::Builder] Node configuration
240
- # @return [void]
241
- def emit_node_start(node_name, node)
242
- return unless LogStream.emitter
243
-
244
- LogStream.emit(
245
- type: "node_start",
246
- node: node_name.to_s,
247
- agent_less: node.agent_less?,
248
- agents: node.agent_configs.map { |ac| ac[:agent].to_s },
249
- dependencies: node.dependencies.map(&:to_s),
250
- timestamp: Time.now.utc.iso8601,
251
- )
252
- end
253
-
254
- # Emit node_stop event
255
- #
256
- # @param node_name [Symbol] Name of the node
257
- # @param node [Node::Builder] Node configuration
258
- # @param result [Result] Node execution result
259
- # @param duration [Float] Node execution duration in seconds
260
- # @param skipped [Boolean] Whether execution was skipped
261
- # @return [void]
262
- def emit_node_stop(node_name, node, result, duration, skipped)
263
- return unless LogStream.emitter
264
-
265
- LogStream.emit(
266
- type: "node_stop",
267
- node: node_name.to_s,
268
- agent_less: node.agent_less?,
269
- skipped: skipped,
270
- agents: node.agent_configs.map { |ac| ac[:agent].to_s },
271
- duration: duration.round(3),
272
- timestamp: Time.now.utc.iso8601,
273
- )
274
- end
275
-
276
- # Execute an agent-less (computation-only) node
277
- #
278
- # Agent-less nodes run pure Ruby code without LLM execution.
279
- # Creates a minimal Result object with the transformed content.
280
- #
281
- # @param node [Node::Builder] Agent-less node configuration
282
- # @param input [String] Input content
283
- # @return [Result] Result with transformed content
284
- def execute_agent_less_node(node, input)
285
- # For agent-less nodes, the "content" is just the input passed through
286
- # The output transformer will do the actual work
287
- Result.new(
288
- content: input,
289
- agent: "computation:#{node.name}",
290
- logs: [],
291
- duration: 0.0,
292
- )
293
- end
294
-
295
- # Validate orchestrator configuration
296
- #
297
- # @return [void]
298
- # @raise [ConfigurationError] If configuration is invalid
299
- def validate!
300
- # Validate start_node exists
301
- unless @nodes.key?(@start_node)
302
- raise ConfigurationError,
303
- "start_node '#{@start_node}' not found. Available nodes: #{@nodes.keys.join(", ")}"
304
- end
305
-
306
- # Validate all nodes
307
- @nodes.each_value(&:validate!)
308
-
309
- # Validate node dependencies reference existing nodes
310
- @nodes.each do |node_name, node|
311
- node.dependencies.each do |dep|
312
- unless @nodes.key?(dep)
313
- raise ConfigurationError,
314
- "Node '#{node_name}' depends on unknown node '#{dep}'"
315
- end
316
- end
317
- end
318
-
319
- # Validate all agents referenced in nodes exist (skip agent-less nodes)
320
- @nodes.each do |node_name, node|
321
- next if node.agent_less? # Skip validation for agent-less nodes
322
-
323
- node.agent_configs.each do |config|
324
- agent_name = config[:agent]
325
- unless @agent_definitions.key?(agent_name)
326
- raise ConfigurationError,
327
- "Node '#{node_name}' references undefined agent '#{agent_name}'"
328
- end
329
-
330
- # Validate delegation targets exist
331
- config[:delegates_to].each do |delegate|
332
- unless @agent_definitions.key?(delegate)
333
- raise ConfigurationError,
334
- "Node '#{node_name}' agent '#{agent_name}' delegates to undefined agent '#{delegate}'"
335
- end
336
- end
337
- end
338
- end
339
- end
340
-
341
- # Build a swarm instance for a specific node
342
- #
343
- # Creates a new Swarm with only the agents specified in the node,
344
- # configured with the node's delegation topology.
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
- #
351
- # @param node [Node::Builder] Node configuration
352
- # @return [Swarm] Configured swarm instance
353
- def build_swarm_for_node(node)
354
- swarm = Swarm.new(
355
- name: "#{@swarm_name}:#{node.name}",
356
- scratchpad_enabled: @scratchpad_enabled,
357
- )
358
-
359
- # Add each agent specified in this node
360
- node.agent_configs.each do |config|
361
- agent_name = config[:agent]
362
- delegates_to = config[:delegates_to]
363
-
364
- # Get global agent definition
365
- agent_def = @agent_definitions[agent_name]
366
-
367
- # Clone definition with node-specific delegation
368
- node_specific_def = clone_with_delegation(agent_def, delegates_to)
369
-
370
- swarm.add_agent(node_specific_def)
371
- end
372
-
373
- # Set lead agent
374
- swarm.lead = node.lead_agent
375
-
376
- # Inject cached agent instances for context preservation
377
- inject_cached_agents(swarm, node)
378
-
379
- swarm
380
- end
381
-
382
- # Clone an agent definition with different delegates_to
383
- #
384
- # @param agent_def [Agent::Definition] Original definition
385
- # @param delegates_to [Array<Symbol>] New delegation targets
386
- # @return [Agent::Definition] Cloned definition
387
- def clone_with_delegation(agent_def, delegates_to)
388
- config = agent_def.to_h
389
- config[:delegates_to] = delegates_to
390
- Agent::Definition.new(agent_def.name, config)
391
- end
392
-
393
- # Build execution order using topological sort (Kahn's algorithm)
394
- #
395
- # Processes all nodes in dependency order, starting from start_node.
396
- # Ensures all nodes are reachable from start_node.
397
- #
398
- # @return [Array<Symbol>] Ordered list of node names
399
- # @raise [CircularDependencyError] If circular dependency detected
400
- def build_execution_order
401
- # Build in-degree map and adjacency list
402
- in_degree = {}
403
- adjacency = Hash.new { |h, k| h[k] = [] }
404
-
405
- @nodes.each do |node_name, node|
406
- in_degree[node_name] = node.dependencies.size
407
- node.dependencies.each do |dep|
408
- adjacency[dep] << node_name
409
- end
410
- end
411
-
412
- # Start with nodes that have no dependencies
413
- queue = in_degree.select { |_, degree| degree == 0 }.keys
414
- order = []
415
-
416
- while queue.any?
417
- # Process nodes with all dependencies satisfied
418
- node_name = queue.shift
419
- order << node_name
420
-
421
- # Reduce in-degree for dependent nodes
422
- adjacency[node_name].each do |dependent|
423
- in_degree[dependent] -= 1
424
- queue << dependent if in_degree[dependent] == 0
425
- end
426
- end
427
-
428
- # Check for circular dependencies
429
- if order.size < @nodes.size
430
- unprocessed = @nodes.keys - order
431
- raise CircularDependencyError,
432
- "Circular dependency detected. Unprocessed nodes: #{unprocessed.join(", ")}. " \
433
- "Use goto_node in transformers to create loops instead of circular depends_on."
434
- end
435
-
436
- # Verify start_node is in the execution order
437
- unless order.include?(@start_node)
438
- raise ConfigurationError,
439
- "start_node '#{@start_node}' is not reachable in the dependency graph"
440
- end
441
-
442
- # Verify start_node is actually first (or rearrange to make it first)
443
- # This ensures we start from the declared start_node
444
- start_index = order.index(@start_node)
445
- if start_index && start_index > 0
446
- # start_node has dependencies - this violates the assumption
447
- raise ConfigurationError,
448
- "start_node '#{@start_node}' has dependencies: #{@nodes[@start_node].dependencies.join(", ")}. " \
449
- "start_node must have no dependencies."
450
- end
451
-
452
- order
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
590
- end
591
- end