swarm_sdk 2.0.0.pre.2 → 2.0.1

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.
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Node
5
+ # Builder provides DSL for configuring nodes (mini-swarms 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 Builder
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 syntax.
47
+ # If delegates_to is not called, the agent is registered with no delegation.
48
+ #
49
+ # @param name [Symbol] Agent name
50
+ # @return [AgentConfig] Fluent configuration object
51
+ #
52
+ # @example With delegation
53
+ # agent(:backend).delegates_to(:tester, :database)
54
+ #
55
+ # @example Without delegation
56
+ # agent(:planner)
57
+ def agent(name)
58
+ config = AgentConfig.new(name, self)
59
+
60
+ # Register immediately with empty delegation
61
+ # If delegates_to is called later, it will update this
62
+ register_agent(name, [])
63
+
64
+ config
65
+ end
66
+
67
+ # Register an agent configuration (called by AgentConfig)
68
+ #
69
+ # @param agent_name [Symbol] Agent name
70
+ # @param delegates_to [Array<Symbol>] Delegation targets
71
+ # @return [void]
72
+ def register_agent(agent_name, delegates_to)
73
+ # Check if agent already registered
74
+ existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
75
+
76
+ if existing
77
+ # Update delegation (happens when delegates_to is called after agent())
78
+ existing[:delegates_to] = delegates_to
79
+ else
80
+ # Add new agent configuration
81
+ @agent_configs << { agent: agent_name, delegates_to: delegates_to }
82
+ end
83
+ end
84
+
85
+ # Declare dependencies (nodes that must execute before this one)
86
+ #
87
+ # @param node_names [Array<Symbol>] Names of prerequisite nodes
88
+ # @return [void]
89
+ #
90
+ # @example Single dependency
91
+ # depends_on :planning
92
+ #
93
+ # @example Multiple dependencies
94
+ # depends_on :frontend, :backend
95
+ def depends_on(*node_names)
96
+ @dependencies.concat(node_names.map(&:to_sym))
97
+ end
98
+
99
+ # Override the lead agent (first agent is lead by default)
100
+ #
101
+ # @param agent_name [Symbol] Name of agent to make lead
102
+ # @return [void]
103
+ #
104
+ # @example
105
+ # agent(:backend).delegates_to(:tester)
106
+ # agent(:tester)
107
+ # lead :tester # tester is lead instead of backend
108
+ def lead(agent_name)
109
+ @lead_override = agent_name.to_sym
110
+ end
111
+
112
+ # Define input transformer for this node
113
+ #
114
+ # The transformer receives a NodeContext object with access to:
115
+ # - Previous node's result (convenience: ctx.content)
116
+ # - Original user prompt (ctx.original_prompt)
117
+ # - All previous node results (ctx.all_results[:node_name])
118
+ # - Current node metadata (ctx.node_name, ctx.dependencies)
119
+ #
120
+ # Can also be used for side effects (logging, file I/O) since the block
121
+ # runs at execution time, not declaration time.
122
+ #
123
+ # **Skip Execution**: Return a hash with `skip_execution: true` to skip
124
+ # the node's swarm execution and immediately return the provided content.
125
+ # Useful for caching, validation, or conditional execution.
126
+ #
127
+ # @yield [NodeContext] Context with previous results and metadata
128
+ # @return [String, Hash] Transformed input OR skip hash
129
+ #
130
+ # @example Access previous result and original prompt
131
+ # input do |ctx|
132
+ # # Convenience accessor
133
+ # previous_content = ctx.content
134
+ #
135
+ # # Access original prompt
136
+ # "Original: #{ctx.original_prompt}\nPrevious: #{previous_content}"
137
+ # end
138
+ #
139
+ # @example Access results from specific nodes
140
+ # input do |ctx|
141
+ # plan = ctx.all_results[:planning].content
142
+ # design = ctx.all_results[:design].content
143
+ #
144
+ # "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
145
+ # end
146
+ #
147
+ # @example Skip execution (caching)
148
+ # input do |ctx|
149
+ # cached = check_cache(ctx.content)
150
+ # if cached
151
+ # # Skip LLM call, return cached result
152
+ # { skip_execution: true, content: cached }
153
+ # else
154
+ # ctx.content
155
+ # end
156
+ # end
157
+ #
158
+ # @example Skip execution (validation)
159
+ # input do |ctx|
160
+ # if ctx.content.length > 10000
161
+ # # Fail early without LLM call
162
+ # { skip_execution: true, content: "ERROR: Input too long" }
163
+ # else
164
+ # ctx.content
165
+ # end
166
+ # end
167
+ def input(&block)
168
+ @input_transformer = block
169
+ end
170
+
171
+ # Set input transformer as bash command (YAML API)
172
+ #
173
+ # The command receives NodeContext as JSON on STDIN and outputs transformed content.
174
+ #
175
+ # **Exit codes:**
176
+ # - 0: Success, use STDOUT as transformed content
177
+ # - 1: Skip node execution, use current_input unchanged (STDOUT ignored)
178
+ # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
179
+ #
180
+ # @param command [String] Bash command to execute
181
+ # @param timeout [Integer] Timeout in seconds (default: 60)
182
+ # @return [void]
183
+ #
184
+ # @example
185
+ # input_command("scripts/validate.sh", timeout: 30)
186
+ def input_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
187
+ @input_transformer_command = { command: command, timeout: timeout }
188
+ end
189
+
190
+ # Define output transformer for this node
191
+ #
192
+ # The transformer receives a NodeContext object with access to:
193
+ # - Current node's result (convenience: ctx.content)
194
+ # - Original user prompt (ctx.original_prompt)
195
+ # - All completed node results (ctx.all_results[:node_name])
196
+ # - Current node metadata (ctx.node_name)
197
+ #
198
+ # Can also be used for side effects (logging, file I/O) since the block
199
+ # runs at execution time, not declaration time.
200
+ #
201
+ # @yield [NodeContext] Context with current result and metadata
202
+ # @return [String] Transformed output
203
+ #
204
+ # @example Transform and save to file
205
+ # output do |ctx|
206
+ # # Side effect: save to file
207
+ # File.write("results/plan.txt", ctx.content)
208
+ #
209
+ # # Return transformed output for next node
210
+ # "Key decisions: #{extract_decisions(ctx.content)}"
211
+ # end
212
+ #
213
+ # @example Access original prompt
214
+ # output do |ctx|
215
+ # # Include original context in output
216
+ # "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
217
+ # end
218
+ #
219
+ # @example Access multiple node results
220
+ # output do |ctx|
221
+ # plan = ctx.all_results[:planning].content
222
+ # impl = ctx.content
223
+ #
224
+ # "Completed:\nPlan: #{plan}\nImpl: #{impl}"
225
+ # end
226
+ def output(&block)
227
+ @output_transformer = block
228
+ end
229
+
230
+ # Set output transformer as bash command (YAML API)
231
+ #
232
+ # The command receives NodeContext as JSON on STDIN and outputs transformed content.
233
+ #
234
+ # **Exit codes:**
235
+ # - 0: Success, use STDOUT as transformed content
236
+ # - 1: Pass through unchanged, use result.content (STDOUT ignored)
237
+ # - 2: Halt workflow with error, show STDERR (STDOUT ignored)
238
+ #
239
+ # @param command [String] Bash command to execute
240
+ # @param timeout [Integer] Timeout in seconds (default: 60)
241
+ # @return [void]
242
+ #
243
+ # @example
244
+ # output_command("scripts/format.sh", timeout: 30)
245
+ def output_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
246
+ @output_transformer_command = { command: command, timeout: timeout }
247
+ end
248
+
249
+ # Check if node has any input transformer (block or command)
250
+ #
251
+ # @return [Boolean]
252
+ def has_input_transformer?
253
+ @input_transformer || @input_transformer_command
254
+ end
255
+
256
+ # Check if node has any output transformer (block or command)
257
+ #
258
+ # @return [Boolean]
259
+ def has_output_transformer?
260
+ @output_transformer || @output_transformer_command
261
+ end
262
+
263
+ # Transform input using configured transformer (block or command)
264
+ #
265
+ # Executes either Ruby block or bash command transformer.
266
+ #
267
+ # **Exit code behavior (bash commands only):**
268
+ # - Exit 0: Use STDOUT as transformed content
269
+ # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
270
+ # - Exit 2: Halt workflow with error (STDOUT ignored)
271
+ #
272
+ # @param context [NodeContext] Context with previous results and metadata
273
+ # @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
274
+ # @return [String, Hash] Transformed input OR skip hash `{ skip_execution: true, content: "..." }`
275
+ # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
276
+ def transform_input(context, current_input:)
277
+ # No transformer configured: return content as-is
278
+ return context.content unless @input_transformer || @input_transformer_command
279
+
280
+ # Ruby block transformer
281
+ # Ruby blocks can return String (transformed content) OR Hash (skip_execution)
282
+ if @input_transformer
283
+ return @input_transformer.call(context)
284
+ end
285
+
286
+ # Bash command transformer
287
+ # Bash commands use exit codes to control behavior:
288
+ # - Exit 0: Success, use STDOUT as transformed content
289
+ # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
290
+ # - Exit 2: Halt workflow with error (STDOUT ignored)
291
+ if @input_transformer_command
292
+ result = TransformerExecutor.execute(
293
+ command: @input_transformer_command[:command],
294
+ context: context,
295
+ event: "input",
296
+ node_name: @name,
297
+ fallback_content: current_input, # Used for exit 1 (skip)
298
+ timeout: @input_transformer_command[:timeout],
299
+ )
300
+
301
+ # Handle transformer result based on exit code
302
+ if result.halt?
303
+ # Exit 2: Halt workflow with error
304
+ raise ConfigurationError,
305
+ "Input transformer halted workflow for node '#{@name}': #{result.error_message}"
306
+ elsif result.skip_execution?
307
+ # Exit 1: Skip node execution, return skip hash
308
+ # Content is current_input unchanged (STDOUT was ignored)
309
+ { skip_execution: true, content: result.content }
310
+ else
311
+ # Exit 0: Return transformed content from STDOUT
312
+ result.content
313
+ end
314
+ end
315
+ end
316
+
317
+ # Transform output using configured transformer (block or command)
318
+ #
319
+ # Executes either Ruby block or bash command transformer.
320
+ #
321
+ # **Exit code behavior (bash commands only):**
322
+ # - Exit 0: Use STDOUT as transformed content
323
+ # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
324
+ # - Exit 2: Halt workflow with error (STDOUT ignored)
325
+ #
326
+ # @param context [NodeContext] Context with current result and metadata
327
+ # @return [String] Transformed output
328
+ # @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
329
+ def transform_output(context)
330
+ # No transformer configured: return content as-is
331
+ return context.content unless @output_transformer || @output_transformer_command
332
+
333
+ # Ruby block transformer
334
+ # Simply calls the block with context and returns result
335
+ if @output_transformer
336
+ return @output_transformer.call(context)
337
+ end
338
+
339
+ # Bash command transformer
340
+ # Bash commands use exit codes to control behavior:
341
+ # - Exit 0: Success, use STDOUT as transformed content
342
+ # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
343
+ # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
344
+ if @output_transformer_command
345
+ result = TransformerExecutor.execute(
346
+ command: @output_transformer_command[:command],
347
+ context: context,
348
+ event: "output",
349
+ node_name: @name,
350
+ fallback_content: context.content, # result.content for exit 1
351
+ timeout: @output_transformer_command[:timeout],
352
+ )
353
+
354
+ # Handle transformer result based on exit code
355
+ if result.halt?
356
+ # Exit 2: Halt workflow with error
357
+ raise ConfigurationError,
358
+ "Output transformer halted workflow for node '#{@name}': #{result.error_message}"
359
+ else
360
+ # Exit 0: Return transformed content from STDOUT
361
+ # Exit 1: Return fallback (result.content unchanged)
362
+ result.content
363
+ end
364
+ end
365
+ end
366
+
367
+ # Get the lead agent for this node
368
+ #
369
+ # @return [Symbol] Lead agent name
370
+ def lead_agent
371
+ @lead_override || @agent_configs.first&.dig(:agent)
372
+ end
373
+
374
+ # Check if this is an agent-less (computation-only) node
375
+ #
376
+ # Agent-less nodes run pure Ruby code without LLM execution.
377
+ # They must have at least one transformer (input or output).
378
+ #
379
+ # @return [Boolean]
380
+ def agent_less?
381
+ @agent_configs.empty?
382
+ end
383
+
384
+ # Validate node configuration
385
+ #
386
+ # Also auto-adds agents that are referenced in delegates_to but not explicitly declared.
387
+ # This allows writing: agent(:backend).delegates_to(:verifier)
388
+ # without needing: agent(:verifier)
389
+ #
390
+ # @return [void]
391
+ # @raise [ConfigurationError] If configuration is invalid
392
+ def validate!
393
+ # Auto-add agents mentioned in delegates_to but not explicitly declared
394
+ auto_add_delegate_agents
395
+
396
+ # Agent-less nodes (pure computation) are allowed but need transformers
397
+ if @agent_configs.empty?
398
+ unless has_input_transformer? || has_output_transformer?
399
+ raise ConfigurationError,
400
+ "Agent-less node '#{@name}' must have at least one transformer (input or output). " \
401
+ "Either add agents with agent(:name) or add input/output transformers."
402
+ end
403
+ end
404
+
405
+ # If has agents, validate lead override
406
+ if @lead_override && !@agent_configs.any? { |ac| ac[:agent] == @lead_override }
407
+ raise ConfigurationError,
408
+ "Node '#{@name}' lead agent '#{@lead_override}' not found in node's agents"
409
+ end
410
+ end
411
+
412
+ private
413
+
414
+ # Auto-add agents that are mentioned in delegates_to but not explicitly declared
415
+ #
416
+ # This allows:
417
+ # agent(:backend).delegates_to(:tester)
418
+ # Without needing:
419
+ # agent(:tester)
420
+ #
421
+ # The tester agent is automatically added to the node with no delegation.
422
+ #
423
+ # @return [void]
424
+ def auto_add_delegate_agents
425
+ # Collect all agents mentioned in delegates_to
426
+ all_delegates = @agent_configs.flat_map { |ac| ac[:delegates_to] }.uniq
427
+
428
+ # Find delegates that aren't explicitly declared
429
+ declared_agents = @agent_configs.map { |ac| ac[:agent] }
430
+ missing_delegates = all_delegates - declared_agents
431
+
432
+ # Auto-add missing delegates with empty delegation
433
+ missing_delegates.each do |delegate_name|
434
+ @agent_configs << { agent: delegate_name, delegates_to: [] }
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "json"
5
+ require "timeout"
6
+
7
+ module SwarmSDK
8
+ module Node
9
+ # Executes bash command transformers for node input/output transformation
10
+ #
11
+ # Transformers are shell commands that receive NodeContext data on STDIN as JSON
12
+ # and produce transformed content on STDOUT.
13
+ #
14
+ # ## Exit Code Behavior
15
+ #
16
+ # - **Exit 0**: Transform success
17
+ # - Use STDOUT as the transformed content
18
+ # - Node execution proceeds with transformed content
19
+ #
20
+ # - **Exit 1**: Skip node execution (pass-through)
21
+ # - STDOUT is IGNORED
22
+ # - Input transformer: Use current_input unchanged (no transformation)
23
+ # - Output transformer: Use result.content unchanged (no transformation)
24
+ # - For input transformer: Also skips the node's LLM execution
25
+ #
26
+ # - **Exit 2**: Halt entire workflow
27
+ # - STDOUT is IGNORED
28
+ # - STDERR is shown as error message
29
+ # - Workflow stops immediately with error
30
+ #
31
+ # ## JSON Input Format (STDIN)
32
+ #
33
+ # **Input transformer receives:**
34
+ # ```json
35
+ # {
36
+ # "event": "input",
37
+ # "node": "implementation",
38
+ # "original_prompt": "Build auth API",
39
+ # "content": "PLAN: Create endpoints...",
40
+ # "all_results": {
41
+ # "planning": {
42
+ # "content": "Create endpoints...",
43
+ # "agent": "planner",
44
+ # "duration": 2.5,
45
+ # "success": true
46
+ # }
47
+ # },
48
+ # "dependencies": ["planning"]
49
+ # }
50
+ # ```
51
+ #
52
+ # **Output transformer receives:**
53
+ # ```json
54
+ # {
55
+ # "event": "output",
56
+ # "node": "implementation",
57
+ # "original_prompt": "Build auth API",
58
+ # "content": "Implementation complete",
59
+ # "all_results": {
60
+ # "planning": {...},
61
+ # "implementation": {...}
62
+ # }
63
+ # }
64
+ # ```
65
+ #
66
+ # @example Input transformer that validates
67
+ # # validate.sh
68
+ # #!/bin/bash
69
+ # INPUT=$(cat)
70
+ # CONTENT=$(echo "$INPUT" | jq -r '.content')
71
+ #
72
+ # if [ ${#CONTENT} -gt 10000 ]; then
73
+ # echo "Content too long" >&2
74
+ # exit 2 # Halt workflow
75
+ # fi
76
+ #
77
+ # echo "$CONTENT"
78
+ # exit 0
79
+ #
80
+ # @example Input transformer that caches (skip execution)
81
+ # # cache_check.sh
82
+ # #!/bin/bash
83
+ # INPUT=$(cat)
84
+ # CONTENT=$(echo "$INPUT" | jq -r '.content')
85
+ #
86
+ # if cached "$CONTENT"; then
87
+ # exit 1 # Skip node execution, pass through unchanged
88
+ # fi
89
+ #
90
+ # echo "$CONTENT"
91
+ # exit 0
92
+ class TransformerExecutor
93
+ DEFAULT_TIMEOUT = 60
94
+
95
+ # Result object for transformer execution
96
+ TransformerResult = Struct.new(:success, :content, :skip_execution, :halt, :error_message, keyword_init: true) do
97
+ def skip_execution?
98
+ skip_execution
99
+ end
100
+
101
+ def halt?
102
+ halt
103
+ end
104
+ end
105
+
106
+ class << self
107
+ # Execute a transformer shell command
108
+ #
109
+ # @param command [String] Shell command to execute
110
+ # @param context [NodeContext] Node context for building JSON input
111
+ # @param event [String] Event type ("input" or "output")
112
+ # @param node_name [Symbol] Current node name
113
+ # @param fallback_content [String] Content to use if skip (exit 1)
114
+ # @param timeout [Integer] Timeout in seconds (default: 60)
115
+ # @return [TransformerResult] Result with transformed content or skip/halt flags
116
+ def execute(command:, context:, event:, node_name:, fallback_content:, timeout: DEFAULT_TIMEOUT)
117
+ # Build JSON input for transformer
118
+ input_json = build_transformer_input(context, event, node_name)
119
+
120
+ # Build environment variables
121
+ env = build_environment(node_name: node_name)
122
+
123
+ # Execute command with JSON stdin and timeout
124
+ stdout, stderr, status = Timeout.timeout(timeout) do
125
+ Open3.capture3(
126
+ env,
127
+ command,
128
+ stdin_data: JSON.generate(input_json),
129
+ )
130
+ end
131
+
132
+ # Handle exit code
133
+ # Exit 0: Transform success, use STDOUT
134
+ # Exit 1: Skip node execution, use fallback_content (IGNORE STDOUT)
135
+ # Exit 2: Halt workflow with error (IGNORE STDOUT)
136
+ case status.exitstatus
137
+ when 0
138
+ # Success: use STDOUT as transformed content (strip trailing newline)
139
+ TransformerResult.new(
140
+ success: true,
141
+ content: stdout.chomp, # Remove trailing newline from echo
142
+ skip_execution: false,
143
+ halt: false,
144
+ error_message: nil,
145
+ )
146
+ when 1
147
+ # Skip node execution: use fallback_content unchanged (IGNORE STDOUT)
148
+ # For input transformer: skip_execution = true (skip LLM call)
149
+ # For output transformer: skip_execution = false (just pass through)
150
+ TransformerResult.new(
151
+ success: true,
152
+ content: fallback_content,
153
+ skip_execution: (event == "input"), # Only skip LLM for input transformers
154
+ halt: false,
155
+ error_message: nil,
156
+ )
157
+ when 2
158
+ # Halt workflow: return error (IGNORE STDOUT)
159
+ error_msg = stderr.strip.empty? ? "Transformer halted workflow (exit 2)" : stderr.strip
160
+ TransformerResult.new(
161
+ success: false,
162
+ content: nil,
163
+ skip_execution: false,
164
+ halt: true,
165
+ error_message: error_msg,
166
+ )
167
+ else
168
+ # Unknown exit code: treat as error (halt)
169
+ error_msg = "Transformer exited with code #{status.exitstatus}\nSTDERR: #{stderr}"
170
+ TransformerResult.new(
171
+ success: false,
172
+ content: nil,
173
+ skip_execution: false,
174
+ halt: true,
175
+ error_message: error_msg,
176
+ )
177
+ end
178
+ rescue Timeout::Error
179
+ # Timeout: halt workflow
180
+ TransformerResult.new(
181
+ success: false,
182
+ content: nil,
183
+ skip_execution: false,
184
+ halt: true,
185
+ error_message: "Transformer command timed out after #{timeout}s",
186
+ )
187
+ rescue StandardError => e
188
+ # Execution error: halt workflow
189
+ TransformerResult.new(
190
+ success: false,
191
+ content: nil,
192
+ skip_execution: false,
193
+ halt: true,
194
+ error_message: "Transformer command failed: #{e.message}",
195
+ )
196
+ end
197
+
198
+ private
199
+
200
+ # Build JSON input for transformer command
201
+ #
202
+ # @param context [NodeContext] Node context
203
+ # @param event [String] Event type ("input" or "output")
204
+ # @param node_name [Symbol] Node name
205
+ # @return [Hash] JSON data to pass on stdin
206
+ def build_transformer_input(context, event, node_name)
207
+ base = {
208
+ event: event,
209
+ node: node_name.to_s,
210
+ original_prompt: context.original_prompt,
211
+ content: context.content,
212
+ }
213
+
214
+ # Add all_results (convert Result objects to hashes)
215
+ if context.all_results && !context.all_results.empty?
216
+ base[:all_results] = context.all_results.transform_values do |result|
217
+ {
218
+ content: result.content,
219
+ agent: result.agent,
220
+ duration: result.duration,
221
+ success: result.success?,
222
+ }
223
+ end
224
+ end
225
+
226
+ # Add dependencies for input transformers
227
+ if event == "input" && context.dependencies
228
+ base[:dependencies] = context.dependencies.map(&:to_s)
229
+ end
230
+
231
+ base
232
+ end
233
+
234
+ # Build environment variables for transformer execution
235
+ #
236
+ # @param node_name [Symbol] Current node name
237
+ # @return [Hash] Environment variables
238
+ def build_environment(node_name:)
239
+ {
240
+ "SWARM_SDK_PROJECT_DIR" => Dir.pwd,
241
+ "SWARM_SDK_NODE_NAME" => node_name.to_s,
242
+ "PATH" => ENV.fetch("PATH", ""),
243
+ }
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end