UNPKG

agents

Version:

A home for your AI agents

332 lines (331 loc) • 12.9 kB
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../../../classPrivateFieldGet2-DZBYAB34.js"; import { t as _classPrivateMethodInitSpec } from "../../../classPrivateMethodInitSpec-qMjJ6sHQ.js"; import { a as getConnectionFlag, l as registerInternalConnectionKeys, t as WebSockets, u as setConnectionFlag } from "../../../websockets-C6SJtWCJ.js"; import { i as participantRoute } from "../../../internal-kmw70xwR.js"; import { n as parseClientFrame, t as WEB_IDENTITY_HEADER } from "../../../protocol-y2sSY584.js"; //#region src/experimental/channels/web/channel.ts /** Where a Channels connection's identity lives in its connection state. */ const IDENTITY_KEY = "_cf_channels"; registerInternalConnectionKeys(IDENTITY_KEY); var _options = /* @__PURE__ */ new WeakMap(); var _websockets = /* @__PURE__ */ new WeakMap(); var _host = /* @__PURE__ */ new WeakMap(); var _pending = /* @__PURE__ */ new WeakMap(); var _reads = /* @__PURE__ */ new WeakMap(); var _WebChannel_brand = /* @__PURE__ */ new WeakSet(); /** * Serves conversations over the agent's WebSockets connections, to clients * the gateway has resolved. Frames are * namespaced `channels:`, so a connection can carry other protocols too. */ var WebChannel = class { constructor(options = {}) { _classPrivateMethodInitSpec(this, _WebChannel_brand); _classPrivateFieldInitSpec(this, _options, void 0); _classPrivateFieldInitSpec(this, _websockets, void 0); _classPrivateFieldInitSpec(this, _host, void 0); _classPrivateFieldInitSpec(this, _pending, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _reads, /* @__PURE__ */ new Map()); _classPrivateFieldSet2(_options, this, options); } mount(host) { if (_classPrivateFieldGet2(_host, this)) throw new Error("A WebChannel can be mounted only once"); _classPrivateFieldSet2(_host, this, host); _classPrivateFieldSet2(_websockets, this, _classPrivateFieldGet2(_options, this).websockets ?? host.websockets(() => new WebSockets())); _classPrivateFieldGet2(_websockets, this).use({ onConnect: (connection, ctx) => _assertClassBrand(_WebChannel_brand, this, _connect).call(this, connection, ctx), onMessage: (connection, message) => _assertClassBrand(_WebChannel_brand, this, _message).call(this, connection, message), onClose: (connection) => _assertClassBrand(_WebChannel_brand, this, _close).call(this, connection) }); } publish(conversationId, update) { for (const [connection, identity] of _assertClassBrand(_WebChannel_brand, this, _connections).call(this)) { if (identity.conversationId !== conversationId) continue; if (update.type === "messages" && _assertClassBrand(_WebChannel_brand, this, _sentBy).call(this, update, connection)) continue; if (update.type === "reset") _assertClassBrand(_WebChannel_brand, this, _stopReads).call(this, connection.id); const frame = toFrame(update, conversationId, identity.participant, _assertClassBrand(_WebChannel_brand, this, _mounted).call(this).operations); const pending = _classPrivateFieldGet2(_pending, this).get(connection.id); if (pending) pending.push(frame); else send(connection, frame); } } }; async function _connect(connection, { request }) { const header = request.headers.get(WEB_IDENTITY_HEADER); if (header === null) return; const resolved = JSON.parse(header); const host = _assertClassBrand(_WebChannel_brand, this, _mounted).call(this); const identity = { ...resolved, conversationId: resolved.conversationId ?? host.defaultConversation(), channelKey: host.channelKey }; setConnectionFlag(connection, IDENTITY_KEY, identity); _classPrivateFieldGet2(_pending, this).set(connection.id, []); await _assertClassBrand(_WebChannel_brand, this, _sendSnapshot).call(this, connection, identity); } async function _message(connection, message) { const identity = _assertClassBrand(_WebChannel_brand, this, _identityOf).call(this, connection); if (!identity || typeof message !== "string") return false; const frame = parseClientFrame(message); if (!frame) return false; if ("invalid" in frame) { send(connection, { type: "channels:ack", conversationId: identity.conversationId, eventId: frame.eventId ?? "", error: `Invalid frame: ${frame.invalid}` }); return true; } if (frame.type === "channels:list-conversations") { await _assertClassBrand(_WebChannel_brand, this, _list).call(this, connection, identity, frame.requestId); return true; } if (frame.type === "channels:subscribe") { _assertClassBrand(_WebChannel_brand, this, _subscribe).call(this, connection, identity, frame.responseId, frame.from); return true; } const { eventId } = frame.event; let result; try { result = await _assertClassBrand(_WebChannel_brand, this, _mounted).call(this).dispatch(frame.event, origin(identity, connection.id)); } catch { send(connection, { type: "channels:ack", conversationId: identity.conversationId, eventId, error: "Event rejected" }); return true; } const followed = result.conversationId; send(connection, { type: "channels:ack", conversationId: followed ?? identity.conversationId, eventId }); if (followed !== void 0 && followed !== identity.conversationId) await _assertClassBrand(_WebChannel_brand, this, _follow).call(this, connection, { ...identity, conversationId: followed }); if (followed !== void 0) await _assertClassBrand(_WebChannel_brand, this, _pushConversations).call(this); return true; } async function _list(connection, identity, requestId) { const frame = { type: "channels:conversations", conversationId: identity.conversationId, requestId }; try { const conversations = await _assertClassBrand(_WebChannel_brand, this, _mounted).call(this).listConversations(); send(connection, { ...frame, conversations }); } catch (error) { console.error("Failed to list conversations", error); send(connection, { ...frame, error: "Failed to list conversations" }); } } /** Tell every connection the agent's conversations changed. */ async function _pushConversations() { let conversations; try { conversations = await _assertClassBrand(_WebChannel_brand, this, _mounted).call(this).listConversations(); } catch (error) { console.error("Failed to list conversations", error); return; } for (const [connection, identity] of _assertClassBrand(_WebChannel_brand, this, _connections).call(this)) { const frame = { type: "channels:conversations", conversationId: identity.conversationId, conversations }; const pending = _classPrivateFieldGet2(_pending, this).get(connection.id); if (pending) pending.push(frame); else send(connection, frame); } } /** Move a connection to another conversation, starting from its snapshot. */ async function _follow(connection, identity) { _assertClassBrand(_WebChannel_brand, this, _stopReads).call(this, connection.id); setConnectionFlag(connection, IDENTITY_KEY, identity); _classPrivateFieldGet2(_pending, this).set(connection.id, []); await _assertClassBrand(_WebChannel_brand, this, _sendSnapshot).call(this, connection, identity); } function _close(connection) { _assertClassBrand(_WebChannel_brand, this, _stopReads).call(this, connection.id); _classPrivateFieldGet2(_pending, this).delete(connection.id); } function _sentBy(update, connection) { const surface = update.origin?.surface; return surface !== void 0 && surface.channelKey === _classPrivateFieldGet2(_host, this)?.channelKey && connectionIdOf(surface.address) === connection.id; } async function _sendSnapshot(connection, identity) { try { const snapshot = await _assertClassBrand(_WebChannel_brand, this, _mounted).call(this).snapshot(identity.conversationId); send(connection, { type: "channels:snapshot", conversationId: identity.conversationId, you: identity.participant, operations: [..._assertClassBrand(_WebChannel_brand, this, _mounted).call(this).operations], ...snapshot }); for (const frame of _classPrivateFieldGet2(_pending, this).get(connection.id) ?? []) send(connection, frame); _classPrivateFieldGet2(_pending, this).delete(connection.id); } catch (error) { console.error("Failed to read the conversation snapshot", error); _classPrivateFieldGet2(_pending, this).delete(connection.id); connection.close(1011, "Snapshot failed"); } } /** One read per response per connection; a repeated subscribe joins it. */ function _subscribe(connection, identity, responseId, from) { let reads = _classPrivateFieldGet2(_reads, this).get(connection.id); if (!reads) _classPrivateFieldGet2(_reads, this).set(connection.id, reads = /* @__PURE__ */ new Map()); if (reads.has(responseId)) return; const controller = new AbortController(); reads.set(responseId, controller); const { conversationId } = identity; _assertClassBrand(_WebChannel_brand, this, _mounted).call(this).readResponse(conversationId, responseId, { from, signal: controller.signal, onChunks: (first, chunks) => send(connection, { type: "channels:chunks", conversationId, responseId, from: first, chunks }), onCaughtUp: (cursor) => send(connection, { type: "channels:caught-up", conversationId, responseId, cursor }) }).then((ending) => send(connection, { type: "channels:end", conversationId, responseId, state: ending }), (error) => { if (controller.signal.aborted) return; console.error(`Failed to read response "${responseId}"`, error); send(connection, { type: "channels:end", conversationId, responseId, state: "interrupted" }); }).finally(() => reads.delete(responseId)); } function _stopReads(connectionId) { for (const controller of _classPrivateFieldGet2(_reads, this).get(connectionId)?.values() ?? []) controller.abort(); _classPrivateFieldGet2(_reads, this).delete(connectionId); } function* _connections() { for (const connection of _classPrivateFieldGet2(_websockets, this)?.getConnections() ?? []) { const identity = _assertClassBrand(_WebChannel_brand, this, _identityOf).call(this, connection); if (identity) yield [connection, identity]; } } function _identityOf(connection) { const identity = getConnectionFlag(connection, IDENTITY_KEY); return identity?.channelKey === _classPrivateFieldGet2(_host, this)?.channelKey ? identity : void 0; } function _mounted() { if (!_classPrivateFieldGet2(_host, this)) throw new Error("WebChannel is not mounted"); return _classPrivateFieldGet2(_host, this); } function origin(identity, connectionId) { return { conversationId: identity.conversationId, participant: identity.participant, surface: { channelKey: identity.channelKey, version: 1, address: { conversationId: identity.conversationId, connectionId }, label: "Web" } }; } function connectionIdOf(address) { return typeof address === "object" && address !== null && "connectionId" in address && typeof address.connectionId === "string" ? address.connectionId : void 0; } function toFrame(update, conversationId, you, operations) { switch (update.type) { case "turn": return { type: "channels:turn", conversationId, turn: update.turn }; case "messages": return { type: "channels:messages", conversationId, messages: update.messages }; case "reset": return { type: "channels:snapshot", conversationId, you, operations: [...operations], messages: [], turns: [] }; case "response-end": return { type: "channels:end", conversationId, responseId: update.responseId, state: update.ending }; } } /** A failed send to one stale socket must not stop delivery to the rest. */ function send(connection, frame) { try { connection.send(JSON.stringify(frame)); } catch {} } //#endregion //#region src/experimental/channels/web/ingress.ts /** * The Web Channel's side in the gateway: it resolves each WebSocket upgrade * and forwards it to the agent, whose `WebChannel` serves the connection. * * ```ts * new ChannelGateway({ * agent: (route) => env.SupportAgent.getByName(route), * channels: { * web: web({ * participant: async (request) => * (await verifySession(request, env))?.id ?? null * }) * } * }); * ``` */ function web(options) { const route = options.route; return { upgrade: { match: options.match ?? matchChannelsPath, participant: (request) => options.participant(request), route: (request, participant) => route ? route(request, participant) : participantRoute(participant) } }; } function matchChannelsPath(request) { const match = /^\/channels(?:\/([^/]+))?$/.exec(new URL(request.url).pathname); if (!match) return void 0; return match[1] === void 0 ? {} : { conversationId: decodeURIComponent(match[1]) }; } //#endregion export { WEB_IDENTITY_HEADER, WebChannel, parseClientFrame, web }; //# sourceMappingURL=index.js.map