create-nola-lang 0.1.13 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/add.d.ts +3 -0
- package/dist/add.js +2 -1
- package/dist/checkout.js +1 -0
- package/dist/{chunk-LUSVIOAN.js → chunk-UTVGG44K.js} +124 -95
- package/dist/examples.d.ts +4 -2
- package/dist/examples.js +4 -2
- package/dist/flow.d.ts +20 -3
- package/dist/flow.js +49 -17
- package/dist/github.d.ts +1 -1
- package/dist/github.js +1 -1
- package/dist/index.js +1 -1
- package/dist/launch.d.ts +2 -2
- package/dist/launch.js +1 -1
- package/dist/main.js +1 -1
- package/dist/node-version.d.ts +3 -2
- package/dist/node-version.js +4 -3
- package/dist/package-manager.d.ts +5 -0
- package/dist/package-manager.js +7 -0
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +1 -1
- package/dist/registry.d.ts +21 -12
- package/dist/registry.js +27 -5
- package/dist/scaffold.d.ts +36 -16
- package/dist/scaffold.js +96 -116
- package/package.json +1 -1
- package/skills/nola/SKILL.md +72 -17
- package/skills/nola/references/config.md +35 -6
- package/skills/nola/references/patterns.md +129 -31
- package/skills/nola/references/pitfalls.md +318 -56
- package/skills/nola/references/syntax.md +345 -121
- package/templates/empty/package.json +5 -5
- package/templates/empty/src/main.tsi +8 -0
- package/templates/empty/src/main.ts +0 -5
- package/templates/feature-extraction/README.md +0 -20
- package/templates/feature-extraction/nola.config.ts +0 -14
- package/templates/feature-extraction/nola.replay.jsonl +0 -2
- package/templates/feature-extraction/package.json +0 -22
- package/templates/feature-extraction/src/main.tsi +0 -22
- package/templates/feature-extraction/tsconfig.json +0 -12
- package/templates/function-calling/README.md +0 -21
- package/templates/function-calling/nola.config.ts +0 -14
- package/templates/function-calling/nola.replay.jsonl +0 -1
- package/templates/function-calling/package.json +0 -22
- package/templates/function-calling/src/main.tsi +0 -12
- package/templates/function-calling/src/tickets.ts +0 -17
- package/templates/function-calling/tsconfig.json +0 -12
- package/templates/typescript-interop/README.md +0 -15
- package/templates/typescript-interop/nola.config.ts +0 -14
- package/templates/typescript-interop/nola.replay.jsonl +0 -1
- package/templates/typescript-interop/package.json +0 -22
- package/templates/typescript-interop/src/main.ts +0 -8
- package/templates/typescript-interop/src/person.tsi +0 -11
- package/templates/typescript-interop/tsconfig.json +0 -12
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
## The feature-extraction project: one .tsi file is the program
|
|
4
4
|
|
|
5
5
|
The smallest Nola program is a single `.tsi` file run directly — `ask` is
|
|
6
|
-
legal at the top level, a bare template literal
|
|
7
|
-
|
|
6
|
+
legal at the top level, a bare template literal on its own line is a context
|
|
7
|
+
statement every ask below it sees, and `const .x` bindings are context the
|
|
8
8
|
model sees at every ask that follows them (`npm create nola` scaffolds this
|
|
9
9
|
shape as `feature-extraction`):
|
|
10
10
|
|
|
@@ -21,17 +21,17 @@ interface Person {
|
|
|
21
21
|
|
|
22
22
|
const .message = "Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.";
|
|
23
23
|
|
|
24
|
-
const person = ask `the person described in the text
|
|
24
|
+
const person = ask `the person described in the text`: Person;
|
|
25
25
|
|
|
26
26
|
// declared after the first ask, so only the second ask sees it — one answer feeds the next
|
|
27
27
|
const .role = person.job;
|
|
28
|
-
const seniority = ask `the seniority level the role implies
|
|
28
|
+
const seniority = ask `the seniority level the role implies`: "junior" | "mid" | "senior" | "staff";
|
|
29
29
|
|
|
30
30
|
console.log(JSON.stringify({ ...person, seniority }));
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Rules that matter here:
|
|
34
|
-
|
|
33
|
+
Rules that matter here: a context statement applies to the asks written
|
|
34
|
+
after it, wherever it stands (an `import` or a type before it is fine); a
|
|
35
35
|
`.` binding is visible to the asks declared after it in the same or an
|
|
36
36
|
enclosing block, never to its own initializer; `ask` at the top level is
|
|
37
37
|
legal in the module body and top-level blocks/loops, not inside a plain
|
|
@@ -63,16 +63,19 @@ import { createTicket } from "./tickets.js";
|
|
|
63
63
|
|
|
64
64
|
const .message = "Hi, I can't log in since this morning and I have a customer demo in an hour — please help!";
|
|
65
65
|
|
|
66
|
-
const ticket = ask createTicket(
|
|
66
|
+
const ticket = ask createTicket(
|
|
67
|
+
`a short ticket title`: string,
|
|
68
|
+
`priority 1-5, where 1 is most urgent`: number
|
|
69
|
+
);
|
|
67
70
|
|
|
68
71
|
console.log(JSON.stringify(ticket));
|
|
69
72
|
```
|
|
70
73
|
|
|
71
74
|
`ask` yields the callee's SETTLED value — a `Ticket`, not a `Promise<Ticket>`.
|
|
72
75
|
The import uses the NodeNext `./tickets.js` specifier for the on-disk
|
|
73
|
-
`tickets.ts`. Note there is no
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
`tickets.ts`. Note there is no context statement here: a template literal on
|
|
77
|
+
its own line after the `import` would be one, and every ask below it would
|
|
78
|
+
see it.
|
|
76
79
|
|
|
77
80
|
## The typescript-interop project, end to end
|
|
78
81
|
|
|
@@ -102,7 +105,7 @@ export interface Person {
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
export infer function extractPerson(.message: string) {
|
|
105
|
-
const person = ask `the person described in the text
|
|
108
|
+
const person = ask `the person described in the text`: Person;
|
|
106
109
|
return person;
|
|
107
110
|
}
|
|
108
111
|
```
|
|
@@ -129,8 +132,8 @@ nola build # dist/ — plain JS + source maps + .d.ts
|
|
|
129
132
|
|
|
130
133
|
## Composing several asks in one invocation
|
|
131
134
|
|
|
132
|
-
Every `ask` in one invocation shares that invocation's context — the
|
|
133
|
-
contextual parameters and the function's
|
|
135
|
+
Every `ask` in one invocation shares that invocation's context — the `.`
|
|
136
|
+
contextual parameters and the function's context statements. That is what lets
|
|
134
137
|
you split one big prompt into several small, individually-typed asks instead of
|
|
135
138
|
demanding everything at once.
|
|
136
139
|
|
|
@@ -145,7 +148,7 @@ export infer function solve(.problem: string) {
|
|
|
145
148
|
// other's answers automatically — the reasoning is handed to the second ask
|
|
146
149
|
// explicitly through `${}`.
|
|
147
150
|
const reasoning = ask `think step by step about the problem before answering`;
|
|
148
|
-
const answer = ask `the final numeric answer, given this reasoning: ${reasoning}
|
|
151
|
+
const answer = ask `the final numeric answer, given this reasoning: ${reasoning}`: number;
|
|
149
152
|
return { reasoning, answer };
|
|
150
153
|
}
|
|
151
154
|
```
|
|
@@ -158,8 +161,8 @@ later prompt:
|
|
|
158
161
|
export type Category = "billing" | "refund" | "fraud" | "other";
|
|
159
162
|
|
|
160
163
|
export infer function classifyMessage(.message: string) {
|
|
161
|
-
const category = ask `the category of the customer message
|
|
162
|
-
const urgent = ask `does the message need urgent attention
|
|
164
|
+
const category = ask `the category of the customer message`: Category;
|
|
165
|
+
const urgent = ask `does the message need urgent attention`: "yes" | "no";
|
|
163
166
|
|
|
164
167
|
// plain TS from here on
|
|
165
168
|
if (category === "fraud") return { category, urgent: true, escalate: true };
|
|
@@ -178,11 +181,11 @@ export interface Conclusion {
|
|
|
178
181
|
}
|
|
179
182
|
|
|
180
183
|
export infer function nextQuery(.question: string, .notes: string[]) {
|
|
181
|
-
return ask `the single best search query to advance the research; keywords only
|
|
184
|
+
return ask `the single best search query to advance the research; keywords only`: string;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
export infer function conclude(.question: string, .notes: string[]) {
|
|
185
|
-
return ask `answer the research question using only the collected notes
|
|
188
|
+
return ask `answer the research question using only the collected notes`: Conclusion;
|
|
186
189
|
}
|
|
187
190
|
```
|
|
188
191
|
|
|
@@ -228,7 +231,7 @@ import { createTicket } from "./tickets.js";
|
|
|
228
231
|
export infer function fileTicket(.request: string) {
|
|
229
232
|
// Sigil-less: the extractor argument makes this call an intent. `2` is a
|
|
230
233
|
// plain argument and is passed through untouched.
|
|
231
|
-
const id = ask createTicket(
|
|
234
|
+
const id = ask createTicket(`a short ticket title for the request`: string, 2);
|
|
232
235
|
return id;
|
|
233
236
|
}
|
|
234
237
|
```
|
|
@@ -239,14 +242,15 @@ spelling that carries a hint:
|
|
|
239
242
|
```tsi
|
|
240
243
|
export infer function fileTicketCarefully(.request: string) {
|
|
241
244
|
return ask createTicket`file the ticket exactly as the customer described it`(
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
`a short ticket title`: string,
|
|
246
|
+
`priority 1-5, where 1 is most urgent`: number,
|
|
244
247
|
);
|
|
245
248
|
}
|
|
246
249
|
```
|
|
247
250
|
|
|
248
|
-
Every extractor slot needs an explicit
|
|
249
|
-
resolve together in a
|
|
251
|
+
Every extractor slot needs an explicit type (an untyped template in the
|
|
252
|
+
arguments is a plain string), and all slots of one call resolve together in a
|
|
253
|
+
single provider round trip.
|
|
250
254
|
|
|
251
255
|
`createTicket` is async, but `id` is a `string`, not a `Promise<string>` — a
|
|
252
256
|
call intent awaits a promise-returning callee itself (`ask` ≈ `await`), so
|
|
@@ -254,9 +258,103 @@ call intent awaits a promise-returning callee itself (`ask` ≈ `await`), so
|
|
|
254
258
|
ask, `.withRetry(n)` on a call intent re-invokes it on failure — only use it
|
|
255
259
|
when the target is idempotent.
|
|
256
260
|
|
|
261
|
+
## The tool-use project: offer functions, let the model call them
|
|
262
|
+
|
|
263
|
+
The same one-file shape for the third feature (`npm create nola` scaffolds
|
|
264
|
+
it as `tool-use`): the top-level `ask` offers two plain-TypeScript functions
|
|
265
|
+
as tools. The model calls them as it needs — here it lists a folder, reads
|
|
266
|
+
a file to be sure, then answers — and the typed answer is what `ask` yields.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// src/tools.ts — plain TypeScript; the JSDoc is the contract the model sees
|
|
270
|
+
/**
|
|
271
|
+
* Lists the files under a directory on disc D.
|
|
272
|
+
* @param dir an absolute directory path, such as D:/docs
|
|
273
|
+
* @param pattern keeps only the paths that contain it
|
|
274
|
+
*/
|
|
275
|
+
export async function searchFiles(dir: string, pattern?: string): Promise<string[]> { /* … */ return []; }
|
|
276
|
+
|
|
277
|
+
/** Reads a file's text. */
|
|
278
|
+
export async function readFile(path: string): Promise<string> { /* … */ return ""; }
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
```tsi
|
|
282
|
+
// src/main.tsi — run with: nola run src/main.tsi
|
|
283
|
+
import { readFile, searchFiles } from "./tools.js";
|
|
284
|
+
|
|
285
|
+
const files = ask `the files on disc D that belong to a US visa application` [searchFiles, readFile]: string[];
|
|
286
|
+
|
|
287
|
+
console.log(JSON.stringify(files));
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Parentheses run a function once with model-filled arguments (the
|
|
291
|
+
function-calling project); brackets offer functions the model may call zero
|
|
292
|
+
or more times. Cap the loop with `.withMaxTurns(n)` when a task should not
|
|
293
|
+
wander; return a failure as a value (`{ error: "no such file" }`) when the
|
|
294
|
+
model should recover from it, since a thrown error ends the ask.
|
|
295
|
+
|
|
296
|
+
## Data in place: a bare value in the instruction
|
|
297
|
+
|
|
298
|
+
When the model needs a value beside the words that explain it, write the
|
|
299
|
+
value bare between texts (an ask sequence): it reaches the model as a
|
|
300
|
+
named input block where you wrote it. A `${}` hole splices the value into
|
|
301
|
+
your words instead — pick by what the model should treat it as.
|
|
302
|
+
|
|
303
|
+
```tsi
|
|
304
|
+
// src/main.tsi
|
|
305
|
+
interface Person {
|
|
306
|
+
name: string;
|
|
307
|
+
}
|
|
308
|
+
declare const project: { id: string; lead: string };
|
|
309
|
+
declare const policy: string;
|
|
310
|
+
|
|
311
|
+
// data where it stands: <input name="project"> … </input>, then your next words
|
|
312
|
+
export const owner = ask `the person responsible for` project `given this policy` policy: Person;
|
|
313
|
+
|
|
314
|
+
// words: the id is spliced into the sentence
|
|
315
|
+
export const lead = ask `the lead of project ${project.id}`: string;
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The same shape puts words on a call (`` ask `register the applicant` createUser(`user id`: string) ``)
|
|
319
|
+
and words between tool lists (`` ask `find` [searchFiles] `or, when stale,` [readDir] `done`: string[] ``).
|
|
320
|
+
The kind follows the parts, never the data: texts and data extract, a call
|
|
321
|
+
with a typed slot is function calling, lists are tool use.
|
|
322
|
+
|
|
323
|
+
## An agent loop with a live context statement
|
|
324
|
+
|
|
325
|
+
A context statement is read at EACH ask that sees it, so one placed before a
|
|
326
|
+
loop shows the model the current state on every pass — no re-prompting code:
|
|
327
|
+
|
|
328
|
+
```tsi
|
|
329
|
+
type Problem = { brief: string; solved: boolean };
|
|
330
|
+
|
|
331
|
+
export infer function solveAll(.query: string) {
|
|
332
|
+
`Solve every problem in the query, one at a time.`
|
|
333
|
+
const solved: Problem[] = [];
|
|
334
|
+
`Already solved:` solved.map((p) => p.brief);
|
|
335
|
+
while (solved.length < 5) {
|
|
336
|
+
const next = ask `the next unsolved problem, or a problem with solved: true when none is left`: Problem;
|
|
337
|
+
if (next.solved) break;
|
|
338
|
+
solved.push(next);
|
|
339
|
+
}
|
|
340
|
+
return solved;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Every `ask` inside the loop sees `Already solved: ["…", "…"]` with the list as
|
|
345
|
+
it is at that moment. The value is your words (JSON), not an `<input>` block;
|
|
346
|
+
use `const .solved` instead if the model should treat it as data.
|
|
347
|
+
|
|
348
|
+
The statement is rendered once per ask, not once per pass: five passes do not
|
|
349
|
+
put five copies in front of the model, and the model never sees a history of
|
|
350
|
+
earlier renderings. A statement written INSIDE the loop body is scoped to
|
|
351
|
+
those braces like a `const` — only the asks inside them see it, and an ask
|
|
352
|
+
after the loop does not. What changes from pass to pass is the value a
|
|
353
|
+
statement reads, never the set of statements.
|
|
354
|
+
|
|
257
355
|
## Typing the answers
|
|
258
356
|
|
|
259
|
-
Prefer a NAMED, exported `type` or `interface`
|
|
357
|
+
Prefer a NAMED, exported `type` or `interface` as the extractor's type over an inline object
|
|
260
358
|
literal: it documents the contract, it is reusable from plain TS, and JSDoc
|
|
261
359
|
comments on its members become descriptions in the schema the LLM sees.
|
|
262
360
|
|
|
@@ -279,7 +377,7 @@ export interface Invoice {
|
|
|
279
377
|
}
|
|
280
378
|
|
|
281
379
|
export infer function extractInvoice(.document: string) {
|
|
282
|
-
return ask `the invoice data from the document
|
|
380
|
+
return ask `the invoice data from the document`: Invoice;
|
|
283
381
|
}
|
|
284
382
|
```
|
|
285
383
|
|
|
@@ -296,9 +394,9 @@ export enum Sentiment {
|
|
|
296
394
|
}
|
|
297
395
|
|
|
298
396
|
export infer function triage(.message: string) {
|
|
299
|
-
const category = ask `the category of the customer message
|
|
300
|
-
const sentiment = ask `the overall sentiment of the message
|
|
301
|
-
const urgent = ask `does the message need urgent attention
|
|
397
|
+
const category = ask `the category of the customer message`: Category;
|
|
398
|
+
const sentiment = ask `the overall sentiment of the message`: Sentiment;
|
|
399
|
+
const urgent = ask `does the message need urgent attention`: "yes" | "no";
|
|
302
400
|
return { category, sentiment, urgent: urgent === "yes" };
|
|
303
401
|
}
|
|
304
402
|
```
|
|
@@ -323,7 +421,7 @@ export interface Person {
|
|
|
323
421
|
import type { Person } from "./models.js";
|
|
324
422
|
|
|
325
423
|
export infer function extractPerson(.text: string) {
|
|
326
|
-
return ask `the person described in the text
|
|
424
|
+
return ask `the person described in the text`: Person;
|
|
327
425
|
}
|
|
328
426
|
```
|
|
329
427
|
|
|
@@ -347,7 +445,7 @@ export type TreeNode = {
|
|
|
347
445
|
};
|
|
348
446
|
|
|
349
447
|
export infer function parseTree(.input: string) {
|
|
350
|
-
return ask `the tree structure described in the input
|
|
448
|
+
return ask `the tree structure described in the input`: TreeNode;
|
|
351
449
|
}
|
|
352
450
|
```
|
|
353
451
|
|
|
@@ -357,7 +455,7 @@ export infer function parseTree(.input: string) {
|
|
|
357
455
|
export type CalendarEvent = { title: string; at: Date };
|
|
358
456
|
|
|
359
457
|
export infer function nextEvent(.calendar: string) {
|
|
360
|
-
const event = ask `the next event on the calendar
|
|
458
|
+
const event = ask `the next event on the calendar`: CalendarEvent;
|
|
361
459
|
const when: Date = event.at; // a Date, not a string
|
|
362
460
|
return when;
|
|
363
461
|
}
|