@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
@@ -145,6 +145,14 @@ export abstract class Graph<
145
145
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
146
146
  invokedToolIds?: Set<string>;
147
147
  handlerRegistry: HandlerRegistry | undefined;
148
+ /**
149
+ * True when event-driven tool execution can be routed through callbacks even
150
+ * though this graph intentionally does not own the full handler registry.
151
+ * Self-spawned subagent graphs use this shape: their callback forwarder sends
152
+ * `ON_TOOL_EXECUTE` to the parent's handler, while child run-step events stay
153
+ * wrapped as `ON_SUBAGENT_UPDATE` instead of leaking as parent events.
154
+ */
155
+ eventToolExecutionAvailable: boolean = false;
148
156
  hookRegistry: HookRegistry | undefined;
149
157
  /**
150
158
  * Run-scoped HITL configuration. When `humanInTheLoop?.enabled` is
@@ -167,10 +175,8 @@ export abstract class Graph<
167
175
  eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
168
176
  eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
169
177
  eagerEventToolUsageCount: Map<string, number> = new Map();
170
- private eagerEventToolUsageCountsByAgentId: Map<
171
- string,
172
- Map<string, number>
173
- > = new Map();
178
+ private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
179
+ new Map();
174
180
  eagerEventToolCallChunks: Map<string, t.EagerEventToolCallChunkState> =
175
181
  new Map();
176
182
  /**
@@ -1554,7 +1560,23 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1554
1560
  parentAgentId: agentContext.agentId,
1555
1561
  tokenCounter: agentContext.tokenCounter,
1556
1562
  maxDepth: effectiveSubagentDepth,
1557
- createChildGraph: (input): StandardGraph => new StandardGraph(input),
1563
+ createChildGraph: (input): StandardGraph => {
1564
+ const childGraph = new StandardGraph(input);
1565
+ childGraph.hookRegistry = this.hookRegistry;
1566
+ /**
1567
+ * Do not propagate `humanInTheLoop` into the child graph yet:
1568
+ * nested subagent interrupts need a stable child checkpoint and
1569
+ * resume bridge. Child hooks still fire; `ask` decisions fail
1570
+ * closed inside the subagent until that flow is implemented.
1571
+ */
1572
+ childGraph.toolOutputReferences = this.toolOutputReferences;
1573
+ childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
1574
+ childGraph.toolExecution = this.toolExecution;
1575
+ childGraph.eventToolExecutionAvailable =
1576
+ this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
1577
+ null;
1578
+ return childGraph;
1579
+ },
1558
1580
  });
1559
1581
 
1560
1582
  const subagentTool = tool(async (rawInput, config) => {
@@ -109,6 +109,44 @@ describe('executeHooks', () => {
109
109
  consoleWarnSpy.mockRestore();
110
110
  });
111
111
 
112
+ describe('abort listener management', () => {
113
+ it('uses one abort listener for many hooks on one matcher', async () => {
114
+ const registry = new HookRegistry();
115
+ const listenerCounts = new Map<AbortSignal, number>();
116
+ let maxAbortListeners = 0;
117
+ const addEventListenerSpy = jest
118
+ .spyOn(AbortSignal.prototype, 'addEventListener')
119
+ .mockImplementation(function (
120
+ this: AbortSignal,
121
+ type: string,
122
+ _listener: EventListenerOrEventListenerObject | null
123
+ ): void {
124
+ if (type !== 'abort') {
125
+ return;
126
+ }
127
+ const count = (listenerCounts.get(this) ?? 0) + 1;
128
+ listenerCounts.set(this, count);
129
+ maxAbortListeners = Math.max(maxAbortListeners, count);
130
+ });
131
+ const hooks = Array.from({ length: 12 }, () =>
132
+ runStartHook(async (): Promise<RunStartHookOutput> => ({}))
133
+ );
134
+
135
+ try {
136
+ registry.register('RunStart', { hooks });
137
+
138
+ await executeHooks({
139
+ registry,
140
+ input: runStartInput(),
141
+ timeoutMs: 1000,
142
+ });
143
+ expect(maxAbortListeners).toBe(1);
144
+ } finally {
145
+ addEventListenerSpy.mockRestore();
146
+ }
147
+ });
148
+ });
149
+
112
150
  describe('empty matcher set', () => {
113
151
  it('returns an empty aggregated result when no matchers are registered', async () => {
114
152
  const registry = new HookRegistry();
@@ -46,6 +46,11 @@ interface HookOutcome {
46
46
  timedOut: boolean;
47
47
  }
48
48
 
49
+ interface AbortRace {
50
+ promise: Promise<never>;
51
+ cleanup: () => void;
52
+ }
53
+
49
54
  function freshResult(): AggregatedHookResult {
50
55
  return {
51
56
  additionalContexts: [],
@@ -110,10 +115,10 @@ async function runHook(
110
115
  hook: WideCallback,
111
116
  input: HookInput,
112
117
  signal: AbortSignal,
118
+ abortPromise: Promise<never>,
113
119
  matcher: WideMatcher
114
120
  ): Promise<HookOutcome> {
115
121
  const hookPromise = Promise.resolve().then(() => hook(input, signal));
116
- const { promise: abortPromise, cleanup } = makeAbortPromise(signal);
117
122
  try {
118
123
  const output = await Promise.race([hookPromise, abortPromise]);
119
124
  return { matcher, output, error: null, timedOut: false };
@@ -124,8 +129,22 @@ async function runHook(
124
129
  error: describeError(err),
125
130
  timedOut: isTimeout(err),
126
131
  };
132
+ }
133
+ }
134
+
135
+ async function runMatcherHooks(
136
+ matcher: WideMatcher,
137
+ input: HookInput,
138
+ signal: AbortSignal
139
+ ): Promise<HookOutcome[]> {
140
+ const abortRace: AbortRace = makeAbortPromise(signal);
141
+ const tasks = matcher.hooks.map((hook) =>
142
+ runHook(hook, input, signal, abortRace.promise, matcher)
143
+ );
144
+ try {
145
+ return await Promise.all(tasks);
127
146
  } finally {
128
- cleanup();
147
+ abortRace.cleanup();
129
148
  }
130
149
  }
131
150
 
@@ -373,7 +392,7 @@ export async function executeHooks(
373
392
  }
374
393
 
375
394
  // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---
376
- const tasks: Promise<HookOutcome>[] = [];
395
+ const tasks: Promise<HookOutcome[]>[] = [];
377
396
  for (const matcher of matchers) {
378
397
  if (!matchesQuery(matcher.pattern, matchQuery)) {
379
398
  continue;
@@ -381,18 +400,19 @@ export async function executeHooks(
381
400
  if (matcher.once === true) {
382
401
  registry.removeMatcher(event, matcher, sessionId);
383
402
  }
403
+ if (matcher.hooks.length === 0) {
404
+ continue;
405
+ }
384
406
  const perHookTimeout = matcher.timeout ?? timeoutMs;
385
407
  const matcherSignal = combineSignals(signal, perHookTimeout);
386
- for (const hook of matcher.hooks) {
387
- tasks.push(runHook(hook, input, matcherSignal, matcher));
388
- }
408
+ tasks.push(runMatcherHooks(matcher, input, matcherSignal));
389
409
  }
390
410
  // --- END SYNC CRITICAL SECTION ---
391
411
  if (tasks.length === 0) {
392
412
  return freshResult();
393
413
  }
394
414
 
395
- const outcomes = await Promise.all(tasks);
415
+ const outcomes = (await Promise.all(tasks)).flat();
396
416
  reportErrors(outcomes, event, logger);
397
417
  const aggregated = fold(outcomes);
398
418
  /**
@@ -17,9 +17,13 @@ import type {
17
17
  ChatAnthropicToolType,
18
18
  AnthropicMCPServerURLDefinition,
19
19
  AnthropicContextManagementConfigParam,
20
+ AnthropicRequestOptions,
20
21
  } from '@/llm/anthropic/types';
21
22
  import { _makeMessageChunkFromAnthropicEvent } from './utils/message_outputs';
22
- import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
23
+ import {
24
+ _convertMessagesToAnthropicPayload,
25
+ stripUnsupportedAssistantPrefill,
26
+ } from './utils/message_inputs';
23
27
  import { handleToolChoice } from './utils/tools';
24
28
 
25
29
  const DEFAULT_STREAM_DELAY = 25;
@@ -591,6 +595,26 @@ export class CustomAnthropic extends ChatAnthropicMessages {
591
595
  });
592
596
  }
593
597
 
598
+ protected override async createStreamWithRetry(
599
+ request: AnthropicStreamingMessageCreateParams,
600
+ options?: AnthropicRequestOptions
601
+ ): ReturnType<ChatAnthropicMessages['createStreamWithRetry']> {
602
+ return super.createStreamWithRetry(
603
+ stripUnsupportedAssistantPrefill(request),
604
+ options
605
+ );
606
+ }
607
+
608
+ protected override async completionWithRetry(
609
+ request: AnthropicMessageCreateParams,
610
+ options: AnthropicRequestOptions
611
+ ): ReturnType<ChatAnthropicMessages['completionWithRetry']> {
612
+ return super.completionWithRetry(
613
+ stripUnsupportedAssistantPrefill(request),
614
+ options
615
+ );
616
+ }
617
+
594
618
  async *_streamResponseChunks(
595
619
  messages: BaseMessage[],
596
620
  options: this['ParsedCallOptions'],
@@ -599,11 +623,11 @@ export class CustomAnthropic extends ChatAnthropicMessages {
599
623
  this.resetTokenEvents();
600
624
  const params = this.invocationParams(options);
601
625
  const formattedMessages = _convertMessagesToAnthropicPayload(messages);
602
- const payload = {
626
+ const payload = stripUnsupportedAssistantPrefill({
603
627
  ...params,
604
628
  ...formattedMessages,
605
629
  stream: true,
606
- } as const;
630
+ } as const);
607
631
  const coerceContentToString =
608
632
  !_toolsInParams(payload) &&
609
633
  !_documentsInParams(payload) &&
@@ -64,7 +64,11 @@ import type {
64
64
  ToolEndEvent,
65
65
  TPayload,
66
66
  } from '@/types';
67
- import { _convertMessagesToAnthropicPayload } from './utils/message_inputs';
67
+ import {
68
+ _convertMessagesToAnthropicPayload,
69
+ modelDisallowsAssistantPrefill,
70
+ stripUnsupportedAssistantPrefill,
71
+ } from './utils/message_inputs';
68
72
  import {
69
73
  _makeMessageChunkFromAnthropicEvent,
70
74
  getAnthropicUsageMetadata,
@@ -2637,6 +2641,61 @@ describe('Anthropic Reasoning with contentBlocks', () => {
2637
2641
  });
2638
2642
  });
2639
2643
 
2644
+ describe('Claude assistant prefill compatibility', () => {
2645
+ test.each([
2646
+ 'claude-sonnet-4-6',
2647
+ 'claude-sonnet-4-6@20260217',
2648
+ 'claude-opus-4-7',
2649
+ 'claude-opus-4-10',
2650
+ 'global.anthropic.claude-opus-4-6-v1:0',
2651
+ 'anthropic/claude-sonnet-4.6',
2652
+ 'anthropic/claude-sonnet-4.12',
2653
+ ])('detects %s as not supporting assistant prefill', (model) => {
2654
+ expect(modelDisallowsAssistantPrefill(model)).toBe(true);
2655
+ });
2656
+
2657
+ test.each([
2658
+ 'claude-sonnet-4-5-20250929',
2659
+ 'claude-opus-4-20250514',
2660
+ 'anthropic.claude-opus-4-20250514-v1:0',
2661
+ 'gpt-5.4',
2662
+ ])('leaves %s prefill support unchanged', (model) => {
2663
+ expect(modelDisallowsAssistantPrefill(model)).toBe(false);
2664
+ });
2665
+
2666
+ test('strips trailing assistant messages for Claude 4.6+ requests', () => {
2667
+ const request = {
2668
+ model: 'claude-opus-4-6',
2669
+ max_tokens: 100,
2670
+ messages: [
2671
+ { role: 'user' as const, content: 'What changed?' },
2672
+ { role: 'assistant' as const, content: 'Draft prefill' },
2673
+ { role: 'assistant' as const, content: 'Another prefill' },
2674
+ ],
2675
+ };
2676
+
2677
+ const sanitized = stripUnsupportedAssistantPrefill(request);
2678
+
2679
+ expect(sanitized).not.toBe(request);
2680
+ expect(sanitized.messages).toEqual([
2681
+ { role: 'user', content: 'What changed?' },
2682
+ ]);
2683
+ });
2684
+
2685
+ test('does not strip assistant messages for older Claude models', () => {
2686
+ const request = {
2687
+ model: 'claude-sonnet-4-5-20250929',
2688
+ max_tokens: 100,
2689
+ messages: [
2690
+ { role: 'user' as const, content: 'Write JSON only.' },
2691
+ { role: 'assistant' as const, content: '{' },
2692
+ ],
2693
+ };
2694
+
2695
+ expect(stripUnsupportedAssistantPrefill(request)).toBe(request);
2696
+ });
2697
+ });
2698
+
2640
2699
  const opus46Model = 'claude-opus-4-6';
2641
2700
 
2642
2701
  describe('Opus 4.6', () => {
@@ -49,6 +49,10 @@ type GoogleFunctionCallBlock = MessageContentComplex & {
49
49
  };
50
50
 
51
51
  const ANTHROPIC_EMPTY_TEXT_PLACEHOLDER = '_';
52
+ const CLAUDE_4_RELEASE_DATE_MODEL_PATTERN =
53
+ /claude-(?:opus|sonnet|haiku)-4-\d{8}(?:[-.@]|$)/i;
54
+ const CLAUDE_4_MINOR_MODEL_PATTERN =
55
+ /claude-(?:opus|sonnet|haiku)-4[-.](\d+)(?:[-.@]|$)/i;
52
56
 
53
57
  function _formatImage(imageUrl: string) {
54
58
  const parsed = parseBase64DataUrl({ dataUrl: imageUrl });
@@ -796,6 +800,48 @@ export function _convertMessagesToAnthropicPayload(
796
800
  } as AnthropicMessageCreateParams;
797
801
  }
798
802
 
803
+ export function modelDisallowsAssistantPrefill(model?: string): boolean {
804
+ const modelId = model ?? '';
805
+ if (CLAUDE_4_RELEASE_DATE_MODEL_PATTERN.test(modelId)) {
806
+ return false;
807
+ }
808
+
809
+ const match = CLAUDE_4_MINOR_MODEL_PATTERN.exec(modelId);
810
+ if (!match) {
811
+ return false;
812
+ }
813
+ return Number(match[1]) >= 6;
814
+ }
815
+
816
+ export function stripUnsupportedAssistantPrefill<
817
+ T extends Pick<AnthropicMessageCreateParams, 'messages'> & { model?: string },
818
+ >(request: T): T {
819
+ if (!modelDisallowsAssistantPrefill(request.model)) {
820
+ return request;
821
+ }
822
+
823
+ const messages = request.messages;
824
+ if (
825
+ messages.length <= 1 ||
826
+ messages[messages.length - 1]?.role !== 'assistant'
827
+ ) {
828
+ return request;
829
+ }
830
+
831
+ const nextMessages = [...messages];
832
+ while (
833
+ nextMessages.length > 1 &&
834
+ nextMessages[nextMessages.length - 1]?.role === 'assistant'
835
+ ) {
836
+ nextMessages.pop();
837
+ }
838
+
839
+ return {
840
+ ...request,
841
+ messages: nextMessages,
842
+ };
843
+ }
844
+
799
845
  function mergeMessages(messages: AnthropicMessageCreateParams['messages']) {
800
846
  if (messages.length <= 1) {
801
847
  return messages;
@@ -1,4 +1,4 @@
1
- import { HumanMessage } from '@langchain/core/messages';
1
+ import { AIMessage, HumanMessage } from '@langchain/core/messages';
2
2
  import { FakeListChatModel } from '@langchain/core/utils/testing';
3
3
  import type { ToolCall } from '@langchain/core/messages/tool';
4
4
  import type { RunnableConfig } from '@langchain/core/runnables';
@@ -220,6 +220,92 @@ describe('Subagent Integration', () => {
220
220
  expect(subagentTool).toBeDefined();
221
221
  });
222
222
 
223
+ it('inherits eager event-tool settings into self-spawn child graphs', async () => {
224
+ const originalCreateWorkflow = StandardGraph.prototype.createWorkflow;
225
+ const observedChildGraphs: Array<{
226
+ eagerEventToolExecution: StandardGraph['eagerEventToolExecution'];
227
+ toolOutputReferences: StandardGraph['toolOutputReferences'];
228
+ eventToolExecutionAvailable: boolean;
229
+ }> = [];
230
+ const createWorkflowSpy = jest
231
+ .spyOn(StandardGraph.prototype, 'createWorkflow')
232
+ .mockImplementation(function (this: StandardGraph) {
233
+ if (this.runId?.includes('_sub_') === true) {
234
+ observedChildGraphs.push({
235
+ eagerEventToolExecution: this.eagerEventToolExecution,
236
+ toolOutputReferences: this.toolOutputReferences,
237
+ eventToolExecutionAvailable: this.eventToolExecutionAvailable,
238
+ });
239
+ return {
240
+ invoke: jest.fn(async () => ({
241
+ messages: [new AIMessage('child done')],
242
+ })),
243
+ } as unknown as ReturnType<StandardGraph['createWorkflow']>;
244
+ }
245
+ return originalCreateWorkflow.call(this);
246
+ });
247
+
248
+ const agentWithSelfSpawn: t.AgentInputs = {
249
+ agentId: 'self-parent',
250
+ provider: Providers.OPENAI,
251
+ clientOptions: { modelName: 'gpt-4o-mini', apiKey: 'test-key' },
252
+ instructions: 'Agent with self-spawn for context isolation.',
253
+ maxContextTokens: 8000,
254
+ toolDefinitions: [{ name: 'mcp_lookup' }],
255
+ subagentConfigs: [
256
+ {
257
+ type: 'isolated',
258
+ name: 'Isolated Worker',
259
+ description: 'Runs a task with isolated context',
260
+ self: true,
261
+ },
262
+ ],
263
+ };
264
+
265
+ const run = await Run.create<t.IState>({
266
+ runId: `self-spawn-eager-${Date.now()}`,
267
+ graphConfig: {
268
+ type: 'standard',
269
+ agents: [agentWithSelfSpawn],
270
+ },
271
+ customHandlers: {
272
+ [GraphEvents.ON_TOOL_EXECUTE]: {
273
+ handle: async () => undefined,
274
+ },
275
+ },
276
+ eagerEventToolExecution: { enabled: true },
277
+ toolOutputReferences: { enabled: true },
278
+ returnContent: true,
279
+ skipCleanup: true,
280
+ });
281
+
282
+ const context = (run.Graph as StandardGraph).agentContexts.get(
283
+ 'self-parent'
284
+ );
285
+ const subagentTool = (context?.graphTools as t.GenericTool[]).find(
286
+ (tool) => 'name' in tool && tool.name === Constants.SUBAGENT
287
+ );
288
+ expect(subagentTool).toBeDefined();
289
+
290
+ await subagentTool!.invoke(
291
+ {
292
+ description: 'Use your MCP tool.',
293
+ subagent_type: 'isolated',
294
+ },
295
+ callerConfig
296
+ );
297
+
298
+ expect(observedChildGraphs).toEqual([
299
+ {
300
+ eagerEventToolExecution: { enabled: true },
301
+ toolOutputReferences: { enabled: true },
302
+ eventToolExecutionAvailable: true,
303
+ },
304
+ ]);
305
+
306
+ createWorkflowSpy.mockRestore();
307
+ });
308
+
223
309
  it('should not create subagent tool when maxSubagentDepth is 0', async () => {
224
310
  const agentWithZeroDepth: t.AgentInputs = {
225
311
  ...createParentAgent(),