@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.
@@ -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;;;;;;;GAOG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAKrE;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,GAAG,CAAC,CAS/D"}
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
- return template.replace(PLACEHOLDER, (match, path) => {
25
+ let emptied = false;
26
+ const out = template.replace(PLACEHOLDER, (match, path) => {
16
27
  const value = lookup(scope, path.split("."));
17
- return value === undefined || value === null ? match : stringify(value);
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 || typeof current !== "object")
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;;;;;;;GAOG;;AAUH,wBAKC;AAGD,gCASC;AAnBD,MAAM,WAAW,GAAG,0BAA0B,CAAC;AAE/C,SAAgB,MAAM,CAAC,QAAgB,EAAE,KAAoB;IAC3D,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7C,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,wFAAwF;AACxF,SAAgB,UAAU,CAAI,KAAQ,EAAE,KAAoB;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,EAAE,KAAK,CAAM,CAAC;IAChE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAa,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAM,CAAC;IAC5F,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAC5D,CAAC;IACT,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,MAAM,CAAC,IAAa,EAAE,IAAc;IAC3C,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACtE,OAAO,GAAI,OAAmC,CAAC,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAClF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC"}
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../../../src/utils/template.ts"],"names":[],"mappings":"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"}
@@ -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;;;;;;;GAOG;AAEH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAKrE;AAED,wFAAwF;AACxF,wBAAgB,UAAU,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,GAAG,CAAC,CAS/D"}
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"}
@@ -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
- return template.replace(PLACEHOLDER, (match, path) => {
21
+ let emptied = false;
22
+ const out = template.replace(PLACEHOLDER, (match, path) => {
12
23
  const value = lookup(scope, path.split("."));
13
- return value === undefined || value === null ? match : stringify(value);
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 || typeof current !== "object")
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;;;;;;;GAOG;AAQH,MAAM,WAAW,GAAG,0BAA0B,CAAC;AAE/C,MAAM,UAAU,MAAM,CAAC,QAAgB,EAAE,KAAoB;IAC3D,OAAO,QAAQ,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,IAAY,EAAE,EAAE;QAC3D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7C,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;AACL,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,UAAU,CAAI,KAAQ,EAAE,KAAoB;IAC1D,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,KAAK,EAAE,KAAK,CAAM,CAAC;IAChE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAa,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAM,CAAC;IAC5F,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,OAAO,MAAM,CAAC,WAAW,CACvB,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAC5D,CAAC;IACT,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,MAAM,CAAC,IAAa,EAAE,IAAc;IAC3C,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,OAAO,SAAS,CAAC;QACtE,OAAO,GAAI,OAAmC,CAAC,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAClF,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;AAC/B,CAAC"}
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). A path that resolves to nothing keeps its `{{...}}`, so a typo stays visible.
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
- | `requires`, `skip`, `auto`, `reply`, step `hooks`, `prepare`, `finalize` | known-field skipping, `maxAsks`, `do`, `if`, `wait`, `say` |
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`, `!` exclusions | `Template = string` with `{{data.x}}` `{{context.x}}` `{{input.x}}`; `Pred` (function or JSON) |
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. A path that resolves to nothing keeps its placeholder, so a typo stays visible.
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.4",
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",
@@ -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
- return template.replace(PLACEHOLDER, (match, path: string) => {
30
+ let emptied = false;
31
+ const out = template.replace(PLACEHOLDER, (match, path: string) => {
20
32
  const value = lookup(scope, path.split("."));
21
- return value === undefined || value === null ? match : stringify(value);
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 || typeof current !== "object") return undefined;
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;