@librechat/agents 3.3.0 → 3.3.1

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 (129) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +116 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +2 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +229 -48
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +102 -27
  8. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  9. package/dist/cjs/llm/bedrock/index.cjs +11 -2
  10. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  11. package/dist/cjs/llm/contextOverflowRecovery.cjs +132 -0
  12. package/dist/cjs/llm/contextOverflowRecovery.cjs.map +1 -0
  13. package/dist/cjs/llm/google/index.cjs +1 -1
  14. package/dist/cjs/llm/invoke.cjs +60 -3
  15. package/dist/cjs/llm/invoke.cjs.map +1 -1
  16. package/dist/cjs/main.cjs +20 -7
  17. package/dist/cjs/messages/prune.cjs +16 -5
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/messages/recency.cjs +2 -0
  20. package/dist/cjs/messages/recency.cjs.map +1 -1
  21. package/dist/cjs/run.cjs +15 -2
  22. package/dist/cjs/run.cjs.map +1 -1
  23. package/dist/cjs/stream.cjs +205 -49
  24. package/dist/cjs/stream.cjs.map +1 -1
  25. package/dist/cjs/summarization/node.cjs +55 -7
  26. package/dist/cjs/summarization/node.cjs.map +1 -1
  27. package/dist/cjs/tools/BashExecutor.cjs +2 -2
  28. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  29. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -2
  30. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  31. package/dist/cjs/tools/CodeExecutor.cjs +69 -8
  32. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +19 -15
  34. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  35. package/dist/cjs/tools/ToolNode.cjs +32 -3
  36. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  37. package/dist/cjs/utils/errors.cjs +317 -52
  38. package/dist/cjs/utils/errors.cjs.map +1 -1
  39. package/dist/esm/agents/AgentContext.mjs +117 -3
  40. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  41. package/dist/esm/common/enum.mjs +2 -0
  42. package/dist/esm/common/enum.mjs.map +1 -1
  43. package/dist/esm/graphs/Graph.mjs +227 -46
  44. package/dist/esm/graphs/Graph.mjs.map +1 -1
  45. package/dist/esm/graphs/MultiAgentGraph.mjs +102 -27
  46. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  47. package/dist/esm/llm/bedrock/index.mjs +11 -2
  48. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  49. package/dist/esm/llm/contextOverflowRecovery.mjs +130 -0
  50. package/dist/esm/llm/contextOverflowRecovery.mjs.map +1 -0
  51. package/dist/esm/llm/google/index.mjs +1 -1
  52. package/dist/esm/llm/invoke.mjs +59 -4
  53. package/dist/esm/llm/invoke.mjs.map +1 -1
  54. package/dist/esm/main.mjs +9 -9
  55. package/dist/esm/messages/prune.mjs +14 -6
  56. package/dist/esm/messages/prune.mjs.map +1 -1
  57. package/dist/esm/messages/recency.mjs +2 -1
  58. package/dist/esm/messages/recency.mjs.map +1 -1
  59. package/dist/esm/run.mjs +15 -2
  60. package/dist/esm/run.mjs.map +1 -1
  61. package/dist/esm/stream.mjs +205 -49
  62. package/dist/esm/stream.mjs.map +1 -1
  63. package/dist/esm/summarization/node.mjs +55 -7
  64. package/dist/esm/summarization/node.mjs.map +1 -1
  65. package/dist/esm/tools/BashExecutor.mjs +3 -3
  66. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  67. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +3 -3
  68. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  69. package/dist/esm/tools/CodeExecutor.mjs +62 -9
  70. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  71. package/dist/esm/tools/ProgrammaticToolCalling.mjs +20 -16
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  73. package/dist/esm/tools/ToolNode.mjs +32 -3
  74. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  75. package/dist/esm/utils/errors.mjs +317 -53
  76. package/dist/esm/utils/errors.mjs.map +1 -1
  77. package/dist/types/agents/AgentContext.d.ts +62 -3
  78. package/dist/types/common/enum.d.ts +2 -0
  79. package/dist/types/graphs/Graph.d.ts +16 -2
  80. package/dist/types/graphs/MultiAgentGraph.d.ts +1 -0
  81. package/dist/types/llm/contextOverflowRecovery.d.ts +85 -0
  82. package/dist/types/llm/invoke.d.ts +35 -6
  83. package/dist/types/messages/prune.d.ts +10 -2
  84. package/dist/types/messages/recency.d.ts +1 -0
  85. package/dist/types/run.d.ts +2 -0
  86. package/dist/types/tools/CodeExecutor.d.ts +14 -1
  87. package/dist/types/types/llm.d.ts +7 -4
  88. package/dist/types/types/stream.d.ts +5 -4
  89. package/dist/types/types/summarize.d.ts +22 -0
  90. package/dist/types/utils/__tests__/fixtures/contextOverflowSignatures.d.ts +40 -0
  91. package/dist/types/utils/errors.d.ts +65 -16
  92. package/dist/types/utils/redactSecrets.d.ts +3 -0
  93. package/package.json +7 -8
  94. package/src/agents/AgentContext.ts +188 -7
  95. package/src/agents/__tests__/AgentContext.overflow.test.ts +205 -0
  96. package/src/common/enum.ts +2 -0
  97. package/src/graphs/Graph.ts +389 -58
  98. package/src/graphs/MultiAgentGraph.ts +184 -46
  99. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +631 -0
  100. package/src/llm/__tests__/contextOverflowRecovery.test.ts +401 -0
  101. package/src/llm/__tests__/fallbackOverflow.test.ts +287 -0
  102. package/src/llm/bedrock/index.ts +25 -12
  103. package/src/llm/contextOverflowRecovery.ts +292 -0
  104. package/src/llm/invoke.ts +119 -4
  105. package/src/messages/prune.ts +24 -11
  106. package/src/messages/recency.ts +3 -1
  107. package/src/run.ts +24 -1
  108. package/src/scripts/context-overflow-probe.ts +997 -0
  109. package/src/specs/agent-handoffs.test.ts +903 -1
  110. package/src/specs/context-overflow-recovery.live.test.ts +213 -0
  111. package/src/splitStream.test.ts +882 -0
  112. package/src/stream.ts +315 -51
  113. package/src/summarization/__tests__/aggregator.test.ts +83 -0
  114. package/src/summarization/__tests__/node.test.ts +139 -0
  115. package/src/summarization/node.ts +99 -14
  116. package/src/tools/BashExecutor.ts +4 -2
  117. package/src/tools/BashProgrammaticToolCalling.ts +4 -7
  118. package/src/tools/CodeExecutor.ts +119 -8
  119. package/src/tools/ProgrammaticToolCalling.ts +29 -27
  120. package/src/tools/ToolNode.ts +50 -8
  121. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +297 -3
  122. package/src/types/llm.ts +8 -1
  123. package/src/types/stream.ts +5 -4
  124. package/src/types/summarize.ts +22 -0
  125. package/src/utils/__tests__/errors.test.ts +270 -0
  126. package/src/utils/__tests__/fixtures/contextOverflowSignatures.ts +336 -0
  127. package/src/utils/__tests__/redactSecrets.test.ts +56 -0
  128. package/src/utils/errors.ts +484 -66
  129. package/src/utils/redactSecrets.ts +61 -0
@@ -15,6 +15,10 @@ import {
15
15
  resolvePromptCacheTtl,
16
16
  type PromptCacheTtl,
17
17
  } from '@/messages/cache';
18
+ import {
19
+ DEFAULT_RETAIN_RECENT_TURNS,
20
+ splitAtRecencyBoundary,
21
+ } from '@/messages/recency';
18
22
  import {
19
23
  Constants,
20
24
  ContentTypes,
@@ -25,7 +29,6 @@ import {
25
29
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
26
30
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
27
31
  import { createRemoveAllMessage } from '@/messages/reducer';
28
- import { splitAtRecencyBoundary } from '@/messages/recency';
29
32
  import { getMaxOutputTokensKey } from '@/llm/request';
30
33
  import { initializeModel } from '@/llm/init';
31
34
  import { getChunkContent } from '@/stream';
@@ -43,8 +46,6 @@ const SUMMARIZATION_PARAM_KEYS = new Set(['maxSummaryTokens']);
43
46
  * `retainRecent.turns` to `0` reverts to the legacy "summarize every
44
47
  * message" behavior.
45
48
  */
46
- const DEFAULT_RETAIN_RECENT_TURNS = 2;
47
-
48
49
  /**
49
50
  * Token overhead of the XML wrapper + instruction text added around the
50
51
  * summary at injection time in AgentContext.buildSystemRunnable:
@@ -517,7 +518,16 @@ async function executeSummarizationWithFallback(params: {
517
518
  stepId: string;
518
519
  usePromptCache: boolean;
519
520
  log: LogFn;
520
- }): Promise<{ text: string; usage?: Partial<UsageMetadata> }> {
521
+ }): Promise<{
522
+ text: string;
523
+ usage?: Partial<UsageMetadata>;
524
+ /**
525
+ * True when every model call failed and `text` is the generated metadata
526
+ * stub rather than a real summary. Callers that would replace history with
527
+ * this need to know it carries none of the original content.
528
+ */
529
+ usedMetadataStub?: boolean;
530
+ }> {
521
531
  const {
522
532
  agentContext,
523
533
  messages,
@@ -532,6 +542,7 @@ async function executeSummarizationWithFallback(params: {
532
542
 
533
543
  let summaryText = '';
534
544
  let summaryUsage: Partial<UsageMetadata> | undefined;
545
+ let usedMetadataStub = false;
535
546
 
536
547
  try {
537
548
  /**
@@ -634,10 +645,11 @@ async function executeSummarizationWithFallback(params: {
634
645
  }
635
646
  );
636
647
  summaryText = generateMetadataStub(messages);
648
+ usedMetadataStub = true;
637
649
  }
638
650
  }
639
651
 
640
- return { text: summaryText, usage: summaryUsage };
652
+ return { text: summaryText, usage: summaryUsage, usedMetadataStub };
641
653
  }
642
654
 
643
655
  /** Dispatches run step completion, ON_SUMMARIZE_COMPLETE, and rebuilds token map. */
@@ -774,6 +786,34 @@ export function createSummarizeNode({
774
786
  return { summarizationRequest: undefined };
775
787
  }
776
788
 
789
+ /**
790
+ * Overflow recovery routes through this node purely to get back to the
791
+ * agent node with a corrected budget, and deliberately spends no model
792
+ * call on its first attempt: re-pruning under the raised context pressure
793
+ * drives the pruner's tool-output compression and masking, which is
794
+ * cheaper than a summary and cannot lose message content. Summarization
795
+ * is also skipped outright when the caller never enabled it.
796
+ */
797
+ if (
798
+ request.reason === 'overflow' &&
799
+ (request.allowSummarization !== true ||
800
+ agentContext.summarizationEnabled !== true)
801
+ ) {
802
+ emitAgentLog(
803
+ config,
804
+ 'debug',
805
+ 'summarize',
806
+ 'Overflow recovery re-prune — compressing tool output without a summarization call',
807
+ {
808
+ maxContextTokens: agentContext.maxContextTokens,
809
+ summarizationEnabled: agentContext.summarizationEnabled === true,
810
+ allowSummarization: request.allowSummarization === true,
811
+ },
812
+ { runId: graph.runId, agentId: request.agentId }
813
+ );
814
+ return { summarizationRequest: undefined };
815
+ }
816
+
777
817
  const maxCtx = agentContext.maxContextTokens ?? 0;
778
818
  if (maxCtx > 0 && agentContext.instructionTokens >= maxCtx) {
779
819
  emitAgentLog(
@@ -976,16 +1016,61 @@ export function createSummarizeNode({
976
1016
  }
977
1017
  : undefined;
978
1018
 
979
- const { text: rawText, usage: summaryUsage } =
980
- await executeSummarizationWithFallback({
981
- agentContext,
982
- messages: messagesToRefine,
983
- clientConfig,
984
- summarizeConfig,
1019
+ const {
1020
+ text: rawText,
1021
+ usage: summaryUsage,
1022
+ usedMetadataStub,
1023
+ } = await executeSummarizationWithFallback({
1024
+ agentContext,
1025
+ messages: messagesToRefine,
1026
+ clientConfig,
1027
+ summarizeConfig,
1028
+ stepId,
1029
+ usePromptCache: isSelfSummarizeModel && hasPromptCache,
1030
+ log,
1031
+ });
1032
+
1033
+ /**
1034
+ * The metadata stub describes the history rather than summarizing it, so
1035
+ * committing it means removing the head and keeping nothing of what it
1036
+ * said. That trade is never worth making to paper over an overflow: the
1037
+ * recovery would "succeed" only by destroying the conversation it was
1038
+ * supposed to preserve. Leave state untouched and let the provider error
1039
+ * surface instead.
1040
+ */
1041
+ if (usedMetadataStub === true && request.reason === 'overflow') {
1042
+ log(
1043
+ 'warn',
1044
+ 'Overflow summarization failed; keeping history rather than replacing it with a metadata stub'
1045
+ );
1046
+ agentContext.markSummarizationTriggered(state.messages.length);
1047
+ /**
1048
+ * The run step was already dispatched, so it has to be resolved here or
1049
+ * consumers tracking step lifecycle keep an unfinished placeholder for
1050
+ * the rest of the run.
1051
+ */
1052
+ await graph.dispatchRunStepCompleted(
985
1053
  stepId,
986
- usePromptCache: isSelfSummarizeModel && hasPromptCache,
987
- log,
988
- });
1054
+ {
1055
+ type: 'summary',
1056
+ summary: placeholderSummary,
1057
+ } satisfies t.SummaryCompleted,
1058
+ runnableConfig
1059
+ );
1060
+ if (runnableConfig) {
1061
+ await safeDispatchCustomEvent(
1062
+ GraphEvents.ON_SUMMARIZE_COMPLETE,
1063
+ {
1064
+ id: stepId,
1065
+ agentId: request.agentId,
1066
+ error:
1067
+ 'Summarization failed during overflow recovery; conversation history was preserved',
1068
+ } satisfies t.SummarizeCompleteEvent,
1069
+ runnableConfig
1070
+ );
1071
+ }
1072
+ return { summarizationRequest: undefined };
1073
+ }
989
1074
 
990
1075
  if (!rawText) {
991
1076
  agentContext.markSummarizationTriggered(0);
@@ -11,7 +11,9 @@ import {
11
11
  appendCodeSessionFileSummary,
12
12
  emptyOutputMessage,
13
13
  buildCodeApiHttpErrorMessage,
14
+ CodeApiRequestError,
14
15
  getCodeBaseURL,
16
+ normalizeCodeApiRequestError,
15
17
  resolveCodeApiAuthHeaders,
16
18
  } from './CodeExecutor';
17
19
  import { Constants } from '@/common';
@@ -250,7 +252,7 @@ function createBashExecutionTool(
250
252
  }
251
253
  const response = await fetch(EXEC_ENDPOINT, fetchOptions);
252
254
  if (!response.ok) {
253
- throw new Error(
255
+ throw new CodeApiRequestError(
254
256
  await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)
255
257
  );
256
258
  }
@@ -291,7 +293,7 @@ function createBashExecutionTool(
291
293
  ];
292
294
  } catch (error) {
293
295
  const messageWithReminder = appendFailedExecutionFileReminder(
294
- (error as Error | undefined)?.message ?? '',
296
+ normalizeCodeApiRequestError(error).message,
295
297
  command
296
298
  );
297
299
  throw new Error(`Execution error:\n\n${messageWithReminder}`);
@@ -7,6 +7,8 @@ import {
7
7
  BASH_SHELL_GUIDANCE,
8
8
  CODE_ARTIFACT_PATH_GUIDANCE,
9
9
  appendFailedExecutionFileReminder,
10
+ buildCodeApiExecutionErrorMessage,
11
+ CodeApiRequestError,
10
12
  getCodeBaseURL,
11
13
  } from './CodeExecutor';
12
14
  import {
@@ -434,15 +436,10 @@ export function createBashProgrammaticToolCallingTool(
434
436
  }
435
437
 
436
438
  if (response.status === 'error') {
437
- throw new Error(
438
- `Execution error: ${response.error}` +
439
- (response.stderr != null && response.stderr !== ''
440
- ? `\n\nStderr:\n${response.stderr}`
441
- : '')
442
- );
439
+ throw new Error(buildCodeApiExecutionErrorMessage(response));
443
440
  }
444
441
 
445
- throw new Error(`Unexpected response status: ${response.status}`);
442
+ throw new CodeApiRequestError();
446
443
  } catch (error) {
447
444
  const messageWithReminder = appendFailedExecutionFileReminder(
448
445
  (error as Error).message,
@@ -111,6 +111,102 @@ const EXEC_ENDPOINT = `${baseEndpoint}/exec`;
111
111
 
112
112
  type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
113
113
 
114
+ const MAX_RETRY_AFTER_SECONDS = 3600;
115
+
116
+ export const CODE_API_UNAVAILABLE_ERROR_MESSAGE =
117
+ 'Code execution is temporarily unavailable. Please retry.';
118
+ export const CODE_API_AUTHORIZATION_ERROR_MESSAGE =
119
+ 'Code execution is not authorized. Verify access before trying again.';
120
+ export const CODE_API_EXECUTION_FAILED_ERROR_MESSAGE = 'Code execution failed.';
121
+ export const CODE_API_INVALID_REQUEST_ERROR_MESSAGE =
122
+ 'The code execution request was rejected. Please check the tool input and try again.';
123
+ export const CODE_API_RATE_LIMITED_ERROR_MESSAGE =
124
+ 'Code execution is temporarily rate-limited. Please retry shortly.';
125
+
126
+ const SAFE_CODE_API_EXECUTION_ERROR_DETAILS: Readonly<
127
+ Partial<Record<string, string>>
128
+ > = {
129
+ 'Execution failed or timed out': 'Execution failed or timed out.',
130
+ 'Out of memory': 'Execution exceeded the memory limit.',
131
+ 'Time limit exceeded': 'Execution exceeded the time limit.',
132
+ 'sandbox emitted an empty pending tool call block; aborting to avoid a tight retry loop':
133
+ 'Generated code emitted an invalid empty tool request.',
134
+ 'stderr length exceeded': 'Execution error output exceeded the size limit.',
135
+ 'stdout length exceeded': 'Execution output exceeded the size limit.',
136
+ };
137
+
138
+ export class CodeApiRequestError extends Error {
139
+ constructor(message = CODE_API_UNAVAILABLE_ERROR_MESSAGE) {
140
+ super(message);
141
+ this.name = 'CodeApiRequestError';
142
+ }
143
+ }
144
+
145
+ function getRetryAfterSeconds(responseBody: string): number | undefined {
146
+ try {
147
+ const parsed = JSON.parse(responseBody) as {
148
+ error?: unknown;
149
+ retry_after_seconds?: unknown;
150
+ };
151
+ if (
152
+ parsed.error !== 'rate_limited' ||
153
+ typeof parsed.retry_after_seconds !== 'number' ||
154
+ !Number.isFinite(parsed.retry_after_seconds) ||
155
+ parsed.retry_after_seconds <= 0
156
+ ) {
157
+ return undefined;
158
+ }
159
+ return Math.min(
160
+ Math.ceil(parsed.retry_after_seconds),
161
+ MAX_RETRY_AFTER_SECONDS
162
+ );
163
+ } catch {
164
+ return undefined;
165
+ }
166
+ }
167
+
168
+ export function normalizeCodeApiRequestError(
169
+ error: unknown
170
+ ): CodeApiRequestError {
171
+ return error instanceof CodeApiRequestError
172
+ ? error
173
+ : new CodeApiRequestError();
174
+ }
175
+
176
+ function getSafeCodeApiExecutionErrorDetail(
177
+ error: unknown
178
+ ): string | undefined {
179
+ if (typeof error !== 'string') {
180
+ return undefined;
181
+ }
182
+ const exactMatch = SAFE_CODE_API_EXECUTION_ERROR_DETAILS[error];
183
+ if (exactMatch != null) {
184
+ return exactMatch;
185
+ }
186
+ if (/^Sandbox exited with code (?:-?\d{1,4}|unknown)$/.test(error)) {
187
+ return error.replace(/^Sandbox/, 'Execution');
188
+ }
189
+ if (/^Sandbox requested an unregistered tool: .+$/.test(error)) {
190
+ return 'Generated code requested a tool that is not available.';
191
+ }
192
+ return undefined;
193
+ }
194
+
195
+ export function buildCodeApiExecutionErrorMessage(response: {
196
+ error?: unknown;
197
+ stderr?: unknown;
198
+ }): string {
199
+ const safeDetail = getSafeCodeApiExecutionErrorDetail(response.error);
200
+ const message =
201
+ safeDetail != null
202
+ ? `${CODE_API_EXECUTION_FAILED_ERROR_MESSAGE} ${safeDetail}`
203
+ : CODE_API_EXECUTION_FAILED_ERROR_MESSAGE;
204
+ if (typeof response.stderr === 'string' && response.stderr !== '') {
205
+ return `${message}\n\nStderr:\n${response.stderr}`;
206
+ }
207
+ return message;
208
+ }
209
+
114
210
  export async function resolveCodeApiAuthHeaders(
115
211
  authHeaders?: t.CodeApiAuthHeaders
116
212
  ): Promise<t.CodeApiAuthHeaderMap> {
@@ -118,14 +214,19 @@ export async function resolveCodeApiAuthHeaders(
118
214
  return {};
119
215
  }
120
216
  if (typeof authHeaders === 'function') {
121
- return authHeaders();
217
+ try {
218
+ const resolvedHeaders = await authHeaders();
219
+ return resolvedHeaders;
220
+ } catch {
221
+ throw new CodeApiRequestError(CODE_API_AUTHORIZATION_ERROR_MESSAGE);
222
+ }
122
223
  }
123
224
  return authHeaders;
124
225
  }
125
226
 
126
227
  export async function buildCodeApiHttpErrorMessage(
127
- method: string,
128
- endpoint: string,
228
+ _method: string,
229
+ _endpoint: string,
129
230
  response: { status: number; text: () => Promise<string> }
130
231
  ): Promise<string> {
131
232
  let responseBody = '';
@@ -134,9 +235,19 @@ export async function buildCodeApiHttpErrorMessage(
134
235
  } catch {
135
236
  responseBody = '';
136
237
  }
137
- const body = responseBody.trim();
138
- const bodySuffix = body === '' ? '' : `, body: ${body.slice(0, 1000)}`;
139
- return `CodeAPI request failed: ${method} ${endpoint} returned ${response.status}${bodySuffix}`;
238
+ if (response.status === 429) {
239
+ const retryAfterSeconds = getRetryAfterSeconds(responseBody);
240
+ return retryAfterSeconds != null
241
+ ? `Code execution is temporarily rate-limited. Retry after ${retryAfterSeconds} seconds.`
242
+ : CODE_API_RATE_LIMITED_ERROR_MESSAGE;
243
+ }
244
+ if (response.status === 401 || response.status === 403) {
245
+ return CODE_API_AUTHORIZATION_ERROR_MESSAGE;
246
+ }
247
+ if (response.status === 400 || response.status === 422) {
248
+ return CODE_API_INVALID_REQUEST_ERROR_MESSAGE;
249
+ }
250
+ return CODE_API_UNAVAILABLE_ERROR_MESSAGE;
140
251
  }
141
252
 
142
253
  export const CodeExecutionToolDescription = `
@@ -314,7 +425,7 @@ function createCodeExecutionTool(
314
425
  }
315
426
  const response = await fetch(EXEC_ENDPOINT, fetchOptions);
316
427
  if (!response.ok) {
317
- throw new Error(
428
+ throw new CodeApiRequestError(
318
429
  await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)
319
430
  );
320
431
  }
@@ -358,7 +469,7 @@ function createCodeExecutionTool(
358
469
  ];
359
470
  } catch (error) {
360
471
  const messageWithReminder = appendFailedExecutionFileReminder(
361
- (error as Error | undefined)?.message ?? '',
472
+ normalizeCodeApiRequestError(error).message,
362
473
  code
363
474
  );
364
475
  throw new Error(`Execution error:\n\n${messageWithReminder}`);
@@ -10,10 +10,13 @@ import {
10
10
  CODE_ARTIFACT_PATH_GUIDANCE,
11
11
  appendCodeSessionFileSummary,
12
12
  appendFailedExecutionFileReminder,
13
+ buildCodeApiExecutionErrorMessage,
13
14
  buildCodeApiHttpErrorMessage,
15
+ CodeApiRequestError,
14
16
  emptyOutputMessage,
15
17
  getCodeBaseURL,
16
18
  appendTmpScratchReminder,
19
+ normalizeCodeApiRequestError,
17
20
  resolveCodeApiAuthHeaders,
18
21
  } from './CodeExecutor';
19
22
  import {
@@ -474,30 +477,34 @@ export async function makeRequest(
474
477
  proxy?: string,
475
478
  authHeaders?: t.CodeApiAuthHeaders
476
479
  ): Promise<t.ProgrammaticExecutionResponse> {
477
- const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders);
478
- const fetchOptions: RequestInit = {
479
- method: 'POST',
480
- headers: {
481
- 'Content-Type': 'application/json',
482
- 'User-Agent': 'LibreChat/1.0',
483
- ...resolvedAuthHeaders,
484
- },
485
- body: JSON.stringify(body),
486
- };
480
+ try {
481
+ const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders);
482
+ const fetchOptions: RequestInit = {
483
+ method: 'POST',
484
+ headers: {
485
+ 'Content-Type': 'application/json',
486
+ 'User-Agent': 'LibreChat/1.0',
487
+ ...resolvedAuthHeaders,
488
+ },
489
+ body: JSON.stringify(body),
490
+ };
487
491
 
488
- if (proxy != null && proxy !== '') {
489
- fetchOptions.agent = new HttpsProxyAgent(proxy);
490
- }
492
+ if (proxy != null && proxy !== '') {
493
+ fetchOptions.agent = new HttpsProxyAgent(proxy);
494
+ }
491
495
 
492
- const response = await fetch(endpoint, fetchOptions);
496
+ const response = await fetch(endpoint, fetchOptions);
493
497
 
494
- if (!response.ok) {
495
- throw new Error(
496
- await buildCodeApiHttpErrorMessage('POST', endpoint, response)
497
- );
498
- }
498
+ if (!response.ok) {
499
+ throw new CodeApiRequestError(
500
+ await buildCodeApiHttpErrorMessage('POST', endpoint, response)
501
+ );
502
+ }
499
503
 
500
- return (await response.json()) as t.ProgrammaticExecutionResponse;
504
+ return (await response.json()) as t.ProgrammaticExecutionResponse;
505
+ } catch (error) {
506
+ throw normalizeCodeApiRequestError(error);
507
+ }
501
508
  }
502
509
 
503
510
  /**
@@ -1014,15 +1021,10 @@ export function createProgrammaticToolCallingTool(
1014
1021
  }
1015
1022
 
1016
1023
  if (response.status === 'error') {
1017
- throw new Error(
1018
- `Execution error: ${response.error}` +
1019
- (response.stderr != null && response.stderr !== ''
1020
- ? `\n\nStderr:\n${response.stderr}`
1021
- : '')
1022
- );
1024
+ throw new Error(buildCodeApiExecutionErrorMessage(response));
1023
1025
  }
1024
1026
 
1025
- throw new Error(`Unexpected response status: ${response.status}`);
1027
+ throw new CodeApiRequestError();
1026
1028
  } catch (error) {
1027
1029
  const messageWithReminder = appendFailedExecutionFileReminder(
1028
1030
  (error as Error).message,
@@ -1,3 +1,4 @@
1
+ import { nanoid } from 'nanoid';
1
2
  import { ToolCall } from '@langchain/core/messages/tool';
2
3
  import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
3
4
  import {
@@ -119,6 +120,9 @@ type RunToolBatchContext<T = unknown> = {
119
120
  };
120
121
 
121
122
  const TOOL_NODE_RUN_NAME = 'tool_batch';
123
+ const NANOID_URL_ALPHABET =
124
+ '_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
125
+ const RUNTIME_HANDOFF_GROUP_OFFSET = 2 ** 48;
122
126
 
123
127
  /**
124
128
  * Per-batch context for `dispatchToolEvents` / `executeViaEvent`.
@@ -158,6 +162,41 @@ function isHandoffToolName(name: string): boolean {
158
162
  return name.startsWith(Constants.LC_TRANSFER_TO_);
159
163
  }
160
164
 
165
+ /**
166
+ * Encodes 48 random bits from the persisted batch key into a safe integer.
167
+ * The high offset keeps runtime groups disjoint from low, structural group IDs.
168
+ */
169
+ function getRuntimeHandoffGroupId(batch: string): number {
170
+ let value = 0;
171
+ for (const char of batch.slice(0, 8)) {
172
+ const digit = NANOID_URL_ALPHABET.indexOf(char);
173
+ value = value * 64 + Math.max(digit, 0);
174
+ }
175
+ return RUNTIME_HANDOFF_GROUP_OFFSET + value;
176
+ }
177
+
178
+ function findHandoffMessage(
179
+ messages: BaseMessage[],
180
+ destination: string
181
+ ): ToolMessage | undefined {
182
+ for (let i = messages.length - 1; i >= 0; i--) {
183
+ const message = messages[i];
184
+ if (message.getType() !== 'tool') {
185
+ continue;
186
+ }
187
+ const toolMessage = message as ToolMessage;
188
+ const isStandardHandoff =
189
+ toolMessage.name === `${Constants.LC_TRANSFER_TO_}${destination}`;
190
+ const isConditionalHandoff =
191
+ toolMessage.name === 'conditional_transfer' &&
192
+ toolMessage.additional_kwargs.handoff_destination === destination;
193
+ if (isStandardHandoff || isConditionalHandoff) {
194
+ return toolMessage;
195
+ }
196
+ }
197
+ return undefined;
198
+ }
199
+
161
200
  /**
162
201
  * Format a fail-closed diagnostic for malformed approval-decision
163
202
  * fields. Hosts deserialize resume payloads from untyped JSON, so
@@ -3854,21 +3893,24 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3854
3893
  const goto = cmd.goto;
3855
3894
  return typeof goto === 'string' ? goto : (goto as string[])[0];
3856
3895
  });
3896
+ const parallelBatch = nanoid();
3897
+ const parallelGroupId = getRuntimeHandoffGroupId(parallelBatch);
3857
3898
 
3858
3899
  const sends = handoffCommands.map((cmd, idx) => {
3859
3900
  const destination = allDestinations[idx];
3860
3901
  /** Get siblings (other destinations, not this one) */
3861
3902
  const siblings = allDestinations.filter((d) => d !== destination);
3862
3903
 
3863
- /** Add siblings to ToolMessage additional_kwargs */
3864
3904
  const update = cmd.update as { messages?: BaseMessage[] } | undefined;
3865
- if (update && update.messages) {
3866
- for (const msg of update.messages) {
3867
- if (msg.getType() === 'tool') {
3868
- (msg as ToolMessage).additional_kwargs.handoff_parallel_siblings =
3869
- siblings;
3870
- }
3871
- }
3905
+ const handoffMessage = update?.messages
3906
+ ? findHandoffMessage(update.messages, destination)
3907
+ : undefined;
3908
+ if (handoffMessage) {
3909
+ handoffMessage.additional_kwargs.handoff_parallel_siblings = siblings;
3910
+ handoffMessage.additional_kwargs[Constants.HANDOFF_PARALLEL_BATCH] =
3911
+ parallelBatch;
3912
+ handoffMessage.additional_kwargs[Constants.HANDOFF_GROUP_ID] =
3913
+ parallelGroupId;
3872
3914
  }
3873
3915
 
3874
3916
  return new Send(destination, cmd.update);