UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

140 lines (139 loc) • 4.18 kB
import { PipeableTransformStream } from "../../utils/stream/PipeableTransformStream.js"; import { SSE_HEADERS, createSSEJsonDecoder, createSSEJsonEncoder } from "../../utils/stream/SSEJson.js"; import sjson from "secure-json-parse"; // src/core/serialization/assistant-transport/AssistantTransport.ts const noFields = () => true; const requiredObject = (key) => (c) => { const value = c[key]; return typeof value === "object" && value !== null && !Array.isArray(value); }; const requiredString = (key) => (c) => typeof c[key] === "string"; const requiredArray = (key) => (c) => Array.isArray(c[key]); const optionalBoolean = (key) => (c) => c[key] === void 0 || typeof c[key] === "boolean"; const KNOWN_CHUNK_TYPES = { "part-start": { kind: "message", valid: requiredObject("part") }, "part-finish": { kind: "part-addressed", valid: noFields }, "tool-call-args-text-finish": { kind: "part-addressed", valid: noFields }, "tool-call-approval": { kind: "part-addressed", valid: (c) => requiredObject("approval")(c) && requiredString("id")(c["approval"]) && optionalBoolean("approved")(c["approval"]) }, "text-delta": { kind: "part-addressed", valid: requiredString("textDelta") }, annotations: { kind: "message", valid: requiredArray("annotations") }, data: { kind: "message", valid: requiredArray("data") }, "step-start": { kind: "message", valid: noFields }, "step-finish": { kind: "message", valid: requiredString("finishReason") }, "message-finish": { kind: "message", valid: requiredString("finishReason") }, result: { kind: "part-addressed", valid: optionalBoolean("isError") }, error: { kind: "message", valid: noFields }, "update-state": { kind: "message", valid: requiredArray("operations") } }; const parseChunk = (data) => { let value; try { value = sjson.parse(data); } catch { return "unparseable"; } if (typeof value !== "object" || value === null || Array.isArray(value)) return "not-an-object"; const { type, path } = value; if (typeof type !== "string" || !Object.prototype.hasOwnProperty.call(KNOWN_CHUNK_TYPES, type)) return "unknown-type"; const rule = KNOWN_CHUNK_TYPES[type]; if (!rule.valid(value)) return `invalid-fields:${type}`; if (path === void 0) { if (rule.kind !== "message") return `missing-path:${type}`; return { ...value, path: [] }; } if (!Array.isArray(path) || !path.every((entry) => Number.isInteger(entry) && entry >= 0)) return "invalid-path"; return value; }; /** * AssistantTransportEncoder encodes AssistantStreamChunks into SSE format * and emits [DONE] when the stream completes. */ var AssistantTransportEncoder = class extends PipeableTransformStream { headers = new Headers(SSE_HEADERS); constructor() { super(createSSEJsonEncoder("[DONE]")); } }; /** * AssistantTransportDecoder decodes SSE format into AssistantStreamChunks. * It stops decoding when it encounters [DONE]. */ var AssistantTransportDecoder = class extends PipeableTransformStream { constructor(options = {}) { const strict = options.strict ?? true; const warnedReasons = /* @__PURE__ */ new Set(); super(createSSEJsonDecoder({ parse(data, controller) { const chunk = parseChunk(data); if (typeof chunk === "string") { if (!warnedReasons.has(chunk)) { warnedReasons.add(chunk); console.warn(`Dropped invalid assistant-transport chunk (${chunk}): ${data.slice(0, 200)}`); } } else controller.enqueue(chunk); }, done: { marker: "[DONE]", onMissing() { if (strict) throw new Error("Stream ended abruptly without receiving [DONE] marker"); console.warn("Stream ended abruptly without receiving [DONE] marker"); } }, maxLineLength: options.maxLineLength, maxEventLength: options.maxEventLength, ...strict ? { strict: true } : { strict: false, onUnknownEvent(event) { if (!warnedReasons.has(`event:${event}`)) { warnedReasons.add(`event:${event}`); console.error(`Ignored unknown SSE event type: ${event}`); } } } })); } }; export { AssistantTransportDecoder, AssistantTransportEncoder }; //# sourceMappingURL=AssistantTransport.js.map