Intents
An intent describes work to run later. Calling an infer function returns an Intent<T>; resolving it produces a T. You can store an intent, pass it to another function or configure it before it runs.
The analogy
Section titled “The analogy”async/await |
Nola |
|---|---|
async function f(): Promise<T> |
infer function f(): Intent<T> |
a Promise<T> — a value standing for a T that is not here yet |
an Intent<T> — a value standing for a T the model has not produced yet |
await p resolves the promise to its T |
ask i resolves the intent to its T |
the body returns T; the caller sees Promise<T> |
the body returns T; the caller sees Intent<T> |
| a promise is thenable | an Intent<T> is thenable — plain TypeScript awaits it |
export infer function summarize(.text: string) { return ask `a one-sentence summary`: string;}import { summarize } from "./summarize.tsi";
const intent = summarize("The quarterly report shows revenue up 12% on lower costs."); // Intent<string> — nothing has runconst summary = await intent; // now it runsconsole.log(summary);Two moments: construction and resolution
Section titled “Two moments: construction and resolution”Calling an async function starts its body immediately. Calling an infer function leaves the body unstarted until you resolve its intent. There are two steps:
- Construction captures the request: an infer function call captures its arguments, while an extractor evaluates ordinary
${…}substitutions and records its target type. This does not contact a provider. - Resolution runs the work through
ask, or throughawaitfor an infer function’s intent. The runtime composes the prompt using the context available then, calls the provider and validates the answer.
You can prepare several intents and resolve only the ones you need:
export infer function triage(.ticket: string) { const severity = ..`the severity: low, medium or high`: "low" | "medium" | "high"; // constructed, not run const owner = ..`the team that should own this ticket`: string; // constructed, not run
const s = ask severity; // one provider call if (s === "low") return { severity: s, owner: "queue" }; return { severity: s, owner: ask owner }; // a second call, only on this branch}The low branch returns without making a provider request for owner.
What produces an intent
Section titled “What produces an intent”There are three ways to create an intent:
| Expression | Produces | Tier |
|---|---|---|
calling an infer function — summarize(text) |
the function’s result | Intent<T> |
an extractor — ..`instruction`: T (the dots are optional directly after ask and in a call’s argument list) |
a request to pull a T from context |
Askable<T> |
a call intent — fn`hint`(…) or a plain call with an extractor argument |
a request to fill the arguments, then call fn |
Askable<T> |
| Tier | What it can do |
|---|---|
Askable<T> |
resolvable only through ask; carries .withRetry(n), .withModel(…), .withParams({…}), .withTimeout(ms) |
Intent<T> extends Askable<T>, PromiseLike<T> |
everything above, plus .detached() and then — so plain TypeScript can await it |
An infer function’s intent includes enough information to start an invocation: its function, instruction, source file and contextual parameters. An extractor or call intent needs an existing invocation and uses the context of the ask that resolves it. Only Intent<T> supports await. See Intent methods for configuration and Intent API for signatures.
What resolves an intent
Section titled “What resolves an intent”| Where you are | Use | Accepts |
|---|---|---|
| an infer function body | ask |
any intent — it runs on the asking invocation |
| plain TypeScript | await |
an Intent<T> — it opens a fresh invocation |
A raw extractor or call intent cannot be bare-awaited: with no invocation to borrow, the runtime throws NOLA3010. From plain TypeScript, await the infer function’s result instead — see ask vs await.
Intent<T> works with APIs that accept thenables. For example, Promise.all resolves several infer-function intents concurrently:
import { summarize } from "./summarize.tsi";
const [a, b] = await Promise.all([ summarize("Revenue is up 12% on lower costs."), summarize("Churn fell to 2% after the pricing change."),]);console.log(a, b);A failed inference rejects at the resolution site, exactly as a rejected promise does at await: wrap the ask or the await in try/catch. See Error handling.
Properties
Section titled “Properties”These properties determine when an intent runs and how it can be reused.
Nothing runs until something resolves the intent. An intent that is never asked never reaches a provider — the triage example above depends on that.
Single-shot
Section titled “Single-shot”An intent resolves at most once and retains its result. Resolving it again does not repeat the work. This is useful when two callers need the same answer, but a shared intent can accidentally reuse an answer from an earlier invocation:
const nameIntent = ..`the person's full name`: string; // one intent for the life of the module
export infer function whoIsIt(.text: string) { return ask nameIntent; // the first invocation runs it; every later invocation gets that FIRST answer}Build intents where you use them, or behind a function that returns a fresh one:
const nameIntent = () => ..`the person's full name`: string;
export infer function whoIsIt(.text: string) { return ask nameIntent(); // a new intent per invocation}Methods clone
Section titled “Methods clone”Every .with* method returns a new, unstarted intent with the option merged in; the original is untouched. Chain what you need, then resolve the result:
export infer function tuned(.text: string) { const base = ..`the title of the text`: string; const careful = base.withModel("careful").withRetry(2); // two clones; base is still unstarted return ask careful;}Thenable, not a Promise
Section titled “Thenable, not a Promise”Intent<T> implements PromiseLike<T>. Its then method makes it work with await, Promise.all, Promise.race and async return values. It has no catch or finally methods. Use try/catch around await, or wrap it in Promise.resolve(intent) when you need those methods.
For annotations and runtime checks:
- Annotate an infer function with
Intent<T>, neverPromise<T>—nola checkreportsTS2739otherwise (return type). isIntent(value)from@nola-lang/runtimeis the runtime guard for “is this any kind of intent” (Intent API).
Where the analogy ends
Section titled “Where the analogy ends”| A promise… | An intent… |
|---|---|
runs its executor immediately when created with new Promise |
leaves inference unstarted until ask or await |
| has no built-in model or inference settings | carries model, retry, parameter and timeout settings |
| resolves the same wherever it is awaited | resolves in the invocation that asks it: the ask site lends its context, history and provider pin to the intent |
| can be awaited anywhere | as an Askable<T>, can be resolved only by ask — inside an infer function or at the top of a module |
Context is supplied when an intent resolves. An extractor created in one function can therefore use another function’s context:
import type { Askable } from "@nola-lang/runtime";
export infer function answer(.document: string, question: Askable<string>) { return ask question; // resolves against answer's context — the document}
export infer function headline(.text: string) { const question = ..`a headline for the document`: string; // built here, no document in sight return ask answer(text, question); // resolved there, with the document}Intents are ordinary values: store them, pass them, return them from helpers. Only ask is position-restricted — an infer function body or the module body (NOLA2001); construction is legal anywhere.
Next: infer functions