← Blog

Nola 0.1.14: context statements

  • release

Welcome to Nola 0.1.14. Here are the new features and improvements we think you’ll like most:

  • Context statements. Give the model instructions anywhere in your code, and show it live values from your program.
  • Colon types. Type an ask the same way you type a variable: ask `the ticket id`: string.
  • Simpler function calls. Let the model fill in a function’s arguments without the .. prefix.
  • A prompt you can read. The model gets your own words in four plain tags, and nothing else.
  • A new starter. npm create nola now includes a small agent in a single file.
  • Better debugging. Breakpoints stop where you put them.

A few older forms were removed along the way. If you are coming from 0.1.13, read Upgrading before you update.

Language

Context statements

Until now, a function or a file could carry a single instruction for the model. Real programs need more: different guidance at different points, and a way to show the model what the program has worked out so far.

In this release, any template literal on a line of its own, in an infer function or at the top level of a file, is a context statement. Every ask below it sees the text. Add a value after the text, and the model sees that value as it is at the moment of the ask.

`You triage a support inbox. Suggest one step at a time.`
const .ticket = "Since yesterday our nightly export job fails with \"disk quota exceeded\".";
const steps: string[] = [];
`Steps taken so far:` steps;
while (steps.length < 3) {
`Answer "done" when nothing is left to do.`
const next = ask `the next step`: string;
if (next === "done") break;
steps.push(next);
}
console.log(steps);

Each time the loop asks, the model sees the current list of steps. There is no prompt string to rebuild and no history to manage. You update the array, and the next ask sees it.

A few things worth knowing:

  • Scoped like a const. A context statement applies to the asks below it, inside the same braces. The “done” line above reaches only the asks inside the loop.
  • Never repeated. Every ask reads each statement once, fresh. Three passes through the loop do not put three copies in front of the model.
  • File-wide at the top. A statement at the top of a file also applies to the infer functions declared below it.
  • Your words, not data. The model follows a context statement as an instruction, so keep it to text you control. Anything that comes from a user or a document belongs in a .param or a const .x binding, like .ticket above. The model is told to treat those as data.

Prefer ${…}? It now works in every instruction. `Page ${oncall} first.` and `Page` oncall `first.` mean the same thing.

Read more in Context statements.

Type an ask with a colon

You can now write the type of an ask after a colon, the way you annotate a variable:

const .message = "Hi, I'm Ada Lovelace. I'm 36 and I work as a mathematician.";
const name = ask `the person's name`: string;
const age = ask `the person's age`: number;
console.log(name, age);

The angle-bracket form, ask `the person's name`<string>, still works and means exactly the same, so your existing code needs no change. The docs, the examples and the editor snippets use the colon from now on.

Keep the angle brackets when you chain a method straight off the ask, as in ask `the person's name`<string>.withRetry(2).

Function calls without the dots

When you ask the model to fill in a function’s arguments, you no longer need the .. prefix on each one. Write the prompt and its type right where the value would go:

const .request = "Checkout shows a blank page after paying, and I was charged twice.";
function createTicket(title: string, priority: number) {
console.log(`[p${priority}] ${title}`);
}
ask createTicket(
`a short ticket title`: string,
`priority 1-5, where 1 is most urgent`: number,
);

The model fills in both arguments, and then createTicket runs with them. You can pass ordinary values too: ask createTicket(`a short ticket title`: string, 2) asks the model for the title only. Code that uses the dots keeps working.

A prompt you can read

Nola no longer adds wording of its own to your prompt. The model receives the text you wrote, wrapped in four tags that say what each part is:

Tag What it holds
<context> One function or file: its context statements, then its inputs
<input> One .param or const .x value, by name
<task> The ask itself
<correction> What was wrong with a rejected reply, when Nola retries

Here is what the third ask of the triage loop above sends:

<context module="src/triage.tsi">
You triage a support inbox. Suggest one step at a time.
Steps taken so far: ["check the disk quota on the export host","clear last month's export archives"]
Answer "done" when nothing is left to do.
<input name="ticket">
Since yesterday our nightly export job fails with "disk quota exceeded".
</input>
</context>
<task>
the next step
</task>

That is the whole message. The answer’s type is not in it, because it travels through the provider’s structured output instead. The system message is four short sentences, and you can replace them with system.message in nola.config.ts.

If your server cannot enforce a schema, set structuredOutputs: false on the provider, and Nola writes the schema into the system message.

The prompt has more examples.

Tooling

A new starter: the agent loop

npm create nola now opens with five ready-made examples and an empty project. The new one, agent-loop, is a small agent in a single file: a while loop that keeps asking until the model answers null, with a context statement that shows the model what it has found so far. It runs offline on a mock provider, so you can try it without an API key.

Two smaller changes: the empty template now starts you in src/main.tsi, and example templates are downloaded from GitHub when you pick one, so you need to be online to scaffold them.

nola run exits like node

nola run now ends when your program is done, the way node does. A script that starts a server keeps running, and the last traces reach the console before the process exits.

Debugging

  • A breakpoint on an ask line could be skipped when a multi-line instruction sat above it. It now stops where you put it.
  • You can put a breakpoint on a context statement. Inside a loop, it pauses once on every pass.
  • Stepping over an ask with F10 takes you to the next line of your program.

Editor

Completion lists and unused-variable warnings no longer show the __nola names that Nola generates. The VS Code extension highlights colon types, and its snippets write the new forms.

Custom providers

If you maintain your own provider, there is now one method to implement: infer. It receives the ask as data. Call renderPrompt for the default prompt, send it to your backend, and return the reply:

my-provider.ts
import type { LanguageModel } from "@nola-lang/runtime";
import { renderPrompt } from "@nola-lang/providers";
export const myProvider: LanguageModel = {
name: "my-provider",
async infer(req) {
const sent = renderPrompt(req.intent); // { system, messages }
const text = await callMyBackend(sent, req.intent.output, req.signal);
return { text, sent };
},
};

Here callMyBackend stands for your own request code, and req.intent.output carries the schema of the answer. Return sent, and the receipt shows exactly what went out. Providers API has a complete example.

Upgrading

Update every Nola package to 0.1.14 together, then rebuild your project. Code built with 0.1.13 does not run on the new runtime.

Then look for these, which were removed in this release:

  • An instruction after a function’s name, infer function f`…`(…). Move the text into the body as its first line. The compiler reports each one as NOLA1019.
  • ${.member} in an instruction. Delete it. The value already reaches the model as an <input> block.
  • ..choice, ..scale and ..prob. Write the type instead: ask `Which team?`: Choice<{ billing: "Payments"; sales: "Pricing" }>. See Decision types.
  • complete in a custom provider. Rename it to infer and get the text from renderPrompt(req.intent). In a mockProvider function, read req.prompt instead of req.payload.
  • Replay ledgers recorded with 0.1.13. Record them again. The request format changed, so the old answers no longer match and replay fails with NOLA3008.

Everything else keeps working, including <T> types and .. in call arguments.

Questions or feedback? Join us on Discord or open an issue on GitHub.