@falai/agent 2.4.3 → 2.5.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/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 +19 -36
- 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/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 +20 -37
- 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/package.json +1 -1
- 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 +23 -44
- 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/dist/utils/retry.d.ts
CHANGED
|
@@ -5,8 +5,37 @@ export interface RetryOptions<T> {
|
|
|
5
5
|
onRetry?: (attempt: number, error: unknown) => void;
|
|
6
6
|
onFailure?: (error: unknown) => boolean;
|
|
7
7
|
}
|
|
8
|
+
/** Provider timeout (ms) + retry count, after defaults are applied. */
|
|
9
|
+
export interface RetryConfig {
|
|
10
|
+
timeout: number;
|
|
11
|
+
retries: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Normalize a provider's optional retry config against the defaults. `timeout`
|
|
15
|
+
* uses `||` (a 0ms timeout is degenerate — it aborts every call immediately — so
|
|
16
|
+
* fall back to the default), while `retries` uses `??` so an explicit
|
|
17
|
+
* `retries: 0` (disable retries) is honored rather than treated as unset. Single
|
|
18
|
+
* definition so this distinction can't drift between providers.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveRetryConfig(input?: {
|
|
21
|
+
timeout?: number;
|
|
22
|
+
retries?: number;
|
|
23
|
+
}): RetryConfig;
|
|
8
24
|
export declare function retry<T>(options: RetryOptions<T>): Promise<T>;
|
|
9
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Combine abort signals into one that aborts as soon as any input aborts.
|
|
27
|
+
* `undefined` inputs are skipped, so a caller can pass an optional caller-signal
|
|
28
|
+
* alongside a required per-attempt one. Returns the sole signal when only one is
|
|
29
|
+
* present (no wrapper), or `undefined` when none are.
|
|
30
|
+
*
|
|
31
|
+
* Prefers the platform `AbortSignal.any`, which cleans up its listeners via weak
|
|
32
|
+
* refs — so merging onto a long-lived caller signal that's reused across many
|
|
33
|
+
* calls can't accumulate listeners. Falls back to a manual controller on older
|
|
34
|
+
* runtimes (< Node 20.3); there the listeners live until a source aborts or is
|
|
35
|
+
* garbage-collected, which is fine for the usual per-request signal.
|
|
36
|
+
*/
|
|
37
|
+
export declare function combineAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal | undefined;
|
|
38
|
+
export declare const withTimeoutAndRetry: <T>(operation: (signal: AbortSignal) => Promise<T>, timeoutMs?: number, maxRetries?: number, operationName?: string) => Promise<T>;
|
|
10
39
|
export interface StreamRetryOptions {
|
|
11
40
|
/** Maximum number of retries after the first attempt. Defaults to 3. */
|
|
12
41
|
maxRetries?: number;
|
|
@@ -14,18 +43,25 @@ export interface StreamRetryOptions {
|
|
|
14
43
|
delay?: (attempt: number) => number;
|
|
15
44
|
/** Label used in retry logs. */
|
|
16
45
|
operationName?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Max ms to wait for the *first* chunk (time-to-first-token) before treating
|
|
48
|
+
* the attempt as failed. Guards a provider that opens a stream and then
|
|
49
|
+
* stalls. Only the first chunk is bounded — later chunks are unbounded so a
|
|
50
|
+
* long but healthy stream is never cut off. `0`/omitted disables the deadline.
|
|
51
|
+
*/
|
|
52
|
+
firstChunkTimeoutMs?: number;
|
|
17
53
|
}
|
|
18
54
|
/**
|
|
19
55
|
* Streaming analog of {@link withTimeoutAndRetry}. Re-runs an async-generator
|
|
20
|
-
* factory as long as it fails *before yielding its first chunk* —
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* consumer has already received.
|
|
56
|
+
* factory as long as it fails *before yielding its first chunk* — an empty
|
|
57
|
+
* completion that throws "No response", an error while establishing the stream,
|
|
58
|
+
* or (with `firstChunkTimeoutMs`) a stall before the first token. Once any chunk
|
|
59
|
+
* has been yielded the stream is committed and further errors propagate, so a
|
|
60
|
+
* retry can never double-emit deltas the consumer has already received.
|
|
25
61
|
*
|
|
26
62
|
* This mirrors the non-streaming path, where the provider throws on an empty
|
|
27
63
|
* completion inside `withTimeoutAndRetry` and is retried on the same model
|
|
28
64
|
* before the caller falls through to backup models.
|
|
29
65
|
*/
|
|
30
|
-
export declare function withStreamRetry<T>(factory: () => AsyncGenerator<T>, options?: StreamRetryOptions): AsyncGenerator<T>;
|
|
66
|
+
export declare function withStreamRetry<T>(factory: (signal: AbortSignal) => AsyncGenerator<T>, options?: StreamRetryOptions): AsyncGenerator<T>;
|
|
31
67
|
//# sourceMappingURL=retry.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CACzC;AAED,wBAAsB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA8BnE;AAED,eAAO,MAAM,mBAAmB,GAAU,CAAC,EACzC,WAAW,MAAM,OAAO,CAAC,CAAC,CAAC,
|
|
1
|
+
{"version":3,"file":"retry.d.ts","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACpD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC;CACzC;AAED,uEAAuE;AACvE,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAOD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,CAAC,EAAE;IACzC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,GAAG,WAAW,CAKd;AAED,wBAAsB,KAAK,CAAC,CAAC,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CA8BnE;AAMD;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,OAAO,EAAE,KAAK,CAAC,WAAW,GAAG,SAAS,CAAC,GACzC,WAAW,GAAG,SAAS,CAgBzB;AAED,eAAO,MAAM,mBAAmB,GAAU,CAAC,EACzC,WAAW,CAAC,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAC9C,YAAW,MAAc,EACzB,aAAY,MAAU,EACtB,gBAAe,MAAuB,KACrC,OAAO,CAAC,CAAC,CAiDX,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,gCAAgC;IAChC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAuB,eAAe,CAAC,CAAC,EACtC,OAAO,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,cAAc,CAAC,CAAC,CAAC,EACnD,OAAO,GAAE,kBAAuB,GAC/B,cAAc,CAAC,CAAC,CAAC,CAwDnB"}
|
package/dist/utils/retry.js
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
* Retry utility with exponential backoff
|
|
3
3
|
*/
|
|
4
4
|
import { logger } from './logger';
|
|
5
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
6
|
+
timeout: 60000,
|
|
7
|
+
retries: 3,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Normalize a provider's optional retry config against the defaults. `timeout`
|
|
11
|
+
* uses `||` (a 0ms timeout is degenerate — it aborts every call immediately — so
|
|
12
|
+
* fall back to the default), while `retries` uses `??` so an explicit
|
|
13
|
+
* `retries: 0` (disable retries) is honored rather than treated as unset. Single
|
|
14
|
+
* definition so this distinction can't drift between providers.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveRetryConfig(input) {
|
|
17
|
+
return {
|
|
18
|
+
timeout: input?.timeout || DEFAULT_RETRY_CONFIG.timeout,
|
|
19
|
+
retries: input?.retries ?? DEFAULT_RETRY_CONFIG.retries,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
5
22
|
export async function retry(options) {
|
|
6
23
|
const { operation, maxRetries, delay, onRetry, onFailure } = options;
|
|
7
24
|
let lastError;
|
|
@@ -28,6 +45,38 @@ export async function retry(options) {
|
|
|
28
45
|
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
29
46
|
throw lastError ?? new Error("Operation failed");
|
|
30
47
|
}
|
|
48
|
+
/** Capped exponential backoff (1s, 2s, 4s, … max 5s) shared by the retry helpers. */
|
|
49
|
+
const defaultBackoff = (attempt) => Math.min(1000 * Math.pow(2, attempt), 5000);
|
|
50
|
+
/**
|
|
51
|
+
* Combine abort signals into one that aborts as soon as any input aborts.
|
|
52
|
+
* `undefined` inputs are skipped, so a caller can pass an optional caller-signal
|
|
53
|
+
* alongside a required per-attempt one. Returns the sole signal when only one is
|
|
54
|
+
* present (no wrapper), or `undefined` when none are.
|
|
55
|
+
*
|
|
56
|
+
* Prefers the platform `AbortSignal.any`, which cleans up its listeners via weak
|
|
57
|
+
* refs — so merging onto a long-lived caller signal that's reused across many
|
|
58
|
+
* calls can't accumulate listeners. Falls back to a manual controller on older
|
|
59
|
+
* runtimes (< Node 20.3); there the listeners live until a source aborts or is
|
|
60
|
+
* garbage-collected, which is fine for the usual per-request signal.
|
|
61
|
+
*/
|
|
62
|
+
export function combineAbortSignals(...signals) {
|
|
63
|
+
const present = signals.filter((s) => s != null);
|
|
64
|
+
if (present.length <= 1)
|
|
65
|
+
return present[0];
|
|
66
|
+
if (typeof AbortSignal.any === "function")
|
|
67
|
+
return AbortSignal.any(present);
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
for (const signal of present) {
|
|
70
|
+
if (signal.aborted) {
|
|
71
|
+
controller.abort(signal.reason);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
signal.addEventListener("abort", () => controller.abort(signal.reason), {
|
|
75
|
+
once: true,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return controller.signal;
|
|
79
|
+
}
|
|
31
80
|
export const withTimeoutAndRetry = async (operation, timeoutMs = 60000, maxRetries = 3, operationName = "AI operation") => {
|
|
32
81
|
const createTimeoutOperation = () => async () => {
|
|
33
82
|
const controller = new AbortController();
|
|
@@ -35,8 +84,11 @@ export const withTimeoutAndRetry = async (operation, timeoutMs = 60000, maxRetri
|
|
|
35
84
|
controller.abort();
|
|
36
85
|
}, timeoutMs);
|
|
37
86
|
try {
|
|
87
|
+
// Hand the operation the timeout signal so the in-flight upstream call is
|
|
88
|
+
// actually cancelled when the deadline fires — otherwise the abandoned
|
|
89
|
+
// attempt keeps running while the retry stacks a second concurrent call.
|
|
38
90
|
const result = await Promise.race([
|
|
39
|
-
operation(),
|
|
91
|
+
operation(controller.signal),
|
|
40
92
|
new Promise((_, reject) => {
|
|
41
93
|
controller.signal.addEventListener("abort", () => {
|
|
42
94
|
reject(new Error(`Operation timed out after ${timeoutMs}ms`));
|
|
@@ -54,11 +106,11 @@ export const withTimeoutAndRetry = async (operation, timeoutMs = 60000, maxRetri
|
|
|
54
106
|
return retry({
|
|
55
107
|
operation: createTimeoutOperation(),
|
|
56
108
|
maxRetries,
|
|
57
|
-
delay:
|
|
109
|
+
delay: defaultBackoff,
|
|
58
110
|
onRetry: (attempt, error) => {
|
|
59
111
|
const message = error instanceof Error ? error.message : String(error);
|
|
60
112
|
logger.error(`[${operationName}] Failed attempt ${attempt + 1}:`, message);
|
|
61
|
-
const delay =
|
|
113
|
+
const delay = defaultBackoff(attempt);
|
|
62
114
|
logger.debug(`[${operationName}] Retrying in ${delay}ms...`);
|
|
63
115
|
logger.debug(`[${operationName}] Attempt ${attempt + 2}/${maxRetries + 1}`);
|
|
64
116
|
},
|
|
@@ -68,28 +120,49 @@ export const withTimeoutAndRetry = async (operation, timeoutMs = 60000, maxRetri
|
|
|
68
120
|
},
|
|
69
121
|
});
|
|
70
122
|
};
|
|
123
|
+
/** Reject if `next` hasn't settled within `timeoutMs`; always clears its timer. */
|
|
124
|
+
function raceFirstChunk(next, timeoutMs, operationName) {
|
|
125
|
+
let timer;
|
|
126
|
+
const timeout = new Promise((_, reject) => {
|
|
127
|
+
timer = setTimeout(() => reject(new Error(`[${operationName}] Stream timed out: no first chunk within ${timeoutMs}ms`)), timeoutMs);
|
|
128
|
+
});
|
|
129
|
+
return Promise.race([next, timeout]).finally(() => clearTimeout(timer));
|
|
130
|
+
}
|
|
71
131
|
/**
|
|
72
132
|
* Streaming analog of {@link withTimeoutAndRetry}. Re-runs an async-generator
|
|
73
|
-
* factory as long as it fails *before yielding its first chunk* —
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* consumer has already received.
|
|
133
|
+
* factory as long as it fails *before yielding its first chunk* — an empty
|
|
134
|
+
* completion that throws "No response", an error while establishing the stream,
|
|
135
|
+
* or (with `firstChunkTimeoutMs`) a stall before the first token. Once any chunk
|
|
136
|
+
* has been yielded the stream is committed and further errors propagate, so a
|
|
137
|
+
* retry can never double-emit deltas the consumer has already received.
|
|
78
138
|
*
|
|
79
139
|
* This mirrors the non-streaming path, where the provider throws on an empty
|
|
80
140
|
* completion inside `withTimeoutAndRetry` and is retried on the same model
|
|
81
141
|
* before the caller falls through to backup models.
|
|
82
142
|
*/
|
|
83
143
|
export async function* withStreamRetry(factory, options = {}) {
|
|
84
|
-
const { maxRetries = 3, delay =
|
|
144
|
+
const { maxRetries = 3, delay = defaultBackoff, operationName = "AI stream", firstChunkTimeoutMs, } = options;
|
|
85
145
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
146
|
+
// One controller per attempt: aborting it on abandon cancels the upstream
|
|
147
|
+
// SDK call so a retry can't stack a second concurrent stream.
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const iterator = factory(controller.signal);
|
|
86
150
|
let yielded = false;
|
|
151
|
+
let completed = false;
|
|
87
152
|
try {
|
|
88
|
-
|
|
153
|
+
// Drive the iterator by hand so the first chunk can be raced against a
|
|
154
|
+
// deadline; for-await would give no hook for that.
|
|
155
|
+
for (;;) {
|
|
156
|
+
const result = !yielded && firstChunkTimeoutMs
|
|
157
|
+
? await raceFirstChunk(iterator.next(), firstChunkTimeoutMs, operationName)
|
|
158
|
+
: await iterator.next();
|
|
159
|
+
if (result.done) {
|
|
160
|
+
completed = true;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
89
163
|
yielded = true;
|
|
90
|
-
yield
|
|
164
|
+
yield result.value;
|
|
91
165
|
}
|
|
92
|
-
return;
|
|
93
166
|
}
|
|
94
167
|
catch (error) {
|
|
95
168
|
// Can't retry once deltas are out, and don't retry past the budget.
|
|
@@ -102,6 +175,19 @@ export async function* withStreamRetry(factory, options = {}) {
|
|
|
102
175
|
logger.debug(`[${operationName}] Retrying in ${delayMs}ms... (attempt ${attempt + 2}/${maxRetries + 1})`);
|
|
103
176
|
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
104
177
|
}
|
|
178
|
+
finally {
|
|
179
|
+
// Abandoning an attempt (first-chunk timeout, pre-yield error before a
|
|
180
|
+
// retry, or a consumer that breaks early): abort the upstream SDK call
|
|
181
|
+
// first so it's actually torn down — not left running while we retry —
|
|
182
|
+
// then dispose the iterator. Aborting also unblocks a stalled
|
|
183
|
+
// iterator.next() so return() can run the generator's cleanup. Both are
|
|
184
|
+
// fire-and-forget: a wedged generator's return() may never settle, so we
|
|
185
|
+
// must not await it.
|
|
186
|
+
if (!completed) {
|
|
187
|
+
controller.abort();
|
|
188
|
+
void iterator.return?.(undefined)?.catch(() => undefined);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
105
191
|
}
|
|
106
192
|
}
|
|
107
193
|
//# sourceMappingURL=retry.js.map
|
package/dist/utils/retry.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAC,MAAM,EAAC,MAAM,UAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAC,MAAM,EAAC,MAAM,UAAU,CAAA;AAgB/B,MAAM,oBAAoB,GAAgB;IACxC,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,CAAC;CACX,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAGlC;IACC,OAAO;QACL,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,oBAAoB,CAAC,OAAO;QACvD,OAAO,EAAE,KAAK,EAAE,OAAO,IAAI,oBAAoB,CAAC,OAAO;KACxD,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAI,OAAwB;IACrD,MAAM,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC;IAErE,IAAI,SAAkB,CAAC;IAEvB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,OAAO,MAAM,SAAS,EAAE,CAAC;QAC3B,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,SAAS,GAAG,KAAK,CAAC;YAElB,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBAC3B,MAAM,aAAa,GAAG,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBAC9D,IAAI,aAAa,EAAE,CAAC;oBAClB,MAAM,SAAS,CAAC;gBAClB,CAAC;gBACD,MAAM;YACR,CAAC;YAED,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;YAC9B,CAAC;YAED,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YAC/B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC/D,CAAC;IACH,CAAC;IAED,+DAA+D;IAC/D,MAAM,SAAS,IAAI,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;AACnD,CAAC;AAED,qFAAqF;AACrF,MAAM,cAAc,GAAG,CAAC,OAAe,EAAU,EAAE,CACjD,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,mBAAmB,CACjC,GAAG,OAAuC;IAE1C,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAoB,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;IACnE,IAAI,OAAO,CAAC,MAAM,IAAI,CAAC;QAAE,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3C,IAAI,OAAO,WAAW,CAAC,GAAG,KAAK,UAAU;QAAE,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAE3E,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YACnB,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAChC,MAAM;QACR,CAAC;QACD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE;YACtE,IAAI,EAAE,IAAI;SACX,CAAC,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC,MAAM,CAAC;AAC3B,CAAC;AAED,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EACtC,SAA8C,EAC9C,YAAoB,KAAK,EACzB,aAAqB,CAAC,EACtB,gBAAwB,cAAc,EAC1B,EAAE;IACd,MAAM,sBAAsB,GAAG,GAAG,EAAE,CAAC,KAAK,IAAgB,EAAE;QAC1D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YAChC,UAAU,CAAC,KAAK,EAAE,CAAC;QACrB,CAAC,EAAE,SAAS,CAAC,CAAC;QAEd,IAAI,CAAC;YACH,0EAA0E;YAC1E,uEAAuE;YACvE,yEAAyE;YACzE,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;gBAChC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC;gBAC5B,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;oBAC/B,UAAU,CAAC,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE;wBAC/C,MAAM,CAAC,IAAI,KAAK,CAAC,6BAA6B,SAAS,IAAI,CAAC,CAAC,CAAC;oBAChE,CAAC,CAAC,CAAC;gBACL,CAAC,CAAC;aACH,CAAC,CAAC;YAEH,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,KAAK,CAAI;QACd,SAAS,EAAE,sBAAsB,EAAE;QACnC,UAAU;QACV,KAAK,EAAE,cAAc;QACrB,OAAO,EAAE,CAAC,OAAe,EAAE,KAAc,EAAE,EAAE;YAC3C,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,CAAC,KAAK,CACV,IAAI,aAAa,oBAAoB,OAAO,GAAG,CAAC,GAAG,EACnD,OAAO,CACR,CAAC;YACF,MAAM,KAAK,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,CAAC,KAAK,CAAC,IAAI,aAAa,iBAAiB,KAAK,OAAO,CAAC,CAAC;YAC7D,MAAM,CAAC,KAAK,CACV,IAAI,aAAa,aAAa,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,EAAE,CAC9D,CAAC;QACJ,CAAC;QACD,SAAS,EAAE,CAAC,MAAe,EAAE,EAAE;YAC7B,MAAM,CAAC,KAAK,CAAC,IAAI,aAAa,SAAS,UAAU,GAAG,CAAC,kBAAkB,CAAC,CAAC;YACzE,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC,CAAC;AACL,CAAC,CAAC;AAkBF,mFAAmF;AACnF,SAAS,cAAc,CACrB,IAAgC,EAChC,SAAiB,EACjB,aAAqB;IAErB,IAAI,KAAgD,CAAC;IACrD,MAAM,OAAO,GAAG,IAAI,OAAO,CAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE;QAC/C,KAAK,GAAG,UAAU,CAChB,GAAG,EAAE,CACH,MAAM,CACJ,IAAI,KAAK,CACP,IAAI,aAAa,6CAA6C,SAAS,IAAI,CAC5E,CACF,EACH,SAAS,CACV,CAAC;IACJ,CAAC,CAAC,CAAC;IACH,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1E,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,SAAS,CAAC,CAAC,eAAe,CACpC,OAAmD,EACnD,UAA8B,EAAE;IAEhC,MAAM,EACJ,UAAU,GAAG,CAAC,EACd,KAAK,GAAG,cAAc,EACtB,aAAa,GAAG,WAAW,EAC3B,mBAAmB,GACpB,GAAG,OAAO,CAAC;IAEZ,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,UAAU,EAAE,OAAO,EAAE,EAAE,CAAC;QACvD,0EAA0E;QAC1E,8DAA8D;QAC9D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;QAC5C,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,SAAS,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC;YACH,uEAAuE;YACvE,mDAAmD;YACnD,SAAS,CAAC;gBACR,MAAM,MAAM,GACV,CAAC,OAAO,IAAI,mBAAmB;oBAC7B,CAAC,CAAC,MAAM,cAAc,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,mBAAmB,EAAE,aAAa,CAAC;oBAC3E,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;gBAC5B,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;oBAChB,SAAS,GAAG,IAAI,CAAC;oBACjB,OAAO;gBACT,CAAC;gBACD,OAAO,GAAG,IAAI,CAAC;gBACf,MAAM,MAAM,CAAC,KAAK,CAAC;YACrB,CAAC;QACH,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,oEAAoE;YACpE,IAAI,OAAO,IAAI,OAAO,KAAK,UAAU,EAAE,CAAC;gBACtC,MAAM,KAAK,CAAC;YACd,CAAC;YACD,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,MAAM,CAAC,KAAK,CAAC,IAAI,aAAa,oBAAoB,OAAO,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAC3E,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;YAC/B,MAAM,CAAC,KAAK,CACV,IAAI,aAAa,iBAAiB,OAAO,kBAAkB,OAAO,GAAG,CAAC,IAAI,UAAU,GAAG,CAAC,GAAG,CAC5F,CAAC;YACF,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC/D,CAAC;gBAAS,CAAC;YACT,uEAAuE;YACvE,uEAAuE;YACvE,uEAAuE;YACvE,8DAA8D;YAC9D,wEAAwE;YACxE,yEAAyE;YACzE,qBAAqB;YACrB,IAAI,CAAC,SAAS,EAAE,CAAC;gBACf,UAAU,CAAC,KAAK,EAAE,CAAC;gBACnB,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAC5D,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -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 {
|