Class: SwarmSDK::Workflow::NodeBuilder

Inherits:
Object
  • Object
show all
Defined in:
lib/swarm_sdk/workflow/node_builder.rb

Overview

NodeBuilder provides DSL for configuring individual nodes within a workflow

A node represents a stage in a multi-step workflow where a specific set of agents collaborate. Each node creates an independent swarm execution.

Examples:

Solo agent node

node :planning do
  agent(:architect)
end

Multi-agent node with delegation

node :implementation do
  agent(:backend).delegates_to(:tester, :database)
  agent(:tester).delegates_to(:database)
  agent(:database)

  depends_on :planning
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name) ⇒ NodeBuilder

Returns a new instance of NodeBuilder.



33
34
35
36
37
38
39
40
41
42
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 33

def initialize(name)
  @name = name
  @agent_configs = []
  @dependencies = []
  @lead_override = nil
  @input_transformer = nil # Ruby block
  @output_transformer = nil # Ruby block
  @input_transformer_command = nil # Bash command
  @output_transformer_command = nil # Bash command
end

Instance Attribute Details

#agent_configsObject (readonly)

Returns the value of attribute agent_configs.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def agent_configs
  @agent_configs
end

#dependenciesObject (readonly)

Returns the value of attribute dependencies.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def dependencies
  @dependencies
end

#input_transformerObject (readonly)

Returns the value of attribute input_transformer.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def input_transformer
  @input_transformer
end

#input_transformer_commandObject (readonly)

Returns the value of attribute input_transformer_command.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def input_transformer_command
  @input_transformer_command
end

#lead_overrideObject (readonly)

Returns the value of attribute lead_override.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def lead_override
  @lead_override
end

#nameObject (readonly)

Returns the value of attribute name.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def name
  @name
end

#output_transformerObject (readonly)

Returns the value of attribute output_transformer.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def output_transformer
  @output_transformer
end

#output_transformer_commandObject (readonly)

Returns the value of attribute output_transformer_command.



24
25
26
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 24

def output_transformer_command
  @output_transformer_command
end

Instance Method Details

#agent(name, reset_context: true) ⇒ AgentConfig

Configure an agent for this node

Returns an AgentConfig object that supports fluent delegation and tool override syntax. If delegates_to/tools are not called, the agent uses global configuration.

By default, agents get fresh context in each node (reset_context: true). Set reset_context: false to preserve conversation history across nodes.

Examples:

With delegation

agent(:backend).delegates_to(:tester, :database)

Without delegation

agent(:planner)

Preserve context across nodes

agent(:architect, reset_context: false)

Override tools for this node

agent(:backend).tools(:Read, :Think)

Combine delegation and tools

agent(:backend).delegates_to(:tester).tools(:Read, :Edit, :Write)

Parameters:

  • Agent name

  • (defaults to: true)

    Whether to reset agent context (default: true)

Returns:

  • Fluent configuration object



70
71
72
73
74
75
76
77
78
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 70

def agent(name, reset_context: true)
  config = AgentConfig.new(name, self, reset_context: reset_context)

  # Register immediately with empty delegation and no tool override
  # If delegates_to/tools are called later, they will update this
  register_agent(name, [], reset_context, nil)

  config
end

#agent_less?Boolean

Check if this is an agent-less (computation-only) node

Agent-less nodes run pure Ruby code without LLM execution. They must have at least one transformer (input or output).

Returns:



453
454
455
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 453

def agent_less?
  @agent_configs.empty?
end

#depends_on(*node_names) ⇒ void

This method returns an undefined value.

Declare dependencies (nodes that must execute before this one)

Examples:

Single dependency

depends_on :planning

Multiple dependencies

depends_on :frontend, :backend

Parameters:

  • Names of prerequisite nodes



117
118
119
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 117

def depends_on(*node_names)
  @dependencies.concat(node_names.map(&:to_sym))
end

#has_input_transformer?Boolean

Check if node has any input transformer (block or command)

Returns:



300
301
302
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 300

def has_input_transformer?
  @input_transformer || @input_transformer_command
end

#has_output_transformer?Boolean

Check if node has any output transformer (block or command)

Returns:



307
308
309
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 307

def has_output_transformer?
  @output_transformer || @output_transformer_command
end

#input {|NodeContext| ... } ⇒ String, Hash

Note:

The input block is automatically converted to a lambda, which means return statements work safely and only exit the transformer, not the entire program. This allows natural control flow patterns.

Define input transformer for this node

The transformer receives a NodeContext object with access to:

  • Previous node's result (convenience: ctx.content)
  • Original user prompt (ctx.original_prompt)
  • All previous node results (ctx.all_results)
  • Current node metadata (ctx.node_name, ctx.dependencies)

Can also be used for side effects (logging, file I/O) since the block runs at execution time, not declaration time.

Control Flow: Return a hash with special keys to control execution:

  • skip_execution: true - Skip node's LLM execution, return content immediately
  • halt_workflow: true - Halt entire workflow with content as final result
  • goto_node: :node_name - Jump to different node with content as input

Examples:

Access previous result and original prompt

input do |ctx|
  # Convenience accessor
  previous_content = ctx.content

  # Access original prompt
  "Original: #{ctx.original_prompt}\nPrevious: #{previous_content}"
end

Access results from specific nodes

input do |ctx|
  plan = ctx.all_results[:planning].content
  design = ctx.all_results[:design].content

  "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
end

Skip execution (caching) - using return

input do |ctx|
  cached = check_cache(ctx.content)
  return ctx.skip_execution(content: cached) if cached
  ctx.content
end

Halt workflow (validation) - using return

input do |ctx|
  if ctx.content.length > 10000
    # Halt entire workflow - return works safely!
    return ctx.halt_workflow(content: "ERROR: Input too long")
  end
  ctx.content
end

Jump to different node (conditional routing) - using return

input do |ctx|
  if ctx.content.include?("NEEDS_REVIEW")
    # Jump to review node instead - return works safely!
    return ctx.goto_node(:review, content: ctx.content)
  end
  ctx.content
end

Yields:

  • (NodeContext)

    Context with previous results and metadata

Returns:

  • Transformed input OR control hash



198
199
200
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 198

def input(&block)
  @input_transformer = ProcHelpers.to_lambda(block)
end

#input_command(command, timeout: nil) ⇒ void

This method returns an undefined value.

Set input transformer as bash command (YAML API)

The command receives NodeContext as JSON on STDIN and outputs transformed content.

Exit codes:

  • 0: Success, use STDOUT as transformed content
  • 1: Skip node execution, use current_input unchanged (STDOUT ignored)
  • 2: Halt workflow with error, show STDERR (STDOUT ignored)

Examples:

input_command("scripts/validate.sh", timeout: 30)

Parameters:

  • Bash command to execute

  • (defaults to: nil)

    Timeout in seconds (default: 60)



217
218
219
220
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 217

def input_command(command, timeout: nil)
  timeout ||= SwarmSDK.config.transformer_command_timeout
  @input_transformer_command = { command: command, timeout: timeout }
end

#lead(agent_name) ⇒ void

This method returns an undefined value.

Override the lead agent (first agent is lead by default)

Examples:

agent(:backend).delegates_to(:tester)
agent(:tester)
lead :tester  # tester is lead instead of backend

Parameters:

  • Name of agent to make lead



130
131
132
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 130

def lead(agent_name)
  @lead_override = agent_name.to_sym
end

#lead_agentSymbol

Get the lead agent for this node

Returns:

  • Lead agent name



443
444
445
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 443

def lead_agent
  @lead_override || @agent_configs.first&.dig(:agent)
end

#output {|NodeContext| ... } ⇒ String, Hash

Note:

The output block is automatically converted to a lambda, which means return statements work safely and only exit the transformer, not the entire program. This allows natural control flow patterns.

Define output transformer for this node

The transformer receives a NodeContext object with access to:

  • Current node's result (convenience: ctx.content)
  • Original user prompt (ctx.original_prompt)
  • All completed node results (ctx.all_results)
  • Current node metadata (ctx.node_name)

Can also be used for side effects (logging, file I/O) since the block runs at execution time, not declaration time.

Control Flow: Return a hash with special keys to control execution:

  • halt_workflow: true - Halt entire workflow with content as final result
  • goto_node: :node_name - Jump to different node with content as input

Examples:

Transform and save to file

output do |ctx|
  # Side effect: save to file
  File.write("results/plan.txt", ctx.content)

  # Return transformed output for next node
  "Key decisions: #{extract_decisions(ctx.content)}"
end

Access original prompt

output do |ctx|
  # Include original context in output
  "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
end

Halt workflow (convergence check) - using return

output do |ctx|
  return ctx.halt_workflow(content: ctx.content) if converged?(ctx.content)
  ctx.content
end

Jump to different node (conditional routing) - using return

output do |ctx|
  if needs_revision?(ctx.content)
    # Go back to revision node - return works safely!
    return ctx.goto_node(:revision, content: ctx.content)
  end
  ctx.content
end

Yields:

  • (NodeContext)

    Context with current result and metadata

Returns:

  • Transformed output OR control hash



273
274
275
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 273

def output(&block)
  @output_transformer = ProcHelpers.to_lambda(block)
end

#output_command(command, timeout: nil) ⇒ void

This method returns an undefined value.

Set output transformer as bash command (YAML API)

The command receives NodeContext as JSON on STDIN and outputs transformed content.

Exit codes:

  • 0: Success, use STDOUT as transformed content
  • 1: Pass through unchanged, use result.content (STDOUT ignored)
  • 2: Halt workflow with error, show STDERR (STDOUT ignored)

Examples:

output_command("scripts/format.sh", timeout: 30)

Parameters:

  • Bash command to execute

  • (defaults to: nil)

    Timeout in seconds (default: 60)



292
293
294
295
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 292

def output_command(command, timeout: nil)
  timeout ||= SwarmSDK.config.transformer_command_timeout
  @output_transformer_command = { command: command, timeout: timeout }
end

#register_agent(agent_name, delegates_to, reset_context = true, tools = nil) ⇒ void

This method returns an undefined value.

Register an agent configuration (called by AgentConfig)

Parameters:

  • Agent name

  • Delegation targets

  • (defaults to: true)

    Whether to reset agent context

  • (defaults to: nil)

    Tool override for this node (nil = use global)



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 87

def register_agent(agent_name, delegates_to, reset_context = true, tools = nil)
  # Check if agent already registered
  existing = @agent_configs.find { |ac| ac[:agent] == agent_name }

  if existing
    # Update delegation, reset_context, and tools (happens when methods are called after agent())
    existing[:delegates_to] = delegates_to
    existing[:reset_context] = reset_context
    existing[:tools] = tools unless tools.nil?
  else
    # Add new agent configuration
    @agent_configs << {
      agent: agent_name,
      delegates_to: delegates_to,
      reset_context: reset_context,
      tools: tools,
    }
  end
end

#transform_input(context, current_input:) ⇒ String, Hash

Transform input using configured transformer (block or command)

Executes either Ruby block or bash command transformer.

Ruby block return values:

  • String: Transformed content
  • Hash with skip_execution: true: Skip node execution
  • Hash with halt_workflow: true: Halt entire workflow
  • Hash with goto_node: :name: Jump to different node

Exit code behavior (bash commands only):

  • Exit 0: Use STDOUT as transformed content
  • Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
  • Exit 2: Halt workflow with error (STDOUT ignored)

Parameters:

  • Context with previous results and metadata

  • Fallback content for exit 1 (skip), also used for halt error context

Returns:

  • Transformed input OR control hash (skip_execution, halt_workflow, goto_node)

Raises:

  • If bash transformer halts workflow (exit 2)



330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 330

def transform_input(context, current_input:)
  # No transformer configured: return content as-is
  return context.content unless @input_transformer || @input_transformer_command

  # Ruby block transformer
  # Ruby blocks can return String (transformed content) OR Hash (control flow)
  if @input_transformer
    result = @input_transformer.call(context)

    # If hash, validate control flow keys
    if result.is_a?(Hash)
      validate_transformer_hash(result, :input)
    end

    return result
  end

  # Bash command transformer
  # Bash commands use exit codes to control behavior:
  # - Exit 0: Success, use STDOUT as transformed content
  # - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
  # - Exit 2: Halt workflow with error (STDOUT ignored)
  if @input_transformer_command
    result = TransformerExecutor.execute(
      command: @input_transformer_command[:command],
      context: context,
      event: "input",
      node_name: @name,
      fallback_content: current_input, # Used for exit 1 (skip)
      timeout: @input_transformer_command[:timeout],
    )

    # Handle transformer result based on exit code
    if result.halt?
      # Exit 2: Halt workflow with error
      raise ConfigurationError,
        "Input transformer halted workflow for node '#{@name}': #{result.error_message}"
    elsif result.skip_execution?
      # Exit 1: Skip node execution, return skip hash
      # Content is current_input unchanged (STDOUT was ignored)
      { skip_execution: true, content: result.content }
    else
      # Exit 0: Return transformed content from STDOUT
      result.content
    end
  end
end

#transform_output(context) ⇒ String, Hash

Transform output using configured transformer (block or command)

Executes either Ruby block or bash command transformer.

Ruby block return values:

  • String: Transformed content
  • Hash with halt_workflow: true: Halt entire workflow
  • Hash with goto_node: :name: Jump to different node

Exit code behavior (bash commands only):

  • Exit 0: Use STDOUT as transformed content
  • Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
  • Exit 2: Halt workflow with error (STDOUT ignored)

Parameters:

  • Context with current result and metadata

Returns:

  • Transformed output OR control hash (halt_workflow, goto_node)

Raises:

  • If bash transformer halts workflow (exit 2)



395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 395

def transform_output(context)
  # No transformer configured: return content as-is
  return context.content unless @output_transformer || @output_transformer_command

  # Ruby block transformer
  # Ruby blocks can return String (transformed content) OR Hash (control flow)
  if @output_transformer
    result = @output_transformer.call(context)

    # If hash, validate control flow keys
    if result.is_a?(Hash)
      validate_transformer_hash(result, :output)
    end

    return result
  end

  # Bash command transformer
  # Bash commands use exit codes to control behavior:
  # - Exit 0: Success, use STDOUT as transformed content
  # - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
  # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
  if @output_transformer_command
    result = TransformerExecutor.execute(
      command: @output_transformer_command[:command],
      context: context,
      event: "output",
      node_name: @name,
      fallback_content: context.content, # result.content for exit 1
      timeout: @output_transformer_command[:timeout],
    )

    # Handle transformer result based on exit code
    if result.halt?
      # Exit 2: Halt workflow with error
      raise ConfigurationError,
        "Output transformer halted workflow for node '#{@name}': #{result.error_message}"
    else
      # Exit 0: Return transformed content from STDOUT
      # Exit 1: Return fallback (result.content unchanged)
      result.content
    end
  end
end

#validate!void

This method returns an undefined value.

Validate node configuration

Also auto-adds agents that are referenced in delegates_to but not explicitly declared. This allows writing: agent(:backend).delegates_to(:verifier) without needing: agent(:verifier)

Raises:

  • If configuration is invalid



465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
# File 'lib/swarm_sdk/workflow/node_builder.rb', line 465

def validate!
  # Auto-add agents mentioned in delegates_to but not explicitly declared
  auto_add_delegate_agents

  # Agent-less nodes (pure computation) are allowed but need transformers
  if @agent_configs.empty?
    unless has_input_transformer? || has_output_transformer?
      raise ConfigurationError,
        "Agent-less node '#{@name}' must have at least one transformer (input or output). " \
          "Either add agents with agent(:name) or add input/output transformers."
    end
  end

  # If has agents, validate lead override
  if @lead_override && !@agent_configs.any? { |ac| ac[:agent] == @lead_override }
    raise ConfigurationError,
      "Node '#{@name}' lead agent '#{@lead_override}' not found in node's agents"
  end
end