@illuma-ai/agents 1.1.19 → 1.1.20
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/common/enum.cjs +2 -0
- package/dist/cjs/common/enum.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +87 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/main.cjs +3 -0
- package/dist/cjs/main.cjs.map +1 -1
- package/dist/cjs/nodes/ApprovalGateNode.cjs +75 -0
- package/dist/cjs/nodes/ApprovalGateNode.cjs.map +1 -0
- package/dist/cjs/run.cjs +45 -0
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +21 -18
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/types/graph.cjs.map +1 -1
- package/dist/cjs/utils/run.cjs +6 -1
- package/dist/cjs/utils/run.cjs.map +1 -1
- package/dist/esm/common/enum.mjs +2 -0
- package/dist/esm/common/enum.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +87 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/main.mjs +1 -0
- package/dist/esm/main.mjs.map +1 -1
- package/dist/esm/nodes/ApprovalGateNode.mjs +72 -0
- package/dist/esm/nodes/ApprovalGateNode.mjs.map +1 -0
- package/dist/esm/run.mjs +45 -0
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +22 -19
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/types/graph.mjs.map +1 -1
- package/dist/esm/utils/run.mjs +6 -1
- package/dist/esm/utils/run.mjs.map +1 -1
- package/dist/types/common/enum.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/nodes/ApprovalGateNode.d.ts +49 -0
- package/dist/types/nodes/index.d.ts +2 -0
- package/dist/types/run.d.ts +25 -1
- package/dist/types/tools/ToolNode.d.ts +7 -5
- package/dist/types/types/graph.d.ts +31 -0
- package/dist/types/types/tools.d.ts +7 -9
- package/package.json +1 -1
- package/src/common/enum.ts +2 -0
- package/src/graphs/MultiAgentGraph.ts +108 -1
- package/src/index.ts +3 -0
- package/src/nodes/ApprovalGateNode.ts +117 -0
- package/src/nodes/__tests__/ApprovalGateNode.test.ts +206 -0
- package/src/nodes/index.ts +5 -0
- package/src/run.ts +55 -1
- package/src/specs/agent-handoffs-bedrock.integration.test.ts +2 -2
- package/src/specs/agent-handoffs.test.ts +153 -6
- package/src/tools/ToolNode.ts +28 -23
- package/src/tools/__tests__/ToolApproval.test.ts +162 -325
- package/src/types/graph.ts +32 -0
- package/src/types/tools.ts +7 -9
- package/src/utils/run.ts +9 -1
package/src/run.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/run.ts
|
|
2
2
|
import './instrumentation';
|
|
3
3
|
import { ObservabilityCallbackHandler } from '@illuma-ai/observability-langchain';
|
|
4
|
+
import { Command } from '@langchain/langgraph';
|
|
4
5
|
import { PromptTemplate } from '@langchain/core/prompts';
|
|
5
6
|
import { RunnableLambda } from '@langchain/core/runnables';
|
|
6
7
|
import { AzureChatOpenAI, ChatOpenAI } from '@langchain/openai';
|
|
@@ -250,8 +251,16 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
250
251
|
};
|
|
251
252
|
}
|
|
252
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Processes the graph stream for a given input.
|
|
256
|
+
*
|
|
257
|
+
* @param inputs - Either the initial state (IState) for a new run, or a
|
|
258
|
+
* Command (e.g., `new Command({ resume: ... })`) to resume from an interrupt.
|
|
259
|
+
* @param config - Runnable config with version and optional run_id.
|
|
260
|
+
* @param streamOptions - Optional stream event callbacks and options.
|
|
261
|
+
*/
|
|
253
262
|
async processStream(
|
|
254
|
-
inputs: t.IState,
|
|
263
|
+
inputs: t.IState | Command,
|
|
255
264
|
config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; run_id?: string },
|
|
256
265
|
streamOptions?: t.EventStreamOptions
|
|
257
266
|
): Promise<MessageContentComplex[] | undefined> {
|
|
@@ -409,6 +418,51 @@ export class Run<_T extends t.BaseGraphState> {
|
|
|
409
418
|
}) as t.SystemCallbacks[K];
|
|
410
419
|
}
|
|
411
420
|
|
|
421
|
+
/**
|
|
422
|
+
* Checks whether the graph was interrupted (e.g., by HITL tool approval).
|
|
423
|
+
* Call after processStream() returns to determine if the graph is waiting
|
|
424
|
+
* for a resume via Command({ resume }).
|
|
425
|
+
*
|
|
426
|
+
* Requires a checkpointer to be configured — without one, interrupt state
|
|
427
|
+
* is not persisted and this always returns false.
|
|
428
|
+
*/
|
|
429
|
+
async hasInterrupts(
|
|
430
|
+
config: Partial<RunnableConfig>
|
|
431
|
+
): Promise<boolean> {
|
|
432
|
+
if (!this.graphRunnable) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
const state = await this.graphRunnable.getState(config);
|
|
437
|
+
return state.tasks?.some((task) => task.interrupts?.length > 0) ?? false;
|
|
438
|
+
} catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Returns the interrupt values from the graph state.
|
|
445
|
+
* Each interrupt's `value` contains the data passed to `interrupt()` by the node
|
|
446
|
+
* (e.g., a ToolApprovalRequest for HITL).
|
|
447
|
+
*/
|
|
448
|
+
async getInterruptValues(
|
|
449
|
+
config: Partial<RunnableConfig>
|
|
450
|
+
): Promise<unknown[]> {
|
|
451
|
+
if (!this.graphRunnable) {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const state = await this.graphRunnable.getState(config);
|
|
456
|
+
return (
|
|
457
|
+
state.tasks?.flatMap((task) =>
|
|
458
|
+
(task.interrupts ?? []).map((i) => i.value)
|
|
459
|
+
) ?? []
|
|
460
|
+
);
|
|
461
|
+
} catch {
|
|
462
|
+
return [];
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
412
466
|
getCallbacks(clientCallbacks: t.ClientCallbacks): t.SystemCallbacks {
|
|
413
467
|
return {
|
|
414
468
|
[Callback.TOOL_ERROR]: this.createSystemCallback(
|
|
@@ -145,11 +145,11 @@ describeIf('Agent Handoff E2E with Bedrock', () => {
|
|
|
145
145
|
|
|
146
146
|
// Verify the handoff tools have enriched descriptions
|
|
147
147
|
const salesTool = findToolByName(
|
|
148
|
-
supervisorContext?.
|
|
148
|
+
supervisorContext?.graphTools,
|
|
149
149
|
`${Constants.LC_TRANSFER_TO_}agent_W47hBnn2RoVZEOy5595GC`
|
|
150
150
|
);
|
|
151
151
|
const supportTool = findToolByName(
|
|
152
|
-
supervisorContext?.
|
|
152
|
+
supervisorContext?.graphTools,
|
|
153
153
|
`${Constants.LC_TRANSFER_TO_}agent_X92kLmn4TpQR8vw3221HD`
|
|
154
154
|
);
|
|
155
155
|
|
|
@@ -1018,7 +1018,7 @@ describe('Agent Handoffs Tests', () => {
|
|
|
1018
1018
|
'supervisor'
|
|
1019
1019
|
);
|
|
1020
1020
|
const handoffTool = findToolByName(
|
|
1021
|
-
supervisorContext?.
|
|
1021
|
+
supervisorContext?.graphTools,
|
|
1022
1022
|
`${Constants.LC_TRANSFER_TO_}data_analyst`
|
|
1023
1023
|
);
|
|
1024
1024
|
|
|
@@ -1052,7 +1052,7 @@ describe('Agent Handoffs Tests', () => {
|
|
|
1052
1052
|
'supervisor'
|
|
1053
1053
|
);
|
|
1054
1054
|
const handoffTool = findToolByName(
|
|
1055
|
-
supervisorContext?.
|
|
1055
|
+
supervisorContext?.graphTools,
|
|
1056
1056
|
`${Constants.LC_TRANSFER_TO_}writer`
|
|
1057
1057
|
);
|
|
1058
1058
|
|
|
@@ -1086,7 +1086,7 @@ describe('Agent Handoffs Tests', () => {
|
|
|
1086
1086
|
'supervisor'
|
|
1087
1087
|
);
|
|
1088
1088
|
const handoffTool = findToolByName(
|
|
1089
|
-
supervisorContext?.
|
|
1089
|
+
supervisorContext?.graphTools,
|
|
1090
1090
|
`${Constants.LC_TRANSFER_TO_}agent_b`
|
|
1091
1091
|
);
|
|
1092
1092
|
|
|
@@ -1130,15 +1130,15 @@ describe('Agent Handoffs Tests', () => {
|
|
|
1130
1130
|
);
|
|
1131
1131
|
|
|
1132
1132
|
const salesTool = findToolByName(
|
|
1133
|
-
routerContext?.
|
|
1133
|
+
routerContext?.graphTools,
|
|
1134
1134
|
`${Constants.LC_TRANSFER_TO_}sales`
|
|
1135
1135
|
);
|
|
1136
1136
|
const supportTool = findToolByName(
|
|
1137
|
-
routerContext?.
|
|
1137
|
+
routerContext?.graphTools,
|
|
1138
1138
|
`${Constants.LC_TRANSFER_TO_}support`
|
|
1139
1139
|
);
|
|
1140
1140
|
const billingTool = findToolByName(
|
|
1141
|
-
routerContext?.
|
|
1141
|
+
routerContext?.graphTools,
|
|
1142
1142
|
`${Constants.LC_TRANSFER_TO_}billing`
|
|
1143
1143
|
);
|
|
1144
1144
|
|
|
@@ -1294,4 +1294,151 @@ describe('Agent Handoffs Tests', () => {
|
|
|
1294
1294
|
}
|
|
1295
1295
|
});
|
|
1296
1296
|
});
|
|
1297
|
+
|
|
1298
|
+
describe('Multi-Turn Agent Resumption', () => {
|
|
1299
|
+
it('should route START to resumeFromAgentId when valid', async () => {
|
|
1300
|
+
const agents: t.AgentInputs[] = [
|
|
1301
|
+
createBasicAgent('router', 'You are the router agent'),
|
|
1302
|
+
createBasicAgent('writer', 'You are the writer agent'),
|
|
1303
|
+
createBasicAgent('researcher', 'You are the researcher agent'),
|
|
1304
|
+
];
|
|
1305
|
+
|
|
1306
|
+
const edges: t.GraphEdge[] = [
|
|
1307
|
+
{ from: 'router', to: 'writer', edgeType: EdgeType.TRANSFER },
|
|
1308
|
+
{ from: 'router', to: 'researcher', edgeType: EdgeType.TRANSFER },
|
|
1309
|
+
];
|
|
1310
|
+
|
|
1311
|
+
// Create run WITH resumeFromAgentId set to 'writer'
|
|
1312
|
+
const run = await Run.create({
|
|
1313
|
+
runId: `resume-test-${Date.now()}`,
|
|
1314
|
+
graphConfig: {
|
|
1315
|
+
type: 'multi-agent',
|
|
1316
|
+
agents,
|
|
1317
|
+
edges,
|
|
1318
|
+
resumeFromAgentId: 'writer',
|
|
1319
|
+
},
|
|
1320
|
+
returnContent: true,
|
|
1321
|
+
skipCleanup: true,
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
// Graph should compile without error
|
|
1325
|
+
expect(run.graphRunnable).toBeDefined();
|
|
1326
|
+
|
|
1327
|
+
// The writer agent should be reachable from START
|
|
1328
|
+
// (We verify by checking that the graph compiled successfully with conditional routing)
|
|
1329
|
+
expect(run.Graph).toBeDefined();
|
|
1330
|
+
});
|
|
1331
|
+
|
|
1332
|
+
it('should fall back to default starting nodes when resumeFromAgentId is invalid', async () => {
|
|
1333
|
+
const agents: t.AgentInputs[] = [
|
|
1334
|
+
createBasicAgent('router', 'You are the router agent'),
|
|
1335
|
+
createBasicAgent('writer', 'You are the writer agent'),
|
|
1336
|
+
];
|
|
1337
|
+
|
|
1338
|
+
const edges: t.GraphEdge[] = [
|
|
1339
|
+
{ from: 'router', to: 'writer', edgeType: EdgeType.TRANSFER },
|
|
1340
|
+
];
|
|
1341
|
+
|
|
1342
|
+
// Create run with an invalid resumeFromAgentId
|
|
1343
|
+
const run = await Run.create({
|
|
1344
|
+
runId: `resume-invalid-test-${Date.now()}`,
|
|
1345
|
+
graphConfig: {
|
|
1346
|
+
type: 'multi-agent',
|
|
1347
|
+
agents,
|
|
1348
|
+
edges,
|
|
1349
|
+
resumeFromAgentId: 'nonexistent_agent',
|
|
1350
|
+
},
|
|
1351
|
+
returnContent: true,
|
|
1352
|
+
skipCleanup: true,
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// Should still compile (falls back to default starting nodes)
|
|
1356
|
+
expect(run.graphRunnable).toBeDefined();
|
|
1357
|
+
expect(run.Graph).toBeDefined();
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
it('should compile normally without resumeFromAgentId (backward compat)', async () => {
|
|
1361
|
+
const agents: t.AgentInputs[] = [
|
|
1362
|
+
createBasicAgent('router', 'You are the router agent'),
|
|
1363
|
+
createBasicAgent('writer', 'You are the writer agent'),
|
|
1364
|
+
];
|
|
1365
|
+
|
|
1366
|
+
const edges: t.GraphEdge[] = [
|
|
1367
|
+
{ from: 'router', to: 'writer', edgeType: EdgeType.TRANSFER },
|
|
1368
|
+
];
|
|
1369
|
+
|
|
1370
|
+
// Create run WITHOUT resumeFromAgentId — existing behavior
|
|
1371
|
+
const run = await Run.create(createTestConfig(agents, edges));
|
|
1372
|
+
|
|
1373
|
+
expect(run.graphRunnable).toBeDefined();
|
|
1374
|
+
expect(run.Graph).toBeDefined();
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
it('should route to resume agent and track lastActiveAgentId correctly', async () => {
|
|
1378
|
+
const agents: t.AgentInputs[] = [
|
|
1379
|
+
createBasicAgent('router', 'You are the router agent'),
|
|
1380
|
+
createBasicAgent('writer', 'You are the writer agent'),
|
|
1381
|
+
];
|
|
1382
|
+
|
|
1383
|
+
const edges: t.GraphEdge[] = [
|
|
1384
|
+
{ from: 'router', to: 'writer', edgeType: EdgeType.TRANSFER },
|
|
1385
|
+
];
|
|
1386
|
+
|
|
1387
|
+
const run = await Run.create({
|
|
1388
|
+
runId: `resume-execution-test-${Date.now()}`,
|
|
1389
|
+
graphConfig: {
|
|
1390
|
+
type: 'multi-agent',
|
|
1391
|
+
agents,
|
|
1392
|
+
edges,
|
|
1393
|
+
resumeFromAgentId: 'writer',
|
|
1394
|
+
},
|
|
1395
|
+
returnContent: true,
|
|
1396
|
+
skipCleanup: true,
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1399
|
+
// Override test model to respond directly (no transfer)
|
|
1400
|
+
run.Graph?.overrideTestModel(['I am the writer, continuing your work.'], 10);
|
|
1401
|
+
|
|
1402
|
+
const messages = [new HumanMessage('Make the intro shorter')];
|
|
1403
|
+
const config: Partial<RunnableConfig> & { version: 'v1' | 'v2'; streamMode: string } = {
|
|
1404
|
+
configurable: { thread_id: 'resume-exec-test' },
|
|
1405
|
+
streamMode: 'values',
|
|
1406
|
+
version: 'v2' as const,
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
await run.processStream({ messages }, config);
|
|
1410
|
+
|
|
1411
|
+
// After execution, lastActiveAgentId should be 'writer' (not 'router')
|
|
1412
|
+
expect(run.getLastActiveAgentId()).toBe('writer');
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
it('should handle resumeFromAgentId with parallel starting nodes', async () => {
|
|
1416
|
+
const agents: t.AgentInputs[] = [
|
|
1417
|
+
createBasicAgent('agent_a', 'You are agent A'),
|
|
1418
|
+
createBasicAgent('agent_b', 'You are agent B'),
|
|
1419
|
+
createBasicAgent('agent_c', 'You are agent C'),
|
|
1420
|
+
];
|
|
1421
|
+
|
|
1422
|
+
// Both A and B are starting nodes (no incoming edges), C has incoming
|
|
1423
|
+
const edges: t.GraphEdge[] = [
|
|
1424
|
+
{ from: 'agent_a', to: 'agent_c', edgeType: EdgeType.TRANSFER },
|
|
1425
|
+
{ from: 'agent_b', to: 'agent_c', edgeType: EdgeType.TRANSFER },
|
|
1426
|
+
];
|
|
1427
|
+
|
|
1428
|
+
// Resume from agent_c — should override the parallel start (A + B)
|
|
1429
|
+
const run = await Run.create({
|
|
1430
|
+
runId: `resume-parallel-test-${Date.now()}`,
|
|
1431
|
+
graphConfig: {
|
|
1432
|
+
type: 'multi-agent',
|
|
1433
|
+
agents,
|
|
1434
|
+
edges,
|
|
1435
|
+
resumeFromAgentId: 'agent_c',
|
|
1436
|
+
},
|
|
1437
|
+
returnContent: true,
|
|
1438
|
+
skipCleanup: true,
|
|
1439
|
+
});
|
|
1440
|
+
|
|
1441
|
+
expect(run.graphRunnable).toBeDefined();
|
|
1442
|
+
});
|
|
1443
|
+
});
|
|
1297
1444
|
});
|
package/src/tools/ToolNode.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
Send,
|
|
10
10
|
Command,
|
|
11
11
|
isCommand,
|
|
12
|
+
interrupt,
|
|
12
13
|
isGraphInterrupt,
|
|
13
14
|
MessagesAnnotation,
|
|
14
15
|
} from '@langchain/langgraph';
|
|
@@ -226,21 +227,23 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
/**
|
|
229
|
-
* Requests human approval for a tool call
|
|
230
|
-
* Dispatches an ON_TOOL_APPROVAL_REQUIRED event and waits for the host
|
|
231
|
-
* to resolve the promise with an approval response.
|
|
230
|
+
* Requests human approval for a tool call using LangGraph's native interrupt().
|
|
232
231
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
232
|
+
* Flow:
|
|
233
|
+
* 1. Dispatches ON_TOOL_APPROVAL_REQUIRED notification (no resolve/reject — data only)
|
|
234
|
+
* so the host can persist the request and send UI events.
|
|
235
|
+
* 2. Calls interrupt() which checkpoints graph state and pauses execution.
|
|
236
|
+
* 3. When the host resumes via Command({ resume: ToolApprovalResponse }),
|
|
237
|
+
* interrupt() returns the response synchronously.
|
|
235
238
|
*
|
|
236
239
|
* @param call - The tool call requiring approval
|
|
237
240
|
* @param config - The runnable config for event dispatch
|
|
238
241
|
* @returns The approval response from the human
|
|
239
242
|
*/
|
|
240
|
-
private
|
|
243
|
+
private requestApproval(
|
|
241
244
|
call: ToolCall,
|
|
242
245
|
config: RunnableConfig
|
|
243
|
-
):
|
|
246
|
+
): t.ToolApprovalResponse {
|
|
244
247
|
const approvalRequest: t.ToolApprovalRequest = {
|
|
245
248
|
type: 'tool_approval_required',
|
|
246
249
|
toolCallId: call.id ?? '',
|
|
@@ -250,17 +253,18 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
250
253
|
description: `Tool "${call.name}" wants to execute with the provided arguments.`,
|
|
251
254
|
};
|
|
252
255
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
256
|
+
// Notify host before interrupting — allows SSE event emission and DB persistence.
|
|
257
|
+
// This is a fire-and-forget notification, NOT the approval mechanism.
|
|
258
|
+
safeDispatchCustomEvent(
|
|
259
|
+
GraphEvents.ON_TOOL_APPROVAL_REQUIRED,
|
|
260
|
+
approvalRequest,
|
|
261
|
+
config
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// interrupt() throws GraphInterrupt on first call (checkpoints state),
|
|
265
|
+
// returns the resume value on re-execution after Command({ resume }).
|
|
266
|
+
const response = interrupt(approvalRequest) as t.ToolApprovalResponse;
|
|
267
|
+
return response;
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
/**
|
|
@@ -363,13 +367,13 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
363
367
|
|
|
364
368
|
// ========================================================================
|
|
365
369
|
// HITL: Check if this tool requires human approval before execution.
|
|
366
|
-
// Uses
|
|
367
|
-
//
|
|
370
|
+
// Uses LangGraph interrupt() — checkpoints state and pauses the graph.
|
|
371
|
+
// Resumes when host sends Command({ resume: ToolApprovalResponse }).
|
|
368
372
|
// ========================================================================
|
|
369
373
|
if (
|
|
370
374
|
this.requiresApproval(call.name, call.args as Record<string, unknown>)
|
|
371
375
|
) {
|
|
372
|
-
const approvalResponse =
|
|
376
|
+
const approvalResponse = this.requestApproval(call, config);
|
|
373
377
|
if (!approvalResponse.approved) {
|
|
374
378
|
// Human denied the tool call - return a denial message
|
|
375
379
|
return new ToolMessage({
|
|
@@ -811,7 +815,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
811
815
|
): Promise<ToolMessage[]> {
|
|
812
816
|
// ========================================================================
|
|
813
817
|
// HITL: Check approval for event-dispatched tools (browser, MCP, etc.)
|
|
814
|
-
// before dispatching.
|
|
818
|
+
// before dispatching. Uses LangGraph interrupt() for each tool needing
|
|
819
|
+
// approval — counter-based matching handles sequential interrupts.
|
|
815
820
|
// ========================================================================
|
|
816
821
|
const approvedCalls: ToolCall[] = [];
|
|
817
822
|
const denialMessages: ToolMessage[] = [];
|
|
@@ -820,7 +825,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
820
825
|
if (
|
|
821
826
|
this.requiresApproval(call.name, call.args as Record<string, unknown>)
|
|
822
827
|
) {
|
|
823
|
-
const approvalResponse =
|
|
828
|
+
const approvalResponse = this.requestApproval(call, config);
|
|
824
829
|
if (!approvalResponse.approved) {
|
|
825
830
|
denialMessages.push(
|
|
826
831
|
new ToolMessage({
|