create-nola-lang 0.1.11 → 0.1.13
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 +19 -13
- package/dist/agents.d.ts +13 -1
- package/dist/agents.js +107 -9
- package/dist/checkout.d.ts +20 -0
- package/dist/checkout.js +48 -1
- package/dist/{chunk-MAULRZZD.js → chunk-LUSVIOAN.js} +248 -77
- package/dist/flow.d.ts +19 -5
- package/dist/flow.js +139 -51
- package/dist/ide.d.ts +2 -2
- package/dist/ide.js +12 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -1
- package/dist/launch.d.ts +2 -0
- package/dist/launch.js +1 -0
- package/dist/main.js +1 -1
- package/dist/providers.d.ts +7 -4
- package/dist/providers.js +3 -2
- package/dist/registry.d.ts +30 -2
- package/dist/registry.js +34 -7
- package/dist/scaffold.d.ts +20 -6
- package/dist/scaffold.js +42 -13
- package/package.json +1 -1
- package/skills/nola/SKILL.md +8 -5
- package/skills/nola/references/config.md +22 -13
- package/skills/nola/references/patterns.md +92 -17
- package/skills/nola/references/pitfalls.md +66 -19
- package/skills/nola/references/syntax.md +98 -28
- package/templates/_providers/typesafe.config.ts +18 -0
- package/templates/feature-extraction/README.md +20 -0
- package/templates/feature-extraction/nola.config.ts +14 -0
- package/templates/feature-extraction/nola.replay.jsonl +2 -0
- package/templates/feature-extraction/package.json +22 -0
- package/templates/feature-extraction/src/main.tsi +22 -0
- package/templates/function-calling/README.md +21 -0
- package/templates/function-calling/nola.config.ts +14 -0
- package/templates/function-calling/nola.replay.jsonl +1 -0
- package/templates/function-calling/package.json +22 -0
- package/templates/function-calling/src/main.tsi +12 -0
- package/templates/function-calling/src/tickets.ts +17 -0
- package/templates/function-calling/tsconfig.json +12 -0
- package/templates/{starter → typescript-interop}/nola.config.ts +1 -1
- package/templates/{starter → typescript-interop}/src/person.tsi +1 -1
- package/templates/typescript-interop/tsconfig.json +12 -0
- /package/templates/{starter → feature-extraction}/tsconfig.json +0 -0
- /package/templates/{starter → typescript-interop}/README.md +0 -0
- /package/templates/{starter → typescript-interop}/nola.replay.jsonl +0 -0
- /package/templates/{starter → typescript-interop}/package.json +0 -0
- /package/templates/{starter → typescript-interop}/src/main.ts +0 -0
package/dist/registry.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
|
+
/** What a template runs unless it names its own entry: the plain-TS main. */
|
|
2
|
+
export const DEFAULT_ENTRY = "src/main.ts";
|
|
1
3
|
export const TEMPLATES = [
|
|
2
|
-
{ name: "
|
|
4
|
+
{ name: "feature-extraction", label: "extract typed data from context", source: "builtin", entry: "src/main.tsi" },
|
|
5
|
+
{
|
|
6
|
+
name: "function-calling",
|
|
7
|
+
label: "call an async function from a TypeScript file",
|
|
8
|
+
source: "builtin",
|
|
9
|
+
entry: "src/main.tsi",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "typescript-interop",
|
|
13
|
+
label: "an infer function example, imported and awaited from plain TypeScript",
|
|
14
|
+
source: "builtin",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: "triage-ticket",
|
|
18
|
+
label: "ticket triage on a non-chat model: literal unions and booleans as typed questions",
|
|
19
|
+
source: "example",
|
|
20
|
+
featured: true,
|
|
21
|
+
entry: "src/main.tsi",
|
|
22
|
+
provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" },
|
|
23
|
+
},
|
|
3
24
|
{ name: "empty", label: "nola.config + tsconfig only, bring your own code", source: "builtin" },
|
|
4
25
|
{ name: "file-ticket", label: "call intents: the model fills a function's arguments", source: "example" },
|
|
5
26
|
{ name: "extract-resume", label: "nested arrays of objects, JSDoc schema descriptions", source: "example" },
|
|
@@ -7,17 +28,23 @@ export const TEMPLATES = [
|
|
|
7
28
|
{ name: "classify-message", label: "closed label sets: union alias, string enum, inline union", source: "example" },
|
|
8
29
|
{ name: "chain-of-thought", label: "two asks sharing accumulating context", source: "example" },
|
|
9
30
|
{ name: "research-notes", label: "TS control flow orchestrating nola functions", source: "example" },
|
|
10
|
-
{
|
|
11
|
-
name: "triage-ticket",
|
|
12
|
-
label: "ticket triage: literal unions and booleans as typed questions",
|
|
13
|
-
source: "example",
|
|
14
|
-
provider: { label: "typesafe.ai", envVar: "TYPESAFE_API_KEY" },
|
|
15
|
-
},
|
|
16
31
|
];
|
|
32
|
+
/** The first menu's templates: the builtins and the featured example, in registry order. */
|
|
33
|
+
export function featuredNames() {
|
|
34
|
+
return TEMPLATES.filter((t) => t.source === "builtin" || t.featured).map((t) => t.name);
|
|
35
|
+
}
|
|
36
|
+
/** The examples behind "More examples…": every example the first menu does not carry. */
|
|
37
|
+
export function exampleNames() {
|
|
38
|
+
return TEMPLATES.filter((t) => t.source === "example" && !t.featured).map((t) => t.name);
|
|
39
|
+
}
|
|
17
40
|
export function templateByName(name) {
|
|
18
41
|
return TEMPLATES.find((t) => t.name === name);
|
|
19
42
|
}
|
|
20
43
|
export function templateNames() {
|
|
21
44
|
return TEMPLATES.map((t) => t.name);
|
|
22
45
|
}
|
|
46
|
+
/** The template's entry file — what `start` runs, launch.json runs and VS Code opens. */
|
|
47
|
+
export function entryFile(template) {
|
|
48
|
+
return (template === undefined ? undefined : templateByName(template)?.entry) ?? DEFAULT_ENTRY;
|
|
49
|
+
}
|
|
23
50
|
//# sourceMappingURL=registry.js.map
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -6,14 +6,14 @@ export interface ScaffoldResult {
|
|
|
6
6
|
}
|
|
7
7
|
export interface ScaffoldOptions {
|
|
8
8
|
name?: string;
|
|
9
|
-
/** template name; default "
|
|
9
|
+
/** template name; default "feature-extraction" */
|
|
10
10
|
template?: string;
|
|
11
11
|
/** remove existing files in a non-empty target first (set only after an interactive confirm) */
|
|
12
12
|
force?: boolean;
|
|
13
13
|
/**
|
|
14
14
|
* The inference provider chosen in the wizard; default "none" (the
|
|
15
15
|
* template's own config). Any live provider replaces `nola.config.ts` with
|
|
16
|
-
* that provider's config, skips the
|
|
16
|
+
* that provider's config, skips the offline templates' replay ledger, and renders
|
|
17
17
|
* the matching README notes.
|
|
18
18
|
*/
|
|
19
19
|
provider?: ProviderId;
|
|
@@ -28,6 +28,19 @@ export interface ScaffoldOptions {
|
|
|
28
28
|
}
|
|
29
29
|
/** The config a live provider choice writes over the template's own (`templates/_providers/<id>.config.ts`). */
|
|
30
30
|
export declare function providerConfigUrl(provider: Exclude<ProviderId, "none">): URL;
|
|
31
|
+
/**
|
|
32
|
+
* The env var a scaffold's config reads: the chosen vendor's, else — under
|
|
33
|
+
* "none" — the one the template's own config names (a pinned template such
|
|
34
|
+
* as triage-ticket). Undefined for nola (the trial key lands in `.env`
|
|
35
|
+
* itself) and for an offline scaffold.
|
|
36
|
+
*/
|
|
37
|
+
export declare function vendorEnvVar(template: string, provider: ProviderId): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* The `.env.example` a vendor scaffold gets: the key's slot, to copy to
|
|
40
|
+
* `.env` and fill in. `.env.example` is the one env file the recommended
|
|
41
|
+
* `.gitignore` keeps trackable (`!.env.example`), so it can be committed.
|
|
42
|
+
*/
|
|
43
|
+
export declare function envExample(envVar: string): string;
|
|
31
44
|
/**
|
|
32
45
|
* Add the recommended `.gitignore` to an example's files. Examples are copied
|
|
33
46
|
* verbatim from `examples/`, where the monorepo's root ignore file covers them,
|
|
@@ -35,10 +48,11 @@ export declare function providerConfigUrl(provider: Exclude<ProviderId, "none">)
|
|
|
35
48
|
*/
|
|
36
49
|
export declare function withRecommendedGitignore(files: Map<string, string>): Promise<Map<string, string>>;
|
|
37
50
|
/**
|
|
38
|
-
* The comment that opens src/main.ts
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
51
|
+
* The comment that opens the entry file (src/main.ts, or the `.tsi` entry
|
|
52
|
+
* of a one-file template) — the file the scaffold lands the user on (VS Code opens it
|
|
53
|
+
* as the active editor), so it carries the first three things to do. The VS
|
|
54
|
+
* Code variant only ships with the editor step's .vscode files, which are
|
|
55
|
+
* what make F5 and the extension prompt real.
|
|
42
56
|
*/
|
|
43
57
|
export declare function nextStepsComment(ide: "vscode" | "none", template: string): string;
|
|
44
58
|
export declare function ownVersion(): Promise<string>;
|
package/dist/scaffold.js
CHANGED
|
@@ -11,6 +11,23 @@ const TEMPLATES_DIR = fileURLToPath(new URL("../templates/", import.meta.url));
|
|
|
11
11
|
export function providerConfigUrl(provider) {
|
|
12
12
|
return new URL(`../templates/_providers/${provider}.config.ts`, import.meta.url);
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The env var a scaffold's config reads: the chosen vendor's, else — under
|
|
16
|
+
* "none" — the one the template's own config names (a pinned template such
|
|
17
|
+
* as triage-ticket). Undefined for nola (the trial key lands in `.env`
|
|
18
|
+
* itself) and for an offline scaffold.
|
|
19
|
+
*/
|
|
20
|
+
export function vendorEnvVar(template, provider) {
|
|
21
|
+
return providerById(provider)?.envVar ?? (provider === "none" ? templateByName(template)?.provider?.envVar : undefined);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The `.env.example` a vendor scaffold gets: the key's slot, to copy to
|
|
25
|
+
* `.env` and fill in. `.env.example` is the one env file the recommended
|
|
26
|
+
* `.gitignore` keeps trackable (`!.env.example`), so it can be committed.
|
|
27
|
+
*/
|
|
28
|
+
export function envExample(envVar) {
|
|
29
|
+
return `# Copy to .env and fill in — \`nola run\` applies .env before evaluating nola.config.ts.\n${envVar}=\n`;
|
|
30
|
+
}
|
|
14
31
|
/** _gitignore ships underscored (npm pack strips nested .gitignore files). */
|
|
15
32
|
const RENAMES = { _gitignore: ".gitignore" };
|
|
16
33
|
/** The ONE recommended .gitignore — every scaffold gets it; no template keeps a copy of its own. */
|
|
@@ -26,20 +43,25 @@ export async function withRecommendedGitignore(files) {
|
|
|
26
43
|
return files;
|
|
27
44
|
}
|
|
28
45
|
/** Files whose __NAME__/__VERSION__, README-note and __NEXT_STEPS__ placeholders are substituted. */
|
|
29
|
-
const SUBSTITUTED = new Set(["package.json", "README.md", "main.ts"]);
|
|
46
|
+
const SUBSTITUTED = new Set(["package.json", "README.md", "main.ts", "main.tsi"]);
|
|
30
47
|
/**
|
|
31
|
-
* The comment that opens src/main.ts
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
48
|
+
* The comment that opens the entry file (src/main.ts, or the `.tsi` entry
|
|
49
|
+
* of a one-file template) — the file the scaffold lands the user on (VS Code opens it
|
|
50
|
+
* as the active editor), so it carries the first three things to do. The VS
|
|
51
|
+
* Code variant only ships with the editor step's .vscode files, which are
|
|
52
|
+
* what make F5 and the extension prompt real.
|
|
35
53
|
*/
|
|
36
54
|
export function nextStepsComment(ide, template) {
|
|
37
55
|
if (ide === "vscode") {
|
|
38
|
-
const breakpointIn = template
|
|
56
|
+
const breakpointIn = templateByName(template)?.entry
|
|
57
|
+
? "on the `ask` line below"
|
|
58
|
+
: template === "typescript-interop"
|
|
59
|
+
? "in src/person.tsi"
|
|
60
|
+
: "in your .tsi file";
|
|
39
61
|
return [
|
|
40
62
|
"// Next steps in VS Code:",
|
|
41
63
|
"// 1. Press F5 to run this file (.vscode/launch.json is already set up).",
|
|
42
|
-
`// 2. Set a breakpoint
|
|
64
|
+
`// 2. Set a breakpoint ${breakpointIn} and press F5 again to step through the ask.`,
|
|
43
65
|
'// 3. Install the recommended "Nola" extension when VS Code offers it — IntelliSense,',
|
|
44
66
|
"// go to definition and diagnostics inside .tsi files.",
|
|
45
67
|
].join("\n");
|
|
@@ -50,14 +72,14 @@ export function nextStepsComment(ide, template) {
|
|
|
50
72
|
"// 2. Set up your editor (VS Code extension, debugging): https://nola.sh/docs/start/editor-setup/",
|
|
51
73
|
].join("\n");
|
|
52
74
|
}
|
|
53
|
-
/**
|
|
75
|
+
/** Template files that only make sense for the offline (replay) configuration. */
|
|
54
76
|
const OFFLINE_ONLY = new Set(["nola.replay.jsonl"]);
|
|
55
77
|
const README_NOTES = {
|
|
56
78
|
offline: {
|
|
57
79
|
START_NOTE: "works offline, no API key needed",
|
|
58
|
-
PROVIDER_NOTE: "
|
|
80
|
+
PROVIDER_NOTE: "This project runs offline: `nola.config.ts` replays answers from the committed\n" +
|
|
59
81
|
"`nola.replay.jsonl` ledger. The ledger is keyed by the exact prompt, so once\n" +
|
|
60
|
-
"you edit
|
|
82
|
+
"you edit the `.tsi` file or add your own asks, switch the config to a real\n" +
|
|
61
83
|
"model (see the comment in `nola.config.ts`): `model: \"nola\"` with a key from\n" +
|
|
62
84
|
"`npx nola-lang key` (25 free runs), or your own provider and its key.",
|
|
63
85
|
},
|
|
@@ -81,8 +103,8 @@ function readmeNotes(provider) {
|
|
|
81
103
|
return {
|
|
82
104
|
START_NOTE: `set ${def.envVar} in .env first`,
|
|
83
105
|
PROVIDER_NOTE: `\`nola.config.ts\` sets \`model: ${def.model}\` — ${def.label} serves inference with the key it reads\n` +
|
|
84
|
-
`from \`${def.envVar}\`.
|
|
85
|
-
|
|
106
|
+
`from \`${def.envVar}\`. Copy \`.env.example\` to \`.env\` (git-ignored; \`nola run\` applies it) and fill in\n` +
|
|
107
|
+
`\`${def.envVar}\` before the first \`npm start\`, or switch models in \`nola.config.ts\`.`,
|
|
86
108
|
};
|
|
87
109
|
}
|
|
88
110
|
export async function ownVersion() {
|
|
@@ -108,19 +130,22 @@ async function prepareTarget(absRoot, force) {
|
|
|
108
130
|
export async function scaffold(targetDir, opts = {}) {
|
|
109
131
|
const root = targetDir;
|
|
110
132
|
const absRoot = resolve(targetDir);
|
|
111
|
-
const template = opts.template ?? "
|
|
133
|
+
const template = opts.template ?? "feature-extraction";
|
|
112
134
|
const def = templateByName(template);
|
|
113
135
|
if (!def)
|
|
114
136
|
throw new Error(`unknown template "${template}" (valid: ${templateNames().join(", ")})`);
|
|
115
137
|
await prepareTarget(absRoot, opts.force ?? false);
|
|
116
138
|
const name = opts.name ?? basename(absRoot);
|
|
117
139
|
const version = await ownVersion();
|
|
140
|
+
const envVar = vendorEnvVar(template, opts.provider ?? "none");
|
|
118
141
|
if (def.source === "example") {
|
|
119
142
|
const dev = await devExamplesDir();
|
|
120
143
|
const exampleFiles = await withRecommendedGitignore(dev ? await collectExampleFromDisk(dev, template) : await fetchExampleFromGitHub(template, version));
|
|
121
144
|
const manifest = exampleFiles.get("package.json");
|
|
122
145
|
if (manifest)
|
|
123
146
|
exampleFiles.set("package.json", rewriteExamplePackageJson(manifest, { name, version }));
|
|
147
|
+
if (envVar !== undefined && !exampleFiles.has(".env.example"))
|
|
148
|
+
exampleFiles.set(".env.example", envExample(envVar));
|
|
124
149
|
for (const [relPath, content] of exampleFiles) {
|
|
125
150
|
const target = join(absRoot, relPath);
|
|
126
151
|
await mkdir(join(target, ".."), { recursive: true });
|
|
@@ -168,6 +193,10 @@ export async function scaffold(targetDir, opts = {}) {
|
|
|
168
193
|
await writeFile(join(absRoot, ".gitignore"), await readFile(GITIGNORE_URL, "utf8"));
|
|
169
194
|
files.push(".gitignore");
|
|
170
195
|
}
|
|
196
|
+
if (envVar !== undefined) {
|
|
197
|
+
await writeFile(join(absRoot, ".env.example"), envExample(envVar));
|
|
198
|
+
files.push(".env.example");
|
|
199
|
+
}
|
|
171
200
|
return { root, files: files.sort() };
|
|
172
201
|
}
|
|
173
202
|
//# sourceMappingURL=scaffold.js.map
|
package/package.json
CHANGED
package/skills/nola/SKILL.md
CHANGED
|
@@ -22,7 +22,7 @@ import type { Person } from "./types.js";
|
|
|
22
22
|
// thenable Intent<T>. `await`ing it (or `ask`) runs the inference.
|
|
23
23
|
infer function extractPerson(.text: string) {
|
|
24
24
|
// `ask` resolves an intent the way `await` resolves a promise.
|
|
25
|
-
const person = ask
|
|
25
|
+
const person = ask `Extract the person described in the text`<Person>;
|
|
26
26
|
return person;
|
|
27
27
|
}
|
|
28
28
|
```
|
|
@@ -33,10 +33,13 @@ infer function extractPerson(.text: string) {
|
|
|
33
33
|
- `.name: T` parameters are CONTEXT parameters: their values are shown to
|
|
34
34
|
the LLM. Plain (no dot) parameters are ordinary values the LLM never
|
|
35
35
|
sees. `.` is only legal on infer-function parameters. Rule of thumb:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
- `` ask
|
|
39
|
-
`${...}` interpolation works inside the backticks.
|
|
36
|
+
one dot marks a value flowing INTO the model (`.name`); a template after
|
|
37
|
+
`ask` is a value coming OUT.
|
|
38
|
+
- `` ask `prompt`<T> `` — an extractor: asks the LLM for a `T`.
|
|
39
|
+
`${...}` interpolation works inside the backticks. Anywhere that is NOT
|
|
40
|
+
directly after `ask` — a stored intent, a call-intent argument, an
|
|
41
|
+
object/array literal — spell it `` ..`prompt`<T> `` (a typed template
|
|
42
|
+
there without the dots is NOLA2014); `` ask ..`prompt` `` is still legal.
|
|
40
43
|
- `` ask fn`hint`(...) `` — or a plain call whose arguments contain an
|
|
41
44
|
extractor — is a call intent (the LLM fills the extractor-shaped
|
|
42
45
|
arguments, then the function runs). Only `` fn`hint`(...) `` carries
|
|
@@ -42,11 +42,18 @@ export default defineConfig({
|
|
|
42
42
|
`apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`.
|
|
43
43
|
|
|
44
44
|
`typesafe()` (typesafe.ai's Jev, reads `TYPESAFE_API_KEY`, model defaults to
|
|
45
|
-
`jev-latest`) is NOT a chat model: it
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
`jev-latest`) is NOT a chat model: it is the DECISION model that answers the
|
|
46
|
+
intrinsic `Choice<{…}>` / `Scale<[…]>` / `Prob` types (see syntax.md) beside
|
|
47
|
+
the plain forms — a string or number literal union, a boolean, or a flat object
|
|
48
|
+
of those — and fails definitively before the network on anything else. The
|
|
49
|
+
contextual values (`.params`, `const .x`) travel as its JSON state and each
|
|
50
|
+
property's JSDoc is that question's instructions. An ask whose type contains a
|
|
51
|
+
`Choice` / `Scale` / `Prob` on a chat model is NOLA3018 before the network, so
|
|
52
|
+
route those by name (`model: { default: openai(…), decision: typesafe() }` +
|
|
53
|
+
`ask with decision`), or use `fallback([typesafe(), openai({ model:
|
|
54
|
+
"gpt-5-mini" })])` so plain asks escalate. `threshold` (default 0.5, or
|
|
55
|
+
`.withParams({ providerOptions: { threshold } })` per ask) is the plain-boolean
|
|
56
|
+
cut-off: `true` strictly above it. Never scaffold it as the only model.
|
|
50
57
|
|
|
51
58
|
`nola` is a NAMESPACE, not a function (`nola({})` is a type error).
|
|
52
59
|
The platform model is written as the STRING `"nola"` — the Nola platform
|
|
@@ -78,9 +85,11 @@ is implied — traces go to a Nola console only when `telemetry` lists its
|
|
|
78
85
|
URL (or `nola.tracer()`).
|
|
79
86
|
|
|
80
87
|
`npm create nola` asks *Select an inference provider:* right after the
|
|
81
|
-
template —
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
template — `nola: dev` (25 free hosted runs, no API key, suited for dev experiments) first, then OpenAI / Anthropic / Gemini /
|
|
89
|
+
typesafe.ai (its row brackets the caveat: literal unions and booleans only), then
|
|
90
|
+
skip — and for `nola: dev` writes this config plus the key into `.env` (a vendor gets
|
|
91
|
+
its own config and a `.env.example` with the key's empty slot, e.g. `OPENAI_API_KEY=`,
|
|
92
|
+
to copy to `.env`; the key itself is left to the user); in an existing project `npx nola-lang
|
|
84
93
|
key` mints one (and `npx nola-lang init --add --provider nola` does that plus
|
|
85
94
|
the config and deps).
|
|
86
95
|
A machine gets one anonymous trial; later projects sign in
|
|
@@ -115,8 +124,8 @@ export default defineConfig({
|
|
|
115
124
|
|
|
116
125
|
```tsi
|
|
117
126
|
export infer function summarize(.text: string) {
|
|
118
|
-
const draft = ask with fast
|
|
119
|
-
return ask with careful
|
|
127
|
+
const draft = ask with fast `a rough summary`<string>;
|
|
128
|
+
return ask with careful `a polished summary of: ${draft}`<string>;
|
|
120
129
|
}
|
|
121
130
|
```
|
|
122
131
|
|
|
@@ -223,11 +232,11 @@ the providers package:
|
|
|
223
232
|
"check": "nola check"
|
|
224
233
|
},
|
|
225
234
|
"dependencies": {
|
|
226
|
-
"@nola-lang/providers": "^0.1.
|
|
227
|
-
"@nola-lang/runtime": "^0.1.
|
|
235
|
+
"@nola-lang/providers": "^0.1.13",
|
|
236
|
+
"@nola-lang/runtime": "^0.1.13"
|
|
228
237
|
},
|
|
229
238
|
"devDependencies": {
|
|
230
|
-
"nola-lang": "^0.1.
|
|
239
|
+
"nola-lang": "^0.1.13",
|
|
231
240
|
"typescript": "^5.6.0"
|
|
232
241
|
},
|
|
233
242
|
"engines": { "node": ">=22.18" }
|
|
@@ -1,10 +1,85 @@
|
|
|
1
1
|
# Nola patterns — worked examples
|
|
2
2
|
|
|
3
|
-
## The
|
|
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 as the FIRST statement is
|
|
7
|
+
the instruction for the whole file, and `const .x` bindings are context the
|
|
8
|
+
model sees at every ask that follows them (`npm create nola` scaffolds this
|
|
9
|
+
shape as `feature-extraction`):
|
|
10
|
+
|
|
11
|
+
```tsi
|
|
12
|
+
// src/main.tsi — run with: nola run src/main.tsi
|
|
13
|
+
`You read short professional bios. Answer from the text alone; never invent facts.`
|
|
14
|
+
|
|
15
|
+
interface Person {
|
|
16
|
+
name: string;
|
|
17
|
+
age: number;
|
|
18
|
+
employer: string;
|
|
19
|
+
job: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const .message = "Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.";
|
|
23
|
+
|
|
24
|
+
const person = ask `the person described in the text`<Person>;
|
|
25
|
+
|
|
26
|
+
// declared after the first ask, so only the second ask sees it — one answer feeds the next
|
|
27
|
+
const .role = person.job;
|
|
28
|
+
const seniority = ask `the seniority level the role implies`<"junior" | "mid" | "senior" | "staff">;
|
|
29
|
+
|
|
30
|
+
console.log(JSON.stringify({ ...person, seniority }));
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Rules that matter here: the instruction literal must be the very first
|
|
34
|
+
statement (a comment before it is fine, an `import` or a type is not); a
|
|
35
|
+
`.` binding is visible to the asks declared after it in the same or an
|
|
36
|
+
enclosing block, never to its own initializer; `ask` at the top level is
|
|
37
|
+
legal in the module body and top-level blocks/loops, not inside a plain
|
|
38
|
+
function or callback (NOLA2001). When a script outgrows one file, move the
|
|
39
|
+
asks into an `infer function` and call it from plain TypeScript — the shape
|
|
40
|
+
below.
|
|
41
|
+
|
|
42
|
+
## The function-calling project: a top-level call intent
|
|
43
|
+
|
|
44
|
+
The same one-file shape, with the other feature (`npm create nola` scaffolds
|
|
45
|
+
it as `function-calling`): the top-level `ask` is a CALL INTENT — the model
|
|
46
|
+
fills the extractor-shaped arguments of an ordinary async function that lives
|
|
47
|
+
in a plain `.ts` file next door, and the call runs with them.
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
// src/tickets.ts — plain TypeScript, nothing Nola about it
|
|
51
|
+
export interface Ticket { id: string; title: string; priority: number }
|
|
52
|
+
const tickets: Ticket[] = [];
|
|
53
|
+
export async function createTicket(title: string, priority: number): Promise<Ticket> {
|
|
54
|
+
const ticket = { id: `T-${tickets.length + 1}`, title, priority };
|
|
55
|
+
tickets.push(ticket);
|
|
56
|
+
return ticket;
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```tsi
|
|
61
|
+
// src/main.tsi — run with: nola run src/main.tsi
|
|
62
|
+
import { createTicket } from "./tickets.js";
|
|
63
|
+
|
|
64
|
+
const .message = "Hi, I can't log in since this morning and I have a customer demo in an hour — please help!";
|
|
65
|
+
|
|
66
|
+
const ticket = ask createTicket(..`a short ticket title`<string>, ..`priority 1-5, where 1 is most urgent`<number>);
|
|
67
|
+
|
|
68
|
+
console.log(JSON.stringify(ticket));
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`ask` yields the callee's SETTLED value — a `Ticket`, not a `Promise<Ticket>`.
|
|
72
|
+
The import uses the NodeNext `./tickets.js` specifier for the on-disk
|
|
73
|
+
`tickets.ts`. Note there is no first-line instruction here: an `import` is a
|
|
74
|
+
statement, so a template literal placed after it is not the file's first
|
|
75
|
+
statement and would be a no-op.
|
|
76
|
+
|
|
77
|
+
## The typescript-interop project, end to end
|
|
78
|
+
|
|
79
|
+
This is the shape a Nola library or app takes: `.tsi` files hold the infer
|
|
6
80
|
functions, a plain `.ts` entry point calls them, and `nola run` executes the
|
|
7
|
-
entry with the loader and `nola.config.ts` in place
|
|
81
|
+
entry with the loader and `nola.config.ts` in place (`npm create nola`
|
|
82
|
+
scaffolds it as `typescript-interop`).
|
|
8
83
|
|
|
9
84
|
```
|
|
10
85
|
my-app/
|
|
@@ -27,7 +102,7 @@ export interface Person {
|
|
|
27
102
|
}
|
|
28
103
|
|
|
29
104
|
export infer function extractPerson(.message: string) {
|
|
30
|
-
const person = ask
|
|
105
|
+
const person = ask `the person described in the text`<Person>;
|
|
31
106
|
return person;
|
|
32
107
|
}
|
|
33
108
|
```
|
|
@@ -69,8 +144,8 @@ export infer function solve(.problem: string) {
|
|
|
69
144
|
// Both asks see `problem` (the contextual parameter). They do NOT see each
|
|
70
145
|
// other's answers automatically — the reasoning is handed to the second ask
|
|
71
146
|
// explicitly through `${}`.
|
|
72
|
-
const reasoning = ask
|
|
73
|
-
const answer = ask
|
|
147
|
+
const reasoning = ask `think step by step about the problem before answering`;
|
|
148
|
+
const answer = ask `the final numeric answer, given this reasoning: ${reasoning}`<number>;
|
|
74
149
|
return { reasoning, answer };
|
|
75
150
|
}
|
|
76
151
|
```
|
|
@@ -83,8 +158,8 @@ later prompt:
|
|
|
83
158
|
export type Category = "billing" | "refund" | "fraud" | "other";
|
|
84
159
|
|
|
85
160
|
export infer function classifyMessage(.message: string) {
|
|
86
|
-
const category = ask
|
|
87
|
-
const urgent = ask
|
|
161
|
+
const category = ask `the category of the customer message`<Category>;
|
|
162
|
+
const urgent = ask `does the message need urgent attention`<"yes" | "no">;
|
|
88
163
|
|
|
89
164
|
// plain TS from here on
|
|
90
165
|
if (category === "fraud") return { category, urgent: true, escalate: true };
|
|
@@ -103,11 +178,11 @@ export interface Conclusion {
|
|
|
103
178
|
}
|
|
104
179
|
|
|
105
180
|
export infer function nextQuery(.question: string, .notes: string[]) {
|
|
106
|
-
return ask
|
|
181
|
+
return ask `the single best search query to advance the research; keywords only`<string>;
|
|
107
182
|
}
|
|
108
183
|
|
|
109
184
|
export infer function conclude(.question: string, .notes: string[]) {
|
|
110
|
-
return ask
|
|
185
|
+
return ask `answer the research question using only the collected notes`<Conclusion>;
|
|
111
186
|
}
|
|
112
187
|
```
|
|
113
188
|
|
|
@@ -204,7 +279,7 @@ export interface Invoice {
|
|
|
204
279
|
}
|
|
205
280
|
|
|
206
281
|
export infer function extractInvoice(.document: string) {
|
|
207
|
-
return ask
|
|
282
|
+
return ask `the invoice data from the document`<Invoice>;
|
|
208
283
|
}
|
|
209
284
|
```
|
|
210
285
|
|
|
@@ -221,9 +296,9 @@ export enum Sentiment {
|
|
|
221
296
|
}
|
|
222
297
|
|
|
223
298
|
export infer function triage(.message: string) {
|
|
224
|
-
const category = ask
|
|
225
|
-
const sentiment = ask
|
|
226
|
-
const urgent = ask
|
|
299
|
+
const category = ask `the category of the customer message`<Category>;
|
|
300
|
+
const sentiment = ask `the overall sentiment of the message`<Sentiment>;
|
|
301
|
+
const urgent = ask `does the message need urgent attention`<"yes" | "no">;
|
|
227
302
|
return { category, sentiment, urgent: urgent === "yes" };
|
|
228
303
|
}
|
|
229
304
|
```
|
|
@@ -248,7 +323,7 @@ export interface Person {
|
|
|
248
323
|
import type { Person } from "./models.js";
|
|
249
324
|
|
|
250
325
|
export infer function extractPerson(.text: string) {
|
|
251
|
-
return ask
|
|
326
|
+
return ask `the person described in the text`<Person>;
|
|
252
327
|
}
|
|
253
328
|
```
|
|
254
329
|
|
|
@@ -272,7 +347,7 @@ export type TreeNode = {
|
|
|
272
347
|
};
|
|
273
348
|
|
|
274
349
|
export infer function parseTree(.input: string) {
|
|
275
|
-
return ask
|
|
350
|
+
return ask `the tree structure described in the input`<TreeNode>;
|
|
276
351
|
}
|
|
277
352
|
```
|
|
278
353
|
|
|
@@ -282,7 +357,7 @@ export infer function parseTree(.input: string) {
|
|
|
282
357
|
export type CalendarEvent = { title: string; at: Date };
|
|
283
358
|
|
|
284
359
|
export infer function nextEvent(.calendar: string) {
|
|
285
|
-
const event = ask
|
|
360
|
+
const event = ask `the next event on the calendar`<CalendarEvent>;
|
|
286
361
|
const when: Date = event.at; // a Date, not a string
|
|
287
362
|
return when;
|
|
288
363
|
}
|