@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.
Files changed (96) hide show
  1. package/dist/cjs/core/ToolLoopExecutor.d.ts +11 -0
  2. package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
  3. package/dist/cjs/core/ToolLoopExecutor.js +75 -51
  4. package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
  5. package/dist/cjs/index.d.ts +3 -0
  6. package/dist/cjs/index.d.ts.map +1 -1
  7. package/dist/cjs/index.js +4 -2
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  10. package/dist/cjs/providers/AnthropicProvider.js +18 -35
  11. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  12. package/dist/cjs/providers/DeepSeekProvider.d.ts +1 -12
  13. package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
  14. package/dist/cjs/providers/DeepSeekProvider.js +3 -20
  15. package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
  16. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  17. package/dist/cjs/providers/GeminiProvider.js +19 -36
  18. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  19. package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
  20. package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
  21. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +93 -0
  22. package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -0
  23. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +23 -5
  24. package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  25. package/dist/cjs/providers/OpenAICompatibleProvider.js +46 -44
  26. package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
  27. package/dist/cjs/providers/index.d.ts +3 -1
  28. package/dist/cjs/providers/index.d.ts.map +1 -1
  29. package/dist/cjs/providers/index.js +3 -1
  30. package/dist/cjs/providers/index.js.map +1 -1
  31. package/dist/cjs/utils/completion.d.ts +20 -0
  32. package/dist/cjs/utils/completion.d.ts.map +1 -0
  33. package/dist/cjs/utils/completion.js +33 -0
  34. package/dist/cjs/utils/completion.js.map +1 -0
  35. package/dist/cjs/utils/index.d.ts +3 -2
  36. package/dist/cjs/utils/index.d.ts.map +1 -1
  37. package/dist/cjs/utils/index.js +7 -1
  38. package/dist/cjs/utils/index.js.map +1 -1
  39. package/dist/cjs/utils/retry.d.ts +43 -7
  40. package/dist/cjs/utils/retry.d.ts.map +1 -1
  41. package/dist/cjs/utils/retry.js +100 -12
  42. package/dist/cjs/utils/retry.js.map +1 -1
  43. package/dist/core/ToolLoopExecutor.d.ts +11 -0
  44. package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
  45. package/dist/core/ToolLoopExecutor.js +76 -52
  46. package/dist/core/ToolLoopExecutor.js.map +1 -1
  47. package/dist/index.d.ts +3 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +1 -0
  50. package/dist/index.js.map +1 -1
  51. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  52. package/dist/providers/AnthropicProvider.js +19 -36
  53. package/dist/providers/AnthropicProvider.js.map +1 -1
  54. package/dist/providers/DeepSeekProvider.d.ts +1 -12
  55. package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
  56. package/dist/providers/DeepSeekProvider.js +3 -20
  57. package/dist/providers/DeepSeekProvider.js.map +1 -1
  58. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  59. package/dist/providers/GeminiProvider.js +20 -37
  60. package/dist/providers/GeminiProvider.js.map +1 -1
  61. package/dist/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
  62. package/dist/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
  63. package/dist/providers/GenericOpenAICompatibleProvider.js +87 -0
  64. package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -0
  65. package/dist/providers/OpenAICompatibleProvider.d.ts +23 -5
  66. package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
  67. package/dist/providers/OpenAICompatibleProvider.js +47 -45
  68. package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
  69. package/dist/providers/index.d.ts +3 -1
  70. package/dist/providers/index.d.ts.map +1 -1
  71. package/dist/providers/index.js +1 -0
  72. package/dist/providers/index.js.map +1 -1
  73. package/dist/utils/completion.d.ts +20 -0
  74. package/dist/utils/completion.d.ts.map +1 -0
  75. package/dist/utils/completion.js +29 -0
  76. package/dist/utils/completion.js.map +1 -0
  77. package/dist/utils/index.d.ts +3 -2
  78. package/dist/utils/index.d.ts.map +1 -1
  79. package/dist/utils/index.js +3 -1
  80. package/dist/utils/index.js.map +1 -1
  81. package/dist/utils/retry.d.ts +43 -7
  82. package/dist/utils/retry.d.ts.map +1 -1
  83. package/dist/utils/retry.js +98 -12
  84. package/dist/utils/retry.js.map +1 -1
  85. package/package.json +1 -1
  86. package/src/core/ToolLoopExecutor.ts +108 -52
  87. package/src/index.ts +3 -0
  88. package/src/providers/AnthropicProvider.ts +22 -43
  89. package/src/providers/DeepSeekProvider.ts +3 -35
  90. package/src/providers/GeminiProvider.ts +23 -44
  91. package/src/providers/GenericOpenAICompatibleProvider.ts +138 -0
  92. package/src/providers/OpenAICompatibleProvider.ts +87 -67
  93. package/src/providers/index.ts +4 -0
  94. package/src/utils/completion.ts +38 -0
  95. package/src/utils/index.ts +5 -2
  96. package/src/utils/retry.ts +136 -14
@@ -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
- export declare const withTimeoutAndRetry: <T>(operation: () => Promise<T>, timeoutMs?: number, maxRetries?: number, operationName?: string) => Promise<T>;
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* — e.g. an
21
- * empty completion that throws "No response", or an error while establishing
22
- * the stream. Once any chunk has been yielded the stream is committed and
23
- * further errors propagate, so a retry can never double-emit deltas that the
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,EAC3B,YAAW,MAAc,EACzB,aAAY,MAAU,EACtB,gBAAe,MAAuB,KACrC,OAAO,CAAC,CAAC,CA8CX,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;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAuB,eAAe,CAAC,CAAC,EACtC,OAAO,EAAE,MAAM,cAAc,CAAC,CAAC,CAAC,EAChC,OAAO,GAAE,kBAAuB,GAC/B,cAAc,CAAC,CAAC,CAAC,CA6BnB"}
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"}
@@ -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: (attempt) => Math.min(1000 * Math.pow(2, attempt), 5000),
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 = Math.min(1000 * Math.pow(2, attempt), 5000);
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* — e.g. an
74
- * empty completion that throws "No response", or an error while establishing
75
- * the stream. Once any chunk has been yielded the stream is committed and
76
- * further errors propagate, so a retry can never double-emit deltas that the
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 = (attempt) => Math.min(1000 * Math.pow(2, attempt), 5000), operationName = "AI stream", } = options;
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
- for await (const chunk of factory()) {
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 chunk;
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
@@ -1 +1 @@
1
- {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../../src/utils/retry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falai/agent",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "description": "Conversational state engine for TypeScript where the AI understands, but the code is in control",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -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
- let { toolCalls, session } = params;
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
- // Create HistoryItem format for tool results
168
- // Add assistant message with tool_calls
169
- toolResultHistoryItems.push({
170
- role: "assistant" as const,
171
- content: null,
172
- tool_calls: [{
173
- id: toolCall.toolName,
174
- name: toolCall.toolName,
175
- arguments: toolCall.arguments,
176
- }],
177
- });
178
- // Add tool result
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
- role: "assistant" as const,
369
- content: null,
370
- tool_calls: [{
371
- id: toolName,
372
- name: toolName,
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
- // Tools ran but streaming produced no post-tool message — force one
526
- // from the results, mirroring runLoop. (The streaming chunk only ever
527
- // carries the pre-tool preamble, never a result-aware response.)
528
- if (toolResultsMap.size > 0) {
529
- const forced = await this.forceFinalTextFromTools({
530
- history,
531
- toolResultsMap,
532
- toolArgsMap,
533
- responsePrompt,
534
- responseSchema,
535
- context,
536
- signal,
537
- });
538
- finalMessage = forced.finalMessage;
539
- structured = forced.structured;
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 structured message with no tool calls is just as
385
- // empty as no text at all — throw so withTimeoutAndRetry retries instead
386
- // of returning {"message":""}.
387
- if (
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 — mirror of the non-streaming path. A blank
616
- // effective message (structured message under a schema, else accumulated
617
- // text) with no tool calls means the model produced nothing usable; throw
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 {