agentic examples

The Graph Style Guide

The Graph Style Guide: RuboCop for plans. Cops with thresholds run against any orchestrator's graph - depth, fan-in, orphans, and the style rule I care most about: fan-ins of two or more should NAME their dependencies, because a join you can't name is a join you don't understand.

Developer Experience Round 7 Sandi Metz exit 0

source on github

bundle exec ruby examples/graph_style.rb

a real captured run

GRAPH STYLE GUIDE (4 cops)

  tidy plan: no offenses

  messy plan: 4 offense(s)
    Graph/MaxDepth: present sits 6 deep (limit 4)
      (every level is latency and a failure domain)
    Graph/MaxFanIn: funnel joins 4 (limit 3)
      (wide joins own too many failure modes)
    Graph/NoOrphans: stray touches nothing and is touched by nothing
      (unconnected tasks are in the wrong plan or missing an edge)
    Graph/NamedFanIns: funnel joins 4 but 4 edge(s) are unnamed - use needs:
      (a join you can't name is a join you don't understand)

style guides work because they argue once, in a config file,
instead of every review. these thresholds are this team's taste -
yours may differ. that they're WRITTEN DOWN is the feature.

source

# frozen_string_literal: true

# The Graph Style Guide: RuboCop for plans. Cops with thresholds run
# against any orchestrator's graph - depth, fan-in, orphans, and the
# style rule I care most about: fan-ins of two or more should NAME
# their dependencies, because a join you can't name is a join you
# don't understand.
#
#   bundle exec ruby examples/graph_style.rb
#
# Runs offline; lints a tidy plan and a messy one.

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

STYLE = {
  class="s">"Graph/MaxDepth" => {limit: 4, why: class="s">"every level is latency and a failure domain"},
  class="s">"Graph/MaxFanIn" => {limit: 3, why: class="s">"wide joins own too many failure modes"},
  class="s">"Graph/NoOrphans" => {why: class="s">"unconnected tasks are in the wrong plan or missing an edge"},
  class="s">"Graph/NamedFanIns" => {min_to_name: 2, why: class="s">"a join you can't name is a join you don't understand"}
}.freeze

def lint(graph, style)
  names = graph[class="y">:tasks].transform_values(&class="y">:description)
  stats = graph[class="y">:stats]
  offenses = []

  if stats[class="y">:max_depth] > style[class="s">"Graph/MaxDepth"][class="y">:limit]
    deepest = stats[class="y">:depth].max_by { |_, d| d }.first
    offenses << [class="s">"Graph/MaxDepth", class="s">"#{names[deepest]} sits #{stats[class="y">:max_depth]} deep (limit #{style["Graph/MaxDepthclass="s">"][class="y">:limit]})"]
  end

  graph[class="y">:dependencies].each do |id, deps|
    if deps.size > style[class="s">"Graph/MaxFanIn"][class="y">:limit]
      offenses << [class="s">"Graph/MaxFanIn", class="s">"#{names[id]} joins #{deps.size} (limit #{style["Graph/MaxFanInclass="s">"][class="y">:limit]})"]
    end
  end

  graph[class="y">:dependencies].each do |id, deps|
    if deps.empty? && graph[class="y">:edges].none? { |e| e[class="y">:from] == id } && graph[class="y">:tasks].size > 1
      offenses << [class="s">"Graph/NoOrphans", class="s">"#{names[id]} touches nothing and is touched by nothing"]
    end
  end

  graph[class="y">:dependencies].each do |id, deps|
    next if deps.size < style[class="s">"Graph/NamedFanIns"][class="y">:min_to_name]

    unnamed = graph[class="y">:edges].count { |e| e[class="y">:to] == id && e[class="y">:label].nil? }
    if unnamed.positive?
      offenses << [class="s">"Graph/NamedFanIns", class="s">"#{names[id]} joins #{deps.size} but #{unnamed} edge(s) are unnamed - use needs:"]
    end
  end

  offenses
end

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

# --- a tidy plan ------------------------------------------------------------
tidy = Agentic:class="y">:PlanOrchestrator.new
a = step(class="s">"fetch users")
b = step(class="s">"fetch orders")
c = step(class="s">"merge report")
tidy.add_task(a)
tidy.add_task(b)
tidy.add_task(c, needs: {users: a, orders: b})

# --- a messy one --------------------------------------------------------------
messy = Agentic:class="y">:PlanOrchestrator.new
sources = 4.times.map { |i| step(class="s">"source-#{i}") }
funnel = step(class="s">"funnel")
steps = %w[polish buff shine present].map { |n| step(n) }
stray = step(class="s">"stray")
sources.each { |t| messy.add_task(t) }
messy.add_task(funnel, sources)
previous = funnel
steps.each { |t| messy.add_task(t, [previous]) && (previous = t) }
messy.add_task(stray)

puts class="s">"GRAPH STYLE GUIDE (#{STYLE.size} cops)"
{class="s">"tidy plan" => tidy, class="s">"messy plan" => messy}.each do |label, orchestrator|
  offenses = lint(orchestrator.graph, STYLE)
  puts
  puts class="s">"  #{label}: #{offenses.empty? ? "no offensesclass="s">" : "#{offenses.size} offense(s)class="s">"}"
  offenses.each do |cop, message|
    puts class="s">"    #{cop}: #{message}"
    puts class="s">"      (#{STYLE[cop][class="y">:why]})"
  end
end

puts
puts class="s">"style guides work because they argue once, in a config file,"
puts class="s">"instead of every review. these thresholds are this team's taste -"
puts class="s">"yours may differ. that they're WRITTEN DOWN is the feature."