@falai/agent 4.0.0-alpha.2 → 4.0.0-alpha.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.
Files changed (41) hide show
  1. package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
  2. package/dist/cjs/core/FlowSpec.js +12 -0
  3. package/dist/cjs/core/FlowSpec.js.map +1 -1
  4. package/dist/cjs/core/Prompt.d.ts.map +1 -1
  5. package/dist/cjs/core/Prompt.js +7 -1
  6. package/dist/cjs/core/Prompt.js.map +1 -1
  7. package/dist/cjs/core/Speak.d.ts.map +1 -1
  8. package/dist/cjs/core/Speak.js +11 -2
  9. package/dist/cjs/core/Speak.js.map +1 -1
  10. package/dist/cjs/core/Understand.d.ts.map +1 -1
  11. package/dist/cjs/core/Understand.js +13 -7
  12. package/dist/cjs/core/Understand.js.map +1 -1
  13. package/dist/cjs/utils/phrases.d.ts +25 -0
  14. package/dist/cjs/utils/phrases.d.ts.map +1 -0
  15. package/dist/cjs/utils/phrases.js +38 -0
  16. package/dist/cjs/utils/phrases.js.map +1 -0
  17. package/dist/core/FlowSpec.d.ts.map +1 -1
  18. package/dist/core/FlowSpec.js +12 -0
  19. package/dist/core/FlowSpec.js.map +1 -1
  20. package/dist/core/Prompt.d.ts.map +1 -1
  21. package/dist/core/Prompt.js +7 -1
  22. package/dist/core/Prompt.js.map +1 -1
  23. package/dist/core/Speak.d.ts.map +1 -1
  24. package/dist/core/Speak.js +11 -2
  25. package/dist/core/Speak.js.map +1 -1
  26. package/dist/core/Understand.d.ts.map +1 -1
  27. package/dist/core/Understand.js +13 -7
  28. package/dist/core/Understand.js.map +1 -1
  29. package/dist/utils/phrases.d.ts +25 -0
  30. package/dist/utils/phrases.d.ts.map +1 -0
  31. package/dist/utils/phrases.js +35 -0
  32. package/dist/utils/phrases.js.map +1 -0
  33. package/docs/guides/error-handling.md +2 -0
  34. package/docs/migration/v3-to-v4.md +5 -2
  35. package/docs/reference/trigger.md +20 -0
  36. package/package.json +1 -1
  37. package/src/core/FlowSpec.ts +18 -0
  38. package/src/core/Prompt.ts +7 -1
  39. package/src/core/Speak.ts +11 -2
  40. package/src/core/Understand.ts +9 -5
  41. package/src/utils/phrases.ts +40 -0
@@ -9,6 +9,7 @@
9
9
 
10
10
  import type { AgentOptions } from "../types/agent.js";
11
11
  import type { FieldDef, Instruction } from "../types/flow.js";
12
+ import { splitPhrases } from "../utils/phrases.js";
12
13
  import { isKnown } from "../utils/schema.js";
13
14
  import { render, type TemplateScope } from "../utils/template.js";
14
15
 
@@ -106,7 +107,12 @@ export function instructionsSection<C, D>(groups: InstructionGroup<C, D>[], scop
106
107
  const text = render(item.prompt, scope).trim();
107
108
  if (!text) continue;
108
109
  const when = item.when === undefined ? [] : Array.isArray(item.when) ? item.when : [item.when];
109
- const condition = when.length ? ` (apply only when: ${when.join(" OR ")})` : "";
110
+ const { counts, excludes } = splitPhrases(when);
111
+ const clauses = [
112
+ ...(counts.length ? [`apply only when: ${counts.join(" OR ")}`] : []),
113
+ ...(excludes.length ? [`never when: ${excludes.join(" OR ")}`] : []),
114
+ ];
115
+ const condition = clauses.length ? ` (${clauses.join("; ")})` : "";
110
116
  lines.push(`- [${item.kind ?? "should"}] ${group.caption} ${text}${condition}`);
111
117
  }
112
118
  }
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";
@@ -245,8 +246,9 @@ function flowsSection<C, D>(candidates: Flow<C, D>[], aliases: Aliases, t: (text
245
246
  ];
246
247
  candidates.forEach((flow, i) => {
247
248
  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("; ")}`);
249
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "message"));
250
+ if (counts.length) lines.push(` The customer: ${counts.map(t).join("; ")}`);
251
+ if (excludes.length) lines.push(` Score 0 when: ${excludes.map(t).join("; ")}`);
250
252
  });
251
253
  lines.push(
252
254
  "",
@@ -265,12 +267,14 @@ function mentionsSection<C, D>(flows: Flow<C, D>[], aliases: Aliases, t: (text:
265
267
  const lines = [
266
268
  "## Things the customer may mention",
267
269
  "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.",
270
+ "Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough. " +
271
+ "A 'Does not count when' line overrides a match: if one of those fits, answer false.",
269
272
  ];
270
273
  for (const flow of flows) {
271
274
  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("; ")}`);
275
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "mention"));
276
+ if (counts.length) lines.push(` Counts when: ${counts.map(t).join("; ")}`);
277
+ if (excludes.length) lines.push(` Does not count when: ${excludes.map(t).join("; ")}`);
274
278
  const defs = extractDefs(flow);
275
279
  if (defs) {
276
280
  lines.push(" When true, also extract:");
@@ -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
+ }