agentic examples

The RBS Export

The RBS Export: a capability contract already knows its types - it validates them at runtime on every call. RBS is the same knowledge written down for tools that read instead of run: steep, IDEs, docs. This generates .rbs signatures from contracts, so the type checker and the validator can never disagree - they're projections of one declaration.

Data & Pipelines Round 14 Soutaro Matsumoto exit 0

source on github

bundle exec ruby examples/rbs_export.rb

a real captured run

THE RBS EXPORT (contracts already know their types; write them down)

  # Quote a shipment (v2.1.0)
  # Enum/bounds/rules are enforced at runtime by CapabilityValidator;
  # RBS carries the SHAPE, the validator carries the LAW.
  class QuoteShippingCapability
    def call: ({ mode: String, weight_kg: Numeric, ?express: bool, ?customs_code: String } inputs) -> { price_cents: Integer, carrier: String }
  end

  # Route a ticket (v1.1.0)
  # Enum/bounds/rules are enforced at runtime by CapabilityValidator;
  # RBS carries the SHAPE, the validator carries the LAW.
  class ClassifyTicketCapability
    def call: ({ text: String, ?urgency: Numeric } inputs) -> { queue: String }
  end

  agreement spot-check against the validator:
    omitting ?-marked keys (express, customs_code): accepted  - agrees
    omitting an unmarked key (weight_kg):           rejected  - agrees

  the division of labor, stated precisely: RBS carries the SHAPE
  (keys, types, optionality - what steep and your IDE can check
  before anything runs), and the validator carries the LAW (enums,
  bounds, cross-field rules - what needs values to judge). neither
  replaces the other; both project from ONE declaration, which is
  why they cannot drift the way hand-written sig files against
  hand-written validations always, always do. gradual typing works
  when the types come from where the truth already lives.

source

# frozen_string_literal: true

# The RBS Export: a capability contract already knows its types -
# it validates them at runtime on every call. RBS is the same
# knowledge written down for tools that read instead of run: steep,
# IDEs, docs. This generates .rbs signatures from contracts, so the
# type checker and the validator can never disagree - they're
# projections of one declaration.
#
#   bundle exec ruby examples/rbs_export.rb
#
# Runs offline; the signatures are printed and self-checked.

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

# Contract type -> RBS type. Optional inputs may be omitted entirely,
# so they project as optional KEYS (key: ?), while nilability is a
# separate question the contract answers with its type check.
RBS_TYPES = {
  class="s">"string" => class="s">"String", class="s">"number" => class="s">"Numeric", class="s">"integer" => class="s">"Integer",
  class="s">"boolean" => class="s">"bool", class="s">"array" => class="s">"Array[untyped]", class="s">"object" => class="s">"Hash[Symbol, untyped]",
  class="s">"hash" => class="s">"Hash[Symbol, untyped]", nil => class="s">"untyped"
}.freeze

def rbs_record(declared)
  fields = declared.map { |key, decl|
    marker = decl[class="y">:required] ? class="s">"" : class="s">"?"
    class="s">"#{marker}#{key}: #{RBS_TYPES.fetch(decl[class="y">:type], "untypedclass="s">")}"
  }
  class="s">"{ #{fields.join(", class="s">")} }"
end

def to_rbs(spec)
  method_name = spec.name.gsub(/[^a-z0-9_]/, class="s">"_")
  <<~RBS
    # #{spec.description} (v#{spec.version})
    # Enum/bounds/rules are enforced at runtime by CapabilityValidator;
    # RBS carries the SHAPE, the validator carries the LAW.
    class #{method_name.split(class="s">"_").map(&class="y">:capitalize).join}Capability
      def call: (#{rbs_record(spec.inputs)} inputs) -> #{rbs_record(spec.outputs)}
    end
  RBS
end

SPECS = [
  Agentic:class="y">:CapabilitySpecification.new(
    name: class="s">"quote_shipping", description: class="s">"Quote a shipment", version: class="s">"2.1.0",
    inputs: {
      mode: {type: class="s">"string", required: true, enum: %w[air sea road]},
      weight_kg: {type: class="s">"number", required: true, min: 1, max: 5_000},
      express: {type: class="s">"boolean"},
      customs_code: {type: class="s">"string"}
    },
    outputs: {price_cents: {type: class="s">"integer", required: true}, carrier: {type: class="s">"string", required: true}}
  ),
  Agentic:class="y">:CapabilitySpecification.new(
    name: class="s">"classify_ticket", description: class="s">"Route a ticket", version: class="s">"1.1.0",
    inputs: {text: {type: class="s">"string", required: true, non_empty: true}, urgency: {type: class="s">"number"}},
    outputs: {queue: {type: class="s">"string", required: true}}
  )
].freeze

puts class="s">"THE RBS EXPORT (contracts already know their types; write them down)"
puts
SPECS.each do |spec|
  to_rbs(spec).lines.each { |line| puts class="s">"  #{line}" }
  puts
end

# --- the agreement check: what RBS says optional, the validator permits ---------
# (Same discipline as round 10's projection prover: two renderings of
# one declaration must be spot-checked against each other.)
spec = SPECS.first
validator = Agentic:class="y">:CapabilityValidator.new(spec)
optional_omitted = {mode: class="s">"air", weight_kg: 100} # express, customs_code omitted
required_omitted = {mode: class="s">"air"}                 # weight_kg missing

validator.validate_inputs!(optional_omitted)
agreement_a = true
agreement_b = begin
  validator.validate_inputs!(required_omitted)
  false # validator allowed what RBS marks required - disagreement!
rescue Agentic:class="y">:Errors:class="y">:ValidationError
  true
end

puts class="s">"  agreement spot-check against the validator:"
puts class="s">"    omitting ?-marked keys (express, customs_code): accepted  #{agreement_a ? "- agreesclass="s">" : "DISAGREESclass="s">"}"
puts class="s">"    omitting an unmarked key (weight_kg):           rejected  #{agreement_b ? "- agreesclass="s">" : "DISAGREESclass="s">"}"
puts
puts class="s">"  the division of labor, stated precisely: RBS carries the SHAPE"
puts class="s">"  (keys, types, optionality - what steep and your IDE can check"
puts class="s">"  before anything runs), and the validator carries the LAW (enums,"
puts class="s">"  bounds, cross-field rules - what needs values to judge). neither"
puts class="s">"  replaces the other; both project from ONE declaration, which is"
puts class="s">"  why they cannot drift the way hand-written sig files against"
puts class="s">"  hand-written validations always, always do. gradual typing works"
puts class="s">"  when the types come from where the truth already lives."
exit((agreement_a && agreement_b) ? 0 : 1)