@librechat/agents 3.1.88 → 3.1.90

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/graphs/Graph.cjs +25 -1
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/hooks/executeHooks.cjs +14 -7
  4. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  5. package/dist/cjs/llm/anthropic/index.cjs +8 -2
  6. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  7. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +34 -0
  8. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  9. package/dist/cjs/main.cjs +9 -0
  10. package/dist/cjs/main.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +115 -8
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/tools/BashExecutor.cjs +10 -9
  14. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  15. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +12 -8
  16. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  17. package/dist/cjs/tools/CodeExecutor.cjs +35 -11
  18. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  19. package/dist/cjs/tools/CodeSessionFileSummary.cjs +63 -0
  20. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -0
  21. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -12
  22. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  23. package/dist/cjs/tools/ToolNode.cjs +32 -12
  24. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  25. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +319 -29
  26. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  27. package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
  28. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  29. package/dist/cjs/utils/events.cjs +3 -1
  30. package/dist/cjs/utils/events.cjs.map +1 -1
  31. package/dist/esm/graphs/Graph.mjs +25 -1
  32. package/dist/esm/graphs/Graph.mjs.map +1 -1
  33. package/dist/esm/hooks/executeHooks.mjs +14 -7
  34. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  35. package/dist/esm/llm/anthropic/index.mjs +9 -3
  36. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  37. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -1
  38. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  39. package/dist/esm/main.mjs +2 -1
  40. package/dist/esm/main.mjs.map +1 -1
  41. package/dist/esm/stream.mjs +115 -8
  42. package/dist/esm/stream.mjs.map +1 -1
  43. package/dist/esm/tools/BashExecutor.mjs +11 -10
  44. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  45. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +13 -9
  46. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  47. package/dist/esm/tools/CodeExecutor.mjs +29 -12
  48. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  49. package/dist/esm/tools/CodeSessionFileSummary.mjs +60 -0
  50. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -0
  51. package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -13
  52. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  53. package/dist/esm/tools/ToolNode.mjs +32 -12
  54. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  55. package/dist/esm/tools/subagent/SubagentExecutor.mjs +320 -31
  56. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  57. package/dist/esm/tools/toolOutputReferences.mjs +8 -1
  58. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  59. package/dist/esm/utils/events.mjs +3 -1
  60. package/dist/esm/utils/events.mjs.map +1 -1
  61. package/dist/types/graphs/Graph.d.ts +8 -0
  62. package/dist/types/llm/anthropic/index.d.ts +3 -1
  63. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +4 -0
  64. package/dist/types/tools/BashExecutor.d.ts +3 -3
  65. package/dist/types/tools/CodeExecutor.d.ts +10 -3
  66. package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
  67. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -4
  68. package/dist/types/tools/subagent/SubagentExecutor.d.ts +8 -5
  69. package/dist/types/types/tools.d.ts +11 -3
  70. package/dist/types/utils/events.d.ts +1 -1
  71. package/package.json +1 -1
  72. package/src/__tests__/stream.eagerEventExecution.test.ts +1073 -221
  73. package/src/graphs/Graph.ts +27 -5
  74. package/src/hooks/__tests__/executeHooks.test.ts +38 -0
  75. package/src/hooks/executeHooks.ts +27 -7
  76. package/src/llm/anthropic/index.ts +27 -3
  77. package/src/llm/anthropic/llm.spec.ts +60 -1
  78. package/src/llm/anthropic/utils/message_inputs.ts +46 -0
  79. package/src/specs/subagent.test.ts +87 -1
  80. package/src/stream.ts +163 -12
  81. package/src/tools/BashExecutor.ts +21 -10
  82. package/src/tools/BashProgrammaticToolCalling.ts +21 -9
  83. package/src/tools/CodeExecutor.ts +55 -12
  84. package/src/tools/CodeSessionFileSummary.ts +80 -0
  85. package/src/tools/ProgrammaticToolCalling.ts +25 -12
  86. package/src/tools/ToolNode.ts +142 -116
  87. package/src/tools/__tests__/BashExecutor.test.ts +9 -0
  88. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +43 -0
  89. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +100 -16
  90. package/src/tools/__tests__/SubagentExecutor.test.ts +540 -6
  91. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
  92. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +52 -0
  93. package/src/tools/__tests__/subagentHooks.test.ts +237 -0
  94. package/src/tools/subagent/SubagentExecutor.ts +514 -36
  95. package/src/types/tools.ts +11 -3
  96. package/src/utils/events.ts +4 -2
@@ -7,9 +7,13 @@ import type { ToolCall } from '@langchain/core/messages/tool';
7
7
  import type { ProgrammaticToolCallingJsonSchema } from './ptcTimeout';
8
8
  import type * as t from '@/types';
9
9
  import {
10
+ CODE_ARTIFACT_PATH_GUIDANCE,
11
+ appendCodeSessionFileSummary,
12
+ appendFailedExecutionFileReminder,
10
13
  buildCodeApiHttpErrorMessage,
11
14
  emptyOutputMessage,
12
15
  getCodeBaseURL,
16
+ appendTmpScratchReminder,
13
17
  resolveCodeApiAuthHeaders,
14
18
  } from './CodeExecutor';
15
19
  import {
@@ -36,15 +40,17 @@ You MUST complete your entire workflow in ONE code block: query → process →
36
40
  DO NOT split work across multiple calls expecting to reuse variables.`;
37
41
 
38
42
  const CORE_RULES = `Rules:
39
- - EVERYTHING in one call—no state persists between executions
40
- - Just write code with await—auto-wrapped in async context
41
- - DO NOT define async def main() or call asyncio.run()
43
+ - One call: state does not persist
44
+ - Auto-wrapped async; use await, no main()/asyncio.run()
42
45
  - Tools are pre-defined—DO NOT write function definitions
46
+ - Call tools with keyword args only (await tool(arg=value), never pass a dict)
47
+ - Tool results are decoded Python values (dict/list/str)
43
48
  - Only print() output returns to the model
49
+ - ${CODE_ARTIFACT_PATH_GUIDANCE}
44
50
  - timeout caps one sandbox run/replay iteration, not the total multi-round-trip workflow`;
45
51
 
46
- const ADDITIONAL_RULES = `- Generated files are automatically available in /mnt/data/ for subsequent executions
47
- - Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix`;
52
+ const ADDITIONAL_RULES =
53
+ '- Tool names normalized: hyphens→underscores, keywords get `_tool` suffix';
48
54
 
49
55
  const EXAMPLES = `Example (Complete workflow in one call):
50
56
  # Query data
@@ -678,15 +684,16 @@ export async function executeTools(
678
684
  /**
679
685
  * Formats the completed response for the agent.
680
686
  *
681
- * Output is stdout/stderr only see `CodeExecutor.ts`. The
682
- * artifact still carries every file so the host's session map
683
- * stays in sync; the LLM doesn't see them in the tool result text.
687
+ * Output includes stdout/stderr plus a compact session-file summary
688
+ * when artifacts were persisted. The artifact still carries every
689
+ * file so the host's session map stays in sync.
684
690
  *
685
691
  * @param response - The completed API response
686
692
  * @returns Tuple of [formatted string, artifact]
687
693
  */
688
694
  export function formatCompletedResponse(
689
- response: t.ProgrammaticExecutionResponse
695
+ response: t.ProgrammaticExecutionResponse,
696
+ sourceCode = ''
690
697
  ): [string, t.ProgrammaticExecutionArtifact] {
691
698
  let formatted = '';
692
699
 
@@ -700,8 +707,10 @@ export function formatCompletedResponse(
700
707
  formatted += `stderr:\n${response.stderr}\n`;
701
708
  }
702
709
 
710
+ const outputWithReminder = appendTmpScratchReminder(formatted, sourceCode);
711
+
703
712
  return [
704
- formatted.trim(),
713
+ appendCodeSessionFileSummary(outputWithReminder, response.files),
705
714
  {
706
715
  session_id: response.session_id,
707
716
  files: response.files,
@@ -859,7 +868,7 @@ export function createProgrammaticToolCallingTool(
859
868
  // ====================================================================
860
869
 
861
870
  if (response.status === 'completed') {
862
- return formatCompletedResponse(response);
871
+ return formatCompletedResponse(response, code);
863
872
  }
864
873
 
865
874
  if (response.status === 'error') {
@@ -873,8 +882,12 @@ export function createProgrammaticToolCallingTool(
873
882
 
874
883
  throw new Error(`Unexpected response status: ${response.status}`);
875
884
  } catch (error) {
885
+ const messageWithReminder = appendFailedExecutionFileReminder(
886
+ (error as Error).message,
887
+ code
888
+ );
876
889
  throw new Error(
877
- `Programmatic execution failed: ${(error as Error).message}`
890
+ `Programmatic execution failed: ${messageWithReminder}`
878
891
  );
879
892
  }
880
893
  },
@@ -46,6 +46,7 @@ import {
46
46
  buildReferenceKey,
47
47
  ToolOutputReferenceRegistry,
48
48
  } from '@/tools/toolOutputReferences';
49
+ import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
49
50
  import {
50
51
  resolveLocalToolRegistry,
51
52
  resolveLocalExecutionTools,
@@ -911,8 +912,9 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
911
912
  * Both session_id and _injected_files are injected directly to invokeParams
912
913
  * (not inside args) so they bypass Zod schema validation and reach config.toolCall.
913
914
  *
914
- * session_id is always injected when available (even without tracked files)
915
- * so the CodeExecutor can fall back to the /files endpoint for session continuity.
915
+ * session_id is always injected when available, but concrete file refs
916
+ * still need to travel through `_injected_files`; the legacy
917
+ * `/files/<session_id>` fallback was removed from the executors.
916
918
  */
917
919
  if (CODE_EXECUTION_TOOLS.has(call.name)) {
918
920
  const codeSession = this.sessions?.get(Constants.EXECUTE_CODE) as
@@ -959,6 +961,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
959
961
  if (this.toolOutputRegistry != null || unresolvedRefs.length > 0) {
960
962
  if (typeof toolMsg.content === 'string') {
961
963
  const rawContent = toolMsg.content;
964
+ const registryContent = stripCodeSessionFileSummary(rawContent);
962
965
  const llmContent = truncateToolResultContent(
963
966
  rawContent,
964
967
  this.maxToolResultChars
@@ -966,7 +969,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
966
969
  toolMsg.content = llmContent;
967
970
  const refMeta = this.recordOutputReference(
968
971
  runId,
969
- rawContent,
972
+ registryContent,
970
973
  refKey,
971
974
  unresolvedRefs
972
975
  );
@@ -1015,7 +1018,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1015
1018
  );
1016
1019
  const refMeta = this.recordOutputReference(
1017
1020
  runId,
1018
- rawContent,
1021
+ stripCodeSessionFileSummary(rawContent),
1019
1022
  refKey,
1020
1023
  unresolvedRefs
1021
1024
  );
@@ -1062,13 +1065,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1062
1065
  handlerError:
1063
1066
  handlerError instanceof Error
1064
1067
  ? {
1065
- message: handlerError.message,
1066
- stack: handlerError.stack ?? undefined,
1067
- }
1068
+ message: handlerError.message,
1069
+ stack: handlerError.stack ?? undefined,
1070
+ }
1068
1071
  : {
1069
- message: String(handlerError),
1070
- stack: undefined,
1071
- },
1072
+ message: String(handlerError),
1073
+ stack: undefined,
1074
+ },
1072
1075
  });
1073
1076
  }
1074
1077
  }
@@ -1076,11 +1079,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1076
1079
  const refMeta =
1077
1080
  unresolvedRefs.length > 0
1078
1081
  ? this.recordOutputReference(
1079
- runId,
1080
- errorContent,
1081
- undefined,
1082
- unresolvedRefs
1083
- )
1082
+ runId,
1083
+ errorContent,
1084
+ undefined,
1085
+ unresolvedRefs
1086
+ )
1084
1087
  : undefined;
1085
1088
  return new ToolMessage({
1086
1089
  status: 'error',
@@ -2432,59 +2435,77 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2432
2435
  dispatchRequests.length === 0
2433
2436
  ? Promise.resolve([] as t.ToolExecuteResult[])
2434
2437
  : new Promise<t.ToolExecuteResult[]>((resolve, reject) => {
2435
- let dispatchSettled = false;
2436
- let resultSettled = false;
2437
- let settledResults: t.ToolExecuteResult[] | undefined;
2438
+ let dispatchSettled = false;
2439
+ let resultSettled = false;
2440
+ let settledResults: t.ToolExecuteResult[] | undefined;
2438
2441
 
2439
- const maybeResolve = (): void => {
2440
- if (dispatchSettled && resultSettled) {
2441
- resolve(settledResults ?? []);
2442
- }
2443
- };
2442
+ const maybeResolve = (): void => {
2443
+ if (dispatchSettled && resultSettled) {
2444
+ resolve(settledResults ?? []);
2445
+ }
2446
+ };
2444
2447
 
2445
- const batchRequest: t.ToolExecuteBatchRequest = {
2446
- toolCalls: dispatchRequests,
2447
- userId: config.configurable?.user_id as string | undefined,
2448
- agentId: this.agentId,
2449
- configurable: config.configurable as
2448
+ const batchRequest: t.ToolExecuteBatchRequest = {
2449
+ toolCalls: dispatchRequests,
2450
+ userId: config.configurable?.user_id as string | undefined,
2451
+ agentId: this.agentId,
2452
+ configurable: config.configurable as
2450
2453
  | Record<string, unknown>
2451
2454
  | undefined,
2452
- metadata: config.metadata as
2455
+ metadata: config.metadata as
2453
2456
  | Record<string, unknown>
2454
2457
  | undefined,
2455
- resolve: (results): void => {
2456
- resultSettled = true;
2457
- settledResults = results;
2458
- maybeResolve();
2459
- },
2460
- reject,
2461
- };
2458
+ resolve: (results): void => {
2459
+ resultSettled = true;
2460
+ settledResults = results;
2461
+ maybeResolve();
2462
+ },
2463
+ reject,
2464
+ };
2462
2465
 
2463
- void safeDispatchCustomEvent(
2464
- GraphEvents.ON_TOOL_EXECUTE,
2465
- batchRequest,
2466
- config
2467
- )
2468
- .then(() => {
2469
- dispatchSettled = true;
2470
- maybeResolve();
2471
- })
2472
- .catch(reject);
2473
- });
2466
+ void safeDispatchCustomEvent(
2467
+ GraphEvents.ON_TOOL_EXECUTE,
2468
+ batchRequest,
2469
+ config
2470
+ )
2471
+ .then(() => {
2472
+ dispatchSettled = true;
2473
+ maybeResolve();
2474
+ })
2475
+ .catch(reject);
2476
+ });
2474
2477
 
2475
2478
  const eagerResultsPromise = Promise.all(
2476
- eagerExecutions.map(({ request, execution }) =>
2477
- this.resolveEagerEventExecution(request, execution)
2478
- )
2479
- ).then((results) => results.flat());
2479
+ eagerExecutions.map(async ({ request, execution }) => {
2480
+ const results = await this.resolveEagerEventExecution(
2481
+ request,
2482
+ execution
2483
+ );
2484
+ return {
2485
+ results,
2486
+ completionDispatched:
2487
+ execution.completionDispatched === true &&
2488
+ execution.request.turn === request.turn,
2489
+ toolCallId: request.id,
2490
+ };
2491
+ })
2492
+ );
2480
2493
 
2481
2494
  const [eagerResults, dispatchedResults] = await Promise.all([
2482
2495
  eagerResultsPromise,
2483
2496
  dispatchPromise,
2484
2497
  ]);
2498
+ const eagerCompletionDispatchedIds = new Set(
2499
+ eagerResults
2500
+ .filter((result) => result.completionDispatched)
2501
+ .map((result) => result.toolCallId)
2502
+ );
2503
+ const flattenedEagerResults = eagerResults.flatMap(
2504
+ (result) => result.results
2505
+ );
2485
2506
  const results = [
2486
2507
  ...plan.rejectedResults,
2487
- ...eagerResults,
2508
+ ...flattenedEagerResults,
2488
2509
  ...dispatchedResults,
2489
2510
  ];
2490
2511
 
@@ -2537,11 +2558,11 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2537
2558
  const errorRefMeta =
2538
2559
  unresolved.length > 0
2539
2560
  ? this.recordOutputReference(
2540
- registryRunId,
2541
- contentString,
2542
- undefined,
2543
- unresolved
2544
- )
2561
+ registryRunId,
2562
+ contentString,
2563
+ undefined,
2564
+ unresolved
2565
+ )
2545
2566
  : undefined;
2546
2567
  toolMessage = new ToolMessage({
2547
2568
  status: 'error',
@@ -2643,7 +2664,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2643
2664
  : undefined;
2644
2665
  const successRefMeta = this.recordOutputReference(
2645
2666
  registryRunId,
2646
- registryRaw,
2667
+ stripCodeSessionFileSummary(registryRaw),
2647
2668
  refKey,
2648
2669
  unresolved
2649
2670
  );
@@ -2660,14 +2681,16 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2660
2681
  });
2661
2682
  }
2662
2683
 
2663
- await this.dispatchStepCompleted(
2664
- result.toolCallId,
2665
- toolName,
2666
- request?.args ?? {},
2667
- contentString,
2668
- config,
2669
- request?.turn
2670
- );
2684
+ if (!eagerCompletionDispatchedIds.has(result.toolCallId)) {
2685
+ await this.dispatchStepCompleted(
2686
+ result.toolCallId,
2687
+ toolName,
2688
+ request?.args ?? {},
2689
+ contentString,
2690
+ config,
2691
+ request?.turn
2692
+ );
2693
+ }
2671
2694
 
2672
2695
  postToolBatchEntryByCallId.set(result.toolCallId, {
2673
2696
  toolName,
@@ -2706,8 +2729,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2706
2729
  this.eventDrivenMode &&
2707
2730
  this.eagerEventToolExecution?.enabled === true &&
2708
2731
  this.hookRegistry == null &&
2709
- this.humanInTheLoop?.enabled !== true &&
2710
- this.toolOutputRegistry == null
2732
+ this.humanInTheLoop?.enabled !== true
2711
2733
  );
2712
2734
  }
2713
2735
 
@@ -2725,10 +2747,14 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
2725
2747
 
2726
2748
  this.eagerEventToolExecutions?.delete(request.id);
2727
2749
 
2750
+ // Only tool identity + canonical args define side-effect identity here.
2751
+ // `request.turn` is final-planning metadata; if it drifts between the
2752
+ // streamed eager reservation and model-end materialization, consume the
2753
+ // same-name/same-args eager result and let the final request drive refs,
2754
+ // completion metadata, and PostToolBatch state.
2728
2755
  if (
2729
2756
  execution.toolName !== request.name ||
2730
- !recordArgsEqual(execution.args, request.args) ||
2731
- execution.request.turn !== request.turn
2757
+ !recordArgsEqual(execution.args, request.args)
2732
2758
  ) {
2733
2759
  return {
2734
2760
  toolCallId: request.id,
@@ -3015,15 +3041,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3015
3041
  outputs =
3016
3042
  directAdditionalContexts.length > 0
3017
3043
  ? [
3018
- sendOutput,
3019
- new HumanMessage({
3020
- content: directAdditionalContexts.join('\n\n'),
3021
- // Match the event-driven path's marker so hosts /
3022
- // model-side annotators treat this as system intent
3023
- // rather than ordinary user text. Codex P2 [46].
3024
- additional_kwargs: { role: 'system', source: 'hook' },
3025
- }),
3026
- ]
3044
+ sendOutput,
3045
+ new HumanMessage({
3046
+ content: directAdditionalContexts.join('\n\n'),
3047
+ // Match the event-driven path's marker so hosts /
3048
+ // model-side annotators treat this as system intent
3049
+ // rather than ordinary user text. Codex P2 [46].
3050
+ additional_kwargs: { role: 'system', source: 'hook' },
3051
+ }),
3052
+ ]
3027
3053
  : [sendOutput];
3028
3054
  await this.handleRunToolCompletions(
3029
3055
  [input.lg_tool_call],
@@ -3174,17 +3200,17 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3174
3200
  const directOutputs: (BaseMessage | Command)[] =
3175
3201
  directCalls.length > 0
3176
3202
  ? await Promise.all(
3177
- directCalls.map((call, i) =>
3178
- this.runDirectToolWithLifecycleHooks(call, config, {
3179
- batchIndex: directIndices[i],
3180
- turn,
3181
- batchScopeId,
3182
- resolvedArgsByCallId,
3183
- preBatchSnapshot,
3184
- additionalContextsSink: directAdditionalContexts,
3185
- })
3186
- )
3203
+ directCalls.map((call, i) =>
3204
+ this.runDirectToolWithLifecycleHooks(call, config, {
3205
+ batchIndex: directIndices[i],
3206
+ turn,
3207
+ batchScopeId,
3208
+ resolvedArgsByCallId,
3209
+ preBatchSnapshot,
3210
+ additionalContextsSink: directAdditionalContexts,
3211
+ })
3187
3212
  )
3213
+ )
3188
3214
  : [];
3189
3215
 
3190
3216
  if (directCalls.length > 0 && directOutputs.length > 0) {
@@ -3199,29 +3225,29 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3199
3225
  const eventResult =
3200
3226
  eventCalls.length > 0
3201
3227
  ? await this.dispatchToolEvents(eventCalls, config, {
3202
- batchIndices: eventIndices,
3203
- turn,
3204
- batchScopeId,
3205
- preResolvedArgs: preResolvedEventArgs,
3206
- preBatchSnapshot,
3207
- })
3228
+ batchIndices: eventIndices,
3229
+ turn,
3230
+ batchScopeId,
3231
+ preResolvedArgs: preResolvedEventArgs,
3232
+ preBatchSnapshot,
3233
+ })
3208
3234
  : {
3209
- toolMessages: [] as ToolMessage[],
3210
- injected: [] as BaseMessage[],
3211
- };
3235
+ toolMessages: [] as ToolMessage[],
3236
+ injected: [] as BaseMessage[],
3237
+ };
3212
3238
 
3213
3239
  const directInjected: BaseMessage[] =
3214
3240
  directAdditionalContexts.length > 0
3215
3241
  ? [
3216
- new HumanMessage({
3217
- content: directAdditionalContexts.join('\n\n'),
3218
- // System-role metadata to match the event-driven
3219
- // path so policy/recovery guidance is treated
3220
- // consistently regardless of whether the tool ran
3221
- // direct or dispatched. Codex P2 [46].
3222
- additional_kwargs: { role: 'system', source: 'hook' },
3223
- }),
3224
- ]
3242
+ new HumanMessage({
3243
+ content: directAdditionalContexts.join('\n\n'),
3244
+ // System-role metadata to match the event-driven
3245
+ // path so policy/recovery guidance is treated
3246
+ // consistently regardless of whether the tool ran
3247
+ // direct or dispatched. Codex P2 [46].
3248
+ additional_kwargs: { role: 'system', source: 'hook' },
3249
+ }),
3250
+ ]
3225
3251
  : [];
3226
3252
  outputs = [
3227
3253
  ...directOutputs,
@@ -3260,15 +3286,15 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3260
3286
  outputs =
3261
3287
  directAdditionalContexts.length > 0
3262
3288
  ? [
3263
- ...toolOutputs,
3264
- new HumanMessage({
3265
- content: directAdditionalContexts.join('\n\n'),
3266
- // Same system-role marker the event-driven path
3267
- // uses so direct vs dispatched is invisible to
3268
- // downstream consumers. Codex P2 [46].
3269
- additional_kwargs: { role: 'system', source: 'hook' },
3270
- }),
3271
- ]
3289
+ ...toolOutputs,
3290
+ new HumanMessage({
3291
+ content: directAdditionalContexts.join('\n\n'),
3292
+ // Same system-role marker the event-driven path
3293
+ // uses so direct vs dispatched is invisible to
3294
+ // downstream consumers. Codex P2 [46].
3295
+ additional_kwargs: { role: 'system', source: 'hook' },
3296
+ }),
3297
+ ]
3272
3298
  : toolOutputs;
3273
3299
  }
3274
3300
  }
@@ -18,6 +18,15 @@ describe('buildBashExecutionToolDescription', () => {
18
18
  ).toBe(BashExecutionToolDescription);
19
19
  });
20
20
 
21
+ it('warns about compact bash shell pitfalls', () => {
22
+ expect(BashExecutionToolDescription).toContain('heredoc/printf');
23
+ expect(BashExecutionToolDescription).toContain('not bare Python');
24
+ expect(BashExecutionToolDescription).toContain(
25
+ 'failed executions do not register new files'
26
+ );
27
+ expect(BashExecutionToolDescription).toContain('not later-call storage');
28
+ });
29
+
21
30
  it('appends the tool-output references guide when enabled', () => {
22
31
  const composed = buildBashExecutionToolDescription({
23
32
  enableToolOutputReferences: true,
@@ -165,6 +165,17 @@ describe('CodeAPI auth header injection', () => {
165
165
  ).not.toHaveProperty('authHeaders');
166
166
  });
167
167
 
168
+ it('tolerates null params for direct code execution', async () => {
169
+ fetchMock.mockResolvedValueOnce(
170
+ jsonResponse({ session_id: 'session_123', stdout: '1\n' })
171
+ );
172
+ const tool = createCodeExecutionTool(null);
173
+
174
+ await expect(
175
+ tool.invoke({ lang: 'py', code: 'print(1)' })
176
+ ).resolves.toBeDefined();
177
+ });
178
+
168
179
  it('forwards Authorization for bash execution', async () => {
169
180
  fetchMock.mockResolvedValueOnce(
170
181
  jsonResponse({ session_id: 'session_123', stdout: '1\n' })
@@ -333,6 +344,38 @@ describe('CodeAPI auth header injection', () => {
333
344
  );
334
345
  });
335
346
 
347
+ it('reminds that failed bash programmatic executions do not register new files', async () => {
348
+ fetchMock.mockResolvedValueOnce(
349
+ jsonResponse({
350
+ status: 'error',
351
+ error: 'jq failed',
352
+ stderr: 'jq: Cannot index string with string "name"',
353
+ })
354
+ );
355
+ const tool = createBashProgrammaticToolCallingTool();
356
+
357
+ await expect(
358
+ tool.invoke(
359
+ {
360
+ code: [
361
+ 'lookup_user "{}" > /mnt/data/user.json',
362
+ 'jq -r \'.result.name\' /mnt/data/user.json',
363
+ ].join('\n'),
364
+ },
365
+ {
366
+ toolCall: {
367
+ name: 'bash_programmatic_code_execution',
368
+ args: {},
369
+ toolMap: toolMap(),
370
+ toolDefs,
371
+ },
372
+ }
373
+ )
374
+ ).rejects.toThrow(
375
+ 'files written during this failed call were not registered for later calls'
376
+ );
377
+ });
378
+
336
379
  it('fetches session files with the CodeAPI resource scope and auth headers', async () => {
337
380
  fetchMock.mockResolvedValueOnce(
338
381
  jsonResponse([