ask()
One Jev call, any number of questions about the same input. Outputs the typed answers.
One call, many questions#
ask sends its questions about the node's input to Jev in one request and outputs the answers, keyed like the questions. Jev reads the state once and answers every question in parallel, so five questions cost roughly what one does. Ask for everything you need up front.
import { ask, choice, noul, score } from "jevchain";
const read = ask("read-the-pr", {
title: "Read the PR",
questions: {
clarity: score("How clearly does the description explain the change?", [
"no description", "vague", "clear", "exemplary",
]),
tests: noul("Does the PR add or update tests?"),
scope: choice("What kind of change is this?", ["typo", "feature", "refactor", "migration"]),
},
});The output is a plain object of answers, one per key:
{
"clarity": { "type": "score", "score": 1.9, "probabilities": { "0": 0.02, "1": 0.2, "2": 0.64, "3": 0.14 },
"legend": { "0": "no description", ... }, "confidence": 0.31 },
"tests": { "type": "noul", "noul": 0.93 },
"scope": { "type": "choice", "choice": "feature",
"probabilities": { "typo": 0.01, "feature": 0.88, "refactor": 0.08, "migration": 0.03 },
"confidence": 0.62 }
}| name | type | default | what it does |
|---|---|---|---|
| id | string | Names the node in traces and graphs. Needn't be unique. | |
| questions | Questions | Named questions from choice, score and noul. At least one. | |
| state | string | (input) => Entry | default the input | What Jev reads. See below. |
| model | string | default client's model | Pin this node to a model, e.g. "jev-1.13.0". |
| title / description | string | Labels for UIs and traces. |
Choosing the state#
By default Jev sees the node's input. Often you want it to see less (just the message, not the metadata), or something reshaped. state takes one of three forms:
// 1. omitted: the node's input, as-is (strings stay strings, objects go as JSON)
ask("a", { questions });
// 2. a template: one hole keeps the raw value, so objects stay structured
ask("b", { questions, state: "{{input.messages}}" });
// 2b. text with holes: values are stringified into the text
ask("c", { questions, state: "From {{input.user}}: {{input.text}}" });
// 3. a function: full control (serialized as a $ref)
ask("d", { questions, state: (pr: PullRequest) => ({ title: pr.title, body: pr.body }) });- Templates are paths only:
{{input.user.name}},{{input.items.0}}. No expressions, noeval. Besidesinputyou can reachrun(the run's original input) andresults(finished nodes' outputs, by id). - Whatever you end up with is coerced into something Jev accepts:
nullandundefinedbecomenull, numbers and booleans become strings, objects are made JSON-safe.
Typed output#
The output type is Answers<Q>: each key maps to the answer type of its question, and choice labels stay literal unions. Pull it out with OutputOf and the next step is checked end to end.
import { chain, step, type OutputOf } from "jevchain";
type Read = OutputOf<typeof read>;
const verdict = step("verdict", (a: Read) =>
a.scope.choice === "migration" && a.tests.noul < 0.5
? "no tests on a migration. bold."
: `clarity ${a.clarity.score.toFixed(1)} / 3`,
);
export const review = chain("review", read, verdict);Rename a label in the choice and the comparison in verdict stops compiling. That's the idea.
ask, then a step that weighs them into a risk number. a.scope.choice indexes a lookup table by label, and the compiler knows every label exists.{"title":"quick fix for users table","description":"drops the legacy_email column, should be fine. deploying friday evening so it's quiet","filesChanged":1}
When to reach for it#
ask is for when you want the numbers, not a branch. Rule of thumb:
- You'll combine several answers in code (weights, lookup tables, a formula): use
askand a step. - One choice picks what happens next: use route. It asks and branches in one node, and the trace records the decision.
- One number has to clear a bar: use gate.
- Independent reads that each deserve their own node (or their own state): several asks under a parallel. Same state still means one request.
ask vs. alsoAsk#
Routes and gates take alsoAsk: extra questions that ride in the same call and are recorded in the trace but don't affect the branch. Use it for “I'll want to know this later” signals like sentiment. When you need to use the answers downstream, use an ask.
parallel, all over the same chat. Same state, same tick: the client sends one request.{"me":"Sam","messages":[{"from":"Priya","text":"so are we still doing brunch sunday"},{"from":"Sam","text":"can't this week sorry!!"},{"from":"Jordan","text":"classic"},{"from":"Priya","text":"no worries. we'll just pla…