agentic examples

The Plan Lockfile

The Plan Lockfile: Gemfile.lock for workflows. A plan that says "give me text.summarize ~> 1.0" is a WISH; production runs need a FACT. `lock` resolves constraints once and writes plan.lock - exact versions plus a content digest per capability. `run --frozen` resolves nothing: it verifies the world still matches the lockfile and refuses to run anything it didn't agree to - a new version published? ignored until you relock. an implementation edited in place under the same …

Developer Experience Round 17 André Arko exit 0

source on github

bundle exec ruby examples/plan_lockfile.rb

a real captured run

THE PLAN LOCKFILE (a constraint is a wish; production runs need a fact)

  day 1   lock + frozen run: text.summarize 1.1.0, markdown.render 2.3.1 -> "<p>Lock your plans.</p>"
  day 30  text.summarize 1.2.0 published; frozen run: text.summarize 1.1.0, markdown.render 2.3.1 (ignored it)

  day 31  markdown.render 2.3.1 edited in place (same version, new code):
    FROZEN RUN REFUSED: markdown.render 2.3.1: content digest mismatch (locked b867f561da7a, found cc89e08e96e1)
    the fix is explicit, one command, and leaves a diff: plan lock --update

  relock  text.summarize 1.2.0, markdown.render 2.3.1 -> "<p class='tracked'>LOCK YOUR PLANS. TRUST YOUR DEPLOYS.</p>"
          (1.2.0 adopted NOW, in a diff someone reviews - not silently on day 30)

  three moments, one discipline: the constraint file says what you
  can ACCEPT, the lockfile says what you ARE RUNNING, and nothing
  moves between them without a human making a diff. the digest is
  the underrated half - version numbers are claims, and every
  ecosystem eventually meets code that lies about itself. bundler
  spent a decade earning these rules; plans that call LLMs and
  APIs and each other get to inherit them for the cost of one
  JSON file.

source

# frozen_string_literal: true

# The Plan Lockfile: Gemfile.lock for workflows. A plan that says
# "give me text.summarize ~> 1.0" is a WISH; production runs need a
# FACT. `lock` resolves constraints once and writes plan.lock -
# exact versions plus a content digest per capability. `run --frozen`
# resolves nothing: it verifies the world still matches the lockfile
# and refuses to run anything it didn't agree to - a new version
# published? ignored until you relock. an implementation edited
# in place under the same version number? REFUSED BY DIGEST,
# because "same version, different code" is the lie lockfiles exist
# to catch.
#
#   bundle exec ruby examples/plan_lockfile.rb
#
# Runs offline; exits 1 unless frozen runs are deterministic and
# drift is refused with a usable message.

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

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

# The capability "rubygems.org": versions are immutable... unless
# someone edits one in place, which is exactly what we'll do to prove
# the digests earn their keep
REGISTRY = {
  class="s">"text.summarize" => {
    class="s">"1.0.0" => class="s">"->(t) { t[class="y">:text].split('. ').first }",
    class="s">"1.1.0" => class="s">"->(t) { t[class="y">:text].split('. ').first + '.' }"
  },
  class="s">"markdown.render" => {
    class="s">"2.3.1" => class="s">"->(t) { \"<p>\#{t[class="y">:text]}</p>\class="s">" }"
  }
}

PLAN_REQUIREMENTS = {class="s">"text.summarize" => class="s">"~> 1.0", class="s">"markdown.render" => class="s">"~> 2.3"}.freeze

def resolve(requirements)
  requirements.to_h do |name, constraint|
    versions = REGISTRY.fetch(name).keys.map { |v| Gem:class="y">:Version.new(v) }.sort
    best = versions.reverse.find { |v| Gem:class="y">:Requirement.new(constraint).satisfied_by?(v) }
    raise class="s">"no version of #{name} satisfies #{constraint}" unless best
    [name, best.to_s]
  end
end

def write_lock(path, resolution)
  entries = resolution.to_h { |name, version| [name, {class="s">"version" => version, class="s">"digest" => Digest:class="y">:SHA256.hexdigest(REGISTRY[name][version])[0, 12]}] }
  File.write(path, JSON.pretty_generate({class="s">"capabilities" => entries, class="s">"locked_by" => class="s">"plan_lockfile 1.0"}))
end

# Frozen semantics: verify, never resolve. Every failure names the
# capability, what was expected, what was found, and the way out.
def frozen_check(path)
  lock = JSON.parse(File.read(path))
  lock[class="s">"capabilities"].filter_map do |name, entry|
    source = REGISTRY.dig(name, entry[class="s">"version"])
    if source.nil?
      class="s">"#{name} #{entry["versionclass="s">"]} is locked but no longer available"
    elsif Digest:class="y">:SHA256.hexdigest(source)[0, 12] != entry[class="s">"digest"]
      class="s">"#{name} #{entry["versionclass="s">"]}: content digest mismatch (locked #{entry["digestclass="s">"]}, found #{Digest:class="y">:SHA256.hexdigest(source)[0, 12]})"
    end
  end
end

def run_plan(path)
  lock = JSON.parse(File.read(path))
  impls = lock[class="s">"capabilities"].to_h { |name, e| [name, eval(REGISTRY[name][e[class="s">"version"]])] } # rubocop:disable Security/Eval -- registry sources are this file's own fixtures
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 2)
  summarize = Agentic:class="y">:Task.new(description: class="s">"summarize", agent_spec: {class="s">"name" => class="s">"s", class="s">"instructions" => class="s">"w"})
  render = Agentic:class="y">:Task.new(description: class="s">"render", agent_spec: {class="s">"name" => class="s">"r", class="s">"instructions" => class="s">"w"})
  orchestrator.add_task(summarize, agent: ->(_t) { impls[class="s">"text.summarize"].call({text: class="s">"Lock your plans. Trust your deploys."}) })
  orchestrator.add_task(render, [summarize], agent: ->(t) { impls[class="s">"markdown.render"].call({text: t.previous_output}) })
  result = orchestrator.execute_plan
  [result.task_result(render.id).output, lock[class="s">"capabilities"].map { |n, e| class="s">"#{n} #{e["versionclass="s">"]}" }]
end

failures = []
puts class="s">"THE PLAN LOCKFILE (a constraint is a wish; production runs need a fact)"
puts

Dir.mktmpdir(class="s">"plan_lock") do |dir|
  lockfile = File.join(dir, class="s">"plan.lock")

  # Day 1: developer locks and deploys
  write_lock(lockfile, resolve(PLAN_REQUIREMENTS))
  output, versions = run_plan(lockfile)
  puts class="s">"  day 1   lock + frozen run: #{versions.join(", class="s">")} -> #{output.inspect}"

  # Day 30: a new version is published upstream. The frozen run does
  # not care - determinism means new code enters through a relock, ever
  REGISTRY[class="s">"text.summarize"][class="s">"1.2.0"] = class="s">"->(t) { t[class="y">:text].upcase }"
  drift = frozen_check(lockfile)
  output2, versions2 = run_plan(lockfile)
  puts class="s">"  day 30  text.summarize 1.2.0 published; frozen run: #{versions2.join(", class="s">")} (ignored it)"
  failures << class="s">"frozen run drifted to a new version" unless versions2 == versions && output2 == output && drift.empty?

  # Day 31: someone edits 2.3.1 IN PLACE - same version, different code
  REGISTRY[class="s">"markdown.render"][class="s">"2.3.1"] = class="s">"->(t) { \"<p class='tracked'>\#{t[class="y">:text]}</p>\class="s">" }"
  drift = frozen_check(lockfile)
  puts
  puts class="s">"  day 31  markdown.render 2.3.1 edited in place (same version, new code):"
  drift.each { |d| puts class="s">"    FROZEN RUN REFUSED: #{d}" }
  puts class="s">"    the fix is explicit, one command, and leaves a diff: plan lock --update"
  failures << class="s">"digest drift was not refused" if drift.empty?

  # The relock: deliberate, reviewable, and the plan runs again
  write_lock(lockfile, resolve(PLAN_REQUIREMENTS))
  output3, versions3 = run_plan(lockfile)
  puts
  puts class="s">"  relock  #{versions3.join(", class="s">")} -> #{output3.inspect}"
  puts class="s">"          (1.2.0 adopted NOW, in a diff someone reviews - not silently on day 30)"
  failures << class="s">"relock didn't adopt the new version" unless versions3.first.include?(class="s">"1.2.0")
end

puts
puts class="s">"  three moments, one discipline: the constraint file says what you"
puts class="s">"  can ACCEPT, the lockfile says what you ARE RUNNING, and nothing"
puts class="s">"  moves between them without a human making a diff. the digest is"
puts class="s">"  the underrated half - version numbers are claims, and every"
puts class="s">"  ecosystem eventually meets code that lies about itself. bundler"
puts class="s">"  spent a decade earning these rules; plans that call LLMs and"
puts class="s">"  APIs and each other get to inherit them for the cost of one"
puts class="s">"  JSON file."
exit(failures.empty? ? 0 : 1)