swarm_sdk 2.7.13 → 3.0.0.alpha1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  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 -273
  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 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  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 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -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