@librechat/agents 3.7.20 → 3.7.22

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 (81) hide show
  1. package/dist/cjs/graphs/Graph.cjs +21 -4
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/invoke.cjs +21 -6
  4. package/dist/cjs/llm/invoke.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +2 -0
  6. package/dist/cjs/stream.cjs +38 -1
  7. package/dist/cjs/stream.cjs.map +1 -1
  8. package/dist/cjs/tools/BashExecutor.cjs +3 -2
  9. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  10. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +2 -1
  11. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  12. package/dist/cjs/tools/CodeExecutor.cjs +27 -10
  13. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  14. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +6 -5
  15. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  16. package/dist/cjs/tools/ToolNode.cjs +95 -18
  17. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  18. package/dist/cjs/tools/ToolSearch.cjs +130 -24
  19. package/dist/cjs/tools/ToolSearch.cjs.map +1 -1
  20. package/dist/cjs/tools/diagnostics.cjs +26 -0
  21. package/dist/cjs/tools/diagnostics.cjs.map +1 -0
  22. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +1 -1
  23. package/dist/cjs/tools/preparedSubagents.cjs +129 -0
  24. package/dist/cjs/tools/preparedSubagents.cjs.map +1 -0
  25. package/dist/cjs/tools/search/rerankers.cjs +6 -4
  26. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  27. package/dist/cjs/tools/search/search.cjs +14 -2
  28. package/dist/cjs/tools/search/search.cjs.map +1 -1
  29. package/dist/cjs/tools/search/tool.cjs +1 -1
  30. package/dist/esm/graphs/Graph.mjs +21 -4
  31. package/dist/esm/graphs/Graph.mjs.map +1 -1
  32. package/dist/esm/llm/invoke.mjs +21 -6
  33. package/dist/esm/llm/invoke.mjs.map +1 -1
  34. package/dist/esm/main.mjs +2 -2
  35. package/dist/esm/stream.mjs +39 -2
  36. package/dist/esm/stream.mjs.map +1 -1
  37. package/dist/esm/tools/BashExecutor.mjs +3 -2
  38. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  39. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +2 -1
  40. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  41. package/dist/esm/tools/CodeExecutor.mjs +27 -10
  42. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  43. package/dist/esm/tools/ProgrammaticToolCalling.mjs +6 -5
  44. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  45. package/dist/esm/tools/ToolNode.mjs +95 -18
  46. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  47. package/dist/esm/tools/ToolSearch.mjs +129 -25
  48. package/dist/esm/tools/ToolSearch.mjs.map +1 -1
  49. package/dist/esm/tools/diagnostics.mjs +25 -0
  50. package/dist/esm/tools/diagnostics.mjs.map +1 -0
  51. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +1 -1
  52. package/dist/esm/tools/preparedSubagents.mjs +128 -0
  53. package/dist/esm/tools/preparedSubagents.mjs.map +1 -0
  54. package/dist/esm/tools/search/rerankers.mjs +6 -5
  55. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  56. package/dist/esm/tools/search/search.mjs +14 -2
  57. package/dist/esm/tools/search/search.mjs.map +1 -1
  58. package/dist/esm/tools/search/tool.mjs +1 -1
  59. package/dist/types/graphs/Graph.d.ts +6 -1
  60. package/dist/types/tools/CodeExecutor.d.ts +13 -2
  61. package/dist/types/tools/ToolNode.d.ts +13 -1
  62. package/dist/types/tools/ToolSearch.d.ts +46 -2
  63. package/dist/types/tools/diagnostics.d.ts +52 -0
  64. package/dist/types/tools/preparedSubagents.d.ts +26 -0
  65. package/dist/types/tools/search/rerankers.d.ts +10 -0
  66. package/dist/types/types/tools.d.ts +17 -0
  67. package/package.json +3 -2
  68. package/src/graphs/Graph.ts +63 -5
  69. package/src/llm/invoke.ts +35 -8
  70. package/src/stream.ts +70 -1
  71. package/src/tools/BashExecutor.ts +9 -4
  72. package/src/tools/BashProgrammaticToolCalling.ts +6 -3
  73. package/src/tools/CodeExecutor.ts +88 -15
  74. package/src/tools/ProgrammaticToolCalling.ts +24 -12
  75. package/src/tools/ToolNode.ts +189 -43
  76. package/src/tools/ToolSearch.ts +278 -28
  77. package/src/tools/diagnostics.ts +107 -0
  78. package/src/tools/preparedSubagents.ts +228 -0
  79. package/src/tools/search/rerankers.ts +18 -3
  80. package/src/tools/search/search.ts +30 -5
  81. package/src/types/tools.ts +17 -0
package/src/llm/invoke.ts CHANGED
@@ -51,6 +51,7 @@ import {
51
51
  } from '@/llm/providers';
52
52
  import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
53
53
  import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
54
+ import { PreparedSubagentError } from '@/tools/preparedSubagents';
54
55
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
55
56
  import { resolveClientOptionsModel } from '@/llm/request';
56
57
  import { safeDispatchCustomEvent } from '@/utils/events';
@@ -702,9 +703,8 @@ export async function attemptInvoke(
702
703
  },
703
704
  };
704
705
  const request = resolveAttemptRequest(params, providerStampedConfig);
705
- const configuredModel = providerStampedConfig.metadata?.[
706
- Constants.INVOKED_MODEL
707
- ];
706
+ const configuredModel =
707
+ providerStampedConfig.metadata?.[Constants.INVOKED_MODEL];
708
708
  const modelId =
709
709
  request.modelId ??
710
710
  (typeof configuredModel === 'string' ? configuredModel : undefined);
@@ -745,8 +745,14 @@ export async function attemptInvoke(
745
745
  if (leaseTarget != null && generationKey != null) {
746
746
  registerActiveStreamLimitGeneration(leaseTarget, generationKey);
747
747
  }
748
+ const prepared =
749
+ params.context?.eagerEventToolExecution?.enabled === true
750
+ ? params.context.preparedSubagents
751
+ : undefined;
752
+ const preparedAttempt = resolveGenerationKey(stampedConfig.metadata);
753
+ prepared?.begin(preparedAttempt, stampedConfig);
748
754
  try {
749
- return await attemptInvokeBody(
755
+ const result = await attemptInvokeBody(
750
756
  {
751
757
  request,
752
758
  context: params.context,
@@ -755,7 +761,27 @@ export async function attemptInvoke(
755
761
  },
756
762
  stampedConfig
757
763
  );
764
+ const calls =
765
+ result.messages?.flatMap((message) =>
766
+ 'tool_calls' in message
767
+ ? ((message.tool_calls as ToolCall[] | undefined) ?? [])
768
+ : []
769
+ ) ?? [];
770
+ prepared?.finish(preparedAttempt, calls);
771
+ return result;
772
+ } catch (error) {
773
+ prepared?.finish(preparedAttempt, undefined, error);
774
+ throw error;
758
775
  } finally {
776
+ const chunks = params.context?.eagerEventToolCallChunks;
777
+ if (prepared != null && chunks != null) {
778
+ const prefix = `prepared:${preparedAttempt}\u0000`;
779
+ for (const key of chunks.keys()) {
780
+ if (key.startsWith(prefix)) {
781
+ chunks.delete(key);
782
+ }
783
+ }
784
+ }
759
785
  if (leaseTarget != null && generationKey != null) {
760
786
  releaseStreamLimitGeneration(leaseTarget, generationKey);
761
787
  }
@@ -1053,7 +1079,7 @@ async function attemptInvokeBody(
1053
1079
  const signal = config.signal;
1054
1080
  if (
1055
1081
  signal?.aborted === true &&
1056
- signal.reason instanceof StreamLimitExceededError
1082
+ (signal.reason instanceof StreamLimitExceededError || signal.reason instanceof PreparedSubagentError)
1057
1083
  ) {
1058
1084
  throw signal.reason;
1059
1085
  }
@@ -1290,6 +1316,7 @@ async function attemptInvokeBody(
1290
1316
  const ownAbort =
1291
1317
  restartRoute === 'aborted' &&
1292
1318
  !(error instanceof StreamLimitExceededError) &&
1319
+ !(error instanceof PreparedSubagentError) &&
1293
1320
  config.signal?.aborted !== true;
1294
1321
  if (!ownAbort) {
1295
1322
  throw error;
@@ -1639,7 +1666,7 @@ export async function tryFallbackProviders({
1639
1666
  * a run that must reject. Check before every fallback invocation. */
1640
1667
  if (
1641
1668
  config?.signal?.aborted === true &&
1642
- config.signal.reason instanceof StreamLimitExceededError
1669
+ (config.signal.reason instanceof StreamLimitExceededError || config.signal.reason instanceof PreparedSubagentError)
1643
1670
  ) {
1644
1671
  throw config.signal.reason;
1645
1672
  }
@@ -1673,7 +1700,7 @@ export async function tryFallbackProviders({
1673
1700
  * provider failure. Continuing would try the remaining fallbacks and a
1674
1701
  * succeeding one would resolve a run that must reject.
1675
1702
  */
1676
- if (e instanceof StreamLimitExceededError) {
1703
+ if (e instanceof StreamLimitExceededError || e instanceof PreparedSubagentError) {
1677
1704
  throw e;
1678
1705
  }
1679
1706
  /** A parallel sibling's trip aborts this branch's composed signal, and
@@ -1682,7 +1709,7 @@ export async function tryFallbackProviders({
1682
1709
  * abort. Rethrow the breaker's own reason instead. */
1683
1710
  if (
1684
1711
  config?.signal?.aborted === true &&
1685
- config.signal.reason instanceof StreamLimitExceededError
1712
+ (config.signal.reason instanceof StreamLimitExceededError || config.signal.reason instanceof PreparedSubagentError)
1686
1713
  ) {
1687
1714
  throw config.signal.reason;
1688
1715
  }
package/src/stream.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  requiresStreamLimitAccounting,
16
16
  StreamLimitExceededError,
17
17
  STREAM_LIMIT_EPOCH_KEY,
18
+ resolveGenerationKey,
18
19
  } from '@/llm/streamLimits';
19
20
  import {
20
21
  getStreamedToolCallSeal,
@@ -56,6 +57,7 @@ import {
56
57
  } from '@/utils/truncation';
57
58
  import { resolveToolOutcome, outcomeFieldsFromResult } from '@/tools/intentArg';
58
59
  import { TOOL_OUTPUT_REF_PATTERN } from '@/tools/toolOutputReferences';
60
+ import { PreparedSubagentError } from '@/tools/preparedSubagents';
59
61
  import { isReasoningContentBlock } from '@/messages/core';
60
62
  import { safeDispatchCustomEvent } from '@/utils/events';
61
63
  import { composeAbortSignals } from '@/utils/misc';
@@ -1319,6 +1321,65 @@ function startReadyStreamedEagerToolExecutions(args: {
1319
1321
  });
1320
1322
  }
1321
1323
 
1324
+ /** Shares provider sealing/parsing with event tools, but never relaxes their batch guard. */
1325
+ function startPreparedSubagents(
1326
+ graph: StandardGraph,
1327
+ agentContext: AgentContext | undefined,
1328
+ chunk: Partial<AIMessageChunk>,
1329
+ metadata?: Record<string, unknown>
1330
+ ): void {
1331
+ const attempt = resolveGenerationKey(metadata);
1332
+ if (
1333
+ (graph as Partial<StandardGraph>).canPrestartSubagents?.(agentContext) !==
1334
+ true ||
1335
+ graph.preparedSubagents.isOpen(attempt) !== true ||
1336
+ graph.hookRegistry?.hasResultAlteringHooks(
1337
+ graph.preparedSubagents.getConfig(attempt)?.configurable?.run_id
1338
+ ) === true
1339
+ ) {
1340
+ return;
1341
+ }
1342
+ let calls: ToolCall[];
1343
+ if (hasOnArrivalToolCallSeal(chunk) && (chunk.tool_calls?.length ?? 0) > 0) {
1344
+ calls = chunk.tool_calls!;
1345
+ } else {
1346
+ const seal = getStreamedToolCallSeal(chunk.response_metadata);
1347
+ const allowSequentialSeal =
1348
+ canPrestartSequentialStreamedToolChunks(agentContext) ||
1349
+ streamedToolCallAdapterAllowsSequentialSeal(chunk.response_metadata);
1350
+ if (!hasExplicitStreamedToolCallSeals(chunk) && !allowSequentialSeal) {
1351
+ return;
1352
+ }
1353
+ const stepKey = `prepared:${attempt}`;
1354
+ recordEagerToolCallChunks({
1355
+ graph,
1356
+ stepKey,
1357
+ toolCallChunks: chunk.tool_call_chunks ?? [],
1358
+ seal,
1359
+ });
1360
+ calls = getStreamedReadyToolCalls({
1361
+ graph,
1362
+ stepKey,
1363
+ toolCallChunks: chunk.tool_call_chunks,
1364
+ seal,
1365
+ allowSequentialSeal,
1366
+ });
1367
+ pruneEagerToolCallChunkStates({
1368
+ graph,
1369
+ stepKey,
1370
+ toolCallIds: new Set(calls.map((call) => call.id!)),
1371
+ });
1372
+ }
1373
+ for (const call of calls) {
1374
+ if (
1375
+ call.name === Constants.SUBAGENT &&
1376
+ !hasToolOutputReference(call.args)
1377
+ ) {
1378
+ graph.prestartSubagent(call, attempt, agentContext);
1379
+ }
1380
+ }
1381
+ }
1382
+
1322
1383
  export function getChunkContent({
1323
1384
  chunk,
1324
1385
  provider,
@@ -1597,7 +1658,7 @@ export class ChatModelStreamHandler implements t.EventHandler {
1597
1658
  if (
1598
1659
  eventBreaker != null &&
1599
1660
  eventBreaker.signal.aborted &&
1600
- eventBreaker.signal.reason instanceof StreamLimitExceededError
1661
+ (eventBreaker.signal.reason instanceof StreamLimitExceededError || eventBreaker.signal.reason instanceof PreparedSubagentError)
1601
1662
  ) {
1602
1663
  throw eventBreaker.signal.reason;
1603
1664
  }
@@ -1743,6 +1804,9 @@ export class ChatModelStreamHandler implements t.EventHandler {
1743
1804
  return;
1744
1805
  }
1745
1806
  throwIfRunBreakerTripped();
1807
+ if (hasOnArrivalToolCallSeal(chunk)) {
1808
+ startPreparedSubagents(graph, agentContext, chunk, metadata);
1809
+ }
1746
1810
  if (hasFinalToolCallSignal(chunk)) {
1747
1811
  startEagerToolExecutions({
1748
1812
  graph,
@@ -1839,6 +1903,11 @@ export class ChatModelStreamHandler implements t.EventHandler {
1839
1903
  }
1840
1904
  }
1841
1905
 
1906
+ if (!hasOnArrivalToolCallSeal(chunk) && !runScopeInvalidated()) {
1907
+ throwIfRunBreakerTripped();
1908
+ startPreparedSubagents(graph, agentContext, chunk, metadata);
1909
+ }
1910
+
1842
1911
  if (isEmptyContent) {
1843
1912
  return;
1844
1913
  }
@@ -18,6 +18,7 @@ import {
18
18
  resolveCodeApiAuthHeaders,
19
19
  selectRuntimeSessionHint,
20
20
  } from './CodeExecutor';
21
+ import { logCodeApiDiagnostic } from '@/tools/diagnostics';
21
22
  import { resolveFetchProxyAgent } from '@/utils/proxy';
22
23
  import { INTENT_PROPERTY } from '@/tools/intentArg';
23
24
  import { Constants } from '@/common';
@@ -249,9 +250,11 @@ function createBashExecutionTool(
249
250
  session_id.length > 0 &&
250
251
  !Array.isArray(postData.files)
251
252
  ) {
252
- // eslint-disable-next-line no-console
253
- console.debug(
254
- `[BashExecutor] No injected files for session_id=${session_id} — exec will run without input files`
253
+ logCodeApiDiagnostic(
254
+ 'BashExecutor',
255
+ 'debug',
256
+ 'session carried no injected files; exec will run without input files',
257
+ { files: 'none' }
255
258
  );
256
259
  }
257
260
 
@@ -278,7 +281,9 @@ function createBashExecutionTool(
278
281
  const response = await fetch(execEndpoint, fetchOptions);
279
282
  if (!response.ok) {
280
283
  throw new CodeApiRequestError(
281
- await buildCodeApiHttpErrorMessage('POST', execEndpoint, response)
284
+ await buildCodeApiHttpErrorMessage('POST', execEndpoint, response, {
285
+ profile: executionProfile,
286
+ })
282
287
  );
283
288
  }
284
289
 
@@ -31,6 +31,7 @@ import {
31
31
  runPlainExecution,
32
32
  formatCompletedResponse,
33
33
  } from './ProgrammaticToolCalling';
34
+ import { logCodeApiDiagnostic } from '@/tools/diagnostics';
34
35
  import { INTENT_PROPERTY } from '@/tools/intentArg';
35
36
  import { Constants } from '@/common';
36
37
 
@@ -423,9 +424,11 @@ export function createBashProgrammaticToolCallingTool(
423
424
  if (_injected_files && _injected_files.length > 0) {
424
425
  files = _injected_files;
425
426
  } else if (session_id != null && session_id.length > 0) {
426
- // eslint-disable-next-line no-console
427
- console.debug(
428
- `[BashProgrammaticToolCalling] No injected files for session_id=${session_id} — exec will run without input files`
427
+ logCodeApiDiagnostic(
428
+ 'BashProgrammaticToolCalling',
429
+ 'debug',
430
+ 'session carried no injected files; exec will run without input files',
431
+ { files: 'none' }
429
432
  );
430
433
  }
431
434
 
@@ -2,7 +2,13 @@ import { config } from 'dotenv';
2
2
  import fetch, { RequestInit } from 'node-fetch';
3
3
  import { getEnvironmentVariable } from '@langchain/core/utils/env';
4
4
  import { tool, DynamicStructuredTool } from '@langchain/core/tools';
5
+ import type { Readable } from 'node:stream';
6
+ import type { CodeApiMethod } from '@/tools/diagnostics';
5
7
  import type * as t from '@/types';
8
+ import {
9
+ describeCodeApiError,
10
+ logCodeApiDiagnostic,
11
+ } from '@/tools/diagnostics';
6
12
  import { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
7
13
  import { resolveFetchProxyAgent } from '@/utils/proxy';
8
14
  import { INTENT_PROPERTY } from '@/tools/intentArg';
@@ -119,8 +125,7 @@ export const CodeExecutionToolSchema = {
119
125
  required: ['lang', 'code'],
120
126
  } as const;
121
127
 
122
- export const CODE_API_EXPECTED_PROFILE_HEADER =
123
- 'X-CodeAPI-Expected-Profile';
128
+ export const CODE_API_EXPECTED_PROFILE_HEADER = 'X-CodeAPI-Expected-Profile';
124
129
 
125
130
  type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];
126
131
 
@@ -221,7 +226,8 @@ export function buildCodeApiExecutionErrorMessage(response: {
221
226
  }
222
227
 
223
228
  export async function resolveCodeApiAuthHeaders(
224
- authHeaders?: t.CodeApiAuthHeaders
229
+ authHeaders?: t.CodeApiAuthHeaders,
230
+ options?: { recoverable?: boolean }
225
231
  ): Promise<t.CodeApiAuthHeaderMap> {
226
232
  if (authHeaders == null) {
227
233
  return {};
@@ -230,7 +236,15 @@ export async function resolveCodeApiAuthHeaders(
230
236
  try {
231
237
  const resolvedHeaders = await authHeaders();
232
238
  return resolvedHeaders;
233
- } catch {
239
+ } catch (error) {
240
+ if (options?.recoverable !== true) {
241
+ logCodeApiDiagnostic(
242
+ 'CodeExecutor',
243
+ 'error',
244
+ 'auth header resolution failed; the Code API request was never sent',
245
+ describeCodeApiError(error)
246
+ );
247
+ }
234
248
  throw new CodeApiRequestError(CODE_API_AUTHORIZATION_ERROR_MESSAGE);
235
249
  }
236
250
  }
@@ -250,18 +264,73 @@ export function addCodeApiExecutionProfileHeader(
250
264
  };
251
265
  }
252
266
 
267
+ type CodeApiErrorResponse = {
268
+ status: number;
269
+ text: () => Promise<string>;
270
+ /** node-fetch types this as `NodeJS.ReadableStream`, which does not declare
271
+ * `destroy`; every runtime instance is a `Readable`, and a test double may
272
+ * supply nothing at all. */
273
+ body?: NodeJS.ReadableStream | null;
274
+ };
275
+
276
+ /**
277
+ * Only the 429 branch reads the body, and node-fetch keeps the stream and its
278
+ * socket alive until something does. Repeated backend failures would otherwise
279
+ * accumulate connections holding payloads this module deliberately discards.
280
+ */
281
+ function discardResponseBody(response: CodeApiErrorResponse): void {
282
+ const body = response.body;
283
+ if (body == null || !('destroy' in body)) {
284
+ return;
285
+ }
286
+ const destroy = (body as NodeJS.ReadableStream & Partial<Readable>).destroy;
287
+ if (typeof destroy !== 'function') {
288
+ return;
289
+ }
290
+ try {
291
+ destroy.call(body);
292
+ } catch {
293
+ /* Already finished or not destroyable; nothing is being held open. */
294
+ }
295
+ }
296
+
253
297
  export async function buildCodeApiHttpErrorMessage(
254
- _method: string,
298
+ method: CodeApiMethod,
255
299
  _endpoint: string,
256
- response: { status: number; text: () => Promise<string> }
300
+ response: CodeApiErrorResponse,
301
+ options?: { recoverable?: boolean; profile?: t.CodeApiExecutionProfile }
257
302
  ): Promise<string> {
258
- let responseBody = '';
259
- try {
260
- responseBody = await response.text();
261
- } catch {
262
- responseBody = '';
303
+ /* Logged before the body is touched. A non-OK response can leave a chunked
304
+ body open, and there is no read timeout here, so draining first would
305
+ withhold the diagnostic indefinitely for exactly the backend it identifies.
306
+ The body itself is never logged — it is upstream free text that can echo
307
+ the header that was sent — and the endpoint is host-configured, so the
308
+ backend is named by the profile this module chose rather than by its
309
+ address. A recoverable caller reports its own outcome; logging here too
310
+ would raise an error-level alert for a lookup that succeeds by returning
311
+ nothing. */
312
+ if (options?.recoverable !== true) {
313
+ logCodeApiDiagnostic(
314
+ 'CodeExecutor',
315
+ response.status === 429 ? 'warn' : 'error',
316
+ 'Code API rejected the request',
317
+ {
318
+ method,
319
+ profile: options?.profile ?? 'unset',
320
+ status: response.status,
321
+ }
322
+ );
323
+ }
324
+ if (response.status !== 429) {
325
+ discardResponseBody(response);
263
326
  }
264
327
  if (response.status === 429) {
328
+ let responseBody = '';
329
+ try {
330
+ responseBody = await response.text();
331
+ } catch {
332
+ responseBody = '';
333
+ }
265
334
  const retryAfterSeconds = getRetryAfterSeconds(responseBody);
266
335
  return retryAfterSeconds != null
267
336
  ? `Code execution is temporarily rate-limited. Retry after ${retryAfterSeconds} seconds.`
@@ -445,9 +514,11 @@ function createCodeExecutionTool(
445
514
  session_id.length > 0 &&
446
515
  !Array.isArray(postData.files)
447
516
  ) {
448
- // eslint-disable-next-line no-console
449
- console.debug(
450
- `[CodeExecutor] No injected files for session_id=${session_id} — exec will run without input files`
517
+ logCodeApiDiagnostic(
518
+ 'CodeExecutor',
519
+ 'debug',
520
+ 'session carried no injected files; exec will run without input files',
521
+ { files: 'none' }
451
522
  );
452
523
  }
453
524
 
@@ -474,7 +545,9 @@ function createCodeExecutionTool(
474
545
  const response = await fetch(execEndpoint, fetchOptions);
475
546
  if (!response.ok) {
476
547
  throw new CodeApiRequestError(
477
- await buildCodeApiHttpErrorMessage('POST', execEndpoint, response)
548
+ await buildCodeApiHttpErrorMessage('POST', execEndpoint, response, {
549
+ profile: executionProfile,
550
+ })
478
551
  );
479
552
  }
480
553
 
@@ -33,6 +33,10 @@ import {
33
33
  createCodeApiRunTimeoutSchema,
34
34
  resolveCodeApiRunTimeoutMs,
35
35
  } from './ptcTimeout';
36
+ import {
37
+ describeCodeApiError,
38
+ logCodeApiDiagnostic,
39
+ } from '@/tools/diagnostics';
36
40
  import { resolveFetchProxyAgent } from '@/utils/proxy';
37
41
  import { INTENT_PROPERTY } from '@/tools/intentArg';
38
42
  import { Constants } from '@/common';
@@ -449,7 +453,9 @@ export async function fetchSessionFiles(
449
453
  }
450
454
  }
451
455
  const filesEndpoint = `${baseUrl}/files/${encodeURIComponent(sessionId)}?${query.toString()}`;
452
- const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders);
456
+ const resolvedAuthHeaders = await resolveCodeApiAuthHeaders(authHeaders, {
457
+ recoverable: true,
458
+ });
453
459
  const fetchOptions: RequestInit = {
454
460
  method: 'GET',
455
461
  headers: {
@@ -466,7 +472,9 @@ export async function fetchSessionFiles(
466
472
  const response = await fetch(filesEndpoint, fetchOptions);
467
473
  if (!response.ok) {
468
474
  throw new Error(
469
- await buildCodeApiHttpErrorMessage('GET', filesEndpoint, response)
475
+ await buildCodeApiHttpErrorMessage('GET', filesEndpoint, response, {
476
+ recoverable: true,
477
+ })
470
478
  );
471
479
  }
472
480
 
@@ -479,9 +487,11 @@ export async function fetchSessionFiles(
479
487
  .filter(isCodeApiSessionFileWire)
480
488
  .map((file) => normalizeSessionFile(file, sessionId, scope));
481
489
  } catch (error) {
482
- // eslint-disable-next-line no-console
483
- console.warn(
484
- `Failed to fetch files for session: ${sessionId}, ${(error as Error).message}`
490
+ logCodeApiDiagnostic(
491
+ 'ProgrammaticToolCalling',
492
+ 'warn',
493
+ 'session file lookup failed; continuing without input files',
494
+ describeCodeApiError(error)
485
495
  );
486
496
  return [];
487
497
  }
@@ -525,7 +535,9 @@ export async function makeRequest(
525
535
 
526
536
  if (!response.ok) {
527
537
  throw new CodeApiRequestError(
528
- await buildCodeApiHttpErrorMessage('POST', endpoint, response)
538
+ await buildCodeApiHttpErrorMessage('POST', endpoint, response, {
539
+ profile: executionProfile,
540
+ })
529
541
  );
530
542
  }
531
543
 
@@ -931,9 +943,7 @@ export async function runPlainExecution(args: {
931
943
  {
932
944
  lang: args.lang,
933
945
  code:
934
- args.lang === 'py'
935
- ? wrapPythonForPlainExecution(args.code)
936
- : args.code,
946
+ args.lang === 'py' ? wrapPythonForPlainExecution(args.code) : args.code,
937
947
  ...(args.timeout != null ? { timeout: args.timeout } : {}),
938
948
  ...(args.sessionId != null && args.sessionId !== ''
939
949
  ? { session_id: args.sessionId }
@@ -1085,9 +1095,11 @@ export function createProgrammaticToolCallingTool(
1085
1095
  if (_injected_files && _injected_files.length > 0) {
1086
1096
  files = _injected_files;
1087
1097
  } else if (session_id != null && session_id.length > 0) {
1088
- // eslint-disable-next-line no-console
1089
- console.debug(
1090
- `[ProgrammaticToolCalling] No injected files for session_id=${session_id} — exec will run without input files`
1098
+ logCodeApiDiagnostic(
1099
+ 'ProgrammaticToolCalling',
1100
+ 'debug',
1101
+ 'session carried no injected files; exec will run without input files',
1102
+ { files: 'none' }
1091
1103
  );
1092
1104
  }
1093
1105