nola-lang 0.1.2 → 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.
- package/README.md +1 -1
- package/dist/main.js +3 -2
- package/dist/main.js.map +1 -1
- package/dist/skill.d.ts +1 -0
- package/dist/skill.d.ts.map +1 -1
- package/dist/skill.js +2 -2
- package/dist/skill.js.map +1 -1
- package/package.json +9 -10
- package/skills/nola/SKILL.md +0 -75
- package/skills/nola/references/config.md +0 -227
- package/skills/nola/references/patterns.md +0 -277
- package/skills/nola/references/pitfalls.md +0 -281
- package/skills/nola/references/syntax.md +0 -377
package/README.md
CHANGED
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@ const HELP = `nola <command>
|
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
12
|
init [dir] [--template <t>|--add] [--ide vscode] [--agents <list>] scaffold or retrofit a Nola project
|
|
13
|
-
skill install [--agents <list>] write agent skill files (Claude Code, Cursor, Copilot, AGENTS.md) into the project
|
|
13
|
+
skill install [--agents <list>] [--force] write agent skill files (Claude Code, Cursor, Copilot, AGENTS.md) into the project
|
|
14
14
|
build [dir] [--out <dir>] compile all .tsi files (js+map+d.ts to --out)
|
|
15
15
|
run <entry> run a .tsi/.js entry with the Nola loader + nola.config.ts
|
|
16
16
|
check [dir] type-check lowered .tsi files AND the project's .ts files, positions mapped back
|
|
@@ -26,6 +26,7 @@ async function main() {
|
|
|
26
26
|
ide: { type: "string" },
|
|
27
27
|
watch: { type: "boolean" },
|
|
28
28
|
agents: { type: "string" },
|
|
29
|
+
force: { type: "boolean" },
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
32
|
const [command, arg] = positionals;
|
|
@@ -76,7 +77,7 @@ async function main() {
|
|
|
76
77
|
case "init":
|
|
77
78
|
return cmdInit(arg, { template: values.template, add: values.add, ide: values.ide, agents: values.agents });
|
|
78
79
|
case "skill":
|
|
79
|
-
return cmdSkill(arg, { agents: values.agents });
|
|
80
|
+
return cmdSkill(arg, { agents: values.agents, force: values.force });
|
|
80
81
|
default:
|
|
81
82
|
console.log(HELP);
|
|
82
83
|
return command ? 1 : 0;
|
package/dist/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
|
package/dist/skill.d.ts
CHANGED
package/dist/skill.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAIA,kFAAkF;AAClF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"skill.d.ts","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAIA,kFAAkF;AAClF,wBAAsB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAMnH"}
|
package/dist/skill.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { runSkillInstall } from "create-nola-lang";
|
|
2
|
-
const USAGE = "nola skill install [--agents claude,cursor,copilot,agents-md | all | none]";
|
|
2
|
+
const USAGE = "nola skill install [--agents claude,cursor,copilot,agents-md | all | none] [--force]";
|
|
3
3
|
/** `nola skill install` — write agent skill adapters into the current project. */
|
|
4
4
|
export async function cmdSkill(sub, opts) {
|
|
5
5
|
if (sub !== "install") {
|
|
6
6
|
console.log(USAGE);
|
|
7
7
|
return sub ? 1 : 0;
|
|
8
8
|
}
|
|
9
|
-
return runSkillInstall({ agents: opts.agents });
|
|
9
|
+
return runSkillInstall({ agents: opts.agents, force: opts.force });
|
|
10
10
|
}
|
|
11
11
|
//# sourceMappingURL=skill.js.map
|
package/dist/skill.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,KAAK,GAAG,
|
|
1
|
+
{"version":3,"file":"skill.js","sourceRoot":"","sources":["../src/skill.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,KAAK,GAAG,sFAAsF,CAAC;AAErG,kFAAkF;AAClF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAuB,EAAE,IAA0C;IAChG,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnB,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC;IACD,OAAO,eAAe,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AACrE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nola-lang",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "The Nola language toolchain: nola init/build/run/check and the Node loader for .tsi files",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nola",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"type": "module",
|
|
29
29
|
"bin": {
|
|
30
|
-
"nola": "
|
|
30
|
+
"nola": "dist/main.js"
|
|
31
31
|
},
|
|
32
32
|
"exports": {
|
|
33
33
|
".": {
|
|
@@ -38,18 +38,17 @@
|
|
|
38
38
|
},
|
|
39
39
|
"files": [
|
|
40
40
|
"dist",
|
|
41
|
-
"!dist/.tsbuildinfo"
|
|
42
|
-
"skills"
|
|
41
|
+
"!dist/.tsbuildinfo"
|
|
43
42
|
],
|
|
44
43
|
"dependencies": {
|
|
45
44
|
"@ampproject/remapping": "^2.3.0",
|
|
46
45
|
"@jridgewell/trace-mapping": "^0.3.25",
|
|
47
|
-
"@nola-lang/ast": "0.1.
|
|
48
|
-
"@nola-lang/compiler": "0.1.
|
|
49
|
-
"@nola-lang/node-loader": "0.1.
|
|
50
|
-
"@nola-lang/parser": "0.1.
|
|
51
|
-
"@nola-lang/runtime": "0.1.
|
|
52
|
-
"create-nola-lang": "0.1.
|
|
46
|
+
"@nola-lang/ast": "0.1.4",
|
|
47
|
+
"@nola-lang/compiler": "0.1.4",
|
|
48
|
+
"@nola-lang/node-loader": "0.1.4",
|
|
49
|
+
"@nola-lang/parser": "0.1.4",
|
|
50
|
+
"@nola-lang/runtime": "0.1.4",
|
|
51
|
+
"create-nola-lang": "0.1.4",
|
|
53
52
|
"esbuild": "^0.25.0",
|
|
54
53
|
"typescript": "^5.6.0"
|
|
55
54
|
},
|
package/skills/nola/SKILL.md
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
|
|
@@ -1,227 +0,0 @@
|
|
|
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-lang` 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-lang 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.
|
|
@@ -1,277 +0,0 @@
|
|
|
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
|
-
```
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
# Common Nola errors and their fixes
|
|
2
|
-
|
|
3
|
-
Diagnostic codes are stable: `NOLA1xxx` parse, `NOLA2xxx` compile, `NOLA3xxx`
|
|
4
|
-
run time.
|
|
5
|
-
|
|
6
|
-
## NOLA1009 — `ask with` needs a static provider name
|
|
7
|
-
|
|
8
|
-
> expected a provider name after `ask with` — for a dynamic provider use
|
|
9
|
-
> `.withProvider(...)` on the intent.
|
|
10
|
-
|
|
11
|
-
The alias after `with` must be a bare identifier naming a key of the
|
|
12
|
-
`providers` map in `nola.config.ts`. A string literal, a variable expression or
|
|
13
|
-
a parenthesized expression will not parse.
|
|
14
|
-
|
|
15
|
-
```tsi
|
|
16
|
-
export infer function summarize(.text: string, useFast: boolean) {
|
|
17
|
-
// WRONG
|
|
18
|
-
const a = ask with "fast" ..`a rough summary`<string>;
|
|
19
|
-
const b = ask with providers.fast ..`a rough summary`<string>;
|
|
20
|
-
|
|
21
|
-
// RIGHT — name it in nola.config.ts, then use that name
|
|
22
|
-
const c = ask with fast ..`a rough summary`<string>;
|
|
23
|
-
|
|
24
|
-
// RIGHT — dynamic choice
|
|
25
|
-
const d = ask (..`a rough summary`<string>).withProvider(useFast ? "fast" : "careful");
|
|
26
|
-
|
|
27
|
-
return { a, b, c, d };
|
|
28
|
-
}
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
// nola.config.ts
|
|
33
|
-
export default defineConfig({
|
|
34
|
-
providers: {
|
|
35
|
-
default: openai({ model: "gpt-5-mini" }),
|
|
36
|
-
fast: openai({ model: "gpt-5-nano" }),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## NOLA1010 — `.` on a non-infer function
|
|
42
|
-
|
|
43
|
-
> `.` context parameters are only allowed on infer function parameters.
|
|
44
|
-
|
|
45
|
-
```tsi
|
|
46
|
-
// WRONG — a plain function has no inference context to put the value in
|
|
47
|
-
function summarize(.text: string) {
|
|
48
|
-
return text.slice(0, 10);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// RIGHT
|
|
52
|
-
export infer function summarize(.text: string) {
|
|
53
|
-
return ask ..`a one-sentence summary`<string>;
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
If the function is genuinely plain TypeScript, drop the `.`; the parameter is
|
|
58
|
-
an ordinary argument.
|
|
59
|
-
|
|
60
|
-
## NOLA2001 — `ask` outside an infer function body
|
|
61
|
-
|
|
62
|
-
> `ask` is only allowed directly inside an infer function body.
|
|
63
|
-
|
|
64
|
-
`ask` is not legal at module level, and not inside a nested closure — not even
|
|
65
|
-
one written inside an infer function.
|
|
66
|
-
|
|
67
|
-
```tsi
|
|
68
|
-
// WRONG
|
|
69
|
-
const kind = ask ..`the kind`<string>; // module level
|
|
70
|
-
|
|
71
|
-
export infer function f(.t: string) {
|
|
72
|
-
const g = () => ask ..`the kind`<string>; // nested closure
|
|
73
|
-
return g();
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// RIGHT — ask directly in the body; from plain TS, await the infer function
|
|
77
|
-
export infer function f(.t: string) {
|
|
78
|
-
return ask ..`the kind`<string>;
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
Constructing an extractor outside a body is fine — only resolving it is
|
|
83
|
-
restricted:
|
|
84
|
-
|
|
85
|
-
```tsi
|
|
86
|
-
export const nameIntent = ..`the user's full name`<string>; // legal, inert
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## NOLA2002 — a type the compiler cannot turn into a schema
|
|
90
|
-
|
|
91
|
-
> unsupported type for intent schema: …
|
|
92
|
-
|
|
93
|
-
An extractor's `<T>` must describe a JSON-shaped value: strings, numbers,
|
|
94
|
-
booleans, `Date`, arrays, plain object/interface/type-alias shapes,
|
|
95
|
-
string-literal unions, string enums, and references to those (same-file or
|
|
96
|
-
imported). Ambient lib types like `Map`, `Set`, `RegExp`, functions and
|
|
97
|
-
generics are not derivable.
|
|
98
|
-
|
|
99
|
-
```tsi
|
|
100
|
-
export infer function tally(.doc: string) {
|
|
101
|
-
// WRONG
|
|
102
|
-
const wrong = ask ..`counts per label`<Map<string, number>>;
|
|
103
|
-
|
|
104
|
-
// RIGHT — a JSON-shaped type; convert afterwards in plain TS
|
|
105
|
-
const counts = ask ..`counts per label`<{ label: string; count: number }[]>;
|
|
106
|
-
const asMap = new Map(counts.map((c) => [c.label, c.count]));
|
|
107
|
-
return asMap;
|
|
108
|
-
}
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Always give an extractor a concrete `<T>` in an expression position. An
|
|
112
|
-
extractor is a value-producing expression; it is not a statement, a type, or a
|
|
113
|
-
declaration.
|
|
114
|
-
|
|
115
|
-
## NOLA2008 — an underivable `.` contextual parameter type
|
|
116
|
-
|
|
117
|
-
> contextual parameter 'm' has a type that cannot be derived for inference:
|
|
118
|
-
> unsupported type for intent schema: Map<string, number>. Set
|
|
119
|
-
> compiler.underivableContextType to "prune" or "omit" in nola.config.ts to
|
|
120
|
-
> allow it.
|
|
121
|
-
|
|
122
|
-
The same derivability rules apply to contextual parameters, because their
|
|
123
|
-
values are serialized into the prompt.
|
|
124
|
-
|
|
125
|
-
```tsi
|
|
126
|
-
// WRONG
|
|
127
|
-
export infer function topLabel(.index: Map<string, number>) {
|
|
128
|
-
return ask ..`the label with the highest count`<string>;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// RIGHT — pass a JSON-shaped view as the contextual parameter
|
|
132
|
-
export infer function topLabel(.index: { label: string; count: number }[]) {
|
|
133
|
-
return ask ..`the label with the highest count`<string>;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// RIGHT — keep the exotic value, but as a PLAIN parameter (the LLM never
|
|
137
|
-
// sees its value, so nothing needs deriving)
|
|
138
|
-
export infer function topLabel(.summary: string, index: Map<string, number>) {
|
|
139
|
-
const label = ask ..`the label with the highest count`<string>;
|
|
140
|
-
return { label, count: index.get(label) ?? 0 };
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
If you must keep an underivable member on a contextual type, relax the policy
|
|
145
|
-
in `nola.config.ts` — `"prune"` drops just the underivable members and keeps
|
|
146
|
-
the rest of the type; `"omit"` drops the whole type silently:
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
export default defineConfig({
|
|
150
|
-
providers: { default: openai({ model: "gpt-5-mini" }) },
|
|
151
|
-
compiler: { underivableContextType: "prune" }, // default is "error"
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
Keep that value a literal — the editor reads it statically and never executes
|
|
156
|
-
your config.
|
|
157
|
-
|
|
158
|
-
## NOLA2004 — a call-intent slot with no `<T>`
|
|
159
|
-
|
|
160
|
-
> an extractor used as a call-intent argument must have an explicit `<T>`.
|
|
161
|
-
|
|
162
|
-
```tsi
|
|
163
|
-
declare function createTicket(title: string, priority: number): Promise<string>;
|
|
164
|
-
|
|
165
|
-
export infer function fileTicket(.request: string) {
|
|
166
|
-
// WRONG — the slot has no type
|
|
167
|
-
const wrong = ask createTicket(..`a short ticket title`, 2);
|
|
168
|
-
|
|
169
|
-
// RIGHT
|
|
170
|
-
const id = ask createTicket(..`a short ticket title`<string>, 2);
|
|
171
|
-
return id;
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## NOLA3010 — bare `await` on a raw extract or call intent
|
|
176
|
-
|
|
177
|
-
> extract/call intents carry no construction scope — only `ask` supplies their
|
|
178
|
-
> frame.
|
|
179
|
-
|
|
180
|
-
A raw extractor or call intent has no context of its own; it borrows the frame
|
|
181
|
-
of the `ask` that resolves it. Awaiting one directly (typically from plain TS,
|
|
182
|
-
or after storing it in a variable) throws at run time.
|
|
183
|
-
|
|
184
|
-
```ts
|
|
185
|
-
// WRONG — nothing supplies the inference context
|
|
186
|
-
import { nameIntent } from "./person.tsi";
|
|
187
|
-
const name = await nameIntent;
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
```tsi
|
|
191
|
-
// RIGHT — resolve it with `ask` inside an infer function
|
|
192
|
-
import { nameIntent } from "./person.tsi";
|
|
193
|
-
|
|
194
|
-
export infer function whoIsIt(.text: string) {
|
|
195
|
-
return ask nameIntent;
|
|
196
|
-
}
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
// RIGHT — from plain TS, await the INFER FUNCTION's result (that is an
|
|
201
|
-
// Intent, which does open its own invocation)
|
|
202
|
-
import { whoIsIt } from "./person.tsi";
|
|
203
|
-
|
|
204
|
-
const name = await whoIsIt("Alice Smith, 32, works at Acme Corp.");
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
## Import mistakes
|
|
208
|
-
|
|
209
|
-
- `.tsi` imports keep the LITERAL extension:
|
|
210
|
-
|
|
211
|
-
```ts
|
|
212
|
-
import { extractPerson } from "./person.tsi"; // RIGHT
|
|
213
|
-
import { extractPerson } from "./person"; // WRONG — unresolved
|
|
214
|
-
import { extractPerson } from "./person.js"; // WRONG — no such file
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
- Plain TypeScript imports use the NodeNext `.js` specifier, even though the
|
|
218
|
-
file on disk is `.ts`:
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
import { createTicket } from "./tickets.js"; // RIGHT
|
|
222
|
-
import { createTicket } from "./tickets.ts"; // WRONG — TS5097
|
|
223
|
-
import { createTicket } from "./tickets"; // WRONG — TS2835
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
- Never import a `*.nola.*` module. Those are internal companion modules the
|
|
227
|
-
compiler generates for cross-file types; only generated code imports them,
|
|
228
|
-
and a hand-written file with such a name is NOLA2006.
|
|
229
|
-
|
|
230
|
-
```ts
|
|
231
|
-
import { Person } from "./models.nola.js"; // WRONG — internal
|
|
232
|
-
import type { Person } from "./models.js"; // RIGHT
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
## Never write generated-code names
|
|
236
|
-
|
|
237
|
-
`__nola` and any identifier starting with `__nola` are reserved in `.tsi`.
|
|
238
|
-
`__nola.ask(...)`, `__nola.intents.ExtractIntent(...)`, `__nola.types.string()`
|
|
239
|
-
and `__nola_file_ctx()` are what the compiler EMITS — they are not an API you
|
|
240
|
-
call, and writing them by hand is an error.
|
|
241
|
-
|
|
242
|
-
```tsi
|
|
243
|
-
export infer function read(.doc: string) {
|
|
244
|
-
// WRONG — this is emitted code, not a user-facing API
|
|
245
|
-
const wrong = await __nola.ask(
|
|
246
|
-
__nola.intents.ExtractIntent({ instruction: "the value", type: __nola.types.string(), loc: "1:1" }),
|
|
247
|
-
__frame,
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
// RIGHT
|
|
251
|
-
const v = ask ..`the value`<string>;
|
|
252
|
-
return v;
|
|
253
|
-
}
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
## NOLA2009 — `${.member}` outside a Nola instruction
|
|
257
|
-
|
|
258
|
-
`${.x}` is prompt-scope access and only means something inside an
|
|
259
|
-
infer-function marker, an extractor's backticks, or a call-intent hint. In a
|
|
260
|
-
plain template literal it is an error — use a lexical value there.
|
|
261
|
-
|
|
262
|
-
## NOLA2010 — a Nola construct inside a marker / call-hint hole
|
|
263
|
-
|
|
264
|
-
Marker and call-hint literals are re-emitted from source, so `..`, call
|
|
265
|
-
intents and `ask` cannot appear in their holes. Compute the value first and
|
|
266
|
-
interpolate the result, or move the ask into the function body.
|
|
267
|
-
|
|
268
|
-
## NOLA3014 — a prompt template rendered nothing (or threw)
|
|
269
|
-
|
|
270
|
-
A `${.member}` template must produce text. An empty result usually means the
|
|
271
|
-
template only read members that were undefined; a throw is a bug in the
|
|
272
|
-
template's own JS. Fixed at the template — there is no retry.
|
|
273
|
-
|
|
274
|
-
## Other things that will not parse
|
|
275
|
-
|
|
276
|
-
- `async infer function f(...)` — an infer function is never `async` in source;
|
|
277
|
-
`await` is already legal in its body.
|
|
278
|
-
- `export default infer function f(...)` — export by name instead.
|
|
279
|
-
- `infer` on a method, arrow function or function expression — top-level
|
|
280
|
-
function declarations only.
|
|
281
|
-
- `fn(..)` — the bare derive-all call form is reserved (NOLA1004).
|
|
@@ -1,377 +0,0 @@
|
|
|
1
|
-
# Nola syntax reference
|
|
2
|
-
|
|
3
|
-
Everything valid in TypeScript is valid in a `.tsi` file. This file documents
|
|
4
|
-
only the additions. `ask` and `with` are reserved words in `.tsi` (`ask` stays
|
|
5
|
-
legal as a member/property name); `infer` is a keyword only directly before
|
|
6
|
-
`function` at statement or export position, so `T extends infer U` in a
|
|
7
|
-
conditional type is untouched.
|
|
8
|
-
|
|
9
|
-
## `infer function`
|
|
10
|
-
|
|
11
|
-
An `infer function` declares an LLM-backed function. Calling one runs NOTHING:
|
|
12
|
-
it returns a lazy, thenable `Intent<T>`. The work happens when the intent is
|
|
13
|
-
resolved — with `ask` inside another infer function, or with `await` from
|
|
14
|
-
plain TS.
|
|
15
|
-
|
|
16
|
-
```tsi
|
|
17
|
-
// plain
|
|
18
|
-
infer function summarize(.text: string) {
|
|
19
|
-
return ask ..`a one-sentence summary`<string>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// exported
|
|
23
|
-
export infer function classify(.message: string) {
|
|
24
|
-
return ask ..`the category of the message`<string>;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// with an instruction marker between the name and the parameter list
|
|
28
|
-
export infer function triage`triage the ticket like a support lead`(.ticket: string) {
|
|
29
|
-
return ask ..`the severity: low, medium or high`<"low" | "medium" | "high">;
|
|
30
|
-
}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
Rules:
|
|
34
|
-
|
|
35
|
-
- Top-level function declarations only. `infer` on a method, arrow function,
|
|
36
|
-
or function expression is a "reserved for a future Nola version" error.
|
|
37
|
-
- `async infer function` is a parse error — an infer function is never `async`
|
|
38
|
-
in source; it is implicitly awaitable through `Intent`.
|
|
39
|
-
- The instruction marker is a template literal. `${expr}` holes interpolate
|
|
40
|
-
lexical values into the instruction; a `${.member}` hole makes the marker a
|
|
41
|
-
prompt TEMPLATE for the function's CONTEXT block (see "Prompt templates").
|
|
42
|
-
- `export default infer function` does NOT parse. Export by name
|
|
43
|
-
(`export infer function f(...)`) and let consumers import the name.
|
|
44
|
-
- `ask` is legal only DIRECTLY inside an infer function body — not at module
|
|
45
|
-
level, not inside a nested closure (NOLA2001).
|
|
46
|
-
- `await` IS legal inside the body, for ordinary promises (fetch, DB, any
|
|
47
|
-
library):
|
|
48
|
-
|
|
49
|
-
```tsi
|
|
50
|
-
export infer function enrich(.handle: string, fetchProfile: (h: string) => Promise<string>) {
|
|
51
|
-
const profile = await fetchProfile(handle); // ordinary promise
|
|
52
|
-
return ask ..`the person's job title from: ${profile}`<string>;
|
|
53
|
-
}
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
### Return type annotation
|
|
57
|
-
|
|
58
|
-
Leave the return type off and let it infer — that is what every example in
|
|
59
|
-
this repo does. When you do annotate, the annotation is `Intent<T>` (the body
|
|
60
|
-
returns `T`, the way an `async` function body returns `T` under `Promise<T>`):
|
|
61
|
-
|
|
62
|
-
```tsi
|
|
63
|
-
import type { Intent } from "@nola-lang/runtime";
|
|
64
|
-
|
|
65
|
-
interface User {
|
|
66
|
-
name: string;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export infer function getUser(.message: string): Intent<User> {
|
|
70
|
-
const user = ask ..`the user described in the message`<User>;
|
|
71
|
-
return user;
|
|
72
|
-
}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
Do NOT annotate it `Promise<T>`: `Intent<T>` is `PromiseLike<T>`, not a
|
|
76
|
-
`Promise`, so `nola check` reports TS2739 (missing `catch`, `finally`,
|
|
77
|
-
`[Symbol.toStringTag]`).
|
|
78
|
-
|
|
79
|
-
## `.` contextual parameters
|
|
80
|
-
|
|
81
|
-
A parameter prefixed with ONE dot is a CONTEXT parameter: its name, type and
|
|
82
|
-
runtime VALUE are composed into the prompt of every `ask` in that invocation.
|
|
83
|
-
A plain parameter is an ordinary JS argument — its name and type reach the
|
|
84
|
-
LLM, its value does not. Mnemonic: one dot IN (`.name`), two dots OUT
|
|
85
|
-
(`` ..`prompt` ``). Writing `..name` on a parameter is NOLA1013.
|
|
86
|
-
|
|
87
|
-
```tsi
|
|
88
|
-
export type Issue = { id: string; description: string };
|
|
89
|
-
|
|
90
|
-
// `issue` is visible to the LLM; `fallback` is a normal JS value only.
|
|
91
|
-
export infer function classifyIssue(.issue: Issue, fallback: string) {
|
|
92
|
-
const kind = ask ..`the kind of this issue`<string>;
|
|
93
|
-
return kind || fallback;
|
|
94
|
-
}
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
- `.` is legal ONLY on infer-function parameters. On any other function it is
|
|
98
|
-
NOLA1010.
|
|
99
|
-
- A contextual parameter's TYPE must be derivable to an inference schema:
|
|
100
|
-
strings, numbers, booleans, `Date`, arrays, plain object/interface/type-alias
|
|
101
|
-
shapes, string-literal unions, string enums, and same-file or imported
|
|
102
|
-
references to those. `Map`, `Set` and other ambient lib types are NOT
|
|
103
|
-
derivable and raise NOLA2008 under the default policy.
|
|
104
|
-
- Several contextual parameters are fine; they compose into one context block:
|
|
105
|
-
|
|
106
|
-
```tsi
|
|
107
|
-
export infer function nextQuery(.question: string, .notes: string[]) {
|
|
108
|
-
return ask ..`the single best search query to advance the research`<string>;
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
- `const .x = …` (a contextual BINDING inside the body) is reserved for a
|
|
112
|
-
future Nola version — NOLA1014 today.
|
|
113
|
-
|
|
114
|
-
## Extractors — `` ..`instruction`<T> ``
|
|
115
|
-
|
|
116
|
-
An extractor is the request itself: instruction text in backticks plus an
|
|
117
|
-
optional type argument.
|
|
118
|
-
|
|
119
|
-
```tsi
|
|
120
|
-
export infer function parse(.doc: string) {
|
|
121
|
-
const id = ask ..`the ticket id`<string>; // typed
|
|
122
|
-
const count = ask ..`how many line items`<number>;
|
|
123
|
-
const free = ask ..`think step by step about the document`; // untyped
|
|
124
|
-
return { id, count, free };
|
|
125
|
-
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
- `${expr}` interpolation is legal inside the backticks and is evaluated at
|
|
129
|
-
intent-construction time. Strings splice as-is; anything else is
|
|
130
|
-
JSON-stringified. A hole starting with a single dot (`${.type}`) is NOT a
|
|
131
|
-
lexical value — it reads the extractor's prompt scope and turns the
|
|
132
|
-
backticks into a prompt template (see "Prompt templates"):
|
|
133
|
-
|
|
134
|
-
```tsi
|
|
135
|
-
interface Person {
|
|
136
|
-
name: string;
|
|
137
|
-
age: number;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export infer function lookup(text: string) {
|
|
141
|
-
return ask ..`the person described in: ${text}`<Person>;
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
- With no `<T>`, the extractor asks for free text: the wire schema is a plain
|
|
146
|
-
string and the static TS type is `any`. Give every extractor an explicit
|
|
147
|
-
`<T>` unless you deliberately want unconstrained prose.
|
|
148
|
-
- `<T>` accepts scalars, `Date`, arrays, inline object literals, same-file
|
|
149
|
-
and imported non-generic aliases/interfaces, string-literal unions and
|
|
150
|
-
string enums. JSDoc comments on members become schema descriptions:
|
|
151
|
-
|
|
152
|
-
```tsi
|
|
153
|
-
export interface Conclusion {
|
|
154
|
-
answer: string;
|
|
155
|
-
/** the collected notes that directly support the answer */
|
|
156
|
-
evidence: string[];
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
- An extractor may be CONSTRUCTED anywhere in a `.tsi` file, module level
|
|
161
|
-
included — construction needs no context. Only `ask` is position-restricted:
|
|
162
|
-
|
|
163
|
-
```tsi
|
|
164
|
-
export const nameIntent = ..`the user's full name`<string>; // legal, inert
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
## The `ask` operator
|
|
168
|
-
|
|
169
|
-
`ask` is a unary prefix operator with `await`'s precedence. It resolves any
|
|
170
|
-
`Askable` — an extractor, a call intent, or the `Intent` returned by calling
|
|
171
|
-
an infer function — to its value.
|
|
172
|
-
|
|
173
|
-
```tsi
|
|
174
|
-
import { getUserById } from "./users.tsi";
|
|
175
|
-
|
|
176
|
-
type User = { name: string };
|
|
177
|
-
|
|
178
|
-
export infer function report(.text: string) {
|
|
179
|
-
const user = ask ..`the user named in the text`<User>; // extractor
|
|
180
|
-
const record = ask getUserById(user.name); // another infer function
|
|
181
|
-
return record;
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### `ask with <name>` — pin one ask to a provider
|
|
186
|
-
|
|
187
|
-
```tsi
|
|
188
|
-
export infer function summarize(.text: string) {
|
|
189
|
-
const draft = ask with fast ..`a rough summary`<string>;
|
|
190
|
-
const final = ask with careful ..`a polished summary of: ${draft}`<string>;
|
|
191
|
-
return final;
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
- `<name>` must be a STATIC identifier naming a key of the `providers` map in
|
|
196
|
-
`nola.config.ts`. An extractor, a parenthesized expression, a string literal
|
|
197
|
-
or anything else after `with` is NOLA1009 — use `.withProvider(...)` for a
|
|
198
|
-
dynamic provider.
|
|
199
|
-
- The name is matched against the config at ask time, not compile time; an
|
|
200
|
-
unknown name is a runtime `NolaConfigError` (NOLA3004).
|
|
201
|
-
- `ask without` is a plain ask of the identifier `without`, not a pin.
|
|
202
|
-
|
|
203
|
-
## Call intents
|
|
204
|
-
|
|
205
|
-
A call intent lets the LLM fill some of a function's arguments, then calls the
|
|
206
|
-
function with them. All slots of one call intent resolve in ONE provider call.
|
|
207
|
-
|
|
208
|
-
Three spellings:
|
|
209
|
-
|
|
210
|
-
```tsi
|
|
211
|
-
declare function createTicket(title: string, priority: number): Promise<string>;
|
|
212
|
-
|
|
213
|
-
export infer function file(.request: string) {
|
|
214
|
-
// 1. sigil-less — a plain call whose arguments contain an extractor
|
|
215
|
-
const a = ask createTicket(..`a short ticket title`<string>, 2);
|
|
216
|
-
|
|
217
|
-
// 2. empty marker — identical lowering; the only spelling for a call
|
|
218
|
-
// intent whose arguments are all plain
|
|
219
|
-
const b = ask createTicket``("fallback title", 3);
|
|
220
|
-
|
|
221
|
-
// 3. hint marker — the ONLY carrier of instruction text for the call
|
|
222
|
-
const c = ask createTicket`file the ticket the customer asked for`(
|
|
223
|
-
..`a short ticket title`<string>,
|
|
224
|
-
..`priority 1-5, 1 is most urgent`<number>,
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
return { a, b, c };
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
Detection rule for the sigil-less form — BOTH must hold:
|
|
232
|
-
|
|
233
|
-
- the callee is an `Identifier` or a `MemberExpression` (any nesting, computed
|
|
234
|
-
included), and
|
|
235
|
-
- at least one well-formed extractor appears in a slot position: a direct
|
|
236
|
-
argument, or nested at any depth inside plain object/array literals.
|
|
237
|
-
|
|
238
|
-
```tsi
|
|
239
|
-
declare const api: { save(order: { qty: number; note: string }): Promise<string> };
|
|
240
|
-
|
|
241
|
-
export infer function place(.request: string) {
|
|
242
|
-
// member callee + extractor nested in an object literal → call intent
|
|
243
|
-
return ask api.save({ qty: 1, note: ..`a one-line note for the warehouse`<string> });
|
|
244
|
-
}
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
These stay PLAIN calls (the extractor is just a value argument): an extractor
|
|
248
|
-
inside a ternary, logical expression, spread element or template substitution;
|
|
249
|
-
a nested call (in `` outer(inner(..`x`<T>)) `` the INNER call is the intent and
|
|
250
|
-
`outer` receives an `Askable`); `new Foo(...)`, `super(...)`, `import(...)`,
|
|
251
|
-
optional calls (`fn?.(...)`, `a?.b(...)`); and exotic callees (`getFn()(...)`,
|
|
252
|
-
IIFEs) — use the marker form if you want a call intent on one of those.
|
|
253
|
-
|
|
254
|
-
Parenthesizing an extractor does NOT opt out. To pass an intent as a plain
|
|
255
|
-
value, bind it to a variable first:
|
|
256
|
-
|
|
257
|
-
```tsi
|
|
258
|
-
const i = ..`a short title`<string>;
|
|
259
|
-
helper(i); // plain call — helper receives the Askable
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
Every extractor used as a call-intent slot must carry an explicit `<T>`
|
|
263
|
-
(NOLA2004). The bare derive-all form `fn(..)` is reserved (NOLA1004).
|
|
264
|
-
|
|
265
|
-
### Result of a call intent — async callees are awaited
|
|
266
|
-
|
|
267
|
-
`ask fn(...)` yields the callee's SETTLED value, exactly like `await fn(...)`
|
|
268
|
-
would: if the function returns a promise (or any thenable), the intent awaits
|
|
269
|
-
it before resolving. Its static type is `Awaited<ReturnType<typeof fn>>`. Never
|
|
270
|
-
write `await ask fn(...)` — the extra `await` is a no-op.
|
|
271
|
-
|
|
272
|
-
```tsi
|
|
273
|
-
declare function createTicket(title: string, priority: number): Promise<string>;
|
|
274
|
-
|
|
275
|
-
export infer function file(.request: string) {
|
|
276
|
-
const id = ask createTicket(..`a short ticket title`<string>, 2); // id: string, not Promise<string>
|
|
277
|
-
return id;
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Two consequences to keep in mind:
|
|
282
|
-
|
|
283
|
-
- The callee runs INSIDE the ask, so a rejected promise fails the ask at the
|
|
284
|
-
call site (NolaResolutionError with the intent's location) — and
|
|
285
|
-
`.withRetry(n)` re-runs the WHOLE ask, including the callee. Do not put
|
|
286
|
-
`.withRetry` on a call intent whose target is not idempotent.
|
|
287
|
-
- The invocation timeout (`ask.timeoutMs` / `.withTimeout`) bounds provider
|
|
288
|
-
round trips only. Once the arguments are filled, the callee's own promise
|
|
289
|
-
runs to completion, the same as a plain `await fn()` in your code.
|
|
290
|
-
|
|
291
|
-
## Prompt templates — `${.member}`
|
|
292
|
-
|
|
293
|
-
Every instruction literal (infer-function marker, extractor prompt, call-intent
|
|
294
|
-
hint) can act as a TEMPLATE for the prompt block that intent contributes. One
|
|
295
|
-
rule: a substitution hole whose expression starts with a single dot reads the
|
|
296
|
-
intent's prompt scope; every other hole is ordinary lexical JavaScript.
|
|
297
|
-
|
|
298
|
-
```tsi
|
|
299
|
-
infer function analyze`${.default}
|
|
300
|
-
Rules: answer only from the arguments above; never invent ids.`(.ticket: Ticket) {
|
|
301
|
-
const id = ask ..`ticket id, comply with ${.type}`<string>;
|
|
302
|
-
return id;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// A full custom CONTEXT block — everything after the dot is plain TypeScript
|
|
306
|
-
infer function triage`
|
|
307
|
-
CONTEXT — inside ${.signature}, ${.file}
|
|
308
|
-
${.args.map(a => `- ${a.name} (${a.type}): ${JSON.stringify(a.value)}`)}
|
|
309
|
-
|
|
310
|
-
TASK
|
|
311
|
-
${.next}
|
|
312
|
-
`(.ticket: string) { … }
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
- Override rule (static): a literal with at least ONE `${.x}` hole is a
|
|
316
|
-
template — its rendered text REPLACES that intent's block (CONTEXT for an
|
|
317
|
-
infer function, TASK for an extractor / call hint). A literal with no scope
|
|
318
|
-
hole is an instruction, exactly as before (`Purpose:` / `<request>` inside
|
|
319
|
-
the built-in block), even when it has lexical `${}` holes.
|
|
320
|
-
- Function scope (`FunctionPromptScope`): `.fn`, `.signature`, `.file`,
|
|
321
|
-
`.args[]` (`name`, `type` — native type text, `value`, `contextual`),
|
|
322
|
-
`.nested`, `.hasContext`, `.default` (the built-in CONTEXT block, no
|
|
323
|
-
Purpose line), `.next` (the rest of the prompt: callee blocks + TASK).
|
|
324
|
-
- Extractor / call-hint scope (`ExtractPromptScope`): `.type` (native type
|
|
325
|
-
text of the target), `.schema` (the JSON Schema, serialized),
|
|
326
|
-
`.hasContext`, `.default` (the built-in TASK block), `.format` (the JSON
|
|
327
|
-
response rules).
|
|
328
|
-
- Safe by default: a function template that never reads `.next` gets the
|
|
329
|
-
rest of the prompt appended after it; an extractor template that never
|
|
330
|
-
reads `.format` gets the JSON response rules appended. Read them only to
|
|
331
|
-
choose WHERE they go (wrapping). `.next`/`.default`/`.format` are
|
|
332
|
-
getters, memoized — reading twice does not compose twice.
|
|
333
|
-
- Rendering: arrays render one item per line (no `.join` needed),
|
|
334
|
-
`undefined`/`null` render as nothing, objects as JSON, `Date` as ISO.
|
|
335
|
-
Templates render when the ask composes its prompt — lexical values inside
|
|
336
|
-
a TEMPLATE are read then, not at construction (the only observable
|
|
337
|
-
difference from an instruction's eager `${}`).
|
|
338
|
-
- Nested holes follow the same rule: `${.file}` inside a `.map` callback's
|
|
339
|
-
own template literal still reads the scope. Keyword members work
|
|
340
|
-
(`${.default}`).
|
|
341
|
-
- Editor: completion, hover and precise TS errors work inside the backticks
|
|
342
|
-
(an unknown member is a TS2339 at the member).
|
|
343
|
-
- Errors: `${.x}` in a template literal that is not a Nola instruction is
|
|
344
|
-
NOLA2009; a Nola construct (`..`, call intent, `ask`) inside a marker /
|
|
345
|
-
call-hint hole is NOLA2010; a template that throws or renders empty fails
|
|
346
|
-
the ask with NOLA3014 (definitive).
|
|
347
|
-
|
|
348
|
-
## Intent methods
|
|
349
|
-
|
|
350
|
-
Every intent (extractor, call intent, infer-function result) accepts:
|
|
351
|
-
|
|
352
|
-
```tsi
|
|
353
|
-
export infer function tuned(.text: string) {
|
|
354
|
-
const a = ask (..`the title`<string>).withRetry(2);
|
|
355
|
-
const b = ask (..`the body`<string>).withProvider("careful");
|
|
356
|
-
const c = ask (..`a creative tagline`<string>).withParams({ temperature: 0.9, maxOutputTokens: 200 });
|
|
357
|
-
return { a, b, c };
|
|
358
|
-
}
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
- `.withRetry(n)` — `n` extra whole-ask attempts, flat, no backoff.
|
|
362
|
-
- `.withProvider(nameOrProvider)` — the dynamic form of `ask with`.
|
|
363
|
-
- `.withParams({ temperature, maxOutputTokens, providerOptions })` — wire knobs,
|
|
364
|
-
merged per field with anything already set.
|
|
365
|
-
|
|
366
|
-
Two more exist ONLY on the `Intent` an infer function returns (they act when
|
|
367
|
-
the intent roots an invocation), and are typically used from plain TS:
|
|
368
|
-
|
|
369
|
-
```ts
|
|
370
|
-
import { extractPerson } from "./person.tsi";
|
|
371
|
-
|
|
372
|
-
const person = await extractPerson(text).withTimeout(30_000);
|
|
373
|
-
const loose = await extractPerson(text).detached(); // do not inherit the caller frame's context
|
|
374
|
-
```
|
|
375
|
-
|
|
376
|
-
All of these CLONE the intent — the original stays unstarted, and an intent
|
|
377
|
-
resolves at most once.
|