skip to content
docs / building blocks

cascade()

Ask cheaply first. Escalate only when Jev isn't confident, then fall back to anything.

Basics#

Most inputs are easy. A cascade asks the cheap question first and only climbs to a more expensive one when Jev isn't confident enough. When no rung is sure, it hands off to a fallback, which can be any node at all: an LLM, a human queue, a coin.

text-them-back.ts
import { cascade, tier, choice, step } from "jevchain";

const verdict = choice("Should the recipient reply to this text message?", {
  reply: "Replying is kind, safe and likely to lead somewhere good.",
  "leave-on-read": "Replying would restart something unhealthy, or the message doesn't need a reply.",
});

const decide = cascade("should-i-reply", {
  tiers: [
    // Cheap: only the message itself.
    tier("gut-check", { ask: verdict, minConfidence: 0.7, state: "{{input.message}}" }),
    // Thorough: the whole input, history and receipts included.
    tier("full-context", { ask: verdict, minConfidence: 0.5 }),
  ],
  // Nobody was sure. Hand off to something that isn't Jev.
  fallback: step("ask-the-group-chat", () => "Screenshot it and send it to the group chat."),
});

Tiers run in order, one Jev call each. The first tier whose confidence clears its minConfidence answers, and the rest never run. So on easy inputs you pay for one small call; on hard ones you pay for exactly as much thinking as it took.

▶ run itShould I Text Them Back?gallerysource →
Tier one reads only the message. If that's not enough, tier two reads the full context. If that's still a coin flip, the group chat decides. Try “u up?” versus “the ex, again”.
cascade · should-i-replycascadeShould I reply?tier · should-i-replytierGut checktier · should-i-replytierFull contextstep · ask-the-group-chatstepAsk the group chatstep · verdictstepverdictescalateescalateacceptaccept

{"message":"u up?","context":{"theirLastMessage":"5 weeks ago","timesTheyCancelledPlans":4,"howIFeel":"I finally stopped checking my phone"}}

open in studio →

Tiers#

Build each rung with tier(id, config). A tier asks exactly one question of any type, and its confidence is confidenceOf(answer): Jev's own confidence for choice and score, distance from a coin flip for noul. It accepts when that number is at least minConfidence; otherwise it escalates.

tier(id, config)
idstringNames the rung in the result and the trace. "fallback" is reserved.
askQuestionThe question this rung asks. Rungs can ask the same question or different ones.
minConfidencenumber (0–1)Accept this rung's answer at or above this confidence.
statestring | (input) => Entrydefault the inputWhat this rung shows Jev. The trick of a good cascade: give early rungs less (a template like "{{input.message}}"), later rungs more.
modelstringdefault client'sPin a model for this rung, e.g. jev-1.13.0.
titlestringLabel for UIs.

The fallback#

The fallback is a node. It runs with the cascade's input (not the tiers' answers) and its output lands in the result. A step is the usual choice, because that's where your code, and your more expensive model, lives:

refunds.ts
const decide = cascade("refund-policy", {
  tiers: [tier("quick", { ask: isRefundable, minConfidence: 0.8 })],
  fallback: step("ask-an-llm", async (ticket: Ticket, ctx) => {
    ctx.log("escalating to the expensive model");
    return llm.complete({ prompt: render(ticket), signal: ctx.signal });
  }, { timeoutMs: _000, retries: 1 }),
});

It could just as well be an emit (“a human will get back to you”), a route or a whole chain. In the graph, tiers are drawn as a ladder: dotted escalate edges climb from rung to rung and finally to the fallback.

Reading the result#

A cascade outputs a tagged union, so you can't read an answer without first checking who gave it:

nodes.ts
type CascadeResult<Tiers, F> =
  | { resolvedBy: "tier"; tier: string; answer: AnswerOf<Tiers[number]["ask"]> }
  | { resolvedBy: "fallback"; output: F };
text-them-back.ts
const next = step("verdict", (r: OutputOf<typeof decide>) => {
  if (r.resolvedBy === "fallback") return r.output;   // whatever the fallback returned
  // r.tier: which rung answered ("gut-check" | "full-context", as a string)
  // r.answer: the typed answer, ChoiceAnswer<"reply" | "leave-on-read">
  return r.answer.choice === "reply" ? "Reply." : "Leave them on read.";
});

The decision in the trace records the climb:

  • metric is "confidence", and every tier is an edge whose value is the confidence it reached (null for rungs that never ran).
  • taken is the tier id that answered, or "fallback".
  • summary spells it out, e.g. Escalated past "gut-check" (0.41); "full-context" answered at 0.78 confidence (needed 0.50).

api key

bring your own typesafe key, or ride the shared one (rate-limited, be nice).

shared key
checking…
your key
not set

your key stays in this browser (localStorage, jevchain.byok). it only travels to this site's /api/jev proxy in an x-typesafe-key header, which forwards it to typesafe and immediately forgets it. nothing is logged or stored server-side. requests on your own key get a much roomier rate limit.

keyboard shortcuts

fewer clicks, more chains. these work anywhere outside a text field.