UNPKG

agents

Version:

A home for your AI agents

227 lines (226 loc) • 8.76 kB
//#region src/harness/container/protocol.ts /** * The wire between `ContainerHarness` (in a Durable Object) and the harness * daemon (in the container). Isomorphic and dependency-free: the daemon * imports it in Node, the harness in Workers. * * One WebSocket per session, dialled by the object through * `ctx.container.getTcpPort(port)` at `/sessions/<id>`. The daemon speaks * first with `hello`; the object answers with `open` (when the daemon does * not hold the session yet) and `replay`, then delivers `prompt`s. * * Everything the daemon produces for a session is a numbered `frame`. The * numbers start at 1 for each session in each container, so a frame is * addressed by `(runtimeId, seq)`. The daemon keeps frames until they are * acknowledged, so an object that lost its socket (evicted, redeployed) * replays what it missed from its cursor. A different `runtimeId` in * `hello` means a different container: its numbering starts again, and * whatever the old container was doing is gone. * * @experimental The wire may change before it stabilizes. The object and the image are * a matched pair; a version mismatch is reported in `hello`. */ /** Bumped on any incompatible change. */ const CONTAINER_PROTOCOL_VERSION = 1; /** The daemon answers `200` here once it accepts connections. */ const CONTAINER_HEALTH_PATH = "/healthz"; /** The session socket: `/sessions/<encoded session id>`. */ const CONTAINER_SESSION_PATH = "/sessions/"; /** The shared secret the object passes on every request. */ const CONTAINER_TOKEN_HEADER = "x-cf-harness-token"; /** The environment variables the object starts the container with. */ const CONTAINER_ENV = { /** Shared secret for `CONTAINER_TOKEN_HEADER`. */ token: "CF_HARNESS_TOKEN", /** This container's id. Echoed in `hello`. */ runtimeId: "CF_HARNESS_RUNTIME_ID", /** The port the daemon listens on. */ port: "CF_HARNESS_PORT" }; /** WebSocket close code: a newer connection for the session replaced this one. */ const CLOSE_REPLACED = 4001; /** WebSocket close code: the request was not authorized. */ const CLOSE_UNAUTHORIZED = 4003; /** * Most JSON characters the object puts in one `restore` message, and the * daemon in one `persist` frame (a single larger entry travels alone). * Keeps every message well inside WebSocket message limits. */ const MAX_CHUNK_CHARS = 256 * 1024; function isFields(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function isString(value) { return typeof value === "string"; } function isCount(value) { return typeof value === "number" && Number.isSafeInteger(value) && value >= 0; } function isJson(value) { if (value === null) return true; switch (typeof value) { case "string": case "boolean": return true; case "number": return Number.isFinite(value); case "object": if (Array.isArray(value)) return value.every(isJson); return Object.values(value).every(isJson); default: return false; } } function isOutcome(value) { if (!isFields(value)) return false; if (value.status === "done") return isString(value.text); return value.status === "unanswered" && isString(value.reason); } function isEvent(value) { if (!isFields(value) || !isString(value.type) || !isJson(value)) return false; switch (value.type) { case "text-delta": case "reasoning-delta": return isString(value.messageId) && isString(value.delta); case "message": return isFields(value.message) && isString(value.message.id) && (value.message.role === "user" || value.message.role === "assistant") && Array.isArray(value.message.parts); case "usage": return true; case "log": return isString(value.message); case "raw": return "value" in value; default: return false; } } function isFrame(value) { if (!isFields(value)) return false; switch (value.kind) { case "start": return isString(value.operationId); case "event": return (value.operationId === null || isString(value.operationId)) && isEvent(value.event); case "persist": return Array.isArray(value.entries) && value.entries.every(isJson); case "settle": return isString(value.operationId) && isOutcome(value.outcome); default: return false; } } function isDaemonOperation(value) { if (!isFields(value) || !isString(value.operationId)) return false; if (value.status === "queued" || value.status === "running") return true; return value.status === "settled" && isOutcome(value.outcome); } function isInput(value) { if (isString(value)) return true; return Array.isArray(value) && value.every((part) => isFields(part) && (part.type === "text" && isString(part.text) || part.type === "image" && isString(part.mediaType) && isString(part.data))); } function isSettings(value) { return isFields(value) && (value.model === void 0 || isString(value.model)) && (value.options === void 0 || isJson(value.options)); } function decode(text) { try { return JSON.parse(text); } catch { return; } } /** * Parse one message from the daemon. * * @param text - The raw WebSocket message. * @returns The message, or `undefined` when it is not one. */ function parseDaemonMessage(text) { const value = decode(text); if (!isFields(value)) return void 0; switch (value.type) { case "hello": if (isCount(value.protocol) && isString(value.runtimeId) && isString(value.session) && isFields(value.adapter) && isString(value.adapter.id) && isString(value.adapter.version) && Array.isArray(value.adapter.capabilities) && value.adapter.capabilities.every(isString) && (value.open === null || isCount(value.open)) && isCount(value.lastSeq) && Array.isArray(value.operations) && value.operations.every(isDaemonOperation)) return value; return; case "frame": return isCount(value.seq) && isFrame(value.frame) ? value : void 0; case "caught-up": return isCount(value.lastSeq) ? { type: "caught-up", lastSeq: value.lastSeq } : void 0; case "error": return isString(value.message) ? { type: "error", message: value.message } : void 0; default: return; } } /** * Parse one message from the object. * * @param text - The raw WebSocket message. * @returns The message, or `undefined` when it is not one. */ function parseHostMessage(text) { const value = decode(text); if (!isFields(value)) return void 0; switch (value.type) { case "restore": return isCount(value.generation) && Array.isArray(value.entries) && value.entries.every(isJson) ? { type: "restore", generation: value.generation, entries: value.entries } : void 0; case "open": return isCount(value.generation) && isSettings(value.settings) && Array.isArray(value.restore) && value.restore.every(isJson) ? { type: "open", generation: value.generation, settings: value.settings, restore: value.restore } : void 0; case "replay": return isCount(value.after) ? { type: "replay", after: value.after } : void 0; case "prompt": return isString(value.operationId) && isInput(value.input) && (value.whenBusy === "followUp" || value.whenBusy === "steer") ? { type: "prompt", operationId: value.operationId, input: value.input, whenBusy: value.whenBusy } : void 0; case "abort": if (value.operationId === void 0) return { type: "abort" }; return isString(value.operationId) ? { type: "abort", operationId: value.operationId } : void 0; case "configure": return isSettings(value.settings) ? { type: "configure", settings: value.settings } : void 0; case "ack": return isCount(value.seq) ? { type: "ack", seq: value.seq } : void 0; default: return; } } /** * The text of an input, with image parts left out. * * @param input - A prompt's input. * @returns Its text. */ function inputText(input) { if (typeof input === "string") return input; return input.map((part) => part.type === "text" ? part.text : "").join("\n"); } /** * Split entries into groups whose JSON stays under `MAX_CHUNK_CHARS`. An * entry larger than that is a group of its own. * * @param entries - The entries, in order. * @returns The groups, in order. Empty for no entries. */ function chunkEntries(entries) { const groups = []; let group = []; let size = 0; for (const entry of entries) { const length = JSON.stringify(entry).length; if (group.length > 0 && size + length > 262144) { groups.push(group); group = []; size = 0; } group.push(entry); size += length; } if (group.length > 0) groups.push(group); return groups; } //#endregion export { CONTAINER_PROTOCOL_VERSION as a, MAX_CHUNK_CHARS as c, parseDaemonMessage as d, parseHostMessage as f, CONTAINER_HEALTH_PATH as i, chunkEntries as l, CLOSE_UNAUTHORIZED as n, CONTAINER_SESSION_PATH as o, CONTAINER_ENV as r, CONTAINER_TOKEN_HEADER as s, CLOSE_REPLACED as t, inputText as u }; //# sourceMappingURL=protocol-cOklVBBD.js.map