@falai/agent 4.0.0-alpha.4 → 4.0.0-alpha.6
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/dist/cjs/utils/template.d.ts +8 -0
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +40 -3
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/utils/template.d.ts +8 -0
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +40 -3
- package/dist/utils/template.js.map +1 -1
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/migration/v3-to-v4.md +3 -2
- package/docs/reference/actions-events-conditions.md +1 -1
- package/docs/start/04-add-tools.md +1 -1
- package/package.json +3 -2
- package/src/utils/template.ts +39 -3
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
|
+
*
|
|
9
|
+
* A path the host answered with nothing is a different case: it knows the
|
|
10
|
+
* field and knows it is blank. Then the placeholder goes and `tidy` closes the
|
|
11
|
+
* gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends "Ola, tudo bem?"
|
|
12
|
+
* and never "Ola , tudo bem?". Two things count as blank: an empty string, and
|
|
13
|
+
* a path that walks THROUGH a null — `context.lead` being `null` means there
|
|
14
|
+
* is no lead, so "the lead's name" is blank, not mistyped. A null at the end of
|
|
15
|
+
* a path is still unknown: a field that collected nothing keeps its braces.
|
|
8
16
|
*/
|
|
9
17
|
export interface TemplateScope {
|
|
10
18
|
data?: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/utils/template.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../../src/utils/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAOD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAcrE;AAiBD,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,GAAG,CAAC,CAS/D"}
|
|
@@ -6,16 +6,51 @@
|
|
|
6
6
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
7
7
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
8
8
|
* of vanishing into an empty string.
|
|
9
|
+
*
|
|
10
|
+
* A path the host answered with nothing is a different case: it knows the
|
|
11
|
+
* field and knows it is blank. Then the placeholder goes and `tidy` closes the
|
|
12
|
+
* gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends "Ola, tudo bem?"
|
|
13
|
+
* and never "Ola , tudo bem?". Two things count as blank: an empty string, and
|
|
14
|
+
* a path that walks THROUGH a null — `context.lead` being `null` means there
|
|
15
|
+
* is no lead, so "the lead's name" is blank, not mistyped. A null at the end of
|
|
16
|
+
* a path is still unknown: a field that collected nothing keeps its braces.
|
|
9
17
|
*/
|
|
10
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
19
|
exports.render = render;
|
|
12
20
|
exports.renderDeep = renderDeep;
|
|
13
21
|
const PLACEHOLDER = /\{\{\s*([^}\s]+)\s*\}\}/g;
|
|
22
|
+
/** The path ran into a `null` on its way down: the container the host named is absent. */
|
|
23
|
+
const ABSENT = Symbol("absent");
|
|
14
24
|
function render(template, scope) {
|
|
15
|
-
|
|
25
|
+
let emptied = false;
|
|
26
|
+
const out = template.replace(PLACEHOLDER, (match, path) => {
|
|
16
27
|
const value = lookup(scope, path.split("."));
|
|
17
|
-
|
|
28
|
+
if (value === ABSENT) {
|
|
29
|
+
emptied = true;
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
if (value === undefined || value === null)
|
|
33
|
+
return match;
|
|
34
|
+
const text = stringify(value);
|
|
35
|
+
if (text === "")
|
|
36
|
+
emptied = true;
|
|
37
|
+
return text;
|
|
18
38
|
});
|
|
39
|
+
return emptied ? tidy(out) : out;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Close the hole an empty value leaves: a doubled space, a space before
|
|
43
|
+
* punctuation, a space at the end of a line.
|
|
44
|
+
*
|
|
45
|
+
* Deliberately narrow. It only runs on a string where something substituted to
|
|
46
|
+
* "", and it only collapses a run of spaces that follows a visible character,
|
|
47
|
+
* so indentation in a markdown list survives.
|
|
48
|
+
*/
|
|
49
|
+
function tidy(text) {
|
|
50
|
+
return text
|
|
51
|
+
.replace(/(?<=\S)[ \t]{2,}/g, " ")
|
|
52
|
+
.replace(/ +([,.;:!?\u2026])/g, "$1")
|
|
53
|
+
.replace(/[ \t]+$/gm, "");
|
|
19
54
|
}
|
|
20
55
|
/** `render` over every string inside a value, recursively. Non-strings pass through. */
|
|
21
56
|
function renderDeep(value, scope) {
|
|
@@ -31,7 +66,9 @@ function renderDeep(value, scope) {
|
|
|
31
66
|
function lookup(root, keys) {
|
|
32
67
|
let current = root;
|
|
33
68
|
for (const key of keys) {
|
|
34
|
-
if (current === null
|
|
69
|
+
if (current === null)
|
|
70
|
+
return ABSENT;
|
|
71
|
+
if (typeof current !== "object")
|
|
35
72
|
return undefined;
|
|
36
73
|
current = current[key];
|
|
37
74
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/utils/template.ts"],"names":[],"mappings":";AAAA
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/utils/template.ts"],"names":[],"mappings":"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"}
|
package/dist/utils/template.d.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
|
+
*
|
|
9
|
+
* A path the host answered with nothing is a different case: it knows the
|
|
10
|
+
* field and knows it is blank. Then the placeholder goes and `tidy` closes the
|
|
11
|
+
* gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends "Ola, tudo bem?"
|
|
12
|
+
* and never "Ola , tudo bem?". Two things count as blank: an empty string, and
|
|
13
|
+
* a path that walks THROUGH a null — `context.lead` being `null` means there
|
|
14
|
+
* is no lead, so "the lead's name" is blank, not mistyped. A null at the end of
|
|
15
|
+
* a path is still unknown: a field that collected nothing keeps its braces.
|
|
8
16
|
*/
|
|
9
17
|
export interface TemplateScope {
|
|
10
18
|
data?: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/utils/template.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../../src/utils/template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAOD,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAcrE;AAiBD,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,GAAG,CAAC,CAS/D"}
|
package/dist/utils/template.js
CHANGED
|
@@ -5,13 +5,48 @@
|
|
|
5
5
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
|
+
*
|
|
9
|
+
* A path the host answered with nothing is a different case: it knows the
|
|
10
|
+
* field and knows it is blank. Then the placeholder goes and `tidy` closes the
|
|
11
|
+
* gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends "Ola, tudo bem?"
|
|
12
|
+
* and never "Ola , tudo bem?". Two things count as blank: an empty string, and
|
|
13
|
+
* a path that walks THROUGH a null — `context.lead` being `null` means there
|
|
14
|
+
* is no lead, so "the lead's name" is blank, not mistyped. A null at the end of
|
|
15
|
+
* a path is still unknown: a field that collected nothing keeps its braces.
|
|
8
16
|
*/
|
|
9
17
|
const PLACEHOLDER = /\{\{\s*([^}\s]+)\s*\}\}/g;
|
|
18
|
+
/** The path ran into a `null` on its way down: the container the host named is absent. */
|
|
19
|
+
const ABSENT = Symbol("absent");
|
|
10
20
|
export function render(template, scope) {
|
|
11
|
-
|
|
21
|
+
let emptied = false;
|
|
22
|
+
const out = template.replace(PLACEHOLDER, (match, path) => {
|
|
12
23
|
const value = lookup(scope, path.split("."));
|
|
13
|
-
|
|
24
|
+
if (value === ABSENT) {
|
|
25
|
+
emptied = true;
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
if (value === undefined || value === null)
|
|
29
|
+
return match;
|
|
30
|
+
const text = stringify(value);
|
|
31
|
+
if (text === "")
|
|
32
|
+
emptied = true;
|
|
33
|
+
return text;
|
|
14
34
|
});
|
|
35
|
+
return emptied ? tidy(out) : out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Close the hole an empty value leaves: a doubled space, a space before
|
|
39
|
+
* punctuation, a space at the end of a line.
|
|
40
|
+
*
|
|
41
|
+
* Deliberately narrow. It only runs on a string where something substituted to
|
|
42
|
+
* "", and it only collapses a run of spaces that follows a visible character,
|
|
43
|
+
* so indentation in a markdown list survives.
|
|
44
|
+
*/
|
|
45
|
+
function tidy(text) {
|
|
46
|
+
return text
|
|
47
|
+
.replace(/(?<=\S)[ \t]{2,}/g, " ")
|
|
48
|
+
.replace(/ +([,.;:!?\u2026])/g, "$1")
|
|
49
|
+
.replace(/[ \t]+$/gm, "");
|
|
15
50
|
}
|
|
16
51
|
/** `render` over every string inside a value, recursively. Non-strings pass through. */
|
|
17
52
|
export function renderDeep(value, scope) {
|
|
@@ -27,7 +62,9 @@ export function renderDeep(value, scope) {
|
|
|
27
62
|
function lookup(root, keys) {
|
|
28
63
|
let current = root;
|
|
29
64
|
for (const key of keys) {
|
|
30
|
-
if (current === null
|
|
65
|
+
if (current === null)
|
|
66
|
+
return ABSENT;
|
|
67
|
+
if (typeof current !== "object")
|
|
31
68
|
return undefined;
|
|
32
69
|
current = current[key];
|
|
33
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/utils/template.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"template.js","sourceRoot":"","sources":["../../src/utils/template.ts"],"names":[],"mappings":"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"}
|
|
@@ -53,7 +53,7 @@ A `do` step fills the parameters with `with`. When the agent is built, `validate
|
|
|
53
53
|
- a value has the wrong type; nothing is coerced, so `"3"` is not a number. A template such as `"{{data.cep}}"` is a string, so templates can only fill string parameters (and the items of a string array);
|
|
54
54
|
- `with` names a parameter the action does not have.
|
|
55
55
|
|
|
56
|
-
Templates in `with` are rendered right before `run` is called, against `data` (collected fields), `context` (this turn's host context) and `input` (the run's input).
|
|
56
|
+
Templates in `with` are rendered right before `run` is called, against `data` (collected fields), `context` (this turn's host context) and `input` (the run's input). An unknown path keeps its `{{...}}`, so a typo stays visible. A blank one drops out, and the space or comma it left behind goes with it — an empty string, or a path that walks through a `null` such as `{{context.lead.name}}` when there is no lead.
|
|
57
57
|
|
|
58
58
|
## What `run` sees
|
|
59
59
|
|
|
@@ -410,13 +410,14 @@ Host actions, events and conditions are registered once on the agent and referen
|
|
|
410
410
|
| `agent.dispatch`, `pendingDirective`, `Directive`, `flow.merge`, `flow.validate` | `then` / `else` on steps |
|
|
411
411
|
| `Flow` class, `Step` class, `flow` namespace, `FlowOptions`, `StepOptions` | plain objects: `Flow`, `Step` |
|
|
412
412
|
| `title`, `when`, `if`, `reentrant`, `requiredFields`, `optionalFields`, `onComplete`, flow `hooks` | `id` + `name`, `on[]`, `repeat`, `clearOnStart`, `onEnd`, `while` |
|
|
413
|
-
| `
|
|
413
|
+
| `skip`, `auto`, `reply`, step `hooks`, `prepare`, `finalize` | known-field skipping, `maxAsks`, `do`, `wait`, `say` |
|
|
414
|
+
| `requires` | an `if` step: `{ if: { known: [...] }, then: '<step>', else: 'end' }`. Not known-field skipping — that answers "do I still need to ask?", while `requires` answers "may this step run at all?". They differ exactly when the customer never answers: `maxAsks` retires the field and the step proceeds with it unknown. |
|
|
414
415
|
| `Signal`, `SignalContext`, `SignalFiring`, `signals`, `signalBatchSize`, `triggeredSignals` | `mention` flows, `repeat`, `claims` |
|
|
415
416
|
| `ToolContext.updateContext / updateData / setField / dispatch`, `ToolResult.dataUpdate / contextUpdate / directive`, `ToolManager`, `ToolScope`, tool config helpers | `Tool.handler(args, ctx) → { value?, data? }` |
|
|
416
417
|
| `PersistenceAdapter`, `SessionRepository`, `MessageRepository`, `PersistenceManager`, `SessionManager`, `restoreSession`, `createPersistedState`, `enterFlow`, `enterStep`, `completeCurrentFlow`, `mergeCollected` | `Store`, the seven `*Store` classes, `migrateSession` |
|
|
417
418
|
| `SessionState`, `CollectedStateData`, `SessionData` | `Session` |
|
|
418
419
|
| `AgentResponse.executedSteps / stoppedReason / endedFlows / appliedInstructions / isFlowComplete` | `TurnResult.outcomes / started / ended / skipped / messages / schedule / llmCalls` |
|
|
419
|
-
| `Template` as a function, `TemplateContext`, `ConditionEvaluator`, `ConditionWhen`, `ConditionIf
|
|
420
|
+
| `Template` as a function, `TemplateContext`, `ConditionEvaluator`, `ConditionWhen`, `ConditionIf` | `Template = string` with `{{data.x}}` `{{context.x}}` `{{input.x}}`; `Pred` (function or JSON). **`!` exclusions are not gone** — a phrase opening with `!` still rules a trigger out. Copy the list across unchanged. |
|
|
420
421
|
| `Term`, `terms` | put the glossary in `knowledgeBase` or an instruction |
|
|
421
422
|
| `Instruction.enabled / tags / metadata` | filter before passing |
|
|
422
423
|
| `promptCache`, `PromptSectionCache`, `PromptCacheConfig` | gone; every prompt is built per call. `compaction` stays and runs once per turn on the history you pass |
|
|
@@ -116,7 +116,7 @@ f.action<const P extends ParamDefs>(def: {
|
|
|
116
116
|
|
|
117
117
|
### Behaviour
|
|
118
118
|
|
|
119
|
-
- `with` is rendered before `run` sees it. `{{data.x}}`, `{{context.x}}` and `{{input.x}}` are replaced inside every string, at any depth.
|
|
119
|
+
- `with` is rendered before `run` sees it. `{{data.x}}`, `{{context.x}}` and `{{input.x}}` are replaced inside every string, at any depth. An unknown path keeps its placeholder, so a typo stays visible. A blank one drops out instead and the gap it left in the sentence closes — an empty string, or a path through a `null` (`{{context.lead.name}}` with no lead). A `null` at the end of a path is unknown, not blank.
|
|
120
120
|
- `with` is checked when the agent is built, not on the turn that reaches the step. A missing required parameter, an unknown parameter, or a value outside `enum` throws `FlowConfigurationError`. So does a wrong type: `"3"` is not a number, because values are never coerced. A string that contains `{{` skips the `enum` check, because its value is only known at run time.
|
|
121
121
|
- Actions run in the Run phase, by code, with zero model calls. They run while `silenced` too.
|
|
122
122
|
- A `do` step whose action name is not registered throws at build. If the registry changed under a running agent, the step reports `code: 'action-failed'` with `detail: 'unknown action "notify"'`.
|
|
@@ -174,7 +174,7 @@ const agent = f.agent({
|
|
|
174
174
|
|
|
175
175
|
`parameters` use the same `type`, `enum` and `description` as fields (plus `optional: true`), and `run` receives them already typed: `mensagem` is a `string` above, nothing to check.
|
|
176
176
|
|
|
177
|
-
`with` fills the parameters. `{{data.x}}`, `{{context.x}}` and `{{input.x}}` are replaced before the action runs. A placeholder whose value is unknown stays as written, so a skipped `tamanho` reaches the seller as `{{data.tamanho}}`. Word the message without it, or fork first with an `if` step on `{ known: ["tamanho"] }`. A `do` step never talks to the model: zero calls.
|
|
177
|
+
`with` fills the parameters. `{{data.x}}`, `{{context.x}}` and `{{input.x}}` are replaced before the action runs. A placeholder whose value is unknown stays as written, so a skipped `tamanho` reaches the seller as `{{data.tamanho}}`. One the host filled with an empty string drops out instead. Word the message without it, or fork first with an `if` step on `{ known: ["tamanho"] }`. A `do` step never talks to the model: zero calls.
|
|
178
178
|
|
|
179
179
|
The name and the parameters are checked when the agent is built. An unknown action, or a `with` missing a required parameter, throws `FlowConfigurationError` at startup with the step id and the fix.
|
|
180
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@falai/agent",
|
|
3
|
-
"version": "4.0.0-alpha.
|
|
3
|
+
"version": "4.0.0-alpha.6",
|
|
4
4
|
"description": "Conversational state engine for TypeScript where the AI understands, but the code is in control",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -59,7 +59,8 @@
|
|
|
59
59
|
"release:alpha": "bun publish --tag alpha",
|
|
60
60
|
"release": "bun publish",
|
|
61
61
|
"test": "bun test tests/*.test.ts tests/scenarios/*.test.ts",
|
|
62
|
-
"eval:live": "bun run scripts/eval/live.ts"
|
|
62
|
+
"eval:live": "bun run scripts/eval/live.ts",
|
|
63
|
+
"eval:exclusions": "bun run scripts/eval/exclusions.ts"
|
|
63
64
|
},
|
|
64
65
|
"keywords": [
|
|
65
66
|
"ai",
|
package/src/utils/template.ts
CHANGED
|
@@ -5,6 +5,14 @@
|
|
|
5
5
|
* per-turn context) and `input` (the run's trigger payload). A path that
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
|
+
*
|
|
9
|
+
* A path the host answered with nothing is a different case: it knows the
|
|
10
|
+
* field and knows it is blank. Then the placeholder goes and `tidy` closes the
|
|
11
|
+
* gap it left, so "Ola {{context.lead.name}}, tudo bem?" sends "Ola, tudo bem?"
|
|
12
|
+
* and never "Ola , tudo bem?". Two things count as blank: an empty string, and
|
|
13
|
+
* a path that walks THROUGH a null — `context.lead` being `null` means there
|
|
14
|
+
* is no lead, so "the lead's name" is blank, not mistyped. A null at the end of
|
|
15
|
+
* a path is still unknown: a field that collected nothing keeps its braces.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
export interface TemplateScope {
|
|
@@ -15,11 +23,38 @@ export interface TemplateScope {
|
|
|
15
23
|
|
|
16
24
|
const PLACEHOLDER = /\{\{\s*([^}\s]+)\s*\}\}/g;
|
|
17
25
|
|
|
26
|
+
/** The path ran into a `null` on its way down: the container the host named is absent. */
|
|
27
|
+
const ABSENT = Symbol("absent");
|
|
28
|
+
|
|
18
29
|
export function render(template: string, scope: TemplateScope): string {
|
|
19
|
-
|
|
30
|
+
let emptied = false;
|
|
31
|
+
const out = template.replace(PLACEHOLDER, (match, path: string) => {
|
|
20
32
|
const value = lookup(scope, path.split("."));
|
|
21
|
-
|
|
33
|
+
if (value === ABSENT) {
|
|
34
|
+
emptied = true;
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
if (value === undefined || value === null) return match;
|
|
38
|
+
const text = stringify(value);
|
|
39
|
+
if (text === "") emptied = true;
|
|
40
|
+
return text;
|
|
22
41
|
});
|
|
42
|
+
return emptied ? tidy(out) : out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Close the hole an empty value leaves: a doubled space, a space before
|
|
47
|
+
* punctuation, a space at the end of a line.
|
|
48
|
+
*
|
|
49
|
+
* Deliberately narrow. It only runs on a string where something substituted to
|
|
50
|
+
* "", and it only collapses a run of spaces that follows a visible character,
|
|
51
|
+
* so indentation in a markdown list survives.
|
|
52
|
+
*/
|
|
53
|
+
function tidy(text: string): string {
|
|
54
|
+
return text
|
|
55
|
+
.replace(/(?<=\S)[ \t]{2,}/g, " ")
|
|
56
|
+
.replace(/ +([,.;:!?\u2026])/g, "$1")
|
|
57
|
+
.replace(/[ \t]+$/gm, "");
|
|
23
58
|
}
|
|
24
59
|
|
|
25
60
|
/** `render` over every string inside a value, recursively. Non-strings pass through. */
|
|
@@ -37,7 +72,8 @@ export function renderDeep<T>(value: T, scope: TemplateScope): T {
|
|
|
37
72
|
function lookup(root: unknown, keys: string[]): unknown {
|
|
38
73
|
let current = root;
|
|
39
74
|
for (const key of keys) {
|
|
40
|
-
if (current === null
|
|
75
|
+
if (current === null) return ABSENT;
|
|
76
|
+
if (typeof current !== "object") return undefined;
|
|
41
77
|
current = (current as Record<string, unknown>)[key];
|
|
42
78
|
}
|
|
43
79
|
return current;
|