/* eslint-disable no-console */ // src/utils/events.ts import { dispatchCustomEvent } from '@langchain/core/callbacks/dispatch'; import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons'; import type { RunnableConfig } from '@langchain/core/runnables'; /** * Returns the RunnableConfig currently active in LangChain's AsyncLocalStorage, * or undefined if none is installed. This is the per-async-branch config that * LangGraph installs when entering a node — it carries the correct * `metadata.spawnKey` for child subgraph invocations inside `Promise.all` * parallel handoffs. * * Prefer this over any Graph-instance-cached config (e.g. `this.config`) * when dispatching events from code that may run concurrently across multiple * child subgraphs. An instance-level cache is shared state and races between * siblings — the last child to enter wins, so events fire with the wrong * child's metadata and the backend routes them to the wrong spawnKey. */ export function getCurrentRunnableConfig(): RunnableConfig | undefined { try { return AsyncLocalStorageProviderSingleton.getInstance().getStore() as | RunnableConfig | undefined; } catch { return undefined; } } /** * Safely dispatches a custom event and properly awaits it to avoid * race conditions where events are dispatched after run cleanup. * * **Parallel-handoff correctness:** callers should prefer passing * `undefined` (or the per-node runtime config). When `config` is omitted, * LangChain's `ensureConfig` reads the current RunnableConfig from * AsyncLocalStorage, which is correctly isolated per async branch under * `Promise.all`. Passing a stale instance-cached config overrides that * implicit config's metadata and cross-contaminates parallel children. */ export async function safeDispatchCustomEvent( event: string, payload: unknown, config?: RunnableConfig ): Promise { try { // If the caller did not pass a config, fall back to the current // AsyncLocalStorage-resident runnable config so nested Promise.all // branches each use their own metadata. const effectiveConfig = config ?? getCurrentRunnableConfig(); await dispatchCustomEvent(event, payload, effectiveConfig); } catch (e) { // Check if this is the known EventStreamCallbackHandler error if ( e instanceof Error && e.message.includes('handleCustomEvent: Run ID') && e.message.includes('not found in run map') ) { // Suppress — expected during parallel/async execution return; } // Log other errors console.error('Error dispatching custom event:', e); } }