Skip to main content

GenUI streaming protocol

A connector advertises GenUI support by implementing onGenUIChunk(callback). The engine then renders any chunks the connector emits as a single synthetic genui message.

The chunk types

type AIChunk =
| { type: "text"; content: string; id: number; }
| { type: "ui"; component: string; props: Record<string, unknown>; id: number; }
| { type: "event"; name: string; payload: unknown; id: number; for?: number; };

Schema: schemas/genui/ai-chunk.schema.json.

ChunkBehaviour
textAppends a Markdown line. Useful for "intro text" before a component.
uiMounts a registered component. component is the name in GenUIRegistry. id becomes the component's identity within this stream.
eventSent by the bot to update an already-mounted component. If for is set, only the component with that id receives it. Otherwise it's broadcast to all listeners in the message bubble.

Emitting chunks from a connector

import type { IConnector, GenUIChunkHandler, AIChunk } from "@chativa/core";

class MyAIConnector implements IConnector {
// ...required methods...

private genUICallback: GenUIChunkHandler | null = null;

onGenUIChunk(callback: GenUIChunkHandler): void {
this.genUICallback = callback;
}

private async streamAnswer(streamId: string) {
const cb = this.genUICallback!;
cb(streamId, { type: "text", content: "Here are your options:", id: 1 }, false);

cb(streamId, {
type: "ui",
component: "genui-card",
props: { title: "Pro Plan", description: "Unlimited" },
id: 2,
}, false);

// … later, after the user submits the form …
cb(streamId, { type: "event", name: "form_success", payload: { code: "OK" }, id: 3, for: 2 }, true);
}
}

done = true flips the message's streamingComplete flag. Components can use that to swap a "submitting…" state for a final success view.

SSE / fetch helper

For OpenAI-style or LangChain-style streaming endpoints, @chativa/genui ships a tiny helper:

import { streamFromFetch } from "@chativa/genui";

await streamFromFetch("/api/chat/stream", (chunk: AIChunk) => {
this.genUICallback?.(streamId, chunk, false);
});

It expects a text/event-stream body where each data: line is one JSON-encoded AIChunk.

Receiving events back from a component

When a registered component fires sendEvent(name, payload) (for example form_submit), the engine routes it to the connector via receiveComponentEvent:

class MyAIConnector implements IConnector {
receiveComponentEvent(streamId: string, eventType: string, payload: unknown): void {
if (eventType === "form_submit") {
this.persistFormSubmission(payload).then((code) => {
this.genUICallback?.(streamId, {
type: "event",
name: "form_success",
payload: { code },
id: 99,
for: 2, // target the form by its chunk id
}, true);
});
}
}
}

This round-trip pattern is how built-in components like genui-form and genui-rating close the loop.