Server-tier coordination + execution for MemberJunction Realtime Bridges. This package completes the realtime engine's long-deferred unified media-transport seam — the pipe that connects a bridge driver's media plane to a realtime agent session — and adds the bot-session lifecycle, host affinity + janitor, participant tracking, and turn-taking integration around it.
It is the server half of the AIBridgeEngineBase / AIBridgeEngine pair, exactly mirroring how
@memberjunction/aiengine's AIEngine builds on @memberjunction/ai-engine-base's AIEngineBase.
The cross-platform, metadata-only base (provider/identity/channel cache, the abstract
BaseRealtimeBridge driver, TurnTakingPolicy, media-track types) lives in
@memberjunction/ai-bridge-base; this package adds everything that actually runs a
bridged session.
See the architecture plan at
/plans/realtime/realtime-bridges-architecture.md(§1 transport seam, §2 layer cake, §7 lifecycle, §10 security/janitor) for the full design.
| Export | Purpose |
|---|---|
AIBridgeEngine |
The server engine. Extends AIBridgeEngineBase; starts/stops bridged sessions, wires the transport seam, tracks participants, integrates turn-taking, reconciles orphans. Singleton via AIBridgeEngine.Instance. |
LoopbackBridge |
An in-memory, platform-free bridge driver (@RegisterClass(BaseRealtimeBridge, 'LoopbackBridge')) that echoes outbound media back inbound — proves the seam round-trips with zero external infrastructure. |
StartBridgeSessionParams / ActiveBridgeSession |
The start contract + the live in-memory session handle. |
IHostInstanceIdentity / DefaultHostInstanceIdentity |
The injectable host-instance identity used for HostInstanceID stamping + janitor affinity (host apps inject the real one; a default is provided for standalone use). |
LoadLoopbackBridge |
Tree-shaking-prevention loader for the loopback driver. |
npm install @memberjunction/ai-bridge-server
The realtime session contract (IRealtimeSession in @memberjunction/ai) is already
media-agnostic — SendInput(frame) is what the agent hears (and sees), OnOutput(handler) is what it says,
OnVideoFrame(handler) is the video it shows — but it had no client-facing pipe. A bridge is that pipe.
AIBridgeEngine.wireTransportSeam connects the two directions:
// Inbound: endpoint media → the agent hears (and, for a video model, sees) it.
bridge.OnMedia((frame) => {
const chunk = frameToArrayBuffer(frame); // unwrap BridgeMediaFrame → ArrayBuffer
const kind = mediaKindOf(frame.Track); // 'video' for video-in and screen-in, else 'audio'
if (chunk) realtimeSession.SendInput({ Data: chunk, Kind: kind, MimeType: frame.MimeType, TimestampMs: frame.TimestampMs });
});
// Outbound: the agent speaks → into the meeting/call.
realtimeSession.OnOutput((chunk) => {
bridge.SendMedia('audio-out', arrayBufferToFrame(chunk, 'audio-out'));
});
// Outbound video: the agent's typed video frames → video-out, with their MIME type.
realtimeSession.OnVideoFrame?.((frame) => {
bridge.SendMedia('video-out', { Track: 'video-out', Bytes: frame.Data, MimeType: frame.MimeType });
});
The snippet leaves out the video gate, the notes to the model and the floor gate; see Video. The engine
never constructs the realtime model — the IRealtimeSession is injected via
StartBridgeSessionParams.RealtimeSession, so the only coupling is the seam itself.
StartBridgeSession passes the driver its Configuration plus facts read from the realtime session: its audio rates
(InboundSampleRate, OutboundSampleRate) and its video facts, InboundVideoStreams (how many camera or screen sources
it takes at once; 0 when it takes no video) and InboundVideoRate (frames per second; absent when the model declares
none). The video facts come from the session's declared capabilities (InboundVideoStreamsOf, InboundVideoRateOf in
@memberjunction/ai), so the rate comes from the model, not the driver.
video-in or screen-in frame goes only to a session whose capabilities declare an inbound video
track. Any other session would read the image as audio, so the engine drops the frame (one verbose log line per
session). No inbound frame is sent while the model session is being replaced.SourceID; a frame without one gets no note.OnVideoSourceEnded that a source whose frames
reached the current model session stopped, a switch to another source included. A source the model never saw gets no
note.SourceLabel ("Ada's camera"). A source without one is "a
participant's camera" or "a participant's screen", never its SourceID.SendContextNote, and a driver may hold them: Gemini Live queues a note
until the agent's reply ends but sends frames at once, so while the agent talks, a new source's first frames can reach
the model before its note.VideoSourceSeenNote and VideoSourceEndedNote build the two notes.
OnVideoFrame → SendMedia('video-out'). Each typed frame (RealtimeVideoFrame: a fragmented MP4 piece, an encoded
chunk or an image) goes to the bridge with its bytes, its MimeType, so the driver can tell an avatar's MP4 from other
video, and what it says about its video (Width, Height, KeyFrame):
KeyFrame: true;KeyFrame;When the session's speech is floor-gated (FullDuplexTurnGate, in a room with more than one agent), its video is gated
too. An fMP4 fragment counts by the duration of its audio track, which carries the agent's voice
(FullDuplexTurnGate.OnOutputDuration). An init segment passes and is kept for reading the fragments. Chunks and images
carry no voice and pass. A cut flushes the bridge's queued media.
The deprecated OnVideoOutput (untyped bytes) is still forwarded to video-out, without a MIME type.
A driver that publishes the agent's avatar reports through OnAvatarUnavailable when it took the avatar down
('decoder-failed' or 'publish-failed'). The engine then replaces the model session once, through the host's
StartBridgeSessionParams.RecoverRealtimeSessionWithoutAvatar, with one that renders no avatar, seeded with the
conversation so far, so the model stops generating video nobody sees. Inbound audio is held while the replacement opens,
and the replacement must use the same audio sample rates. Nothing is replaced when the host gave no factory, or the
session is ending, recovering or already replaced; if the replacement fails, the session goes on as it was. This is
separate from RecoverRealtimeSession's attempts after a dropped model session.
The engine records no model usage. For an agent's bridged session, RealtimeClientSessionService.WireBridgeRealtimeSession
in @memberjunction/ai-agents records the model session's OnUsage on that session's own co-agent prompt run, so a
replacement session opened through it has a run of its own.
import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
import { RegexAddressedMatcher } from '@memberjunction/ai-bridge-base';
const engine = AIBridgeEngine.Instance;
await engine.Config(false, contextUser, provider); // load the bridge registry (inherited from base)
// `realtimeSession` is an already-open IRealtimeSession from the agent/session layer.
const active = await engine.StartBridgeSession({
AgentSessionID: existingAgentSessionId,
Provider: zoomProviderRow, // MJAIBridgeProviderEntity (DriverClass resolves the driver)
RealtimeSession: realtimeSession, // injected — the engine wires its media plane
Address: 'https://zoom.us/j/123456789',
TurnMode: 'Passive',
TurnMatcher: new RegexAddressedMatcher(['Sage']),
ContextUser: contextUser,
MetadataProvider: provider,
});
// ...later, on hang-up / host-ended:
await engine.StopBridgeSession(active.SessionBridgeID, 'Explicit');
StartBridgeSession drives the AIAgentSessionBridge status state machine
Pending → Connecting → Connected (and → Failed on any error), stamps HostInstanceID for node
affinity, resolves the driver via ClassFactory.CreateInstance(BaseRealtimeBridge, provider.DriverClass),
then wires the transport seam + participant tracking + turn-taking. StopBridgeSession disconnects the
driver and transitions the row to Disconnected with a CloseReason. Both are idempotent.
Every bridge row is stamped with HostInstanceID. ReconcileOrphans(contextUser, provider)
force-closes Connected/Connecting bridges left by a prior boot of this host (matching hostname
prefix, differing instance id) with CloseReason = 'Janitor' — mirroring SessionJanitor. The
scheduling (run-once-at-boot + periodic sweep) is a host-provided hook so this package carries no
timer/IO of its own; call ReconcileOrphans from the host's janitor.
A TurnTakingPolicy is built per session from TurnMode (Passive default / Active / Hybrid)
and fed the session's diarized user transcript segments. Speak requests a spoken update on the
session (when the provider supports RequestSpokenUpdate), PostToChat is a documented hook for the
Phase-2 bridge-chat channel, and Silent does nothing.
LoopbackBridge echoes every outbound frame back inbound, so with a mock IRealtimeSession you can
assert media flows bridge → session and session → bridge with no external service. Its
EmitVideoSourceEnded and EmitAvatarUnavailable drive the ended-source note and the audio-only replacement. See
src/__tests__/transport-seam.test.ts for the round-trip, lifecycle, turn-taking, participant,
janitor and video-note tests, and src/__tests__/avatar-output.test.ts for video out, the floor gate and the
audio-only replacement.
| Layer | Package | Role |
|---|---|---|
| Metadata cache + abstract driver + turn policy | @memberjunction/ai-bridge-base |
provider/identity/channel cache, BaseRealtimeBridge, TurnTakingPolicy, media-track types |
| Coordination + execution + transport seam | @memberjunction/ai-bridge-server (this) |
bot lifecycle, seam wiring, participant tracking, janitor |
| Realtime session contract | @memberjunction/ai |
IRealtimeSession / BaseRealtimeModel (injected, never constructed here) |
Business Source License 1.1 — see LICENSE for details.