agentic examples

Duck Agents

Duck Agents: the agent: seam asks one question - "can you be called with a task?" - and five differently-shaped objects all answer yes: a lambda, an instance, a Method, a curried proc, and a decorator wrapping any of the others. Nobody was asked what class they are. Ask for what you need, not for who someone is.

Plans & Graphs Round 9 Sandi Metz exit 0

source on github

bundle exec ruby examples/duck_agents.rb

a real captured run

DUCK AGENTS (one seam, five shapes)

  fetch    lambda                     -> {:records=>12, :source=>"fetch"}
  dedupe   instance with #call        -> {:unique=>9, :dropped=>3, :pass=>1}
  stats    Method object              -> {:mean=>4.2, :max=>9}
  render   curried proc               -> {:rendered=>true, :format=>"html"}
  audit    decorator around a lambda  -> {:audited=>true, :timed_ms=>0.0}

  the seam asked one question: respond to call (or execute) with a
  task. it never asked anyone's class, so anything shaped right
  walks in: closures for the quick cases, instances when logic
  deserves a home, Method objects when it already has one, curry
  for pre-applied config, and decorators that stack because they
  honor the same contract they consume. depend on messages and
  every object ever written - and not yet written - is a plugin.

source

# frozen_string_literal: true

# Duck Agents: the agent: seam asks one question - "can you be called
# with a task?" - and five differently-shaped objects all answer yes:
# a lambda, an instance, a Method, a curried proc, and a decorator
# wrapping any of the others. Nobody was asked what class they are.
# Ask for what you need, not for who someone is.
#
#   bundle exec ruby examples/duck_agents.rb
#
# Runs offline; one plan, five ducks, one timing decorator.

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

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

# Duck 1: a lambda - the shape you reach for first
fetch = ->(task) { {records: 12, source: task.description} }

# Duck 2: a plain object with #call - state and a real home for logic
class Deduper
  def initialize
    @seen = 0
  end

  def call(_task)
    @seen += 1
    {unique: 9, dropped: 3, pass: @seen}
  end
end

# Duck 3: a Method object - module functions join the plan unwrapped
module Stats
  def self.summarize(_task)
    {mean: 4.2, max: 9}
  end
end

# Duck 4: a curried proc - configuration applied ahead of time,
# leaving exactly the one-argument shape the seam asks for
render = lambda { |format, _task| {rendered: true, format: format} }
render_html = render.curry[class="s">"html"]

# Duck 5: a decorator - wraps ANY of the above, because it only
# relies on the same one-message contract it provides
class Timed
  def initialize(inner)
    @inner = inner
  end

  def call(task)
    started = Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC)
    @inner.call(task).merge(
      timed_ms: ((Process.clock_gettime(Process:class="y">:CLOCK_MONOTONIC) - started) * 1000).round(2)
    )
  end
end

def task(name)
  Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"work"})
end

orchestrator = Agentic:class="y">:PlanOrchestrator.new
fetch_task = task(class="s">"fetch")
dedupe_task = task(class="s">"dedupe")
stats_task = task(class="s">"stats")
render_task = task(class="s">"render")
audit_task = task(class="s">"audit")

orchestrator.add_task(fetch_task, agent: fetch)
orchestrator.add_task(dedupe_task, [fetch_task], agent: Deduper.new)
orchestrator.add_task(stats_task, [dedupe_task], agent: Stats.method(class="y">:summarize))
orchestrator.add_task(render_task, [stats_task], agent: render_html)
orchestrator.add_task(audit_task, [render_task], agent: Timed.new(->(_t) { {audited: true} }))

result = orchestrator.execute_plan

DUCKS = {
  class="s">"fetch" => class="s">"lambda",
  class="s">"dedupe" => class="s">"instance with #call",
  class="s">"stats" => class="s">"Method object",
  class="s">"render" => class="s">"curried proc",
  class="s">"audit" => class="s">"decorator around a lambda"
}.freeze

puts class="s">"DUCK AGENTS (one seam, five shapes)"
puts
[fetch_task, dedupe_task, stats_task, render_task, audit_task].each do |t|
  output = result.task_result(t.id).output
  puts format(class="s">"  %-8s %-26s -> %s", t.description, DUCKS[t.description], output)
end

puts
puts class="s">"  the seam asked one question: respond to call (or execute) with a"
puts class="s">"  task. it never asked anyone's class, so anything shaped right"
puts class="s">"  walks in: closures for the quick cases, instances when logic"
puts class="s">"  deserves a home, Method objects when it already has one, curry"
puts class="s">"  for pre-applied config, and decorators that stack because they"
puts class="s">"  honor the same contract they consume. depend on messages and"
puts class="s">"  every object ever written - and not yet written - is a plugin."