Member Junction
    Preparing search index...

    Class BaseRealtimeChannelClient<TSurface>Abstract

    Base class for CLIENT-SIDE interactive-channel plugins (per plans/ai-agent-sessions.md → "Interactive Channels" / "Pluggable Channel Interfaces").

    An interactive channel is a bidirectional surface the session's single realtime agent both PERCEIVES and ACTS UPON (whiteboard, shared doc, map, …). A concrete plugin contributes everything the channel needs, so the session service / call overlay carry ZERO channel-specific wiring:

    1. a CLIENT-EXECUTED TOOL SET (GetToolDefinitions, declared to the realtime model at session mint) plus the local executor (ApplyAgentTool) the host routes {@link ToolNamePrefix}* calls to — the ACTION direction;
    2. a STATE→CONTEXT SERIALIZER policy — the plugin owns its state engine and pushes coalesced deltas through RealtimeChannelContext.SendContextNote — the PERCEPTION direction;
    3. an OPTIONAL ANGULAR SURFACE (GetSurfaceComponent) the overlay creates dynamically in a channel tab, handed back through BindSurface so the plugin wires its own inputs/outputs (the host never knows the component's API). A channel may be server-only (no rendered surface) — e.g. a bridge-contributed meeting-controls or native-whiteboard channel whose surface lives on the external platform, not in MJ. Such a channel returns null from GetSurfaceComponent (HasSurface is false) and the overlay simply skips its tab while still wiring its tools + perception;
    4. a STATE OF RECORD (SerializeState, persisted via RealtimeChannelContext.RequestSave under ChannelName).

    Concrete plugins are @RegisterClass(BaseRealtimeChannelClient, '<ClientPluginClass>') and are resolved at session start from the MJ: AI Agent Channels registry: each ACTIVE row's ClientPluginClass is the ClassFactory key (exactly how BaseRealtimeClient drivers resolve by provider key). Ship a Load<YourChannel>() no-op alongside the class and call it from a static code path to defeat tree-shaking.

    ClassFactory.CreateInstance → Initialize(ctx) → zero or more BindSurface/UnbindSurface cycles (the Angular overlay creates the surface the first time it is shown and keeps it until the channel leaves the session; another host may recreate it), each telling the plugin when its surface goes in and out of sight (OnSurfaceVisibilityChange) and where it is placed (OnSurfacePlacementChange) → Dispose at teardown. ApplyAgentTool MUST work with NO surface bound (apply to the state engine directly; skip the UI garnish) — tool calls can arrive before the surface is first shown.

    Type Parameters

    • TSurface extends object = object

      The plugin's Angular surface component type. The host only ever sees the default (object) — the typed parameter exists so concrete plugins get a fully typed BindSurface without casts.

    Hierarchy (View Summary)

    Index

    Constructors

    Properties

    Context: RealtimeChannelContext | null = null

    The host context, available from Initialize until Dispose. null outside that window — guard with ?. in any code that can run early/late.

    Events$: Observable<RealtimeChannelEvent> = ...

    Everything this channel streams out, as typed events: state changes, discrete user actions, frames pushed, opened/completed. Hosts subscribe (the embeddable element re-emits these as DOM events); the model is told about a change through a coalesced note instead — see RecordChange. Completes at Dispose.

    Output$: Observable<RealtimeChannelOutput> = ...

    What the channel handed back when it completed (see Complete). Completes at Dispose.

    PerceptionOptions: ChannelPerceptionOptions = DEFAULT_CHANNEL_PERCEPTION_OPTIONS

    Tuning for the perception coalescer (debounce window, max note size). Override to change it.

    SessionIdWaitIntervalMs: number = 200
    SessionIdWaitTimeoutMs: number = 8000

    Max time ResolveAgentSessionId waits for the session id to bind before giving up, and the poll interval it re-checks on. Protected so tests can shrink the wait; production keeps the defaults (the real mint race is sub-second, 8s is generous headroom).

    VisualVideoBridge: ChannelInboundVideoBridge | null = null

    The shared frame bridge that carries frames to the model's inbound video track. Deliberately NOT named videoBridge: channels that predate the pump (the Remote Browser, app channels) declare their own private videoBridge, and a second private of the same name in the base class would stop them compiling.

    StrictStateChecks: boolean = ...

    Whether state snapshots are validated against the descriptor's noun schemas as notes are built — a development aid that surfaces a channel whose state drifted from its own declaration. On by default only when a Node-style environment reports a non-production NODE_ENV (tests, tooling); a browser production build never pays for it. Hosts may set it explicitly.

    Accessors

    • get CaptureKind(): RealtimeCaptureKind | null

      The runtime capture this channel fronts ('camera' or 'screen'), or null. Such a channel is the capture's policy: the runtime starts the capture only while the channel is in the session and the session's policy lets the agent see pixels through it, and the user's "agent can see" choice for the channel decides whether the capture's frames reach the model. Default null.

      Returns RealtimeCaptureKind | null

    • get Exposure(): RealtimeChannelExposure

      How much of this channel the model may perceive without asking: the lowest of the channel's own ceiling (GetDescriptor().MaxExposure), the server policy and the user's choice. A channel nobody has applied a policy to simply has its ceiling, which is what every channel had before exposure policy existed.

      • 'none': no state notes and no frames.
      • 'state': structured state notes; no frames.
      • 'pixels': state notes and frames.

      It governs what flows to the model UNPROMPTED (perception notes, the contents of opened / completed notes, frames). It does not REDACT what a verb's result contains: a verb is all-or-nothing. A verb whose result would show the model something above this level declares ReturnsChannelData on the verb, and when this level is below it the verb is refused whole (exposure_restricted) and listed to the model as unavailable; see RefuseVerbForExposure. A verb that does not declare it returns what the channel says it returns, so a channel whose verbs reveal sensitive data must either declare it or lower its own MaxExposure.

      Returns RealtimeChannelExposure

    • get ShowsAgentVideo(): boolean

      Whether this channel shows the agent's video: it sinks outbound video (GetSunkTracks). The runtime marks it as used when the video arrives, and a host presents its surface as the agent (the call overlay puts it in the agent's place in the call rather than in the focus layout).

      Returns boolean

    • get SurfaceShareable(): boolean

      Whether the user may share this channel's surface on its own: a host that offers it lists the surface, named by TabTitle with TabIcon, under "This panel" in its Share menu while the surface is on screen, and the share then shows only that surface. Default false, so a channel opts in with this getter. A surface that shows the agent its own call (the camera, the screen share, the agent's video) never should.

      Returns boolean

    • get TabColor(): string | null

      OPTIONAL accent color for the channel's tab (a CSS color string, e.g. an hsl() / token). When a plugin supplies one, the overlay paints the tab's dot + active underline with it; when omitted (the default null), the overlay derives a stable, deterministic color from the ChannelName so every channel still reads as a distinct, colored surface. A channel only overrides this to enforce a specific brand accent.

      Returns string | null

    • get ToolNamePrefix(): string

      The shared name prefix of every tool this channel exposes (e.g. 'Whiteboard_'). The host registers ONE local-execution route per plugin: tool calls whose name starts with this prefix go to ApplyAgentTool instead of the server relay.

      Default '' — a v2 channel with NO native tools (it is reached only through the ContextTool proxy). The host registers no route for an empty prefix: an empty prefix would match every tool name and hijack the server relay.

      Returns string

    Methods

    • Whether this channel wants to be offered the artifacts a delegated run just produced (the agent asked Skip or Sage to build something, and it came back as artifacts). A channel that hosts artifacts says yes for the ones it can show — and for an artifact it ALREADY shows, so a newer version replaces it in place.

      Asked of every channel in the session, mounted or merely advertised. For an advertised channel it is asked before the channel has been initialized, so it must not depend on Context; the channel's resolved configuration is passed in for exactly that reason. Saying yes for an advertised channel mounts it, so only answer yes when you will do something with the artifacts.

      Default: false — a channel that does not host artifacts is never bothered.

      Parameters

      Returns boolean

    • Executes ONE agent tool call locally (the ACTION direction) and returns the result JSON string fed back to the model as the tool_response. Called for every tool whose name starts with ToolNamePrefix. Must work both WITH a bound surface (apply + UI garnish) and WITHOUT one (apply to the state engine directly — the surface may not exist yet, e.g. it has not been shown). Should not throw: return a { success: false, error } payload so the model can narrate the failure (the host additionally wraps anything thrown).

      This is the LEGACY entry point. Channels written against the v2 contract override ApplyVerb instead and inherit this method: the default maps the tool name back to its verb, calls ApplyVerb as the agent, and serializes the structured result. Channels written against v1 override THIS and inherit ApplyVerb, which adapts it. A channel must override at least one of the two — overriding neither is reported as a failure result, never a recursion.

      Parameters

      • toolName: string
      • argsJson: string

      Returns string | Promise<string>

    • Applies exposure policy: the server's decision and the user's own choice. The runtime calls this when it mounts the channel and again whenever the user changes their choice.

      Withdrawing exposure takes effect immediately: a pending perception note is dropped, a pending frame is dropped, and the channel's video source stops being forwarded. Restoring it re-baselines (the model's picture of the channel is stale, so the next note is a full snapshot) and sends a fresh frame. When the call is live the model is told about the change, with the reason, so it never assumes it can still see what it can't.

      Parameters

      • settings: ChannelExposureSettings

        Policy — the server-decided exposure; User — the user's choice (omit for none); Reasons — why the policy lowered it, from the server's limits.

      Returns void

    • Runs one verb of this channel — the contract-v2 entry point, reached through the ContextTool proxy (and, via ApplyAgentTool, by native tool calls).

      The runtime has ALREADY resolved the verb against the descriptor, enforced InvokableBy, and validated args against the verb's parameter schema before calling this, so an override can trust its inputs' shape. It must work with no surface bound, and should not throw — return a failure result so the model can recover.

      Default: adapts the legacy ApplyAgentTool (the verb's native tool name, args as JSON), so every v1 channel is reachable through the proxy with no change. A v2 channel overrides this.

      Parameters

      • verb: string

        The verb name (as declared in the descriptor).

      • args: JSONObject

        The validated parameters.

      • actor: RealtimeChannelActor

        Who is acting: the agent (a model call) or the user (their own interaction).

      • OptionalinstanceId: string

        Which instance of a multi-instance channel; undefined for the primary instance.

      Returns RealtimeChannelVerbResult | Promise<RealtimeChannelVerbResult>

    • Called by the host right after it created the surface component (and BEFORE the component's first change detection, so inputs set here are visible in its ngOnInit). The plugin — which knows its own component type — sets inputs (state engine, agent name, …) and subscribes outputs here, wiring perception/garnish flows back through Context. May be called again with a NEW instance after an UnbindSurface (the host recreated the surface).

      Parameters

      Returns void

    • The channel finished and is handing something back (a submitted form, a chosen option, a finished game). Emits on Output$ and Events$ and tells the model with a completed note. The output is validated against the descriptor's Output schema when StrictStateChecks is on; a mismatch is logged, never swallowed into silence or thrown.

      Parameters

      • output: JSONObject

        What the channel hands back.

      • instanceId: string = ...

        The instance that completed, for a multi-instance channel (default: the primary instance).

      Returns void

    • An AGENT action succeeded and changed the channel's picture: push exactly one confirmation frame now and tell the model not to narrate its own change. Call only on SUCCESS — confirming a failed action would assert an edit landed that did not. A no-op until EnableVisualPerception ran.

      Returns Promise<void>

    • Tears the plugin down at session end: release the surface binding, unsubscribe state-engine subscriptions, then drop the context. Subclasses overriding this MUST call super.Dispose(). Any final state save has already been flushed by the host (the debounced RealtimeChannelContext.RequestSave pipeline) before disposal.

      Returns void

    • Emits a typed event on Events$ (NOT to the model — use RecordChange for state the model should perceive, or send an explicit note for a discrete event it must hear about).

      Parameters

      • name: string

        The event name (declare it in the descriptor's Events).

      • payload: JSONObject = {}

        The event payload.

      • OptionalchangeId: number

        The change id the event relates to, when it relates to one.

      • instanceId: string = ...

        The instance the event is about, for a multi-instance channel (default: the primary instance).

      Returns void

    • Opts this channel into VISUAL perception: its surface can be shown to a video-capable model as frames. Call it from OnInitialize with the channel's frame source.

      The channel then needs only to say when its picture changed — NotifyVisualChange after a user edit, ConfirmVisualChange after a successful agent edit — and the base class does the rest: change-driven (no idle heartbeat), paced to the negotiated cadence, a trailing settle so the model ends on the final state of a burst, dedupe of identical frames, and a single confirmation frame (with a "do not narrate" note) after an agent edit. Every frame is tagged with the change id of the state it was captured at (frame_pushed events), so state and pixels can be cross-checked.

      Also makes GetSourcedTracks report the inbound video track, which is what gets it negotiated. Idempotent: calling it again replaces the provider and options.

      Parameters

      Returns void

    • The channel's SELF-DESCRIPTION — what it is, what it holds, what it accepts, how it behaves in a session. This is the contract an agent reads to operate a channel it has never seen, and what the runtime reads to scope, advertise and validate. Pure data; called often, so keep it cheap.

      Default: a descriptor SYNTHESIZED from the v1 members (tools → verbs, intro → instructions), stamped with the legacy contract version — so a channel that implements nothing new is still describable and works exactly as before. A v2 channel overrides this with an authored descriptor. (Build verbs from GetToolDefinitions with BuildToolBackedVerbs when they are the native tools, so a subclass that adds a tool does not leave the descriptor lying.)

      Returns RealtimeChannelDescriptor

    • The channel's FIRST-RUN INTRO content, or null when the channel offers no onboarding. The overlay shows this once per channel per user — the first time the user opens this channel's surface tab — and remembers "seen" via the user's settings (NOT localStorage), so it never re-appears on later sessions or other devices.

      Default: null (no intro). The base Voice/text channel has no plugin at all, so it never shows an intro; an interactive channel with a surface worth explaining (whiteboard, remote browser, …) overrides this to return its ChannelOnboardingDetails. A plugin that doesn't override it simply shows nothing — onboarding is strictly opt-in.

      Returns ChannelOnboardingDetails | null

    • A snapshot of the channel's things (the descriptor's nouns), keyed by noun name. Perception notes are computed as deltas between successive snapshots, so this must be a pure read.

      Default: the parsed SerializeState when that is a JSON object, else {} — which makes a v1 channel's state of record its (single, opaque) snapshot with no code change.

      Returns JSONObject

    • The Angular component the overlay creates dynamically as this channel's tab pane, or null for a server-only channel that renders no MJ surface (its surface, if any, lives on the external platform — e.g. a bridge-contributed native whiteboard or meeting-controls channel).

      When this returns null, the overlay renders NO tab for the channel and never calls BindSurface/UnbindSurface — but the channel's tools (GetToolDefinitions / ApplyAgentTool) and perception (RealtimeChannelContext.SendContextNote) still run. A created surface instance is handed straight back via BindSurface; the host treats it as opaque.

      Default: null (server-only). A channel with a rendered surface overrides this to return its component type.

      Returns Function | null

    • The channel's picture changed because of a USER action (or a scene replacement): push a frame, respecting the cadence. Resolves when the decision has been carried out; never rejects. A no-op until EnableVisualPerception ran and the session has an inbound video track.

      Returns Promise<void>

    • Subclass hook invoked once the realtime session is connected and live (the client driver is created, connected, and media tracks negotiated), or when the channel is opened in a live session. Channels that establish media bridges (e.g. video streaming) can start them here when Context.Client is available. Default: a channel with visual perception adds the inbound video track it sources when the call has none (it was opened mid-call) and brings its frame bridge up; any other channel does nothing. An override that keeps visual perception calls super.OnSessionStarted().

      Returns void

    • Called by the host when the bound surface moves: to the stage (it fills the call), to its tab, or out of sight because the user hid it. A channel can adapt its own chrome, such as a "Move to stage" button that has nothing to do once the surface is on the stage. Called right after BindSurface with the current placement, then on every move, and not after UnbindSurface. Default: no-op.

      Parameters

      Returns void

    • Called by the host when the bound surface comes into sight or goes out of it (the panel collapsed or hidden, another tab active, the call minimized). The surface stays bound and keeps its state; a channel can pause work nobody sees, such as a screenshot poll, and resume it when the surface is shown again. The host calls it right after BindSurface with the surface's current visibility, then on every change, and not after UnbindSurface. Default: no-op, so a surface keeps running out of sight.

      Parameters

      • _visible: boolean

      Returns void

    • Opens the channel — mounts it for use and seeds it with inputs. Called by the runtime when the agent (or user) opens an on-demand channel, after the channel has been initialized.

      Validates inputs against the descriptor's Inputs schema (a violation is a structured 'invalid_params' failure, never a half-opened channel), runs the OnOpen hook, then tells the model the channel is open and what it currently holds (opened note with a state snapshot) — and takes that snapshot as the baseline later changes are described against.

      Parameters

      • inputs: JSONObject = {}

        What the agent asked to seed the channel with.

      Returns Promise<RealtimeChannelVerbResult>

    • Records that the channel's state changed. Call this on EVERY mutation, whoever caused it.

      It (1) assigns the change a monotonic id, (2) emits a state_changed event on Events$ immediately (observers see every change), and (3) — unless Perceive is false — schedules a COALESCED structured note to the model: a burst of N changes inside the debounce window produces ONE [channel:<Key>#<instance>] state_changed {"changeId":…,"changes":N,"delta":{…}} note carrying what differs from what the model was last told (the first note is a full snapshot). Notes that would be too large degrade to a list of changed paths.

      Parameters

      • options: { Author?: RealtimeChannelActor | "system"; Perceive?: boolean } = {}

        Author — who made the change (carried on the event); Perceive — whether the model should be told (default true; a channel that already feeds the model its own perception — like the Whiteboard's scene deltas — passes false).

      Returns number

      The change's id.

    • Whether the agent may be given this verb's result at the channel's current exposure. A verb that declares ReturnsChannelData is refused whole when exposure is below that level, because its result would hand the agent what the user or a zero-data-retention policy held back from it. Policy, not redaction: the result is never filtered.

      Applies to the AGENT (the dispatcher and the native-tool route call it); a user acting through the surface is not restricted.

      Parameters

      Returns string | null

      The sentence to give the agent, or null when the call may proceed.

    • The focus pill's "exit" affordance, routed by the overlay to the channel that holds focus. Implementations should leave focus mode through their OWN surface (so surface toggles stay in sync), ultimately emitting Context.SetFocusMode(false). The overlay defensively clears its layout flag as well, so a no-op default is safe.

      Returns void

    • Resolves the live RealtimeChannelContext.AgentSessionID, briefly WAITING for it when it isn't bound yet rather than giving up instantly. AgentSessionID is a live getter over the session service's current id: it reads null in the window BEFORE the session mints (the realtime model can fire a tool call the very first beat it connects, before mintSession resolves) and again AFTER teardown. Server-backed tool paths (e.g. the Remote Browser channel's browser_* tools) call this instead of reading Context?.AgentSessionID synchronously, so a tool invoked a beat early WAITS for the session to come live — defense-in-depth against the "session id missing" race — instead of returning a hard failure to the model.

      Returns the id as soon as it's non-null (the common path resolves immediately, no delay), or null if it's still unbound after SessionIdWaitTimeoutMs — or the channel was Disposed in the meantime (Context goes null, so we stop waiting on a torn-down session).

      Returns Promise<string | null>

    • Maps a native tool name back to its verb: the descriptor's NativeToolName match first, then the name minus ToolNamePrefix. Protected so a channel with an unusual naming scheme can override it.

      Parameters

      • toolName: string

      Returns string

    • Restores a PRIOR session's saved channel state (the payload a previous session persisted via SerializeState / RealtimeChannelContext.RequestSave). Invoked by the session host AFTER Initialize and BEFORE any surface binding, when a prior session's saved state exists for this channel.

      Returns true when the state was applied; false when the channel ignored it — either because it keeps no persistent state (this default) or because the payload was malformed/incompatible. Implementations MUST be tolerant: never throw on bad input, just return false and start fresh.

      Parameters

      • stateJson: string

      Returns boolean

    • Serializes the channel's current state of record (the payload persisted on the session's channel row), or null when the channel keeps no persistent state. Default: null.

      Returns string | null

    • Called by the host when the surface component is being destroyed (the channel left the session / the overlay was torn down). Drop the instance reference and unsubscribe any output subscriptions — after this, ApplyAgentTool runs in its no-surface mode. Default: no-op.

      Returns void