@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
@@ -5,6 +5,63 @@ 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>;
39
+ export interface StreamRetryOptions {
40
+ /** Maximum number of retries after the first attempt. Defaults to 3. */
41
+ maxRetries?: number;
42
+ /** Backoff before the next attempt, in ms. Defaults to capped exponential. */
43
+ delay?: (attempt: number) => number;
44
+ /** Label used in retry logs. */
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;
53
+ }
54
+ /**
55
+ * Streaming analog of {@link withTimeoutAndRetry}. Re-runs an async-generator
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.
61
+ *
62
+ * This mirrors the non-streaming path, where the provider throws on an empty
63
+ * completion inside `withTimeoutAndRetry` and is retried on the same model
64
+ * before the caller falls through to backup models.
65
+ */
66
+ export declare function withStreamRetry<T>(factory: (signal: AbortSignal) => AsyncGenerator<T>, options?: StreamRetryOptions): AsyncGenerator<T>;
10
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"}
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,4 +120,74 @@ 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
+ }
131
+ /**
132
+ * Streaming analog of {@link withTimeoutAndRetry}. Re-runs an async-generator
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.
138
+ *
139
+ * This mirrors the non-streaming path, where the provider throws on an empty
140
+ * completion inside `withTimeoutAndRetry` and is retried on the same model
141
+ * before the caller falls through to backup models.
142
+ */
143
+ export async function* withStreamRetry(factory, options = {}) {
144
+ const { maxRetries = 3, delay = defaultBackoff, operationName = "AI stream", firstChunkTimeoutMs, } = options;
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);
150
+ let yielded = false;
151
+ let completed = false;
152
+ try {
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
+ }
163
+ yielded = true;
164
+ yield result.value;
165
+ }
166
+ }
167
+ catch (error) {
168
+ // Can't retry once deltas are out, and don't retry past the budget.
169
+ if (yielded || attempt === maxRetries) {
170
+ throw error;
171
+ }
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ logger.error(`[${operationName}] Failed attempt ${attempt + 1}:`, message);
174
+ const delayMs = delay(attempt);
175
+ logger.debug(`[${operationName}] Retrying in ${delayMs}ms... (attempt ${attempt + 2}/${maxRetries + 1})`);
176
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
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
+ }
191
+ }
192
+ }
71
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.2",
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",
@@ -42,6 +42,24 @@ export class PersistenceManager<TData = Record<string, unknown>> {
42
42
  this.messageRepository = config.adapter.messageRepository;
43
43
  }
44
44
 
45
+ /**
46
+ * Reject a non-string id before it reaches a repository query filter.
47
+ *
48
+ * SECURITY: session/user ids flow into adapter query VALUE positions (e.g.
49
+ * Mongo `findOne({ id })`). A non-string value such as `{ $ne: null }` —
50
+ * trivially produced by an HTTP body or an Express `?id[$ne]=` query — would
51
+ * be interpreted by a NoSQL driver as query operators, turning an equality
52
+ * lookup into an operator query (cross-tenant read / mass delete). Callers
53
+ * type these as `string`, but that guarantee is erased at runtime, so we
54
+ * enforce it here: the single layer every public session/user lookup funnels
55
+ * through.
56
+ */
57
+ private assertScalarId(value: unknown, label: string): asserts value is string {
58
+ if (typeof value !== "string" || value.length === 0) {
59
+ throw new Error(`${label} must be a non-empty string`);
60
+ }
61
+ }
62
+
45
63
  /**
46
64
  * Create a new session
47
65
  */
@@ -67,6 +85,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
67
85
  * Get session by ID
68
86
  */
69
87
  async getSession(sessionId: string): Promise<SessionData<TData> | null> {
88
+ this.assertScalarId(sessionId, "Session ID");
70
89
  return await this.sessionRepository.findById(sessionId);
71
90
  }
72
91
 
@@ -80,6 +99,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
80
99
  "userId must be provided or configured in PersistenceConfig"
81
100
  );
82
101
  }
102
+ this.assertScalarId(effectiveUserId, "User ID");
83
103
  return await this.sessionRepository.findActiveByUserId(effectiveUserId);
84
104
  }
85
105
 
@@ -96,6 +116,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
96
116
  "userId must be provided or configured in PersistenceConfig"
97
117
  );
98
118
  }
119
+ this.assertScalarId(effectiveUserId, "User ID");
99
120
  return await this.sessionRepository.findByUserId(effectiveUserId, limit);
100
121
  }
101
122
 
@@ -170,6 +191,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
170
191
  sessionId: string,
171
192
  limit?: number
172
193
  ): Promise<MessageData[]> {
194
+ this.assertScalarId(sessionId, "Session ID");
173
195
  return await this.messageRepository.findBySessionId(sessionId, limit);
174
196
  }
175
197
 
@@ -186,6 +208,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
186
208
  "userId must be provided or configured in PersistenceConfig"
187
209
  );
188
210
  }
211
+ this.assertScalarId(effectiveUserId, "User ID");
189
212
  return await this.messageRepository.findByUserId(effectiveUserId, limit);
190
213
  }
191
214
 
@@ -193,6 +216,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
193
216
  * Delete a session and all its messages
194
217
  */
195
218
  async deleteSession(sessionId: string): Promise<boolean> {
219
+ this.assertScalarId(sessionId, "Session ID");
196
220
  // Delete all messages first
197
221
  await this.messageRepository.deleteBySessionId(sessionId);
198
222
 
@@ -260,9 +284,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
260
284
  sessionStep: SessionState<TData>
261
285
  ): Promise<SessionData<TData> | null> {
262
286
  // Validate input parameters
263
- if (!sessionId || typeof sessionId !== 'string') {
264
- throw new Error('Session ID must be a non-empty string');
265
- }
287
+ this.assertScalarId(sessionId, "Session ID");
266
288
 
267
289
  if (!sessionStep || typeof sessionStep !== 'object') {
268
290
  throw new Error('Session step must be a valid object');
@@ -307,9 +329,10 @@ export class PersistenceManager<TData = Record<string, unknown>> {
307
329
  } else {
308
330
  saved = await this.sessionRepository.create({
309
331
  id: sessionId,
310
- userId: persistenceData.collectedData.metadata?.userId
311
- ? JSON.stringify(persistenceData.collectedData.metadata?.userId)
312
- : this.config.userId,
332
+ // Owner is the authenticated principal from config — never derived
333
+ // from collectedData.metadata (model/user-influenced state), which
334
+ // would let collected data mis-attribute a session's tenant.
335
+ userId: this.config.userId,
313
336
  status: "active",
314
337
  currentFlow: persistenceData.currentFlow,
315
338
  currentStep: persistenceData.currentStep,
@@ -342,6 +365,7 @@ export class PersistenceManager<TData = Record<string, unknown>> {
342
365
  async loadSessionState(
343
366
  sessionId: string
344
367
  ): Promise<SessionState<TData> | null> {
368
+ this.assertScalarId(sessionId, "Session ID");
345
369
  const sessionData = await this.sessionRepository.findById(sessionId);
346
370
 
347
371
  if (!sessionData) {
@@ -1243,6 +1243,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1243
1243
  // Stream chunks with unified tool handling
1244
1244
  for await (const chunk of stream) {
1245
1245
  let toolCalls: Array<{ toolName: string; arguments: Record<string, unknown> }> | undefined = undefined;
1246
+ // Final message/structured may be replaced by a forced post-tool
1247
+ // response (see runStreamingBatch / gap: tools-ran-but-no-text).
1248
+ let finalAccumulated = chunk.accumulated;
1249
+ let finalStructured = chunk.structured;
1246
1250
 
1247
1251
  // Extract tool calls from AI response on final chunk
1248
1252
  if (chunk.done && chunk.structured?.toolCalls) {
@@ -1265,9 +1269,18 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1265
1269
  });
1266
1270
  session = batchResult.session;
1267
1271
  toolCalls = batchResult.toolCalls;
1272
+
1273
+ // Tools ran but the model produced no result-aware text — use
1274
+ // the forced closing message so we never emit the bare
1275
+ // preamble (or an empty message) as the final response.
1276
+ if (batchResult.finalMessage) {
1277
+ finalAccumulated = batchResult.finalMessage;
1278
+ finalStructured = batchResult.structured ?? finalStructured;
1279
+ }
1268
1280
  }
1269
1281
 
1270
- // Extract collected data on final chunk
1282
+ // Extract collected data on final chunk (from the model's own
1283
+ // structured output for this step, not the forced follow-up)
1271
1284
  if (chunk.done && chunk.structured && nextStep.collect) {
1272
1285
  session = await this.collectDataFromResponse({
1273
1286
  result: { structured: chunk.structured },
@@ -1283,7 +1296,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1283
1296
  // - session.currentStep: reflects the executed step
1284
1297
  yield {
1285
1298
  delta: chunk.delta,
1286
- accumulated: chunk.accumulated,
1299
+ accumulated: finalAccumulated,
1287
1300
  done: chunk.done,
1288
1301
  session,
1289
1302
  toolCalls,
@@ -1291,7 +1304,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1291
1304
  executedSteps: chunk.done ? [{ id: nextStep.id, flowId: selectedFlow.id }] : undefined,
1292
1305
  stoppedReason: chunk.done ? 'needs_input' : undefined,
1293
1306
  metadata: chunk.metadata,
1294
- structured: chunk.structured,
1307
+ structured: finalStructured,
1295
1308
  appliedInstructions: chunk.done ? appliedInstructions : undefined,
1296
1309
  };
1297
1310
  }
@@ -59,6 +59,13 @@ export class SessionManager<TData = unknown> {
59
59
  * Works for sessionIds that exist, don't exist, or auto-generated IDs
60
60
  */
61
61
  async getOrCreate(sessionId?: string): Promise<SessionState<TData>> {
62
+ // SECURITY: a non-string sessionId (e.g. `{ $ne: null }` from an HTTP body
63
+ // or `?sessionId[$ne]=` query) forwarded into a NoSQL query becomes an
64
+ // operator clause. Reject it at the entry point; `undefined` is allowed and
65
+ // auto-generates an id, and an empty string keeps its fall-back semantics.
66
+ if (sessionId !== undefined && typeof sessionId !== "string") {
67
+ throw new Error("sessionId must be a string");
68
+ }
62
69
  // Use provided sessionId or fall back to default
63
70
  const effectiveSessionId = sessionId || this.defaultSessionId;
64
71
 
@@ -19,6 +19,7 @@ import type {
19
19
  Tool,
20
20
  TrackedTool,
21
21
  } from "../types/tool";
22
+ import { evaluateToolGates } from "./toolGates";
22
23
 
23
24
  /** Options for the StreamingToolExecutor */
24
25
  interface StreamingToolExecutorOptions {
@@ -221,6 +222,17 @@ export class StreamingToolExecutor<TContext = unknown, TData = unknown> {
221
222
  const batchAbortController = this.siblingAbortController;
222
223
 
223
224
  try {
225
+ // Pre-execution gates (validateInput → checkPermissions), shared with
226
+ // ToolManager.executeTool. These MUST run on the streaming path too:
227
+ // when a gate denies, the handler is NOT invoked. Run before any abort
228
+ // wiring so a denial returns cleanly with nothing to unwind.
229
+ const gateDenial = await evaluateToolGates(tool, toolCall.arguments, this.toolContext);
230
+ if (gateDenial) {
231
+ tracked.results.push(gateDenial);
232
+ tracked.status = "completed";
233
+ return;
234
+ }
235
+
224
236
  // Create a combined abort signal from parent + sibling
225
237
  const toolAbortController = new AbortController();
226
238
  const abortTool = () => {