swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  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/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  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 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -1,593 +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: nil)
218
- timeout ||= SwarmSDK.config.transformer_command_timeout
219
- @input_transformer_command = { command: command, timeout: timeout }
220
- end
221
-
222
- # Define output transformer for this node
223
- #
224
- # The transformer receives a NodeContext object with access to:
225
- # - Current node's result (convenience: ctx.content)
226
- # - Original user prompt (ctx.original_prompt)
227
- # - All completed node results (ctx.all_results[:node_name])
228
- # - Current node metadata (ctx.node_name)
229
- #
230
- # Can also be used for side effects (logging, file I/O) since the block
231
- # runs at execution time, not declaration time.
232
- #
233
- # **Control Flow**: Return a hash with special keys to control execution:
234
- # - `halt_workflow: true` - Halt entire workflow with content as final result
235
- # - `goto_node: :node_name` - Jump to different node with content as input
236
- #
237
- # @yield [NodeContext] Context with current result and metadata
238
- # @return [String, Hash] Transformed output OR control hash
239
- #
240
- # @example Transform and save to file
241
- # output do |ctx|
242
- # # Side effect: save to file
243
- # File.write("results/plan.txt", ctx.content)
244
- #
245
- # # Return transformed output for next node
246
- # "Key decisions: #{extract_decisions(ctx.content)}"
247
- # end
248
- #
249
- # @example Access original prompt
250
- # output do |ctx|
251
- # # Include original context in output
252
- # "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
253
- # end
254
- #
255
- # @example Halt workflow (convergence check) - using return
256
- # output do |ctx|
257
- # return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
258
- # ctx.content
259
- # end
260
- #
261
- # @example Jump to different node (conditional routing) - using return
262
- # output do |ctx|
263
- # if needs_revision?(ctx.content)
264
- # # Go back to revision node - return works safely!
265
- # return ctx.goto_node(:revision, content: ctx.content)
266
- # end
267
- # ctx.content
268
- # end
269
- #
270
- # @note The output block is automatically converted to a lambda, which means
271
- # return statements work safely and only exit the transformer, not the
272
- # entire program. This allows natural control flow patterns.
273
- def output(&block)
274
- @output_transformer = ProcHelpers.to_lambda(block)
275
- end
276
-
277
- # Set output transformer as bash command (YAML API)
278
- #
279
- # The command receives NodeContext as JSON on STDIN and outputs transformed content.
280
- #
281
- # **Exit codes:**
282
- # - 0: Success, use STDOUT as transformed content
283
- # - 1: Pass through unchanged, use result.content (STDOUT ignored)
284
- # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
285
- #
286
- # @param command [String] Bash command to execute
287
- # @param timeout [Integer] Timeout in seconds (default: 60)
288
- # @return [void]
289
- #
290
- # @example
291
- # output_command("scripts/format.sh", timeout: 30)
292
- def output_command(command, timeout: nil)
293
- timeout ||= SwarmSDK.config.transformer_command_timeout
294
- @output_transformer_command = { command: command, timeout: timeout }
295
- end
296
-
297
- # Check if node has any input transformer (block or command)
298
- #
299
- # @return [Boolean]
300
- def has_input_transformer?
301
- @input_transformer || @input_transformer_command
302
- end
303
-
304
- # Check if node has any output transformer (block or command)
305
- #
306
- # @return [Boolean]
307
- def has_output_transformer?
308
- @output_transformer || @output_transformer_command
309
- end
310
-
311
- # Transform input using configured transformer (block or command)
312
- #
313
- # Executes either Ruby block or bash command transformer.
314
- #
315
- # **Ruby block return values:**
316
- # - String: Transformed content
317
- # - Hash with `skip_execution: true`: Skip node execution
318
- # - Hash with `halt_workflow: true`: Halt entire workflow
319
- # - Hash with `goto_node: :name`: Jump to different node
320
- #
321
- # **Exit code behavior (bash commands only):**
322
- # - Exit 0: Use STDOUT as transformed content
323
- # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
324
- # - Exit 2: Halt workflow with error (STDOUT ignored)
325
- #
326
- # @param context [NodeContext] Context with previous results and metadata
327
- # @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
328
- # @return [String, Hash] Transformed input OR control hash (skip_execution, halt_workflow, goto_node)
329
- # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
330
- def transform_input(context, current_input:)
331
- # No transformer configured: return content as-is
332
- return context.content unless @input_transformer || @input_transformer_command
333
-
334
- # Ruby block transformer
335
- # Ruby blocks can return String (transformed content) OR Hash (control flow)
336
- if @input_transformer
337
- result = @input_transformer.call(context)
338
-
339
- # If hash, validate control flow keys
340
- if result.is_a?(Hash)
341
- validate_transformer_hash(result, :input)
342
- end
343
-
344
- return result
345
- end
346
-
347
- # Bash command transformer
348
- # Bash commands use exit codes to control behavior:
349
- # - Exit 0: Success, use STDOUT as transformed content
350
- # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
351
- # - Exit 2: Halt workflow with error (STDOUT ignored)
352
- if @input_transformer_command
353
- result = TransformerExecutor.execute(
354
- command: @input_transformer_command[:command],
355
- context: context,
356
- event: "input",
357
- node_name: @name,
358
- fallback_content: current_input, # Used for exit 1 (skip)
359
- timeout: @input_transformer_command[:timeout],
360
- )
361
-
362
- # Handle transformer result based on exit code
363
- if result.halt?
364
- # Exit 2: Halt workflow with error
365
- raise ConfigurationError,
366
- "Input transformer halted workflow for node '#{@name}': #{result.error_message}"
367
- elsif result.skip_execution?
368
- # Exit 1: Skip node execution, return skip hash
369
- # Content is current_input unchanged (STDOUT was ignored)
370
- { skip_execution: true, content: result.content }
371
- else
372
- # Exit 0: Return transformed content from STDOUT
373
- result.content
374
- end
375
- end
376
- end
377
-
378
- # Transform output using configured transformer (block or command)
379
- #
380
- # Executes either Ruby block or bash command transformer.
381
- #
382
- # **Ruby block return values:**
383
- # - String: Transformed content
384
- # - Hash with `halt_workflow: true`: Halt entire workflow
385
- # - Hash with `goto_node: :name`: Jump to different node
386
- #
387
- # **Exit code behavior (bash commands only):**
388
- # - Exit 0: Use STDOUT as transformed content
389
- # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
390
- # - Exit 2: Halt workflow with error (STDOUT ignored)
391
- #
392
- # @param context [NodeContext] Context with current result and metadata
393
- # @return [String, Hash] Transformed output OR control hash (halt_workflow, goto_node)
394
- # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
395
- def transform_output(context)
396
- # No transformer configured: return content as-is
397
- return context.content unless @output_transformer || @output_transformer_command
398
-
399
- # Ruby block transformer
400
- # Ruby blocks can return String (transformed content) OR Hash (control flow)
401
- if @output_transformer
402
- result = @output_transformer.call(context)
403
-
404
- # If hash, validate control flow keys
405
- if result.is_a?(Hash)
406
- validate_transformer_hash(result, :output)
407
- end
408
-
409
- return result
410
- end
411
-
412
- # Bash command transformer
413
- # Bash commands use exit codes to control behavior:
414
- # - Exit 0: Success, use STDOUT as transformed content
415
- # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
416
- # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
417
- if @output_transformer_command
418
- result = TransformerExecutor.execute(
419
- command: @output_transformer_command[:command],
420
- context: context,
421
- event: "output",
422
- node_name: @name,
423
- fallback_content: context.content, # result.content for exit 1
424
- timeout: @output_transformer_command[:timeout],
425
- )
426
-
427
- # Handle transformer result based on exit code
428
- if result.halt?
429
- # Exit 2: Halt workflow with error
430
- raise ConfigurationError,
431
- "Output transformer halted workflow for node '#{@name}': #{result.error_message}"
432
- else
433
- # Exit 0: Return transformed content from STDOUT
434
- # Exit 1: Return fallback (result.content unchanged)
435
- result.content
436
- end
437
- end
438
- end
439
-
440
- # Get the lead agent for this node
441
- #
442
- # @return [Symbol] Lead agent name
443
- def lead_agent
444
- @lead_override || @agent_configs.first&.dig(:agent)
445
- end
446
-
447
- # Check if this is an agent-less (computation-only) node
448
- #
449
- # Agent-less nodes run pure Ruby code without LLM execution.
450
- # They must have at least one transformer (input or output).
451
- #
452
- # @return [Boolean]
453
- def agent_less?
454
- @agent_configs.empty?
455
- end
456
-
457
- # Validate node configuration
458
- #
459
- # Also auto-adds agents that are referenced in delegates_to but not explicitly declared.
460
- # This allows writing: agent(:backend).delegates_to(:verifier)
461
- # without needing: agent(:verifier)
462
- #
463
- # @return [void]
464
- # @raise [ConfigurationError] If configuration is invalid
465
- def validate!
466
- # Auto-add agents mentioned in delegates_to but not explicitly declared
467
- auto_add_delegate_agents
468
-
469
- # Agent-less nodes (pure computation) are allowed but need transformers
470
- if @agent_configs.empty?
471
- unless has_input_transformer? || has_output_transformer?
472
- raise ConfigurationError,
473
- "Agent-less node '#{@name}' must have at least one transformer (input or output). " \
474
- "Either add agents with agent(:name) or add input/output transformers."
475
- end
476
- end
477
-
478
- # If has agents, validate lead override
479
- if @lead_override && !@agent_configs.any? { |ac| ac[:agent] == @lead_override }
480
- raise ConfigurationError,
481
- "Node '#{@name}' lead agent '#{@lead_override}' not found in node's agents"
482
- end
483
- end
484
-
485
- private
486
-
487
- # Validate transformer hash return value
488
- #
489
- # Ensures hash has valid control flow keys and required content field.
490
- #
491
- # @param hash [Hash] Hash returned from transformer
492
- # @param transformer_type [Symbol] :input or :output
493
- # @return [void]
494
- # @raise [ConfigurationError] If hash is invalid
495
- def validate_transformer_hash(hash, transformer_type)
496
- # Valid control keys
497
- valid_keys = if transformer_type == :input
498
- [:skip_execution, :halt_workflow, :goto_node, :content]
499
- else
500
- [:halt_workflow, :goto_node, :content]
501
- end
502
-
503
- # Check for invalid keys
504
- invalid_keys = hash.keys - valid_keys
505
- if invalid_keys.any?
506
- raise ConfigurationError,
507
- "Invalid #{transformer_type} transformer hash keys: #{invalid_keys.join(", ")}. " \
508
- "Valid keys: #{valid_keys.join(", ")}"
509
- end
510
-
511
- # Ensure content is present
512
- unless hash.key?(:content)
513
- raise ConfigurationError,
514
- "#{transformer_type.capitalize} transformer hash must include :content key"
515
- end
516
-
517
- # Ensure only one control key
518
- control_keys = hash.keys & [:skip_execution, :halt_workflow, :goto_node]
519
- if control_keys.size > 1
520
- raise ConfigurationError,
521
- "#{transformer_type.capitalize} transformer hash can only have one control key, got: #{control_keys.join(", ")}"
522
- end
523
-
524
- # Validate goto_node has valid node name
525
- if hash[:goto_node] && !hash[:goto_node].is_a?(Symbol)
526
- raise ConfigurationError,
527
- "goto_node value must be a Symbol, got: #{hash[:goto_node].class}"
528
- end
529
- end
530
-
531
- # Auto-add agents that are mentioned in delegates_to but not explicitly declared
532
- #
533
- # This allows:
534
- # agent(:backend).delegates_to(:tester)
535
- # Without needing:
536
- # agent(:tester)
537
- #
538
- # The tester agent is automatically added to the node with no delegation
539
- # and reset_context: true (fresh context by default).
540
- #
541
- # @return [void]
542
- def auto_add_delegate_agents
543
- # Collect all agents mentioned in delegates_to
544
- # Extract agent names from all delegation configs (handles hash and array formats)
545
- all_delegates = @agent_configs.flat_map do |ac|
546
- extract_delegate_agent_names(ac[:delegates_to] || [])
547
- end.uniq
548
-
549
- # Find delegates that aren't explicitly declared
550
- declared_agents = @agent_configs.map { |ac| ac[:agent] }
551
- missing_delegates = all_delegates - declared_agents
552
-
553
- # Auto-add missing delegates with empty delegation and default reset_context
554
- missing_delegates.each do |delegate_name|
555
- @agent_configs << { agent: delegate_name, delegates_to: [], reset_context: true }
556
- end
557
- end
558
-
559
- # Extract agent names from delegation configuration
560
- #
561
- # Handles multiple formats:
562
- # - Array of symbols: [:frontend, :backend]
563
- # - Hash: {frontend: "Custom", backend: nil}
564
- # - Array of hashes: [{agent: :frontend, tool_name: "Custom"}]
565
- #
566
- # @param delegation_config [Array, Hash, nil] Delegation configuration
567
- # @return [Array<Symbol>] Array of agent name symbols
568
- def extract_delegate_agent_names(delegation_config)
569
- return [] if delegation_config.nil?
570
- return [] if delegation_config.respond_to?(:empty?) && delegation_config.empty?
571
-
572
- case delegation_config
573
- when Array
574
- delegation_config.map do |item|
575
- case item
576
- when Symbol, String
577
- item.to_sym
578
- when Hash
579
- # Extract agent name from normalized format
580
- agent_name = item[:agent] || item["agent"]
581
- agent_name&.to_sym
582
- end
583
- end.compact # Remove nils from malformed hashes
584
- when Hash
585
- # Hash format: keys are agent names
586
- delegation_config.keys.map(&:to_sym)
587
- else
588
- []
589
- end
590
- end
591
- end
592
- end
593
- end