swarm_memory 2.1.5 → 2.1.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.
Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,554 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- # Workflow executes a multi-node workflow
5
- #
6
- # Each node represents a mini-swarm execution stage. The workflow:
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
- # workflow = Workflow.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 = workflow.execute("Build auth system")
20
- class Workflow
21
- attr_reader :swarm_name, :nodes, :start_node, :agent_definitions, :scratchpad
22
- attr_reader :agents, :delegation_instances, :swarm_id, :parent_swarm_id, :mcp_clients
23
- attr_reader :execution_order
24
- attr_writer :swarm_id, :config_for_hooks
25
- attr_accessor :swarm_registry_config, :original_prompt
26
-
27
- def initialize(swarm_name:, agent_definitions:, nodes:, start_node:, swarm_id: nil, scratchpad: :enabled, allow_filesystem_tools: nil)
28
- @swarm_name = swarm_name
29
- @swarm_id = swarm_id || generate_swarm_id(swarm_name)
30
- @parent_swarm_id = nil # Workflows don't have parent swarms
31
- @agent_definitions = agent_definitions
32
- @nodes = nodes
33
- @start_node = start_node
34
- @scratchpad = normalize_scratchpad_mode(scratchpad)
35
- @allow_filesystem_tools = allow_filesystem_tools
36
- @swarm_registry_config = [] # External swarms config (if using composable swarms)
37
-
38
- # Simplified structure (matches Swarm)
39
- @agents = {} # Cached primary agents from nodes
40
- @delegation_instances = {} # Cached delegation instances from nodes
41
-
42
- # MCP clients per agent (for cleanup compatibility)
43
- @mcp_clients = Hash.new { |h, k| h[k] = [] }
44
-
45
- # Initialize scratchpad storage based on mode
46
- case @scratchpad
47
- when :enabled
48
- # Enabled mode: single scratchpad shared across all nodes
49
- @shared_scratchpad_storage = Tools::Stores::ScratchpadStorage.new
50
- @node_scratchpads = nil
51
- when :per_node
52
- # Per-node mode: separate scratchpad per node (lazy initialized)
53
- @shared_scratchpad_storage = nil
54
- @node_scratchpads = {}
55
- when :disabled
56
- # Disabled: no storage at all
57
- @shared_scratchpad_storage = nil
58
- @node_scratchpads = nil
59
- end
60
-
61
- validate!
62
- @execution_order = build_execution_order
63
- end
64
-
65
- # Provide name method for interface compatibility
66
- def name
67
- @swarm_name
68
- end
69
-
70
- # Implement Snapshotable interface
71
- def primary_agents
72
- @agents
73
- end
74
-
75
- def delegation_instances_hash
76
- @delegation_instances
77
- end
78
-
79
- # No-op for Swarm compatibility (Workflow doesn't track first message)
80
- def first_message_sent?
81
- false
82
- end
83
-
84
- # Get scratchpad storage for a specific node
85
- #
86
- # Returns the appropriate scratchpad based on mode:
87
- # - :enabled - returns the shared scratchpad (same for all nodes)
88
- # - :per_node - returns node-specific scratchpad (lazy initialized)
89
- # - :disabled - returns nil
90
- #
91
- # @param node_name [Symbol] Node name
92
- # @return [Tools::Stores::ScratchpadStorage, nil] Scratchpad instance or nil if disabled
93
- def scratchpad_for(node_name)
94
- case @scratchpad
95
- when :enabled
96
- @shared_scratchpad_storage
97
- when :per_node
98
- # Lazy initialization per node
99
- @node_scratchpads[node_name] ||= Tools::Stores::ScratchpadStorage.new
100
- when :disabled
101
- nil
102
- end
103
- end
104
-
105
- # Get all scratchpad storages (for snapshot/restore)
106
- #
107
- # @return [Hash] { :shared => scratchpad } or { node_name => scratchpad }
108
- def all_scratchpads
109
- case @scratchpad
110
- when :enabled
111
- { shared: @shared_scratchpad_storage }
112
- when :per_node
113
- @node_scratchpads.dup
114
- when :disabled
115
- {}
116
- end
117
- end
118
-
119
- # Check if scratchpad is enabled
120
- #
121
- # @return [Boolean]
122
- def scratchpad_enabled?
123
- @scratchpad != :disabled
124
- end
125
-
126
- # Check if scratchpad is shared between nodes (enabled mode)
127
- #
128
- # @return [Boolean]
129
- def shared_scratchpad?
130
- @scratchpad == :enabled
131
- end
132
-
133
- # Check if scratchpad is per-node
134
- #
135
- # @return [Boolean]
136
- def per_node_scratchpad?
137
- @scratchpad == :per_node
138
- end
139
-
140
- # Backward compatibility accessor
141
- #
142
- # @return [Tools::Stores::ScratchpadStorage, nil]
143
- def shared_scratchpad_storage
144
- if @scratchpad == :per_node
145
- RubyLLM.logger.warn("Workflow: Accessing shared_scratchpad_storage in per-node mode. Use scratchpad_for(node_name) instead.")
146
- end
147
- @shared_scratchpad_storage
148
- end
149
-
150
- # Return the lead agent of the start node for CLI compatibility
151
- #
152
- # @return [Symbol] Lead agent of the start node
153
- def lead_agent
154
- @nodes[@start_node].lead_agent
155
- end
156
-
157
- # Execute the node workflow
158
- #
159
- # Executes nodes in topological order, passing output from each node
160
- # to its dependents. Supports streaming logs if block given.
161
- #
162
- # @param prompt [String] Initial prompt for the workflow
163
- # @param inherit_subscriptions [Boolean] Whether to inherit parent log subscriptions
164
- # (default: true). Set to false to isolate child workflow from parent's event stream.
165
- # @yield [Hash] Log entry if block given (for streaming)
166
- # @return [Result] Final result from last node execution
167
- def execute(prompt, inherit_subscriptions: true, &block)
168
- Executor.new(self).run(prompt, inherit_subscriptions: inherit_subscriptions, &block)
169
- end
170
-
171
- # Create snapshot of current workflow state
172
- #
173
- # Returns a Snapshot object containing agent conversations, context state,
174
- # and scratchpad data from all nodes that have been executed. The snapshot
175
- # captures the state of agents in the agent_instance_cache (both primary and
176
- # delegation instances), as well as scratchpad storage.
177
- #
178
- # Configuration (agent definitions, nodes, transformers) stays in your code
179
- # and is NOT included in snapshots.
180
- #
181
- # Scratchpad behavior depends on scratchpad mode:
182
- # - :enabled (default): single scratchpad shared across all nodes
183
- # - :per_node: separate scratchpad per node
184
- # - :disabled: no scratchpad data
185
- #
186
- # @return [Snapshot] Snapshot object with convenient serialization methods
187
- #
188
- # @example Save snapshot to JSON file
189
- # workflow = Workflow.new(...)
190
- # workflow.execute("Build feature")
191
- # snapshot = workflow.snapshot
192
- # snapshot.write_to_file("workflow_session.json")
193
- def snapshot
194
- StateSnapshot.new(self).snapshot
195
- end
196
-
197
- # Restore workflow state from snapshot
198
- #
199
- # Accepts a Snapshot object, hash, or JSON string. Validates compatibility
200
- # between snapshot and current workflow configuration. Restores agent
201
- # conversations that exist in the cached agents.
202
- #
203
- # The workflow must be created with the SAME configuration (agent definitions,
204
- # nodes) as when the snapshot was created. Only conversation state is restored.
205
- #
206
- # For agents with reset_context: false, restored conversations will be injected
207
- # during node execution. Agents not in cache yet will be skipped (they haven't
208
- # been used yet, so there's nothing to restore).
209
- #
210
- # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
211
- # @return [RestoreResult] Result with warnings about skipped agents
212
- #
213
- # @example Restore from Snapshot object
214
- # workflow = Workflow.new(...) # Same config as snapshot
215
- # snapshot = Snapshot.from_file("workflow_session.json")
216
- # result = workflow.restore(snapshot)
217
- # if result.success?
218
- # puts "All agents restored"
219
- # else
220
- # puts result.summary
221
- # end
222
- #
223
- # Restore workflow state from snapshot
224
- #
225
- # By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
226
- # Set preserve_system_prompts: true to use historical prompts from snapshot.
227
- #
228
- # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
229
- # @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
230
- # @return [RestoreResult] Result with warnings about partial restores
231
- def restore(snapshot, preserve_system_prompts: false)
232
- StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
233
- end
234
-
235
- # Build a swarm instance for a specific node
236
- #
237
- # Creates a new Swarm with only the agents specified in the node,
238
- # configured with the node's delegation topology.
239
- #
240
- # For agents with reset_context: false, injects cached instances
241
- # to preserve conversation history across nodes.
242
- #
243
- # Scratchpad behavior depends on mode:
244
- # - :enabled - all nodes use the same scratchpad instance
245
- # - :per_node - each node gets its own scratchpad instance
246
- # - :disabled - no scratchpad
247
- #
248
- # @param node [Workflow::NodeBuilder] Node configuration
249
- # @return [Swarm] Configured swarm instance
250
- def build_swarm_for_node(node)
251
- # Build hierarchical swarm_id if parent has one (nil auto-generates)
252
- node_swarm_id = @swarm_id ? "#{@swarm_id}/node:#{node.name}" : nil
253
-
254
- swarm = Swarm.new(
255
- name: "#{@swarm_name}:#{node.name}",
256
- swarm_id: node_swarm_id,
257
- parent_swarm_id: @swarm_id,
258
- scratchpad: scratchpad_for(node.name),
259
- scratchpad_mode: :enabled, # Mini-swarms always use enabled (scratchpad instance passed in)
260
- allow_filesystem_tools: @allow_filesystem_tools,
261
- )
262
-
263
- # Setup swarm registry if external swarms are registered
264
- if @swarm_registry_config&.any?
265
- registry = SwarmRegistry.new(parent_swarm_id: node_swarm_id || swarm.swarm_id)
266
- @swarm_registry_config.each do |reg|
267
- registry.register(reg[:name], source: reg[:source], keep_context: reg[:keep_context])
268
- end
269
- swarm.swarm_registry = registry
270
- end
271
-
272
- # Add each agent specified in this node
273
- node.agent_configs.each do |config|
274
- agent_name = config[:agent]
275
- delegates_to = config[:delegates_to]
276
- tools_override = config[:tools]
277
-
278
- # Get global agent definition
279
- agent_def = @agent_definitions[agent_name]
280
-
281
- # Clone definition with node-specific overrides
282
- node_specific_def = clone_agent_for_node(agent_def, delegates_to, tools_override)
283
-
284
- swarm.add_agent(node_specific_def)
285
- end
286
-
287
- # Set lead agent
288
- swarm.lead = node.lead_agent
289
-
290
- # Inject cached agent instances for context preservation
291
- inject_cached_agents(swarm, node)
292
-
293
- swarm
294
- end
295
-
296
- # Cache agent instances from a swarm for potential reuse
297
- #
298
- # Only caches agents that have reset_context: false in this node.
299
- # This allows preserving conversation history across nodes.
300
- #
301
- # @param swarm [Swarm] Swarm instance that just executed
302
- # @param node [Workflow::Builder] Node configuration
303
- # @return [void]
304
- def cache_agent_instances(swarm, node)
305
- return unless swarm.agents
306
-
307
- node.agent_configs.each do |config|
308
- agent_name = config[:agent]
309
- reset_context = config[:reset_context]
310
-
311
- # Only cache if reset_context: false
312
- next if reset_context
313
-
314
- # Cache primary agent
315
- agent_instance = swarm.agents[agent_name]
316
- @agents[agent_name] = agent_instance if agent_instance
317
-
318
- # Cache delegation instances atomically (together with primary)
319
- agent_def = @agent_definitions[agent_name]
320
- agent_def.delegates_to.each do |delegate_name|
321
- delegation_key = "#{delegate_name}@#{agent_name}"
322
- delegation_instance = swarm.delegation_instances[delegation_key]
323
-
324
- if delegation_instance
325
- @delegation_instances[delegation_key] = delegation_instance
326
- end
327
- end
328
- end
329
- end
330
-
331
- private
332
-
333
- # Generate a unique execution ID for workflow
334
- #
335
- # Creates an execution ID that uniquely identifies a single workflow.execute() call.
336
- # Format: "exec_workflow_{random_hex}"
337
- #
338
- # @return [String] Generated execution ID (e.g., "exec_workflow_a3f2b1c8")
339
- def generate_swarm_id(name)
340
- sanitized = name.to_s.gsub(/[^a-z0-9_-]/i, "_").downcase
341
- "#{sanitized}_#{SecureRandom.hex(4)}"
342
- end
343
-
344
- # Validate workflow configuration
345
- #
346
- # @return [void]
347
- # @raise [ConfigurationError] If configuration is invalid
348
- def validate!
349
- # Validate start_node exists
350
- unless @nodes.key?(@start_node)
351
- raise ConfigurationError,
352
- "start_node '#{@start_node}' not found. Available nodes: #{@nodes.keys.join(", ")}"
353
- end
354
-
355
- # Validate all nodes
356
- @nodes.each_value(&:validate!)
357
-
358
- # Validate node dependencies reference existing nodes
359
- @nodes.each do |node_name, node|
360
- node.dependencies.each do |dep|
361
- unless @nodes.key?(dep)
362
- raise ConfigurationError,
363
- "Node '#{node_name}' depends on unknown node '#{dep}'"
364
- end
365
- end
366
- end
367
-
368
- # Validate all agents referenced in nodes exist (skip agent-less nodes)
369
- @nodes.each do |node_name, node|
370
- next if node.agent_less? # Skip validation for agent-less nodes
371
-
372
- node.agent_configs.each do |config|
373
- agent_name = config[:agent]
374
- unless @agent_definitions.key?(agent_name)
375
- raise ConfigurationError,
376
- "Node '#{node_name}' references undefined agent '#{agent_name}'"
377
- end
378
-
379
- # Validate delegation targets exist
380
- config[:delegates_to].each do |delegate|
381
- unless @agent_definitions.key?(delegate)
382
- raise ConfigurationError,
383
- "Node '#{node_name}' agent '#{agent_name}' delegates to undefined agent '#{delegate}'"
384
- end
385
- end
386
- end
387
- end
388
- end
389
-
390
- # Clone an agent definition with node-specific overrides
391
- #
392
- # Allows overriding delegation and tools per node. This enables:
393
- # - Different delegation topology per node
394
- # - Different tool sets per workflow stage
395
- #
396
- # @param agent_def [Agent::Definition] Original definition
397
- # @param delegates_to [Array<Symbol>] New delegation targets
398
- # @param tools [Array<Symbol>, nil] Tool override (nil = use global agent definition)
399
- # @return [Agent::Definition] Cloned definition with overrides
400
- def clone_agent_for_node(agent_def, delegates_to, tools)
401
- config = agent_def.to_h
402
- config[:delegates_to] = delegates_to
403
- config[:tools] = tools if tools # Only override if explicitly set
404
- Agent::Definition.new(agent_def.name, config)
405
- end
406
-
407
- # Build execution order using topological sort (Kahn's algorithm)
408
- #
409
- # Processes all nodes in dependency order, starting from start_node.
410
- # Ensures all nodes are reachable from start_node.
411
- #
412
- # @return [Array<Symbol>] Ordered list of node names
413
- # @raise [CircularDependencyError] If circular dependency detected
414
- def build_execution_order
415
- # Build in-degree map and adjacency list
416
- in_degree = {}
417
- adjacency = Hash.new { |h, k| h[k] = [] }
418
-
419
- @nodes.each do |node_name, node|
420
- in_degree[node_name] = node.dependencies.size
421
- node.dependencies.each do |dep|
422
- adjacency[dep] << node_name
423
- end
424
- end
425
-
426
- # Start with nodes that have no dependencies
427
- queue = in_degree.select { |_, degree| degree == 0 }.keys
428
- order = []
429
-
430
- while queue.any?
431
- # Process nodes with all dependencies satisfied
432
- node_name = queue.shift
433
- order << node_name
434
-
435
- # Reduce in-degree for dependent nodes
436
- adjacency[node_name].each do |dependent|
437
- in_degree[dependent] -= 1
438
- queue << dependent if in_degree[dependent] == 0
439
- end
440
- end
441
-
442
- # Check for circular dependencies
443
- if order.size < @nodes.size
444
- unprocessed = @nodes.keys - order
445
- raise CircularDependencyError,
446
- "Circular dependency detected. Unprocessed nodes: #{unprocessed.join(", ")}. " \
447
- "Use goto_node in transformers to create loops instead of circular depends_on."
448
- end
449
-
450
- # Verify start_node is in the execution order
451
- unless order.include?(@start_node)
452
- raise ConfigurationError,
453
- "start_node '#{@start_node}' is not reachable in the dependency graph"
454
- end
455
-
456
- # Verify start_node is actually first (or rearrange to make it first)
457
- # This ensures we start from the declared start_node
458
- start_index = order.index(@start_node)
459
- if start_index && start_index > 0
460
- # start_node has dependencies - this violates the assumption
461
- raise ConfigurationError,
462
- "start_node '#{@start_node}' has dependencies: #{@nodes[@start_node].dependencies.join(", ")}. " \
463
- "start_node must have no dependencies."
464
- end
465
-
466
- order
467
- end
468
-
469
- # Inject cached agent instances into a swarm
470
- #
471
- # For agents with reset_context: false, reuses cached instances to preserve context.
472
- # Forces agent initialization first (by accessing .agents), then swaps in cached instances.
473
- #
474
- # @param swarm [Swarm] Swarm instance to inject into
475
- # @param node [Workflow::Builder] Node configuration
476
- # @return [void]
477
- def inject_cached_agents(swarm, node)
478
- # Check if any agents need context preservation
479
- has_preserved = node.agent_configs.any? do |c|
480
- !c[:reset_context] && (
481
- @agents[c[:agent]] ||
482
- has_cached_delegations_for?(c[:agent])
483
- )
484
- end
485
- return unless has_preserved
486
-
487
- # Force initialization FIRST
488
- # Without this, @agents will be replaced by initialize_all, losing our injected instances
489
- swarm.agent(node.agent_configs.first[:agent]) # Triggers lazy init
490
-
491
- # Now safely inject cached instances
492
- agents_hash = swarm.agents
493
- delegation_hash = swarm.delegation_instances
494
-
495
- # Inject cached PRIMARY agents
496
- node.agent_configs.each do |config|
497
- agent_name = config[:agent]
498
- next if config[:reset_context]
499
-
500
- cached_agent = @agents[agent_name]
501
- next unless cached_agent
502
-
503
- # Replace freshly initialized agent with cached instance
504
- agents_hash[agent_name] = cached_agent
505
- end
506
-
507
- # Inject cached DELEGATION instances (atomic with primary)
508
- node.agent_configs.each do |config|
509
- agent_name = config[:agent]
510
- next if config[:reset_context]
511
-
512
- agent_def = @agent_definitions[agent_name]
513
-
514
- agent_def.delegates_to.each do |delegate_name|
515
- delegation_key = "#{delegate_name}@#{agent_name}"
516
- cached_delegation = @delegation_instances[delegation_key]
517
- next unless cached_delegation
518
-
519
- # Replace freshly initialized delegation instance
520
- # Tool references intact - atomic caching preserves object graph
521
- delegation_hash[delegation_key] = cached_delegation
522
- end
523
- end
524
- end
525
-
526
- def has_cached_delegations_for?(agent_name)
527
- agent_def = @agent_definitions[agent_name]
528
- agent_def.delegates_to.any? do |delegate_name|
529
- delegation_key = "#{delegate_name}@#{agent_name}"
530
- @delegation_instances[delegation_key]
531
- end
532
- end
533
-
534
- # Normalize scratchpad mode parameter
535
- #
536
- # Accepts symbols: :enabled, :per_node, or :disabled
537
- #
538
- # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
539
- # @return [Symbol] Normalized mode (:enabled, :per_node, or :disabled)
540
- # @raise [ArgumentError] If value is invalid
541
- def normalize_scratchpad_mode(value)
542
- # Convert strings from YAML to symbols
543
- value = value.to_sym if value.is_a?(String)
544
-
545
- case value
546
- when :enabled, :per_node, :disabled
547
- value
548
- else
549
- raise ArgumentError,
550
- "Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
551
- end
552
- end
553
- end
554
- end