agentic examples

Three Shapes

Three Shapes: the same six units of work arranged three ways - a chain, a star, and staged joins - then measured and critiqued. Design is choosing a shape ON PURPOSE, and purpose needs numbers.

Plans & Graphs Round 5 Sandi Metz exit 0

source on github

bundle exec ruby examples/three_shapes.rb

a real captured run

THREE SHAPES: six tasks x 40ms, concurrency 4

  shape    wall       depth    max fan-in the trade
  chain     246ms     6        1         trivially debuggable; pays full serial price
  star      121ms     3        4         fastest; one join owns every failure mode
  staged    125ms     3        3         nearly as fast; each join has one reason to wait

none of these is wrong. the chain is right when the work is truly
sequential; the star when the join is trivial; the staged shape when
the join has judgment in it. what's wrong is not knowing which one
you built - and now the graph will tell you, in two numbers.

source

# frozen_string_literal: true

# Three Shapes: the same six units of work arranged three ways - a
# chain, a star, and staged joins - then measured and critiqued. Design
# is choosing a shape ON PURPOSE, and purpose needs numbers.
#
#   bundle exec ruby examples/three_shapes.rb
#
# Runs offline; each unit of work is 40ms of simulated IO.

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

UNIT = 0.04
NAMES = %w[gather_a gather_b gather_c gather_d combine finish].freeze

def build(shape)
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 4)
  tasks = NAMES.to_h do |name|
    [name, Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"work"})]
  end

  deps = case shape
  when class="y">:chain # one long line: simple to read, nothing overlaps
    NAMES.each_cons(2).to_h { |a, b| [b, [a]] }
  when class="y">:star # everything at once, one god join
    {class="s">"combine" => %w[gather_a gather_b gather_c gather_d], class="s">"finish" => [class="s">"combine"]}
  when class="y">:staged # balanced: pairs join, then join the joins
    {class="s">"gather_c" => [], class="s">"gather_d" => [],
     class="s">"combine" => %w[gather_a gather_b],
     class="s">"finish" => %w[combine gather_c gather_d]}
  end

  NAMES.each do |name|
    orchestrator.add_task(tasks[name], (deps[name] || []).map { |d| tasks.fetch(d) },
      agent: ->(_t) { sleep(UNIT) || class="y">:ok })
  end
  orchestrator
end

# Structural facts, read straight off the graph
def shape_facts(graph)
  dependencies = graph[class="y">:dependencies]
  depth = {}
  measure = ->(id) { depth[id] ||= 1 + (dependencies[id].map { |d| measure.call(d) }.max || 0) }
  {
    max_fan_in: dependencies.values.map(&class="y">:size).max,
    depth: dependencies.keys.map { |id| measure.call(id) }.max
  }
end

puts class="s">"THREE SHAPES: six tasks x #{(UNIT * 1000).round}ms, concurrency 4"
puts
puts format(class="s">"  %-8s %-10s %-8s %-9s %s", class="s">"shape", class="s">"wall", class="s">"depth", class="s">"max fan-in", class="s">"the trade")

TRADES = {
  chain: class="s">"trivially debuggable; pays full serial price",
  star: class="s">"fastest; one join owns every failure mode",
  staged: class="s">"nearly as fast; each join has one reason to wait"
}.freeze

%i[chain star staged].each do |shape|
  orchestrator = build(shape)
  facts = shape_facts(orchestrator.graph)
  result = orchestrator.execute_plan
  puts format(class="s">"  %-8s %4dms     %-8d %-9d %s",
    shape, result.execution_time * 1000, facts[class="y">:depth], facts[class="y">:max_fan_in], TRADES[shape])
end

puts
puts class="s">"none of these is wrong. the chain is right when the work is truly"
puts class="s">"sequential; the star when the join is trivial; the staged shape when"
puts class="s">"the join has judgment in it. what's wrong is not knowing which one"
puts class="s">"you built - and now the graph will tell you, in two numbers."