agentic examples

The Durable Batch

The Durable Batch: six billable "LLM calls" run under an ExecutionJournal. Mid-batch, the process dies for real - exit!, no cleanup, the honest kill -9. Then a second process replays the journal and finishes the batch WITHOUT re-paying for completed work.

Scheduling & Concurrency Round 2 Mike Perham exit 0

source on github

bundle exec ruby examples/durable_batch.rb

a real captured run

run 1: processing 6 invoice(s): invoice-1, invoice-2, invoice-3, invoice-4, invoice-5, invoice-6
  !! power cut during invoice-4 - process dying with exit!(97)
  child exited with status 97 (journal survived on disk)

journal replay: 3 invoice(s) already paid: invoice-1, invoice-2, invoice-3
                3 remain (including the one mid-flight when we died)

run 2: processing 3 invoice(s): invoice-4, invoice-5, invoice-6

RECEIPT
  run 1 paid:  4 calls (3 journaled + 1 lost to the crash)
  run 2 paid:  3 calls
  total spend: $1.75 for 6 invoices (naive rerun-everything: $2.50)
  journal: /tmp/agentic_durable_batch.journal.jsonl

source

# frozen_string_literal: true

# The Durable Batch: six billable "LLM calls" run under an
# ExecutionJournal. Mid-batch, the process dies for real - exit!, no
# cleanup, the honest kill -9. Then a second process replays the
# journal and finishes the batch WITHOUT re-paying for completed work.
#
#   bundle exec ruby examples/durable_batch.rb
#
# Runs offline; the "API" is sleep plus an invoice counter. The number
# to watch is the last one: total paid should equal the batch size,
# crash or no crash.

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

# exit! discards buffered IO - the child's narration would die with it
$stdout.sync = true

JOURNAL = File.join(Dir.tmpdir, class="s">"agentic_durable_batch.journal.jsonl")
File.delete(JOURNAL) if File.exist?(JOURNAL)

INVOICES = %w[invoice-1 invoice-2 invoice-3 invoice-4 invoice-5 invoice-6].freeze
COST_PER_CALL = 0.25 # dollars of imaginary tokens

# Bills the imaginary API, then optionally dies like a deploy
BillableAgent = Struct.new(class="y">:crash_on, class="y">:billed) do
  def execute(prompt)
    invoice = prompt[/invoice-\d+/]
    sleep(0.05) # the API call we pay for
    billed << invoice

    if invoice == crash_on
      puts class="s">"  !! power cut during #{invoice} - process dying with exit!(97)"
      Process.exit!(97) # no ensure blocks, no at_exit - a real crash
    end

    {class="s">"invoice" => invoice, class="s">"status" => class="s">"paid"}
  end
end

Desk = Struct.new(class="y">:agent) do
  def get_agent_for_task(_task)
    agent
  end
end

def run_batch(label, skip: [], crash_on: nil)
  journal = Agentic:class="y">:ExecutionJournal.new(path: JOURNAL)
  billed = []
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(
    concurrency_limit: 1, # deterministic order, one call in flight
    lifecycle_hooks: journal.lifecycle_hooks
  )

  todo = INVOICES - skip
  todo.each do |invoice|
    orchestrator.add_task(Agentic:class="y">:Task.new(
      description: invoice,
      agent_spec: {class="s">"name" => class="s">"Biller", class="s">"instructions" => class="s">"Process #{invoice}"},
      input: {}
    ))
  end

  puts class="s">"#{label}: processing #{todo.size} invoice(s): #{todo.join(", class="s">")}"
  orchestrator.execute_plan(Desk.new(BillableAgent.new(crash_on, billed)))
  billed
end

# Maps journal task ids back to invoice names via task_started events
def completed_invoices(state)
  names = state.events.each_with_object({}) do |event, map|
    map[event[class="y">:task_id]] = event[class="y">:description] if event[class="y">:event] == class="s">"task_started"
  end
  state.completed_task_ids.map { |id| names[id] }
end

# --- Run 1: a child process that will not survive invoice-4 ---------------
child = fork do
  run_batch(class="s">"run 1", crash_on: class="s">"invoice-4")
end
_, status = Process.wait2(child)
puts class="s">"  child exited with status #{status.exitstatus} (journal survived on disk)"
puts

# --- The journal knows exactly what was paid for --------------------------
state = Agentic:class="y">:ExecutionJournal.replay(path: JOURNAL)
paid = completed_invoices(state)
puts class="s">"journal replay: #{paid.size} invoice(s) already paid: #{paid.join(", class="s">")}"
puts class="s">"                #{INVOICES.size - paid.size} remain (including the one mid-flight when we died)"
puts

# --- Run 2: same batch, skip what the journal proves is done --------------
billed_in_run2 = run_batch(class="s">"run 2", skip: paid)
puts

total_calls = paid.size + 1 + billed_in_run2.size # +1: paid for, then died during
naive_calls = paid.size + 1 + INVOICES.size # rerunning the whole batch
puts class="s">"RECEIPT"
puts class="s">"  run 1 paid:  #{paid.size + 1} calls (#{paid.size} journaled + 1 lost to the crash)"
puts class="s">"  run 2 paid:  #{billed_in_run2.size} calls"
puts format(class="s">"  total spend: $%.2f for %d invoices (naive rerun-everything: $%.2f)",
  total_calls * COST_PER_CALL, INVOICES.size, naive_calls * COST_PER_CALL)
puts class="s">"  journal: #{JOURNAL}"