@illuma-ai/agents 1.1.18 → 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 +133 -6
- 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 +47 -1
- 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 +133 -6
- 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 +47 -1
- 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/graphs/MultiAgentGraph.d.ts +6 -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 +40 -0
- package/dist/types/types/run.d.ts +6 -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 +166 -6
- 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 +57 -2
- 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 +41 -0
- package/src/types/run.ts +6 -0
- package/src/types/tools.ts +7 -9
- package/src/utils/run.ts +9 -1
package/dist/types/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import './instrumentation';
|
|
2
|
+
import { Command } from '@langchain/langgraph';
|
|
2
3
|
import type { MessageContentComplex, BaseMessage } from '@langchain/core/messages';
|
|
3
4
|
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
4
5
|
import type * as t from '@/types';
|
|
@@ -41,11 +42,34 @@ export declare class Run<_T extends t.BaseGraphState> {
|
|
|
41
42
|
* and processes them through our handler registry instead of EventStreamCallbackHandler
|
|
42
43
|
*/
|
|
43
44
|
private createCustomEventCallback;
|
|
44
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Processes the graph stream for a given input.
|
|
47
|
+
*
|
|
48
|
+
* @param inputs - Either the initial state (IState) for a new run, or a
|
|
49
|
+
* Command (e.g., `new Command({ resume: ... })`) to resume from an interrupt.
|
|
50
|
+
* @param config - Runnable config with version and optional run_id.
|
|
51
|
+
* @param streamOptions - Optional stream event callbacks and options.
|
|
52
|
+
*/
|
|
53
|
+
processStream(inputs: t.IState | Command, config: Partial<RunnableConfig> & {
|
|
45
54
|
version: 'v1' | 'v2';
|
|
46
55
|
run_id?: string;
|
|
47
56
|
}, streamOptions?: t.EventStreamOptions): Promise<MessageContentComplex[] | undefined>;
|
|
48
57
|
private createSystemCallback;
|
|
58
|
+
/**
|
|
59
|
+
* Checks whether the graph was interrupted (e.g., by HITL tool approval).
|
|
60
|
+
* Call after processStream() returns to determine if the graph is waiting
|
|
61
|
+
* for a resume via Command({ resume }).
|
|
62
|
+
*
|
|
63
|
+
* Requires a checkpointer to be configured — without one, interrupt state
|
|
64
|
+
* is not persisted and this always returns false.
|
|
65
|
+
*/
|
|
66
|
+
hasInterrupts(config: Partial<RunnableConfig>): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Returns the interrupt values from the graph state.
|
|
69
|
+
* Each interrupt's `value` contains the data passed to `interrupt()` by the node
|
|
70
|
+
* (e.g., a ToolApprovalRequest for HITL).
|
|
71
|
+
*/
|
|
72
|
+
getInterruptValues(config: Partial<RunnableConfig>): Promise<unknown[]>;
|
|
49
73
|
getCallbacks(clientCallbacks: t.ClientCallbacks): t.SystemCallbacks;
|
|
50
74
|
generateTitle({ provider, inputText, contentParts, titlePrompt, clientOptions, chainOptions, skipLanguage, titleMethod, titlePromptTemplate, }: t.RunTitleOptions): Promise<{
|
|
51
75
|
language?: string;
|
|
@@ -48,12 +48,14 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
|
|
|
48
48
|
*/
|
|
49
49
|
private requiresApproval;
|
|
50
50
|
/**
|
|
51
|
-
* Requests human approval for a tool call
|
|
52
|
-
* Dispatches an ON_TOOL_APPROVAL_REQUIRED event and waits for the host
|
|
53
|
-
* to resolve the promise with an approval response.
|
|
51
|
+
* Requests human approval for a tool call using LangGraph's native interrupt().
|
|
54
52
|
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* Flow:
|
|
54
|
+
* 1. Dispatches ON_TOOL_APPROVAL_REQUIRED notification (no resolve/reject — data only)
|
|
55
|
+
* so the host can persist the request and send UI events.
|
|
56
|
+
* 2. Calls interrupt() which checkpoints graph state and pauses execution.
|
|
57
|
+
* 3. When the host resumes via Command({ resume: ToolApprovalResponse }),
|
|
58
|
+
* interrupt() returns the response synchronously.
|
|
57
59
|
*
|
|
58
60
|
* @param call - The tool call requiring approval
|
|
59
61
|
* @param config - The runnable config for event dispatch
|
|
@@ -221,6 +221,31 @@ export type StandardGraphInput = {
|
|
|
221
221
|
tokenCounter?: TokenCounter;
|
|
222
222
|
indexTokenCountMap?: Record<string, number>;
|
|
223
223
|
};
|
|
224
|
+
/**
|
|
225
|
+
* Configuration for an approval gate placed on a sequence edge.
|
|
226
|
+
* When present, the graph inserts an approval gate node between the source
|
|
227
|
+
* and destination agents. The gate ALWAYS fires (regardless of ExecutionContext)
|
|
228
|
+
* and calls interrupt() to pause the graph for human approval.
|
|
229
|
+
*/
|
|
230
|
+
export type ApprovalGateConfig = {
|
|
231
|
+
/** Unique identifier for this gate (used as node ID suffix) */
|
|
232
|
+
gateId: string;
|
|
233
|
+
/**
|
|
234
|
+
* Approval channel — where the approval UI is rendered.
|
|
235
|
+
* - 'chat': SSE-based chat UI (default)
|
|
236
|
+
* - 'outlook': MS Graph Actionable Messages
|
|
237
|
+
* - 'telegram': Telegram Bot inline keyboard
|
|
238
|
+
*/
|
|
239
|
+
channel?: 'chat' | 'outlook' | 'telegram';
|
|
240
|
+
/** Optional human-readable prompt shown to the approver */
|
|
241
|
+
prompt?: string;
|
|
242
|
+
/** Optional approver identifier (e.g., email, user ID) */
|
|
243
|
+
approver?: string;
|
|
244
|
+
/** Timeout in ms before the gate auto-expires (default: 5 minutes) */
|
|
245
|
+
timeoutMs?: number;
|
|
246
|
+
/** What to do on denial: 'stop' ends the graph, 'skip' skips the destination agent */
|
|
247
|
+
onDeny?: 'stop' | 'skip';
|
|
248
|
+
};
|
|
224
249
|
export type GraphEdge = {
|
|
225
250
|
/** Agent ID, use a list for multiple sources */
|
|
226
251
|
from: string | string[];
|
|
@@ -263,9 +288,24 @@ export type GraphEdge = {
|
|
|
263
288
|
* Defaults to DEFAULT_HANDOFF_MAX_RESULT_CHARS (32768 chars, ~8192 tokens).
|
|
264
289
|
*/
|
|
265
290
|
maxResultChars?: number;
|
|
291
|
+
/**
|
|
292
|
+
* Approval gate configuration for sequence edges.
|
|
293
|
+
* When set, inserts an approval gate node between source and destination.
|
|
294
|
+
* The gate ALWAYS fires regardless of ExecutionContext (unlike tool approval).
|
|
295
|
+
*/
|
|
296
|
+
approvalGate?: ApprovalGateConfig;
|
|
266
297
|
};
|
|
267
298
|
export type MultiAgentGraphInput = StandardGraphInput & {
|
|
268
299
|
edges: GraphEdge[];
|
|
300
|
+
/**
|
|
301
|
+
* When set, the graph routes START to this agent instead of the default starting nodes.
|
|
302
|
+
* Used for multi-turn resumption: the caller reads `lastActiveAgentId` from the
|
|
303
|
+
* previous turn's metadata and passes it here so follow-up messages route to the
|
|
304
|
+
* agent that last handled the conversation.
|
|
305
|
+
*
|
|
306
|
+
* If the agent ID is invalid (not in the graph), falls back to default starting nodes.
|
|
307
|
+
*/
|
|
308
|
+
resumeFromAgentId?: string;
|
|
269
309
|
};
|
|
270
310
|
/**
|
|
271
311
|
* Structured output mode determines how the agent returns structured data.
|
|
@@ -91,6 +91,12 @@ export type MultiAgentGraphConfig = {
|
|
|
91
91
|
compileOptions?: g.CompileOptions;
|
|
92
92
|
agents: g.AgentInputs[];
|
|
93
93
|
edges: g.GraphEdge[];
|
|
94
|
+
/**
|
|
95
|
+
* Resume from a specific agent on the next turn.
|
|
96
|
+
* When set, START routes directly to this agent instead of default starting nodes.
|
|
97
|
+
* @see MultiAgentGraphInput.resumeFromAgentId
|
|
98
|
+
*/
|
|
99
|
+
resumeFromAgentId?: string;
|
|
94
100
|
};
|
|
95
101
|
export type StandardGraphConfig = Omit<MultiAgentGraphConfig, 'edges' | 'type'> & {
|
|
96
102
|
type?: 'standard';
|
|
@@ -271,16 +271,14 @@ export type ToolApprovalResponse = {
|
|
|
271
271
|
modifiedArgs?: Record<string, unknown>;
|
|
272
272
|
};
|
|
273
273
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
274
|
+
* Notification payload dispatched via ON_TOOL_APPROVAL_REQUIRED.
|
|
275
|
+
* Data-only — no resolve/reject callbacks. The approval mechanism is handled
|
|
276
|
+
* by LangGraph's native interrupt()/Command({ resume }) pattern.
|
|
277
|
+
*
|
|
278
|
+
* The host persists this data (e.g., in MongoDB) and sends UI events.
|
|
279
|
+
* The actual approval response comes back via Command({ resume: ToolApprovalResponse }).
|
|
277
280
|
*/
|
|
278
|
-
export type
|
|
279
|
-
/** Promise resolver - host calls this with the approval response */
|
|
280
|
-
resolve: (response: ToolApprovalResponse) => void;
|
|
281
|
-
/** Promise rejector - host calls this on fatal error */
|
|
282
|
-
reject: (error: Error) => void;
|
|
283
|
-
};
|
|
281
|
+
export type ToolApprovalNotification = ToolApprovalRequest;
|
|
284
282
|
/** Search mode: code_interpreter uses external sandbox, local uses safe substring matching */
|
|
285
283
|
export type ToolSearchMode = 'code_interpreter' | 'local';
|
|
286
284
|
/** Format for MCP tool names in search results */
|
package/package.json
CHANGED
package/src/common/enum.ts
CHANGED
|
@@ -31,6 +31,8 @@ export enum GraphEvents {
|
|
|
31
31
|
ON_TOOL_APPROVAL_REQUIRED = 'on_tool_approval_required',
|
|
32
32
|
/** [Custom] Agent transition event — dispatched when control passes between agents */
|
|
33
33
|
ON_AGENT_TRANSITION = 'on_agent_transition',
|
|
34
|
+
/** [Custom] Approval gate interrupt — dispatched by ApprovalGateNode before interrupt() */
|
|
35
|
+
ON_APPROVAL_GATE = 'on_approval_gate',
|
|
34
36
|
|
|
35
37
|
/* Official Events */
|
|
36
38
|
|
|
@@ -29,6 +29,10 @@ import {
|
|
|
29
29
|
DEFAULT_HANDOFF_MAX_RESULT_CHARS,
|
|
30
30
|
} from '@/common';
|
|
31
31
|
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
32
|
+
import {
|
|
33
|
+
createApprovalGateNode,
|
|
34
|
+
getApprovalGateNodeId,
|
|
35
|
+
} from '@/nodes/ApprovalGateNode';
|
|
32
36
|
|
|
33
37
|
/** Pattern to extract instructions from transfer ToolMessage content */
|
|
34
38
|
const TRANSFER_INSTRUCTIONS_PATTERN = /(?:Instructions?|Context):\s*(.+)/is;
|
|
@@ -78,9 +82,17 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
78
82
|
*/
|
|
79
83
|
private lastActiveAgentId: string | undefined;
|
|
80
84
|
|
|
85
|
+
/**
|
|
86
|
+
* When set, the graph routes START to this agent instead of the default starting nodes.
|
|
87
|
+
* Enables multi-turn resumption: follow-up messages go to the agent that last handled
|
|
88
|
+
* the conversation rather than restarting from the root/router agent.
|
|
89
|
+
*/
|
|
90
|
+
private resumeFromAgentId: string | undefined;
|
|
91
|
+
|
|
81
92
|
constructor(input: t.MultiAgentGraphInput) {
|
|
82
93
|
super(input);
|
|
83
94
|
this.edges = input.edges;
|
|
95
|
+
this.resumeFromAgentId = input.resumeFromAgentId;
|
|
84
96
|
this.categorizeEdges();
|
|
85
97
|
this.analyzeGraph();
|
|
86
98
|
this.createTransferTools();
|
|
@@ -1174,7 +1186,39 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1174
1186
|
|
|
1175
1187
|
const builder = new StateGraph(StateAnnotation);
|
|
1176
1188
|
|
|
1177
|
-
|
|
1189
|
+
/**
|
|
1190
|
+
* Identify agents that are ONLY handoff destinations (not transfer/sequence
|
|
1191
|
+
* destinations and not starting nodes). These agents are invoked inline via
|
|
1192
|
+
* subgraph.invoke() inside handoff tools — they must NOT be added as
|
|
1193
|
+
* top-level nodes in the parent graph because LangGraph validates that all
|
|
1194
|
+
* nodes are reachable from START via edges.
|
|
1195
|
+
*/
|
|
1196
|
+
const handoffOnlyDestinations = new Set<string>();
|
|
1197
|
+
const transferOrSequenceDestinations = new Set<string>();
|
|
1198
|
+
|
|
1199
|
+
for (const edge of this.handoffEdges) {
|
|
1200
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1201
|
+
dests.forEach((d) => handoffOnlyDestinations.add(d));
|
|
1202
|
+
}
|
|
1203
|
+
for (const edge of [...this.transferEdges, ...this.sequenceEdges]) {
|
|
1204
|
+
const dests = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1205
|
+
dests.forEach((d) => transferOrSequenceDestinations.add(d));
|
|
1206
|
+
}
|
|
1207
|
+
// Remove agents that are also transfer/sequence destinations or starting nodes
|
|
1208
|
+
for (const d of transferOrSequenceDestinations) {
|
|
1209
|
+
handoffOnlyDestinations.delete(d);
|
|
1210
|
+
}
|
|
1211
|
+
for (const startNode of this.startingNodes) {
|
|
1212
|
+
handoffOnlyDestinations.delete(startNode);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
if (handoffOnlyDestinations.size > 0) {
|
|
1216
|
+
console.debug(
|
|
1217
|
+
`[MultiAgentGraph] Handoff-only children (subgraph only, no top-level node): [${Array.from(handoffOnlyDestinations).join(', ')}]`
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Add agents as nodes — skip handoff-only children (they exist as subgraphs only)
|
|
1178
1222
|
for (const [agentId] of this.agentContexts) {
|
|
1179
1223
|
// Get all possible destinations for this agent
|
|
1180
1224
|
const transferDestinations = new Set<string>();
|
|
@@ -1218,6 +1262,15 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1218
1262
|
/** Register subgraph for handoff tools (lazy reference resolution) */
|
|
1219
1263
|
this.subgraphRegistry.set(agentId, agentSubgraph);
|
|
1220
1264
|
|
|
1265
|
+
/**
|
|
1266
|
+
* Handoff-only children are invoked inline via subgraph.invoke() — they
|
|
1267
|
+
* don't need a top-level node. Adding them would cause LangGraph to reject
|
|
1268
|
+
* the graph because no edge routes to them (UNREACHABLE_NODE).
|
|
1269
|
+
*/
|
|
1270
|
+
if (handoffOnlyDestinations.has(agentId)) {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1221
1274
|
/** Wrapper function that handles agentMessages channel, handoff reception, and conditional routing */
|
|
1222
1275
|
const agentWrapper = async (
|
|
1223
1276
|
state: t.MultiAgentGraphState,
|
|
@@ -1585,20 +1638,127 @@ export class MultiAgentGraph extends StandardGraph {
|
|
|
1585
1638
|
});
|
|
1586
1639
|
}
|
|
1587
1640
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1641
|
+
/**
|
|
1642
|
+
* Add starting edges from START to entry agent(s).
|
|
1643
|
+
*
|
|
1644
|
+
* Multi-turn resumption: when `resumeFromAgentId` is set and refers to a
|
|
1645
|
+
* valid agent in this graph, START routes exclusively to that agent so
|
|
1646
|
+
* follow-up messages continue where the previous turn left off.
|
|
1647
|
+
*
|
|
1648
|
+
* Default behavior (no resume): static edges to all starting nodes,
|
|
1649
|
+
* preserving parallel execution for graphs with multiple entry points.
|
|
1650
|
+
*/
|
|
1651
|
+
const validResumeAgent =
|
|
1652
|
+
this.resumeFromAgentId != null &&
|
|
1653
|
+
this.agentContexts.has(this.resumeFromAgentId);
|
|
1654
|
+
|
|
1655
|
+
if (validResumeAgent) {
|
|
1656
|
+
const resumeAgentId = this.resumeFromAgentId!;
|
|
1657
|
+
console.debug(
|
|
1658
|
+
`[MultiAgentGraph] Multi-turn resumption: routing START → "${resumeAgentId}" (skipping default starting nodes: [${Array.from(this.startingNodes).join(', ')}])`
|
|
1659
|
+
);
|
|
1660
|
+
|
|
1661
|
+
/**
|
|
1662
|
+
* Build route map containing both the resume agent and default starting
|
|
1663
|
+
* nodes. This is required by LangGraph — all possible destinations must
|
|
1664
|
+
* be declared even if the router always picks one.
|
|
1665
|
+
*/
|
|
1666
|
+
const allPossibleStarts = new Set([
|
|
1667
|
+
...this.startingNodes,
|
|
1668
|
+
resumeAgentId,
|
|
1669
|
+
]);
|
|
1670
|
+
const routeMap: Record<string, string> = {};
|
|
1671
|
+
for (const nodeId of allPossibleStarts) {
|
|
1672
|
+
routeMap[nodeId] = nodeId;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
builder.addConditionalEdges(
|
|
1676
|
+
START,
|
|
1677
|
+
() => resumeAgentId,
|
|
1678
|
+
routeMap as unknown as never
|
|
1679
|
+
);
|
|
1680
|
+
} else {
|
|
1681
|
+
if (this.resumeFromAgentId != null) {
|
|
1682
|
+
console.warn(
|
|
1683
|
+
`[MultiAgentGraph] resumeFromAgentId "${this.resumeFromAgentId}" not found in graph — falling back to default starting nodes`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
for (const startNode of this.startingNodes) {
|
|
1687
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1688
|
+
/** @ts-ignore */
|
|
1689
|
+
builder.addEdge(START, startNode);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Add approval gate nodes for sequence edges with approvalGate config.
|
|
1695
|
+
* Gates are inserted between source and destination agents.
|
|
1696
|
+
* They ALWAYS fire regardless of ExecutionContext.
|
|
1697
|
+
*/
|
|
1698
|
+
const gatedEdges = new Set<t.GraphEdge>();
|
|
1699
|
+
|
|
1700
|
+
for (const edge of this.sequenceEdges) {
|
|
1701
|
+
if (!edge.approvalGate) {
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const sources = Array.isArray(edge.from) ? edge.from : [edge.from];
|
|
1706
|
+
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1707
|
+
|
|
1708
|
+
for (const source of sources) {
|
|
1709
|
+
for (const dest of destinations) {
|
|
1710
|
+
const gateNodeId = getApprovalGateNodeId(edge.approvalGate.gateId);
|
|
1711
|
+
const onDeny = edge.approvalGate.onDeny ?? 'stop';
|
|
1712
|
+
|
|
1713
|
+
// Add the gate node
|
|
1714
|
+
const gateNode = createApprovalGateNode(
|
|
1715
|
+
edge.approvalGate,
|
|
1716
|
+
source,
|
|
1717
|
+
dest,
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1721
|
+
/** @ts-ignore */
|
|
1722
|
+
builder.addNode(gateNodeId, gateNode);
|
|
1723
|
+
|
|
1724
|
+
// Wire: source → gate
|
|
1725
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1726
|
+
/** @ts-ignore */
|
|
1727
|
+
builder.addEdge(source, gateNodeId);
|
|
1728
|
+
|
|
1729
|
+
// Wire: gate → destination (always, since approval is handled
|
|
1730
|
+
// by the interrupt/resume mechanism — if denied, the host
|
|
1731
|
+
// can choose not to resume, or resume with approved=false
|
|
1732
|
+
// and the gate returns empty state)
|
|
1733
|
+
if (onDeny === 'skip') {
|
|
1734
|
+
// Conditional edge: approved → destination, denied → END
|
|
1735
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1736
|
+
/** @ts-ignore */
|
|
1737
|
+
builder.addEdge(gateNodeId, dest);
|
|
1738
|
+
} else {
|
|
1739
|
+
// Direct edge to destination — denial stops via non-resume or
|
|
1740
|
+
// the host terminates the graph
|
|
1741
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
1742
|
+
/** @ts-ignore */
|
|
1743
|
+
builder.addEdge(gateNodeId, dest);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
gatedEdges.add(edge);
|
|
1593
1749
|
}
|
|
1594
1750
|
|
|
1595
1751
|
/**
|
|
1596
1752
|
* Add sequence edges for automatic transitions
|
|
1597
1753
|
* Group edges by destination to handle fan-in scenarios
|
|
1754
|
+
* Skip edges that have approval gates (already handled above)
|
|
1598
1755
|
*/
|
|
1599
1756
|
const edgesByDestination = new Map<string, t.GraphEdge[]>();
|
|
1600
1757
|
|
|
1601
1758
|
for (const edge of this.sequenceEdges) {
|
|
1759
|
+
if (gatedEdges.has(edge)) {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1602
1762
|
const destinations = Array.isArray(edge.to) ? edge.to : [edge.to];
|
|
1603
1763
|
for (const destination of destinations) {
|
|
1604
1764
|
if (!edgesByDestination.has(destination)) {
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { interrupt } from '@langchain/langgraph';
|
|
2
|
+
import type { RunnableConfig } from '@langchain/core/runnables';
|
|
3
|
+
import type { ApprovalGateConfig, BaseGraphState } from '@/types/graph';
|
|
4
|
+
import type { ToolApprovalResponse } from '@/types/tools';
|
|
5
|
+
import { GraphEvents } from '@/common';
|
|
6
|
+
import { safeDispatchCustomEvent } from '@/utils/events';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Interrupt payload for approval gate nodes.
|
|
10
|
+
* Passed to interrupt() and persisted in the checkpoint.
|
|
11
|
+
*/
|
|
12
|
+
export interface ApprovalGateInterrupt {
|
|
13
|
+
/** Discriminator to distinguish from tool approval interrupts */
|
|
14
|
+
type: 'approval_gate';
|
|
15
|
+
/** Unique gate identifier */
|
|
16
|
+
gateId: string;
|
|
17
|
+
/** Approval channel (chat, outlook, telegram) */
|
|
18
|
+
channel: string;
|
|
19
|
+
/** Human-readable prompt for the approver */
|
|
20
|
+
prompt?: string;
|
|
21
|
+
/** Approver identifier */
|
|
22
|
+
approver?: string;
|
|
23
|
+
/** Timeout in ms */
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Source agent ID (who just finished) */
|
|
26
|
+
sourceAgentId?: string;
|
|
27
|
+
/** Destination agent ID (who will run next if approved) */
|
|
28
|
+
destinationAgentId?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a graph node function that acts as an approval gate.
|
|
33
|
+
*
|
|
34
|
+
* Unlike tool approval (which respects ExecutionContext and can be auto-approved
|
|
35
|
+
* in scheduled/handoff modes), approval gates ALWAYS fire. They are placed by
|
|
36
|
+
* the builder between agents in a sequence and represent explicit human
|
|
37
|
+
* checkpoints that cannot be bypassed.
|
|
38
|
+
*
|
|
39
|
+
* Flow:
|
|
40
|
+
* 1. Dispatch ON_APPROVAL_GATE notification (for SSE/persistence)
|
|
41
|
+
* 2. Call interrupt() — graph pauses, state is checkpointed
|
|
42
|
+
* 3. On resume, interrupt() returns the ToolApprovalResponse
|
|
43
|
+
* 4. If approved, pass state through (next agent runs)
|
|
44
|
+
* 5. If denied, return state as-is (routing handled by conditional edge)
|
|
45
|
+
*
|
|
46
|
+
* @param config - The approval gate configuration from the edge definition
|
|
47
|
+
* @param sourceAgentId - The agent that precedes this gate
|
|
48
|
+
* @param destinationAgentId - The agent that follows this gate
|
|
49
|
+
*/
|
|
50
|
+
export function createApprovalGateNode(
|
|
51
|
+
config: ApprovalGateConfig,
|
|
52
|
+
sourceAgentId: string,
|
|
53
|
+
destinationAgentId: string,
|
|
54
|
+
) {
|
|
55
|
+
const {
|
|
56
|
+
gateId,
|
|
57
|
+
channel = 'chat',
|
|
58
|
+
prompt,
|
|
59
|
+
approver,
|
|
60
|
+
timeoutMs,
|
|
61
|
+
} = config;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The gate node function. Receives the current graph state,
|
|
65
|
+
* dispatches a notification, calls interrupt(), and returns
|
|
66
|
+
* the state with an approval result annotation.
|
|
67
|
+
*/
|
|
68
|
+
return async function approvalGateNode(
|
|
69
|
+
state: BaseGraphState,
|
|
70
|
+
runnableConfig?: RunnableConfig,
|
|
71
|
+
): Promise<Partial<BaseGraphState>> {
|
|
72
|
+
const interruptPayload: ApprovalGateInterrupt = {
|
|
73
|
+
type: 'approval_gate',
|
|
74
|
+
gateId,
|
|
75
|
+
channel,
|
|
76
|
+
prompt,
|
|
77
|
+
approver,
|
|
78
|
+
timeoutMs,
|
|
79
|
+
sourceAgentId,
|
|
80
|
+
destinationAgentId,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Dispatch notification event so the host can:
|
|
84
|
+
// 1. Persist the approval request to MongoDB
|
|
85
|
+
// 2. Route to the appropriate channel adapter
|
|
86
|
+
// 3. Emit SSE event for chat UI
|
|
87
|
+
safeDispatchCustomEvent(
|
|
88
|
+
GraphEvents.ON_APPROVAL_GATE,
|
|
89
|
+
interruptPayload,
|
|
90
|
+
runnableConfig,
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Pause the graph — state is checkpointed by the MongoDBSaver.
|
|
94
|
+
// On resume via Command({ resume: ToolApprovalResponse }), interrupt()
|
|
95
|
+
// returns the response value.
|
|
96
|
+
const response = interrupt(interruptPayload) as ToolApprovalResponse;
|
|
97
|
+
|
|
98
|
+
// Return empty state update — the graph structure (conditional edges)
|
|
99
|
+
// handles routing based on the approval result. We store the response
|
|
100
|
+
// in a message so downstream nodes can access it if needed.
|
|
101
|
+
if (response.approved) {
|
|
102
|
+
return {};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// On denial, we could add a system message noting the denial.
|
|
106
|
+
// The conditional edge after this node will route to END or skip.
|
|
107
|
+
return {};
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Node ID for an approval gate, derived from the gate configuration.
|
|
113
|
+
* Used by MultiAgentGraph when inserting gate nodes into the graph.
|
|
114
|
+
*/
|
|
115
|
+
export function getApprovalGateNodeId(gateId: string): string {
|
|
116
|
+
return `approval_gate_${gateId}`;
|
|
117
|
+
}
|