Streaming Responses
Pedelec exposes assistant output through two separate callbacks:
session.onChat()receives completed logical assistant messages.session.onChatDelta()receives best-effort incremental text when the provider exposes a real streaming delta.
Do not treat deltas as an authoritative completed response. A provider may emit only completed messages, or may emit both deltas and a completed message for the same logical output.
Signatures
Section titled “Signatures”const offChat = session.onChat((text, ctx) => { // text is one completed logical assistant message // ctx.type === "chat_message"});
const offChatDelta = session.onChatDelta((delta, ctx) => { // delta is one incremental fragment // ctx.type === "chat_delta"});type ChatEventContext = PedelecEventContext & { type: "chat_message"; turnId: string; turnStartedAt: number; eventReceivedAt: number;};
type ChatDeltaEventContext = PedelecEventContext & { type: "chat_delta"; turnId: string; turnStartedAt: number; eventReceivedAt: number;};A delta can be a few characters, punctuation, a word, or a larger chunk. Chunk boundaries do not carry semantic meaning. Pedelec preserves provider delta text as delivered and does not synthesize a missing suffix when a later completed message contains more text.
Accumulate one live response
Section titled “Accumulate one live response”Use onChatDelta() when the UI should render text while the provider is still generating:
let assistantText = "";
const offChatDelta = session.onChatDelta((delta) => { assistantText += delta; renderAssistantMessage(assistantText);});
await session.sendText("Explain this screen.");offChatDelta();Do not create a new message bubble for every delta.
// Incorrect: produces many tiny messages.session.onChatDelta((delta) => { messages.push({ role: "assistant", text: delta });});Instead, append deltas to one live message for that turn.
Keep the completed message authoritative
Section titled “Keep the completed message authoritative”If your application needs the provider’s completed semantic output, subscribe to onChat() directly:
const completedMessages: string[] = [];
const offChat = session.onChat((text) => { completedMessages.push(text); saveCompletedAssistantMessage(text);});
await session.sendText("Explain this screen.");offChat();Do not compare the final message with accumulated deltas and assume the difference should have arrived as another delta. Delta delivery is best-effort and provider-dependent.
Subscribe to both
Section titled “Subscribe to both”A live UI can use deltas for rendering and the completed callback for the authoritative final message:
let liveText = "";
const offDelta = session.onChatDelta((delta) => { liveText += delta; renderAssistantMessage(liveText);});
const offMessage = session.onChat((text) => { liveText = text; renderAssistantMessage(text); persistAssistantMessage(text);});
await session.sendText(prompt);offDelta();offMessage();Each provider event is delivered only to its matching callback. chat_delta does not implicitly invoke onChat(), and chat_message does not implicitly invoke onChatDelta().
Group deltas by turn
Section titled “Group deltas by turn”type LiveAssistantMessage = { id: string; sessionId: string; turnId: string; text: string;};
const messages = new Map<string, LiveAssistantMessage>();
session.onChatDelta((delta, ctx) => { const key = `${ctx.sessionId}:${ctx.turnId}`; const current = messages.get(key) ?? { id: key, sessionId: ctx.sessionId, turnId: ctx.turnId, text: "", };
current.text += delta; messages.set(key, current); renderMessages([...messages.values()]);});turnId is generated by the SDK for one accepted sendText() or prepare() lifecycle. Public assistant callbacks surface only user-turn output. Treat the ID as opaque:
- do not parse timestamps from it;
- do not depend on its prefix;
- do not use it as a provider resume ID; and
- do not expect it to survive a page reload.
Multiple sessions
Section titled “Multiple sessions”Every callback includes ctx.sessionId, even though the handler is attached to one session. It is useful for generic stores and logging.
session.onChatDelta((delta, ctx) => { transcriptStore.appendLive(ctx.sessionId, ctx.turnId, delta);});
session.onChat((text, ctx) => { transcriptStore.commit(ctx.sessionId, ctx.turnId, text);});When a UI switches active sessions, continue recording events for non-visible sessions rather than appending them to the currently selected transcript.
Timing fields
Section titled “Timing fields”sessionCreatedAt: when this browser-side session object was constructed.turnStartedAt: when the SDK accepted the local turn.eventReceivedAt: when the SDK received the Core event from the extension.eventEmittedAt: when the SDK created and delivered the callback context.
These timestamps are useful for diagnostics and UI metrics. They are browser clock values, not a distributed tracing guarantee.
Rendering performance
Section titled “Rendering performance”Very frequent deltas can cause unnecessary framework renders. Keep correctness first, then batch rendering if needed:
let buffer = "";let scheduled = false;
session.onChatDelta((delta) => { buffer += delta; if (scheduled) return;
scheduled = true; requestAnimationFrame(() => { scheduled = false; renderAssistantMessage(buffer); });});If the provider supplies a completed message, onChat() is the authoritative place to reconcile the final rendered text. sendText() completion remains driven by the matching internal operation completion, not by either chat callback or an idle status alone.
Unsubscribe and component cleanup
Section titled “Unsubscribe and component cleanup”Every registration returns a cleanup function:
const offChat = session.onChat(handleCompletedMessage);const offChatDelta = session.onChatDelta(handleDelta);
onComponentCleanup(() => { offChat(); offChatDelta();});Unsubscribing prevents future callback delivery to that handler. It does not stop the provider turn or end the session.
Repeatedly mounting a component without cleanup causes duplicate callback work and memory growth.
Error and completion boundaries
Section titled “Error and completion boundaries”onChat() is a completed-message callback, but it does not determine whether sendText() resolves. Turn completion is correlated to the matching internal operation ID; status and error events remain observable state/diagnostics, while operation_completed is the semantic terminal.
A failed turn may still have emitted partial deltas. Decide whether to retain, mark, or discard that live partial response. Do not synthesize a completed message from deltas in the SDK contract.
HTML and Markdown safety
Section titled “HTML and Markdown safety”Assistant messages and deltas are plain text from the provider. If you render output as Markdown or HTML, apply the same sanitization and content-security rules used for any untrusted model output. Never insert raw output into innerHTML without a trusted sanitizer.
Continue with Session status and events.