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

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 +42 -4
  21. package/dist/cjs/core/Runner.d.ts.map +1 -1
  22. package/dist/cjs/core/Runner.js +292 -72
  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 +60 -21
  26. package/dist/cjs/core/Speak.js.map +1 -1
  27. package/dist/cjs/core/Understand.d.ts +6 -3
  28. package/dist/cjs/core/Understand.d.ts.map +1 -1
  29. package/dist/cjs/core/Understand.js +34 -55
  30. package/dist/cjs/core/Understand.js.map +1 -1
  31. package/dist/cjs/core/contracts.d.ts +21 -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 +16 -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 +42 -4
  123. package/dist/core/Runner.d.ts.map +1 -1
  124. package/dist/core/Runner.js +293 -73
  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 +61 -22
  128. package/dist/core/Speak.js.map +1 -1
  129. package/dist/core/Understand.d.ts +6 -3
  130. package/dist/core/Understand.d.ts.map +1 -1
  131. package/dist/core/Understand.js +34 -55
  132. package/dist/core/Understand.js.map +1 -1
  133. package/dist/core/contracts.d.ts +21 -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 +16 -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 +13 -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 +9 -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 +290 -69
  246. package/src/core/Speak.ts +64 -21
  247. package/src/core/Understand.ts +35 -55
  248. package/src/core/contracts.ts +21 -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 +16 -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,37 @@ 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.";
100
+ /** The step's fixed question follows this reply (`fixedAfter`): the reply answers the lead and leaves the asking to it. */
101
+ const ANSWER_BEFORE_QUESTION =
102
+ "Answer what the customer's message asks, in the flow of the conversation. Ask nothing yourself: right after your reply, " +
103
+ "this question goes out word for word, so do not ask it or anything like it:";
90
104
  const TOOLS_SECTION =
91
105
  "## 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
106
  const FINAL_SECTION =
93
107
  "## Wrap up\nAnswer the customer now, using the tool results in the conversation. Do not call any tools.";
94
108
  const SPEAK_FIRST =
95
109
  "## Situation\nThere is no new message from the customer. You speak first: open naturally, do not answer a question nobody asked.";
110
+ /** The retry wake after a provider failure, when the customer's last message is still unanswered. */
111
+ const ANSWER_PENDING =
112
+ "## Situation\nThe customer's last message, in the conversation above, has no answer yet. Answer it now.";
96
113
 
97
114
  interface ToolCall {
98
115
  toolName: string;
@@ -145,7 +162,8 @@ export class Speak<C = unknown, D = unknown> {
145
162
  /** The round loop shared by both entry points: yields deltas, returns the outcome. */
146
163
  private async *rounds(req: SpeakRequest<C, D>, streaming: boolean): AsyncGenerator<string, SpeakOutcome> {
147
164
  const talk = req.talk;
148
- const envelope = buildEnvelope(this.options.fields, "idle" in talk ? [] : talk.pending);
165
+ // Fields are read by the fixed question's own answer, never by the reply that precedes it.
166
+ const envelope = buildEnvelope(this.options.fields, "idle" in talk || talk.fixedAfter !== undefined ? [] : talk.pending);
149
167
  const { system, turn: prompt } = buildPrompt(this.options, req, envelope);
150
168
  const maxLoops = this.options.maxToolLoops ?? DEFAULT_MAX_TOOL_LOOPS;
151
169
  const tools = maxLoops > 0 ? req.tools : [];
@@ -154,7 +172,6 @@ export class Speak<C = unknown, D = unknown> {
154
172
  let history: History = req.history;
155
173
  const fields: Record<string, unknown> = {};
156
174
  const data: Record<string, unknown> = {};
157
- const toolCalls: ToolCall[] = [];
158
175
  let llmCalls = 0;
159
176
  let usage: TokenUsage | undefined;
160
177
  let message = "";
@@ -209,14 +226,13 @@ export class Speak<C = unknown, D = unknown> {
209
226
  }
210
227
  history = [...history, ...executed.items];
211
228
  Object.assign(data, executed.data);
212
- toolCalls.push(...read.toolCalls);
213
229
  }
214
230
 
215
231
  if (!message.trim()) {
216
232
  logger.warn(`[Speak] the model returned no message after ${llmCalls} call(s); deferring.`);
217
233
  return { deferred: UNAVAILABLE, llmCalls, ...(usage ? { usage } : {}) };
218
234
  }
219
- return { spoken: { message, fields, data, toolCalls, llmCalls, ...(usage ? { usage } : {}) } };
235
+ return { spoken: { message, fields, data, llmCalls, ...(usage ? { usage } : {}) } };
220
236
  }
221
237
 
222
238
  /** One provider call. Streaming yields clean message deltas; both paths return the same shape. */
@@ -320,9 +336,13 @@ function buildPrompt<C, D>(
320
336
  "idle" in talk
321
337
  ? [guideline(talk.idle.prompt)]
322
338
  : [
323
- `## Flow\n${talk.flow.name}${talk.flow.description ? `: ${talk.flow.description}` : ""}`,
324
- guideline(talk.step.prompt ?? DEFAULT_GUIDELINE),
325
- pendingSection(talk.pending, options.fields, talk.step.ask ?? {}, scope),
339
+ `## Flow\n${talk.flow.name}${talk.flow.description ? `: ${render(talk.flow.description, scope)}` : ""}`,
340
+ ...(talk.fixedAfter !== undefined
341
+ ? [`${GUIDELINE_HEADING}\n${ANSWER_BEFORE_QUESTION}\n"${talk.fixedAfter}"`]
342
+ : [
343
+ guideline(talk.step.prompt ?? (talk.pending.length ? DEFAULT_GUIDELINE : ANSWER_GUIDELINE)),
344
+ pendingSection(talk.pending, options.fields, talk.step.ask ?? {}, scope),
345
+ ]),
326
346
  // `Partial<D>` is a mapped type; the guard is how it reaches an index-signature parameter without a cast.
327
347
  factsSection(options.fields, isRecord(req.data) ? req.data : {}),
328
348
  ];
@@ -333,17 +353,42 @@ function buildPrompt<C, D>(
333
353
  inline,
334
354
  ...body,
335
355
  instructionsSection([{ caption: "[Always]", items: req.instructions }], scope),
336
- inputSection(req.input),
356
+ inputSection(req.input, req.history),
357
+ saidSection(req.said),
337
358
  formatSection(envelope, options.fields),
338
359
  ),
339
360
  };
340
361
  }
341
362
 
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 {
363
+ /**
364
+ * The customer's latest text, quoted. Without one, on anything but a message,
365
+ * the assistant opens the exchange. The one exception is the retry of a step
366
+ * the provider failed, while the history still ends with the customer's
367
+ * message: that message gets its answer. A history that merely ends with a
368
+ * customer message is not enough, because a host may skip a message on
369
+ * purpose (an away message, a filtered first contact) and a follow-up must
370
+ * not answer it.
371
+ */
372
+ function inputSection(input: SpeakRequest["input"], history: History): string | null {
344
373
  const text = input.text?.trim();
345
374
  if (text) return `## Customer's latest message\n"${text}"`;
346
- return input.kind === "message" ? null : SPEAK_FIRST;
375
+ if (input.kind === "message") return null;
376
+ return input.retry && history.at(-1)?.role === "user" ? ANSWER_PENDING : SPEAK_FIRST;
377
+ }
378
+
379
+ /**
380
+ * What this turn already sent before the reply. It sits after the customer's message because that is
381
+ * when it went out; `history` stops before the turn, so it is the only place the model sees it.
382
+ */
383
+ function saidSection(said: SpeakRequest["said"]): string | null {
384
+ const lines = said.map(({ text, media }) =>
385
+ `- ${[text.trim() && `"${text.trim()}"`, media && `[media: ${media.slug}]`].filter(Boolean).join(" ")}`);
386
+ if (!lines.length) return null;
387
+ return [
388
+ "## Already sent",
389
+ "You already sent these messages this turn, in this order, and your reply goes out right after them. Do not repeat what they say.",
390
+ ...lines,
391
+ ].join("\n");
347
392
  }
348
393
 
349
394
  /** The envelope restated in words, for providers that follow the schema by prompt only. */
@@ -374,16 +419,14 @@ function formatSection(envelope: Envelope, fields: FieldDefs): string {
374
419
  // ── Envelope ────────────────────────────────────────────────────────────
375
420
 
376
421
  function buildEnvelope(fields: FieldDefs, pending: string[]): Envelope {
422
+ const aliases = new Aliases(["message"]);
377
423
  const wire = new Map<string, string>();
378
424
  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}`;
425
+ for (const slug of pending) {
426
+ const name = aliases.of(slug, "field_");
384
427
  wire.set(slug, name);
385
428
  defs[name] = fields[slug] ?? { type: "string" };
386
- });
429
+ }
387
430
  return { schema: toWireSchema(defs, { nullable: true }), wire };
388
431
  }
389
432
 
@@ -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,35 +24,35 @@ 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
- const SECTIONS = ["flows", "mentions", "extract", "branches", "fields"] as const;
35
-
36
- /** Gemini rejects any other character in a property name. */
37
- const SAFE_KEY = /^[a-zA-Z0-9_-]+$/;
36
+ const SECTIONS = ["flows", "mentions", "extract", "branches", "fields", "asks"] as const;
38
37
 
39
38
  export class Understand<C = unknown, D = unknown> {
40
- constructor(private readonly options: AgentOptions<C, D>) {}
39
+ /** Some flow opens a step with a fixed question: whether the lead asked something decides if it waits. */
40
+ private readonly judgesAsks: boolean;
41
+
42
+ constructor(private readonly options: AgentOptions<C, D>) {
43
+ this.judgesAsks = (options.flows ?? []).some((flow) => flow.steps.some((step) => "question" in step && step.question !== undefined));
44
+ }
41
45
 
42
46
  async run(req: UnderstandRequest<C, D>): Promise<Understanding> {
43
47
  const candidates = candidateFlows(req);
44
48
  const onlyRouting =
45
49
  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
- }
50
+ // Nothing to compare or extract: no candidates, or only the floor's own flow. A lone
51
+ // candidate with nobody on the floor is here because the runner wants it scored.
52
+ if (onlyRouting && candidates.length <= (req.floor ? 1 : 0)) return empty(0);
52
53
 
53
54
  const aliases = new Aliases();
54
- const jsonSchema = buildEnvelope(req, candidates, aliases);
55
+ const jsonSchema = buildEnvelope(req, candidates, aliases, this.judgesAsks);
55
56
  const { system, turn: prompt } = this.buildPrompt(req, candidates, aliases, jsonSchema);
56
57
 
57
58
  // Provider failures propagate: in this phase the turn throws and the host retries the input.
@@ -113,45 +114,13 @@ function candidateFlows<C, D>(req: UnderstandRequest<C, D>): Flow<C, D>[] {
113
114
  return [...seen.values()];
114
115
  }
115
116
 
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
117
  function branchKey(branch: { runId: string; stepId: string; index: number }): string {
149
118
  return `${branch.runId}/${branch.stepId}/${branch.index}`;
150
119
  }
151
120
 
152
121
  // ── Envelope ────────────────────────────────────────────────────────────
153
122
 
154
- function buildEnvelope<C, D>(req: UnderstandRequest<C, D>, candidates: Flow<C, D>[], aliases: Aliases): StructuredSchema {
123
+ function buildEnvelope<C, D>(req: UnderstandRequest<C, D>, candidates: Flow<C, D>[], aliases: Aliases, judgesAsks: boolean): StructuredSchema {
155
124
  const properties: Record<string, StructuredSchema> = {};
156
125
 
157
126
  if (candidates.length) {
@@ -185,6 +154,9 @@ function buildEnvelope<C, D>(req: UnderstandRequest<C, D>, candidates: Flow<C, D
185
154
  for (const [slug, def] of Object.entries(req.fields)) defs[aliases.of(slug, "d")] = def;
186
155
  properties.fields = toWireSchema(defs, { nullable: true });
187
156
  }
157
+ if (judgesAsks) {
158
+ properties.asks = { type: ["boolean", "null"], description: "The message asks something the reply must answer" };
159
+ }
188
160
 
189
161
  return { type: "object", properties, required: Object.keys(properties), additionalProperties: false };
190
162
  }
@@ -245,8 +217,9 @@ function flowsSection<C, D>(candidates: Flow<C, D>[], aliases: Aliases, t: (text
245
217
  ];
246
218
  candidates.forEach((flow, i) => {
247
219
  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("; ")}`);
220
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "message"));
221
+ if (counts.length) lines.push(` The customer: ${counts.map(t).join("; ")}`);
222
+ if (excludes.length) lines.push(` Score 0 when: ${excludes.map(t).join("; ")}`);
250
223
  });
251
224
  lines.push(
252
225
  "",
@@ -265,12 +238,14 @@ function mentionsSection<C, D>(flows: Flow<C, D>[], aliases: Aliases, t: (text:
265
238
  const lines = [
266
239
  "## Things the customer may mention",
267
240
  "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.",
241
+ "Be conservative: true needs clear, explicit evidence in the message. The phrases under an item are alternatives; one match is enough. " +
242
+ "A 'Does not count when' line overrides a match: if one of those fits, answer false.",
269
243
  ];
270
244
  for (const flow of flows) {
271
245
  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("; ")}`);
246
+ const { counts, excludes } = splitPhrases(triggerPhrases(flow, "mention"));
247
+ if (counts.length) lines.push(` Counts when: ${counts.map(t).join("; ")}`);
248
+ if (excludes.length) lines.push(` Does not count when: ${excludes.map(t).join("; ")}`);
274
249
  const defs = extractDefs(flow);
275
250
  if (defs) {
276
251
  lines.push(" When true, also extract:");
@@ -316,6 +291,9 @@ const OUTPUT_LINES: Record<Section, string> = {
316
291
  extract: "- extract: per item id, an object with the values pulled from the message; null values when the item was not brought up",
317
292
  branches: "- branches: true or false per question id",
318
293
  fields: "- fields: one value per detail, null when the customer did not give it",
294
+ asks:
295
+ "- asks: true when the message asks something the reply has to answer (a question, a request for a price, a photo, " +
296
+ "whether an item is in stock, a store fact); false when it only answers the assistant, greets or confirms",
319
297
  };
320
298
 
321
299
  /**
@@ -323,7 +301,7 @@ const OUTPUT_LINES: Record<Section, string> = {
323
301
  * null for anything extracted, so a prompt-only model never reads a
324
302
  * placeholder as a default value.
325
303
  */
326
- const PLACEHOLDER: Record<Section, unknown> = { flows: 0, mentions: false, extract: null, branches: false, fields: null };
304
+ const PLACEHOLDER: Record<Section, unknown> = { flows: 0, mentions: false, extract: null, branches: false, fields: null, asks: false };
327
305
 
328
306
  function outputSection(schema: StructuredSchema): string {
329
307
  const present = SECTIONS.filter((s) => schema.properties?.[s] !== undefined);
@@ -390,12 +368,14 @@ function parseReply(reply: Record<string, unknown>, aliases: Aliases): Understan
390
368
  for (const [key, raw] of entries(reply.extract)) {
391
369
  if (isRecord(raw)) extract[aliases.real(key)] = given(raw);
392
370
  }
371
+ const asks = coerceField({ type: "boolean" }, reply.asks);
393
372
  return {
394
373
  flows,
395
374
  mentions: booleans(reply.mentions, aliases),
396
375
  extract,
397
376
  branches: booleans(reply.branches, aliases),
398
377
  fields: given(Object.fromEntries(entries(reply.fields).map(([k, v]) => [aliases.real(k), v]))),
378
+ ...(asks.ok && typeof asks.value === "boolean" ? { asks: asks.value } : {}),
399
379
  llmCalls: 1,
400
380
  };
401
381
  }
@@ -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";
@@ -48,6 +48,11 @@ export interface Understanding {
48
48
  branches: Record<string, boolean>;
49
49
  /** field → raw value the lead gave, or nothing. */
50
50
  fields: Record<string, unknown>;
51
+ /**
52
+ * The lead's message asks something a reply must answer. Judged only while a flow has a fixed
53
+ * `question`: one that would go out now waits for the answer when this is not `false`.
54
+ */
55
+ asks?: boolean;
51
56
  llmCalls: number;
52
57
  usage?: TokenUsage;
53
58
  }
@@ -61,6 +66,11 @@ export interface TalkRequest<C = unknown, D = unknown> {
61
66
  step: StepBase<D> & TalkStep<C, D>;
62
67
  /** `step.collect` minus known minus at `maxAsks`, in order. */
63
68
  pending: string[];
69
+ /**
70
+ * The step's fixed question, rendered. It goes out word for word right after this reply, so the
71
+ * reply only answers what the lead asked: it asks nothing and reads no field.
72
+ */
73
+ fixedAfter?: string;
64
74
  }
65
75
 
66
76
  /** No run holds the floor; the idle speaker answers. */
@@ -70,11 +80,19 @@ export interface IdleRequest<C = unknown, D = unknown> {
70
80
 
71
81
  export interface SpeakRequest<C = unknown, D = unknown> {
72
82
  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 };
83
+ /**
84
+ * What started this turn. A wake has no text: the assistant speaks first, unless `retry` says this
85
+ * talk step failed at the provider and is running again, with the customer's message still unanswered.
86
+ */
87
+ input: { kind: InputKind; text?: string; retry?: boolean };
75
88
  context: C;
76
89
  data: Partial<D>;
77
90
  history: History;
91
+ /**
92
+ * What this turn already sent before the reply, in order: a `say`, a fixed question. `history` ends
93
+ * before the turn, so without these the model cannot see them and says the same thing again.
94
+ */
95
+ said: OutboundMessage[];
78
96
  now: Date;
79
97
  /** Already filtered by their `if`; `when` stays for the prompt. */
80
98
  instructions: Instruction<C, D>[];
@@ -87,7 +105,6 @@ export interface Spoken {
87
105
  fields: Record<string, unknown>;
88
106
  /** Data patches returned by tools, merged in call order. */
89
107
  data: Record<string, unknown>;
90
- toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
91
108
  llmCalls: number;
92
109
  usage?: TokenUsage;
93
110
  }
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",