@copilotkit/runtime
Version:
<img src="https://github.com/user-attachments/assets/0a6b64d9-e193-4940-a3f6-60334ac34084" alt="banner" style="border-radius: 12px; border: 2px solid #d6d4fa;" />
280 lines (278 loc) • 8.71 kB
JavaScript
import "reflect-metadata";
import { Observable } from "rxjs";
import { EventType, Middleware } from "@ag-ui/client";
import clarinet from "clarinet";
//#region src/v2/runtime/open-generative-ui-middleware.ts
const TOOL_NAME = "generateSandboxedUi";
const ACTIVITY_TYPE = "open-generative-ui";
/**
* Tracks incremental JSON parsing state for a single tool call's arguments.
* Emits activity events via the onEvent callback as parameters complete.
*/
var ArgsParser = class {
constructor(toolCallId, onEvent) {
this.currentKey = null;
this.depth = 0;
this.currentArrayKey = null;
this.snapshotEmitted = false;
this.streamingHtmlKey = false;
this.htmlEmittedLength = 0;
this.htmlArrayEmitted = false;
this.params = {};
this.messageId = `${toolCallId}-activity`;
this.onEvent = onEvent;
this.parser = clarinet.parser();
this.parser.onopenobject = (key) => {
this.depth++;
if (key !== void 0 && this.depth === 1) {
this.currentKey = key;
this.initHtmlStreaming(key);
}
};
this.parser.onkey = (key) => {
if (this.depth === 1) {
this.currentKey = key;
this.initHtmlStreaming(key);
}
};
this.parser.onvalue = (value) => {
if (this.depth === 1 && this.currentKey) if (this.currentArrayKey) {
const strValue = String(value);
if (this.currentArrayKey === "jsExpressions") {
if (!this.params.jsExpressions) this.params.jsExpressions = [];
this.params.jsExpressions.push(strValue);
} else if (this.currentArrayKey === "placeholderMessages") {
if (!this.params.placeholderMessages) this.params.placeholderMessages = [];
this.params.placeholderMessages.push(strValue);
}
this.emitArrayItemDelta(this.currentArrayKey, strValue);
} else if (this.streamingHtmlKey) {
const fullHtml = value != null ? String(value) : "";
this.params.html = fullHtml || void 0;
this.emitPendingHtml(fullHtml);
this.emitParamDelta("htmlComplete", true);
this.streamingHtmlKey = false;
} else this.setParam(this.currentKey, value);
};
this.parser.onopenarray = () => {
if (this.depth === 1 && this.currentKey) {
const key = this.currentKey;
if (key === "jsExpressions" || key === "placeholderMessages") {
this.currentArrayKey = key;
if (key === "jsExpressions") this.params.jsExpressions = [];
else this.params.placeholderMessages = [];
this.emitParamDelta(key, []);
}
}
};
this.parser.onclosearray = () => {
if (this.depth === 1) {
if (this.currentArrayKey === "jsExpressions") this.emitParamDelta("jsExpressionsComplete", true);
this.currentArrayKey = null;
}
};
this.parser.oncloseobject = () => {
this.depth--;
};
this.parser.onerror = (err) => {
console.warn("[OpenGenerativeUI] JSON parse error in streaming args, resuming:", err?.message ?? err);
this.parser.error = null;
this.parser.resume();
};
}
write(chunk) {
this.parser.write(chunk);
this.flushHtmlChunks();
}
initHtmlStreaming(key) {
if (key === "html") {
this.streamingHtmlKey = true;
this.htmlEmittedLength = 0;
this.htmlArrayEmitted = false;
}
}
/**
* Read clarinet's internal textNode buffer to emit html chunks incrementally.
* Called after every write() so partial string content is emitted as it streams in.
*/
flushHtmlChunks() {
if (!this.streamingHtmlKey) return;
const textNode = this.parser.textNode;
if (typeof textNode !== "string") return;
if (textNode.length === this.htmlEmittedLength) return;
this.emitPendingHtml(textNode);
}
/**
* Emit accumulated html content since the last emission.
* Called by flushHtmlChunks and directly when html completes.
*/
emitPendingHtml(textNode) {
const newContent = textNode.slice(this.htmlEmittedLength);
if (newContent.length === 0) return;
if (!this.htmlArrayEmitted) {
this.htmlArrayEmitted = true;
this.emitParamDelta("html", []);
}
this.emitArrayItemDelta("html", newContent);
this.htmlEmittedLength = textNode.length;
}
setParam(key, value) {
switch (key) {
case "initialHeight":
this.params.initialHeight = typeof value === "number" ? value : void 0;
this.emitSnapshot();
break;
case "css":
this.params.css = value != null ? String(value) : void 0;
this.emitParamDelta("css", this.params.css);
this.emitParamDelta("cssComplete", true);
break;
case "jsFunctions":
this.params.jsFunctions = value != null ? String(value) : void 0;
this.emitParamDelta("jsFunctions", this.params.jsFunctions);
this.emitParamDelta("jsFunctionsComplete", true);
break;
}
}
emitSnapshot() {
if (this.snapshotEmitted) return;
this.snapshotEmitted = true;
const event = {
type: EventType.ACTIVITY_SNAPSHOT,
messageId: this.messageId,
activityType: ACTIVITY_TYPE,
content: {
initialHeight: this.params.initialHeight,
generating: true
}
};
this.onEvent(event);
}
emitParamDelta(key, value) {
const event = {
type: EventType.ACTIVITY_DELTA,
messageId: this.messageId,
activityType: ACTIVITY_TYPE,
patch: [{
op: "add",
path: `/${key}`,
value
}]
};
this.onEvent(event);
}
emitArrayItemDelta(arrayKey, value) {
const event = {
type: EventType.ACTIVITY_DELTA,
messageId: this.messageId,
activityType: ACTIVITY_TYPE,
patch: [{
op: "add",
path: `/${arrayKey}/-`,
value
}]
};
this.onEvent(event);
}
};
var OpenGenerativeUIMiddleware = class extends Middleware {
run(input, next) {
return this.processStream(this.runNextWithState(input, next));
}
processStream(source) {
return new Observable((subscriber) => {
let heldRunFinished = null;
const activeParsers = /* @__PURE__ */ new Map();
const heldToolCallEvents = /* @__PURE__ */ new Map();
const flushedToolCalls = /* @__PURE__ */ new Set();
const flushHeldEvents = (toolCallId) => {
if (flushedToolCalls.has(toolCallId)) return;
flushedToolCalls.add(toolCallId);
const held = heldToolCallEvents.get(toolCallId);
if (held) {
for (const e of held) subscriber.next(e);
heldToolCallEvents.delete(toolCallId);
}
};
const subscription = source.subscribe({
next: (eventWithState) => {
const event = eventWithState.event;
if (heldRunFinished) {
subscriber.next(heldRunFinished.event);
heldRunFinished = null;
}
if (event.type === EventType.RUN_FINISHED) {
heldRunFinished = eventWithState;
return;
}
if (event.type === EventType.TOOL_CALL_START) {
const startEvent = event;
if (startEvent.toolCallName === TOOL_NAME) {
heldToolCallEvents.set(startEvent.toolCallId, [event]);
activeParsers.set(startEvent.toolCallId, new ArgsParser(startEvent.toolCallId, (activityEvent) => {
subscriber.next(activityEvent);
flushHeldEvents(startEvent.toolCallId);
}));
return;
}
}
if (event.type === EventType.TOOL_CALL_ARGS) {
const argsEvent = event;
const parser = activeParsers.get(argsEvent.toolCallId);
if (parser) {
if (!flushedToolCalls.has(argsEvent.toolCallId)) heldToolCallEvents.get(argsEvent.toolCallId).push(event);
else subscriber.next(event);
parser.write(argsEvent.delta);
return;
}
}
if (event.type === EventType.TOOL_CALL_END) {
const endEvent = event;
const parser = activeParsers.get(endEvent.toolCallId);
if (parser) {
const completeEvent = {
type: EventType.ACTIVITY_DELTA,
messageId: parser.messageId,
activityType: ACTIVITY_TYPE,
patch: [{
op: "add",
path: "/generating",
value: false
}]
};
subscriber.next(completeEvent);
if (!flushedToolCalls.has(endEvent.toolCallId)) heldToolCallEvents.get(endEvent.toolCallId).push(event);
else subscriber.next(event);
return;
}
}
subscriber.next(event);
},
error: (err) => {
for (const [, events] of heldToolCallEvents) for (const event of events) subscriber.next(event);
heldToolCallEvents.clear();
if (heldRunFinished) {
subscriber.next(heldRunFinished.event);
heldRunFinished = null;
}
subscriber.error(err);
},
complete: () => {
heldToolCallEvents.forEach((_, toolCallId) => {
flushHeldEvents(toolCallId);
});
if (heldRunFinished) {
subscriber.next(heldRunFinished.event);
heldRunFinished = null;
}
activeParsers.clear();
subscriber.complete();
}
});
return () => subscription.unsubscribe();
});
}
};
//#endregion
export { OpenGenerativeUIMiddleware };
//# sourceMappingURL=open-generative-ui-middleware.mjs.map