innocentdiaz/typesafe_ruby

Typesafe S1 model Jev implementation as a native/primitive Ruby feature.

Ruby

1

1 commits

updated Sep 18, 2026

See the code

See what people are saying (1)

SourceMessageScoreDate

Jev as a Primitive Feature of Ruby

1

Sep 18, 2026

README

typesafe-ruby

Ruby primitives for System One (S1) models

Your code asks a question; it gets back a bool, float, a choice, or a level it can branch on directly.

TL;DR

Illustrative code:

# TypeSafe.configure { |c| c.primitives = true }   # enables the bare noul? / ask below

def handle_chat(chat)
  TypeSafe::S1.about(chat) do
    triage = ask do |q|
      q.noul  :escalate,   "Is the customer asking for a human agent?"
      q.noul  :new_matter, "Is this a new matter?"
      q.score :severity,   "How severe is the injury?", "None", "Minor", "Serious", "Catastrophic"
    end

    return escalate_to_human if triage.true?(:escalate)
    create_case(triage[:severity].level) if triage.true?(:new_matter) && triage[:severity].index >= 1
  end
end

chat = { messages: [] }
inbox.each do |message|          # whatever feeds you messages: a queue, a webhook, a socket
  chat[:messages] << message
  handle_chat(chat)              # one call per message; every question answered fresh
end

Usage

state = TypeSafe::S1::State.new("I have asked three times now. Can I just talk to a real person?")

if state.noul?("Is the customer asking for a human agent?")
  escalate_to_human
end

Or one-liners:

escalate_to_human if TypeSafe::S1::State.new(chat).noul?("is the customer asking for a human agent?")

Or even simpler, if installed as primitives:

chat = { messages: [ "How may I help you?" ... ]}
escalate_to_human if chat.noul? "is the customer asking for a human agent?"

Or even, with no receiver at all:

TypeSafe::S1.about(chat) do
  escalate_to_human if noul? "is the customer asking for a human agent?"

  triage = ask do |q|
    q.score :severity, "How severe is the injury?", "None", "Minor", "Serious", "Catastrophic"
    q.noul :new_matter, "Is this a new matter?"
  end
  create_case(triage[:severity].level) if triage.true?(:new_matter) && triage.true?(:severity) >= 0.5
end

Install

gem "typesafe-ruby"
# config/initializers/typesafe.rb (or anywhere at boot)
TypeSafe.configure do |c|
  c.api_key     = ENV["TYPESAFE_API_KEY"]   # default: read from the environment
  c.provider    = :jev                      # default; a name under Providers, or an instance
  c.model       = "jev-latest"
  c.base_url    = "https://api.typesafe.ai"
  c.timeout     = 30                        # seconds per request
  c.max_retries = 2                         # transient failures before raising
  c.threshold   = 0.5                       # a noul at or above this reads as true
  c.logger      = Rails.logger              # optional; debug lines per request, warns on retry
  c.primitives  = false                     # see below
end

Every value has a default; an initializer is only needed to change one. Ruby ≥ 3.2. No runtime dependencies.

Primitives (opt-in)

Off by default. Turn it on and String, Hash and Array answer questions about themselves; a bare noul? / choice / score / ask acts on the ambient subject.

TypeSafe.configure { |c| c.primitives = true }      # or a subset: [String]
require "typesafe/primitives"                        # equivalent, require-style

"Can I talk to a real person?".noul?("Is the customer asking for a human agent?")
chat.choice("Which team?", returns: "Refunds", billing: "Charges")
chat.ask { |q| q.noul :escalate, "..." }
chat.to_s1(threshold: 0.9).noul?("...")             # the State, for per-call options

TypeSafe::S1.about(chat) { noul?("...") }            # block-scoped subject, restored after
TypeSafe::S1.subject = chat                          # console / long-lived scope

The three primitives

Every question is one of three shapes. Pick by what the answer is.

noul — "Is this true?" Returns a probability, 0 to 1. The probability is the signal.

state.noul("Is the customer asking for a human agent?")    # => #<TypeSafe::S1::Answer::Noul 0.98>  compares like a number
state.noul?("Is the customer asking for a human agent?")   # => true  (at or above the threshold)

# Optional clarification of what counts as yes / no:
state.noul("Has the customer contacted support about this before?",
           true:  "mentions a prior attempt, ticket, or having asked before",
           false: "no sign of any previous contact")

choice — "Which of these options?" An unordered set; you get the pick plus a distribution over all options.

dept = state.choice("Which team should handle this?",
                    returns:  "Exchanges, refunds, wrong or damaged items",
                    shipping: "Delivery status, delays, lost packages",
                    billing:  "Charges, invoices, payment problems")
dept.to_sym          # => :returns
dept[:shipping]      # => 0.0
dept.confidence      # => 1.0

score — "Which level?" An ordered spectrum, worst → best. You get the most likely level, and the probability-weighted position.

sev = state.score("How severe is the reported issue?",
                  "Cosmetic; no impact to functionality",
                  "Broken or degraded feature, but a workaround exists",
                  "Blocking issue; no workaround exists")
sev.level    # => "Blocking issue; no workaround exists"
sev.index    # => 2
sev.to_f     # => 1.68   (weighted position across the levels)

Batch: many questions, one call

Each single-question method above is one request. When several questions share a state, batch them — one round trip, and the model answers each independently (one answer is never hidden context for another).

result = state.ask do |q|
  q.noul   :escalate,   "Is the customer asking for a human agent?"
  q.noul   :repeat,     "Has the customer contacted support about this before?",
                        true: "mentions a prior attempt", false: "no sign of one"
  q.choice :department, "Which team should handle this?", returns: "Refunds", shipping: "Delays", billing: "Charges"
  q.score  :severity,   "How severe is the issue?", "Cosmetic", "Degraded, workaround exists", "Blocking"
end

result[:escalate].true?        # => true
result[:department].to_sym     # => :returns
result[:severity].level        # => "Blocking"
result.usage                   # => { input_tokens: 490, output_tokens: 86 }
result.duration_ms             # => 398

Ask speculatively: include questions whose answers you only need conditionally, then let your code decide which to use. That keeps it to one call.

r = state.ask do |q|
  q.noul  :is_lead,  "Is there a potential new case or matter?"
  q.noul  :qualified, "Is this a qualified lead, based on `firm.criteria`?"
  q.noul  :prior_rep, "Does the lead already have an attorney?"   # asked regardless,
end                                                               # used only when relevant

if r[:is_lead].true? && r[:qualified] >= 0.85
  flag_conflict if r[:prior_rep].true?
end

Structured state and structured questions

State can be a string, or a hash/array (braced — bare keywords to State.new are options, not state). With a hash, instructions can point at fields with backticked paths:

state = TypeSafe::S1::State.new({
  transcript:   utterances,
  case_details: { date_of_incident: "2026-08-15", sol_deadline: "2027-08-15" }
})

state.noul?("Judging from `transcript` and `case_details.sol_deadline`, is the claim still within the statute of limitations?")

Instructions can be structured too — the verification pattern:

state = TypeSafe::S1::State.new({ source_text: "Invoice #4471 issued March 3, 2026 to Beaver Dam Logistics for $12,840.00, net 30." })

state.noul?({ field: { name: "invoice_number", type: "string", description: "The identifier printed on the invoice." },
              extracted_value: "4471",
              question: "Does `extracted_value` match the `field` as it appears in `source_text`?" })

Reading answers

All answers carry probabilities and confident?(threshold); use it to route — act automatically when confident, escalate to a person or a reasoning model when not. Choice and score answers also carry the provider's confidence; a noul's confidence is how far its probability sits from the fence, so confident? only discriminates when the threshold is above 0.5.

answerreads as
Answer::Noulto_f, true? / false?, compares to numbers (a >= 0.85)
Answer::Choiceto_sym, to_s, [option], == :returns
Answer::Scorelevel, index, to_f, levels
answer = result[:department]
if answer.confident?
  route_to(answer.to_sym)
else
  hold_for_review(answer.probabilities)
end

Errors

Rescue by intent, not by HTTP code:

begin
  result = state.ask { |q| ... }
rescue TypeSafe::TransientError => e     # RateLimitError, ServerError, ConnectionError, TimeoutError — retry later
  retry_later(e)
rescue TypeSafe::PermanentError => e     # AuthenticationError, InvalidRequestError, ValidationError — fix the request
  raise
end

Transient failures already retry inside the provider (max_retries, honoring Retry-After) before surfacing.

Testing

Providers::Stub answers without the network. Give it the answers that matter; everything else gets a neutral default (noul 0.5, the first option, the first level).

TypeSafe.configure do |c|
  c.provider = TypeSafe::S1::Providers::Stub.new(escalate: 0.9, department: :billing, severity: 2)
end

Single-question calls (noul?, choice, score) are keyed by their own name: Stub.new(noul: 0.9, choice: :billing, score: 2).

A block form receives the request when an answer should depend on the state:

TypeSafe::S1::Providers::Stub.new { |req| { escalate: req.state.include?("real person") ? 0.95 : 0.1 } }

Observing calls

Hook every completed call for telemetry or a cost ledger. Extra keyword arguments to State.new ride along on the request, so you can attribute a call to its owner:

TypeSafe::S1.on_result do |result, request|
  Ledger.record(owner: request.options[:owner], model: result.model, **result.usage)
end

TypeSafe::S1::State.new(transcript, owner: phone_call).noul?("...")

Providers

A provider is any object responding to call(request) → Result. Configure by name — :jev resolves to TypeSafe::S1::Providers::Jev — or pass an instance. Providers::Base gives a new provider the normalized Answer constructors, so consumers never see a vendor's wire keys.

TypeSafe.configure { |c| c.provider = :jev }
TypeSafe::S1::State.new(text, provider: MyProvider.new)   # per-state override

Development

bin/setup, then bundle exec rake runs the specs and rubocop. bin/console opens IRB with the gem loaded. TYPESAFE_LIVE=1 TYPESAFE_API_KEY=… bundle exec rspec spec/typesafe/live_spec.rb hits the real API.

Why this shape

An LLM writes text for a person to read. A System One model makes a judgment for code to consume: typed question in, calibrated answer out, decision composed in Ruby you can read and test. Decompose a decision into atomic questions, ask them together, combine the answers with plain logic — and hand off to a person or a reasoning model only where the confidence says you should.

License

MIT.

Contributors

innocentdiaz

1 commits

innocentdiaz/typesafe_ruby

Typesafe S1 model Jev implementation as a native/primitive Ruby feature.

Ruby

1

1 commits

updated Sep 18, 2026

See the code

See what people are saying (1)

SourceMessageScoreDate

Jev as a Primitive Feature of Ruby

1

Sep 18, 2026

README

typesafe-ruby

Ruby primitives for System One (S1) models

Your code asks a question; it gets back a bool, float, a choice, or a level it can branch on directly.

TL;DR

Illustrative code:

# TypeSafe.configure { |c| c.primitives = true }   # enables the bare noul? / ask below

def handle_chat(chat)
  TypeSafe::S1.about(chat) do
    triage = ask do |q|
      q.noul  :escalate,   "Is the customer asking for a human agent?"
      q.noul  :new_matter, "Is this a new matter?"
      q.score :severity,   "How severe is the injury?", "None", "Minor", "Serious", "Catastrophic"
    end

    return escalate_to_human if triage.true?(:escalate)
    create_case(triage[:severity].level) if triage.true?(:new_matter) && triage[:severity].index >= 1
  end
end

chat = { messages: [] }
inbox.each do |message|          # whatever feeds you messages: a queue, a webhook, a socket
  chat[:messages] << message
  handle_chat(chat)              # one call per message; every question answered fresh
end

Usage

state = TypeSafe::S1::State.new("I have asked three times now. Can I just talk to a real person?")

if state.noul?("Is the customer asking for a human agent?")
  escalate_to_human
end

Or one-liners:

escalate_to_human if TypeSafe::S1::State.new(chat).noul?("is the customer asking for a human agent?")

Or even simpler, if installed as primitives:

chat = { messages: [ "How may I help you?" ... ]}
escalate_to_human if chat.noul? "is the customer asking for a human agent?"

Or even, with no receiver at all:

TypeSafe::S1.about(chat) do
  escalate_to_human if noul? "is the customer asking for a human agent?"

  triage = ask do |q|
    q.score :severity, "How severe is the injury?", "None", "Minor", "Serious", "Catastrophic"
    q.noul :new_matter, "Is this a new matter?"
  end
  create_case(triage[:severity].level) if triage.true?(:new_matter) && triage.true?(:severity) >= 0.5
end

Install

gem "typesafe-ruby"
# config/initializers/typesafe.rb (or anywhere at boot)
TypeSafe.configure do |c|
  c.api_key     = ENV["TYPESAFE_API_KEY"]   # default: read from the environment
  c.provider    = :jev                      # default; a name under Providers, or an instance
  c.model       = "jev-latest"
  c.base_url    = "https://api.typesafe.ai"
  c.timeout     = 30                        # seconds per request
  c.max_retries = 2                         # transient failures before raising
  c.threshold   = 0.5                       # a noul at or above this reads as true
  c.logger      = Rails.logger              # optional; debug lines per request, warns on retry
  c.primitives  = false                     # see below
end

Every value has a default; an initializer is only needed to change one. Ruby ≥ 3.2. No runtime dependencies.

Primitives (opt-in)

Off by default. Turn it on and String, Hash and Array answer questions about themselves; a bare noul? / choice / score / ask acts on the ambient subject.

TypeSafe.configure { |c| c.primitives = true }      # or a subset: [String]
require "typesafe/primitives"                        # equivalent, require-style

"Can I talk to a real person?".noul?("Is the customer asking for a human agent?")
chat.choice("Which team?", returns: "Refunds", billing: "Charges")
chat.ask { |q| q.noul :escalate, "..." }
chat.to_s1(threshold: 0.9).noul?("...")             # the State, for per-call options

TypeSafe::S1.about(chat) { noul?("...") }            # block-scoped subject, restored after
TypeSafe::S1.subject = chat                          # console / long-lived scope

The three primitives

Every question is one of three shapes. Pick by what the answer is.

noul — "Is this true?" Returns a probability, 0 to 1. The probability is the signal.

state.noul("Is the customer asking for a human agent?")    # => #<TypeSafe::S1::Answer::Noul 0.98>  compares like a number
state.noul?("Is the customer asking for a human agent?")   # => true  (at or above the threshold)

# Optional clarification of what counts as yes / no:
state.noul("Has the customer contacted support about this before?",
           true:  "mentions a prior attempt, ticket, or having asked before",
           false: "no sign of any previous contact")

choice — "Which of these options?" An unordered set; you get the pick plus a distribution over all options.

dept = state.choice("Which team should handle this?",
                    returns:  "Exchanges, refunds, wrong or damaged items",
                    shipping: "Delivery status, delays, lost packages",
                    billing:  "Charges, invoices, payment problems")
dept.to_sym          # => :returns
dept[:shipping]      # => 0.0
dept.confidence      # => 1.0

score — "Which level?" An ordered spectrum, worst → best. You get the most likely level, and the probability-weighted position.

sev = state.score("How severe is the reported issue?",
                  "Cosmetic; no impact to functionality",
                  "Broken or degraded feature, but a workaround exists",
                  "Blocking issue; no workaround exists")
sev.level    # => "Blocking issue; no workaround exists"
sev.index    # => 2
sev.to_f     # => 1.68   (weighted position across the levels)

Batch: many questions, one call

Each single-question method above is one request. When several questions share a state, batch them — one round trip, and the model answers each independently (one answer is never hidden context for another).

result = state.ask do |q|
  q.noul   :escalate,   "Is the customer asking for a human agent?"
  q.noul   :repeat,     "Has the customer contacted support about this before?",
                        true: "mentions a prior attempt", false: "no sign of one"
  q.choice :department, "Which team should handle this?", returns: "Refunds", shipping: "Delays", billing: "Charges"
  q.score  :severity,   "How severe is the issue?", "Cosmetic", "Degraded, workaround exists", "Blocking"
end

result[:escalate].true?        # => true
result[:department].to_sym     # => :returns
result[:severity].level        # => "Blocking"
result.usage                   # => { input_tokens: 490, output_tokens: 86 }
result.duration_ms             # => 398

Ask speculatively: include questions whose answers you only need conditionally, then let your code decide which to use. That keeps it to one call.

r = state.ask do |q|
  q.noul  :is_lead,  "Is there a potential new case or matter?"
  q.noul  :qualified, "Is this a qualified lead, based on `firm.criteria`?"
  q.noul  :prior_rep, "Does the lead already have an attorney?"   # asked regardless,
end                                                               # used only when relevant

if r[:is_lead].true? && r[:qualified] >= 0.85
  flag_conflict if r[:prior_rep].true?
end

Structured state and structured questions

State can be a string, or a hash/array (braced — bare keywords to State.new are options, not state). With a hash, instructions can point at fields with backticked paths:

state = TypeSafe::S1::State.new({
  transcript:   utterances,
  case_details: { date_of_incident: "2026-08-15", sol_deadline: "2027-08-15" }
})

state.noul?("Judging from `transcript` and `case_details.sol_deadline`, is the claim still within the statute of limitations?")

Instructions can be structured too — the verification pattern:

state = TypeSafe::S1::State.new({ source_text: "Invoice #4471 issued March 3, 2026 to Beaver Dam Logistics for $12,840.00, net 30." })

state.noul?({ field: { name: "invoice_number", type: "string", description: "The identifier printed on the invoice." },
              extracted_value: "4471",
              question: "Does `extracted_value` match the `field` as it appears in `source_text`?" })

Reading answers

All answers carry probabilities and confident?(threshold); use it to route — act automatically when confident, escalate to a person or a reasoning model when not. Choice and score answers also carry the provider's confidence; a noul's confidence is how far its probability sits from the fence, so confident? only discriminates when the threshold is above 0.5.

answerreads as
Answer::Noulto_f, true? / false?, compares to numbers (a >= 0.85)
Answer::Choiceto_sym, to_s, [option], == :returns
Answer::Scorelevel, index, to_f, levels
answer = result[:department]
if answer.confident?
  route_to(answer.to_sym)
else
  hold_for_review(answer.probabilities)
end

Errors

Rescue by intent, not by HTTP code:

begin
  result = state.ask { |q| ... }
rescue TypeSafe::TransientError => e     # RateLimitError, ServerError, ConnectionError, TimeoutError — retry later
  retry_later(e)
rescue TypeSafe::PermanentError => e     # AuthenticationError, InvalidRequestError, ValidationError — fix the request
  raise
end

Transient failures already retry inside the provider (max_retries, honoring Retry-After) before surfacing.

Testing

Providers::Stub answers without the network. Give it the answers that matter; everything else gets a neutral default (noul 0.5, the first option, the first level).

TypeSafe.configure do |c|
  c.provider = TypeSafe::S1::Providers::Stub.new(escalate: 0.9, department: :billing, severity: 2)
end

Single-question calls (noul?, choice, score) are keyed by their own name: Stub.new(noul: 0.9, choice: :billing, score: 2).

A block form receives the request when an answer should depend on the state:

TypeSafe::S1::Providers::Stub.new { |req| { escalate: req.state.include?("real person") ? 0.95 : 0.1 } }

Observing calls

Hook every completed call for telemetry or a cost ledger. Extra keyword arguments to State.new ride along on the request, so you can attribute a call to its owner:

TypeSafe::S1.on_result do |result, request|
  Ledger.record(owner: request.options[:owner], model: result.model, **result.usage)
end

TypeSafe::S1::State.new(transcript, owner: phone_call).noul?("...")

Providers

A provider is any object responding to call(request) → Result. Configure by name — :jev resolves to TypeSafe::S1::Providers::Jev — or pass an instance. Providers::Base gives a new provider the normalized Answer constructors, so consumers never see a vendor's wire keys.

TypeSafe.configure { |c| c.provider = :jev }
TypeSafe::S1::State.new(text, provider: MyProvider.new)   # per-state override

Development

bin/setup, then bundle exec rake runs the specs and rubocop. bin/console opens IRB with the gem loaded. TYPESAFE_LIVE=1 TYPESAFE_API_KEY=… bundle exec rspec spec/typesafe/live_spec.rb hits the real API.

Why this shape

An LLM writes text for a person to read. A System One model makes a judgment for code to consume: typed question in, calibrated answer out, decision composed in Ruby you can read and test. Decompose a decision into atomic questions, ask them together, combine the answers with plain logic — and hand off to a person or a reasoning model only where the confidence says you should.

License

MIT.

Contributors

innocentdiaz

1 commits

Languages

Ruby

99.9%