65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
/**
|
|
* AG-UI-style SSE event helpers.
|
|
*
|
|
* We don't implement the full AG-UI protocol — we use a simplified event set
|
|
* that maps cleanly to our chat UX:
|
|
*
|
|
* text_delta — append text to the current assistant message
|
|
* tool_start — model is calling a tool (renders collapsible block)
|
|
* tool_result — local handler finished (fills the block)
|
|
* navigate — render a clickable navigation button inline
|
|
* done — terminal event; carries usage + final assistant message id
|
|
* error — terminal event; carries error detail
|
|
*
|
|
* Each is emitted as one SSE message:
|
|
*
|
|
* event: <name>
|
|
* data: <json>
|
|
*
|
|
* <blank line>
|
|
*/
|
|
export type AGUIEvent =
|
|
| { type: "text_delta"; delta: string }
|
|
| { type: "tool_start"; id: string; name: string; args: Record<string, unknown> }
|
|
| { type: "tool_result"; id: string; result: unknown; durationMs?: number }
|
|
| { type: "navigate"; target: string; label: string }
|
|
| { type: "done"; provider: string; model: string; usage?: Record<string, unknown>; messageId?: string }
|
|
| { type: "error"; message: string };
|
|
|
|
/**
|
|
* Encode an event into the byte-stream chunks expected by the SSE protocol.
|
|
*/
|
|
export function encodeEvent(ev: AGUIEvent): Uint8Array {
|
|
const enc = new TextEncoder();
|
|
const json = JSON.stringify(ev);
|
|
return enc.encode(`event: ${ev.type}\ndata: ${json}\n\n`);
|
|
}
|
|
|
|
/**
|
|
* Helper that creates a ReadableStream + a typed `emit()` callback to push
|
|
* events into it. Caller closes the stream by calling `emit({type:"done"})` or
|
|
* `close()`.
|
|
*/
|
|
export function createEventStream(): {
|
|
stream: ReadableStream<Uint8Array>;
|
|
emit: (ev: AGUIEvent) => void;
|
|
close: () => void;
|
|
} {
|
|
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
start(c) {
|
|
controller = c;
|
|
},
|
|
});
|
|
return {
|
|
stream,
|
|
emit(ev) {
|
|
try {
|
|
controller.enqueue(encodeEvent(ev));
|
|
} catch { /* stream closed */ }
|
|
},
|
|
close() {
|
|
try { controller.close(); } catch { /* already closed */ }
|
|
},
|
|
};
|
|
}
|