@falai/agent 2.4.3 → 2.6.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/dist/cjs/core/FlowRouter.d.ts.map +1 -1
- package/dist/cjs/core/FlowRouter.js +0 -11
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts +51 -2
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +272 -269
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +11 -0
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +75 -51
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +18 -35
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts +1 -12
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -20
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +22 -37
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +93 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +23 -5
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.js +46 -44
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/index.d.ts +3 -1
- package/dist/cjs/providers/index.d.ts.map +1 -1
- package/dist/cjs/providers/index.js +3 -1
- package/dist/cjs/providers/index.js.map +1 -1
- package/dist/cjs/utils/completion.d.ts +20 -0
- package/dist/cjs/utils/completion.d.ts.map +1 -0
- package/dist/cjs/utils/completion.js +33 -0
- package/dist/cjs/utils/completion.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +3 -2
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +7 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +43 -7
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +100 -12
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/cjs/utils/streamingMessage.d.ts +48 -0
- package/dist/cjs/utils/streamingMessage.d.ts.map +1 -0
- package/dist/cjs/utils/streamingMessage.js +210 -0
- package/dist/cjs/utils/streamingMessage.js.map +1 -0
- package/dist/core/FlowRouter.d.ts.map +1 -1
- package/dist/core/FlowRouter.js +0 -11
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/ResponseModal.d.ts +51 -2
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +272 -269
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +11 -0
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +76 -52
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +19 -36
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts +1 -12
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -20
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +23 -38
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
- package/dist/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/providers/GenericOpenAICompatibleProvider.js +87 -0
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts +23 -5
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.js +47 -45
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/index.d.ts +3 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/utils/completion.d.ts +20 -0
- package/dist/utils/completion.d.ts.map +1 -0
- package/dist/utils/completion.js +29 -0
- package/dist/utils/completion.js.map +1 -0
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +43 -7
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +98 -12
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/streamingMessage.d.ts +48 -0
- package/dist/utils/streamingMessage.d.ts.map +1 -0
- package/dist/utils/streamingMessage.js +205 -0
- package/dist/utils/streamingMessage.js.map +1 -0
- package/package.json +1 -1
- package/src/core/FlowRouter.ts +0 -14
- package/src/core/ResponseModal.ts +332 -299
- package/src/core/ToolLoopExecutor.ts +108 -52
- package/src/index.ts +3 -0
- package/src/providers/AnthropicProvider.ts +22 -43
- package/src/providers/DeepSeekProvider.ts +3 -35
- package/src/providers/GeminiProvider.ts +27 -46
- package/src/providers/GenericOpenAICompatibleProvider.ts +138 -0
- package/src/providers/OpenAICompatibleProvider.ts +87 -67
- package/src/providers/index.ts +4 -0
- package/src/utils/completion.ts +38 -0
- package/src/utils/index.ts +5 -2
- package/src/utils/retry.ts +136 -14
- package/src/utils/streamingMessage.ts +220 -0
|
@@ -26,7 +26,7 @@ import type { Flow } from "./Flow";
|
|
|
26
26
|
import type { Step } from "./Step";
|
|
27
27
|
import type { ToolManager } from "./ToolManager";
|
|
28
28
|
import { ResponseGenerationError } from "./ResponseGenerationError";
|
|
29
|
-
import { historyToEvents, logger, serializeToolResult } from "../utils";
|
|
29
|
+
import { historyToEvents, logger, serializeToolResult, assistantMessage, toolMessage } from "../utils";
|
|
30
30
|
|
|
31
31
|
export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
32
32
|
constructor(
|
|
@@ -72,7 +72,10 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
72
72
|
}> {
|
|
73
73
|
try {
|
|
74
74
|
const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal } = params;
|
|
75
|
-
|
|
75
|
+
// The follow-up loop (which reassigned toolCalls) now lives in
|
|
76
|
+
// runFollowUpLoop; here toolCalls is only read.
|
|
77
|
+
const { toolCalls } = params;
|
|
78
|
+
let { session } = params;
|
|
76
79
|
|
|
77
80
|
// Convert HistoryItem[] to Event[] for internal processing
|
|
78
81
|
const historyEvents = historyToEvents(history);
|
|
@@ -146,6 +149,66 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
152
|
+
// Hand off to the multi-round follow-up loop shared with the
|
|
153
|
+
// streaming path. The initial batch above already executed
|
|
154
|
+
// sequentially and populated the result maps.
|
|
155
|
+
return await this.runFollowUpLoop({
|
|
156
|
+
toolCalls,
|
|
157
|
+
session,
|
|
158
|
+
toolResultsMap,
|
|
159
|
+
toolArgsMap,
|
|
160
|
+
context,
|
|
161
|
+
history,
|
|
162
|
+
selectedFlow,
|
|
163
|
+
responsePrompt,
|
|
164
|
+
availableTools,
|
|
165
|
+
responseSchema,
|
|
166
|
+
signal,
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
|
|
170
|
+
toolCallsCount: params.toolCalls?.length || 0,
|
|
171
|
+
availableToolsCount: params.availableTools.length
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The multi-round follow-up loop shared by the non-streaming (`runLoop`) and
|
|
178
|
+
* streaming (`runStreamingBatch`) tool paths: re-prompt the model with the
|
|
179
|
+
* tool results (tools available on the first round so it can chain further
|
|
180
|
+
* calls), execute any further tool calls, repeat up to `maxToolLoops`, then
|
|
181
|
+
* force a result-aware closing message if the model never produced one.
|
|
182
|
+
* Callers run the *initial* batch — sequentially for `runLoop`, concurrently
|
|
183
|
+
* (with progress) for `runStreamingBatch` — and pass the populated result
|
|
184
|
+
* maps; from here both paths behave identically.
|
|
185
|
+
*/
|
|
186
|
+
private async runFollowUpLoop(params: {
|
|
187
|
+
toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
188
|
+
session: SessionState<TData>;
|
|
189
|
+
toolResultsMap: Map<string, string>;
|
|
190
|
+
toolArgsMap: Map<string, Record<string, unknown>>;
|
|
191
|
+
context: TContext;
|
|
192
|
+
history: HistoryItem[];
|
|
193
|
+
selectedFlow?: Flow<TContext, TData>;
|
|
194
|
+
responsePrompt: string;
|
|
195
|
+
availableTools: Array<{
|
|
196
|
+
id: string;
|
|
197
|
+
name: string;
|
|
198
|
+
description?: string;
|
|
199
|
+
parameters?: unknown;
|
|
200
|
+
}>;
|
|
201
|
+
responseSchema?: Record<string, unknown>;
|
|
202
|
+
signal?: AbortSignal;
|
|
203
|
+
}): Promise<{
|
|
204
|
+
session: SessionState<TData>;
|
|
205
|
+
finalToolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
|
|
206
|
+
finalMessage?: string;
|
|
207
|
+
structured?: AgentStructuredResponse;
|
|
208
|
+
}> {
|
|
209
|
+
const { context, history, selectedFlow, responsePrompt, availableTools, responseSchema, signal, toolResultsMap, toolArgsMap } = params;
|
|
210
|
+
let { toolCalls, session } = params;
|
|
211
|
+
try {
|
|
149
212
|
// TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
|
|
150
213
|
// `??` so an explicit `maxToolLoops: 0` is honored instead of being
|
|
151
214
|
// clobbered to the default by a falsy-zero check.
|
|
@@ -164,24 +227,18 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
164
227
|
for (const toolCall of toolCalls || []) {
|
|
165
228
|
const tool = this.findAvailableTool(toolCall.toolName, selectedFlow);
|
|
166
229
|
if (tool) {
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
toolResultHistoryItems.push(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
toolResultHistoryItems.push({
|
|
180
|
-
role: "tool" as const,
|
|
181
|
-
tool_call_id: toolCall.toolName,
|
|
182
|
-
name: toolCall.toolName,
|
|
183
|
-
content: toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
|
|
184
|
-
});
|
|
230
|
+
// assistant tool_call + tool result pair, via the shared
|
|
231
|
+
// history factories (same as forceFinalTextFromTools).
|
|
232
|
+
toolResultHistoryItems.push(
|
|
233
|
+
assistantMessage(null, [
|
|
234
|
+
{ id: toolCall.toolName, name: toolCall.toolName, arguments: toolCall.arguments },
|
|
235
|
+
]),
|
|
236
|
+
toolMessage(
|
|
237
|
+
toolCall.toolName,
|
|
238
|
+
toolCall.toolName,
|
|
239
|
+
toolResultsMap.get(toolCall.toolName) || "Tool executed successfully",
|
|
240
|
+
),
|
|
241
|
+
);
|
|
185
242
|
}
|
|
186
243
|
}
|
|
187
244
|
|
|
@@ -364,21 +421,12 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
364
421
|
// call can see what the tools returned.
|
|
365
422
|
const finalToolResultHistoryItems: HistoryItem[] = [];
|
|
366
423
|
for (const [toolName, toolResult] of toolResultsMap) {
|
|
367
|
-
finalToolResultHistoryItems.push(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
arguments: toolArgsMap.get(toolName) || {},
|
|
374
|
-
}],
|
|
375
|
-
});
|
|
376
|
-
finalToolResultHistoryItems.push({
|
|
377
|
-
role: "tool" as const,
|
|
378
|
-
tool_call_id: toolName,
|
|
379
|
-
name: toolName,
|
|
380
|
-
content: toolResult,
|
|
381
|
-
});
|
|
424
|
+
finalToolResultHistoryItems.push(
|
|
425
|
+
assistantMessage(null, [
|
|
426
|
+
{ id: toolName, name: toolName, arguments: toolArgsMap.get(toolName) || {} },
|
|
427
|
+
]),
|
|
428
|
+
toolMessage(toolName, toolName, toolResult),
|
|
429
|
+
);
|
|
382
430
|
}
|
|
383
431
|
|
|
384
432
|
const finalHistory = [...history, ...finalToolResultHistoryItems];
|
|
@@ -522,25 +570,33 @@ export class ToolLoopExecutor<TContext = unknown, TData = unknown> {
|
|
|
522
570
|
|
|
523
571
|
logger.debug(`[ToolLoopExecutor] Concurrent tool execution completed for ${toolCallRequests.length} tools`);
|
|
524
572
|
|
|
525
|
-
//
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
573
|
+
// Multi-round follow-up shared with the non-streaming path: re-prompt
|
|
574
|
+
// with the tool results so the model can chain further tool calls,
|
|
575
|
+
// then produce result-aware closing text (forced if it never does).
|
|
576
|
+
// Previously a single forced-text call — streaming now loops to parity
|
|
577
|
+
// with runLoop, so a streamed turn can chain tools across rounds.
|
|
578
|
+
const followUp = await this.runFollowUpLoop({
|
|
579
|
+
toolCalls,
|
|
580
|
+
session,
|
|
581
|
+
toolResultsMap,
|
|
582
|
+
toolArgsMap,
|
|
583
|
+
context,
|
|
584
|
+
history,
|
|
585
|
+
selectedFlow,
|
|
586
|
+
responsePrompt,
|
|
587
|
+
availableTools,
|
|
588
|
+
responseSchema,
|
|
589
|
+
signal,
|
|
590
|
+
});
|
|
591
|
+
session = followUp.session;
|
|
592
|
+
toolCalls = followUp.finalToolCalls;
|
|
593
|
+
finalMessage = followUp.finalMessage;
|
|
594
|
+
structured = followUp.structured;
|
|
541
595
|
} catch (error) {
|
|
542
596
|
logger.error(`[ToolLoopExecutor] Concurrent tool execution failed, falling back to sequential:`, error);
|
|
543
|
-
// Fall back to the unified tool loop on failure
|
|
597
|
+
// Fall back to the unified tool loop on failure. runLoop re-executes
|
|
598
|
+
// the tools from scratch, so any partial results collected above are
|
|
599
|
+
// intentionally discarded (it builds and forces its own).
|
|
544
600
|
const toolResult = await this.runLoop({
|
|
545
601
|
toolCalls, context, session, history, selectedFlow,
|
|
546
602
|
responsePrompt, availableTools, responseSchema, signal,
|
package/src/index.ts
CHANGED
|
@@ -32,6 +32,9 @@ export { DeepSeekProvider } from "./providers/DeepSeekProvider";
|
|
|
32
32
|
export type { DeepSeekProviderOptions } from "./providers/DeepSeekProvider";
|
|
33
33
|
// Base class for building OpenAI-compatible providers (Groq, Together, etc.)
|
|
34
34
|
export { OpenAICompatibleProvider } from "./providers/OpenAICompatibleProvider";
|
|
35
|
+
export type { StructuredOutputMode } from "./providers/OpenAICompatibleProvider";
|
|
36
|
+
export { createOpenAICompatibleProvider } from "./providers/GenericOpenAICompatibleProvider";
|
|
37
|
+
export type { OpenAICompatibleOptions } from "./providers/GenericOpenAICompatibleProvider";
|
|
35
38
|
|
|
36
39
|
// Adapters
|
|
37
40
|
export { PrismaAdapter } from "./adapters/PrismaAdapter";
|
|
@@ -17,7 +17,7 @@ import type {
|
|
|
17
17
|
} from "../types";
|
|
18
18
|
import type { ProviderCapabilities } from "../types/ai";
|
|
19
19
|
import type { HistoryItem } from "../types/history";
|
|
20
|
-
import { withTimeoutAndRetry, withStreamRetry, logger } from "../utils";
|
|
20
|
+
import { withTimeoutAndRetry, withStreamRetry, resolveRetryConfig, logger, assertUsableCompletion, combineAbortSignals } from "../utils";
|
|
21
21
|
import {
|
|
22
22
|
classifyProviderError,
|
|
23
23
|
getErrorMessage,
|
|
@@ -26,11 +26,6 @@ import {
|
|
|
26
26
|
type ErrorClassificationOptions,
|
|
27
27
|
} from "./errorClassification";
|
|
28
28
|
|
|
29
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
30
|
-
timeout: 60000,
|
|
31
|
-
retries: 3,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
29
|
/**
|
|
35
30
|
* Configuration options for Anthropic provider
|
|
36
31
|
* Uses types from @anthropic-ai/sdk package
|
|
@@ -105,14 +100,7 @@ export class AnthropicProvider implements AiProvider {
|
|
|
105
100
|
this.primaryModel = model;
|
|
106
101
|
this.backupModels = backupModels;
|
|
107
102
|
this.config = config;
|
|
108
|
-
this.retryConfig =
|
|
109
|
-
// `||` is intentional: a 0ms timeout is degenerate (aborts every call
|
|
110
|
-
// immediately), so fall back to the default.
|
|
111
|
-
timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
112
|
-
// `??` so an explicit `retries: 0` (disable retries) is honored instead of
|
|
113
|
-
// being clobbered to the default by a falsy-zero check.
|
|
114
|
-
retries: retryConfig?.retries ?? DEFAULT_RETRY_CONFIG.retries,
|
|
115
|
-
};
|
|
103
|
+
this.retryConfig = resolveRetryConfig(retryConfig);
|
|
116
104
|
}
|
|
117
105
|
|
|
118
106
|
/**
|
|
@@ -268,7 +256,7 @@ export class AnthropicProvider implements AiProvider {
|
|
|
268
256
|
model: string,
|
|
269
257
|
input: GenerateMessageInput<TContext>
|
|
270
258
|
): Promise<GenerateMessageOutput<TStructured>> {
|
|
271
|
-
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
259
|
+
const operation = async (signal: AbortSignal): Promise<GenerateMessageOutput> => {
|
|
272
260
|
// Anthropic requires max_tokens to be specified
|
|
273
261
|
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
274
262
|
|
|
@@ -332,7 +320,9 @@ export class AnthropicProvider implements AiProvider {
|
|
|
332
320
|
}
|
|
333
321
|
}
|
|
334
322
|
|
|
335
|
-
const response = await this.client.messages.create(params
|
|
323
|
+
const response = await this.client.messages.create(params, {
|
|
324
|
+
signal: combineAbortSignals(input.signal, signal),
|
|
325
|
+
});
|
|
336
326
|
|
|
337
327
|
// Extract text and tool calls from response
|
|
338
328
|
const textContent = response.content.find(
|
|
@@ -381,16 +371,10 @@ export class AnthropicProvider implements AiProvider {
|
|
|
381
371
|
} as AgentStructuredResponse;
|
|
382
372
|
}
|
|
383
373
|
|
|
384
|
-
// A parsed-but-blank
|
|
385
|
-
//
|
|
386
|
-
//
|
|
387
|
-
|
|
388
|
-
toolCalls.length === 0 &&
|
|
389
|
-
typeof structured?.message === "string" &&
|
|
390
|
-
!structured.message.trim()
|
|
391
|
-
) {
|
|
392
|
-
throw new Error("No response from Anthropic");
|
|
393
|
-
}
|
|
374
|
+
// A parsed-but-blank message with no tool calls is as empty as no text;
|
|
375
|
+
// the shared guard throws so withTimeoutAndRetry retries instead of
|
|
376
|
+
// returning {"message":""}.
|
|
377
|
+
assertUsableCompletion(structured, message, toolCalls.length, "Anthropic");
|
|
394
378
|
|
|
395
379
|
return {
|
|
396
380
|
message,
|
|
@@ -423,8 +407,8 @@ export class AnthropicProvider implements AiProvider {
|
|
|
423
407
|
// Try primary model first
|
|
424
408
|
try {
|
|
425
409
|
yield* withStreamRetry(
|
|
426
|
-
() => this.generateStreamWithModel<TContext, TStructured>(this.primaryModel, input),
|
|
427
|
-
{ maxRetries: this.retryConfig.retries, operationName: `Anthropic ${this.primaryModel} stream` }
|
|
410
|
+
(signal) => this.generateStreamWithModel<TContext, TStructured>(this.primaryModel, input, signal),
|
|
411
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `Anthropic ${this.primaryModel} stream` }
|
|
428
412
|
);
|
|
429
413
|
} catch (primaryError: unknown) {
|
|
430
414
|
const primaryErrMsg = getErrorMessage(primaryError);
|
|
@@ -449,8 +433,8 @@ export class AnthropicProvider implements AiProvider {
|
|
|
449
433
|
|
|
450
434
|
try {
|
|
451
435
|
yield* withStreamRetry(
|
|
452
|
-
() => this.generateStreamWithModel<TContext, TStructured>(backupModel, input),
|
|
453
|
-
{ maxRetries: this.retryConfig.retries, operationName: `Anthropic ${backupModel} stream` }
|
|
436
|
+
(signal) => this.generateStreamWithModel<TContext, TStructured>(backupModel, input, signal),
|
|
437
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `Anthropic ${backupModel} stream` }
|
|
454
438
|
);
|
|
455
439
|
logger.debug(`[ANTHROPIC] Backup model ${backupModel} succeeded`);
|
|
456
440
|
return;
|
|
@@ -486,7 +470,8 @@ export class AnthropicProvider implements AiProvider {
|
|
|
486
470
|
TStructured = AgentStructuredResponse
|
|
487
471
|
>(
|
|
488
472
|
model: string,
|
|
489
|
-
input: GenerateMessageInput<TContext
|
|
473
|
+
input: GenerateMessageInput<TContext>,
|
|
474
|
+
attemptSignal?: AbortSignal
|
|
490
475
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
491
476
|
// Anthropic requires max_tokens to be specified
|
|
492
477
|
const maxTokens = input.parameters?.maxOutputTokens || 4096;
|
|
@@ -551,7 +536,9 @@ export class AnthropicProvider implements AiProvider {
|
|
|
551
536
|
}
|
|
552
537
|
}
|
|
553
538
|
|
|
554
|
-
const stream = this.client.messages.stream(params
|
|
539
|
+
const stream = this.client.messages.stream(params, {
|
|
540
|
+
signal: combineAbortSignals(input.signal, attemptSignal),
|
|
541
|
+
});
|
|
555
542
|
|
|
556
543
|
let accumulated = "";
|
|
557
544
|
let currentModel = model;
|
|
@@ -612,17 +599,9 @@ export class AnthropicProvider implements AiProvider {
|
|
|
612
599
|
} as AgentStructuredResponse;
|
|
613
600
|
}
|
|
614
601
|
|
|
615
|
-
// Empty-completion guard —
|
|
616
|
-
//
|
|
617
|
-
|
|
618
|
-
// so withStreamRetry/generateStreamWithBackup retry instead of silently
|
|
619
|
-
// emitting an empty message.
|
|
620
|
-
const messageText = (
|
|
621
|
-
typeof structured?.message === "string" ? structured.message : accumulated
|
|
622
|
-
).trim();
|
|
623
|
-
if (!messageText && toolCalls.length === 0) {
|
|
624
|
-
throw new Error("No response from Anthropic");
|
|
625
|
-
}
|
|
602
|
+
// Empty-completion guard — same definition as the non-streaming path, so the
|
|
603
|
+
// stream retries / falls back to backup instead of emitting an empty message.
|
|
604
|
+
assertUsableCompletion(structured, accumulated, toolCalls.length, "Anthropic");
|
|
626
605
|
|
|
627
606
|
// Yield final chunk
|
|
628
607
|
yield {
|
|
@@ -7,15 +7,9 @@
|
|
|
7
7
|
import OpenAI from "openai";
|
|
8
8
|
import type {
|
|
9
9
|
ChatCompletionChunk,
|
|
10
|
-
ChatCompletionCreateParamsNonStreaming,
|
|
11
10
|
ChatCompletionCreateParamsStreaming,
|
|
12
11
|
} from "openai/resources/chat/completions";
|
|
13
12
|
|
|
14
|
-
import type {
|
|
15
|
-
GenerateMessageInput,
|
|
16
|
-
GenerateMessageOutput,
|
|
17
|
-
StructuredSchema,
|
|
18
|
-
} from "../types";
|
|
19
13
|
import type { ProviderCapabilities } from "../types/ai";
|
|
20
14
|
import { logger } from "../utils";
|
|
21
15
|
import {
|
|
@@ -96,38 +90,12 @@ export class DeepSeekProvider extends OpenAICompatibleProvider {
|
|
|
96
90
|
backupModels,
|
|
97
91
|
config,
|
|
98
92
|
retryConfig,
|
|
93
|
+
// DeepSeek has no responses.parse API; structured output goes through
|
|
94
|
+
// chat completions with a native json_schema response_format.
|
|
95
|
+
structuredOutput: "json_schema",
|
|
99
96
|
});
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
/**
|
|
103
|
-
* DeepSeek has no responses.parse API; structured output goes through
|
|
104
|
-
* chat completions with a json_schema response_format.
|
|
105
|
-
*/
|
|
106
|
-
protected override async executeStructuredGenerate(
|
|
107
|
-
model: string,
|
|
108
|
-
input: GenerateMessageInput<unknown>,
|
|
109
|
-
_jsonSchema: StructuredSchema
|
|
110
|
-
): Promise<GenerateMessageOutput> {
|
|
111
|
-
return this.executeChatCompletion(model, input);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* DeepSeek supports native json_schema enforcement in chat completions
|
|
116
|
-
* (both streaming and non-streaming).
|
|
117
|
-
*/
|
|
118
|
-
protected override structuredResponseFormat(
|
|
119
|
-
jsonSchema: StructuredSchema,
|
|
120
|
-
schemaName: string | undefined
|
|
121
|
-
): ChatCompletionCreateParamsNonStreaming["response_format"] {
|
|
122
|
-
return {
|
|
123
|
-
type: "json_schema" as const,
|
|
124
|
-
json_schema: {
|
|
125
|
-
name: schemaName || "structured_output",
|
|
126
|
-
schema: this.adaptSchema(jsonSchema),
|
|
127
|
-
},
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
99
|
/**
|
|
132
100
|
* DeepSeek reports usage in streaming chunks when explicitly requested.
|
|
133
101
|
*/
|
|
@@ -21,7 +21,8 @@ import type {
|
|
|
21
21
|
} from "../types";
|
|
22
22
|
import type { ProviderCapabilities } from "../types/ai";
|
|
23
23
|
import type { HistoryItem } from "../types/history";
|
|
24
|
-
import { withTimeoutAndRetry, withStreamRetry } from "../utils/retry";
|
|
24
|
+
import { withTimeoutAndRetry, withStreamRetry, resolveRetryConfig, combineAbortSignals } from "../utils/retry";
|
|
25
|
+
import { assertUsableCompletion } from "../utils/completion";
|
|
25
26
|
import { tryParseJSONResponse } from "../utils/json";
|
|
26
27
|
import { logger } from "../utils/logger";
|
|
27
28
|
import {
|
|
@@ -32,11 +33,6 @@ import {
|
|
|
32
33
|
type ErrorClassificationOptions,
|
|
33
34
|
} from "./errorClassification";
|
|
34
35
|
|
|
35
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
36
|
-
timeout: 60000,
|
|
37
|
-
retries: 3,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
36
|
/**
|
|
41
37
|
* Configuration options for Gemini provider
|
|
42
38
|
* Uses types from @google/genai package
|
|
@@ -106,14 +102,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
106
102
|
this.primaryModel = model;
|
|
107
103
|
this.backupModels = backupModels;
|
|
108
104
|
this.config = config;
|
|
109
|
-
this.retryConfig =
|
|
110
|
-
// `||` is intentional: a 0ms timeout is degenerate (aborts every call
|
|
111
|
-
// immediately), so fall back to the default.
|
|
112
|
-
timeout: retryConfig?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
113
|
-
// `??` so an explicit `retries: 0` (disable retries) is honored instead of
|
|
114
|
-
// being clobbered to the default by a falsy-zero check.
|
|
115
|
-
retries: retryConfig?.retries ?? DEFAULT_RETRY_CONFIG.retries,
|
|
116
|
-
};
|
|
105
|
+
this.retryConfig = resolveRetryConfig(retryConfig);
|
|
117
106
|
}
|
|
118
107
|
|
|
119
108
|
/**
|
|
@@ -200,13 +189,15 @@ export class GeminiProvider implements AiProvider {
|
|
|
200
189
|
*
|
|
201
190
|
* @private
|
|
202
191
|
*/
|
|
203
|
-
private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; functionCall?: unknown }> } }> }): string {
|
|
192
|
+
private safeExtractText(responseOrChunk: { text?: string; candidates?: Array<{ content?: { parts?: Array<{ text?: string; thought?: boolean; functionCall?: unknown }> } }> }): string {
|
|
204
193
|
// Always extract text parts manually to avoid SDK warnings about
|
|
205
194
|
// non-text parts like "thoughtSignature" in the response.
|
|
206
195
|
const parts = responseOrChunk.candidates?.[0]?.content?.parts;
|
|
207
196
|
if (parts) {
|
|
197
|
+
// Exclude reasoning parts (thought: true) — with includeThoughts enabled
|
|
198
|
+
// they carry text but must never leak into the user-facing message.
|
|
208
199
|
return parts
|
|
209
|
-
.filter((p) => p.text != null)
|
|
200
|
+
.filter((p) => p.text != null && !p.thought)
|
|
210
201
|
.map((p) => p.text)
|
|
211
202
|
.join("");
|
|
212
203
|
}
|
|
@@ -423,7 +414,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
423
414
|
model: string,
|
|
424
415
|
input: GenerateMessageInput<TContext>
|
|
425
416
|
): Promise<GenerateMessageOutput<TStructured>> {
|
|
426
|
-
const operation = async (): Promise<GenerateMessageOutput> => {
|
|
417
|
+
const operation = async (signal: AbortSignal): Promise<GenerateMessageOutput> => {
|
|
427
418
|
// Schema-required: configure response schema
|
|
428
419
|
const configOverride: Partial<GenerateContentConfig> = { ...this.config };
|
|
429
420
|
|
|
@@ -468,12 +459,13 @@ export class GeminiProvider implements AiProvider {
|
|
|
468
459
|
}
|
|
469
460
|
}
|
|
470
461
|
|
|
462
|
+
const reqSignal = combineAbortSignals(input.signal, signal);
|
|
471
463
|
response = await this.genAI.models.generateContent({
|
|
472
464
|
model,
|
|
473
465
|
contents: historyContents,
|
|
474
466
|
config: {
|
|
475
467
|
...configOverride,
|
|
476
|
-
...(
|
|
468
|
+
...(reqSignal ? { abortSignal: reqSignal } : {}),
|
|
477
469
|
},
|
|
478
470
|
});
|
|
479
471
|
} catch (error: unknown) {
|
|
@@ -542,16 +534,10 @@ export class GeminiProvider implements AiProvider {
|
|
|
542
534
|
} as AgentStructuredResponse;
|
|
543
535
|
}
|
|
544
536
|
|
|
545
|
-
// A parsed-but-blank
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
toolCalls.length === 0 &&
|
|
550
|
-
typeof structured?.message === "string" &&
|
|
551
|
-
!structured.message.trim()
|
|
552
|
-
) {
|
|
553
|
-
throw new Error("No response from Gemini");
|
|
554
|
-
}
|
|
537
|
+
// A parsed-but-blank message with no tool calls is as empty as no text;
|
|
538
|
+
// the shared guard throws so withTimeoutAndRetry retries instead of
|
|
539
|
+
// returning {"message":""}.
|
|
540
|
+
assertUsableCompletion(structured, message, toolCalls.length, "Gemini");
|
|
555
541
|
|
|
556
542
|
return {
|
|
557
543
|
message,
|
|
@@ -582,8 +568,8 @@ export class GeminiProvider implements AiProvider {
|
|
|
582
568
|
// Try primary model first
|
|
583
569
|
try {
|
|
584
570
|
yield* withStreamRetry(
|
|
585
|
-
() => this.generateStreamWithModel(this.primaryModel, input),
|
|
586
|
-
{ maxRetries: this.retryConfig.retries, operationName: `Gemini ${this.primaryModel} stream` }
|
|
571
|
+
(signal) => this.generateStreamWithModel(this.primaryModel, input, signal),
|
|
572
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `Gemini ${this.primaryModel} stream` }
|
|
587
573
|
);
|
|
588
574
|
} catch (primaryError: unknown) {
|
|
589
575
|
const primaryErrMsg = getErrorMessage(primaryError);
|
|
@@ -608,8 +594,8 @@ export class GeminiProvider implements AiProvider {
|
|
|
608
594
|
|
|
609
595
|
try {
|
|
610
596
|
yield* withStreamRetry(
|
|
611
|
-
() => this.generateStreamWithModel(backupModel, input),
|
|
612
|
-
{ maxRetries: this.retryConfig.retries, operationName: `Gemini ${backupModel} stream` }
|
|
597
|
+
(signal) => this.generateStreamWithModel(backupModel, input, signal),
|
|
598
|
+
{ maxRetries: this.retryConfig.retries, firstChunkTimeoutMs: this.retryConfig.timeout, operationName: `Gemini ${backupModel} stream` }
|
|
613
599
|
);
|
|
614
600
|
logger.debug(`[GEMINI] Backup model ${backupModel} succeeded`);
|
|
615
601
|
return;
|
|
@@ -645,8 +631,11 @@ export class GeminiProvider implements AiProvider {
|
|
|
645
631
|
TStructured = AgentStructuredResponse
|
|
646
632
|
>(
|
|
647
633
|
model: string,
|
|
648
|
-
input: GenerateMessageInput<TContext
|
|
634
|
+
input: GenerateMessageInput<TContext>,
|
|
635
|
+
attemptSignal?: AbortSignal
|
|
649
636
|
): AsyncGenerator<GenerateMessageStreamChunk<TStructured>> {
|
|
637
|
+
// Caller cancellation + the retry helper's per-attempt deadline, as one.
|
|
638
|
+
const reqSignal = combineAbortSignals(input.signal, attemptSignal);
|
|
650
639
|
// Streaming: request JSON if schema provided
|
|
651
640
|
const configOverride: Partial<GenerateContentConfig> = { ...this.config };
|
|
652
641
|
|
|
@@ -696,7 +685,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
696
685
|
contents: historyContents,
|
|
697
686
|
config: {
|
|
698
687
|
...configOverride,
|
|
699
|
-
...(
|
|
688
|
+
...(reqSignal ? { abortSignal: reqSignal } : {}),
|
|
700
689
|
},
|
|
701
690
|
});
|
|
702
691
|
} catch (error: unknown) {
|
|
@@ -714,7 +703,7 @@ export class GeminiProvider implements AiProvider {
|
|
|
714
703
|
}> = [];
|
|
715
704
|
|
|
716
705
|
for await (const chunk of stream) {
|
|
717
|
-
if (
|
|
706
|
+
if (reqSignal?.aborted) break;
|
|
718
707
|
|
|
719
708
|
// Safely extract text — chunk.text can throw when chunk has only function calls
|
|
720
709
|
const delta = this.safeExtractText(chunk);
|
|
@@ -768,17 +757,9 @@ export class GeminiProvider implements AiProvider {
|
|
|
768
757
|
} as AgentStructuredResponse;
|
|
769
758
|
}
|
|
770
759
|
|
|
771
|
-
// Empty-completion guard —
|
|
772
|
-
//
|
|
773
|
-
|
|
774
|
-
// produced nothing usable; throw so withStreamRetry/generateStreamWithBackup
|
|
775
|
-
// retry instead of silently emitting an empty message.
|
|
776
|
-
const messageText = (
|
|
777
|
-
typeof structured?.message === "string" ? structured.message : accumulated
|
|
778
|
-
).trim();
|
|
779
|
-
if (!messageText && toolCalls.length === 0) {
|
|
780
|
-
throw new Error("No response from Gemini");
|
|
781
|
-
}
|
|
760
|
+
// Empty-completion guard — same definition as the non-streaming path, so the
|
|
761
|
+
// stream retries / falls back to backup instead of emitting an empty message.
|
|
762
|
+
assertUsableCompletion(structured, accumulated, toolCalls.length, "Gemini");
|
|
782
763
|
|
|
783
764
|
// Yield final chunk
|
|
784
765
|
yield {
|