agentic examples

The Type Séance

The Type Séance: this plan has no type annotations, but it HAD a run - and a run is a set of observations, and observations formalized are types. The séance sits with the departed execution, records the shape of every value that crossed every task seam, and transcribes what the spirits reveal as RBS. Then the strange part: the inferred signatures become a CONTRACT for the next run, and a poltergeist task - one that returns a different type the second time - is caught by the …

Observability & Ops Round 19 Soutaro Matsumoto exit 0

source on github

bundle exec ruby examples/type_seance.rb

a real captured run

THE TYPE SEANCE (types are observations, formalized; the medium remembers)

  the sitting: one run observed at every seam, transcribed to RBS:
    def fetch: (void) -> Array[String]
    def parse: (Array[String]) -> Array[Hash[Symbol, untyped]]
    def score: (Array[Hash[Symbol, untyped]]) -> Array[Integer]
    def format: (Array[Integer]) -> String

  second honest run against the inferred contract: 4/4 seams conform

  then the poltergeist: a score task that was honest on its first
  visit (conformed) and changed its return type on the second:
    SEAM HAUNTED: score - promised (Array[Hash[Symbol, untyped]]) -> Array[Integer],
                  delivered (Array[Hash[Symbol, untyped]]) -> Array[Float]
    SEAM HAUNTED: format - promised (Array[Integer]) -> String,
                  delivered (Array[Float]) -> String

  gradual typing's whole wager, demonstrated seance-style: you do
  not have to WRITE the types - the program already knows them, at
  runtime, at every seam, and one observed run transcribes to RBS
  for free. what inference cannot give you is INTENT: the medium
  transcribed Integer because that's what score returned, not
  because score MEANT it - which is exactly why the poltergeist's
  Float is caught as a haunting rather than accepted as a wider
  union. note the contagion: score's lie condemned format's seam
  too (its INPUT shape changed) - type errors travel downstream
  wearing the caller's clothes, in seances as in Steep. observe
  first, formalize second, verify forever.

source

# frozen_string_literal: true

# The Type Séance: this plan has no type annotations, but it HAD a
# run - and a run is a set of observations, and observations
# formalized are types. The séance sits with the departed execution,
# records the shape of every value that crossed every task seam, and
# transcribes what the spirits reveal as RBS. Then the strange part:
# the inferred signatures become a CONTRACT for the next run, and a
# poltergeist task - one that returns a different type the second
# time - is caught by the ghost of its own first answer. You said
# Array[Integer] in life. The medium remembers.
#
#   bundle exec ruby examples/type_seance.rb
#
# Runs offline; exits 1 unless the séance transcribes correctly and
# the poltergeist is caught at exactly the lying seam.

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

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

# --- the medium: value shapes, transcribed to RBS-ish -------------------------------
def shape_of(value)
  case value
  when Array
    inner = value.map { |v| shape_of(v) }.uniq
    (inner.size == 1) ? class="s">"Array[#{inner.first}]" : class="s">"Array[untyped]"
  when Hash
    keys = value.keys.map { |k| shape_of(k) }.uniq
    vals = value.values.map { |v| shape_of(v) }.uniq
    class="s">"Hash[#{(keys.size == 1) ? keys.first : "untypedclass="s">"}, #{(vals.size == 1) ? vals.first : "untypedclass="s">"}]"
  else value.class.name
  end
end

# One sitting: run the plan, observe every seam, return the transcript
def hold_seance(score_agent)
  orchestrator = Agentic:class="y">:PlanOrchestrator.new(concurrency_limit: 1)
  observed = {}
  agents = {
    class="s">"fetch" => ->(_in) { [class="s">"7 lamps", class="s">"3 rugs", class="s">"9 clocks"] },
    class="s">"parse" => ->(lines) { lines.map { |l| {qty: l.to_i, item: l.split.last} } },
    class="s">"score" => score_agent,
    class="s">"format" => ->(scores) { class="s">"total: #{scores.sum}" }
  }
  previous = nil
  agents.each do |name, fn|
    task = Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"w"})
    orchestrator.add_task(task, previous ? [previous] : [], agent: ->(t) {
      input = t.previous_output
      output = fn.call(input)
      observed[name] = {in: input.nil? ? class="s">"void" : shape_of(input), out: shape_of(output)}
      output
    })
    previous = task
  end
  orchestrator.execute_plan
  observed
end

honest_score = ->(rows) { rows.map { |r| r[class="y">:qty] * 10 } }
poltergeist_visits = 0
poltergeist_score = ->(rows) {
  poltergeist_visits += 1
  (poltergeist_visits == 1) ? rows.map { |r| r[class="y">:qty] * 10 } : rows.map { |r| r[class="y">:qty] * 10.0 } # honest in life...
}

puts class="s">"THE TYPE SEANCE (types are observations, formalized; the medium remembers)"
puts

# --- sitting one: infer RBS from the honest run --------------------------------------
transcript = hold_seance(honest_score)
puts class="s">"  the sitting: one run observed at every seam, transcribed to RBS:"
transcript.each { |name, sig| puts class="s">"    def #{name}: (#{sig[class="y">:in]}) -> #{sig[class="y">:out]}" }
puts

# --- sitting two: the contract holds the honest, catches the poltergeist -------------
honest_again = hold_seance(honest_score)
conform = transcript.keys.select { |k| honest_again[k] == transcript[k] }
puts class="s">"  second honest run against the inferred contract: #{conform.size}/#{transcript.size} seams conform"

haunted_first = hold_seance(poltergeist_score) # visit 1: behaves
haunted_second = hold_seance(poltergeist_score) # visit 2: lies
violations = transcript.keys.reject { |k| haunted_second[k] == transcript[k] }
puts
puts class="s">"  then the poltergeist: a score task that was honest on its first"
puts class="s">"  visit (#{(haunted_first["scoreclass="s">"] == transcript["scoreclass="s">"]) ? "conformedclass="s">" : "?!class="s">"}) and changed its return type on the second:"
violations.each do |name|
  puts class="s">"    SEAM HAUNTED: #{name} - promised (#{transcript[name][class="y">:in]}) -> #{transcript[name][class="y">:out]},"
  puts class="s">"                  delivered (#{haunted_second[name][class="y">:in]}) -> #{haunted_second[name][class="y">:out]}"
end
puts

failures = []
failures << class="s">"inference wrong" unless transcript[class="s">"parse"] == {in: class="s">"Array[String]", out: class="s">"Array[Hash[Symbol, untyped]]"}
failures << class="s">"honest run failed its own ghost" unless conform.size == transcript.size
failures << class="s">"poltergeist not caught, or caught wrongly" unless violations.include?(class="s">"score") && violations.include?(class="s">"format")
failures << class="s">"score's lie mislabeled" unless haunted_second[class="s">"score"][class="y">:out] == class="s">"Array[Float]" && transcript[class="s">"score"][class="y">:out] == class="s">"Array[Integer]"

puts class="s">"  gradual typing's whole wager, demonstrated seance-style: you do"
puts class="s">"  not have to WRITE the types - the program already knows them, at"
puts class="s">"  runtime, at every seam, and one observed run transcribes to RBS"
puts class="s">"  for free. what inference cannot give you is INTENT: the medium"
puts class="s">"  transcribed Integer because that's what score returned, not"
puts class="s">"  because score MEANT it - which is exactly why the poltergeist's"
puts class="s">"  Float is caught as a haunting rather than accepted as a wider"
puts class="s">"  union. note the contagion: score's lie condemned format's seam"
puts class="s">"  too (its INPUT shape changed) - type errors travel downstream"
puts class="s">"  wearing the caller's clothes, in seances as in Steep. observe"
puts class="s">"  first, formalize second, verify forever."
exit(failures.empty? ? 0 : 1)