agents
Version:
A home for your AI agents
332 lines (331 loc) • 12.9 kB
JavaScript
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