agentic examples

The Plan DSL

The Plan DSL: Sinatra's whole argument was that an API is a user interface, and a user interface should read like what it means. The orchestrator's API is honest but administrative - ids, task objects, add_task bookkeeping. Thirty lines of DSL later, a plan reads like a plan. No engine changes: sugar OVER the API, never reaching into it.

Developer Experience Round 12 Konstantin Haase exit 0

source on github

bundle exec ruby examples/plan_dsl.rb

a real captured run

THE PLAN DSL (thirty lines of sugar over the real API)

  net revenue: $120

  and it's all real underneath: 4 tasks, labeled edges
  (orders, refunds), same graph every round-5-to-11 tool consumes.

  what the sugar buys: names instead of ids (symbols resolve to
  tasks at definition time, so a typo'd :fetch_order fails at
  DEFINE, not at run); the block IS the agent (the work sits
  inside the step that owns it); and after:/needs: read as
  English. what the sugar refuses: reaching into the engine.
  every line delegates to public API - add_task, execute_plan,
  graph - so the DSL can never drift ahead of what the engine
  supports, and anything the DSL can't express, you drop down
  one layer without rewriting. Sinatra's rule: the frontend
  should be a pleasure and the escape hatch should be a door,
  not a wall.

source

# frozen_string_literal: true

# The Plan DSL: Sinatra's whole argument was that an API is a user
# interface, and a user interface should read like what it means.
# The orchestrator's API is honest but administrative - ids, task
# objects, add_task bookkeeping. Thirty lines of DSL later, a plan
# reads like a plan. No engine changes: sugar OVER the API, never
# reaching into it.
#
#   bundle exec ruby examples/plan_dsl.rb
#
# Runs offline; the DSL builds a real orchestrator underneath.

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

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

# The whole DSL. Symbols in, wiring out; the block IS the agent.
module Plan
  def self.define(&block)
    builder = Builder.new
    builder.instance_eval(&block)
    builder
  end

  class Builder
    attr_reader class="y">:orchestrator

    def initialize
      @orchestrator = Agentic:class="y">:PlanOrchestrator.new
      @tasks = {}
    end

    def step(name, after: [], needs: nil, &work)
      task = Agentic:class="y">:Task.new(description: name.to_s, agent_spec: {class="s">"name" => name.to_s, class="s">"instructions" => name.to_s})
      @tasks[name] = task
      deps = Array(after).map { |n| @tasks.fetch(n) }
      named = needs&.transform_values { |n| @tasks.fetch(n) }
      @orchestrator.add_task(task, deps, needs: named, agent: ->(t) { work&.call(t) })
      self
    end

    def run
      @orchestrator.execute_plan
    end

    def output_of(name, result)
      result.task_result(@tasks.fetch(name).id).output
    end
  end
end

# --- a plan that reads like a plan ----------------------------------------------
plan = Plan.define do
  step class="y">:fetch_orders do
    [{id: 1, total: 120}, {id: 2, total: 80}]
  end

  step class="y">:fetch_refunds do
    [{order_id: 2, amount: 80}]
  end

  step class="y">:ledger, needs: {orders: class="y">:fetch_orders, refunds: class="y">:fetch_refunds} do |t|
    t.needs[class="y">:orders].sum { |o| o[class="y">:total] } - t.needs[class="y">:refunds].sum { |r| r[class="y">:amount] }
  end

  step class="y">:report, after: class="y">:ledger do |t|
    class="s">"net revenue: $#{t.previous_output}"
  end
end

result = plan.run
puts class="s">"THE PLAN DSL (thirty lines of sugar over the real API)"
puts
puts class="s">"  #{plan.output_of(class="y">:report, result)}"
puts

graph = plan.orchestrator.graph
puts class="s">"  and it's all real underneath: #{graph[class="y">:tasks].size} tasks, labeled edges"
puts class="s">"  (#{graph[class="y">:edges].filter_map { |e| e[class="y">:label] }.join(", class="s">")}), same graph every round-5-to-11 tool consumes."
puts
puts class="s">"  what the sugar buys: names instead of ids (symbols resolve to"
puts class="s">"  tasks at definition time, so a typo'd class="y">:fetch_order fails at"
puts class="s">"  DEFINE, not at run); the block IS the agent (the work sits"
puts class="s">"  inside the step that owns it); and after:/needs: read as"
puts class="s">"  English. what the sugar refuses: reaching into the engine."
puts class="s">"  every line delegates to public API - add_task, execute_plan,"
puts class="s">"  graph - so the DSL can never drift ahead of what the engine"
puts class="s">"  supports, and anything the DSL can't express, you drop down"
puts class="s">"  one layer without rewriting. Sinatra's rule: the frontend"
puts class="s">"  should be a pleasure and the escape hatch should be a door,"
puts class="s">"  not a wall."