@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.
- package/dist/cjs/graphs/Graph.cjs +25 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/hooks/executeHooks.cjs +14 -7
- package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/index.cjs +8 -2
- package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +34 -0
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/main.cjs +9 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/stream.cjs +115 -8
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/BashExecutor.cjs +10 -9
- package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +12 -8
- package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +35 -11
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/CodeSessionFileSummary.cjs +63 -0
- package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -0
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -12
- package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +32 -12
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +319 -29
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -1
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +25 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/hooks/executeHooks.mjs +14 -7
- package/dist/esm/hooks/executeHooks.mjs.map +1 -1
- package/dist/esm/llm/anthropic/index.mjs +9 -3
- package/dist/esm/llm/anthropic/index.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/main.mjs +2 -1
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/stream.mjs +115 -8
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/BashExecutor.mjs +11 -10
- package/dist/esm/tools/BashExecutor.mjs.map +1 -1
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs +13 -9
- package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +29 -12
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/CodeSessionFileSummary.mjs +60 -0
- package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -0
- package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -13
- package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +32 -12
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +320 -31
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +8 -1
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/llm/anthropic/index.d.ts +3 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +4 -0
- package/dist/types/tools/BashExecutor.d.ts +3 -3
- package/dist/types/tools/CodeExecutor.d.ts +10 -3
- package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
- package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -4
- package/dist/types/tools/subagent/SubagentExecutor.d.ts +8 -5
- package/dist/types/types/tools.d.ts +11 -3
- package/dist/types/utils/events.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +1073 -221
- package/src/graphs/Graph.ts +27 -5
- package/src/hooks/__tests__/executeHooks.test.ts +38 -0
- package/src/hooks/executeHooks.ts +27 -7
- package/src/llm/anthropic/index.ts +27 -3
- package/src/llm/anthropic/llm.spec.ts +60 -1
- package/src/llm/anthropic/utils/message_inputs.ts +46 -0
- package/src/specs/subagent.test.ts +87 -1
- package/src/stream.ts +163 -12
- package/src/tools/BashExecutor.ts +21 -10
- package/src/tools/BashProgrammaticToolCalling.ts +21 -9
- package/src/tools/CodeExecutor.ts +55 -12
- package/src/tools/CodeSessionFileSummary.ts +80 -0
- package/src/tools/ProgrammaticToolCalling.ts +25 -12
- package/src/tools/ToolNode.ts +142 -116
- package/src/tools/__tests__/BashExecutor.test.ts +9 -0
- package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +43 -0
- package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +100 -16
- package/src/tools/__tests__/SubagentExecutor.test.ts +540 -6
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +52 -0
- package/src/tools/__tests__/subagentHooks.test.ts +237 -0
- package/src/tools/subagent/SubagentExecutor.ts +514 -36
- package/src/types/tools.ts +11 -3
- 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 {
|
|
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
|
+
});
|