agentic examples

Self-Correcting Output

Self-Correcting Output: the pattern that makes LLM components shippable. The model's output is validated against the capability's contract; violations don't raise to the user - they become the CORRECTION PROMPT for a bounded retry. The contract is the editor, the model is the writer, and the loop converges or fails honestly with the full paper trail.

Reliability & Recovery Round 12 Obie Fernandez exit 0

source on github

bundle exec ruby examples/self_correcting_output.rb

a real captured run

SELF-CORRECTING OUTPUT (the contract is the editor)

  attempt 1: {"vendor":"Initech Supply Co","total_cents":"4200","currency":"usd"}
  -> rejected by the contract; violations become the next prompt:
       Your previous answer violated the output contract:
       - total_cents: must be Numeric
       - currency: must be one of: USD, EUR, GBP
       - due_date: is missing
       Return the SAME data corrected to satisfy every constraint. Do not apologize; return JSON.
  attempt 2: {"vendor":"Initech Supply Co","total_cents":4200,"currency":"USD","due_date":"2026-08-01"}
  -> contract satisfied. shipped after 2 attempt(s).

  the shape of the trick: nothing here trusts the model, and
  nothing here burdens the user. the contract that documents the
  capability (rounds 5-11 built six tools on it) turns out to be
  the exact artifact a correction loop needs - violations arrive
  pre-written as actionable feedback ("currency: must be one of:
  USD, EUR, GBP"), which beats "please try again" by exactly the
  margin your production error rate will show. the loop is
  bounded (3 attempts - unbounded self-correction is a billing
  strategy), each retry costs one more model call and is worth
  it, and when it fails it fails with every draft on record.
  ship the editor with the writer; never ship the writer alone.

source

# frozen_string_literal: true

# Self-Correcting Output: the pattern that makes LLM components
# shippable. The model's output is validated against the capability's
# contract; violations don't raise to the user - they become the
# CORRECTION PROMPT for a bounded retry. The contract is the editor,
# the model is the writer, and the loop converges or fails honestly
# with the full paper trail.
#
#   bundle exec ruby examples/self_correcting_output.rb
#
# Runs offline; the "model" is scripted to be sloppy, then coachable.

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

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

CONTRACT = Agentic:class="y">:CapabilitySpecification.new(
  name: class="s">"extract_invoice", description: class="s">"Extract structured invoice data", version: class="s">"1.0.0",
  inputs: {text: {type: class="s">"string", required: true}},
  outputs: {
    vendor: {type: class="s">"string", required: true, non_empty: true},
    total_cents: {type: class="s">"number", required: true, min: 0},
    currency: {type: class="s">"string", required: true, enum: %w[USD EUR GBP]},
    due_date: {type: class="s">"string", required: true}
  }
)
VALIDATOR = Agentic:class="y">:CapabilityValidator.new(CONTRACT)

# The "model": pass 1 is what models actually do to schemas; pass 2
# reads the corrections like a chastened intern
MODEL = lambda do |prompt, attempt|
  if attempt == 1
    # currency invented, total as a string, due_date forgotten
    {vendor: class="s">"Initech Supply Co", total_cents: class="s">"4200", currency: class="s">"usd"}
  else
    # the correction prompt names each violation; the model complies
    {vendor: class="s">"Initech Supply Co", total_cents: 4200, currency: class="s">"USD", due_date: class="s">"2026-08-01"}
  end
end

def correction_prompt(violations)
  lines = violations.map { |field, messages| class="s">"- #{field}: #{messages.join("; class="s">")}" }
  class="s">"Your previous answer violated the output contract:\n#{lines.join("\nclass="s">")}\n" \
    class="s">"Return the SAME data corrected to satisfy every constraint. Do not apologize; return JSON."
end

MAX_ATTEMPTS = 3
INVOICE = class="s">"Invoice from Initech Supply Co, total $42.00, due Aug 1 2026"

puts class="s">"SELF-CORRECTING OUTPUT (the contract is the editor)"
puts
attempt = 0
output = nil
prompt = class="s">"Extract the invoice fields from: #{INVOICE}"
loop do
  attempt += 1
  output = MODEL.call(prompt, attempt)
  puts class="s">"  attempt #{attempt}: #{JSON.generate(output)}"
  begin
    VALIDATOR.validate_outputs!(output)
    puts class="s">"  -> contract satisfied. shipped after #{attempt} attempt(s)."
    break
  rescue Agentic:class="y">:Errors:class="y">:ValidationError => e
    if attempt >= MAX_ATTEMPTS
      puts class="s">"  -> #{MAX_ATTEMPTS} attempts exhausted; failing HONESTLY with the paper trail."
      raise
    end
    puts class="s">"  -> rejected by the contract; violations become the next prompt:"
    correction = correction_prompt(e.violations)
    correction.lines.each { |l| puts class="s">"       #{l.chomp}" }
    prompt = correction
  end
end

puts
puts class="s">"  the shape of the trick: nothing here trusts the model, and"
puts class="s">"  nothing here burdens the user. the contract that documents the"
puts class="s">"  capability (rounds 5-11 built six tools on it) turns out to be"
puts class="s">"  the exact artifact a correction loop needs - violations arrive"
puts class="s">"  pre-written as actionable feedback (\"currency: must be one of:"
puts class="s">"  USD, EUR, GBP\"), which beats \class="s">"please try again\" by exactly the"
puts class="s">"  margin your production error rate will show. the loop is"
puts class="s">"  bounded (#{MAX_ATTEMPTS} attempts - unbounded self-correction is a billing"
puts class="s">"  strategy), each retry costs one more model call and is worth"
puts class="s">"  it, and when it fails it fails with every draft on record."
puts class="s">"  ship the editor with the writer; never ship the writer alone."