agentic examples

The Railway Plan

The Railway Plan: dry-monads taught Ruby that failure handling is COMPOSITION, not rescue blocks - a pipeline of steps where success rides the happy track and the first failure switches every later step onto the bypass, carrying WHY. TaskResult/TaskFailure are already Result values in street clothes; this gives them bind, so a plan's outcome composes like a railway instead of nesting like an if-tree.

Data & Pipelines Round 16 Piotr Solnica exit 0

source on github

bundle exec ruby examples/railway_plan.rb

a real captured run

THE RAILWAY PLAN (bind, not rescue)

  a full cart:
    -> invoice #41: 1550 cents

  an empty cart:
    -> diverted at: LlmInvalidRequestError - cart is empty
    (price and invoice never ran - no nil checks asked, none needed)

  what the railway buys, precisely: the checkout reads as three
  binds top to bottom, and that reading IS the control flow - no
  rescue pyramid, no `return unless`, no nil creeping past step
  two. the diverted train still carries a first-class TaskFailure
  (type, message, timestamp, retryable verdict), because the
  framework already models failure as data; the monad is just
  fourteen lines acknowledging it. Success/Failure with bind is
  all you need for the pattern - do-notation is nicer, but the
  discipline (failures COMPOSE, they don't interrupt) is the part
  that survives translation into any codebase.

source

# frozen_string_literal: true

# The Railway Plan: dry-monads taught Ruby that failure handling is
# COMPOSITION, not rescue blocks - a pipeline of steps where success
# rides the happy track and the first failure switches every later
# step onto the bypass, carrying WHY. TaskResult/TaskFailure are
# already Result values in street clothes; this gives them bind, so
# a plan's outcome composes like a railway instead of nesting like
# an if-tree.
#
#   bundle exec ruby examples/railway_plan.rb
#
# Runs offline; one train arrives, one is politely diverted.

require class="s">"bundler/setup"
require class="s">"agentic"

Agentic.logger.level = class="y">:fatal

# The whole monad: Success carries a value through bind; Failure
# short-circuits, carrying the failure to the end of the line
module Railway
  Success = Struct.new(class="y">:value) do
    def bind = yield(value)

    def success? = true
  end

  Failure = Struct.new(class="y">:failure) do
    def bind = self # the bypass track: later steps never run

    def success? = false
  end

  # Lift a plan's task outcome onto the railway
  def self.from(result, task)
    task_result = result.task_result(task.id)
    task_result.successful? ? Success.new(task_result.output) : Failure.new(task_result.failure)
  end
end

def run_task(description, payload, &work)
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(retry_policy: {max_retries: 0, retryable_errors: []})
  task = Agentic:class="y">:Task.new(description: description, agent_spec: {class="s">"name" => description, class="s">"instructions" => class="s">"w"}, payload: payload)
  orchestrator.add_task(task, agent: ->(t) { work.call(t.payload) })
  Railway.from(orchestrator.execute_plan, task)
end

# Three steps of a checkout, each a real journaled-able plan task,
# composed with bind - read it top to bottom, that IS the control flow
def checkout(order)
  run_task(class="s">"validate", order) { |o|
    raise Agentic:class="y">:Errors:class="y">:LlmInvalidRequestError, class="s">"cart is empty" if o[class="y">:items].empty?

    o
  }.bind { |o|
    run_task(class="s">"price", o) { |ord| ord.merge(total_cents: ord[class="y">:items].sum { |i| i[class="y">:cents] }) }
  }.bind { |o|
    run_task(class="s">"invoice", o) { |ord| class="s">"invoice ##{ord[class="y">:id]}: #{ord[class="y">:total_cents]} cents" }
  }
end

puts class="s">"THE RAILWAY PLAN (bind, not rescue)"
puts

happy = checkout({id: 41, items: [{cents: 1200}, {cents: 350}]})
puts class="s">"  a full cart:"
puts class="s">"    -> #{happy.success? ? happy.value : happy.failure.message}"
puts

sad = checkout({id: 42, items: []})
puts class="s">"  an empty cart:"
puts class="s">"    -> diverted at: #{sad.failure.type.split("::class="s">").last} - #{sad.failure.message}"
puts class="s">"    (price and invoice never ran - no nil checks asked, none needed)"
puts

puts class="s">"  what the railway buys, precisely: the checkout reads as three"
puts class="s">"  binds top to bottom, and that reading IS the control flow - no"
puts class="s">"  rescue pyramid, no `return unless`, no nil creeping past step"
puts class="s">"  two. the diverted train still carries a first-class TaskFailure"
puts class="s">"  (type, message, timestamp, retryable verdict), because the"
puts class="s">"  framework already models failure as data; the monad is just"
puts class="s">"  fourteen lines acknowledging it. Success/Failure with bind is"
puts class="s">"  all you need for the pattern - do-notation is nicer, but the"
puts class="s">"  discipline (failures COMPOSE, they don't interrupt) is the part"
puts class="s">"  that survives translation into any codebase."