@falai/agent 4.0.0-alpha.1 → 4.0.0-alpha.10

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.
Files changed (118) hide show
  1. package/dist/cjs/core/Agent.d.ts +8 -1
  2. package/dist/cjs/core/Agent.d.ts.map +1 -1
  3. package/dist/cjs/core/Agent.js +9 -0
  4. package/dist/cjs/core/Agent.js.map +1 -1
  5. package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
  6. package/dist/cjs/core/FlowSpec.js +53 -2
  7. package/dist/cjs/core/FlowSpec.js.map +1 -1
  8. package/dist/cjs/core/Prompt.d.ts.map +1 -1
  9. package/dist/cjs/core/Prompt.js +7 -1
  10. package/dist/cjs/core/Prompt.js.map +1 -1
  11. package/dist/cjs/core/Runner.d.ts +14 -3
  12. package/dist/cjs/core/Runner.d.ts.map +1 -1
  13. package/dist/cjs/core/Runner.js +47 -20
  14. package/dist/cjs/core/Runner.js.map +1 -1
  15. package/dist/cjs/core/Speak.d.ts.map +1 -1
  16. package/dist/cjs/core/Speak.js +11 -2
  17. package/dist/cjs/core/Speak.js.map +1 -1
  18. package/dist/cjs/core/Understand.d.ts.map +1 -1
  19. package/dist/cjs/core/Understand.js +17 -13
  20. package/dist/cjs/core/Understand.js.map +1 -1
  21. package/dist/cjs/index.d.ts +1 -1
  22. package/dist/cjs/index.d.ts.map +1 -1
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/providers/ProviderAdapter.d.ts +10 -5
  25. package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
  26. package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
  27. package/dist/cjs/providers/ZaiProvider.d.ts +6 -4
  28. package/dist/cjs/providers/ZaiProvider.d.ts.map +1 -1
  29. package/dist/cjs/providers/ZaiProvider.js +6 -4
  30. package/dist/cjs/providers/ZaiProvider.js.map +1 -1
  31. package/dist/cjs/types/agent.d.ts +12 -2
  32. package/dist/cjs/types/agent.d.ts.map +1 -1
  33. package/dist/cjs/types/index.d.ts +1 -1
  34. package/dist/cjs/types/index.d.ts.map +1 -1
  35. package/dist/cjs/types/index.js.map +1 -1
  36. package/dist/cjs/utils/phrases.d.ts +25 -0
  37. package/dist/cjs/utils/phrases.d.ts.map +1 -0
  38. package/dist/cjs/utils/phrases.js +38 -0
  39. package/dist/cjs/utils/phrases.js.map +1 -0
  40. package/dist/cjs/utils/template.d.ts +8 -0
  41. package/dist/cjs/utils/template.d.ts.map +1 -1
  42. package/dist/cjs/utils/template.js +40 -3
  43. package/dist/cjs/utils/template.js.map +1 -1
  44. package/dist/core/Agent.d.ts +8 -1
  45. package/dist/core/Agent.d.ts.map +1 -1
  46. package/dist/core/Agent.js +9 -0
  47. package/dist/core/Agent.js.map +1 -1
  48. package/dist/core/FlowSpec.d.ts.map +1 -1
  49. package/dist/core/FlowSpec.js +53 -2
  50. package/dist/core/FlowSpec.js.map +1 -1
  51. package/dist/core/Prompt.d.ts.map +1 -1
  52. package/dist/core/Prompt.js +7 -1
  53. package/dist/core/Prompt.js.map +1 -1
  54. package/dist/core/Runner.d.ts +14 -3
  55. package/dist/core/Runner.d.ts.map +1 -1
  56. package/dist/core/Runner.js +47 -20
  57. package/dist/core/Runner.js.map +1 -1
  58. package/dist/core/Speak.d.ts.map +1 -1
  59. package/dist/core/Speak.js +11 -2
  60. package/dist/core/Speak.js.map +1 -1
  61. package/dist/core/Understand.d.ts.map +1 -1
  62. package/dist/core/Understand.js +17 -13
  63. package/dist/core/Understand.js.map +1 -1
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/providers/ProviderAdapter.d.ts +10 -5
  68. package/dist/providers/ProviderAdapter.d.ts.map +1 -1
  69. package/dist/providers/ProviderAdapter.js.map +1 -1
  70. package/dist/providers/ZaiProvider.d.ts +6 -4
  71. package/dist/providers/ZaiProvider.d.ts.map +1 -1
  72. package/dist/providers/ZaiProvider.js +6 -4
  73. package/dist/providers/ZaiProvider.js.map +1 -1
  74. package/dist/types/agent.d.ts +12 -2
  75. package/dist/types/agent.d.ts.map +1 -1
  76. package/dist/types/index.d.ts +1 -1
  77. package/dist/types/index.d.ts.map +1 -1
  78. package/dist/types/index.js.map +1 -1
  79. package/dist/utils/phrases.d.ts +25 -0
  80. package/dist/utils/phrases.d.ts.map +1 -0
  81. package/dist/utils/phrases.js +35 -0
  82. package/dist/utils/phrases.js.map +1 -0
  83. package/dist/utils/template.d.ts +8 -0
  84. package/dist/utils/template.d.ts.map +1 -1
  85. package/dist/utils/template.js +40 -3
  86. package/dist/utils/template.js.map +1 -1
  87. package/docs/concepts/architecture.md +2 -2
  88. package/docs/concepts/pipeline.md +4 -4
  89. package/docs/guides/actions-and-events.md +1 -1
  90. package/docs/guides/conditions.md +1 -1
  91. package/docs/guides/error-handling.md +2 -0
  92. package/docs/guides/persistence.md +1 -1
  93. package/docs/guides/testing.md +1 -1
  94. package/docs/guides/triggers.md +3 -3
  95. package/docs/migration/v3-to-v4.md +12 -7
  96. package/docs/reference/actions-events-conditions.md +1 -1
  97. package/docs/reference/agent.md +8 -4
  98. package/docs/reference/flow-spec.md +1 -1
  99. package/docs/reference/providers.md +2 -2
  100. package/docs/reference/trigger.md +22 -2
  101. package/docs/rfc/v4-one-flow.md +5 -5
  102. package/docs/start/04-add-tools.md +1 -1
  103. package/docs/start/05-go-to-production.md +16 -1
  104. package/examples/06-triggers-and-waits.ts +4 -3
  105. package/package.json +5 -3
  106. package/src/core/Agent.ts +11 -1
  107. package/src/core/FlowSpec.ts +72 -6
  108. package/src/core/Prompt.ts +7 -1
  109. package/src/core/Runner.ts +54 -21
  110. package/src/core/Speak.ts +11 -2
  111. package/src/core/Understand.ts +12 -11
  112. package/src/index.ts +1 -0
  113. package/src/providers/ProviderAdapter.ts +10 -5
  114. package/src/providers/ZaiProvider.ts +6 -4
  115. package/src/types/agent.ts +13 -2
  116. package/src/types/index.ts +1 -0
  117. package/src/utils/phrases.ts +40 -0
  118. package/src/utils/template.ts +39 -3
package/src/core/Speak.ts CHANGED
@@ -78,8 +78,17 @@ function deferralOf(error: unknown): Deferral {
78
78
  // A usage window that says when it reopens is worth exactly one wake, then.
79
79
  // Without that number, waiting is guessing, and the ladder guesses in minutes
80
80
  // at a limit measured in hours.
81
- const retryable = RETRY_KINDS.has(kind) || (kind === "quota" && reset !== undefined);
82
- return { code: DEFER_CODE[kind], retryable, ...(reset !== undefined ? { resetAtMs: reset } : {}) };
81
+ const inferred = RETRY_KINDS.has(kind) || (kind === "quota" && reset !== undefined);
82
+ // Unless the provider said so outright. `x-should-retry` is the one answer
83
+ // nobody has to infer, and core already puts it above its own transience
84
+ // test — a 503 that says don't costs five wakes and ten model calls here if
85
+ // this reads the kind instead.
86
+ const stated = error instanceof ProviderError ? error.shouldRetry : undefined;
87
+ return {
88
+ code: DEFER_CODE[kind],
89
+ retryable: stated ?? inferred,
90
+ ...(reset !== undefined ? { resetAtMs: reset } : {}),
91
+ };
83
92
  }
84
93
  /** Gemini rejects any other envelope property name. */
85
94
  const WIRE_NAME = /^[a-zA-Z0-9_-]+$/;
@@ -23,6 +23,7 @@ import type { FieldDef, FieldDefs, Flow, ParamDef, ParamDefs } from "../types/fl
23
23
  import type { StructuredSchema } from "../types/schema.js";
24
24
  import { extractEmbeddedJSONObject, isRecord } from "../utils/json.js";
25
25
  import { logger } from "../utils/logger.js";
26
+ import { splitPhrases } from "../utils/phrases.js";
26
27
  import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/schema.js";
27
28
  import { render, type TemplateScope } from "../utils/template.js";
28
29
  import { readUsage } from "../utils/usage.js";
@@ -43,12 +44,9 @@ export class Understand<C = unknown, D = unknown> {
43
44
  const candidates = candidateFlows(req);
44
45
  const onlyRouting =
45
46
  req.mentionFlows.length === 0 && req.branches.length === 0 && Object.keys(req.fields).length === 0;
46
- if (onlyRouting && candidates.length <= 1) {
47
- // One eligible flow and nobody on the floor: it starts, no scoring.
48
- // Otherwise there is nothing to compare or extract.
49
- const only = candidates[0];
50
- return only && !req.floor ? { ...empty(0), flows: { [only.id]: 100 } } : empty(0);
51
- }
47
+ // Nothing to compare or extract: no candidates, or only the floor's own flow. A lone
48
+ // candidate with nobody on the floor is here because the runner wants it scored.
49
+ if (onlyRouting && candidates.length <= (req.floor ? 1 : 0)) return empty(0);
52
50
 
53
51
  const aliases = new Aliases();
54
52
  const jsonSchema = buildEnvelope(req, candidates, aliases);
@@ -245,8 +243,9 @@ function flowsSection<C, D>(candidates: Flow<C, D>[], aliases: Aliases, t: (text
245
243
  ];
246
244
  candidates.forEach((flow, i) => {
247
245
  lines.push(`${i + 1}. ${aliases.of(flow.id, "f")} — ${flow.name}${flow.description ? `: ${t(flow.description)}` : ""}`);
248
- const phrases = triggerPhrases(flow, "message");
249
- if (phrases.length) lines.push(` The customer: ${phrases.map(t).join("; ")}`);
246
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "message"));
247
+ if (counts.length) lines.push(` The customer: ${counts.map(t).join("; ")}`);
248
+ if (excludes.length) lines.push(` Score 0 when: ${excludes.map(t).join("; ")}`);
250
249
  });
251
250
  lines.push(
252
251
  "",
@@ -265,12 +264,14 @@ function mentionsSection<C, D>(flows: Flow<C, D>[], aliases: Aliases, t: (text:
265
264
  const lines = [
266
265
  "## Things the customer may mention",
267
266
  "For each item, answer true when the customer's message clearly brings it up, false otherwise. " +
268
- "Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough.",
267
+ "Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough. " +
268
+ "A 'Does not count when' line overrides a match: if one of those fits, answer false.",
269
269
  ];
270
270
  for (const flow of flows) {
271
271
  lines.push(`- ${aliases.of(flow.id, "f")} — ${flow.name}${flow.description ? `: ${t(flow.description)}` : ""}`);
272
- const phrases = triggerPhrases(flow, "mention");
273
- if (phrases.length) lines.push(` Counts when: ${phrases.map(t).join("; ")}`);
272
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "mention"));
273
+ if (counts.length) lines.push(` Counts when: ${counts.map(t).join("; ")}`);
274
+ if (excludes.length) lines.push(` Does not count when: ${excludes.map(t).join("; ")}`);
274
275
  const defs = extractDefs(flow);
275
276
  if (defs) {
276
277
  lines.push(" When true, also extract:");
package/src/index.ts CHANGED
@@ -137,6 +137,7 @@ export type {
137
137
  ParamDef,
138
138
  ParamDefs,
139
139
  Participant,
140
+ PendingWakesInput,
140
141
  Pred,
141
142
  PredCtx,
142
143
  ProviderCapabilities,
@@ -101,11 +101,16 @@ export interface RequestConfig {
101
101
  maxTokens?: number;
102
102
  stopSequences?: string[];
103
103
  /**
104
- * How hard the model thinks, bound as this provider's default. Absent means
105
- * the model's own dynamic thinking, which is what every shape does when the
106
- * field is never sent — so `"none"` is the only way to say *don't*, and it
107
- * matters most under a small `maxTokens`, where thinking tokens come out of
108
- * the same budget as the answer and can consume all of it.
104
+ * How hard the model thinks, bound as this provider's default. What absent
105
+ * means is the shape's own business, not one rule: on Gemini it is the
106
+ * model's dynamic thinking; on the OpenAI and OpenRouter dialects nothing is
107
+ * sent and not thinking is the default anyway; on the Anthropic shape — so
108
+ * `AnthropicProvider` and `ZaiProvider` — `@providerkit/core` resolves an
109
+ * absent effort to `"none"`, and Z.ai is sent an explicit disabled marker
110
+ * because its endpoint reads silence as thinking ON. Set a level to ask for
111
+ * thinking where it is off; it matters most under a small `maxTokens`, where
112
+ * thinking tokens come out of the same budget as the answer and can consume
113
+ * all of it.
109
114
  */
110
115
  effort?: Effort;
111
116
  }
@@ -6,10 +6,12 @@
6
6
  * `@providerkit/core` (adapter + preset); this file is the constructor, the
7
7
  * family default for cheap high-volume work.
8
8
  *
9
- * Dialect notes (measured 2026-09-13, see the core preset for detail):
10
- * model ids are BARE (`glm-5.3-flash`, not `z-ai/glm-5.3-flash`), and
11
- * "no thinking" must be an explicit marker — silence means the model default,
12
- * which is thinking ON.
9
+ * Dialect notes (see the core preset for detail): model ids are BARE
10
+ * (`glm-5.3-flash`, not `z-ai/glm-5.3-flash`), and the endpoint reads an
11
+ * absent `thinking` field as thinking ON, so the preset says "no" out loud.
12
+ * That makes thinking OFF here by default (measured 2026-09-21: a call with no
13
+ * `config.effort` sends `thinking: { type: "disabled" }`); ask for it with
14
+ * `config: { effort: 'high' }`.
13
15
  */
14
16
 
15
17
  import { createZaiCodingProvider, type FallbackOptions, type Provider } from "@providerkit/core";
@@ -87,7 +87,11 @@ export type TurnBase<C = unknown, D = unknown> = ContextField<C> & {
87
87
  sessionId: string;
88
88
  /** Absent on a first turn. A wake never creates a session. */
89
89
  session?: Session<D>;
90
- /** Pass on every input kind, wakes included. */
90
+ /**
91
+ * The conversation BEFORE this input. Pass it on every input kind, wakes included.
92
+ * Leave out the message this turn carries: both calls quote it on their own, so a
93
+ * history that ends with it makes the model read it twice.
94
+ */
91
95
  history?: History;
92
96
  silenced?: Silenced;
93
97
  /** Host anchors this session belongs to, e.g. `{ lead: { key: 'lead:456', lastInboundAt } }`. */
@@ -108,6 +112,13 @@ export type TurnKind =
108
112
 
109
113
  export type TurnInput<C = unknown, D = unknown> = TurnBase<C, D> & TurnKind;
110
114
 
115
+ /** A saved session and the host inputs a turn on it would carry: what `pendingWakes` reads. */
116
+ export type PendingWakesInput<C = unknown, D = unknown> = ContextField<C> & {
117
+ session: Session<D>;
118
+ anchors?: TurnBase<C, D>["anchors"];
119
+ claims?: TurnBase<C, D>["claims"];
120
+ };
121
+
111
122
  // ── turn() result ───────────────────────────────────────────────────────
112
123
 
113
124
  export interface OutboundMessage {
@@ -123,7 +134,7 @@ export interface OutboundMessage {
123
134
  stepId?: string;
124
135
  }
125
136
 
126
- /** A wake to enqueue with `jobId = key`; at fire time call `turn({ wake: key })`. */
137
+ /** A wake to enqueue, keyed by `key` (encode it as a queue job id: BullMQ refuses a `:`); at fire time call `turn({ wake: key })`. */
127
138
  export interface ScheduleEntry {
128
139
  key: string;
129
140
  at: Date;
@@ -10,6 +10,7 @@ export type {
10
10
  EndReason,
11
11
  Idle,
12
12
  OutboundMessage,
13
+ PendingWakesInput,
13
14
  ScheduleEntry,
14
15
  Silenced,
15
16
  TurnBase,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Splitting an AI-judged phrase list into what counts and what rules it out.
3
+ *
4
+ * A trigger's phrases are alternatives: one match is enough. That makes the
5
+ * list useless for saying "but not this" — and "but not this" is what keeps a
6
+ * classifier honest. A customer answering "pode sim" to an offer of a meeting
7
+ * is agreeing to the meeting, not asking for a human, and without a way to say
8
+ * so every cheerful yes reads as a handoff request.
9
+ *
10
+ * So a phrase that opens with `!` is an exclusion: it does not make the trigger
11
+ * fire, it stops it. Exclusions win over matches, and the model is told both
12
+ * lists separately.
13
+ */
14
+
15
+ export interface Phrases {
16
+ /** Any one of these makes it true. */
17
+ counts: string[];
18
+ /** Any one of these makes it false, whatever matched. */
19
+ excludes: string[];
20
+ }
21
+
22
+ /**
23
+ * Split a phrase list on the leading `!`. Blank entries and a bare `"!"` are
24
+ * dropped: they would render as an empty bullet and mean nothing to the model.
25
+ */
26
+ export function splitPhrases(phrases: readonly string[]): Phrases {
27
+ const counts: string[] = [];
28
+ const excludes: string[] = [];
29
+ for (const phrase of phrases) {
30
+ const trimmed = phrase.trim();
31
+ if (!trimmed) continue;
32
+ if (trimmed.startsWith("!")) {
33
+ const body = trimmed.slice(1).trim();
34
+ if (body) excludes.push(body);
35
+ continue;
36
+ }
37
+ counts.push(trimmed);
38
+ }
39
+ return { counts, excludes };
40
+ }
@@ -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;