Skip to content

Running and planning

An expression is a value until you run it. Building one sends nothing; await or .run() sends the requests; .plan() describes them without sending.

import { from } from "jevlish";
import { tickets } from "./support.js";
import { disruption, reportsProblem } from "./vocabulary.js";
const query = from(tickets)
.seenAs((t) => ({ subject: t.subject, body: t.body }))
.where((t) => t.status === "open")
.and(reportsProblem)
.rankedBy(disruption)
.take(5);
// Nothing has been sent yet. The query is a value.
const plan = query.plan();
plan.subjectCount; // 3
plan.decidedByCode; // 1: the closed ticket never reaches the model
plan.questionCount; // one Noul and one Score for each of the other two
plan.requestCount; // 2
plan.requests[0]?.state; // exactly what leaves the process for the first subject
plan.notes; // ["filter: ...", 'ranked by "...", highest first', "take 5"]
// Now spend.
const result = await query;
result.scored;

A Plan has:

  • subjectCount: how many subjects the expression covers (one for given, the list length for from).
  • decidedByCode: how many of them code predicates settled with no request.
  • questionCount and requestCount: what would be asked, and how it batches.
  • requests: each request’s state (exactly what the model would see) and questions.
  • notes: a description of the filter, ranking, and limit on a query.

Every runnable expression has .plan(): predicates, branches, queries, measurements, choices, and ask. On a branch, plan() does not run any handler.

await expression and await expression.run() do the same thing. Each call runs the expression again: a second request, or a cache hit if a cache is configured and nothing changed. Expressions are not memoized.

import { given } from "jevlish";
import type { Ticket } from "./support.js";
import { blocked } from "./vocabulary.js";
// Build expressions in plain functions so callers can still .plan() them.
export const isBlocked = (t: Ticket) => given(t).seenAs((x) => ({ body: x.body })).when(blocked);
isBlocked({ id: "T-1", status: "open", subject: "s", body: "b" }).plan(); // fine
// An async function awaits the expression for you: the caller gets a
// Promise<Judgment<boolean>> and can no longer plan it.
export const isBlockedNow = async (t: Ticket) => given(t).when(blocked);
// .run() and await do the same thing; each call runs the expression again.
const expression = isBlocked({ id: "T-1", status: "open", subject: "s", body: "b" });
const first = await expression.run();
const second = await expression; // a second request (or a cache hit)
first.status === second.status;

An async function awaits its return value. A function that returns given(t).when(blocked) hands the caller an expression they can .plan(), .run(), or .withPolicy() further. An async function that returns the same thing hands them a Promise<Judgment<boolean>> that is already in flight. If callers need to inspect before spending, build expressions in plain functions and let the caller decide when to await.

Most application code should let TypeScript infer expression types. Libraries that need to name a builder type can import the advanced classes from jevlish/types; they are kept out of the root entry point so the common API stays focused on verbs, question builders, and results.

A SenseError means the expression itself is malformed or incomplete: an empty proposition, .and() before .where(), a branch awaited without .whenUncertain(), a scale with one level. Transport failures from the underlying SDK propagate as they are. A thrown error is a failed request, never evidence about the proposition; it does not become uncertain.