agentic examples

The Borrow Checker

The Borrow Checker: I brought Rust's ownership model to a language that never asked for it, and I regret nothing. Task outputs get ownership semantics: a value can be MOVED to exactly one consumer (who may then mutate it - ownership is mutation rights), or BORROWED by any number of readers (who receive it deep-frozen, because a shared reference you can mutate is just a bug with extra steps). Double moves are rejected at ASSEMBLY time with a proper error[E0382], because the …

Testing & Verification Round 19 Steve Klabnik exit 0

source on github

bundle exec ruby examples/borrow_checker.rb

a real captured run

THE BORROW CHECKER (fighting for memory safety in a language with no memory)

  scene 1 - one move (enrich) + one borrow (audit): borrow check PASSES
    the owner mutated freely: records grew to 3 (ownership is mutation rights)
    auditor's mutation attempt stopped by FrozenError (the borrow held)

  scene 2 - both consumers demand a move. the compiler(ish) speaks:
    error[E0382]: use of moved value: `fetch.output`
      --> plan assembly
       note: value moved to `enrich` here
       note: value used again by `audit` after move
       help: consider borrowing instead: mode: :borrow
    nothing executed. the crime was PREVENTED, not avenged - that's the
    entire difference between a checker and a postmortem.

  what ports and what doesn't: the MODEL ports beautifully - move
  semantics are just 'exactly one consumer may treat this as its
  own', borrows are 'everyone else gets it deep-frozen', and the
  aliasing-XOR-mutation rule is enforceable with a Struct, a Hash,
  and Marshal. what doesn't port is the TIMING: Rust rejects at
  compile time; here 'assembly time' plays the part, which is
  still before anything RUNS, which is the part that matters.
  the deep freeze on borrows is the honest cost - in Ruby, the
  only reference nobody can mutate is a frozen copy. fearless
  concurrency starts with knowing whose data it is; turns out you
  can know that in any language, if you're willing to write it
  down at the seam.

source

# frozen_string_literal: true

# The Borrow Checker: I brought Rust's ownership model to a language
# that never asked for it, and I regret nothing. Task outputs get
# ownership semantics: a value can be MOVED to exactly one consumer
# (who may then mutate it - ownership is mutation rights), or
# BORROWED by any number of readers (who receive it deep-frozen,
# because a shared reference you can mutate is just a bug with
# extra steps). Double moves are rejected at ASSEMBLY time with a
# proper error[E0382], because the whole point of a borrow checker
# is that the crime is prevented, not avenged.
#
#   bundle exec ruby examples/borrow_checker.rb
#
# Runs offline; exits 1 unless ownership is actually enforced.

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

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

class BorrowChecker
  Claim = Struct.new(class="y">:consumer, class="y">:mode)

  def initialize
    @claims = Hash.new { |h, k| h[k] = [] }
  end

  def deep_freeze(obj)
    case obj
    when Hash then obj.each { |k, v|
                     deep_freeze(k)
                     deep_freeze(v)
                   }.freeze
    when Array then obj.each { |v| deep_freeze(v) }.freeze
    else obj.freeze
    end
  end

  # Declare intent at assembly time - this is the "compile" phase
  def claim(producer, consumer, mode)
    @claims[producer] << Claim.new(consumer, mode)
  end

  # Rust rule, one graph up: any number of borrows, at most one move
  def check!
    errors = []
    @claims.each do |producer, claims|
      moves = claims.select { |c| c.mode == class="y">:move }
      next unless moves.size > 1
      errors << <<~ERR
        error[E0382]: use of moved value: `#{producer}.output`
          --> plan assembly
           note: value moved to `#{moves[0].consumer}` here
           note: value used again by `#{moves[1].consumer}` after move
           help: consider borrowing instead: mode: class="y">:borrow
      ERR
    end
    errors
  end

  def deliver(value, mode)
    (mode == class="y">:borrow) ? deep_freeze(Marshal.load(Marshal.dump(value))) : value
  end
end

def build_pipeline(second_consumer_mode:)
  checker = BorrowChecker.new
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 3)
  incidents = []

  fetch = Agentic:class="y">:Task.new(description: class="s">"fetch", agent_spec: {class="s">"name" => class="s">"fetch", class="s">"instructions" => class="s">"w"})
  orchestrator.add_task(fetch, agent: ->(_t) { {records: [class="s">"alpha", class="s">"beta"], count: 2} })

  enrich = Agentic:class="y">:Task.new(description: class="s">"enrich", agent_spec: {class="s">"name" => class="s">"enrich", class="s">"instructions" => class="s">"w"})
  checker.claim(class="s">"fetch", class="s">"enrich", class="y">:move)
  orchestrator.add_task(enrich, [fetch], agent: ->(t) {
    owned = checker.deliver(t.previous_output, class="y">:move)
    owned[class="y">:records] << class="s">"gamma" # the owner may mutate; that's what owning MEANS
    owned.merge(enriched: true)
  })

  audit = Agentic:class="y">:Task.new(description: class="s">"audit", agent_spec: {class="s">"name" => class="s">"audit", class="s">"instructions" => class="s">"w"})
  checker.claim(class="s">"fetch", class="s">"audit", second_consumer_mode)
  orchestrator.add_task(audit, [fetch], agent: ->(t) {
    borrowed = checker.deliver(t.previous_output, second_consumer_mode)
    begin
      borrowed[class="y">:records] << class="s">"CORRUPTION" # the auditor turns to crime
      incidents << class="s">"mutation of a borrow SUCCEEDED (checker asleep)"
    rescue FrozenError
      incidents << class="s">"auditor's mutation attempt stopped by FrozenError (the borrow held)"
    end
    {records_seen: borrowed[class="y">:records].size}
  })

  [checker, orchestrator, incidents, {fetch: fetch, enrich: enrich, audit: audit}]
end

puts class="s">"THE BORROW CHECKER (fighting for memory safety in a language with no memory)"
puts

# --- scene 1: a well-typed plan - one move, one borrow, one attempted crime ---------
checker, orchestrator, incidents, tasks = build_pipeline(second_consumer_mode: class="y">:borrow)
compile_errors = checker.check!
result = orchestrator.execute_plan
enriched = result.task_result(tasks[class="y">:enrich].id).output
puts class="s">"  scene 1 - one move (enrich) + one borrow (audit): #{compile_errors.empty? ? "borrow check PASSESclass="s">" : "rejected?!class="s">"}"
puts class="s">"    the owner mutated freely: records grew to #{enriched[class="y">:records].size} (ownership is mutation rights)"
puts class="s">"    #{incidents.first}"
puts

# --- scene 2: two moves of the same value - rejected before anything runs -----------
checker2, _orchestrator2, _incidents2, _tasks2 = build_pipeline(second_consumer_mode: class="y">:move)
errors = checker2.check!
puts class="s">"  scene 2 - both consumers demand a move. the compiler(ish) speaks:"
errors.each { |e| e.lines.each { |l| puts class="s">"    #{l.rstrip}" } }
puts class="s">"    nothing executed. the crime was PREVENTED, not avenged - that's the"
puts class="s">"    entire difference between a checker and a postmortem."
puts

failures = []
failures << class="s">"clean plan failed" unless result.status == class="y">:completed && enriched[class="y">:records].size == 3
failures << class="s">"borrow was mutable" unless incidents.first&.include?(class="s">"FrozenError")
failures << class="s">"double move not rejected" unless errors.size == 1 && errors.first.include?(class="s">"E0382")

puts class="s">"  what ports and what doesn't: the MODEL ports beautifully - move"
puts class="s">"  semantics are just 'exactly one consumer may treat this as its"
puts class="s">"  own', borrows are 'everyone else gets it deep-frozen', and the"
puts class="s">"  aliasing-XOR-mutation rule is enforceable with a Struct, a Hash,"
puts class="s">"  and Marshal. what doesn't port is the TIMING: Rust rejects at"
puts class="s">"  compile time; here 'assembly time' plays the part, which is"
puts class="s">"  still before anything RUNS, which is the part that matters."
puts class="s">"  the deep freeze on borrows is the honest cost - in Ruby, the"
puts class="s">"  only reference nobody can mutate is a frozen copy. fearless"
puts class="s">"  concurrency starts with knowing whose data it is; turns out you"
puts class="s">"  can know that in any language, if you're willing to write it"
puts class="s">"  down at the seam."
exit(failures.empty? ? 0 : 1)