@falai/agent 4.0.0-alpha.8 → 4.0.0
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.
- package/README.md +2 -0
- package/dist/cjs/core/Agent.d.ts +8 -1
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +18 -0
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/CompactionEngine.d.ts.map +1 -1
- package/dist/cjs/core/CompactionEngine.js +21 -7
- package/dist/cjs/core/CompactionEngine.js.map +1 -1
- package/dist/cjs/core/FlowSpec.d.ts +21 -2
- package/dist/cjs/core/FlowSpec.d.ts.map +1 -1
- package/dist/cjs/core/FlowSpec.js +297 -59
- package/dist/cjs/core/FlowSpec.js.map +1 -1
- package/dist/cjs/core/Migrate.d.ts.map +1 -1
- package/dist/cjs/core/Migrate.js +3 -1
- package/dist/cjs/core/Migrate.js.map +1 -1
- package/dist/cjs/core/Prompt.d.ts +16 -0
- package/dist/cjs/core/Prompt.d.ts.map +1 -1
- package/dist/cjs/core/Prompt.js +39 -0
- package/dist/cjs/core/Prompt.js.map +1 -1
- package/dist/cjs/core/Runner.d.ts +37 -4
- package/dist/cjs/core/Runner.d.ts.map +1 -1
- package/dist/cjs/core/Runner.js +281 -72
- package/dist/cjs/core/Runner.js.map +1 -1
- package/dist/cjs/core/Speak.d.ts.map +1 -1
- package/dist/cjs/core/Speak.js +63 -19
- package/dist/cjs/core/Speak.js.map +1 -1
- package/dist/cjs/core/Understand.d.ts +6 -3
- package/dist/cjs/core/Understand.d.ts.map +1 -1
- package/dist/cjs/core/Understand.js +18 -42
- package/dist/cjs/core/Understand.js.map +1 -1
- package/dist/cjs/core/contracts.d.ts +26 -6
- package/dist/cjs/core/contracts.d.ts.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/cjs/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/cjs/persistence/OpenSearchStore.js +2 -2
- package/dist/cjs/persistence/OpenSearchStore.js.map +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts +1 -1
- package/dist/cjs/persistence/RedisStore.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts +2 -5
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +3 -4
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -4
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js +1 -1
- package/dist/cjs/providers/FallbackAiProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts +1 -2
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +3 -4
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAIProvider.js +3 -4
- package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenRouterProvider.js +4 -3
- package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.d.ts +26 -11
- package/dist/cjs/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/cjs/providers/ProviderAdapter.js +13 -13
- package/dist/cjs/providers/ProviderAdapter.js.map +1 -1
- package/dist/cjs/providers/ZaiProvider.js +1 -1
- package/dist/cjs/providers/ZaiProvider.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +14 -3
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/ai.d.ts +10 -6
- package/dist/cjs/types/ai.d.ts.map +1 -1
- package/dist/cjs/types/compaction.d.ts +1 -0
- package/dist/cjs/types/compaction.d.ts.map +1 -1
- package/dist/cjs/types/errors.d.ts +4 -9
- package/dist/cjs/types/errors.d.ts.map +1 -1
- package/dist/cjs/types/errors.js +12 -12
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/types/flow.d.ts +17 -1
- package/dist/cjs/types/flow.d.ts.map +1 -1
- package/dist/cjs/types/history.d.ts +0 -7
- package/dist/cjs/types/history.d.ts.map +1 -1
- package/dist/cjs/types/index.d.ts +2 -2
- package/dist/cjs/types/index.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +4 -2
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/utils/clock.js +1 -1
- package/dist/cjs/utils/clock.js.map +1 -1
- package/dist/cjs/utils/outcomes.d.ts +2 -0
- package/dist/cjs/utils/outcomes.d.ts.map +1 -1
- package/dist/cjs/utils/outcomes.js +2 -0
- package/dist/cjs/utils/outcomes.js.map +1 -1
- package/dist/cjs/utils/schema.d.ts +3 -12
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/cjs/utils/schema.js +3 -43
- package/dist/cjs/utils/schema.js.map +1 -1
- package/dist/cjs/utils/template.d.ts.map +1 -1
- package/dist/cjs/utils/template.js +9 -2
- package/dist/cjs/utils/template.js.map +1 -1
- package/dist/core/Agent.d.ts +8 -1
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +19 -1
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/CompactionEngine.d.ts.map +1 -1
- package/dist/core/CompactionEngine.js +21 -7
- package/dist/core/CompactionEngine.js.map +1 -1
- package/dist/core/FlowSpec.d.ts +21 -2
- package/dist/core/FlowSpec.d.ts.map +1 -1
- package/dist/core/FlowSpec.js +296 -60
- package/dist/core/FlowSpec.js.map +1 -1
- package/dist/core/Migrate.d.ts.map +1 -1
- package/dist/core/Migrate.js +3 -1
- package/dist/core/Migrate.js.map +1 -1
- package/dist/core/Prompt.d.ts +16 -0
- package/dist/core/Prompt.d.ts.map +1 -1
- package/dist/core/Prompt.js +37 -0
- package/dist/core/Prompt.js.map +1 -1
- package/dist/core/Runner.d.ts +37 -4
- package/dist/core/Runner.d.ts.map +1 -1
- package/dist/core/Runner.js +282 -73
- package/dist/core/Runner.js.map +1 -1
- package/dist/core/Speak.d.ts.map +1 -1
- package/dist/core/Speak.js +64 -20
- package/dist/core/Speak.js.map +1 -1
- package/dist/core/Understand.d.ts +6 -3
- package/dist/core/Understand.d.ts.map +1 -1
- package/dist/core/Understand.js +18 -42
- package/dist/core/Understand.js.map +1 -1
- package/dist/core/contracts.d.ts +26 -6
- package/dist/core/contracts.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.d.ts +2 -1
- package/dist/persistence/OpenSearchStore.d.ts.map +1 -1
- package/dist/persistence/OpenSearchStore.js +2 -2
- package/dist/persistence/OpenSearchStore.js.map +1 -1
- package/dist/persistence/RedisStore.d.ts +1 -1
- package/dist/persistence/RedisStore.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts +2 -5
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +3 -4
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -4
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/FallbackAiProvider.js +1 -1
- package/dist/providers/FallbackAiProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts +1 -2
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +3 -4
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.js +4 -4
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/OpenAIProvider.d.ts.map +1 -1
- package/dist/providers/OpenAIProvider.js +3 -4
- package/dist/providers/OpenAIProvider.js.map +1 -1
- package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
- package/dist/providers/OpenRouterProvider.js +4 -3
- package/dist/providers/OpenRouterProvider.js.map +1 -1
- package/dist/providers/ProviderAdapter.d.ts +26 -11
- package/dist/providers/ProviderAdapter.d.ts.map +1 -1
- package/dist/providers/ProviderAdapter.js +14 -14
- package/dist/providers/ProviderAdapter.js.map +1 -1
- package/dist/providers/ZaiProvider.js +1 -1
- package/dist/providers/ZaiProvider.js.map +1 -1
- package/dist/types/agent.d.ts +14 -3
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/ai.d.ts +10 -6
- package/dist/types/ai.d.ts.map +1 -1
- package/dist/types/compaction.d.ts +1 -0
- package/dist/types/compaction.d.ts.map +1 -1
- package/dist/types/errors.d.ts +4 -9
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/errors.js +12 -12
- package/dist/types/errors.js.map +1 -1
- package/dist/types/flow.d.ts +17 -1
- package/dist/types/flow.d.ts.map +1 -1
- package/dist/types/history.d.ts +0 -7
- package/dist/types/history.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session.d.ts +4 -2
- package/dist/types/session.d.ts.map +1 -1
- package/dist/utils/clock.js +1 -1
- package/dist/utils/clock.js.map +1 -1
- package/dist/utils/outcomes.d.ts +2 -0
- package/dist/utils/outcomes.d.ts.map +1 -1
- package/dist/utils/outcomes.js +2 -0
- package/dist/utils/outcomes.js.map +1 -1
- package/dist/utils/schema.d.ts +3 -12
- package/dist/utils/schema.d.ts.map +1 -1
- package/dist/utils/schema.js +3 -42
- package/dist/utils/schema.js.map +1 -1
- package/dist/utils/template.d.ts.map +1 -1
- package/dist/utils/template.js +9 -2
- package/dist/utils/template.js.map +1 -1
- package/docs/concepts/architecture.md +2 -2
- package/docs/concepts/collection.md +40 -5
- package/docs/concepts/pipeline.md +10 -7
- package/docs/concepts/runs-and-waits.md +2 -2
- package/docs/guides/actions-and-events.md +1 -1
- package/docs/guides/branching.md +4 -2
- package/docs/guides/compaction.md +2 -2
- package/docs/guides/error-handling.md +2 -2
- package/docs/guides/flow-control.md +4 -2
- package/docs/guides/persistence.md +2 -2
- package/docs/guides/testing.md +1 -1
- package/docs/guides/triggers.md +3 -3
- package/docs/migration/v3-to-v4.md +8 -7
- package/docs/reference/actions-events-conditions.md +1 -1
- package/docs/reference/agent.md +9 -5
- package/docs/reference/branches.md +1 -1
- package/docs/reference/errors.md +14 -11
- package/docs/reference/fields.md +5 -3
- package/docs/reference/flow-spec.md +35 -8
- package/docs/reference/flow.md +8 -3
- package/docs/reference/outcomes.md +4 -2
- package/docs/reference/providers.md +5 -3
- package/docs/reference/session.md +2 -0
- package/docs/reference/step.md +9 -5
- package/docs/reference/stores.md +5 -3
- package/docs/reference/trigger.md +2 -2
- package/docs/rfc/v4-one-flow.md +5 -3
- package/docs/start/01-install.md +2 -0
- package/docs/start/05-go-to-production.md +18 -1
- package/examples/05-branches.ts +1 -1
- package/examples/06-triggers-and-waits.ts +4 -3
- package/package.json +4 -3
- package/src/core/Agent.ts +23 -2
- package/src/core/CompactionEngine.ts +25 -9
- package/src/core/FlowSpec.ts +311 -72
- package/src/core/Migrate.ts +2 -1
- package/src/core/Prompt.ts +40 -0
- package/src/core/Runner.ts +279 -69
- package/src/core/Speak.ts +67 -19
- package/src/core/Understand.ts +24 -44
- package/src/core/contracts.ts +26 -4
- package/src/index.ts +1 -1
- package/src/persistence/OpenSearchStore.ts +3 -2
- package/src/persistence/RedisStore.ts +1 -1
- package/src/providers/AnthropicProvider.ts +4 -9
- package/src/providers/DeepSeekProvider.ts +2 -4
- package/src/providers/FallbackAiProvider.ts +1 -1
- package/src/providers/GeminiProvider.ts +3 -6
- package/src/providers/GenericOpenAICompatibleProvider.ts +4 -4
- package/src/providers/OpenAIProvider.ts +3 -4
- package/src/providers/OpenRouterProvider.ts +4 -2
- package/src/providers/ProviderAdapter.ts +35 -33
- package/src/providers/ZaiProvider.ts +1 -1
- package/src/types/agent.ts +15 -4
- package/src/types/ai.ts +10 -6
- package/src/types/compaction.ts +1 -0
- package/src/types/errors.ts +11 -12
- package/src/types/flow.ts +17 -1
- package/src/types/history.ts +0 -10
- package/src/types/index.ts +1 -1
- package/src/types/session.ts +5 -1
- package/src/utils/clock.ts +1 -1
- package/src/utils/outcomes.ts +2 -0
- package/src/utils/schema.ts +3 -48
- package/src/utils/template.ts +9 -2
- package/dist/cjs/providers/index.d.ts +0 -26
- package/dist/cjs/providers/index.d.ts.map +0 -1
- package/dist/cjs/providers/index.js +0 -30
- package/dist/cjs/providers/index.js.map +0 -1
- package/dist/cjs/utils/clone.d.ts +0 -8
- package/dist/cjs/utils/clone.d.ts.map +0 -1
- package/dist/cjs/utils/clone.js +0 -32
- package/dist/cjs/utils/clone.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -9
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -29
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -26
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -17
- package/dist/providers/index.js.map +0 -1
- package/dist/utils/clone.d.ts +0 -8
- package/dist/utils/clone.d.ts.map +0 -1
- package/dist/utils/clone.js +0 -29
- package/dist/utils/clone.js.map +0 -1
- package/dist/utils/index.d.ts +0 -9
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -9
- package/dist/utils/index.js.map +0 -1
- package/src/providers/index.ts +0 -38
- package/src/utils/clone.ts +0 -34
- package/src/utils/index.ts +0 -18
package/src/core/Speak.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { render, type TemplateScope } from "../utils/template.js";
|
|
|
32
32
|
import { addUsage, readUsage } from "../utils/usage.js";
|
|
33
33
|
import type { Deferral, SpeakOutcome, SpeakRequest, SpeakStreamChunk } from "./contracts.js";
|
|
34
34
|
import {
|
|
35
|
+
Aliases,
|
|
35
36
|
describeField,
|
|
36
37
|
factsSection,
|
|
37
38
|
instructionsSection,
|
|
@@ -90,18 +91,23 @@ function deferralOf(error: unknown): Deferral {
|
|
|
90
91
|
...(reset !== undefined ? { resetAtMs: reset } : {}),
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
|
-
/** Gemini rejects any other envelope property name. */
|
|
94
|
-
const WIRE_NAME = /^[a-zA-Z0-9_-]+$/;
|
|
95
94
|
|
|
96
95
|
const GUIDELINE_HEADING = "## Guideline for your reply (adapt to the conversation)";
|
|
97
96
|
const DEFAULT_GUIDELINE =
|
|
98
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 lead asked something on a step with a fixed question (`fixedQuestion`): one reply answers it and asks the question. */
|
|
101
|
+
const ANSWER_THEN_FIXED = "Answer what the customer's message asks, then ask the step's question below, in the same message.";
|
|
99
102
|
const TOOLS_SECTION =
|
|
100
103
|
"## 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.";
|
|
101
104
|
const FINAL_SECTION =
|
|
102
105
|
"## Wrap up\nAnswer the customer now, using the tool results in the conversation. Do not call any tools.";
|
|
103
106
|
const SPEAK_FIRST =
|
|
104
107
|
"## Situation\nThere is no new message from the customer. You speak first: open naturally, do not answer a question nobody asked.";
|
|
108
|
+
/** The retry wake after a provider failure, when the customer's last message is still unanswered. */
|
|
109
|
+
const ANSWER_PENDING =
|
|
110
|
+
"## Situation\nThe customer's last message, in the conversation above, has no answer yet. Answer it now.";
|
|
105
111
|
|
|
106
112
|
interface ToolCall {
|
|
107
113
|
toolName: string;
|
|
@@ -154,7 +160,8 @@ export class Speak<C = unknown, D = unknown> {
|
|
|
154
160
|
/** The round loop shared by both entry points: yields deltas, returns the outcome. */
|
|
155
161
|
private async *rounds(req: SpeakRequest<C, D>, streaming: boolean): AsyncGenerator<string, SpeakOutcome> {
|
|
156
162
|
const talk = req.talk;
|
|
157
|
-
|
|
163
|
+
// Fields are read by the fixed question's own answer, never by the reply that precedes it.
|
|
164
|
+
const envelope = buildEnvelope(this.options.fields, "idle" in talk || talk.fixedQuestion !== undefined ? [] : talk.pending);
|
|
158
165
|
const { system, turn: prompt } = buildPrompt(this.options, req, envelope);
|
|
159
166
|
const maxLoops = this.options.maxToolLoops ?? DEFAULT_MAX_TOOL_LOOPS;
|
|
160
167
|
const tools = maxLoops > 0 ? req.tools : [];
|
|
@@ -163,7 +170,6 @@ export class Speak<C = unknown, D = unknown> {
|
|
|
163
170
|
let history: History = req.history;
|
|
164
171
|
const fields: Record<string, unknown> = {};
|
|
165
172
|
const data: Record<string, unknown> = {};
|
|
166
|
-
const toolCalls: ToolCall[] = [];
|
|
167
173
|
let llmCalls = 0;
|
|
168
174
|
let usage: TokenUsage | undefined;
|
|
169
175
|
let message = "";
|
|
@@ -175,6 +181,7 @@ export class Speak<C = unknown, D = unknown> {
|
|
|
175
181
|
...(system ? { system } : {}),
|
|
176
182
|
history,
|
|
177
183
|
context: req.context,
|
|
184
|
+
sessionId: req.sessionId,
|
|
178
185
|
tools: offerTools ? wireTools : undefined,
|
|
179
186
|
parameters: { jsonSchema: envelope.schema, schemaName: "speak" },
|
|
180
187
|
};
|
|
@@ -218,14 +225,13 @@ export class Speak<C = unknown, D = unknown> {
|
|
|
218
225
|
}
|
|
219
226
|
history = [...history, ...executed.items];
|
|
220
227
|
Object.assign(data, executed.data);
|
|
221
|
-
toolCalls.push(...read.toolCalls);
|
|
222
228
|
}
|
|
223
229
|
|
|
224
230
|
if (!message.trim()) {
|
|
225
231
|
logger.warn(`[Speak] the model returned no message after ${llmCalls} call(s); deferring.`);
|
|
226
232
|
return { deferred: UNAVAILABLE, llmCalls, ...(usage ? { usage } : {}) };
|
|
227
233
|
}
|
|
228
|
-
return { spoken: { message, fields, data,
|
|
234
|
+
return { spoken: { message, fields, data, llmCalls, ...(usage ? { usage } : {}) } };
|
|
229
235
|
}
|
|
230
236
|
|
|
231
237
|
/** One provider call. Streaming yields clean message deltas; both paths return the same shape. */
|
|
@@ -329,9 +335,13 @@ function buildPrompt<C, D>(
|
|
|
329
335
|
"idle" in talk
|
|
330
336
|
? [guideline(talk.idle.prompt)]
|
|
331
337
|
: [
|
|
332
|
-
`## Flow\n${talk.flow.name}${talk.flow.description ? `: ${talk.flow.description}` : ""}`,
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
`## Flow\n${talk.flow.name}${talk.flow.description ? `: ${render(talk.flow.description, scope)}` : ""}`,
|
|
339
|
+
...(talk.fixedQuestion !== undefined
|
|
340
|
+
? [`${GUIDELINE_HEADING}\n${ANSWER_THEN_FIXED}`]
|
|
341
|
+
: [
|
|
342
|
+
guideline(talk.step.prompt ?? (talk.pending.length ? DEFAULT_GUIDELINE : ANSWER_GUIDELINE)),
|
|
343
|
+
pendingSection(talk.pending, options.fields, talk.step.ask ?? {}, scope),
|
|
344
|
+
]),
|
|
335
345
|
// `Partial<D>` is a mapped type; the guard is how it reaches an index-signature parameter without a cast.
|
|
336
346
|
factsSection(options.fields, isRecord(req.data) ? req.data : {}),
|
|
337
347
|
];
|
|
@@ -342,17 +352,57 @@ function buildPrompt<C, D>(
|
|
|
342
352
|
inline,
|
|
343
353
|
...body,
|
|
344
354
|
instructionsSection([{ caption: "[Always]", items: req.instructions }], scope),
|
|
345
|
-
inputSection(req.input),
|
|
355
|
+
inputSection(req.input, req.history),
|
|
356
|
+
saidSection(req.said),
|
|
357
|
+
"idle" in talk ? null : fixedQuestionSection(talk.fixedQuestion),
|
|
346
358
|
formatSection(envelope, options.fields),
|
|
347
359
|
),
|
|
348
360
|
};
|
|
349
361
|
}
|
|
350
362
|
|
|
351
|
-
/**
|
|
352
|
-
|
|
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 {
|
|
353
373
|
const text = input.text?.trim();
|
|
354
374
|
if (text) return `## Customer's latest message\n"${text}"`;
|
|
355
|
-
|
|
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");
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* The step's fixed question, for the reply to end with. It sits last, after the customer's message: in the
|
|
396
|
+
* middle of the prompt, a voice instruction ("end with a question") outranked it and the reply asked its own.
|
|
397
|
+
*/
|
|
398
|
+
function fixedQuestionSection(question: string | undefined): string | null {
|
|
399
|
+
if (question === undefined) return null;
|
|
400
|
+
return [
|
|
401
|
+
"## The step's question",
|
|
402
|
+
`"${question}"`,
|
|
403
|
+
"End your message with this question, in these words. Change them only where your answer already covered part of it " +
|
|
404
|
+
"or where they would contradict your answer. Ask nothing else.",
|
|
405
|
+
].join("\n");
|
|
356
406
|
}
|
|
357
407
|
|
|
358
408
|
/** The envelope restated in words, for providers that follow the schema by prompt only. */
|
|
@@ -383,16 +433,14 @@ function formatSection(envelope: Envelope, fields: FieldDefs): string {
|
|
|
383
433
|
// ── Envelope ────────────────────────────────────────────────────────────
|
|
384
434
|
|
|
385
435
|
function buildEnvelope(fields: FieldDefs, pending: string[]): Envelope {
|
|
436
|
+
const aliases = new Aliases(["message"]);
|
|
386
437
|
const wire = new Map<string, string>();
|
|
387
438
|
const defs: FieldDefs = { message: { type: "string" } };
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// rides under an alias; the table maps it back. Ceiling: a real slug named
|
|
391
|
-
// `field_N` could collide with an alias. Upgrade: bump the alias until free.
|
|
392
|
-
const name = WIRE_NAME.test(slug) && slug !== "message" ? slug : `field_${i}`;
|
|
439
|
+
for (const slug of pending) {
|
|
440
|
+
const name = aliases.of(slug, "field_");
|
|
393
441
|
wire.set(slug, name);
|
|
394
442
|
defs[name] = fields[slug] ?? { type: "string" };
|
|
395
|
-
}
|
|
443
|
+
}
|
|
396
444
|
return { schema: toWireSchema(defs, { nullable: true }), wire };
|
|
397
445
|
}
|
|
398
446
|
|
package/src/core/Understand.ts
CHANGED
|
@@ -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
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Zero calls when there is nothing to judge: no candidate flow, or only the
|
|
10
|
+
* floor's own, and no mention, branch or field. A lone eligible message flow
|
|
11
|
+
* starts unscored only when no catch-all passes and `idle` is `'silent'`;
|
|
12
|
+
* Runner decides that and sends no request. Otherwise this call scores it.
|
|
12
13
|
*
|
|
13
14
|
* The envelope is built for two kinds of provider at once. Gemini enforces
|
|
14
15
|
* the schema, so every section is a closed object with every property
|
|
@@ -28,17 +29,19 @@ import { coerceField, isKnown, pendingFields, toWireSchema } from "../utils/sche
|
|
|
28
29
|
import { render, type TemplateScope } from "../utils/template.js";
|
|
29
30
|
import { readUsage } from "../utils/usage.js";
|
|
30
31
|
import type { UnderstandRequest, Understanding } from "./contracts.js";
|
|
31
|
-
import { describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
|
|
32
|
+
import { Aliases, describeField, factsSection, joinSections, stablePrefix } from "./Prompt.js";
|
|
32
33
|
|
|
33
34
|
export const UNDERSTAND_SCHEMA_NAME = "understand";
|
|
34
35
|
|
|
35
|
-
const SECTIONS = ["flows", "mentions", "extract", "branches", "fields"] as const;
|
|
36
|
-
|
|
37
|
-
/** Gemini rejects any other character in a property name. */
|
|
38
|
-
const SAFE_KEY = /^[a-zA-Z0-9_-]+$/;
|
|
36
|
+
const SECTIONS = ["flows", "mentions", "extract", "branches", "fields", "asks"] as const;
|
|
39
37
|
|
|
40
38
|
export class Understand<C = unknown, D = unknown> {
|
|
41
|
-
|
|
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
|
+
}
|
|
42
45
|
|
|
43
46
|
async run(req: UnderstandRequest<C, D>): Promise<Understanding> {
|
|
44
47
|
const candidates = candidateFlows(req);
|
|
@@ -49,7 +52,7 @@ export class Understand<C = unknown, D = unknown> {
|
|
|
49
52
|
if (onlyRouting && candidates.length <= (req.floor ? 1 : 0)) return empty(0);
|
|
50
53
|
|
|
51
54
|
const aliases = new Aliases();
|
|
52
|
-
const jsonSchema = buildEnvelope(req, candidates, aliases);
|
|
55
|
+
const jsonSchema = buildEnvelope(req, candidates, aliases, this.judgesAsks);
|
|
53
56
|
const { system, turn: prompt } = this.buildPrompt(req, candidates, aliases, jsonSchema);
|
|
54
57
|
|
|
55
58
|
// Provider failures propagate: in this phase the turn throws and the host retries the input.
|
|
@@ -58,6 +61,7 @@ export class Understand<C = unknown, D = unknown> {
|
|
|
58
61
|
...(system ? { system } : {}),
|
|
59
62
|
history: req.history,
|
|
60
63
|
context: req.context,
|
|
64
|
+
sessionId: req.sessionId,
|
|
61
65
|
parameters: { jsonSchema, schemaName: UNDERSTAND_SCHEMA_NAME },
|
|
62
66
|
});
|
|
63
67
|
|
|
@@ -111,45 +115,13 @@ function candidateFlows<C, D>(req: UnderstandRequest<C, D>): Flow<C, D>[] {
|
|
|
111
115
|
return [...seen.values()];
|
|
112
116
|
}
|
|
113
117
|
|
|
114
|
-
/**
|
|
115
|
-
* Envelope property names for ids the schema cannot carry. A safe id keeps
|
|
116
|
-
* its own name; anything else (run ids carry `#`, `:` and `/`) gets a short
|
|
117
|
-
* alias, mapped back when the reply is parsed.
|
|
118
|
-
*/
|
|
119
|
-
class Aliases {
|
|
120
|
-
private readonly byReal = new Map<string, string>();
|
|
121
|
-
private readonly byAlias = new Map<string, string>();
|
|
122
|
-
private n = 0;
|
|
123
|
-
|
|
124
|
-
of(real: string, prefix = "k"): string {
|
|
125
|
-
const seen = this.byReal.get(real);
|
|
126
|
-
if (seen) return seen;
|
|
127
|
-
const alias = SAFE_KEY.test(real) && !this.byAlias.has(real) ? real : this.fresh(prefix);
|
|
128
|
-
this.byAlias.set(alias, real);
|
|
129
|
-
this.byReal.set(real, alias);
|
|
130
|
-
return alias;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/** Unknown aliases come back as they are; Runner drops keys it does not know. */
|
|
134
|
-
real(alias: string): string {
|
|
135
|
-
return this.byAlias.get(alias) ?? alias;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
private fresh(prefix: string): string {
|
|
139
|
-
let alias: string;
|
|
140
|
-
do alias = `${prefix}${++this.n}`;
|
|
141
|
-
while (this.byAlias.has(alias));
|
|
142
|
-
return alias;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
118
|
function branchKey(branch: { runId: string; stepId: string; index: number }): string {
|
|
147
119
|
return `${branch.runId}/${branch.stepId}/${branch.index}`;
|
|
148
120
|
}
|
|
149
121
|
|
|
150
122
|
// ── Envelope ────────────────────────────────────────────────────────────
|
|
151
123
|
|
|
152
|
-
function buildEnvelope<C, D>(req: UnderstandRequest<C, D>, candidates: Flow<C, D>[], aliases: Aliases): StructuredSchema {
|
|
124
|
+
function buildEnvelope<C, D>(req: UnderstandRequest<C, D>, candidates: Flow<C, D>[], aliases: Aliases, judgesAsks: boolean): StructuredSchema {
|
|
153
125
|
const properties: Record<string, StructuredSchema> = {};
|
|
154
126
|
|
|
155
127
|
if (candidates.length) {
|
|
@@ -183,6 +155,9 @@ function buildEnvelope<C, D>(req: UnderstandRequest<C, D>, candidates: Flow<C, D
|
|
|
183
155
|
for (const [slug, def] of Object.entries(req.fields)) defs[aliases.of(slug, "d")] = def;
|
|
184
156
|
properties.fields = toWireSchema(defs, { nullable: true });
|
|
185
157
|
}
|
|
158
|
+
if (judgesAsks) {
|
|
159
|
+
properties.asks = { type: ["boolean", "null"], description: "The message asks something the reply must answer" };
|
|
160
|
+
}
|
|
186
161
|
|
|
187
162
|
return { type: "object", properties, required: Object.keys(properties), additionalProperties: false };
|
|
188
163
|
}
|
|
@@ -317,6 +292,9 @@ const OUTPUT_LINES: Record<Section, string> = {
|
|
|
317
292
|
extract: "- extract: per item id, an object with the values pulled from the message; null values when the item was not brought up",
|
|
318
293
|
branches: "- branches: true or false per question id",
|
|
319
294
|
fields: "- fields: one value per detail, null when the customer did not give it",
|
|
295
|
+
asks:
|
|
296
|
+
"- asks: true when the message asks something the reply has to answer (a question, a request for a price, a photo, " +
|
|
297
|
+
"whether an item is in stock, a store fact); false when it only answers the assistant, greets or confirms",
|
|
320
298
|
};
|
|
321
299
|
|
|
322
300
|
/**
|
|
@@ -324,7 +302,7 @@ const OUTPUT_LINES: Record<Section, string> = {
|
|
|
324
302
|
* null for anything extracted, so a prompt-only model never reads a
|
|
325
303
|
* placeholder as a default value.
|
|
326
304
|
*/
|
|
327
|
-
const PLACEHOLDER: Record<Section, unknown> = { flows: 0, mentions: false, extract: null, branches: false, fields: null };
|
|
305
|
+
const PLACEHOLDER: Record<Section, unknown> = { flows: 0, mentions: false, extract: null, branches: false, fields: null, asks: false };
|
|
328
306
|
|
|
329
307
|
function outputSection(schema: StructuredSchema): string {
|
|
330
308
|
const present = SECTIONS.filter((s) => schema.properties?.[s] !== undefined);
|
|
@@ -391,12 +369,14 @@ function parseReply(reply: Record<string, unknown>, aliases: Aliases): Understan
|
|
|
391
369
|
for (const [key, raw] of entries(reply.extract)) {
|
|
392
370
|
if (isRecord(raw)) extract[aliases.real(key)] = given(raw);
|
|
393
371
|
}
|
|
372
|
+
const asks = coerceField({ type: "boolean" }, reply.asks);
|
|
394
373
|
return {
|
|
395
374
|
flows,
|
|
396
375
|
mentions: booleans(reply.mentions, aliases),
|
|
397
376
|
extract,
|
|
398
377
|
branches: booleans(reply.branches, aliases),
|
|
399
378
|
fields: given(Object.fromEntries(entries(reply.fields).map(([k, v]) => [aliases.real(k), v]))),
|
|
379
|
+
...(asks.ok && typeof asks.value === "boolean" ? { asks: asks.value } : {}),
|
|
400
380
|
llmCalls: 1,
|
|
401
381
|
};
|
|
402
382
|
}
|
package/src/core/contracts.ts
CHANGED
|
@@ -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";
|
|
@@ -20,6 +20,8 @@ export type InputKind = "message" | "wake" | "event" | "start";
|
|
|
20
20
|
|
|
21
21
|
/** Everything the understand call may judge this turn. Runner filters by `if` and `repeat` first. */
|
|
22
22
|
export interface UnderstandRequest<C = unknown, D = unknown> {
|
|
23
|
+
/** The session's id: the model call carries it, so a gateway can keep the conversation on one cached route. */
|
|
24
|
+
sessionId: string;
|
|
23
25
|
text: string;
|
|
24
26
|
history: History;
|
|
25
27
|
context: C;
|
|
@@ -48,6 +50,11 @@ export interface Understanding {
|
|
|
48
50
|
branches: Record<string, boolean>;
|
|
49
51
|
/** field → raw value the lead gave, or nothing. */
|
|
50
52
|
fields: Record<string, unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* The lead's message asks something a reply must answer. Judged only while a flow has a fixed
|
|
55
|
+
* `question`: one that would go out now waits for the answer when this is not `false`.
|
|
56
|
+
*/
|
|
57
|
+
asks?: boolean;
|
|
51
58
|
llmCalls: number;
|
|
52
59
|
usage?: TokenUsage;
|
|
53
60
|
}
|
|
@@ -61,6 +68,12 @@ export interface TalkRequest<C = unknown, D = unknown> {
|
|
|
61
68
|
step: StepBase<D> & TalkStep<C, D>;
|
|
62
69
|
/** `step.collect` minus known minus at `maxAsks`, in order. */
|
|
63
70
|
pending: string[];
|
|
71
|
+
/**
|
|
72
|
+
* The step's fixed question, rendered, when the lead's message asked something. The reply answers it
|
|
73
|
+
* and asks this question in the same message, in the owner's words where they fit. It reads no field:
|
|
74
|
+
* the understand call already read the lead's message.
|
|
75
|
+
*/
|
|
76
|
+
fixedQuestion?: string;
|
|
64
77
|
}
|
|
65
78
|
|
|
66
79
|
/** No run holds the floor; the idle speaker answers. */
|
|
@@ -69,12 +82,22 @@ export interface IdleRequest<C = unknown, D = unknown> {
|
|
|
69
82
|
}
|
|
70
83
|
|
|
71
84
|
export interface SpeakRequest<C = unknown, D = unknown> {
|
|
85
|
+
/** See `UnderstandRequest.sessionId`. */
|
|
86
|
+
sessionId: string;
|
|
72
87
|
talk: TalkRequest<C, D> | IdleRequest<C, D>;
|
|
73
|
-
/**
|
|
74
|
-
|
|
88
|
+
/**
|
|
89
|
+
* What started this turn. A wake has no text: the assistant speaks first, unless `retry` says this
|
|
90
|
+
* talk step failed at the provider and is running again, with the customer's message still unanswered.
|
|
91
|
+
*/
|
|
92
|
+
input: { kind: InputKind; text?: string; retry?: boolean };
|
|
75
93
|
context: C;
|
|
76
94
|
data: Partial<D>;
|
|
77
95
|
history: History;
|
|
96
|
+
/**
|
|
97
|
+
* What this turn already sent before the reply, in order: a `say`, a fixed question. `history` ends
|
|
98
|
+
* before the turn, so without these the model cannot see them and says the same thing again.
|
|
99
|
+
*/
|
|
100
|
+
said: OutboundMessage[];
|
|
78
101
|
now: Date;
|
|
79
102
|
/** Already filtered by their `if`; `when` stays for the prompt. */
|
|
80
103
|
instructions: Instruction<C, D>[];
|
|
@@ -87,7 +110,6 @@ export interface Spoken {
|
|
|
87
110
|
fields: Record<string, unknown>;
|
|
88
111
|
/** Data patches returned by tools, merged in call order. */
|
|
89
112
|
data: Record<string, unknown>;
|
|
90
|
-
toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
91
113
|
llmCalls: number;
|
|
92
114
|
usage?: TokenUsage;
|
|
93
115
|
}
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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}]
|
|
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}]
|
|
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}]
|
|
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
|
-
//
|
|
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(
|
|
49
|
-
if (!options.model) throw new Error(
|
|
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(
|
|
63
|
-
if (!options.model)
|
|
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",
|