create-nola-lang 0.1.3 → 0.1.4

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.
@@ -1 +1 @@
1
- {"version":3,"file":"skill-install.js","sourceRoot":"","sources":["../src/skill-install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAgB,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5G,OAAO,EAAiB,aAAa,EAAE,MAAM,WAAW,CAAC;AAczD,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAsB,EAAE,OAA4B,EAAE;IAC1F,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC;IAC5B,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;YACxD,QAAQ,GAAG,aAAa,EAAE,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,QAAQ,GAAG,aAAa,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,IAAI,MAAiB,CAAC;IACtB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;SAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,oFAAoF,CAAC,CAAC;QACpG,OAAO,CAAC,CAAC;IACX,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,uBAAuB,EAAE,aAAa,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QACtG,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,GAAG,MAAmB,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;QACtD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACnD,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,MAAM,OAAO,GACX,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,gCAAgC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,uBAAuB,CAAC;IAChH,IAAI,QAAQ,CAAC,KAAK;QAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;;QACvC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO,CAAC,CAAC;AACX,CAAC"}
1
+ {"version":3,"file":"skill-install.js","sourceRoot":"","sources":["../src/skill-install.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAgB,aAAa,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC5G,OAAO,EAAiB,aAAa,EAAE,MAAM,WAAW,CAAC;AAgBzD,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAsB,EAAE,OAA4B,EAAE;IAC1F,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC;IAC5B,IAAI,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC;IAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,IAAI,WAAW,EAAE,CAAC;YAChB,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAC;YACxD,QAAQ,GAAG,aAAa,EAAE,CAAC;QAC7B,CAAC;aAAM,CAAC;YACN,QAAQ,GAAG,aAAa,EAAE,CAAC;QAC7B,CAAC;IACH,CAAC;IAED,IAAI,MAAiB,CAAC;IACtB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,GAAG,eAAe,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;SAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,oFAAoF,CAAC,CAAC;QACpG,OAAO,CAAC,CAAC;IACX,CAAC;SAAM,CAAC;QACN,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,uBAAuB,EAAE,aAAa,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;QACtG,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC5B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,GAAG,MAAmB,CAAC;IAC/B,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,QAAQ,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;QACtD,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;IAC1E,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,OAAO;QAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,OAAO,GAAG,uBAAuB,CAAC;IACtC,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,GAAG,gCAAgC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;SAC5F,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,GAAG,kEAAkE,CAAC;IACpG,IAAI,QAAQ,CAAC,KAAK;QAAE,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;;QACvC,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5B,OAAO,CAAC,CAAC;AACX,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nola-lang",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Scaffold a new Nola project (npm create nola, or npm create nola-lang)",
5
5
  "keywords": [
6
6
  "nola",
@@ -38,7 +38,8 @@
38
38
  "files": [
39
39
  "dist",
40
40
  "!dist/.tsbuildinfo",
41
- "templates"
41
+ "templates",
42
+ "skills"
42
43
  ],
43
44
  "scripts": {
44
45
  "bundle": "node scripts/bundle.mjs"
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: nola
3
+ description: Writing Nola .tsi files — a TypeScript superset with infer
4
+ functions and ask extractors. Use when creating or editing .tsi files or
5
+ nola.config.ts.
6
+ ---
7
+
8
+ # Writing Nola
9
+
10
+ Nola is a TypeScript superset. `.tsi` files are NOT valid TypeScript — the
11
+ Nola toolchain lowers them to plain TS before tsc, bundlers, or Node see
12
+ them. Everything you know about TypeScript applies inside a `.tsi` file
13
+ EXCEPT the constructs below. A configured LLM resolves the new constructs at
14
+ run time.
15
+
16
+ ## The constructs at a glance
17
+
18
+ ```tsi
19
+ import type { Person } from "./types.js";
20
+
21
+ // An LLM-backed function: lowers to a plain function returning a lazy,
22
+ // thenable Intent<T>. `await`ing it (or `ask`) runs the inference.
23
+ infer function extractPerson(.text: string) {
24
+ // `ask` resolves an intent the way `await` resolves a promise.
25
+ const person = ask ..`Extract the person described in the text`<Person>;
26
+ return person;
27
+ }
28
+ ```
29
+
30
+ - `infer function name(...)` — declares a nola function. An optional
31
+ backtick instruction goes between name and params:
32
+ `` infer function name`instruction`(...) ``. `await` is legal in the body.
33
+ - `.name: T` parameters are CONTEXT parameters: their values are shown to
34
+ the LLM. Plain (no dot) parameters are ordinary values the LLM never
35
+ sees. `.` is only legal on infer-function parameters. Rule of thumb:
36
+ ONE dot in (`.name` — the value flows into the model), TWO dots out
37
+ (`` ..`prompt` `` — a value comes out of it).
38
+ - `` ask ..`prompt`<T> `` — an extractor: asks the LLM for a `T`.
39
+ `${...}` interpolation works inside the backticks.
40
+ - `` ask fn`hint`(...) `` — or a plain call whose arguments contain an
41
+ extractor — is a call intent (the LLM fills the extractor-shaped
42
+ arguments, then the function runs). Only `` fn`hint`(...) `` carries
43
+ instruction text.
44
+ - `ask with <providerName> <intent>` — routes one ask through a named
45
+ provider from `nola.config.ts`. The name must be a static identifier.
46
+ - Prompt templates: inside ANY instruction backticks (marker, extractor,
47
+ call hint) a hole that starts with a single dot — `${.member}` — reads
48
+ the intent's prompt scope; a literal containing one REPLACES that
49
+ intent's prompt block (`${.default}` is the built-in block, `${.next}`
50
+ the rest of the prompt). Every other `${expr}` is a lexical value.
51
+ See `references/syntax.md` → "Prompt templates".
52
+
53
+ ## Where Nola diverges from TypeScript — hard rules
54
+
55
+ - `ask` is a reserved word in `.tsi` (still legal as a member/property
56
+ name). `infer` is contextual: a keyword only directly before `function`.
57
+ - Identifiers starting with `__nola` are reserved. Never write them.
58
+ - Imports of `.tsi` files keep the literal extension:
59
+ `import { f } from "./x.tsi"`. Imports of plain TS use NodeNext style:
60
+ `import { g } from "./y.js"` (the `.js` extension, even though the source
61
+ file is `.ts`).
62
+ - An extractor's `<T>` should be a named, structurally simple type
63
+ (object/array/primitive fields). `Date` works (revived to a real `Date`);
64
+ `Map`/`Set` and other ambient lib types do not.
65
+ - Raw extract/call intents are resolved with `ask`, never bare `await`
66
+ (that throws NOLA3010). Infer-function RETURN values may be awaited.
67
+
68
+ ## References
69
+
70
+ Read these before non-trivial work:
71
+
72
+ - `references/syntax.md` — full grammar and semantics of every construct
73
+ - `references/patterns.md` — worked examples (extraction, call intents)
74
+ - `references/config.md` — `nola.config.ts`, providers, project layout
75
+ - `references/pitfalls.md` — common errors (NOLA codes) and their fixes
@@ -0,0 +1,227 @@
1
+ # `nola.config.ts`, providers, and project layout
2
+
3
+ ## The import surface is FROZEN — use exactly these two specifiers
4
+
5
+ ```ts
6
+ import { defineConfig } from "@nola-lang/runtime";
7
+ import { openai, mockProvider, withRetry } from "@nola-lang/providers";
8
+ ```
9
+
10
+ `defineConfig` and everything app-facing come from `@nola-lang/runtime`.
11
+ Everything provider-shaped — provider factories (`openai`, `anthropic`,
12
+ `google`, `mockProvider`), resilience combinators (`withRetry`, `fallback`,
13
+ `roundRobin`, `constant`, `exponential`) and record/replay (`record`,
14
+ `replay`) — comes from `@nola-lang/providers`.
15
+
16
+ Never import providers from the runtime, never import `defineConfig` from the
17
+ providers package, and never reach for a subpath (`@nola-lang/runtime/config`,
18
+ `@nola-lang/runtime/providers`, `nola-lang/runtime`) — those do not exist.
19
+ `@nola-lang/providers` deliberately does not depend on the runtime, which is
20
+ what keeps a second copy of the runtime out of the install tree.
21
+
22
+ ## Minimal config
23
+
24
+ `nola.config.ts` lives at the project root and default-exports a
25
+ `defineConfig` call. `providers.default` is required:
26
+
27
+ ```ts
28
+ import { openai } from "@nola-lang/providers";
29
+ import { defineConfig } from "@nola-lang/runtime";
30
+
31
+ export default defineConfig({
32
+ providers: {
33
+ // Reads OPENAI_API_KEY from the environment at the first ask.
34
+ default: openai({ model: "gpt-5-mini" }),
35
+ },
36
+ });
37
+ ```
38
+
39
+ `openai({ model })` requires an explicit model; optional fields are `apiKey`,
40
+ `apiKeyEnv` (default `"OPENAI_API_KEY"`), `baseUrl` and `fetch`.
41
+
42
+ ### Named providers become `ask with <name>` targets
43
+
44
+ Every other key of `providers` is a name you choose, and that name is exactly
45
+ what `ask with` and `.withProvider()` accept:
46
+
47
+ ```ts
48
+ import { anthropic, openai } from "@nola-lang/providers";
49
+ import { defineConfig } from "@nola-lang/runtime";
50
+
51
+ export default defineConfig({
52
+ providers: {
53
+ default: openai({ model: "gpt-5-mini" }),
54
+ fast: openai({ model: "gpt-5-nano" }),
55
+ careful: anthropic({ model: "claude-sonnet-4-5" }),
56
+ },
57
+ });
58
+ ```
59
+
60
+ ```tsi
61
+ export infer function summarize(.text: string) {
62
+ const draft = ask with fast ..`a rough summary`<string>;
63
+ return ask with careful ..`a polished summary of: ${draft}`<string>;
64
+ }
65
+ ```
66
+
67
+ An `ask with` name that is not a key of the map fails at run time with
68
+ NOLA3004, listing the configured names.
69
+
70
+ ### Other config sections
71
+
72
+ ```ts
73
+ import { mockProvider, openai } from "@nola-lang/providers";
74
+ import { defineConfig } from "@nola-lang/runtime";
75
+
76
+ export default defineConfig({
77
+ providers: {
78
+ default: openai({ model: "gpt-5-mini" }),
79
+ mock: mockProvider(() => ({ ok: true })),
80
+ },
81
+ // hermetic override — EVERY ask goes here, even .withProvider()-pinned ones
82
+ forceProvider: process.env.CI ? "mock" : undefined,
83
+ observability: { logLevel: "info" },
84
+ ask: { timeoutMs: 60_000 }, // per-invocation timeout; 0 disables
85
+ system: { message: "Answer in British English." },
86
+ compiler: { underivableContextType: "error" }, // "error" | "prune" | "omit"
87
+ build: { target: "app" }, // "app" (default) | "lib"
88
+ });
89
+ ```
90
+
91
+ Keep `compiler.underivableContextType` a LITERAL value — the editor reads it
92
+ statically and cannot execute your config, so a computed value is invisible to
93
+ it.
94
+
95
+ ### Resilience combinators
96
+
97
+ Combinators wrap a provider and return a provider, so they nest:
98
+
99
+ ```ts
100
+ import { exponential, fallback, openai, withRetry } from "@nola-lang/providers";
101
+ import { defineConfig } from "@nola-lang/runtime";
102
+
103
+ export default defineConfig({
104
+ providers: {
105
+ default: fallback([
106
+ withRetry(openai({ model: "gpt-5-mini" }), exponential({ maxRetries: 3 })),
107
+ openai({ model: "gpt-5-nano" }),
108
+ ]),
109
+ },
110
+ });
111
+ ```
112
+
113
+ `withRetry(provider, policy)` takes a policy built by `constant({ maxRetries })`
114
+ or `exponential({ maxRetries, delayMs?, multiplier?, maxDelayMs? })`; it retries
115
+ the wire call and fail-fasts on definitive errors (most 4xx). It is unrelated to
116
+ the intent method `.withRetry(n)`, which re-runs the whole ask.
117
+
118
+ ## Package layout
119
+
120
+ `nola-lang` is a DEV dependency — it holds the compiler, the CLI and
121
+ TypeScript, and never ships to production. The app depends on the runtime and
122
+ the providers package:
123
+
124
+ ```json
125
+ {
126
+ "type": "module",
127
+ "scripts": {
128
+ "start": "nola run src/main.ts",
129
+ "build": "nola build",
130
+ "check": "nola check"
131
+ },
132
+ "dependencies": {
133
+ "@nola-lang/providers": "^0.1.0",
134
+ "@nola-lang/runtime": "^0.1.0"
135
+ },
136
+ "devDependencies": {
137
+ "nola-lang": "^0.1.0",
138
+ "typescript": "^5.6.0"
139
+ },
140
+ "engines": { "node": ">=22" }
141
+ }
142
+ ```
143
+
144
+ `nola-lang`, `@nola-lang/runtime` and `@nola-lang/providers` are released in
145
+ LOCKSTEP — give all three the same version (`npm create nola` pins them
146
+ for you). Do not mix versions.
147
+
148
+ - `nola run <entry>` runs a `.ts`/`.tsi` entry through the loader with
149
+ `nola.config.ts` applied.
150
+ - `nola build [dir] [--out dist]` emits plain JS + source maps + `.d.ts`. For
151
+ app projects (the default) it also emits `dist/nola.config.js` and wires
152
+ every built module to it, so `node dist/main.js` needs no loader. Library
153
+ authors set `build: { target: "lib" }` and let the consuming app configure
154
+ the process.
155
+ - `nola check [dir]` type-checks `.tsi` and `.ts` together with diagnostics
156
+ mapped back to `.tsi` positions. Plain `tsc` over `src` is NOT a check path.
157
+ - Scaffold a project with `npm create nola my-app` (or `npx nola init`).
158
+
159
+ ## tsconfig.json
160
+
161
+ ```json
162
+ {
163
+ "compilerOptions": {
164
+ "strict": true,
165
+ "target": "ES2022",
166
+ "module": "NodeNext",
167
+ "moduleResolution": "NodeNext",
168
+ "allowArbitraryExtensions": true,
169
+ "noEmit": true,
170
+ "skipLibCheck": true
171
+ },
172
+ "include": ["src"]
173
+ }
174
+ ```
175
+
176
+ `include` must be DIRECTORY-style (`["src"]`), never a `.ts`-suffixed glob
177
+ like `["src/**/*.ts"]`: the directory form is what lets the editor tooling
178
+ admit `.tsi` files into the program while plain `tsc` ignores them. Under the
179
+ glob form `.tsi` files fall out of the program and auto-import stops offering
180
+ your infer functions. `module`/`moduleResolution` are NodeNext (hence `./x.js`
181
+ specifiers for plain TS imports) and `allowArbitraryExtensions` is required
182
+ for the `.tsi` declaration pairs.
183
+
184
+ ## Developing offline
185
+
186
+ `mockProvider` returns canned answers — deterministic, no API key, and the
187
+ right default for examples and tests. It takes either a queue of values (one
188
+ per ask, in order) or a function of the request:
189
+
190
+ ```ts
191
+ import { mockProvider } from "@nola-lang/providers";
192
+ import { defineConfig } from "@nola-lang/runtime";
193
+
194
+ export default defineConfig({
195
+ providers: {
196
+ default: mockProvider([{ name: "Alice Smith", age: 32 }]),
197
+ },
198
+ });
199
+ ```
200
+
201
+ ### record / replay
202
+
203
+ `record(inner, path)` wraps a real provider and appends every exchange to a
204
+ JSONL ledger; `replay(path)` serves answers back from that ledger:
205
+
206
+ ```ts
207
+ import { openai, record, replay } from "@nola-lang/providers";
208
+ import { defineConfig } from "@nola-lang/runtime";
209
+
210
+ const live = process.env.NOLA_RECORD === "1";
211
+
212
+ export default defineConfig({
213
+ providers: {
214
+ default: live
215
+ ? record(openai({ model: "gpt-5-mini" }), "./nola.replay.jsonl")
216
+ : replay("./nola.replay.jsonl"),
217
+ },
218
+ });
219
+ ```
220
+
221
+ `replay` is STRICT: entries are keyed by a fingerprint of the exact request, and
222
+ a request with no matching entry fails with NOLA3008 rather than quietly calling
223
+ the network. Any change to a prompt, an instruction, a schema or the provider
224
+ params re-keys the entry — re-record the ledger after editing a `.tsi`.
225
+
226
+ Secrets in production come from the real environment; `.env` files are a
227
+ dev-time convenience of the loader only.
@@ -0,0 +1,277 @@
1
+ # Nola patterns — worked examples
2
+
3
+ ## The starter project, end to end
4
+
5
+ This is the shape every Nola project takes: `.tsi` files hold the infer
6
+ functions, a plain `.ts` entry point calls them, and `nola run` executes the
7
+ entry with the loader and `nola.config.ts` in place.
8
+
9
+ ```
10
+ my-app/
11
+ nola.config.ts # providers
12
+ package.json # nola-lang in devDependencies
13
+ tsconfig.json # include: ["src"]
14
+ src/
15
+ person.tsi # the Nola source
16
+ main.ts # a plain TypeScript consumer
17
+ ```
18
+
19
+ `src/person.tsi` — the type and the infer function live together:
20
+
21
+ ```tsi
22
+ export interface Person {
23
+ name: string;
24
+ age: number;
25
+ employer: string;
26
+ job: string;
27
+ }
28
+
29
+ export infer function extractPerson(.message: string) {
30
+ const person = ask ..`the person described in the text`<Person>;
31
+ return person;
32
+ }
33
+ ```
34
+
35
+ `src/main.ts` — plain TypeScript. Note the literal `.tsi` extension in the
36
+ import, and that `await` on the returned `Intent` is what runs the inference:
37
+
38
+ ```ts
39
+ import { extractPerson } from "./person.tsi";
40
+
41
+ const person = await extractPerson(
42
+ "Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.",
43
+ );
44
+ console.log(JSON.stringify(person));
45
+ ```
46
+
47
+ Run it:
48
+
49
+ ```bash
50
+ nola run src/main.ts # or: npm start
51
+ nola check # type-checks .tsi and .ts together
52
+ nola build # dist/ — plain JS + source maps + .d.ts
53
+ ```
54
+
55
+ ## Composing several asks in one invocation
56
+
57
+ Every `ask` in one invocation shares that invocation's context — the `..`
58
+ contextual parameters and the function's instruction marker. That is what lets
59
+ you split one big prompt into several small, individually-typed asks instead of
60
+ demanding everything at once.
61
+
62
+ What is NOT shared is the answers. Earlier results are recorded on the
63
+ invocation's frame, but history does not compose into later prompts yet: a
64
+ later ask does NOT see what an earlier ask returned. When it needs an earlier
65
+ answer, pass it forward explicitly with `${}` interpolation.
66
+
67
+ ```tsi
68
+ export infer function solve(.problem: string) {
69
+ // Both asks see `problem` (the contextual parameter). They do NOT see each
70
+ // other's answers automatically — the reasoning is handed to the second ask
71
+ // explicitly through `${}`.
72
+ const reasoning = ask ..`think step by step about the problem before answering`;
73
+ const answer = ask ..`the final numeric answer, given this reasoning: ${reasoning}`<number>;
74
+ return { reasoning, answer };
75
+ }
76
+ ```
77
+
78
+ Once an `ask` returns, its result is an ORDINARY typed value — branch on it,
79
+ pass it to plain functions, put it in an object literal, interpolate it into a
80
+ later prompt:
81
+
82
+ ```tsi
83
+ export type Category = "billing" | "refund" | "fraud" | "other";
84
+
85
+ export infer function classifyMessage(.message: string) {
86
+ const category = ask ..`the category of the customer message`<Category>;
87
+ const urgent = ask ..`does the message need urgent attention`<"yes" | "no">;
88
+
89
+ // plain TS from here on
90
+ if (category === "fraud") return { category, urgent: true, escalate: true };
91
+ return { category, urgent: urgent === "yes", escalate: false };
92
+ }
93
+ ```
94
+
95
+ Plain TypeScript orchestrates the loop; the infer functions stay small:
96
+
97
+ ```tsi
98
+ // src/research.tsi
99
+ export interface Conclusion {
100
+ answer: string;
101
+ /** the collected notes that directly support the answer */
102
+ evidence: string[];
103
+ }
104
+
105
+ export infer function nextQuery(.question: string, .notes: string[]) {
106
+ return ask ..`the single best search query to advance the research; keywords only`<string>;
107
+ }
108
+
109
+ export infer function conclude(.question: string, .notes: string[]) {
110
+ return ask ..`answer the research question using only the collected notes`<Conclusion>;
111
+ }
112
+ ```
113
+
114
+ ```ts
115
+ // src/main.ts
116
+ import { conclude, nextQuery } from "./research.tsi";
117
+ import { search } from "./search.js";
118
+
119
+ const question = "who maintains the project?";
120
+ const notes: string[] = [];
121
+ for (let i = 0; i < 3; i++) {
122
+ const query = await nextQuery(question, notes);
123
+ notes.push(await search(query));
124
+ }
125
+ const conclusion = await conclude(question, notes);
126
+ ```
127
+
128
+ ## Call intents — let the LLM fill a function's arguments
129
+
130
+ When you already have a function that DOES something, do not extract its
131
+ arguments one at a time and then call it. Make the call itself the intent: the
132
+ LLM fills every extractor-shaped argument in one provider call, and the
133
+ function runs with the results.
134
+
135
+ `src/tickets.ts` — an ordinary TypeScript helper, nothing Nola about it:
136
+
137
+ ```ts
138
+ export async function createTicket(title: string, priority: number): Promise<string> {
139
+ const res = await fetch("https://example.test/tickets", {
140
+ method: "POST",
141
+ body: JSON.stringify({ title, priority }),
142
+ });
143
+ return (await res.json()).id as string;
144
+ }
145
+ ```
146
+
147
+ `src/file-ticket.tsi` — the helper is imported with the NodeNext `.js`
148
+ specifier, and the call gets extractor arguments:
149
+
150
+ ```tsi
151
+ import { createTicket } from "./tickets.js";
152
+
153
+ export infer function fileTicket(.request: string) {
154
+ // Sigil-less: the extractor argument makes this call an intent. `2` is a
155
+ // plain argument and is passed through untouched.
156
+ const id = ask createTicket(..`a short ticket title for the request`<string>, 2);
157
+ return id;
158
+ }
159
+ ```
160
+
161
+ Add instruction text for the call with the marker form — it is the only
162
+ spelling that carries a hint:
163
+
164
+ ```tsi
165
+ export infer function fileTicketCarefully(.request: string) {
166
+ return ask createTicket`file the ticket exactly as the customer described it`(
167
+ ..`a short ticket title`<string>,
168
+ ..`priority 1-5, where 1 is most urgent`<number>,
169
+ );
170
+ }
171
+ ```
172
+
173
+ Every extractor slot needs an explicit `<T>`, and all slots of one call
174
+ resolve together in a single provider round trip.
175
+
176
+ `createTicket` is async, but `id` is a `string`, not a `Promise<string>` — a
177
+ call intent awaits a promise-returning callee itself (`ask` ≈ `await`), so
178
+ `await ask createTicket(...)` is redundant. Because the callee runs inside the
179
+ ask, `.withRetry(n)` on a call intent re-invokes it on failure — only use it
180
+ when the target is idempotent.
181
+
182
+ ## Typing the answers
183
+
184
+ Prefer a NAMED, exported `type` or `interface` for `<T>` over an inline object
185
+ literal: it documents the contract, it is reusable from plain TS, and JSDoc
186
+ comments on its members become descriptions in the schema the LLM sees.
187
+
188
+ ```tsi
189
+ export interface LineItem {
190
+ description: string;
191
+ quantity: number;
192
+ /** price per unit in USD */
193
+ unitPrice: number;
194
+ }
195
+
196
+ export interface Invoice {
197
+ invoiceNumber: string;
198
+ issuedTo: string;
199
+ lineItems: LineItem[];
200
+ /** grand total in USD */
201
+ total: number;
202
+ /** ISO date; omit when the document has none */
203
+ dueDate?: string;
204
+ }
205
+
206
+ export infer function extractInvoice(.document: string) {
207
+ return ask ..`the invoice data from the document`<Invoice>;
208
+ }
209
+ ```
210
+
211
+ Closed label sets keep the model on-rails — a string-literal union, a string
212
+ enum, or an inline union all work:
213
+
214
+ ```tsi
215
+ export type Category = "billing" | "refund" | "fraud" | "other";
216
+
217
+ export enum Sentiment {
218
+ Positive = "positive",
219
+ Neutral = "neutral",
220
+ Negative = "negative",
221
+ }
222
+
223
+ export infer function triage(.message: string) {
224
+ const category = ask ..`the category of the customer message`<Category>;
225
+ const sentiment = ask ..`the overall sentiment of the message`<Sentiment>;
226
+ const urgent = ask ..`does the message need urgent attention`<"yes" | "no">;
227
+ return { category, sentiment, urgent: urgent === "yes" };
228
+ }
229
+ ```
230
+
231
+ ### Types from another file
232
+
233
+ Cross-file types are supported. Import the type from a plain `.ts` file with a
234
+ type-only import and the NodeNext `.js` specifier; the toolchain derives the
235
+ schema for you:
236
+
237
+ ```ts
238
+ // src/models.ts
239
+ export interface Person {
240
+ name: string;
241
+ age: number;
242
+ }
243
+ ```
244
+
245
+ ```tsi
246
+ // src/report.tsi
247
+ import type { Person } from "./models.js";
248
+
249
+ export infer function extractPerson(.text: string) {
250
+ return ask ..`the person described in the text`<Person>;
251
+ }
252
+ ```
253
+
254
+ Recursive types are legal too:
255
+
256
+ ```tsi
257
+ export type TreeNode = {
258
+ label: string;
259
+ children?: TreeNode[];
260
+ };
261
+
262
+ export infer function parseTree(.input: string) {
263
+ return ask ..`the tree structure described in the input`<TreeNode>;
264
+ }
265
+ ```
266
+
267
+ `Date` fields work and come back as real `Date` instances:
268
+
269
+ ```tsi
270
+ export type CalendarEvent = { title: string; at: Date };
271
+
272
+ export infer function nextEvent(.calendar: string) {
273
+ const event = ask ..`the next event on the calendar`<CalendarEvent>;
274
+ const when: Date = event.at; // a Date, not a string
275
+ return when;
276
+ }
277
+ ```