@falai/agent 2.4.2 → 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.
Files changed (142) hide show
  1. package/dist/cjs/core/PersistenceManager.d.ts +13 -0
  2. package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
  3. package/dist/cjs/core/PersistenceManager.js +29 -6
  4. package/dist/cjs/core/PersistenceManager.js.map +1 -1
  5. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  6. package/dist/cjs/core/ResponseModal.js +15 -3
  7. package/dist/cjs/core/ResponseModal.js.map +1 -1
  8. package/dist/cjs/core/SessionManager.d.ts.map +1 -1
  9. package/dist/cjs/core/SessionManager.js +7 -0
  10. package/dist/cjs/core/SessionManager.js.map +1 -1
  11. package/dist/cjs/core/StreamingToolExecutor.d.ts.map +1 -1
  12. package/dist/cjs/core/StreamingToolExecutor.js +11 -0
  13. package/dist/cjs/core/StreamingToolExecutor.js.map +1 -1
  14. package/dist/cjs/core/ToolLoopExecutor.d.ts +27 -2
  15. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
  16. package/dist/cjs/core/ToolLoopExecutor.js +159 -74
  17. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
  18. package/dist/cjs/core/ToolManager.d.ts.map +1 -1
  19. package/dist/cjs/core/ToolManager.js +8 -31
  20. package/dist/cjs/core/ToolManager.js.map +1 -1
  21. package/dist/cjs/core/toolGates.d.ts +24 -0
  22. package/dist/cjs/core/toolGates.d.ts.map +1 -0
  23. package/dist/cjs/core/toolGates.js +52 -0
  24. package/dist/cjs/core/toolGates.js.map +1 -0
  25. package/dist/cjs/index.d.ts +3 -0
  26. package/dist/cjs/index.d.ts.map +1 -1
  27. package/dist/cjs/index.js +4 -2
  28. package/dist/cjs/index.js.map +1 -1
  29. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  30. package/dist/cjs/providers/AnthropicProvider.js +18 -14
  31. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  32. package/dist/cjs/providers/DeepSeekProvider.d.ts +1 -12
  33. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
  34. package/dist/cjs/providers/DeepSeekProvider.js +3 -20
  35. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
  36. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  37. package/dist/cjs/providers/GeminiProvider.js +19 -15
  38. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  39. package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
  40. package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
  41. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +93 -0
  42. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -0
  43. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +23 -5
  44. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  45. package/dist/cjs/providers/OpenAICompatibleProvider.js +47 -23
  46. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
  47. package/dist/cjs/providers/index.d.ts +3 -1
  48. package/dist/cjs/providers/index.d.ts.map +1 -1
  49. package/dist/cjs/providers/index.js +3 -1
  50. package/dist/cjs/providers/index.js.map +1 -1
  51. package/dist/cjs/utils/completion.d.ts +20 -0
  52. package/dist/cjs/utils/completion.d.ts.map +1 -0
  53. package/dist/cjs/utils/completion.js +33 -0
  54. package/dist/cjs/utils/completion.js.map +1 -0
  55. package/dist/cjs/utils/index.d.ts +3 -2
  56. package/dist/cjs/utils/index.d.ts.map +1 -1
  57. package/dist/cjs/utils/index.js +8 -1
  58. package/dist/cjs/utils/index.js.map +1 -1
  59. package/dist/cjs/utils/retry.d.ts +58 -1
  60. package/dist/cjs/utils/retry.d.ts.map +1 -1
  61. package/dist/cjs/utils/retry.js +128 -3
  62. package/dist/cjs/utils/retry.js.map +1 -1
  63. package/dist/core/PersistenceManager.d.ts +13 -0
  64. package/dist/core/PersistenceManager.d.ts.map +1 -1
  65. package/dist/core/PersistenceManager.js +29 -6
  66. package/dist/core/PersistenceManager.js.map +1 -1
  67. package/dist/core/ResponseModal.d.ts.map +1 -1
  68. package/dist/core/ResponseModal.js +15 -3
  69. package/dist/core/ResponseModal.js.map +1 -1
  70. package/dist/core/SessionManager.d.ts.map +1 -1
  71. package/dist/core/SessionManager.js +7 -0
  72. package/dist/core/SessionManager.js.map +1 -1
  73. package/dist/core/StreamingToolExecutor.d.ts.map +1 -1
  74. package/dist/core/StreamingToolExecutor.js +11 -0
  75. package/dist/core/StreamingToolExecutor.js.map +1 -1
  76. package/dist/core/ToolLoopExecutor.d.ts +27 -2
  77. package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
  78. package/dist/core/ToolLoopExecutor.js +160 -75
  79. package/dist/core/ToolLoopExecutor.js.map +1 -1
  80. package/dist/core/ToolManager.d.ts.map +1 -1
  81. package/dist/core/ToolManager.js +8 -31
  82. package/dist/core/ToolManager.js.map +1 -1
  83. package/dist/core/toolGates.d.ts +24 -0
  84. package/dist/core/toolGates.d.ts.map +1 -0
  85. package/dist/core/toolGates.js +49 -0
  86. package/dist/core/toolGates.js.map +1 -0
  87. package/dist/index.d.ts +3 -0
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +1 -0
  90. package/dist/index.js.map +1 -1
  91. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  92. package/dist/providers/AnthropicProvider.js +19 -15
  93. package/dist/providers/AnthropicProvider.js.map +1 -1
  94. package/dist/providers/DeepSeekProvider.d.ts +1 -12
  95. package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
  96. package/dist/providers/DeepSeekProvider.js +3 -20
  97. package/dist/providers/DeepSeekProvider.js.map +1 -1
  98. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  99. package/dist/providers/GeminiProvider.js +20 -16
  100. package/dist/providers/GeminiProvider.js.map +1 -1
  101. package/dist/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
  102. package/dist/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
  103. package/dist/providers/GenericOpenAICompatibleProvider.js +87 -0
  104. package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -0
  105. package/dist/providers/OpenAICompatibleProvider.d.ts +23 -5
  106. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  107. package/dist/providers/OpenAICompatibleProvider.js +48 -24
  108. package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
  109. package/dist/providers/index.d.ts +3 -1
  110. package/dist/providers/index.d.ts.map +1 -1
  111. package/dist/providers/index.js +1 -0
  112. package/dist/providers/index.js.map +1 -1
  113. package/dist/utils/completion.d.ts +20 -0
  114. package/dist/utils/completion.d.ts.map +1 -0
  115. package/dist/utils/completion.js +29 -0
  116. package/dist/utils/completion.js.map +1 -0
  117. package/dist/utils/index.d.ts +3 -2
  118. package/dist/utils/index.d.ts.map +1 -1
  119. package/dist/utils/index.js +3 -1
  120. package/dist/utils/index.js.map +1 -1
  121. package/dist/utils/retry.d.ts +58 -1
  122. package/dist/utils/retry.d.ts.map +1 -1
  123. package/dist/utils/retry.js +125 -3
  124. package/dist/utils/retry.js.map +1 -1
  125. package/package.json +1 -1
  126. package/src/core/PersistenceManager.ts +30 -6
  127. package/src/core/ResponseModal.ts +16 -3
  128. package/src/core/SessionManager.ts +7 -0
  129. package/src/core/StreamingToolExecutor.ts +12 -0
  130. package/src/core/ToolLoopExecutor.ts +210 -79
  131. package/src/core/ToolManager.ts +8 -32
  132. package/src/core/toolGates.ts +72 -0
  133. package/src/index.ts +3 -0
  134. package/src/providers/AnthropicProvider.ts +26 -20
  135. package/src/providers/DeepSeekProvider.ts +3 -35
  136. package/src/providers/GeminiProvider.ts +29 -17
  137. package/src/providers/GenericOpenAICompatibleProvider.ts +138 -0
  138. package/src/providers/OpenAICompatibleProvider.ts +92 -44
  139. package/src/providers/index.ts +4 -0
  140. package/src/utils/completion.ts +38 -0
  141. package/src/utils/index.ts +5 -2
  142. package/src/utils/retry.ts +181 -4
@@ -11,6 +11,34 @@ export interface RetryOptions<T> {
11
11
  onFailure?: (error: unknown) => boolean;
12
12
  }
13
13
 
14
+ /** Provider timeout (ms) + retry count, after defaults are applied. */
15
+ export interface RetryConfig {
16
+ timeout: number;
17
+ retries: number;
18
+ }
19
+
20
+ const DEFAULT_RETRY_CONFIG: RetryConfig = {
21
+ timeout: 60000,
22
+ retries: 3,
23
+ };
24
+
25
+ /**
26
+ * Normalize a provider's optional retry config against the defaults. `timeout`
27
+ * uses `||` (a 0ms timeout is degenerate — it aborts every call immediately — so
28
+ * fall back to the default), while `retries` uses `??` so an explicit
29
+ * `retries: 0` (disable retries) is honored rather than treated as unset. Single
30
+ * definition so this distinction can't drift between providers.
31
+ */
32
+ export function resolveRetryConfig(input?: {
33
+ timeout?: number;
34
+ retries?: number;
35
+ }): RetryConfig {
36
+ return {
37
+ timeout: input?.timeout || DEFAULT_RETRY_CONFIG.timeout,
38
+ retries: input?.retries ?? DEFAULT_RETRY_CONFIG.retries,
39
+ };
40
+ }
41
+
14
42
  export async function retry<T>(options: RetryOptions<T>): Promise<T> {
15
43
  const { operation, maxRetries, delay, onRetry, onFailure } = options;
16
44
 
@@ -43,8 +71,44 @@ export async function retry<T>(options: RetryOptions<T>): Promise<T> {
43
71
  throw lastError ?? new Error("Operation failed");
44
72
  }
45
73
 
74
+ /** Capped exponential backoff (1s, 2s, 4s, … max 5s) shared by the retry helpers. */
75
+ const defaultBackoff = (attempt: number): number =>
76
+ Math.min(1000 * Math.pow(2, attempt), 5000);
77
+
78
+ /**
79
+ * Combine abort signals into one that aborts as soon as any input aborts.
80
+ * `undefined` inputs are skipped, so a caller can pass an optional caller-signal
81
+ * alongside a required per-attempt one. Returns the sole signal when only one is
82
+ * present (no wrapper), or `undefined` when none are.
83
+ *
84
+ * Prefers the platform `AbortSignal.any`, which cleans up its listeners via weak
85
+ * refs — so merging onto a long-lived caller signal that's reused across many
86
+ * calls can't accumulate listeners. Falls back to a manual controller on older
87
+ * runtimes (< Node 20.3); there the listeners live until a source aborts or is
88
+ * garbage-collected, which is fine for the usual per-request signal.
89
+ */
90
+ export function combineAbortSignals(
91
+ ...signals: Array<AbortSignal | undefined>
92
+ ): AbortSignal | undefined {
93
+ const present = signals.filter((s): s is AbortSignal => s != null);
94
+ if (present.length <= 1) return present[0];
95
+ if (typeof AbortSignal.any === "function") return AbortSignal.any(present);
96
+
97
+ const controller = new AbortController();
98
+ for (const signal of present) {
99
+ if (signal.aborted) {
100
+ controller.abort(signal.reason);
101
+ break;
102
+ }
103
+ signal.addEventListener("abort", () => controller.abort(signal.reason), {
104
+ once: true,
105
+ });
106
+ }
107
+ return controller.signal;
108
+ }
109
+
46
110
  export const withTimeoutAndRetry = async <T>(
47
- operation: () => Promise<T>,
111
+ operation: (signal: AbortSignal) => Promise<T>,
48
112
  timeoutMs: number = 60000,
49
113
  maxRetries: number = 3,
50
114
  operationName: string = "AI operation"
@@ -56,8 +120,11 @@ export const withTimeoutAndRetry = async <T>(
56
120
  }, timeoutMs);
57
121
 
58
122
  try {
123
+ // Hand the operation the timeout signal so the in-flight upstream call is
124
+ // actually cancelled when the deadline fires — otherwise the abandoned
125
+ // attempt keeps running while the retry stacks a second concurrent call.
59
126
  const result = await Promise.race([
60
- operation(),
127
+ operation(controller.signal),
61
128
  new Promise<never>((_, reject) => {
62
129
  controller.signal.addEventListener("abort", () => {
63
130
  reject(new Error(`Operation timed out after ${timeoutMs}ms`));
@@ -76,14 +143,14 @@ export const withTimeoutAndRetry = async <T>(
76
143
  return retry<T>({
77
144
  operation: createTimeoutOperation(),
78
145
  maxRetries,
79
- delay: (attempt: number) => Math.min(1000 * Math.pow(2, attempt), 5000),
146
+ delay: defaultBackoff,
80
147
  onRetry: (attempt: number, error: unknown) => {
81
148
  const message = error instanceof Error ? error.message : String(error);
82
149
  logger.error(
83
150
  `[${operationName}] Failed attempt ${attempt + 1}:`,
84
151
  message
85
152
  );
86
- const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
153
+ const delay = defaultBackoff(attempt);
87
154
  logger.debug(`[${operationName}] Retrying in ${delay}ms...`);
88
155
  logger.debug(
89
156
  `[${operationName}] Attempt ${attempt + 2}/${maxRetries + 1}`
@@ -95,3 +162,113 @@ export const withTimeoutAndRetry = async <T>(
95
162
  },
96
163
  });
97
164
  };
165
+
166
+ export interface StreamRetryOptions {
167
+ /** Maximum number of retries after the first attempt. Defaults to 3. */
168
+ maxRetries?: number;
169
+ /** Backoff before the next attempt, in ms. Defaults to capped exponential. */
170
+ delay?: (attempt: number) => number;
171
+ /** Label used in retry logs. */
172
+ operationName?: string;
173
+ /**
174
+ * Max ms to wait for the *first* chunk (time-to-first-token) before treating
175
+ * the attempt as failed. Guards a provider that opens a stream and then
176
+ * stalls. Only the first chunk is bounded — later chunks are unbounded so a
177
+ * long but healthy stream is never cut off. `0`/omitted disables the deadline.
178
+ */
179
+ firstChunkTimeoutMs?: number;
180
+ }
181
+
182
+ /** Reject if `next` hasn't settled within `timeoutMs`; always clears its timer. */
183
+ function raceFirstChunk<T>(
184
+ next: Promise<IteratorResult<T>>,
185
+ timeoutMs: number,
186
+ operationName: string
187
+ ): Promise<IteratorResult<T>> {
188
+ let timer: ReturnType<typeof setTimeout> | undefined;
189
+ const timeout = new Promise<never>((_, reject) => {
190
+ timer = setTimeout(
191
+ () =>
192
+ reject(
193
+ new Error(
194
+ `[${operationName}] Stream timed out: no first chunk within ${timeoutMs}ms`
195
+ )
196
+ ),
197
+ timeoutMs
198
+ );
199
+ });
200
+ return Promise.race([next, timeout]).finally(() => clearTimeout(timer));
201
+ }
202
+
203
+ /**
204
+ * Streaming analog of {@link withTimeoutAndRetry}. Re-runs an async-generator
205
+ * factory as long as it fails *before yielding its first chunk* — an empty
206
+ * completion that throws "No response", an error while establishing the stream,
207
+ * or (with `firstChunkTimeoutMs`) a stall before the first token. Once any chunk
208
+ * has been yielded the stream is committed and further errors propagate, so a
209
+ * retry can never double-emit deltas the consumer has already received.
210
+ *
211
+ * This mirrors the non-streaming path, where the provider throws on an empty
212
+ * completion inside `withTimeoutAndRetry` and is retried on the same model
213
+ * before the caller falls through to backup models.
214
+ */
215
+ export async function* withStreamRetry<T>(
216
+ factory: (signal: AbortSignal) => AsyncGenerator<T>,
217
+ options: StreamRetryOptions = {}
218
+ ): AsyncGenerator<T> {
219
+ const {
220
+ maxRetries = 3,
221
+ delay = defaultBackoff,
222
+ operationName = "AI stream",
223
+ firstChunkTimeoutMs,
224
+ } = options;
225
+
226
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
227
+ // One controller per attempt: aborting it on abandon cancels the upstream
228
+ // SDK call so a retry can't stack a second concurrent stream.
229
+ const controller = new AbortController();
230
+ const iterator = factory(controller.signal);
231
+ let yielded = false;
232
+ let completed = false;
233
+ try {
234
+ // Drive the iterator by hand so the first chunk can be raced against a
235
+ // deadline; for-await would give no hook for that.
236
+ for (;;) {
237
+ const result =
238
+ !yielded && firstChunkTimeoutMs
239
+ ? await raceFirstChunk(iterator.next(), firstChunkTimeoutMs, operationName)
240
+ : await iterator.next();
241
+ if (result.done) {
242
+ completed = true;
243
+ return;
244
+ }
245
+ yielded = true;
246
+ yield result.value;
247
+ }
248
+ } catch (error: unknown) {
249
+ // Can't retry once deltas are out, and don't retry past the budget.
250
+ if (yielded || attempt === maxRetries) {
251
+ throw error;
252
+ }
253
+ const message = error instanceof Error ? error.message : String(error);
254
+ logger.error(`[${operationName}] Failed attempt ${attempt + 1}:`, message);
255
+ const delayMs = delay(attempt);
256
+ logger.debug(
257
+ `[${operationName}] Retrying in ${delayMs}ms... (attempt ${attempt + 2}/${maxRetries + 1})`
258
+ );
259
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
260
+ } finally {
261
+ // Abandoning an attempt (first-chunk timeout, pre-yield error before a
262
+ // retry, or a consumer that breaks early): abort the upstream SDK call
263
+ // first so it's actually torn down — not left running while we retry —
264
+ // then dispose the iterator. Aborting also unblocks a stalled
265
+ // iterator.next() so return() can run the generator's cleanup. Both are
266
+ // fire-and-forget: a wedged generator's return() may never settle, so we
267
+ // must not await it.
268
+ if (!completed) {
269
+ controller.abort();
270
+ void iterator.return?.(undefined)?.catch(() => undefined);
271
+ }
272
+ }
273
+ }
274
+ }