@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
@@ -2,10 +2,16 @@ import { describe, it, expect, beforeEach } from '@jest/globals';
2
2
  import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
3
3
  import type { BaseMessage } from '@langchain/core/messages';
4
4
  import { HookRegistry } from '@/hooks/HookRegistry';
5
- import { Providers, GraphEvents } from '@/common';
5
+ import { Providers, GraphEvents, StepTypes } from '@/common';
6
6
  import { HandlerRegistry } from '@/events';
7
7
  import { AgentContext } from '@/agents/AgentContext';
8
- import type { AgentInputs, ResolvedSubagentConfig } from '@/types';
8
+ import type {
9
+ AgentInputs,
10
+ ResolvedSubagentConfig,
11
+ SubagentUpdateEvent,
12
+ ToolExecuteBatchRequest,
13
+ ToolExecuteResult,
14
+ } from '@/types';
9
15
  import {
10
16
  SubagentExecutor,
11
17
  filterSubagentResult,
@@ -13,6 +19,7 @@ import {
13
19
  buildChildInputs,
14
20
  summarizeEvent,
15
21
  } from '../subagent';
22
+ import { sanitizeForwardedSubagentUpdateData } from '../subagent/SubagentExecutor';
16
23
  import type { StandardGraph } from '@/graphs/Graph';
17
24
 
18
25
  jest.setTimeout(15000);
@@ -547,15 +554,17 @@ describe('SubagentExecutor', () => {
547
554
  });
548
555
 
549
556
  describe('parentConfigurable inheritance', () => {
557
+ type CapturingGraphFactory = {
558
+ factory: () => StandardGraph;
559
+ getInvokeConfig: () => Record<string, unknown> | undefined;
560
+ };
561
+
550
562
  /**
551
563
  * Build a stub factory that captures the second argument to
552
564
  * `workflow.invoke()` (the runnable config) so tests can assert on
553
565
  * the `configurable` we forwarded to the child graph.
554
566
  */
555
- function makeCapturingGraphFactory(): {
556
- factory: () => StandardGraph;
557
- getInvokeConfig: () => Record<string, unknown> | undefined;
558
- } {
567
+ function makeCapturingGraphFactory(): CapturingGraphFactory {
559
568
  let capturedConfig: Record<string, unknown> | undefined;
560
569
  const factory = (): StandardGraph =>
561
570
  ({
@@ -676,6 +685,41 @@ describe('SubagentExecutor', () => {
676
685
  expect(configurable.requestBody).toEqual({ messageId: 'msg-1' });
677
686
  });
678
687
 
688
+ it('strips LangGraph runtime fields from child workflow.invoke configurable', async () => {
689
+ const { factory, getInvokeConfig } = makeCapturingGraphFactory();
690
+ const executor = createExecutor({ createChildGraph: factory });
691
+
692
+ await executor.execute({
693
+ description: 'task',
694
+ subagentType: 'researcher',
695
+ parentConfigurable: {
696
+ __pregel_abort_signals: { externalAbortSignal: 'parent-signal' },
697
+ __pregel_call: (): void => undefined,
698
+ __pregel_scratchpad: { currentTaskInput: 'large-payload' },
699
+ checkpoint_id: 'parent-checkpoint-id',
700
+ checkpoint_map: { parent: 'checkpoint' },
701
+ checkpoint_ns: 'parent-checkpoint-ns',
702
+ requestBody: { messageId: 'msg-1' },
703
+ thread_id: 'parent-thread',
704
+ user: { id: 'user_abc' },
705
+ },
706
+ });
707
+
708
+ const configurable = getInvokeConfig()!.configurable as Record<
709
+ string,
710
+ unknown
711
+ >;
712
+ expect(configurable.__pregel_abort_signals).toBeUndefined();
713
+ expect(configurable.__pregel_call).toBeUndefined();
714
+ expect(configurable.__pregel_scratchpad).toBeUndefined();
715
+ expect(configurable.checkpoint_id).toBeUndefined();
716
+ expect(configurable.checkpoint_map).toBeUndefined();
717
+ expect(configurable.checkpoint_ns).toBeUndefined();
718
+ expect(configurable.requestBody).toEqual({ messageId: 'msg-1' });
719
+ expect(configurable.thread_id).toBe('parent-thread');
720
+ expect(configurable.user).toEqual({ id: 'user_abc' });
721
+ });
722
+
679
723
  it('does not require parentConfigurable (back-compat with hosts that omit it)', async () => {
680
724
  const { factory, getInvokeConfig } = makeCapturingGraphFactory();
681
725
  const executor = createExecutor({ createChildGraph: factory });
@@ -1099,6 +1143,384 @@ describe('SubagentExecutor', () => {
1099
1143
  ]);
1100
1144
  });
1101
1145
 
1146
+ it('sanitizes ON_TOOL_EXECUTE before wrapping it in ON_SUBAGENT_UPDATE', async () => {
1147
+ const toolRequests: ToolExecuteBatchRequest[] = [];
1148
+ const subagentUpdates: SubagentUpdateEvent[] = [];
1149
+ const registry = new HandlerRegistry();
1150
+ registry.register(GraphEvents.ON_TOOL_EXECUTE, {
1151
+ handle: (_event, rawData): void => {
1152
+ const request = rawData as ToolExecuteBatchRequest;
1153
+ toolRequests.push(request);
1154
+ const results: ToolExecuteResult[] = request.toolCalls.map((call) => ({
1155
+ toolCallId: call.id,
1156
+ status: 'success',
1157
+ content: `ran ${call.name}`,
1158
+ }));
1159
+ request.resolve(results);
1160
+ },
1161
+ });
1162
+ registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
1163
+ handle: (_event, rawData): void => {
1164
+ subagentUpdates.push(rawData as SubagentUpdateEvent);
1165
+ },
1166
+ });
1167
+
1168
+ let capturedInvokeOptions: unknown;
1169
+ const factory: () => StandardGraph = (): StandardGraph =>
1170
+ ({
1171
+ createWorkflow: (): { invoke: jest.Mock } => ({
1172
+ invoke: jest.fn().mockImplementation(async (_state, options) => {
1173
+ capturedInvokeOptions = options;
1174
+ return { messages: [new AIMessage('ok')] };
1175
+ }),
1176
+ }),
1177
+ clearHeavyState: jest.fn(),
1178
+ }) as unknown as StandardGraph;
1179
+
1180
+ const executor = createExecutor({
1181
+ createChildGraph: factory,
1182
+ parentHandlerRegistry: registry,
1183
+ });
1184
+
1185
+ await executor.execute({
1186
+ description: 'Task',
1187
+ subagentType: 'researcher',
1188
+ parentToolCallId: 'call_parent_123',
1189
+ });
1190
+
1191
+ const opts = capturedInvokeOptions as { callbacks?: unknown[] };
1192
+ const forwarder = (opts.callbacks ?? [])[0] as {
1193
+ handleCustomEvent?: (
1194
+ eventName: string,
1195
+ data: unknown
1196
+ ) => Promise<void> | void;
1197
+ };
1198
+
1199
+ const batchRequest: ToolExecuteBatchRequest = {
1200
+ toolCalls: [
1201
+ {
1202
+ id: 'call_child_xyz',
1203
+ name: 'calculator',
1204
+ args: { expression: '21 * 2' },
1205
+ stepId: 'step_secret',
1206
+ turn: 7,
1207
+ },
1208
+ ],
1209
+ agentId: 'researcher',
1210
+ userId: 'user_secret',
1211
+ configurable: {
1212
+ user: {
1213
+ federatedTokens: {
1214
+ access_token: 'access-secret',
1215
+ id_token: 'id-secret',
1216
+ refresh_token: 'refresh-secret',
1217
+ },
1218
+ },
1219
+ requestBody: { currentTaskInput: 'sensitive task input' },
1220
+ },
1221
+ metadata: {
1222
+ access_token: 'metadata-secret',
1223
+ },
1224
+ resolve: jest.fn(),
1225
+ reject: jest.fn(),
1226
+ };
1227
+
1228
+ await forwarder.handleCustomEvent?.(
1229
+ GraphEvents.ON_TOOL_EXECUTE,
1230
+ batchRequest
1231
+ );
1232
+
1233
+ expect(toolRequests).toHaveLength(1);
1234
+ expect(toolRequests[0].configurable).toBe(batchRequest.configurable);
1235
+ expect(toolRequests[0].metadata).toBe(batchRequest.metadata);
1236
+
1237
+ const toolUpdate = subagentUpdates.find(
1238
+ (update) =>
1239
+ update.phase === 'run_step' &&
1240
+ update.label === 'Calling calculator'
1241
+ );
1242
+ expect(toolUpdate?.data).toEqual({
1243
+ agentId: 'researcher',
1244
+ toolCalls: [
1245
+ {
1246
+ id: 'call_child_xyz',
1247
+ name: 'calculator',
1248
+ args: { expression: '21 * 2' },
1249
+ },
1250
+ ],
1251
+ });
1252
+ const serializedUpdate = JSON.stringify(toolUpdate);
1253
+ expect(serializedUpdate).not.toContain('configurable');
1254
+ expect(serializedUpdate).not.toContain('metadata');
1255
+ expect(serializedUpdate).not.toContain('access-secret');
1256
+ expect(serializedUpdate).not.toContain('id-secret');
1257
+ expect(serializedUpdate).not.toContain('refresh-secret');
1258
+ expect(serializedUpdate).not.toContain('metadata-secret');
1259
+ expect(serializedUpdate).not.toContain('sensitive task input');
1260
+ expect(serializedUpdate).not.toContain('step_secret');
1261
+ expect(serializedUpdate).not.toContain('user_secret');
1262
+ });
1263
+
1264
+ it('drains observational updates before stop without parallel handler publishes', async () => {
1265
+ const phases: SubagentUpdateEvent['phase'][] = [];
1266
+ let activePublishes = 0;
1267
+ let maxActivePublishes = 0;
1268
+ const registry = new HandlerRegistry();
1269
+ registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
1270
+ handle: async (_event, rawData): Promise<void> => {
1271
+ const update = rawData as SubagentUpdateEvent;
1272
+ activePublishes += 1;
1273
+ maxActivePublishes = Math.max(maxActivePublishes, activePublishes);
1274
+ if (update.phase === 'message_delta') {
1275
+ await new Promise((resolve) => setTimeout(resolve, 1));
1276
+ }
1277
+ phases.push(update.phase);
1278
+ activePublishes -= 1;
1279
+ },
1280
+ });
1281
+
1282
+ const factory: () => StandardGraph = (): StandardGraph =>
1283
+ ({
1284
+ createWorkflow: (): { invoke: jest.Mock } => ({
1285
+ invoke: jest.fn().mockImplementation(async (_state, options) => {
1286
+ const opts = options as { callbacks?: unknown[] };
1287
+ const forwarder = (opts.callbacks ?? [])[0] as {
1288
+ handleCustomEvent?: (
1289
+ eventName: string,
1290
+ data: unknown
1291
+ ) => Promise<void> | void;
1292
+ };
1293
+ for (let index = 0; index < 5; index++) {
1294
+ await forwarder.handleCustomEvent?.(
1295
+ GraphEvents.ON_MESSAGE_DELTA,
1296
+ {
1297
+ id: `msg_${index}`,
1298
+ delta: { content: [{ type: 'text', text: `${index}` }] },
1299
+ }
1300
+ );
1301
+ }
1302
+ return { messages: [new AIMessage('ok')] };
1303
+ }),
1304
+ }),
1305
+ clearHeavyState: jest.fn(),
1306
+ }) as unknown as StandardGraph;
1307
+
1308
+ const executor = createExecutor({
1309
+ createChildGraph: factory,
1310
+ parentHandlerRegistry: registry,
1311
+ });
1312
+
1313
+ await executor.execute({
1314
+ description: 'Task',
1315
+ subagentType: 'researcher',
1316
+ });
1317
+
1318
+ expect(maxActivePublishes).toBe(1);
1319
+ expect(phases[0]).toBe('start');
1320
+ expect(phases.slice(1, 6)).toEqual([
1321
+ 'message_delta',
1322
+ 'message_delta',
1323
+ 'message_delta',
1324
+ 'message_delta',
1325
+ 'message_delta',
1326
+ ]);
1327
+ expect(phases[phases.length - 1]).toBe('stop');
1328
+ });
1329
+
1330
+ it('allowlists forwarded run step payloads before wrapping them in ON_SUBAGENT_UPDATE', async () => {
1331
+ const subagentUpdates: SubagentUpdateEvent[] = [];
1332
+ const registry = new HandlerRegistry();
1333
+ registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
1334
+ handle: (_event, rawData): void => {
1335
+ subagentUpdates.push(rawData as SubagentUpdateEvent);
1336
+ },
1337
+ });
1338
+
1339
+ const output = 'tool output that should stay visible';
1340
+ const factory: () => StandardGraph = (): StandardGraph =>
1341
+ ({
1342
+ createWorkflow: (): { invoke: jest.Mock } => ({
1343
+ invoke: jest.fn().mockImplementation(async (_state, options) => {
1344
+ const opts = options as { callbacks?: unknown[] };
1345
+ const forwarder = (opts.callbacks ?? [])[0] as {
1346
+ handleCustomEvent?: (
1347
+ eventName: string,
1348
+ data: unknown
1349
+ ) => Promise<void> | void;
1350
+ };
1351
+ await forwarder.handleCustomEvent?.(GraphEvents.ON_RUN_STEP, {
1352
+ id: 'step_1',
1353
+ type: StepTypes.TOOL_CALLS,
1354
+ agentId: 'researcher',
1355
+ index: 0,
1356
+ stepDetails: {
1357
+ type: StepTypes.TOOL_CALLS,
1358
+ tool_calls: [
1359
+ {
1360
+ id: 'call_1',
1361
+ name: 'calculator',
1362
+ args: { expression: '21 * 2' },
1363
+ futureSecret: 'nested-step-secret',
1364
+ },
1365
+ ],
1366
+ futureSecret: 'step-details-secret',
1367
+ },
1368
+ configurable: { access_token: 'access-secret' },
1369
+ metadata: { refresh_token: 'refresh-secret' },
1370
+ futureSecret: 'top-level-step-secret',
1371
+ });
1372
+ await forwarder.handleCustomEvent?.(
1373
+ GraphEvents.ON_RUN_STEP_COMPLETED,
1374
+ {
1375
+ result: {
1376
+ id: 'step_1',
1377
+ index: 0,
1378
+ type: 'tool_call',
1379
+ tool_call: {
1380
+ id: 'call_1',
1381
+ name: 'calculator',
1382
+ args: '{}',
1383
+ output,
1384
+ progress: 1,
1385
+ futureSecret: 'nested-completed-secret',
1386
+ },
1387
+ futureSecret: 'completed-result-secret',
1388
+ },
1389
+ configurable: { access_token: 'access-secret' },
1390
+ metadata: { refresh_token: 'refresh-secret' },
1391
+ futureSecret: 'top-level-completed-secret',
1392
+ }
1393
+ );
1394
+ return { messages: [new AIMessage('ok')] };
1395
+ }),
1396
+ }),
1397
+ clearHeavyState: jest.fn(),
1398
+ }) as unknown as StandardGraph;
1399
+
1400
+ const executor = createExecutor({
1401
+ createChildGraph: factory,
1402
+ parentHandlerRegistry: registry,
1403
+ });
1404
+
1405
+ await executor.execute({
1406
+ description: 'Task',
1407
+ subagentType: 'researcher',
1408
+ });
1409
+
1410
+ const runStep = subagentUpdates.find(
1411
+ (update) => update.phase === 'run_step'
1412
+ );
1413
+ const completedStep = subagentUpdates.find(
1414
+ (update) => update.phase === 'run_step_completed'
1415
+ );
1416
+ expect(runStep?.data).toEqual({
1417
+ id: 'step_1',
1418
+ type: StepTypes.TOOL_CALLS,
1419
+ agentId: 'researcher',
1420
+ index: 0,
1421
+ stepDetails: {
1422
+ type: StepTypes.TOOL_CALLS,
1423
+ tool_calls: [
1424
+ {
1425
+ id: 'call_1',
1426
+ name: 'calculator',
1427
+ args: { expression: '21 * 2' },
1428
+ },
1429
+ ],
1430
+ },
1431
+ });
1432
+ expect(completedStep?.data).toEqual({
1433
+ result: {
1434
+ id: 'step_1',
1435
+ index: 0,
1436
+ type: 'tool_call',
1437
+ tool_call: {
1438
+ id: 'call_1',
1439
+ name: 'calculator',
1440
+ args: '{}',
1441
+ output,
1442
+ progress: 1,
1443
+ },
1444
+ },
1445
+ });
1446
+ const serialized = JSON.stringify([runStep, completedStep]);
1447
+ expect(serialized).toContain(output);
1448
+ expect(serialized).not.toContain('futureSecret');
1449
+ expect(serialized).not.toContain('access-secret');
1450
+ expect(serialized).not.toContain('refresh-secret');
1451
+ expect(serialized).not.toContain('top-level-step-secret');
1452
+ expect(serialized).not.toContain('nested-step-secret');
1453
+ expect(serialized).not.toContain('top-level-completed-secret');
1454
+ expect(serialized).not.toContain('nested-completed-secret');
1455
+ });
1456
+
1457
+ it('does not drop non-droppable updates when the forwarding queue overflows', async () => {
1458
+ const completedIds: string[] = [];
1459
+ const registry = new HandlerRegistry();
1460
+ registry.register(GraphEvents.ON_SUBAGENT_UPDATE, {
1461
+ handle: async (_event, rawData): Promise<void> => {
1462
+ const update = rawData as SubagentUpdateEvent;
1463
+ if (update.phase === 'run_step_completed') {
1464
+ const data = update.data as { result?: { id?: string } };
1465
+ if (data.result?.id != null) {
1466
+ completedIds.push(data.result.id);
1467
+ }
1468
+ await new Promise((resolve) => setTimeout(resolve, 1));
1469
+ }
1470
+ },
1471
+ });
1472
+
1473
+ const factory: () => StandardGraph = (): StandardGraph =>
1474
+ ({
1475
+ createWorkflow: (): { invoke: jest.Mock } => ({
1476
+ invoke: jest.fn().mockImplementation(async (_state, options) => {
1477
+ const opts = options as { callbacks?: unknown[] };
1478
+ const forwarder = (opts.callbacks ?? [])[0] as {
1479
+ handleCustomEvent?: (
1480
+ eventName: string,
1481
+ data: unknown
1482
+ ) => Promise<void> | void;
1483
+ };
1484
+ for (let index = 0; index < 80; index++) {
1485
+ await forwarder.handleCustomEvent?.(
1486
+ GraphEvents.ON_RUN_STEP_COMPLETED,
1487
+ {
1488
+ result: {
1489
+ id: `step_${index}`,
1490
+ index,
1491
+ type: 'tool_call',
1492
+ tool_call: {
1493
+ id: `call_${index}`,
1494
+ name: 'calculator',
1495
+ args: '{}',
1496
+ output: `${index}`,
1497
+ progress: 1,
1498
+ },
1499
+ },
1500
+ }
1501
+ );
1502
+ }
1503
+ return { messages: [new AIMessage('ok')] };
1504
+ }),
1505
+ }),
1506
+ clearHeavyState: jest.fn(),
1507
+ }) as unknown as StandardGraph;
1508
+
1509
+ const executor = createExecutor({
1510
+ createChildGraph: factory,
1511
+ parentHandlerRegistry: registry,
1512
+ });
1513
+
1514
+ await executor.execute({
1515
+ description: 'Task',
1516
+ subagentType: 'researcher',
1517
+ });
1518
+
1519
+ expect(completedIds).toHaveLength(80);
1520
+ expect(completedIds[0]).toBe('step_0');
1521
+ expect(completedIds[completedIds.length - 1]).toBe('step_79');
1522
+ });
1523
+
1102
1524
  it('does NOT forward ON_TOOL_EXECUTE when the parent registry has no handler (safe fallback)', async () => {
1103
1525
  /**
1104
1526
  * The executor strips `toolDefinitions` when the parent registry has
@@ -1294,3 +1716,115 @@ describe('summarizeEvent', () => {
1294
1716
  expect(summarizeEvent('on_unknown_event', {})).toBe('on_unknown_event');
1295
1717
  });
1296
1718
  });
1719
+
1720
+ describe('sanitizeForwardedSubagentUpdateData', () => {
1721
+ it('uses an allowlist for run step payloads', () => {
1722
+ const sanitized = sanitizeForwardedSubagentUpdateData(
1723
+ GraphEvents.ON_RUN_STEP,
1724
+ {
1725
+ id: 'step_1',
1726
+ type: StepTypes.TOOL_CALLS,
1727
+ agentId: 'researcher',
1728
+ index: 0,
1729
+ stepDetails: {
1730
+ type: StepTypes.TOOL_CALLS,
1731
+ tool_calls: [
1732
+ {
1733
+ id: 'call_1',
1734
+ name: 'calculator',
1735
+ args: { expression: '21 * 2' },
1736
+ futureSecret: 'nested-secret',
1737
+ },
1738
+ ],
1739
+ futureSecret: 'details-secret',
1740
+ },
1741
+ configurable: { access_token: 'access-secret' },
1742
+ metadata: { refresh_token: 'refresh-secret' },
1743
+ futureSecret: 'top-level-secret',
1744
+ }
1745
+ );
1746
+
1747
+ expect(sanitized).toEqual({
1748
+ id: 'step_1',
1749
+ type: StepTypes.TOOL_CALLS,
1750
+ agentId: 'researcher',
1751
+ index: 0,
1752
+ stepDetails: {
1753
+ type: StepTypes.TOOL_CALLS,
1754
+ tool_calls: [
1755
+ {
1756
+ id: 'call_1',
1757
+ name: 'calculator',
1758
+ args: { expression: '21 * 2' },
1759
+ },
1760
+ ],
1761
+ },
1762
+ });
1763
+ const serialized = JSON.stringify(sanitized);
1764
+ expect(serialized).not.toContain('futureSecret');
1765
+ expect(serialized).not.toContain('top-level-secret');
1766
+ expect(serialized).not.toContain('details-secret');
1767
+ expect(serialized).not.toContain('nested-secret');
1768
+ expect(serialized).not.toContain('access-secret');
1769
+ expect(serialized).not.toContain('refresh-secret');
1770
+ });
1771
+
1772
+ it('keeps completed tool output while stripping operational fields', () => {
1773
+ const output = 'x'.repeat(10_000);
1774
+ const sanitized = sanitizeForwardedSubagentUpdateData(
1775
+ GraphEvents.ON_RUN_STEP_COMPLETED,
1776
+ {
1777
+ result: {
1778
+ id: 'step_1',
1779
+ index: 0,
1780
+ type: 'tool_call',
1781
+ tool_call: {
1782
+ id: 'call_1',
1783
+ name: 'list_tables_mcp_ClickHouse',
1784
+ args: '{}',
1785
+ output,
1786
+ progress: 1,
1787
+ futureSecret: 'nested-secret',
1788
+ },
1789
+ futureSecret: 'result-secret',
1790
+ },
1791
+ configurable: {
1792
+ user: {
1793
+ federatedTokens: {
1794
+ access_token: 'access-secret',
1795
+ },
1796
+ },
1797
+ },
1798
+ metadata: {
1799
+ refresh_token: 'refresh-secret',
1800
+ },
1801
+ futureSecret: 'top-level-secret',
1802
+ }
1803
+ );
1804
+
1805
+ expect(sanitized).toEqual({
1806
+ result: {
1807
+ id: 'step_1',
1808
+ index: 0,
1809
+ type: 'tool_call',
1810
+ tool_call: {
1811
+ id: 'call_1',
1812
+ name: 'list_tables_mcp_ClickHouse',
1813
+ args: '{}',
1814
+ output,
1815
+ progress: 1,
1816
+ },
1817
+ },
1818
+ });
1819
+ const serialized = JSON.stringify(sanitized);
1820
+ expect(serialized).toContain(output);
1821
+ expect(serialized).not.toContain('futureSecret');
1822
+ expect(serialized).not.toContain('top-level-secret');
1823
+ expect(serialized).not.toContain('result-secret');
1824
+ expect(serialized).not.toContain('nested-secret');
1825
+ expect(serialized).not.toContain('configurable');
1826
+ expect(serialized).not.toContain('metadata');
1827
+ expect(serialized).not.toContain('access-secret');
1828
+ expect(serialized).not.toContain('refresh-secret');
1829
+ });
1830
+ });