← Blog

Nola 0.1.15: tool use

  • release

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

  • Tool use. Put functions in brackets after the task. The model calls them as it needs, and the typed answer ends the loop.
  • Values inside an ask. Name the data right where your sentence mentions it, and spread a long ask over several lines.
  • Bun. Run .tsi from source under Bun, test with bun test, and build a single executable in one step.
  • A new starter. npm create nola has a tool-use example that runs offline.
  • Editor fixes. No more language-server restarts while you type, and import.meta works in .tsi files.

One spelling changed: a typed ask inside a ternary now needs parentheses. See Upgrading.

Language

Tool use

Until now, the model could fill in a function’s arguments, and Nola would run that function once. Many tasks need more than one step: look something up, read what came back, maybe look again, and only then answer.

Now you can offer functions to the model. Write them in brackets between the task and the type:

const disc: Record<string, string> = {
"D:/docs/visa/ds-160.pdf": "DS-160 confirmation for a US visa application",
"D:/docs/visa/passport.pdf": "Passport scan, valid until 2031",
"D:/docs/taxes/2025.pdf": "Tax return for 2025",
};
/** Lists the files under a directory on disc D. */
async function searchFiles(dir: string) {
return Object.keys(disc).filter((path) => path.startsWith(dir));
}
/** Reads a file's text. */
async function readFile(path: string) {
return disc[path] ?? "";
}
const files = ask `the files on disc D that belong to a US visa application` [searchFiles, readFile]: string[];
console.log(files);

The model may call searchFiles and readFile as often as it needs, in the order it chooses. Nola runs each call in your process, hands the result back to the model and asks again. The typed answer ends the loop, and it is the value of the ask, validated like any other.

There is nothing extra to write for a tool. Its signature and JSDoc become the contract the model sees: here searchFiles is offered under that name, described as “Lists the files under a directory on disc D.”, with one required string argument called dir. Add @param tags to describe the arguments. Results go back to the model as text.

A few things worth knowing:

  • Brackets offer, parentheses run. ask getUser(`the id`: string) runs getUser once and gives you its result. ask `the user's name` [getUser]: string offers it, and gives you the model’s answer.
  • The loop is bounded. Ten turns by default. Change it for one ask with .withMaxTurns(n), or for every ask with ask.maxTurns in nola.config.ts.
  • Groups. A module or an object of functions is a group. [fileTools, dbTools] offers them as fileTools.searchFiles; [...fileTools] offers them under their own names.
  • Errors stay loud. A tool that throws ends the ask. A wrong tool name or bad arguments go back to the model to correct, and never reach your function.
  • Every call is on the record. The receipt lists each turn with its arguments, result and duration, and the console shows them under the ask.

Tool use works with the OpenAI, Anthropic and Gemini providers. For tests, give mockProvider the { tools: true } option and script the turns with toolCalls. Read more in Tool use.

Values inside an ask

An ask can now carry data right where your sentence mentions it. Write a value between two texts, and the model sees it there, as an input block under the name you wrote:

const ticket = { id: "T-4821", subject: "Charged twice after checkout" };
const policy = "Refund within 14 days, no questions asked.";
const reply = ask `a reply to this ticket` ticket `that follows our policy` policy: string;
console.log(reply);

This is the task the model receives:

<task>
a reply to this ticket
<input name="ticket">
{
"id": "T-4821",
"subject": "Charged twice after checkout"
}
</input>
that follows our policy
<input name="policy">
Refund within 14 days, no questions asked.
</input>
</task>

The same works for tool lists and calls. Several lists can share one ask, with your words between them saying when each applies: ask `find the files using` [searchFiles, readFile] `or, when the index is stale,` [readDir] `and return their paths`: string[]. And a call can take words in front of it: ask `file the ticket the customer asked for` createTicket(`a short title`: string, 2) means the same as the hint-marker form, which keeps working.

A long ask can span lines, one part per line, with the type on a line of its own. A line break alone never ends an ask. The colon before the type no longer has to touch the backtick either, so ask `p` : number is fine. See Extractors.

Bun

Nola now runs on Bun. bun create nola gives you a Bun project: .tsi files load from source through the @nola-lang/bun plugin, and the start script is bun src/main.tsi.

Terminal window
bun create nola my-app
cd my-app
bun src/main.tsi

bun --watch and bun test work the same way, so your tests can import .tsi modules directly. To ship, pass the plugin to Bun.build and get a bundle or a single executable from .tsi source in one step. Output from nola build also runs under bun with nothing added.

Bun 1.3 or newer is required. The nola command line (check, build, console) and F5 debugging in VS Code still run on Node. See Bun.

Tooling

A new starter: tool use

npm create nola now offers tool-use on its first menu: a tool-use ask in one .tsi file, with two plain TypeScript functions from a neighbouring tools.ts as the tools. It replays a recorded three-turn conversation, so it runs offline with no API key. Pick it, run it, then switch the config to a real model and watch the turns in the console.

Faster parsing

Parsing .tsi is about 1.7 times faster. A build-time switch in the parser was being read once per syntax node, and it is now settled when the parser is bundled.

Editor

  • The language server no longer restarts in a loop when a call has an empty argument while you type, as in f(, 1). An internal compiler error now shows as NOLA2021 on the file and the editor keeps the last good state.
  • import.meta inside a .tsi file no longer reports TS1470 in projects using NodeNext.
  • Format Document lays an ask out as an ask: one space between its parts, the colon glued to the type, and parts on their own lines indented from the ask.
  • Highlighting covers tool lists, spaced colons, asks that span lines and comments between the parts. A new tooli snippet writes a tool-use ask.

Fixes

  • In a bundler’s watch mode (Vite, webpack, Next.js), adding a field to a type no longer leaves the build with the old schema or an “unsupported type any” error.
  • fallback and roundRobin route tool-use asks only to members that support tools, and skip the rest.

For provider and host authors

  • A custom fetch owns authentication. Give openai, anthropic, google or typesafe your own fetch, and no API key is required: the fetch adds whatever a relay or proxy needs, and no vendor header is sent without a key.
  • Tool use in a custom provider. Carry the TOOL_MODEL brand from @nola-lang/core and return toolCalls on the result when the model requests calls. The vendor providers take a toolUse option; toolUse: false marks a host without tool calling, so a fallback group routes tool-use asks around it.
  • Browser-ready libraries. The compiler, runtime and providers no longer import Node’s built-in modules at load time, so they bundle for a browser without shims. The new @nola-lang/browser package has what a webview or a playground needs on top: a type strip that keeps the debug map, and module URLs for the lowered code. Replay ledgers still need a file system, so record and replay are Node-only.

See Providers API.

Upgrading

Update every Nola package to 0.1.15 together, then rebuild your project. Replay ledgers recorded with 0.1.14 keep working: the request format has not changed.

Two things to check:

  • A typed ask as a ternary’s consequent. Parenthesize it: cond ? (ask `p`: T) : fallback. The colon after an ask is now always its type, so the bare form is NOLA1023.
  • Node 22.3 or newer for the runtime packages. The toolchain already required 22.18, so this only matters for a deployment that pins an older 22.

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