swarm_memory 2.1.4 → 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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. data/lib/swarm_memory.rb +7 -2
  4. metadata +6 -185
  5. data/lib/claude_swarm/base_executor.rb +0 -133
  6. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  7. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  8. data/lib/claude_swarm/cli.rb +0 -697
  9. data/lib/claude_swarm/commands/ps.rb +0 -215
  10. data/lib/claude_swarm/commands/show.rb +0 -139
  11. data/lib/claude_swarm/configuration.rb +0 -373
  12. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  13. data/lib/claude_swarm/json_handler.rb +0 -91
  14. data/lib/claude_swarm/mcp_generator.rb +0 -243
  15. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  16. data/lib/claude_swarm/openai/executor.rb +0 -256
  17. data/lib/claude_swarm/openai/responses.rb +0 -319
  18. data/lib/claude_swarm/orchestrator.rb +0 -878
  19. data/lib/claude_swarm/process_tracker.rb +0 -78
  20. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  21. data/lib/claude_swarm/session_path.rb +0 -42
  22. data/lib/claude_swarm/settings_generator.rb +0 -77
  23. data/lib/claude_swarm/system_utils.rb +0 -46
  24. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  25. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  27. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  28. data/lib/claude_swarm/version.rb +0 -5
  29. data/lib/claude_swarm/worktree_manager.rb +0 -475
  30. data/lib/claude_swarm/yaml_loader.rb +0 -22
  31. data/lib/claude_swarm.rb +0 -67
  32. data/lib/swarm_cli/cli.rb +0 -201
  33. data/lib/swarm_cli/command_registry.rb +0 -61
  34. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  35. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  36. data/lib/swarm_cli/commands/migrate.rb +0 -55
  37. data/lib/swarm_cli/commands/run.rb +0 -173
  38. data/lib/swarm_cli/config_loader.rb +0 -98
  39. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  40. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  41. data/lib/swarm_cli/interactive_repl.rb +0 -924
  42. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  43. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  44. data/lib/swarm_cli/migrate_options.rb +0 -54
  45. data/lib/swarm_cli/migrator.rb +0 -132
  46. data/lib/swarm_cli/options.rb +0 -151
  47. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  48. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  49. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  50. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  51. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  52. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  53. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  54. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  55. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  56. data/lib/swarm_cli/ui/icons.rb +0 -36
  57. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  58. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  59. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  60. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  61. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  62. data/lib/swarm_cli/version.rb +0 -5
  63. data/lib/swarm_cli.rb +0 -46
  64. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  65. data/lib/swarm_sdk/agent/builder.rb +0 -552
  66. data/lib/swarm_sdk/agent/chat.rb +0 -774
  67. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  68. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  69. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  70. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  71. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  72. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  73. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  75. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  76. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  77. data/lib/swarm_sdk/agent/context.rb +0 -116
  78. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  79. data/lib/swarm_sdk/agent/definition.rb +0 -477
  80. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  81. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  82. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  83. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  84. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  85. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  86. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  87. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  88. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  89. data/lib/swarm_sdk/configuration.rb +0 -135
  90. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  91. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  92. data/lib/swarm_sdk/context_compactor.rb +0 -335
  93. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  94. data/lib/swarm_sdk/context_management/context.rb +0 -328
  95. data/lib/swarm_sdk/defaults.rb +0 -196
  96. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  97. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  98. data/lib/swarm_sdk/hooks/context.rb +0 -197
  99. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  100. data/lib/swarm_sdk/hooks/error.rb +0 -29
  101. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  102. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  103. data/lib/swarm_sdk/hooks/result.rb +0 -150
  104. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  105. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  106. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  107. data/lib/swarm_sdk/log_collector.rb +0 -227
  108. data/lib/swarm_sdk/log_stream.rb +0 -127
  109. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  110. data/lib/swarm_sdk/model_aliases.json +0 -8
  111. data/lib/swarm_sdk/models.json +0 -1
  112. data/lib/swarm_sdk/models.rb +0 -120
  113. data/lib/swarm_sdk/node_context.rb +0 -245
  114. data/lib/swarm_sdk/observer/builder.rb +0 -81
  115. data/lib/swarm_sdk/observer/config.rb +0 -45
  116. data/lib/swarm_sdk/observer/manager.rb +0 -236
  117. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  118. data/lib/swarm_sdk/permissions/config.rb +0 -239
  119. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  120. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  121. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  122. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  123. data/lib/swarm_sdk/plugin.rb +0 -309
  124. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  125. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  126. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  127. data/lib/swarm_sdk/restore_result.rb +0 -65
  128. data/lib/swarm_sdk/result.rb +0 -123
  129. data/lib/swarm_sdk/snapshot.rb +0 -156
  130. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  131. data/lib/swarm_sdk/state_restorer.rb +0 -476
  132. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  133. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  134. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  135. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  136. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  137. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  138. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  139. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  140. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  141. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  142. data/lib/swarm_sdk/swarm.rb +0 -717
  143. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  144. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  145. data/lib/swarm_sdk/tools/bash.rb +0 -282
  146. data/lib/swarm_sdk/tools/clock.rb +0 -44
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  160. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  161. data/lib/swarm_sdk/tools/read.rb +0 -261
  162. data/lib/swarm_sdk/tools/registry.rb +0 -205
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  166. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  167. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  168. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  169. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  170. data/lib/swarm_sdk/tools/think.rb +0 -98
  171. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  172. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  173. data/lib/swarm_sdk/tools/write.rb +0 -112
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  182. data/lib/swarm_sdk/workflow.rb +0 -554
  183. data/lib/swarm_sdk.rb +0 -524
  184. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -1,555 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- class Workflow
5
- # NodeBuilder provides DSL for configuring individual nodes within a workflow
6
- #
7
- # A node represents a stage in a multi-step workflow where a specific set
8
- # of agents collaborate. Each node creates an independent swarm execution.
9
- #
10
- # @example Solo agent node
11
- # node :planning do
12
- # agent(:architect)
13
- # end
14
- #
15
- # @example Multi-agent node with delegation
16
- # node :implementation do
17
- # agent(:backend).delegates_to(:tester, :database)
18
- # agent(:tester).delegates_to(:database)
19
- # agent(:database)
20
- #
21
- # depends_on :planning
22
- # end
23
- class NodeBuilder
24
- attr_reader :name,
25
- :agent_configs,
26
- :dependencies,
27
- :lead_override,
28
- :input_transformer,
29
- :output_transformer,
30
- :input_transformer_command,
31
- :output_transformer_command
32
-
33
- def initialize(name)
34
- @name = name
35
- @agent_configs = []
36
- @dependencies = []
37
- @lead_override = nil
38
- @input_transformer = nil # Ruby block
39
- @output_transformer = nil # Ruby block
40
- @input_transformer_command = nil # Bash command
41
- @output_transformer_command = nil # Bash command
42
- end
43
-
44
- # Configure an agent for this node
45
- #
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.
51
- #
52
- # @param name [Symbol] Agent name
53
- # @param reset_context [Boolean] Whether to reset agent context (default: true)
54
- # @return [AgentConfig] Fluent configuration object
55
- #
56
- # @example With delegation
57
- # agent(:backend).delegates_to(:tester, :database)
58
- #
59
- # @example Without delegation
60
- # agent(:planner)
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)
72
-
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)
76
-
77
- config
78
- end
79
-
80
- # Register an agent configuration (called by AgentConfig)
81
- #
82
- # @param agent_name [Symbol] Agent name
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)
86
- # @return [void]
87
- def register_agent(agent_name, delegates_to, reset_context = true, tools = nil)
88
- # Check if agent already registered
89
- existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
90
-
91
- if existing
92
- # Update delegation, reset_context, and tools (happens when methods are called after agent())
93
- existing[:delegates_to] = delegates_to
94
- existing[:reset_context] = reset_context
95
- existing[:tools] = tools unless tools.nil?
96
- else
97
- # Add new agent configuration
98
- @agent_configs << {
99
- agent: agent_name,
100
- delegates_to: delegates_to,
101
- reset_context: reset_context,
102
- tools: tools,
103
- }
104
- end
105
- end
106
-
107
- # Declare dependencies (nodes that must execute before this one)
108
- #
109
- # @param node_names [Array<Symbol>] Names of prerequisite nodes
110
- # @return [void]
111
- #
112
- # @example Single dependency
113
- # depends_on :planning
114
- #
115
- # @example Multiple dependencies
116
- # depends_on :frontend, :backend
117
- def depends_on(*node_names)
118
- @dependencies.concat(node_names.map(&:to_sym))
119
- end
120
-
121
- # Override the lead agent (first agent is lead by default)
122
- #
123
- # @param agent_name [Symbol] Name of agent to make lead
124
- # @return [void]
125
- #
126
- # @example
127
- # agent(:backend).delegates_to(:tester)
128
- # agent(:tester)
129
- # lead :tester # tester is lead instead of backend
130
- def lead(agent_name)
131
- @lead_override = agent_name.to_sym
132
- end
133
-
134
- # Define input transformer for this node
135
- #
136
- # The transformer receives a NodeContext object with access to:
137
- # - Previous node's result (convenience: ctx.content)
138
- # - Original user prompt (ctx.original_prompt)
139
- # - All previous node results (ctx.all_results[:node_name])
140
- # - Current node metadata (ctx.node_name, ctx.dependencies)
141
- #
142
- # Can also be used for side effects (logging, file I/O) since the block
143
- # runs at execution time, not declaration time.
144
- #
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
149
- #
150
- # @yield [NodeContext] Context with previous results and metadata
151
- # @return [String, Hash] Transformed input OR control hash
152
- #
153
- # @example Access previous result and original prompt
154
- # input do |ctx|
155
- # # Convenience accessor
156
- # previous_content = ctx.content
157
- #
158
- # # Access original prompt
159
- # "Original: #{ctx.original_prompt}\nPrevious: #{previous_content}"
160
- # end
161
- #
162
- # @example Access results from specific nodes
163
- # input do |ctx|
164
- # plan = ctx.all_results[:planning].content
165
- # design = ctx.all_results[:design].content
166
- #
167
- # "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
168
- # end
169
- #
170
- # @example Skip execution (caching) - using return
171
- # input do |ctx|
172
- # cached = check_cache(ctx.content)
173
- # return ctx.skip_execution(content: cached) if cached
174
- # ctx.content
175
- # end
176
- #
177
- # @example Halt workflow (validation) - using return
178
- # input do |ctx|
179
- # if ctx.content.length > 10000
180
- # # Halt entire workflow - return works safely!
181
- # return ctx.halt_workflow(content: "ERROR: Input too long")
182
- # end
183
- # ctx.content
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.
198
- def input(&block)
199
- @input_transformer = ProcHelpers.to_lambda(block)
200
- end
201
-
202
- # Set input transformer as bash command (YAML API)
203
- #
204
- # The command receives NodeContext as JSON on STDIN and outputs transformed content.
205
- #
206
- # **Exit codes:**
207
- # - 0: Success, use STDOUT as transformed content
208
- # - 1: Skip node execution, use current_input unchanged (STDOUT ignored)
209
- # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
210
- #
211
- # @param command [String] Bash command to execute
212
- # @param timeout [Integer] Timeout in seconds (default: 60)
213
- # @return [void]
214
- #
215
- # @example
216
- # input_command("scripts/validate.sh", timeout: 30)
217
- def input_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
218
- @input_transformer_command = { command: command, timeout: timeout }
219
- end
220
-
221
- # Define output transformer for this node
222
- #
223
- # The transformer receives a NodeContext object with access to:
224
- # - Current node's result (convenience: ctx.content)
225
- # - Original user prompt (ctx.original_prompt)
226
- # - All completed node results (ctx.all_results[:node_name])
227
- # - Current node metadata (ctx.node_name)
228
- #
229
- # Can also be used for side effects (logging, file I/O) since the block
230
- # runs at execution time, not declaration time.
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
- #
236
- # @yield [NodeContext] Context with current result and metadata
237
- # @return [String, Hash] Transformed output OR control hash
238
- #
239
- # @example Transform and save to file
240
- # output do |ctx|
241
- # # Side effect: save to file
242
- # File.write("results/plan.txt", ctx.content)
243
- #
244
- # # Return transformed output for next node
245
- # "Key decisions: #{extract_decisions(ctx.content)}"
246
- # end
247
- #
248
- # @example Access original prompt
249
- # output do |ctx|
250
- # # Include original context in output
251
- # "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
252
- # end
253
- #
254
- # @example Halt workflow (convergence check) - using return
255
- # output do |ctx|
256
- # return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
257
- # ctx.content
258
- # end
259
- #
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
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.
272
- def output(&block)
273
- @output_transformer = ProcHelpers.to_lambda(block)
274
- end
275
-
276
- # Set output transformer as bash command (YAML API)
277
- #
278
- # The command receives NodeContext as JSON on STDIN and outputs transformed content.
279
- #
280
- # **Exit codes:**
281
- # - 0: Success, use STDOUT as transformed content
282
- # - 1: Pass through unchanged, use result.content (STDOUT ignored)
283
- # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
284
- #
285
- # @param command [String] Bash command to execute
286
- # @param timeout [Integer] Timeout in seconds (default: 60)
287
- # @return [void]
288
- #
289
- # @example
290
- # output_command("scripts/format.sh", timeout: 30)
291
- def output_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
292
- @output_transformer_command = { command: command, timeout: timeout }
293
- end
294
-
295
- # Check if node has any input transformer (block or command)
296
- #
297
- # @return [Boolean]
298
- def has_input_transformer?
299
- @input_transformer || @input_transformer_command
300
- end
301
-
302
- # Check if node has any output transformer (block or command)
303
- #
304
- # @return [Boolean]
305
- def has_output_transformer?
306
- @output_transformer || @output_transformer_command
307
- end
308
-
309
- # Transform input using configured transformer (block or command)
310
- #
311
- # Executes either Ruby block or bash command transformer.
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
- #
319
- # **Exit code behavior (bash commands only):**
320
- # - Exit 0: Use STDOUT as transformed content
321
- # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
322
- # - Exit 2: Halt workflow with error (STDOUT ignored)
323
- #
324
- # @param context [NodeContext] Context with previous results and metadata
325
- # @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
326
- # @return [String, Hash] Transformed input OR control hash (skip_execution, halt_workflow, goto_node)
327
- # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
328
- def transform_input(context, current_input:)
329
- # No transformer configured: return content as-is
330
- return context.content unless @input_transformer || @input_transformer_command
331
-
332
- # Ruby block transformer
333
- # Ruby blocks can return String (transformed content) OR Hash (control flow)
334
- if @input_transformer
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
343
- end
344
-
345
- # Bash command transformer
346
- # Bash commands use exit codes to control behavior:
347
- # - Exit 0: Success, use STDOUT as transformed content
348
- # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
349
- # - Exit 2: Halt workflow with error (STDOUT ignored)
350
- if @input_transformer_command
351
- result = TransformerExecutor.execute(
352
- command: @input_transformer_command[:command],
353
- context: context,
354
- event: "input",
355
- node_name: @name,
356
- fallback_content: current_input, # Used for exit 1 (skip)
357
- timeout: @input_transformer_command[:timeout],
358
- )
359
-
360
- # Handle transformer result based on exit code
361
- if result.halt?
362
- # Exit 2: Halt workflow with error
363
- raise ConfigurationError,
364
- "Input transformer halted workflow for node '#{@name}': #{result.error_message}"
365
- elsif result.skip_execution?
366
- # Exit 1: Skip node execution, return skip hash
367
- # Content is current_input unchanged (STDOUT was ignored)
368
- { skip_execution: true, content: result.content }
369
- else
370
- # Exit 0: Return transformed content from STDOUT
371
- result.content
372
- end
373
- end
374
- end
375
-
376
- # Transform output using configured transformer (block or command)
377
- #
378
- # Executes either Ruby block or bash command transformer.
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
- #
385
- # **Exit code behavior (bash commands only):**
386
- # - Exit 0: Use STDOUT as transformed content
387
- # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
388
- # - Exit 2: Halt workflow with error (STDOUT ignored)
389
- #
390
- # @param context [NodeContext] Context with current result and metadata
391
- # @return [String, Hash] Transformed output OR control hash (halt_workflow, goto_node)
392
- # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
393
- def transform_output(context)
394
- # No transformer configured: return content as-is
395
- return context.content unless @output_transformer || @output_transformer_command
396
-
397
- # Ruby block transformer
398
- # Ruby blocks can return String (transformed content) OR Hash (control flow)
399
- if @output_transformer
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
408
- end
409
-
410
- # Bash command transformer
411
- # Bash commands use exit codes to control behavior:
412
- # - Exit 0: Success, use STDOUT as transformed content
413
- # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
414
- # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
415
- if @output_transformer_command
416
- result = TransformerExecutor.execute(
417
- command: @output_transformer_command[:command],
418
- context: context,
419
- event: "output",
420
- node_name: @name,
421
- fallback_content: context.content, # result.content for exit 1
422
- timeout: @output_transformer_command[:timeout],
423
- )
424
-
425
- # Handle transformer result based on exit code
426
- if result.halt?
427
- # Exit 2: Halt workflow with error
428
- raise ConfigurationError,
429
- "Output transformer halted workflow for node '#{@name}': #{result.error_message}"
430
- else
431
- # Exit 0: Return transformed content from STDOUT
432
- # Exit 1: Return fallback (result.content unchanged)
433
- result.content
434
- end
435
- end
436
- end
437
-
438
- # Get the lead agent for this node
439
- #
440
- # @return [Symbol] Lead agent name
441
- def lead_agent
442
- @lead_override || @agent_configs.first&.dig(:agent)
443
- end
444
-
445
- # Check if this is an agent-less (computation-only) node
446
- #
447
- # Agent-less nodes run pure Ruby code without LLM execution.
448
- # They must have at least one transformer (input or output).
449
- #
450
- # @return [Boolean]
451
- def agent_less?
452
- @agent_configs.empty?
453
- end
454
-
455
- # Validate node configuration
456
- #
457
- # Also auto-adds agents that are referenced in delegates_to but not explicitly declared.
458
- # This allows writing: agent(:backend).delegates_to(:verifier)
459
- # without needing: agent(:verifier)
460
- #
461
- # @return [void]
462
- # @raise [ConfigurationError] If configuration is invalid
463
- def validate!
464
- # Auto-add agents mentioned in delegates_to but not explicitly declared
465
- auto_add_delegate_agents
466
-
467
- # Agent-less nodes (pure computation) are allowed but need transformers
468
- if @agent_configs.empty?
469
- unless has_input_transformer? || has_output_transformer?
470
- raise ConfigurationError,
471
- "Agent-less node '#{@name}' must have at least one transformer (input or output). " \
472
- "Either add agents with agent(:name) or add input/output transformers."
473
- end
474
- end
475
-
476
- # If has agents, validate lead override
477
- if @lead_override && !@agent_configs.any? { |ac| ac[:agent] == @lead_override }
478
- raise ConfigurationError,
479
- "Node '#{@name}' lead agent '#{@lead_override}' not found in node's agents"
480
- end
481
- end
482
-
483
- private
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
-
529
- # Auto-add agents that are mentioned in delegates_to but not explicitly declared
530
- #
531
- # This allows:
532
- # agent(:backend).delegates_to(:tester)
533
- # Without needing:
534
- # agent(:tester)
535
- #
536
- # The tester agent is automatically added to the node with no delegation
537
- # and reset_context: true (fresh context by default).
538
- #
539
- # @return [void]
540
- def auto_add_delegate_agents
541
- # Collect all agents mentioned in delegates_to
542
- all_delegates = @agent_configs.flat_map { |ac| ac[:delegates_to] }.uniq
543
-
544
- # Find delegates that aren't explicitly declared
545
- declared_agents = @agent_configs.map { |ac| ac[:agent] }
546
- missing_delegates = all_delegates - declared_agents
547
-
548
- # Auto-add missing delegates with empty delegation and default reset_context
549
- missing_delegates.each do |delegate_name|
550
- @agent_configs << { agent: delegate_name, delegates_to: [], reset_context: true }
551
- end
552
- end
553
- end
554
- end
555
- end