@falai/agent 4.0.0-alpha.5 → 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 +7 -4
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +16 -5
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/utils/template.d.ts +7 -4
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +16 -5
- package/dist/utils/template.js.map +1 -1
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/reference/actions-events-conditions.md +1 -1
- package/package.json +1 -1
- package/src/utils/template.ts +16 -5
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
8
|
*
|
|
9
|
-
* A path
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "Ola, tudo bem?"
|
|
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.
|
|
13
16
|
*/
|
|
14
17
|
export interface TemplateScope {
|
|
15
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"}
|
|
@@ -7,19 +7,28 @@
|
|
|
7
7
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
8
8
|
* of vanishing into an empty string.
|
|
9
9
|
*
|
|
10
|
-
* A path
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* "Ola, tudo bem?"
|
|
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.
|
|
14
17
|
*/
|
|
15
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
19
|
exports.render = render;
|
|
17
20
|
exports.renderDeep = renderDeep;
|
|
18
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");
|
|
19
24
|
function render(template, scope) {
|
|
20
25
|
let emptied = false;
|
|
21
26
|
const out = template.replace(PLACEHOLDER, (match, path) => {
|
|
22
27
|
const value = lookup(scope, path.split("."));
|
|
28
|
+
if (value === ABSENT) {
|
|
29
|
+
emptied = true;
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
23
32
|
if (value === undefined || value === null)
|
|
24
33
|
return match;
|
|
25
34
|
const text = stringify(value);
|
|
@@ -57,7 +66,9 @@ function renderDeep(value, scope) {
|
|
|
57
66
|
function lookup(root, keys) {
|
|
58
67
|
let current = root;
|
|
59
68
|
for (const key of keys) {
|
|
60
|
-
if (current === null
|
|
69
|
+
if (current === null)
|
|
70
|
+
return ABSENT;
|
|
71
|
+
if (typeof current !== "object")
|
|
61
72
|
return undefined;
|
|
62
73
|
current = current[key];
|
|
63
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
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
8
|
*
|
|
9
|
-
* A path
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "Ola, tudo bem?"
|
|
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.
|
|
13
16
|
*/
|
|
14
17
|
export interface TemplateScope {
|
|
15
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
|
@@ -6,16 +6,25 @@
|
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
8
|
*
|
|
9
|
-
* A path
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "Ola, tudo bem?"
|
|
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.
|
|
13
16
|
*/
|
|
14
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");
|
|
15
20
|
export function render(template, scope) {
|
|
16
21
|
let emptied = false;
|
|
17
22
|
const out = template.replace(PLACEHOLDER, (match, path) => {
|
|
18
23
|
const value = lookup(scope, path.split("."));
|
|
24
|
+
if (value === ABSENT) {
|
|
25
|
+
emptied = true;
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
19
28
|
if (value === undefined || value === null)
|
|
20
29
|
return match;
|
|
21
30
|
const text = stringify(value);
|
|
@@ -53,7 +62,9 @@ export function renderDeep(value, scope) {
|
|
|
53
62
|
function lookup(root, keys) {
|
|
54
63
|
let current = root;
|
|
55
64
|
for (const key of keys) {
|
|
56
|
-
if (current === null
|
|
65
|
+
if (current === null)
|
|
66
|
+
return ABSENT;
|
|
67
|
+
if (typeof current !== "object")
|
|
57
68
|
return undefined;
|
|
58
69
|
current = current[key];
|
|
59
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). An unknown path keeps its `{{...}}`, so a typo stays visible. A
|
|
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
|
|
|
@@ -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. An unknown path keeps its placeholder, so a typo stays visible. A
|
|
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"'`.
|
package/package.json
CHANGED
package/src/utils/template.ts
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* resolves to nothing keeps its placeholder, so a typo stays visible instead
|
|
7
7
|
* of vanishing into an empty string.
|
|
8
8
|
*
|
|
9
|
-
* A path
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* "Ola, tudo bem?"
|
|
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.
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
export interface TemplateScope {
|
|
@@ -20,10 +23,17 @@ export interface TemplateScope {
|
|
|
20
23
|
|
|
21
24
|
const PLACEHOLDER = /\{\{\s*([^}\s]+)\s*\}\}/g;
|
|
22
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
|
+
|
|
23
29
|
export function render(template: string, scope: TemplateScope): string {
|
|
24
30
|
let emptied = false;
|
|
25
31
|
const out = template.replace(PLACEHOLDER, (match, path: string) => {
|
|
26
32
|
const value = lookup(scope, path.split("."));
|
|
33
|
+
if (value === ABSENT) {
|
|
34
|
+
emptied = true;
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
27
37
|
if (value === undefined || value === null) return match;
|
|
28
38
|
const text = stringify(value);
|
|
29
39
|
if (text === "") emptied = true;
|
|
@@ -62,7 +72,8 @@ export function renderDeep<T>(value: T, scope: TemplateScope): T {
|
|
|
62
72
|
function lookup(root: unknown, keys: string[]): unknown {
|
|
63
73
|
let current = root;
|
|
64
74
|
for (const key of keys) {
|
|
65
|
-
if (current === null
|
|
75
|
+
if (current === null) return ABSENT;
|
|
76
|
+
if (typeof current !== "object") return undefined;
|
|
66
77
|
current = (current as Record<string, unknown>)[key];
|
|
67
78
|
}
|
|
68
79
|
return current;
|