agentic examples

Feature Flags for Plans

Feature Flags for Plans: shipping a new pipeline step shouldn't be a deploy decision - it should be a FLAG decision. A tiny Flipper- shaped adapter (boolean, actor, percentage gates) decides per run whether the experimental step joins the plan, and rewire_task splices it in or routes around it. Same code in production for everyone; different plans per actor.

Data & Pipelines Round 13 John Nunemaker exit 0

source on github

bundle exec ruby examples/feature_flags.rb

a real captured run

FEATURE FLAGS FOR PLANS (one codebase, per-actor shapes)

  phase 1 - flag off for everyone:
    acme     fetch -> summarize -> publish                  published: summary
    globex   fetch -> summarize -> publish                  published: summary
    umbrella fetch -> summarize -> publish                  published: summary

  phase 2 - enabled for actor acme (the design partner):
    acme     fetch -> summarize -> fact_check -> publish    published: checked summary
    globex   fetch -> summarize -> publish                  published: summary
    umbrella fetch -> summarize -> publish                  published: summary

  phase 3 - 50% rollout (deterministic per-tenant bucketing):
    acme     fetch -> summarize -> fact_check -> publish    published: checked summary
    globex   fetch -> summarize -> fact_check -> publish    published: checked summary
    umbrella fetch -> summarize -> publish                  published: summary

  the shape of the trick: the experimental step isn't hidden
  behind an if INSIDE a task - it's a different PLAN, built per
  run, spliced in with rewire_task at exactly one seam. acme has
  been running fact-checked for a phase before anyone else, the
  50% bucket is deterministic (same tenant, same verdict, every
  run - flapping flags are worse than no flags), and rollback is
  disable, not deploy. flags decouple SHIPPING code from RUNNING
  it; plans-as-data means they can decouple shipping a STEP from
  running it, too.

source

# frozen_string_literal: true

# Feature Flags for Plans: shipping a new pipeline step shouldn't be
# a deploy decision - it should be a FLAG decision. A tiny Flipper-
# shaped adapter (boolean, actor, percentage gates) decides per run
# whether the experimental step joins the plan, and rewire_task
# splices it in or routes around it. Same code in production for
# everyone; different plans per actor.
#
#   bundle exec ruby examples/feature_flags.rb
#
# Runs offline; three tenants, one flag, three rollout phases.

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

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

# Flipper's essential shape in 30 lines: gates checked in order
class Flags
  def initialize
    @features = Hash.new { |h, k| h[k] = {boolean: false, actors: [], percentage: 0} }
  end

  def enable(name) = @features[name][class="y">:boolean] = true

  def enable_actor(name, actor) = @features[name][class="y">:actors] << actor

  def enable_percentage(name, pct) = @features[name][class="y">:percentage] = pct

  def enabled?(name, actor = nil)
    f = @features[name]
    return true if f[class="y">:boolean]
    return true if actor && f[class="y">:actors].include?(actor)
    return true if actor && f[class="y">:percentage].positive? &&
      (actor.sum(&class="y">:ord) % 100) < f[class="y">:percentage] # deterministic bucketing

    false
  end
end

FLAGS = Flags.new

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

# One plan definition; the flag decides its SHAPE per actor
def build_plan(tenant)
  o = Agentic:class="y">:PlanOrchestrator.new
  fetch = task_named(class="s">"fetch")
  summarize = task_named(class="s">"summarize")
  publish = task_named(class="s">"publish")
  o.add_task(fetch, agent: ->(_t) { class="s">"articles" })
  o.add_task(summarize, [fetch], agent: ->(_t) { class="s">"summary" })
  o.add_task(publish, [summarize], agent: ->(t) { class="s">"published: #{t.previous_output}" })

  if FLAGS.enabled?(class="y">:fact_check, tenant)
    check = task_named(class="s">"fact_check")
    o.add_task(check, [summarize], agent: ->(_t) { class="s">"checked summary" })
    o.rewire_task(publish, [check]) # splice the new step into the seam
  end
  [o, publish]
end

TENANTS = %w[acme globex umbrella].freeze

def survey(phase)
  puts class="s">"  #{phase}:"
  TENANTS.each do |tenant|
    orchestrator, publish = build_plan(tenant)
    shape = orchestrator.graph[class="y">:order].map { |id| orchestrator.graph[class="y">:tasks][id].description }.join(class="s">" -> ")
    result = orchestrator.execute_plan
    puts format(class="s">"    %-8s %-46s %s", tenant, shape, result.task_result(publish.id).output)
  end
  puts
end

puts class="s">"FEATURE FLAGS FOR PLANS (one codebase, per-actor shapes)"
puts
survey(class="s">"phase 1 - flag off for everyone")

FLAGS.enable_actor(class="y">:fact_check, class="s">"acme")
survey(class="s">"phase 2 - enabled for actor acme (the design partner)")

FLAGS.enable_percentage(class="y">:fact_check, 50)
survey(class="s">"phase 3 - 50% rollout (deterministic per-tenant bucketing)")

puts class="s">"  the shape of the trick: the experimental step isn't hidden"
puts class="s">"  behind an if INSIDE a task - it's a different PLAN, built per"
puts class="s">"  run, spliced in with rewire_task at exactly one seam. acme has"
puts class="s">"  been running fact-checked for a phase before anyone else, the"
puts class="s">"  50% bucket is deterministic (same tenant, same verdict, every"
puts class="s">"  run - flapping flags are worse than no flags), and rollback is"
puts class="s">"  disable, not deploy. flags decouple SHIPPING code from RUNNING"
puts class="s">"  it; plans-as-data means they can decouple shipping a STEP from"
puts class="s">"  running it, too."