@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
360 lines (359 loc) • 12.7 kB
JavaScript
/**
* Data-channel event bridge.
*
* Connects NeuroLink's event emitter to the LiveKit room's data channel: it
* forwards NeuroLink events (text deltas, tool start/result, HITL prompts,
* stream lifecycle) to the browser as small versioned envelopes, and accepts
* control messages (HITL responses) back, re-emitting them onto the emitter so
* NeuroLink's HITL manager resolves.
*
* This is the WebRTC counterpart of the chat-mode SSE controller: same event
* source, different transport. The browser renders the envelopes (transcript,
* tool status, charts, confirmation prompts).
*
* `@livekit/rtc-node` is an optional dependency, imported dynamically only for
* the `RoomEvent` enum value. All payloads arrive typed as `unknown` and are
* narrowed with runtime guards — no type assertions.
*
* See docs/features/livekit-voice-agent.md.
*/
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;
/** NeuroLink emitter event names this bridge forwards. */
const USER_TEXT_EVENT = "voice:user-transcript";
const TEXT_EVENT = "response:chunk";
const TOOL_START_EVENT = "tool:start";
const TOOL_END_EVENT = "tool:end";
const HITL_REQUEST_EVENT = "hitl:confirmation-request";
const STREAM_START_EVENT = "stream:start";
const STREAM_COMPLETE_EVENT = "stream:complete";
const STREAM_END_EVENT = "stream:end";
const STREAM_ERROR_EVENT = "stream:error";
/** Read a property off a non-null object as `unknown` without a type assertion. */
function readProp(source, key) {
return Reflect.get(source, key);
}
/** Narrow an unknown value to a non-null object, or `undefined`. */
function asObject(value) {
return typeof value === "object" && value !== null ? value : undefined;
}
/** Extract the text delta from a `response:chunk` payload. */
function readTextDelta(payload) {
return typeof payload === "string" && payload.length > 0
? payload
: undefined;
}
/** Extract tool fields from a `tool:start` / `tool:end` payload. */
function readToolFields(payload) {
const obj = asObject(payload);
if (!obj) {
return undefined;
}
const toolName = readProp(obj, "toolName");
const tool = readProp(obj, "tool");
const name = typeof toolName === "string"
? toolName
: typeof tool === "string"
? tool
: undefined;
if (name === undefined) {
return undefined;
}
const fields = { name };
const executionId = readProp(obj, "executionId");
if (typeof executionId === "string") {
fields.id = executionId;
}
const input = readProp(obj, "input");
if (input !== undefined) {
fields.input = input;
}
const result = readProp(obj, "result");
if (result !== undefined) {
fields.result = result;
}
const success = readProp(obj, "success");
if (typeof success === "boolean") {
fields.success = success;
}
const error = readProp(obj, "error");
if (typeof error === "string") {
fields.error = error;
}
return fields;
}
/** Extract the HITL prompt fields from a `hitl:confirmation-request` payload. */
function readHitlPrompt(payload) {
const event = asObject(payload);
if (!event) {
return undefined;
}
const inner = asObject(readProp(event, "payload"));
if (!inner) {
return undefined;
}
const confirmationId = readProp(inner, "confirmationId");
const toolName = readProp(inner, "toolName");
if (typeof confirmationId !== "string" || typeof toolName !== "string") {
return undefined;
}
const data = {
confirmationId,
toolName,
};
const actionType = readProp(inner, "actionType");
if (typeof actionType === "string") {
data.actionType = actionType;
}
const args = readProp(inner, "arguments");
if (args !== undefined) {
data.arguments = args;
}
const timeoutMs = readProp(inner, "timeoutMs");
if (typeof timeoutMs === "number") {
data.timeoutMs = timeoutMs;
}
const allowModification = readProp(inner, "allowModification");
if (typeof allowModification === "boolean") {
data.allowModification = allowModification;
}
return { type: "hitl-prompt", data };
}
/** Narrow a decoded inbound message to a control message, or `undefined`. */
function readControlMessage(value) {
const obj = asObject(value);
if (!obj) {
return undefined;
}
const action = readProp(obj, "action");
const confirmationId = readProp(obj, "confirmationId");
if (typeof confirmationId !== "string") {
return undefined;
}
if (action === "hitl:accept") {
const message = {
action: "hitl:accept",
confirmationId,
};
const modifiedArguments = readProp(obj, "modifiedArguments");
if (modifiedArguments !== undefined) {
message.modifiedArguments = modifiedArguments;
}
return message;
}
if (action === "hitl:reject") {
const message = {
action: "hitl:reject",
confirmationId,
};
const reason = readProp(obj, "reason");
if (typeof reason === "string") {
message.reason = reason;
}
return message;
}
return undefined;
}
/**
* Attach the data-channel event bridge to a room.
*
* Returns a handle whose `dispose()` removes every listener and stops
* publishing; it is safe to call more than once.
*/
export async function attachEventBridge(params) {
const { room, emitter, options } = params;
const eventsTopic = options?.eventsTopic ?? DEFAULT_EVENTS_TOPIC;
const controlTopic = options?.controlTopic ?? DEFAULT_CONTROL_TOPIC;
const maxInlineBytes = options?.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
const include = options?.include && options.include.length > 0
? new Set(options.include)
: undefined;
const { RoomEvent } = await import("@livekit/rtc-node");
const encoder = new TextEncoder();
const decoder = new TextDecoder();
let seq = 0;
let disposed = false;
function publish(event) {
if (disposed) {
return;
}
if (include && !include.has(event.type)) {
return;
}
const localParticipant = room.localParticipant;
if (!localParticipant) {
return;
}
seq += 1;
const envelope = Object.assign({ seq, ts: Date.now() }, event);
const json = JSON.stringify(envelope);
const bytes = encoder.encode(json);
const onError = (transport) => (error) => {
logger.warn("voice.bridge.publishFailed", {
transport,
type: event.type,
error: error instanceof Error ? error.message : String(error),
});
};
if (bytes.byteLength <= maxInlineBytes) {
void localParticipant
.publishData(bytes, { reliable: true, topic: eventsTopic })
.catch(onError("publishData"));
}
else {
// Large payloads (e.g. chart data) exceed a single reliable data packet;
// the chunked text stream handles arbitrary sizes.
void localParticipant
.sendText(json, { topic: eventsTopic })
.catch(onError("sendText"));
}
}
// --- Outbound: NeuroLink emitter → data channel ---------------------------
const onUserText = (...args) => {
const payload = asObject(args[0]);
if (!payload) {
return;
}
const text = readProp(payload, "text");
const final = readProp(payload, "final");
const replacesPrevious = readProp(payload, "replacesPrevious");
if (typeof text === "string" && text.trim().length > 0) {
const data = {
text,
final: final === true,
};
if (replacesPrevious === true) {
data.replacesPrevious = true;
}
publish({ type: "user-text", data });
}
};
const onText = (...args) => {
const delta = readTextDelta(args[0]);
if (delta !== undefined) {
publish({ type: "text", data: { delta } });
}
};
const onToolStart = (...args) => {
const fields = readToolFields(args[0]);
if (fields) {
publish({
type: "tool-start",
data: { id: fields.id, name: fields.name, input: fields.input },
});
}
};
const onToolEnd = (...args) => {
const fields = readToolFields(args[0]);
if (fields) {
publish({
type: "tool-result",
data: {
id: fields.id,
name: fields.name,
result: fields.result,
success: fields.success,
error: fields.error,
},
});
}
};
const onHitlRequest = (...args) => {
const event = readHitlPrompt(args[0]);
if (event) {
publish(event);
}
};
const onStreamStart = () => {
publish({ type: "status", data: { state: "thinking" } });
};
const onStreamComplete = () => {
publish({ type: "done", data: {} });
};
const onStreamError = (...args) => {
const obj = asObject(args[0]);
const message = obj ? readProp(obj, "message") : undefined;
publish({
type: "status",
data: {
state: "error",
detail: typeof message === "string" ? message : undefined,
},
});
};
emitter.on(USER_TEXT_EVENT, onUserText);
emitter.on(TEXT_EVENT, onText);
emitter.on(TOOL_START_EVENT, onToolStart);
emitter.on(TOOL_END_EVENT, onToolEnd);
emitter.on(HITL_REQUEST_EVENT, onHitlRequest);
emitter.on(STREAM_START_EVENT, onStreamStart);
emitter.on(STREAM_COMPLETE_EVENT, onStreamComplete);
emitter.on(STREAM_END_EVENT, onStreamComplete);
emitter.on(STREAM_ERROR_EVENT, onStreamError);
// --- Inbound: data channel → NeuroLink emitter (HITL responses) -----------
const onData = (...args) => {
const payload = args[0];
const topic = args[3];
if (topic !== controlTopic || !(payload instanceof Uint8Array)) {
return;
}
let parsed;
try {
parsed = JSON.parse(decoder.decode(payload));
}
catch (error) {
logger.warn("voice.bridge.controlMalformed", {
error: error instanceof Error ? error.message : String(error),
});
return;
}
const message = readControlMessage(parsed);
if (!message) {
return;
}
const responsePayload = {
confirmationId: message.confirmationId,
approved: message.action === "hitl:accept",
metadata: { timestamp: new Date().toISOString(), responseTime: 0 },
};
if (message.action === "hitl:reject" && message.reason !== undefined) {
responsePayload.reason = message.reason;
}
if (message.action === "hitl:accept" &&
message.modifiedArguments !== undefined) {
responsePayload.modifiedArguments = message.modifiedArguments;
}
emitter.emit("hitl:confirmation-response", {
type: "hitl:confirmation-response",
payload: responsePayload,
});
};
room.on(RoomEvent.DataReceived, onData);
publish({ type: "status", data: { state: "listening" } });
logger.info("voice.bridge.attached", {
eventsTopic,
controlTopic,
filtered: include !== undefined,
});
return {
dispose() {
if (disposed) {
return;
}
disposed = true;
emitter.off(USER_TEXT_EVENT, onUserText);
emitter.off(TEXT_EVENT, onText);
emitter.off(TOOL_START_EVENT, onToolStart);
emitter.off(TOOL_END_EVENT, onToolEnd);
emitter.off(HITL_REQUEST_EVENT, onHitlRequest);
emitter.off(STREAM_START_EVENT, onStreamStart);
emitter.off(STREAM_COMPLETE_EVENT, onStreamComplete);
emitter.off(STREAM_END_EVENT, onStreamComplete);
emitter.off(STREAM_ERROR_EVENT, onStreamError);
room.off(RoomEvent.DataReceived, onData);
logger.info("voice.bridge.disposed", {});
},
};
}