UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

182 lines (181 loc) • 6.83 kB
/** * Realtime data-channel event bridge. * * The cascaded bridge (`eventBridge.ts`) is driven by NeuroLink's event emitter. */ import { z } from "zod"; import { logger } from "../../utils/logger.js"; const DEFAULT_EVENTS_TOPIC = "ai-events"; const DEFAULT_CONTROL_TOPIC = "ai-control"; const DEFAULT_MAX_INLINE_BYTES = 12_000; const DEFAULT_HITL_TIMEOUT_MS = 45_000; const hitlControlMessageSchema = z.object({ action: z.enum(["hitl:accept", "hitl:reject"]), confirmationId: z.string(), }); /** * Attach the realtime event bridge to a room. * * Returns `publishEvent` (worker → browser), `requestConfirmation` (HITL * round-trip), and `dispose` (remove the control listener, decline anything * still pending). */ export async function attachRealtimeEventBridge(params) { const { room } = params; const eventsTopic = params.eventsTopic ?? DEFAULT_EVENTS_TOPIC; const controlTopic = params.controlTopic ?? DEFAULT_CONTROL_TOPIC; const maxInlineBytes = params.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES; const hitlTimeoutMs = params.hitlTimeoutMs ?? DEFAULT_HITL_TIMEOUT_MS; const { RoomEvent } = await import("@livekit/rtc-node"); const encoder = new TextEncoder(); const decoder = new TextDecoder(); let seq = 0; let disposed = false; const startedAt = Date.now(); const publishEvent = (type, data) => { if (disposed) { return; } const participant = room.localParticipant; if (!participant) { return; } try { seq += 1; const json = JSON.stringify({ seq, ts: Date.now(), type, data }); const bytes = encoder.encode(json); const via = bytes.byteLength <= maxInlineBytes ? "data" : "stream"; logger.debug("realtime.bridge.publish", { ms: Date.now() - startedAt, seq, type, via, bytes: bytes.byteLength, }); // Both calls are detached on purpose — the bridge must never make the // caller wait on the UI. But `void` alone does NOT make a rejection // harmless, and the try/catch below cannot help: a synchronous catch // never sees a rejected promise. Measured — a rejecting publish with // this exact shape leaves the catch untouched and, with no // unhandledRejection handler installed, terminates the process: // SYNC_CATCH_FIRED = false · UNHANDLED_COUNT = 1 · exit 1 // These are network calls to a peer that can disconnect mid-publish, so // the rejection is ordinary, not exotic. Attaching a handler is what // actually makes the bridge best-effort, which is what the catch below // only claimed. Same class as the detached-pump crash in anthropic. const publishFailed = (error) => { logger.debug("realtime.bridge.publishFailed", { seq, type, via, error: error instanceof Error ? error.message : String(error), }); }; if (via === "data") { void participant .publishData(bytes, { reliable: true, topic: eventsTopic }) .catch(publishFailed); } else { void participant .sendText(json, { topic: eventsTopic }) .catch(publishFailed); } } catch { /* non-fatal — covers the synchronous half only (encode/stringify); the detached publishes carry their own handler above. */ } }; // HITL: WRITE-labeled tools pause for user confirmation. We publish a // `hitl-prompt`, the UI replies on the control topic, and we resolve the // pending request by confirmationId. A timeout is treated as a decline so a // turn can never hang waiting on the user. const pendingHitl = new Map(); let hitlSeq = 0; const onData = (...args) => { const payload = args[0]; const topic = args[3]; if (topic !== controlTopic || !(payload instanceof Uint8Array)) { return; } let raw; try { raw = JSON.parse(decoder.decode(payload)); } catch { return; } const message = hitlControlMessageSchema.safeParse(raw); if (!message.success) { return; } const { action, confirmationId } = message.data; const resolver = pendingHitl.get(confirmationId); if (resolver) { logger.debug("realtime.bridge.controlReceived", { action, confirmationId, }); pendingHitl.delete(confirmationId); resolver(action === "hitl:accept"); } else { logger.warn("realtime.bridge.controlUnmatched", { action, confirmationId, }); } }; room.on(RoomEvent.DataReceived, onData); const requestConfirmation = (toolName, args) => { if (disposed) { logger.warn("realtime.bridge.hitlDisposed", { toolName }); return Promise.resolve(false); } hitlSeq += 1; const confirmationId = `hitl_${seq}_${hitlSeq}`; publishEvent("hitl-prompt", { confirmationId, toolName, actionType: `Run ${toolName}`, arguments: args ?? {}, }); logger.info("realtime.bridge.hitlPrompt", { toolName, confirmationId }); return new Promise((resolve) => { const timer = setTimeout(() => { pendingHitl.delete(confirmationId); logger.warn("realtime.bridge.hitlTimeout", { toolName, confirmationId, timeoutMs: hitlTimeoutMs, }); resolve(false); }, hitlTimeoutMs); pendingHitl.set(confirmationId, (approved) => { clearTimeout(timer); logger.info("realtime.bridge.hitlResolved", { toolName, confirmationId, approved, }); resolve(approved); }); }); }; return { publishEvent, requestConfirmation, dispose() { if (disposed) { return; } disposed = true; room.off(RoomEvent.DataReceived, onData); for (const [, resolver] of pendingHitl) { resolver(false); } pendingHitl.clear(); }, }; }