import { interrupt } from '@langchain/langgraph'; import type { RunnableConfig } from '@langchain/core/runnables'; import type { ApprovalGateConfig, BaseGraphState } from '@/types/graph'; import type { ToolApprovalResponse } from '@/types/tools'; import { GraphEvents } from '@/common'; import { safeDispatchCustomEvent } from '@/utils/events'; /** * Interrupt payload for approval gate nodes. * Passed to interrupt() and persisted in the checkpoint. */ export interface ApprovalGateInterrupt { /** Discriminator to distinguish from tool approval interrupts */ type: 'approval_gate'; /** Unique gate identifier */ gateId: string; /** Approval channel (chat, outlook, telegram) */ channel: string; /** Human-readable prompt for the approver */ prompt?: string; /** Approver identifier */ approver?: string; /** Timeout in ms */ timeoutMs?: number; /** Source agent ID (who just finished) */ sourceAgentId?: string; /** Destination agent ID (who will run next if approved) */ destinationAgentId?: string; } /** * Creates a graph node function that acts as an approval gate. * * Unlike tool approval (which respects ExecutionContext and can be auto-approved * in scheduled/handoff modes), approval gates ALWAYS fire. They are placed by * the builder between agents in a sequence and represent explicit human * checkpoints that cannot be bypassed. * * Flow: * 1. Dispatch ON_APPROVAL_GATE notification (for SSE/persistence) * 2. Call interrupt() — graph pauses, state is checkpointed * 3. On resume, interrupt() returns the ToolApprovalResponse * 4. If approved, pass state through (next agent runs) * 5. If denied, return state as-is (routing handled by conditional edge) * * @param config - The approval gate configuration from the edge definition * @param sourceAgentId - The agent that precedes this gate * @param destinationAgentId - The agent that follows this gate */ export function createApprovalGateNode( config: ApprovalGateConfig, sourceAgentId: string, destinationAgentId: string ) { const { gateId, channel = 'chat', prompt, approver, timeoutMs } = config; /** * The gate node function. Receives the current graph state, * dispatches a notification, calls interrupt(), and returns * the state with an approval result annotation. */ return async function approvalGateNode( state: BaseGraphState, runnableConfig?: RunnableConfig ): Promise> { const interruptPayload: ApprovalGateInterrupt = { type: 'approval_gate', gateId, channel, prompt, approver, timeoutMs, sourceAgentId, destinationAgentId, }; // Dispatch notification event so the host can: // 1. Persist the approval request to MongoDB // 2. Route to the appropriate channel adapter // 3. Emit SSE event for chat UI safeDispatchCustomEvent( GraphEvents.ON_APPROVAL_GATE, interruptPayload, runnableConfig ); // Pause the graph — state is checkpointed by the MongoDBSaver. // On resume via Command({ resume: ToolApprovalResponse }), interrupt() // returns the response value. const response = interrupt(interruptPayload) as ToolApprovalResponse; // Return empty state update — the graph structure (conditional edges) // handles routing based on the approval result. We store the response // in a message so downstream nodes can access it if needed. if (response.approved) { return {}; } // On denial, we could add a system message noting the denial. // The conditional edge after this node will route to END or skip. return {}; }; } /** * Node ID for an approval gate, derived from the gate configuration. * Used by MultiAgentGraph when inserting gate nodes into the graph. */ export function getApprovalGateNodeId(gateId: string): string { return `approval_gate_${gateId}`; }