@falai/agent 4.0.0-alpha.2 → 4.0.0-alpha.20

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 (300) hide show
  1. package/README.md +5 -3
  2. package/dist/cjs/core/Agent.d.ts +8 -1
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +18 -0
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
  7. package/dist/cjs/core/CompactionEngine.js +8 -3
  8. package/dist/cjs/core/CompactionEngine.js.map +1 -1
  9. package/dist/cjs/core/FlowSpec.d.ts +21 -2
  10. package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
  11. package/dist/cjs/core/FlowSpec.js +324 -51
  12. package/dist/cjs/core/FlowSpec.js.map +1 -1
  13. package/dist/cjs/core/Migrate.d.ts.map +1 -1
  14. package/dist/cjs/core/Migrate.js +3 -1
  15. package/dist/cjs/core/Migrate.js.map +1 -1
  16. package/dist/cjs/core/Prompt.d.ts +16 -0
  17. package/dist/cjs/core/Prompt.d.ts.map +1 -1
  18. package/dist/cjs/core/Prompt.js +46 -1
  19. package/dist/cjs/core/Prompt.js.map +1 -1
  20. package/dist/cjs/core/Runner.d.ts +37 -4
  21. package/dist/cjs/core/Runner.d.ts.map +1 -1
  22. package/dist/cjs/core/Runner.js +269 -74
  23. package/dist/cjs/core/Runner.js.map +1 -1
  24. package/dist/cjs/core/Speak.d.ts.map +1 -1
  25. package/dist/cjs/core/Speak.js +50 -19
  26. package/dist/cjs/core/Speak.js.map +1 -1
  27. package/dist/cjs/core/Understand.d.ts +4 -3
  28. package/dist/cjs/core/Understand.d.ts.map +1 -1
  29. package/dist/cjs/core/Understand.js +22 -51
  30. package/dist/cjs/core/Understand.js.map +1 -1
  31. package/dist/cjs/core/contracts.d.ts +11 -6
  32. package/dist/cjs/core/contracts.d.ts.map +1 -1
  33. package/dist/cjs/index.d.ts +1 -1
  34. package/dist/cjs/index.d.ts.map +1 -1
  35. package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
  36. package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
  37. package/dist/cjs/persistence/OpenSearchStore.js +2 -2
  38. package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
  39. package/dist/cjs/persistence/RedisStore.d.ts +1 -1
  40. package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
  41. package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
  42. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  43. package/dist/cjs/providers/AnthropicProvider.js +3 -4
  44. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  45. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
  46. package/dist/cjs/providers/DeepSeekProvider.js +3 -4
  47. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
  48. package/dist/cjs/providers/FallbackAiProvider.js +1 -1
  49. package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
  50. package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
  51. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  52. package/dist/cjs/providers/GeminiProvider.js +3 -4
  53. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  54. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
  55. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
  56. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  57. package/dist/cjs/providers/OpenAIProvider.js +3 -4
  58. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  59. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  60. package/dist/cjs/providers/OpenRouterProvider.js +4 -3
  61. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  62. package/dist/cjs/providers/ProviderAdapter.d.ts +15 -10
  63. package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
  64. package/dist/cjs/providers/ProviderAdapter.js +8 -3
  65. package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
  66. package/dist/cjs/providers/ZaiProvider.js +1 -1
  67. package/dist/cjs/providers/ZaiProvider.js.map +1 -1
  68. package/dist/cjs/types/agent.d.ts +19 -4
  69. package/dist/cjs/types/agent.d.ts.map +1 -1
  70. package/dist/cjs/types/ai.d.ts +4 -6
  71. package/dist/cjs/types/ai.d.ts.map +1 -1
  72. package/dist/cjs/types/compaction.d.ts +1 -0
  73. package/dist/cjs/types/compaction.d.ts.map +1 -1
  74. package/dist/cjs/types/errors.d.ts +4 -9
  75. package/dist/cjs/types/errors.d.ts.map +1 -1
  76. package/dist/cjs/types/errors.js +12 -12
  77. package/dist/cjs/types/errors.js.map +1 -1
  78. package/dist/cjs/types/flow.d.ts +15 -1
  79. package/dist/cjs/types/flow.d.ts.map +1 -1
  80. package/dist/cjs/types/history.d.ts +0 -7
  81. package/dist/cjs/types/history.d.ts.map +1 -1
  82. package/dist/cjs/types/index.d.ts +2 -2
  83. package/dist/cjs/types/index.d.ts.map +1 -1
  84. package/dist/cjs/types/session.d.ts +4 -2
  85. package/dist/cjs/types/session.d.ts.map +1 -1
  86. package/dist/cjs/utils/clock.js +1 -1
  87. package/dist/cjs/utils/clock.js.map +1 -1
  88. package/dist/cjs/utils/outcomes.d.ts +1 -0
  89. package/dist/cjs/utils/outcomes.d.ts.map +1 -1
  90. package/dist/cjs/utils/outcomes.js +1 -0
  91. package/dist/cjs/utils/outcomes.js.map +1 -1
  92. package/dist/cjs/utils/phrases.d.ts +25 -0
  93. package/dist/cjs/utils/phrases.d.ts.map +1 -0
  94. package/dist/cjs/utils/phrases.js +38 -0
  95. package/dist/cjs/utils/phrases.js.map +1 -0
  96. package/dist/cjs/utils/schema.d.ts +3 -12
  97. package/dist/cjs/utils/schema.d.ts.map +1 -1
  98. package/dist/cjs/utils/schema.js +3 -43
  99. package/dist/cjs/utils/schema.js.map +1 -1
  100. package/dist/cjs/utils/template.d.ts +8 -0
  101. package/dist/cjs/utils/template.d.ts.map +1 -1
  102. package/dist/cjs/utils/template.js +47 -3
  103. package/dist/cjs/utils/template.js.map +1 -1
  104. package/dist/core/Agent.d.ts +8 -1
  105. package/dist/core/Agent.d.ts.map +1 -1
  106. package/dist/core/Agent.js +19 -1
  107. package/dist/core/Agent.js.map +1 -1
  108. package/dist/core/CompactionEngine.d.ts.map +1 -1
  109. package/dist/core/CompactionEngine.js +8 -3
  110. package/dist/core/CompactionEngine.js.map +1 -1
  111. package/dist/core/FlowSpec.d.ts +21 -2
  112. package/dist/core/FlowSpec.d.ts.map +1 -1
  113. package/dist/core/FlowSpec.js +323 -52
  114. package/dist/core/FlowSpec.js.map +1 -1
  115. package/dist/core/Migrate.d.ts.map +1 -1
  116. package/dist/core/Migrate.js +3 -1
  117. package/dist/core/Migrate.js.map +1 -1
  118. package/dist/core/Prompt.d.ts +16 -0
  119. package/dist/core/Prompt.d.ts.map +1 -1
  120. package/dist/core/Prompt.js +44 -1
  121. package/dist/core/Prompt.js.map +1 -1
  122. package/dist/core/Runner.d.ts +37 -4
  123. package/dist/core/Runner.d.ts.map +1 -1
  124. package/dist/core/Runner.js +270 -75
  125. package/dist/core/Runner.js.map +1 -1
  126. package/dist/core/Speak.d.ts.map +1 -1
  127. package/dist/core/Speak.js +51 -20
  128. package/dist/core/Speak.js.map +1 -1
  129. package/dist/core/Understand.d.ts +4 -3
  130. package/dist/core/Understand.d.ts.map +1 -1
  131. package/dist/core/Understand.js +22 -51
  132. package/dist/core/Understand.js.map +1 -1
  133. package/dist/core/contracts.d.ts +11 -6
  134. package/dist/core/contracts.d.ts.map +1 -1
  135. package/dist/index.d.ts +1 -1
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/persistence/OpenSearchStore.d.ts +2 -1
  138. package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
  139. package/dist/persistence/OpenSearchStore.js +2 -2
  140. package/dist/persistence/OpenSearchStore.js.map +1 -1
  141. package/dist/persistence/RedisStore.d.ts +1 -1
  142. package/dist/persistence/RedisStore.d.ts.map +1 -1
  143. package/dist/providers/AnthropicProvider.d.ts +2 -5
  144. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  145. package/dist/providers/AnthropicProvider.js +3 -4
  146. package/dist/providers/AnthropicProvider.js.map +1 -1
  147. package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
  148. package/dist/providers/DeepSeekProvider.js +3 -4
  149. package/dist/providers/DeepSeekProvider.js.map +1 -1
  150. package/dist/providers/FallbackAiProvider.js +1 -1
  151. package/dist/providers/FallbackAiProvider.js.map +1 -1
  152. package/dist/providers/GeminiProvider.d.ts +1 -2
  153. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  154. package/dist/providers/GeminiProvider.js +3 -4
  155. package/dist/providers/GeminiProvider.js.map +1 -1
  156. package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
  157. package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
  158. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  159. package/dist/providers/OpenAIProvider.js +3 -4
  160. package/dist/providers/OpenAIProvider.js.map +1 -1
  161. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  162. package/dist/providers/OpenRouterProvider.js +4 -3
  163. package/dist/providers/OpenRouterProvider.js.map +1 -1
  164. package/dist/providers/ProviderAdapter.d.ts +15 -10
  165. package/dist/providers/ProviderAdapter.d.ts.map +1 -1
  166. package/dist/providers/ProviderAdapter.js +8 -3
  167. package/dist/providers/ProviderAdapter.js.map +1 -1
  168. package/dist/providers/ZaiProvider.js +1 -1
  169. package/dist/providers/ZaiProvider.js.map +1 -1
  170. package/dist/types/agent.d.ts +19 -4
  171. package/dist/types/agent.d.ts.map +1 -1
  172. package/dist/types/ai.d.ts +4 -6
  173. package/dist/types/ai.d.ts.map +1 -1
  174. package/dist/types/compaction.d.ts +1 -0
  175. package/dist/types/compaction.d.ts.map +1 -1
  176. package/dist/types/errors.d.ts +4 -9
  177. package/dist/types/errors.d.ts.map +1 -1
  178. package/dist/types/errors.js +12 -12
  179. package/dist/types/errors.js.map +1 -1
  180. package/dist/types/flow.d.ts +15 -1
  181. package/dist/types/flow.d.ts.map +1 -1
  182. package/dist/types/history.d.ts +0 -7
  183. package/dist/types/history.d.ts.map +1 -1
  184. package/dist/types/index.d.ts +2 -2
  185. package/dist/types/index.d.ts.map +1 -1
  186. package/dist/types/session.d.ts +4 -2
  187. package/dist/types/session.d.ts.map +1 -1
  188. package/dist/utils/clock.js +1 -1
  189. package/dist/utils/clock.js.map +1 -1
  190. package/dist/utils/outcomes.d.ts +1 -0
  191. package/dist/utils/outcomes.d.ts.map +1 -1
  192. package/dist/utils/outcomes.js +1 -0
  193. package/dist/utils/outcomes.js.map +1 -1
  194. package/dist/utils/phrases.d.ts +25 -0
  195. package/dist/utils/phrases.d.ts.map +1 -0
  196. package/dist/utils/phrases.js +35 -0
  197. package/dist/utils/phrases.js.map +1 -0
  198. package/dist/utils/schema.d.ts +3 -12
  199. package/dist/utils/schema.d.ts.map +1 -1
  200. package/dist/utils/schema.js +3 -42
  201. package/dist/utils/schema.js.map +1 -1
  202. package/dist/utils/template.d.ts +8 -0
  203. package/dist/utils/template.d.ts.map +1 -1
  204. package/dist/utils/template.js +47 -3
  205. package/dist/utils/template.js.map +1 -1
  206. package/docs/concepts/architecture.md +3 -3
  207. package/docs/concepts/collection.md +40 -5
  208. package/docs/concepts/pipeline.md +12 -10
  209. package/docs/concepts/runs-and-waits.md +2 -2
  210. package/docs/guides/actions-and-events.md +2 -2
  211. package/docs/guides/branching.md +4 -2
  212. package/docs/guides/compaction.md +2 -2
  213. package/docs/guides/conditions.md +1 -1
  214. package/docs/guides/error-handling.md +3 -1
  215. package/docs/guides/flow-control.md +4 -2
  216. package/docs/guides/persistence.md +2 -2
  217. package/docs/guides/testing.md +1 -1
  218. package/docs/guides/triggers.md +5 -5
  219. package/docs/migration/v3-to-v4.md +16 -11
  220. package/docs/reference/actions-events-conditions.md +2 -2
  221. package/docs/reference/agent.md +11 -7
  222. package/docs/reference/branches.md +1 -1
  223. package/docs/reference/errors.md +10 -7
  224. package/docs/reference/fields.md +5 -3
  225. package/docs/reference/flow-spec.md +36 -9
  226. package/docs/reference/flow.md +8 -3
  227. package/docs/reference/outcomes.md +3 -2
  228. package/docs/reference/providers.md +2 -0
  229. package/docs/reference/session.md +2 -0
  230. package/docs/reference/step.md +8 -5
  231. package/docs/reference/stores.md +5 -3
  232. package/docs/reference/trigger.md +24 -4
  233. package/docs/rfc/v4-one-flow.md +7 -5
  234. package/docs/start/01-install.md +5 -3
  235. package/docs/start/04-add-tools.md +1 -1
  236. package/docs/start/05-go-to-production.md +18 -1
  237. package/examples/05-branches.ts +1 -1
  238. package/examples/06-triggers-and-waits.ts +4 -3
  239. package/package.json +5 -3
  240. package/src/core/Agent.ts +23 -2
  241. package/src/core/CompactionEngine.ts +10 -3
  242. package/src/core/FlowSpec.ts +352 -60
  243. package/src/core/Migrate.ts +2 -1
  244. package/src/core/Prompt.ts +47 -1
  245. package/src/core/Runner.ts +266 -71
  246. package/src/core/Speak.ts +53 -19
  247. package/src/core/Understand.ts +17 -50
  248. package/src/core/contracts.ts +11 -4
  249. package/src/index.ts +1 -1
  250. package/src/persistence/OpenSearchStore.ts +3 -2
  251. package/src/persistence/RedisStore.ts +1 -1
  252. package/src/providers/AnthropicProvider.ts +4 -9
  253. package/src/providers/DeepSeekProvider.ts +2 -4
  254. package/src/providers/FallbackAiProvider.ts +1 -1
  255. package/src/providers/GeminiProvider.ts +3 -6
  256. package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
  257. package/src/providers/OpenAIProvider.ts +3 -4
  258. package/src/providers/OpenRouterProvider.ts +4 -2
  259. package/src/providers/ProviderAdapter.ts +18 -13
  260. package/src/providers/ZaiProvider.ts +1 -1
  261. package/src/types/agent.ts +20 -5
  262. package/src/types/ai.ts +4 -6
  263. package/src/types/compaction.ts +1 -0
  264. package/src/types/errors.ts +11 -12
  265. package/src/types/flow.ts +15 -1
  266. package/src/types/history.ts +0 -10
  267. package/src/types/index.ts +1 -1
  268. package/src/types/session.ts +4 -1
  269. package/src/utils/clock.ts +1 -1
  270. package/src/utils/outcomes.ts +1 -0
  271. package/src/utils/phrases.ts +40 -0
  272. package/src/utils/schema.ts +3 -48
  273. package/src/utils/template.ts +46 -3
  274. package/dist/cjs/providers/index.d.ts +0 -26
  275. package/dist/cjs/providers/index.d.ts.map +0 -1
  276. package/dist/cjs/providers/index.js +0 -30
  277. package/dist/cjs/providers/index.js.map +0 -1
  278. package/dist/cjs/utils/clone.d.ts +0 -8
  279. package/dist/cjs/utils/clone.d.ts.map +0 -1
  280. package/dist/cjs/utils/clone.js +0 -32
  281. package/dist/cjs/utils/clone.js.map +0 -1
  282. package/dist/cjs/utils/index.d.ts +0 -9
  283. package/dist/cjs/utils/index.d.ts.map +0 -1
  284. package/dist/cjs/utils/index.js +0 -29
  285. package/dist/cjs/utils/index.js.map +0 -1
  286. package/dist/providers/index.d.ts +0 -26
  287. package/dist/providers/index.d.ts.map +0 -1
  288. package/dist/providers/index.js +0 -17
  289. package/dist/providers/index.js.map +0 -1
  290. package/dist/utils/clone.d.ts +0 -8
  291. package/dist/utils/clone.d.ts.map +0 -1
  292. package/dist/utils/clone.js +0 -29
  293. package/dist/utils/clone.js.map +0 -1
  294. package/dist/utils/index.d.ts +0 -9
  295. package/dist/utils/index.d.ts.map +0 -1
  296. package/dist/utils/index.js +0 -9
  297. package/dist/utils/index.js.map +0 -1
  298. package/src/providers/index.ts +0 -38
  299. package/src/utils/clone.ts +0 -34
  300. 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,
@@ -78,21 +79,33 @@ function deferralOf(error: unknown): Deferral {
78
79
  // A usage window that says when it reopens is worth exactly one wake, then.
79
80
  // Without that number, waiting is guessing, and the ladder guesses in minutes
80
81
  // 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 } : {}) };
82
+ const inferred = RETRY_KINDS.has(kind) || (kind === "quota" && reset !== undefined);
83
+ // Unless the provider said so outright. `x-should-retry` is the one answer
84
+ // nobody has to infer, and core already puts it above its own transience
85
+ // test — a 503 that says don't costs five wakes and ten model calls here if
86
+ // this reads the kind instead.
87
+ const stated = error instanceof ProviderError ? error.shouldRetry : undefined;
88
+ return {
89
+ code: DEFER_CODE[kind],
90
+ retryable: stated ?? inferred,
91
+ ...(reset !== undefined ? { resetAtMs: reset } : {}),
92
+ };
83
93
  }
84
- /** Gemini rejects any other envelope property name. */
85
- const WIRE_NAME = /^[a-zA-Z0-9_-]+$/;
86
94
 
87
95
  const GUIDELINE_HEADING = "## Guideline for your reply (adapt to the conversation)";
88
96
  const DEFAULT_GUIDELINE =
89
97
  "Collect what is still missing below, in the flow of the conversation, one or two things per message.";
98
+ /** A step with no prompt and nothing left to collect: the step an `onEnd: 'stay'` run answers from. */
99
+ const ANSWER_GUIDELINE = "Answer the customer's message, in the flow of the conversation.";
90
100
  const TOOLS_SECTION =
91
101
  "## Tools\nCall the tools provided when you need to look something up or act before answering. Once you have what you need, answer the customer.";
92
102
  const FINAL_SECTION =
93
103
  "## Wrap up\nAnswer the customer now, using the tool results in the conversation. Do not call any tools.";
94
104
  const SPEAK_FIRST =
95
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.";
96
109
 
97
110
  interface ToolCall {
98
111
  toolName: string;
@@ -154,7 +167,6 @@ export class Speak<C = unknown, D = unknown> {
154
167
  let history: History = req.history;
155
168
  const fields: Record<string, unknown> = {};
156
169
  const data: Record<string, unknown> = {};
157
- const toolCalls: ToolCall[] = [];
158
170
  let llmCalls = 0;
159
171
  let usage: TokenUsage | undefined;
160
172
  let message = "";
@@ -209,14 +221,13 @@ export class Speak<C = unknown, D = unknown> {
209
221
  }
210
222
  history = [...history, ...executed.items];
211
223
  Object.assign(data, executed.data);
212
- toolCalls.push(...read.toolCalls);
213
224
  }
214
225
 
215
226
  if (!message.trim()) {
216
227
  logger.warn(`[Speak] the model returned no message after ${llmCalls} call(s); deferring.`);
217
228
  return { deferred: UNAVAILABLE, llmCalls, ...(usage ? { usage } : {}) };
218
229
  }
219
- return { spoken: { message, fields, data, toolCalls, llmCalls, ...(usage ? { usage } : {}) } };
230
+ return { spoken: { message, fields, data, llmCalls, ...(usage ? { usage } : {}) } };
220
231
  }
221
232
 
222
233
  /** One provider call. Streaming yields clean message deltas; both paths return the same shape. */
@@ -320,8 +331,8 @@ function buildPrompt<C, D>(
320
331
  "idle" in talk
321
332
  ? [guideline(talk.idle.prompt)]
322
333
  : [
323
- `## Flow\n${talk.flow.name}${talk.flow.description ? `: ${talk.flow.description}` : ""}`,
324
- guideline(talk.step.prompt ?? DEFAULT_GUIDELINE),
334
+ `## Flow\n${talk.flow.name}${talk.flow.description ? `: ${render(talk.flow.description, scope)}` : ""}`,
335
+ guideline(talk.step.prompt ?? (talk.pending.length ? DEFAULT_GUIDELINE : ANSWER_GUIDELINE)),
325
336
  pendingSection(talk.pending, options.fields, talk.step.ask ?? {}, scope),
326
337
  // `Partial<D>` is a mapped type; the guard is how it reaches an index-signature parameter without a cast.
327
338
  factsSection(options.fields, isRecord(req.data) ? req.data : {}),
@@ -333,17 +344,42 @@ function buildPrompt<C, D>(
333
344
  inline,
334
345
  ...body,
335
346
  instructionsSection([{ caption: "[Always]", items: req.instructions }], scope),
336
- inputSection(req.input),
347
+ inputSection(req.input, req.history),
348
+ saidSection(req.said),
337
349
  formatSection(envelope, options.fields),
338
350
  ),
339
351
  };
340
352
  }
341
353
 
342
- /** The customer's latest text, quoted. Without one, on anything but a message, the assistant opens the exchange. */
343
- function inputSection(input: SpeakRequest["input"]): string | null {
354
+ /**
355
+ * The customer's latest text, quoted. Without one, on anything but a message,
356
+ * the assistant opens the exchange. The one exception is the retry of a step
357
+ * the provider failed, while the history still ends with the customer's
358
+ * message: that message gets its answer. A history that merely ends with a
359
+ * customer message is not enough, because a host may skip a message on
360
+ * purpose (an away message, a filtered first contact) and a follow-up must
361
+ * not answer it.
362
+ */
363
+ function inputSection(input: SpeakRequest["input"], history: History): string | null {
344
364
  const text = input.text?.trim();
345
365
  if (text) return `## Customer's latest message\n"${text}"`;
346
- return input.kind === "message" ? null : SPEAK_FIRST;
366
+ if (input.kind === "message") return null;
367
+ return input.retry && history.at(-1)?.role === "user" ? ANSWER_PENDING : SPEAK_FIRST;
368
+ }
369
+
370
+ /**
371
+ * What this turn already sent before the reply. It sits after the customer's message because that is
372
+ * when it went out; `history` stops before the turn, so it is the only place the model sees it.
373
+ */
374
+ function saidSection(said: SpeakRequest["said"]): string | null {
375
+ const lines = said.map(({ text, media }) =>
376
+ `- ${[text.trim() && `"${text.trim()}"`, media && `[media: ${media.slug}]`].filter(Boolean).join(" ")}`);
377
+ if (!lines.length) return null;
378
+ return [
379
+ "## Already sent",
380
+ "You already sent these messages this turn, in this order, and your reply goes out right after them. Do not repeat what they say.",
381
+ ...lines,
382
+ ].join("\n");
347
383
  }
348
384
 
349
385
  /** The envelope restated in words, for providers that follow the schema by prompt only. */
@@ -374,16 +410,14 @@ function formatSection(envelope: Envelope, fields: FieldDefs): string {
374
410
  // ── Envelope ────────────────────────────────────────────────────────────
375
411
 
376
412
  function buildEnvelope(fields: FieldDefs, pending: string[]): Envelope {
413
+ const aliases = new Aliases(["message"]);
377
414
  const wire = new Map<string, string>();
378
415
  const defs: FieldDefs = { message: { type: "string" } };
379
- pending.forEach((slug, i) => {
380
- // ponytail: a slug spelled like the reply key or with characters Gemini rejects
381
- // rides under an alias; the table maps it back. Ceiling: a real slug named
382
- // `field_N` could collide with an alias. Upgrade: bump the alias until free.
383
- const name = WIRE_NAME.test(slug) && slug !== "message" ? slug : `field_${i}`;
416
+ for (const slug of pending) {
417
+ const name = aliases.of(slug, "field_");
384
418
  wire.set(slug, name);
385
419
  defs[name] = fields[slug] ?? { type: "string" };
386
- });
420
+ }
387
421
  return { schema: toWireSchema(defs, { nullable: true }), wire };
388
422
  }
389
423
 
@@ -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
@@ -23,19 +24,17 @@ import type { FieldDef, FieldDefs, Flow, ParamDef, ParamDefs } from "../types/fl
23
24
  import type { StructuredSchema } from "../types/schema.js";
24
25
  import { extractEmbeddedJSONObject, isRecord } from "../utils/json.js";
25
26
  import { logger } from "../utils/logger.js";
27
+ import { splitPhrases } from "../utils/phrases.js";
26
28
  import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/schema.js";
27
29
  import { render, type TemplateScope } from "../utils/template.js";
28
30
  import { readUsage } from "../utils/usage.js";
29
31
  import type { UnderstandRequest, Understanding } from "./contracts.js";
30
- import { describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
32
+ import { Aliases, describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
31
33
 
32
34
  export const UNDERSTAND_SCHEMA_NAME = "understand";
33
35
 
34
36
  const SECTIONS = ["flows", "mentions", "extract", "branches", "fields"] as const;
35
37
 
36
- /** Gemini rejects any other character in a property name. */
37
- const SAFE_KEY = /^[a-zA-Z0-9_-]+$/;
38
-
39
38
  export class Understand<C = unknown, D = unknown> {
40
39
  constructor(private readonly options: AgentOptions<C, D>) {}
41
40
 
@@ -43,12 +42,9 @@ export class Understand<C = unknown, D = unknown> {
43
42
  const candidates = candidateFlows(req);
44
43
  const onlyRouting =
45
44
  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
- }
45
+ // Nothing to compare or extract: no candidates, or only the floor's own flow. A lone
46
+ // candidate with nobody on the floor is here because the runner wants it scored.
47
+ if (onlyRouting && candidates.length <= (req.floor ? 1 : 0)) return empty(0);
52
48
 
53
49
  const aliases = new Aliases();
54
50
  const jsonSchema = buildEnvelope(req, candidates, aliases);
@@ -113,38 +109,6 @@ function candidateFlows<C, D>(req: UnderstandRequest<C, D>): Flow<C, D>[] {
113
109
  return [...seen.values()];
114
110
  }
115
111
 
116
- /**
117
- * Envelope property names for ids the schema cannot carry. A safe id keeps
118
- * its own name; anything else (run ids carry `#`, `:` and `/`) gets a short
119
- * alias, mapped back when the reply is parsed.
120
- */
121
- class Aliases {
122
- private readonly byReal = new Map<string, string>();
123
- private readonly byAlias = new Map<string, string>();
124
- private n = 0;
125
-
126
- of(real: string, prefix = "k"): string {
127
- const seen = this.byReal.get(real);
128
- if (seen) return seen;
129
- const alias = SAFE_KEY.test(real) && !this.byAlias.has(real) ? real : this.fresh(prefix);
130
- this.byAlias.set(alias, real);
131
- this.byReal.set(real, alias);
132
- return alias;
133
- }
134
-
135
- /** Unknown aliases come back as they are; Runner drops keys it does not know. */
136
- real(alias: string): string {
137
- return this.byAlias.get(alias) ?? alias;
138
- }
139
-
140
- private fresh(prefix: string): string {
141
- let alias: string;
142
- do alias = `${prefix}${++this.n}`;
143
- while (this.byAlias.has(alias));
144
- return alias;
145
- }
146
- }
147
-
148
112
  function branchKey(branch: { runId: string; stepId: string; index: number }): string {
149
113
  return `${branch.runId}/${branch.stepId}/${branch.index}`;
150
114
  }
@@ -245,8 +209,9 @@ function flowsSection<C, D>(candidates: Flow<C, D>[], aliases: Aliases, t: (text
245
209
  ];
246
210
  candidates.forEach((flow, i) => {
247
211
  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("; ")}`);
212
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "message"));
213
+ if (counts.length) lines.push(` The customer: ${counts.map(t).join("; ")}`);
214
+ if (excludes.length) lines.push(` Score 0 when: ${excludes.map(t).join("; ")}`);
250
215
  });
251
216
  lines.push(
252
217
  "",
@@ -265,12 +230,14 @@ function mentionsSection<C, D>(flows: Flow<C, D>[], aliases: Aliases, t: (text:
265
230
  const lines = [
266
231
  "## Things the customer may mention",
267
232
  "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.",
233
+ "Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough. " +
234
+ "A 'Does not count when' line overrides a match: if one of those fits, answer false.",
269
235
  ];
270
236
  for (const flow of flows) {
271
237
  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("; ")}`);
238
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "mention"));
239
+ if (counts.length) lines.push(` Counts when: ${counts.map(t).join("; ")}`);
240
+ if (excludes.length) lines.push(` Does not count when: ${excludes.map(t).join("; ")}`);
274
241
  const defs = extractDefs(flow);
275
242
  if (defs) {
276
243
  lines.push(" When true, also extract:");
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { TokenUsage } from "../types/ai.js";
11
- import type { Idle } from "../types/agent.js";
11
+ import type { Idle, OutboundMessage } from "../types/agent.js";
12
12
  import type { FieldDef, Flow, Instruction, StepBase, TalkStep } from "../types/flow.js";
13
13
  import type { History } from "../types/history.js";
14
14
  import type { Run, StepOutcomeCode } from "../types/session.js";
@@ -70,11 +70,19 @@ 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;
81
+ /**
82
+ * What this turn already sent before the reply, in order: a `say`, a fixed question. `history` ends
83
+ * before the turn, so without these the model cannot see them and says the same thing again.
84
+ */
85
+ said: OutboundMessage[];
78
86
  now: Date;
79
87
  /** Already filtered by their `if`; `when` stays for the prompt. */
80
88
  instructions: Instruction<C, D>[];
@@ -87,7 +95,6 @@ export interface Spoken {
87
95
  fields: Record<string, unknown>;
88
96
  /** Data patches returned by tools, merged in call order. */
89
97
  data: Record<string, unknown>;
90
- toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
91
98
  llmCalls: number;
92
99
  usage?: TokenUsage;
93
100
  }
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,
@@ -137,6 +136,7 @@ export type {
137
136
  ParamDef,
138
137
  ParamDefs,
139
138
  Participant,
139
+ PendingWakesInput,
140
140
  Pred,
141
141
  PredCtx,
142
142
  ProviderCapabilities,
@@ -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;
@@ -104,7 +100,9 @@ export interface RequestConfig {
104
100
  * How hard the model thinks, bound as this provider's default. What absent
105
101
  * means is the shape's own business, not one rule: on Gemini it is the
106
102
  * 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
103
+ * sent, so the model decides — `medium` on GPT-5 and older, and on
104
+ * OpenRouter GLM 5.3 Flash thinks on most of the hosts that serve it. Set
105
+ * `"none"` where a turn must not think; on the Anthropic shape — so
108
106
  * `AnthropicProvider` and `ZaiProvider` — `@providerkit/core` resolves an
109
107
  * absent effort to `"none"`, and Z.ai is sent an explicit disabled marker
110
108
  * because its endpoint reads silence as thinking ON. Set a level to ask for
@@ -290,6 +288,8 @@ export abstract class ProviderAdapter implements AiProvider {
290
288
  public abstract readonly capabilities: ProviderCapabilities;
291
289
 
292
290
  protected readonly provider: Provider;
291
+ /** `provider` without the fallback chain: the one model this adapter was built for. */
292
+ private readonly primary: Provider;
293
293
  protected readonly primaryModel: string;
294
294
  protected readonly backupModels: string[];
295
295
  protected readonly retryConfig: RetryConfig;
@@ -304,6 +304,7 @@ export abstract class ProviderAdapter implements AiProvider {
304
304
  this.primaryModel = init.model;
305
305
  this.backupModels = init.backupModels ?? [];
306
306
  this.retryConfig = resolveRetryConfig(init.retryConfig);
307
+ this.primary = init.provider;
307
308
 
308
309
  if (init.fallbacks && init.fallbacks.length > 0) {
309
310
  const coreProviders: Provider[] = [
@@ -337,9 +338,14 @@ export abstract class ProviderAdapter implements AiProvider {
337
338
  * boot when it is `null` — that model cannot serve this framework's turns).
338
339
  * Log `calls`: `0/3 and 3/3` is what makes the next model swap's regression
339
340
  * obvious. Costs `samples × 2` short calls, and errors propagate.
341
+ *
342
+ * It asks the primary alone, never the fallback chain. The answer configures
343
+ * this adapter's model; a call the chain handed to a fallback would score
344
+ * another model as this one. Each fallback is an adapter of its own: probe it
345
+ * the same way.
340
346
  */
341
347
  async probeJsonWithTools(opts?: ProbeOptions): Promise<JsonWithToolsProbe> {
342
- return probeJsonWithTools(this.provider, {
348
+ return probeJsonWithTools(this.primary, {
343
349
  model: this.primaryModel,
344
350
  ...(opts ?? {}),
345
351
  });
@@ -468,8 +474,7 @@ export abstract class ProviderAdapter implements AiProvider {
468
474
  tokensUsed: acc.promptTokens + (acc.completionTokens ?? 0),
469
475
  promptTokens: acc.promptTokens,
470
476
  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.
477
+ // The cache-hit subset of the prompt, billed far cheaper.
473
478
  cachedInputTokens: acc.cachedInputTokens,
474
479
  }
475
480
  : {}),
@@ -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({
@@ -78,8 +78,12 @@ export interface AgentOptions<C = unknown, D = unknown> {
78
78
 
79
79
  // ── turn() input ────────────────────────────────────────────────────────
80
80
 
81
- /** The host's reason the assistant cannot speak. Zero calls unless `understand: true`. */
82
- export type Silenced = string | { reason: string; understand?: boolean };
81
+ /**
82
+ * The host's reason the assistant cannot speak. Zero calls unless `understand: true`.
83
+ * A talk or `say` step a run reaches under it ends the run; with `skip: true` the step is
84
+ * skipped and the run goes on, for a gate that only stops messages (a closed channel window).
85
+ */
86
+ export type Silenced = string | { reason: string; understand?: boolean; skip?: boolean };
83
87
 
84
88
  type ContextField<C> = undefined extends C ? { context?: C } : { context: C };
85
89
 
@@ -87,7 +91,11 @@ export type TurnBase<C = unknown, D = unknown> = ContextField<C> & {
87
91
  sessionId: string;
88
92
  /** Absent on a first turn. A wake never creates a session. */
89
93
  session?: Session<D>;
90
- /** Pass on every input kind, wakes included. */
94
+ /**
95
+ * The conversation BEFORE this input. Pass it on every input kind, wakes included.
96
+ * Leave out the message this turn carries: both calls quote it on their own, so a
97
+ * history that ends with it makes the model read it twice.
98
+ */
91
99
  history?: History;
92
100
  silenced?: Silenced;
93
101
  /** Host anchors this session belongs to, e.g. `{ lead: { key: 'lead:456', lastInboundAt } }`. */
@@ -108,6 +116,13 @@ export type TurnKind =
108
116
 
109
117
  export type TurnInput<C = unknown, D = unknown> = TurnBase<C, D> & TurnKind;
110
118
 
119
+ /** A saved session and the host inputs a turn on it would carry: what `pendingWakes` reads. */
120
+ export type PendingWakesInput<C = unknown, D = unknown> = ContextField<C> & {
121
+ session: Session<D>;
122
+ anchors?: TurnBase<C, D>["anchors"];
123
+ claims?: TurnBase<C, D>["claims"];
124
+ };
125
+
111
126
  // ── turn() result ───────────────────────────────────────────────────────
112
127
 
113
128
  export interface OutboundMessage {
@@ -123,7 +138,7 @@ export interface OutboundMessage {
123
138
  stepId?: string;
124
139
  }
125
140
 
126
- /** A wake to enqueue with `jobId = key`; at fire time call `turn({ wake: key })`. */
141
+ /** 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
142
  export interface ScheduleEntry {
128
143
  key: string;
129
144
  at: Date;
@@ -134,7 +149,7 @@ export interface ScheduleEntry {
134
149
  export type EndReason = "end" | "flow" | "reset" | "skipped" | "failed" | "replaced";
135
150
 
136
151
  export interface TurnResult<D = unknown> {
137
- /** Version unchanged; the host bumps it on save. */
152
+ /** Version unchanged by a turn; `Store.save` bumps it and returns the saved session. */
138
153
  session: Session<D>;
139
154
  /** False: save nothing, send nothing. */
140
155
  changed: boolean;