skip to content
docs / start here

Questions.

Three question types, typed answers, and one number for “how sure”.

The three types#

Everything Jev can be asked is one of three shapes. They mirror TypeSafe's /v1/systemone wire format exactly, so a question built with these helpers is sent to the API untouched. The helpers exist to validate your input and carry your labels through to the answer types.

choice(instructions, labels)ChoiceQuestion<L>Pick exactly one label. 2–255 options. Answers with the winning label, a probability per label and a confidence.
score(instructions, levels)ScoreQuestionRate on an ordered rubric, lowest level first. 2–10 levels. Answers with a probability-weighted level that can land between levels.
noul(instructions, criteria?)NoulQuestionYes or no. Answers with noul: the probability of yes, 0–1.

instructions is an Entry: a string, a JSON object, a JSON array or null. The same goes for each label's or level's description. Bad shapes throw a TypeError right away, when the question is built, not three nodes into a run.

choice#

Pass an array when the labels speak for themselves, or an object when Jev would benefit from a description. Descriptions are the cheapest accuracy you'll ever buy.

choice.ts
import { choice } from "jevchain";

// labels only: criteria are null
choice("Which team?", ["billing", "bug", "vibes"]);

// labels with descriptions: Jev reads them
choice("Which team?", {
  billing: "money, invoices, refunds",
  bug: "something is broken",
  vibes: "no actionable content, just vibes",
});

The answer's choice is the highest-probability label, and probabilities covers every label and sums to 1:

{
  "type": "choice",
  "choice": "billing",
  "probabilities": { "billing": 0.94, "bug": 0.05, "vibes": 0.01 },
  "confidence": 0.81
}

score#

Levels are ordered, and the index is the level: 0 is the first one. Instead of picking one, Jev spreads probability across the levels, and score is the probability-weighted average. That makes it a smooth number you can put a threshold on.

score.ts
import { score } from "jevchain";

score("How spicy is this take?", [
  "mild",                        // level 0
  "medium",                      // level 1
  "call the fire department",    // level 2
]);
{
  "type": "score",
  "score": 1.62,
  "probabilities": { "0": 0.08, "1": 0.22, "2": 0.70 },
  "legend": { "0": "mild", "1": "medium", "2": "call the fire department" },
  "confidence": 0.37
}

legend maps each level index back to its description, so a trace can be read on its own without the chain next to it.

noul#

A yes/no question, answered as p(yes). The optional criteria tell Jev what counts as true and what counts as false, which helps a lot on questions that humans would also argue about.

question
import { noul } from "jevchain";

noul("Is the user blocked right now?");

noul("Is the user blocked right now?", {
  true: "cannot do their job",
  false: "mild inconvenience",
});
answer
{
  "type": "noul",
  "noul": 0.87
}

Inferred answer types#

Choice labels are captured as a const type parameter, so answers come back typed with your literal labels, not string. Put questions in an ask and its output type has one answer per key, each typed for its question.

types.ts
const read = ask("read", {
  questions: {
    mood: choice("Mood?", ["cursed", "blessed"]),
    spice: score("Spice?", ["mild", "hot"]),
    drama: noul("Is there drama?"),
  },
});

type Out = OutputOf<typeof read>;
// {
//   mood:  ChoiceAnswer<"cursed" | "blessed">;
//   spice: ScoreAnswer;
//   drama: NoulAnswer;
// }

type Mood = Out["mood"]["choice"]; // "cursed" | "blessed"

step("react", (a: Out) => {
  if (a.mood.choice === "cursd") {} // ✗ no overlap with "cursed" | "blessed"
  return a.mood.probabilities.blessed; // number
});
  • AnswerOf<Q> maps a question type to its answer type. Answers<Qs> does it for a whole question object.
  • LabelsOf<Q> pulls out a choice's label union, for when you want it on its own.
▶ run itPlant wellness checkdocs chainchains.ts ↗
One ask with one question of each type. Jev reads the plant's complaint once and answers all three questions in a single call.
chains.ts
const plantCheck = ask("plant-check", {
  title: "Plant wellness check",
  questions: {
    // choice: pick one label. The labels become a literal union type.
    mood: choice("How is this houseplant doing?", {
      thriving: "new leaves, upright, smug",
      thirsty: "drooping, crispy edges, dry soil",
      dramatic: "fine, but making a scene about it",
    }),
    // score: an ordered rubric, lowest first. You get a probability-weighted level.
    drama: score("How dramatic is this plant being?", [
      "stoic",
      "sighing",
      "wilting theatrically",
      "writing its will",
    ]),
    // noul: yes or no. You get p(yes).
    overwatered: noul("Has this plant been overwatered?"),
  },
});
ask · plant-checkaskPlant wellness check

Dropped three leaves overnight. Soil is soggy. Pot has no drainage hole.

open in studio →

Confidence#

Every answer can say how sure it is on the same 0–1 scale. Choice and score answers carry Jev's own confidence, where 0 means the distribution was uniform and 1 means all-in on one option. Nouls don't carry one, so confidenceOf measures the distance from a coin flip: 0.5 maps to 0, and 0 or 1 maps to 1.

confidence.ts
import { confidenceOf } from "jevchain";

confidenceOf({ type: "choice", choice: "bug", probabilities: { bug: 0.5, billing: 0.5 }, confidence: 0 }); // 0
confidenceOf({ type: "noul", noul: 0.5 });  // 0    (a coin flip)
confidenceOf({ type: "noul", noul: 0.95 }); // 0.9  (|0.95 − 0.5| × 2)
confidenceOf({ type: "noul", noul: 0.02 }); // 0.96 (sure it's a no)

Where confidence is used#

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.