@falai/agent 4.0.0-alpha.13 → 4.0.0-alpha.15

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 (245) hide show
  1. package/README.md +5 -3
  2. package/dist/cjs/core/Agent.js +9 -0
  3. package/dist/cjs/core/Agent.js.map +1 -1
  4. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
  5. package/dist/cjs/core/CompactionEngine.js +8 -3
  6. package/dist/cjs/core/CompactionEngine.js.map +1 -1
  7. package/dist/cjs/core/FlowSpec.d.ts +19 -2
  8. package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
  9. package/dist/cjs/core/FlowSpec.js +256 -57
  10. package/dist/cjs/core/FlowSpec.js.map +1 -1
  11. package/dist/cjs/core/Prompt.d.ts +16 -0
  12. package/dist/cjs/core/Prompt.d.ts.map +1 -1
  13. package/dist/cjs/core/Prompt.js +39 -0
  14. package/dist/cjs/core/Prompt.js.map +1 -1
  15. package/dist/cjs/core/Runner.d.ts +2 -0
  16. package/dist/cjs/core/Runner.d.ts.map +1 -1
  17. package/dist/cjs/core/Runner.js +26 -15
  18. package/dist/cjs/core/Runner.js.map +1 -1
  19. package/dist/cjs/core/Speak.d.ts.map +1 -1
  20. package/dist/cjs/core/Speak.js +22 -16
  21. package/dist/cjs/core/Speak.js.map +1 -1
  22. package/dist/cjs/core/Understand.d.ts +4 -3
  23. package/dist/cjs/core/Understand.d.ts.map +1 -1
  24. package/dist/cjs/core/Understand.js +5 -38
  25. package/dist/cjs/core/Understand.js.map +1 -1
  26. package/dist/cjs/core/contracts.d.ts +5 -5
  27. package/dist/cjs/core/contracts.d.ts.map +1 -1
  28. package/dist/cjs/index.d.ts +1 -1
  29. package/dist/cjs/index.d.ts.map +1 -1
  30. package/dist/cjs/index.js.map +1 -1
  31. package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
  32. package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
  33. package/dist/cjs/persistence/OpenSearchStore.js +2 -2
  34. package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
  35. package/dist/cjs/persistence/RedisStore.d.ts +1 -1
  36. package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
  37. package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
  38. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  39. package/dist/cjs/providers/AnthropicProvider.js +3 -4
  40. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  41. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
  42. package/dist/cjs/providers/DeepSeekProvider.js +3 -4
  43. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
  44. package/dist/cjs/providers/FallbackAiProvider.js +1 -1
  45. package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
  46. package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
  47. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  48. package/dist/cjs/providers/GeminiProvider.js +3 -4
  49. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  50. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
  51. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
  52. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  53. package/dist/cjs/providers/OpenAIProvider.js +3 -4
  54. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  55. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  56. package/dist/cjs/providers/OpenRouterProvider.js +4 -3
  57. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  58. package/dist/cjs/providers/ProviderAdapter.d.ts +12 -9
  59. package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
  60. package/dist/cjs/providers/ProviderAdapter.js +8 -3
  61. package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
  62. package/dist/cjs/providers/ZaiProvider.js +1 -1
  63. package/dist/cjs/providers/ZaiProvider.js.map +1 -1
  64. package/dist/cjs/types/agent.d.ts +1 -1
  65. package/dist/cjs/types/agent.d.ts.map +1 -1
  66. package/dist/cjs/types/ai.d.ts +4 -6
  67. package/dist/cjs/types/ai.d.ts.map +1 -1
  68. package/dist/cjs/types/compaction.d.ts +1 -0
  69. package/dist/cjs/types/compaction.d.ts.map +1 -1
  70. package/dist/cjs/types/errors.d.ts +4 -9
  71. package/dist/cjs/types/errors.d.ts.map +1 -1
  72. package/dist/cjs/types/errors.js +12 -12
  73. package/dist/cjs/types/errors.js.map +1 -1
  74. package/dist/cjs/types/history.d.ts +0 -7
  75. package/dist/cjs/types/history.d.ts.map +1 -1
  76. package/dist/cjs/types/index.d.ts +1 -1
  77. package/dist/cjs/types/index.d.ts.map +1 -1
  78. package/dist/cjs/types/index.js.map +1 -1
  79. package/dist/cjs/types/session.d.ts +1 -1
  80. package/dist/cjs/types/session.d.ts.map +1 -1
  81. package/dist/cjs/utils/clock.js +1 -1
  82. package/dist/cjs/utils/clock.js.map +1 -1
  83. package/dist/cjs/utils/schema.d.ts +2 -11
  84. package/dist/cjs/utils/schema.d.ts.map +1 -1
  85. package/dist/cjs/utils/schema.js +2 -43
  86. package/dist/cjs/utils/schema.js.map +1 -1
  87. package/dist/core/Agent.js +10 -1
  88. package/dist/core/Agent.js.map +1 -1
  89. package/dist/core/CompactionEngine.d.ts.map +1 -1
  90. package/dist/core/CompactionEngine.js +8 -3
  91. package/dist/core/CompactionEngine.js.map +1 -1
  92. package/dist/core/FlowSpec.d.ts +19 -2
  93. package/dist/core/FlowSpec.d.ts.map +1 -1
  94. package/dist/core/FlowSpec.js +254 -57
  95. package/dist/core/FlowSpec.js.map +1 -1
  96. package/dist/core/Prompt.d.ts +16 -0
  97. package/dist/core/Prompt.d.ts.map +1 -1
  98. package/dist/core/Prompt.js +37 -0
  99. package/dist/core/Prompt.js.map +1 -1
  100. package/dist/core/Runner.d.ts +2 -0
  101. package/dist/core/Runner.d.ts.map +1 -1
  102. package/dist/core/Runner.js +27 -16
  103. package/dist/core/Runner.js.map +1 -1
  104. package/dist/core/Speak.d.ts.map +1 -1
  105. package/dist/core/Speak.js +23 -17
  106. package/dist/core/Speak.js.map +1 -1
  107. package/dist/core/Understand.d.ts +4 -3
  108. package/dist/core/Understand.d.ts.map +1 -1
  109. package/dist/core/Understand.js +5 -38
  110. package/dist/core/Understand.js.map +1 -1
  111. package/dist/core/contracts.d.ts +5 -5
  112. package/dist/core/contracts.d.ts.map +1 -1
  113. package/dist/index.d.ts +1 -1
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js.map +1 -1
  116. package/dist/persistence/OpenSearchStore.d.ts +2 -1
  117. package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
  118. package/dist/persistence/OpenSearchStore.js +2 -2
  119. package/dist/persistence/OpenSearchStore.js.map +1 -1
  120. package/dist/persistence/RedisStore.d.ts +1 -1
  121. package/dist/persistence/RedisStore.d.ts.map +1 -1
  122. package/dist/providers/AnthropicProvider.d.ts +2 -5
  123. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  124. package/dist/providers/AnthropicProvider.js +3 -4
  125. package/dist/providers/AnthropicProvider.js.map +1 -1
  126. package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
  127. package/dist/providers/DeepSeekProvider.js +3 -4
  128. package/dist/providers/DeepSeekProvider.js.map +1 -1
  129. package/dist/providers/FallbackAiProvider.js +1 -1
  130. package/dist/providers/FallbackAiProvider.js.map +1 -1
  131. package/dist/providers/GeminiProvider.d.ts +1 -2
  132. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  133. package/dist/providers/GeminiProvider.js +3 -4
  134. package/dist/providers/GeminiProvider.js.map +1 -1
  135. package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
  136. package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
  137. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  138. package/dist/providers/OpenAIProvider.js +3 -4
  139. package/dist/providers/OpenAIProvider.js.map +1 -1
  140. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  141. package/dist/providers/OpenRouterProvider.js +4 -3
  142. package/dist/providers/OpenRouterProvider.js.map +1 -1
  143. package/dist/providers/ProviderAdapter.d.ts +12 -9
  144. package/dist/providers/ProviderAdapter.d.ts.map +1 -1
  145. package/dist/providers/ProviderAdapter.js +8 -3
  146. package/dist/providers/ProviderAdapter.js.map +1 -1
  147. package/dist/providers/ZaiProvider.js +1 -1
  148. package/dist/providers/ZaiProvider.js.map +1 -1
  149. package/dist/types/agent.d.ts +1 -1
  150. package/dist/types/agent.d.ts.map +1 -1
  151. package/dist/types/ai.d.ts +4 -6
  152. package/dist/types/ai.d.ts.map +1 -1
  153. package/dist/types/compaction.d.ts +1 -0
  154. package/dist/types/compaction.d.ts.map +1 -1
  155. package/dist/types/errors.d.ts +4 -9
  156. package/dist/types/errors.d.ts.map +1 -1
  157. package/dist/types/errors.js +12 -12
  158. package/dist/types/errors.js.map +1 -1
  159. package/dist/types/history.d.ts +0 -7
  160. package/dist/types/history.d.ts.map +1 -1
  161. package/dist/types/index.d.ts +1 -1
  162. package/dist/types/index.d.ts.map +1 -1
  163. package/dist/types/index.js.map +1 -1
  164. package/dist/types/session.d.ts +1 -1
  165. package/dist/types/session.d.ts.map +1 -1
  166. package/dist/utils/clock.js +1 -1
  167. package/dist/utils/clock.js.map +1 -1
  168. package/dist/utils/schema.d.ts +2 -11
  169. package/dist/utils/schema.d.ts.map +1 -1
  170. package/dist/utils/schema.js +2 -42
  171. package/dist/utils/schema.js.map +1 -1
  172. package/docs/concepts/pipeline.md +2 -2
  173. package/docs/concepts/runs-and-waits.md +1 -1
  174. package/docs/guides/actions-and-events.md +1 -1
  175. package/docs/guides/branching.md +1 -1
  176. package/docs/guides/compaction.md +2 -2
  177. package/docs/guides/error-handling.md +1 -1
  178. package/docs/guides/flow-control.md +1 -1
  179. package/docs/guides/persistence.md +1 -1
  180. package/docs/migration/v3-to-v4.md +2 -2
  181. package/docs/reference/agent.md +1 -1
  182. package/docs/reference/errors.md +10 -7
  183. package/docs/reference/flow-spec.md +29 -5
  184. package/docs/reference/outcomes.md +1 -1
  185. package/docs/reference/providers.md +2 -0
  186. package/docs/reference/step.md +1 -1
  187. package/docs/reference/stores.md +5 -3
  188. package/docs/start/01-install.md +5 -3
  189. package/package.json +2 -2
  190. package/src/core/Agent.ts +12 -1
  191. package/src/core/CompactionEngine.ts +10 -3
  192. package/src/core/FlowSpec.ts +263 -65
  193. package/src/core/Prompt.ts +40 -0
  194. package/src/core/Runner.ts +27 -14
  195. package/src/core/Speak.ts +23 -16
  196. package/src/core/Understand.ts +5 -39
  197. package/src/core/contracts.ts +5 -3
  198. package/src/index.ts +0 -1
  199. package/src/persistence/OpenSearchStore.ts +3 -2
  200. package/src/persistence/RedisStore.ts +1 -1
  201. package/src/providers/AnthropicProvider.ts +4 -9
  202. package/src/providers/DeepSeekProvider.ts +2 -4
  203. package/src/providers/FallbackAiProvider.ts +1 -1
  204. package/src/providers/GeminiProvider.ts +3 -6
  205. package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
  206. package/src/providers/OpenAIProvider.ts +3 -4
  207. package/src/providers/OpenRouterProvider.ts +4 -2
  208. package/src/providers/ProviderAdapter.ts +15 -12
  209. package/src/providers/ZaiProvider.ts +1 -1
  210. package/src/types/agent.ts +1 -1
  211. package/src/types/ai.ts +4 -6
  212. package/src/types/compaction.ts +1 -0
  213. package/src/types/errors.ts +11 -12
  214. package/src/types/history.ts +0 -10
  215. package/src/types/index.ts +0 -1
  216. package/src/types/session.ts +1 -1
  217. package/src/utils/clock.ts +1 -1
  218. package/src/utils/schema.ts +2 -48
  219. package/dist/cjs/providers/index.d.ts +0 -26
  220. package/dist/cjs/providers/index.d.ts.map +0 -1
  221. package/dist/cjs/providers/index.js +0 -30
  222. package/dist/cjs/providers/index.js.map +0 -1
  223. package/dist/cjs/utils/clone.d.ts +0 -8
  224. package/dist/cjs/utils/clone.d.ts.map +0 -1
  225. package/dist/cjs/utils/clone.js +0 -32
  226. package/dist/cjs/utils/clone.js.map +0 -1
  227. package/dist/cjs/utils/index.d.ts +0 -9
  228. package/dist/cjs/utils/index.d.ts.map +0 -1
  229. package/dist/cjs/utils/index.js +0 -29
  230. package/dist/cjs/utils/index.js.map +0 -1
  231. package/dist/providers/index.d.ts +0 -26
  232. package/dist/providers/index.d.ts.map +0 -1
  233. package/dist/providers/index.js +0 -17
  234. package/dist/providers/index.js.map +0 -1
  235. package/dist/utils/clone.d.ts +0 -8
  236. package/dist/utils/clone.d.ts.map +0 -1
  237. package/dist/utils/clone.js +0 -29
  238. package/dist/utils/clone.js.map +0 -1
  239. package/dist/utils/index.d.ts +0 -9
  240. package/dist/utils/index.d.ts.map +0 -1
  241. package/dist/utils/index.js +0 -9
  242. package/dist/utils/index.js.map +0 -1
  243. package/src/providers/index.ts +0 -38
  244. package/src/utils/clone.ts +0 -34
  245. package/src/utils/index.ts +0 -18
package/src/core/Speak.ts CHANGED
@@ -32,6 +32,7 @@ import { render, type TemplateScope } from "../utils/template.js";
32
32
  import { addUsage, readUsage } from "../utils/usage.js";
33
33
  import type { Deferral, SpeakOutcome, SpeakRequest, SpeakStreamChunk } from "./contracts.js";
34
34
  import {
35
+ Aliases,
35
36
  describeField,
36
37
  factsSection,
37
38
  instructionsSection,
@@ -90,8 +91,6 @@ function deferralOf(error: unknown): Deferral {
90
91
  ...(reset !== undefined ? { resetAtMs: reset } : {}),
91
92
  };
92
93
  }
93
- /** Gemini rejects any other envelope property name. */
94
- const WIRE_NAME = /^[a-zA-Z0-9_-]+$/;
95
94
 
96
95
  const GUIDELINE_HEADING = "## Guideline for your reply (adapt to the conversation)";
97
96
  const DEFAULT_GUIDELINE =
@@ -104,6 +103,9 @@ const FINAL_SECTION =
104
103
  "## Wrap up\nAnswer the customer now, using the tool results in the conversation. Do not call any tools.";
105
104
  const SPEAK_FIRST =
106
105
  "## Situation\nThere is no new message from the customer. You speak first: open naturally, do not answer a question nobody asked.";
106
+ /** The retry wake after a provider failure, when the customer's last message is still unanswered. */
107
+ const ANSWER_PENDING =
108
+ "## Situation\nThe customer's last message, in the conversation above, has no answer yet. Answer it now.";
107
109
 
108
110
  interface ToolCall {
109
111
  toolName: string;
@@ -165,7 +167,6 @@ export class Speak<C = unknown, D = unknown> {
165
167
  let history: History = req.history;
166
168
  const fields: Record<string, unknown> = {};
167
169
  const data: Record<string, unknown> = {};
168
- const toolCalls: ToolCall[] = [];
169
170
  let llmCalls = 0;
170
171
  let usage: TokenUsage | undefined;
171
172
  let message = "";
@@ -220,14 +221,13 @@ export class Speak<C = unknown, D = unknown> {
220
221
  }
221
222
  history = [...history, ...executed.items];
222
223
  Object.assign(data, executed.data);
223
- toolCalls.push(...read.toolCalls);
224
224
  }
225
225
 
226
226
  if (!message.trim()) {
227
227
  logger.warn(`[Speak] the model returned no message after ${llmCalls} call(s); deferring.`);
228
228
  return { deferred: UNAVAILABLE, llmCalls, ...(usage ? { usage } : {}) };
229
229
  }
230
- return { spoken: { message, fields, data, toolCalls, llmCalls, ...(usage ? { usage } : {}) } };
230
+ return { spoken: { message, fields, data, llmCalls, ...(usage ? { usage } : {}) } };
231
231
  }
232
232
 
233
233
  /** One provider call. Streaming yields clean message deltas; both paths return the same shape. */
@@ -331,7 +331,7 @@ function buildPrompt<C, D>(
331
331
  "idle" in talk
332
332
  ? [guideline(talk.idle.prompt)]
333
333
  : [
334
- `## Flow\n${talk.flow.name}${talk.flow.description ? `: ${talk.flow.description}` : ""}`,
334
+ `## Flow\n${talk.flow.name}${talk.flow.description ? `: ${render(talk.flow.description, scope)}` : ""}`,
335
335
  guideline(talk.step.prompt ?? (talk.pending.length ? DEFAULT_GUIDELINE : ANSWER_GUIDELINE)),
336
336
  pendingSection(talk.pending, options.fields, talk.step.ask ?? {}, scope),
337
337
  // `Partial<D>` is a mapped type; the guard is how it reaches an index-signature parameter without a cast.
@@ -344,17 +344,26 @@ function buildPrompt<C, D>(
344
344
  inline,
345
345
  ...body,
346
346
  instructionsSection([{ caption: "[Always]", items: req.instructions }], scope),
347
- inputSection(req.input),
347
+ inputSection(req.input, req.history),
348
348
  formatSection(envelope, options.fields),
349
349
  ),
350
350
  };
351
351
  }
352
352
 
353
- /** The customer's latest text, quoted. Without one, on anything but a message, the assistant opens the exchange. */
354
- function inputSection(input: SpeakRequest["input"]): string | null {
353
+ /**
354
+ * The customer's latest text, quoted. Without one, on anything but a message,
355
+ * the assistant opens the exchange. The one exception is the retry of a step
356
+ * the provider failed, while the history still ends with the customer's
357
+ * message: that message gets its answer. A history that merely ends with a
358
+ * customer message is not enough, because a host may skip a message on
359
+ * purpose (an away message, a filtered first contact) and a follow-up must
360
+ * not answer it.
361
+ */
362
+ function inputSection(input: SpeakRequest["input"], history: History): string | null {
355
363
  const text = input.text?.trim();
356
364
  if (text) return `## Customer's latest message\n"${text}"`;
357
- return input.kind === "message" ? null : SPEAK_FIRST;
365
+ if (input.kind === "message") return null;
366
+ return input.retry && history.at(-1)?.role === "user" ? ANSWER_PENDING : SPEAK_FIRST;
358
367
  }
359
368
 
360
369
  /** The envelope restated in words, for providers that follow the schema by prompt only. */
@@ -385,16 +394,14 @@ function formatSection(envelope: Envelope, fields: FieldDefs): string {
385
394
  // ── Envelope ────────────────────────────────────────────────────────────
386
395
 
387
396
  function buildEnvelope(fields: FieldDefs, pending: string[]): Envelope {
397
+ const aliases = new Aliases(["message"]);
388
398
  const wire = new Map<string, string>();
389
399
  const defs: FieldDefs = { message: { type: "string" } };
390
- pending.forEach((slug, i) => {
391
- // ponytail: a slug spelled like the reply key or with characters Gemini rejects
392
- // rides under an alias; the table maps it back. Ceiling: a real slug named
393
- // `field_N` could collide with an alias. Upgrade: bump the alias until free.
394
- const name = WIRE_NAME.test(slug) && slug !== "message" ? slug : `field_${i}`;
400
+ for (const slug of pending) {
401
+ const name = aliases.of(slug, "field_");
395
402
  wire.set(slug, name);
396
403
  defs[name] = fields[slug] ?? { type: "string" };
397
- });
404
+ }
398
405
  return { schema: toWireSchema(defs, { nullable: true }), wire };
399
406
  }
400
407
 
@@ -6,9 +6,10 @@
6
6
  * hands back raw values keyed by their real ids. It never writes session
7
7
  * data: Runner validates, coerces and applies.
8
8
  *
9
- * Two shortcuts cost zero calls: a single eligible message flow with nothing
10
- * else to judge starts outright, and a turn with nothing AI-conditioned
11
- * returns an empty Understanding.
9
+ * Zero calls when there is nothing to judge: no candidate flow, or only the
10
+ * floor's own, and no mention, branch or field. A lone eligible message flow
11
+ * starts unscored only when no catch-all passes and `idle` is `'silent'`;
12
+ * Runner decides that and sends no request. Otherwise this call scores it.
12
13
  *
13
14
  * The envelope is built for two kinds of provider at once. Gemini enforces
14
15
  * the schema, so every section is a closed object with every property
@@ -28,15 +29,12 @@ import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/sche
28
29
  import { render, type TemplateScope } from "../utils/template.js";
29
30
  import { readUsage } from "../utils/usage.js";
30
31
  import type { UnderstandRequest, Understanding } from "./contracts.js";
31
- import { describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
32
+ import { Aliases, describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
32
33
 
33
34
  export const UNDERSTAND_SCHEMA_NAME = "understand";
34
35
 
35
36
  const SECTIONS = ["flows", "mentions", "extract", "branches", "fields"] as const;
36
37
 
37
- /** Gemini rejects any other character in a property name. */
38
- const SAFE_KEY = /^[a-zA-Z0-9_-]+$/;
39
-
40
38
  export class Understand<C = unknown, D = unknown> {
41
39
  constructor(private readonly options: AgentOptions<C, D>) {}
42
40
 
@@ -111,38 +109,6 @@ function candidateFlows<C, D>(req: UnderstandRequest<C, D>): Flow<C, D>[] {
111
109
  return [...seen.values()];
112
110
  }
113
111
 
114
- /**
115
- * Envelope property names for ids the schema cannot carry. A safe id keeps
116
- * its own name; anything else (run ids carry `#`, `:` and `/`) gets a short
117
- * alias, mapped back when the reply is parsed.
118
- */
119
- class Aliases {
120
- private readonly byReal = new Map<string, string>();
121
- private readonly byAlias = new Map<string, string>();
122
- private n = 0;
123
-
124
- of(real: string, prefix = "k"): string {
125
- const seen = this.byReal.get(real);
126
- if (seen) return seen;
127
- const alias = SAFE_KEY.test(real) && !this.byAlias.has(real) ? real : this.fresh(prefix);
128
- this.byAlias.set(alias, real);
129
- this.byReal.set(real, alias);
130
- return alias;
131
- }
132
-
133
- /** Unknown aliases come back as they are; Runner drops keys it does not know. */
134
- real(alias: string): string {
135
- return this.byAlias.get(alias) ?? alias;
136
- }
137
-
138
- private fresh(prefix: string): string {
139
- let alias: string;
140
- do alias = `${prefix}${++this.n}`;
141
- while (this.byAlias.has(alias));
142
- return alias;
143
- }
144
- }
145
-
146
112
  function branchKey(branch: { runId: string; stepId: string; index: number }): string {
147
113
  return `${branch.runId}/${branch.stepId}/${branch.index}`;
148
114
  }
@@ -70,8 +70,11 @@ export interface IdleRequest<C = unknown, D = unknown> {
70
70
 
71
71
  export interface SpeakRequest<C = unknown, D = unknown> {
72
72
  talk: TalkRequest<C, D> | IdleRequest<C, D>;
73
- /** What started this turn. A wake has no text: the assistant speaks first. */
74
- input: { kind: InputKind; text?: string };
73
+ /**
74
+ * What started this turn. A wake has no text: the assistant speaks first, unless `retry` says this
75
+ * talk step failed at the provider and is running again, with the customer's message still unanswered.
76
+ */
77
+ input: { kind: InputKind; text?: string; retry?: boolean };
75
78
  context: C;
76
79
  data: Partial<D>;
77
80
  history: History;
@@ -87,7 +90,6 @@ export interface Spoken {
87
90
  fields: Record<string, unknown>;
88
91
  /** Data patches returned by tools, merged in call order. */
89
92
  data: Record<string, unknown>;
90
- toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
91
93
  llmCalls: number;
92
94
  usage?: TokenUsage;
93
95
  }
package/src/index.ts CHANGED
@@ -111,7 +111,6 @@ export type {
111
111
  ConditionSpec,
112
112
  DoStep,
113
113
  Duration,
114
- EmittedEvent,
115
114
  EndReason,
116
115
  Event,
117
116
  EventDef,
@@ -39,6 +39,7 @@ export interface OpenSearchClient {
39
39
  }
40
40
 
41
41
  export interface OpenSearchStoreOptions {
42
+ client: OpenSearchClient;
42
43
  /** Index name. Default `agent_sessions`. */
43
44
  indices?: { sessions?: string };
44
45
  /** Create the index with its mappings on `initialize()`. Default true. */
@@ -58,8 +59,8 @@ export class OpenSearchStore<D = unknown> implements Store<D> {
58
59
  private readonly autoCreateIndices: boolean;
59
60
  private readonly refresh: Refresh;
60
61
 
61
- constructor(client: OpenSearchClient, options: OpenSearchStoreOptions = {}) {
62
- this.client = client;
62
+ constructor(options: OpenSearchStoreOptions) {
63
+ this.client = options.client;
63
64
  this.index = options.indices?.sessions ?? "agent_sessions";
64
65
  this.autoCreateIndices = options.autoCreateIndices ?? true;
65
66
  this.refresh = options.refresh ?? false;
@@ -23,7 +23,7 @@ export interface RedisStoreOptions {
23
23
  redis: RedisClient;
24
24
  /** Prefix of every key. Default `agent:`. */
25
25
  keyPrefix?: string;
26
- /** Seconds a session lives after its last save; `0` keeps it forever. Default 7 days. */
26
+ /** Seconds a session lives after its last save; `0` keeps it forever. Default 7 days. Keep it above your longest wait: an expired session loses its parked runs and claims. */
27
27
  sessionTTL?: number;
28
28
  }
29
29
 
@@ -30,11 +30,8 @@ export interface AnthropicProviderOptions {
30
30
  /** Idle-stream deadline and retry budget */
31
31
  retryConfig?: { timeout?: number; retries?: number };
32
32
  /**
33
- * Replacement `fetch`, for tests that script the wire.
34
- *
35
- * v3 note: this replaces the injected SDK client. There is no SDK now, so a
36
- * test drives the same bytes the provider really receives rather than an
37
- * SDK's idea of them.
33
+ * Replacement `fetch`, for tests that script the wire. There is no vendor
34
+ * SDK, so a test drives the same bytes the provider really receives.
38
35
  */
39
36
  fetchImpl?: typeof fetch;
40
37
  }
@@ -50,10 +47,8 @@ export class AnthropicProvider extends ProviderAdapter {
50
47
  };
51
48
 
52
49
  constructor(options: AnthropicProviderOptions) {
53
- if (!options.apiKey) throw new Error("Anthropic API key is required");
54
- if (!options.model) {
55
- throw new Error("Model is required. Example: 'claude-sonnet-5' or 'claude-opus-5'");
56
- }
50
+ if (!options.apiKey) throw new Error(`[AnthropicProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.ANTHROPIC_API_KEY } and check the variable is set.`);
51
+ if (!options.model) throw new Error(`[AnthropicProvider] model is empty: there is no default. Pass one, e.g. { model: "claude-sonnet-5" }.`);
57
52
 
58
53
  super({
59
54
  provider: createAnthropicProvider({
@@ -51,10 +51,8 @@ export class DeepSeekProvider extends OpenAICompatibleProvider {
51
51
  };
52
52
 
53
53
  constructor(options: DeepSeekProviderOptions) {
54
- if (!options.apiKey) throw new Error("DeepSeek API key is required");
55
- if (!options.model) {
56
- throw new Error("Model is required. Example: 'deepseek-chat' or 'deepseek-reasoner'");
57
- }
54
+ if (!options.apiKey) throw new Error(`[DeepSeekProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.DEEPSEEK_API_KEY } and check the variable is set.`);
55
+ if (!options.model) throw new Error(`[DeepSeekProvider] model is empty: there is no default. Pass one, e.g. { model: "deepseek-chat" }.`);
58
56
 
59
57
  super({
60
58
  id: "deepseek",
@@ -38,7 +38,7 @@ export class FallbackAiProvider implements AiProvider {
38
38
 
39
39
  constructor(options: FallbackAiProviderOptions) {
40
40
  if (!options.providers || options.providers.length === 0) {
41
- throw new Error("FallbackAiProvider requires at least one provider");
41
+ throw new Error("[FallbackAiProvider] providers is empty: there is nothing to call. Pass at least one, e.g. { providers: [primary, backup] }.");
42
42
  }
43
43
 
44
44
  this.name = `fallback(${options.providers.map((p) => p.name).join("->")})`;
@@ -40,8 +40,7 @@ export interface GeminiProviderOptions {
40
40
  config?: RequestConfig;
41
41
  /** Idle-stream deadline and retry budget */
42
42
  retryConfig?: { timeout?: number; retries?: number };
43
- /** Replacement `fetch`, for tests that script the wire. Replaces v2's
44
- * injected SDK client — see {@link AnthropicProviderOptions.fetchImpl}. */
43
+ /** Replacement `fetch`, for tests that script the wire — see {@link AnthropicProviderOptions.fetchImpl}. */
45
44
  fetchImpl?: typeof fetch;
46
45
  }
47
46
 
@@ -56,10 +55,8 @@ export class GeminiProvider extends ProviderAdapter {
56
55
  };
57
56
 
58
57
  constructor(options: GeminiProviderOptions) {
59
- if (!options.apiKey) throw new Error("Gemini API key is required");
60
- if (!options.model) {
61
- throw new Error("Model is required. Example: 'gemini-3.1-pro-preview'");
62
- }
58
+ if (!options.apiKey) throw new Error(`[GeminiProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.GEMINI_API_KEY } and check the variable is set.`);
59
+ if (!options.model) throw new Error(`[GeminiProvider] model is empty: there is no default. Pass one, e.g. { model: "gemini-2.5-flash" }.`);
63
60
 
64
61
  super({
65
62
  provider: createGeminiProvider({
@@ -97,18 +97,18 @@ class GenericOpenAICompatibleProvider extends OpenAICompatibleProvider {
97
97
 
98
98
  constructor(options: OpenAICompatibleOptions) {
99
99
  if (!options.name) {
100
- throw new Error("An OpenAI-compatible provider needs a `name`.");
100
+ throw new Error('[OpenAICompatibleProvider] name is empty: it labels the provider in logs and errors. Pass one, e.g. { name: "ollama" }.');
101
101
  }
102
102
  if (!options.baseURL) {
103
- throw new Error(`[${options.name}] A \`baseURL\` is required.`);
103
+ throw new Error(`[${options.name}] baseURL is empty: there is no server to call. Pass one, e.g. { baseURL: "http://localhost:11434/v1" }.`);
104
104
  }
105
105
  if (!options.apiKey) {
106
106
  throw new Error(
107
- `[${options.name}] An \`apiKey\` is required — use any non-empty string for servers that ignore it.`
107
+ `[${options.name}] apiKey is empty: the provider cannot authenticate. Pass your key, or any non-empty string for a server that ignores it.`
108
108
  );
109
109
  }
110
110
  if (!options.model) {
111
- throw new Error(`[${options.name}] A \`model\` is required.`);
111
+ throw new Error(`[${options.name}] model is empty: there is no default. Pass the id your server serves, e.g. { model: "llama3.3" }.`);
112
112
  }
113
113
 
114
114
  super({
@@ -39,14 +39,13 @@ export class OpenAIProvider extends OpenAICompatibleProvider {
39
39
  supportsNativeJsonSchema: true,
40
40
  supportsStreaming: true,
41
41
  supportsStreamingToolCalls: true,
42
- // v3: the shape auto-caches repeated prefixes and now reports the hit
43
- // count, which the old adapter never read.
42
+ // The shape auto-caches repeated prefixes and reports the hit count.
44
43
  supportsPromptCaching: true,
45
44
  };
46
45
 
47
46
  constructor(options: OpenAIProviderOptions) {
48
- if (!options.apiKey) throw new Error("OpenAI API key is required");
49
- if (!options.model) throw new Error("Model is required. Example: 'gpt-5.6' or 'gpt-5.5'");
47
+ if (!options.apiKey) throw new Error(`[OpenAIProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.OPENAI_API_KEY } and check the variable is set.`);
48
+ if (!options.model) throw new Error(`[OpenAIProvider] model is empty: there is no default. Pass one, e.g. { model: "gpt-5.6" }.`);
50
49
 
51
50
  super({
52
51
  id: "openai",
@@ -59,8 +59,10 @@ export class OpenRouterProvider extends OpenAICompatibleProvider {
59
59
  };
60
60
 
61
61
  constructor(options: OpenRouterProviderOptions) {
62
- if (!options.apiKey) throw new Error("OpenRouter API key is required");
63
- if (!options.model) throw new Error("Model is required. See https://openrouter.ai/models");
62
+ if (!options.apiKey) throw new Error(`[OpenRouterProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.OPENROUTER_API_KEY } and check the variable is set.`);
63
+ if (!options.model) {
64
+ throw new Error(`[OpenRouterProvider] model is empty: there is no default. Pass one, e.g. { model: "anthropic/claude-sonnet-5" }; the ids are listed at https://openrouter.ai/models.`);
65
+ }
64
66
 
65
67
  super({
66
68
  id: "openrouter",
@@ -61,11 +61,9 @@ export interface RetryConfig {
61
61
  /**
62
62
  * How long a stream may stay SILENT before it is considered wedged.
63
63
  *
64
- * In v2 this was a total wall-clock cap on a non-streaming call, so a healthy
65
- * but long generation died at the one-minute mark. It now bounds silence
66
- * instead: time to the first byte, and the gap between any two after it. A
67
- * request that never gets a reply still fails at the same moment; one that is
68
- * steadily producing tokens is left alone.
64
+ * It bounds silence, not the whole call: time to the first byte, and the gap
65
+ * between any two after it. A request that never gets a reply fails at that
66
+ * moment; one that is steadily producing tokens is left alone, however long.
69
67
  */
70
68
  timeout: number;
71
69
  /** Retries AFTER the first attempt, so `0` still performs one call. */
@@ -90,10 +88,8 @@ export function resolveRetryConfig(input?: { timeout?: number; retries?: number
90
88
  /**
91
89
  * Request defaults sent with every call.
92
90
  *
93
- * Only the fields every supported shape has. In v2 each provider took its own
94
- * vendor's SDK parameter type here — which is how one vendor's package ended
95
- * up in the dependency tree of consumers who used a different vendor, and how
96
- * a field set on one provider silently vanished on another.
91
+ * Only the fields every supported shape has, so a field set on one provider
92
+ * means the same on another and no vendor's package enters the dependency tree.
97
93
  */
98
94
  export interface RequestConfig {
99
95
  temperature?: number;
@@ -290,6 +286,8 @@ export abstract class ProviderAdapter implements AiProvider {
290
286
  public abstract readonly capabilities: ProviderCapabilities;
291
287
 
292
288
  protected readonly provider: Provider;
289
+ /** `provider` without the fallback chain: the one model this adapter was built for. */
290
+ private readonly primary: Provider;
293
291
  protected readonly primaryModel: string;
294
292
  protected readonly backupModels: string[];
295
293
  protected readonly retryConfig: RetryConfig;
@@ -304,6 +302,7 @@ export abstract class ProviderAdapter implements AiProvider {
304
302
  this.primaryModel = init.model;
305
303
  this.backupModels = init.backupModels ?? [];
306
304
  this.retryConfig = resolveRetryConfig(init.retryConfig);
305
+ this.primary = init.provider;
307
306
 
308
307
  if (init.fallbacks && init.fallbacks.length > 0) {
309
308
  const coreProviders: Provider[] = [
@@ -337,9 +336,14 @@ export abstract class ProviderAdapter implements AiProvider {
337
336
  * boot when it is `null` — that model cannot serve this framework's turns).
338
337
  * Log `calls`: `0/3 and 3/3` is what makes the next model swap's regression
339
338
  * obvious. Costs `samples × 2` short calls, and errors propagate.
339
+ *
340
+ * It asks the primary alone, never the fallback chain. The answer configures
341
+ * this adapter's model; a call the chain handed to a fallback would score
342
+ * another model as this one. Each fallback is an adapter of its own: probe it
343
+ * the same way.
340
344
  */
341
345
  async probeJsonWithTools(opts?: ProbeOptions): Promise<JsonWithToolsProbe> {
342
- return probeJsonWithTools(this.provider, {
346
+ return probeJsonWithTools(this.primary, {
343
347
  model: this.primaryModel,
344
348
  ...(opts ?? {}),
345
349
  });
@@ -468,8 +472,7 @@ export abstract class ProviderAdapter implements AiProvider {
468
472
  tokensUsed: acc.promptTokens + (acc.completionTokens ?? 0),
469
473
  promptTokens: acc.promptTokens,
470
474
  completionTokens: acc.completionTokens,
471
- // New in v3: the cache-hit subset of the prompt, which the old
472
- // adapters never read and which is billed far cheaper.
475
+ // The cache-hit subset of the prompt, billed far cheaper.
473
476
  cachedInputTokens: acc.cachedInputTokens,
474
477
  }
475
478
  : {}),
@@ -51,7 +51,7 @@ export class ZaiProvider extends ProviderAdapter {
51
51
  };
52
52
 
53
53
  constructor(options: ZaiProviderOptions) {
54
- if (!options.apiKey) throw new Error("Z.ai Coding Plan API key is required");
54
+ if (!options.apiKey) throw new Error(`[ZaiProvider] apiKey is empty: the provider cannot authenticate. Pass { apiKey: process.env.ZAI_API_KEY } and check the variable is set.`);
55
55
 
56
56
  const model = options.model ?? "glm-5.3-flash";
57
57
  super({
@@ -145,7 +145,7 @@ export interface ScheduleEntry {
145
145
  export type EndReason = "end" | "flow" | "reset" | "skipped" | "failed" | "replaced";
146
146
 
147
147
  export interface TurnResult<D = unknown> {
148
- /** Version unchanged; the host bumps it on save. */
148
+ /** Version unchanged by a turn; `Store.save` bumps it and returns the saved session. */
149
149
  session: Session<D>;
150
150
  /** False: save nothing, send nothing. */
151
151
  changed: boolean;
package/src/types/ai.ts CHANGED
@@ -8,12 +8,10 @@ import type { HistoryItem } from "./history.js";
8
8
  /**
9
9
  * How hard the model thinks before answering.
10
10
  *
11
- * v3 takes `@providerkit/core`'s effort union, which every provider speaks:
12
- * "none" | "low" | "medium" | "high" | "max". Absent means the provider's own
13
- * default and is never sent. v2's "minimal" is spelled "low"; `summary` and
14
- * `includeThoughts` are gone because they are no longer choices — the shapes
15
- * that need them get them switched on whenever an effort is set, which is the
16
- * only setting that ever produced reasoning output.
11
+ * `@providerkit/core`'s effort union, which every provider speaks: "none" |
12
+ * "low" | "medium" | "high" | "max". Absent means the provider's own default
13
+ * and is never sent. The shapes that need a thought summary switched on get it
14
+ * whenever an effort is set, so there is no separate setting for it.
17
15
  */
18
16
  export interface ReasoningConfig {
19
17
  effort?: Effort;
@@ -9,6 +9,7 @@ import type { HistoryItem } from "./history.js";
9
9
  * Configuration for the compaction engine.
10
10
  *
11
11
  * Validation constraints:
12
+ * - `maxTokens` must be > 0
12
13
  * - `compactionThreshold` must be between 0.5 and 0.95
13
14
  * - `preserveRecentCount` must be >= 2
14
15
  * - `maxToolResultChars` must be > 0
@@ -5,16 +5,11 @@
5
5
  /**
6
6
  * The normalized provider failure, and the kinds it comes in.
7
7
  *
8
- * v3 re-exports these from `@providerkit/core` rather than keeping a second
9
- * copy. The taxonomy is wider there — thirteen kinds named by what actually
10
- * fixes them, where this package had eight — so a caller can now tell an
8
+ * `ProviderError` comes from `@providerkit/core` and is re-exported here. Its
9
+ * thirteen kinds are named by what fixes them, so a caller can tell an
11
10
  * exhausted balance from a per-minute throttle, a plan that never included the
12
- * API from a wrong key, and an outgrown context window from a bad request.
13
- *
14
- * Breaking: the kind lives on `error.kind`, not `error.code`, and the spellings
15
- * changed with the taxonomy (`rate_limited` is `rate`, `overloaded` is
16
- * `overload`, `invalid_request` splits into `invalid`, `context`, `model` and
17
- * `content`). `schema_rejected` is gone: nothing ever produced it.
11
+ * API from a wrong key, and an outgrown context window from a bad request. The
12
+ * kind lives on `error.kind`.
18
13
  */
19
14
  export { ProviderError } from "@providerkit/core";
20
15
  export type { ErrorKind } from "@providerkit/core";
@@ -33,9 +28,13 @@ export class SessionConflictError extends Error {
33
28
  public readonly actualVersion: number | undefined,
34
29
  ) {
35
30
  super(
36
- `[SessionConflictError] Session "${sessionId}" was modified concurrently: ` +
37
- `expected version ${expectedVersion}, found ${actualVersion ?? 'none'}. ` +
38
- `Reload the session and retry the operation.`
31
+ // A row that vanished between load and save (a TTL expiry, a delete) is not a race.
32
+ actualVersion === undefined && expectedVersion > 0
33
+ ? `[SessionConflictError] Session "${sessionId}" is gone from the store: it was at version ${expectedVersion} ` +
34
+ `and has since been deleted or expired. Load it again; a load that finds nothing starts a new conversation.`
35
+ : `[SessionConflictError] Session "${sessionId}" was modified concurrently: ` +
36
+ `expected version ${expectedVersion}, found ${actualVersion ?? 'none'}. ` +
37
+ `Reload the session and retry the operation.`
39
38
  );
40
39
  this.name = 'SessionConflictError';
41
40
  }
@@ -176,13 +176,3 @@ export interface Event<
176
176
  /** Unique event identifier */
177
177
  id?: string;
178
178
  }
179
-
180
- /**
181
- * An emitted event (staged for inclusion)
182
- */
183
- export interface EmittedEvent<
184
- TData = MessageEventData | ToolEventData | StatusEventData
185
- > extends Event<TData> {
186
- /** Whether this event has been committed */
187
- committed?: boolean;
188
- }
@@ -85,7 +85,6 @@ export type {
85
85
 
86
86
  export type {
87
87
  AssistantHistoryItem,
88
- EmittedEvent,
89
88
  Event,
90
89
  History,
91
90
  HistoryItem,
@@ -127,7 +127,7 @@ export interface StepOutcome {
127
127
  export interface Session<D = unknown> {
128
128
  id: string;
129
129
  v: 4;
130
- /** Optimistic-concurrency version; the host bumps it on save. */
130
+ /** Optimistic-concurrency version. A turn leaves it unchanged; `Store.save` bumps it and returns the saved session. */
131
131
  version: number;
132
132
  data: Partial<D>;
133
133
  /** Live runs only. */
@@ -34,7 +34,7 @@ export function fakeClock(iso: string): FakeClock {
34
34
 
35
35
  function parseIso(iso: string): number {
36
36
  const ms = Date.parse(iso);
37
- if (Number.isNaN(ms)) throw new Error(`fakeClock: "${iso}" is not a date.`);
37
+ if (Number.isNaN(ms)) throw new Error(`[fakeClock] "${iso}" is not a date: Date.parse cannot read it. Pass ISO text, e.g. "2026-09-20T10:00:00.000Z".`);
38
38
  return ms;
39
39
  }
40
40