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
.tsifrom source under Bun, test withbun test, and build a single executable in one step. - A new starter.
npm create nolahas atool-useexample that runs offline. - Editor fixes. No more language-server restarts while you type, and
import.metaworks in.tsifiles.
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)runsgetUseronce and gives you its result.ask `the user's name` [getUser]: stringoffers 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 withask.maxTurnsinnola.config.ts. - Groups. A module or an object of functions is a group.
[fileTools, dbTools]offers them asfileTools.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.
bun create nola my-appcd my-appbun src/main.tsibun --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 asNOLA2021on the file and the editor keeps the last good state. import.metainside a.tsifile no longer reportsTS1470in projects usingNodeNext.- 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
toolisnippet 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.
fallbackandroundRobinroute tool-use asks only to members that support tools, and skip the rest.
For provider and host authors
- A custom
fetchowns authentication. Giveopenai,anthropic,googleortypesafeyour ownfetch, 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_MODELbrand from@nola-lang/coreand returntoolCallson the result when the model requests calls. The vendor providers take atoolUseoption;toolUse: falsemarks a host without tool calling, so afallbackgroup 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/browserpackage 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, sorecordandreplayare 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 isNOLA1023. - 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.