UNPKG

agents

Version:

A home for your AI agents

608 lines (607 loc) 25 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 { t as isChannelMessageSurface } from "../surface-bZZJqBka.js"; import { c as unsupported, t as compositeDestinations } from "../internal-CYlgHl1l.js"; import { n as collectText, r as consumeChunks, t as matchesPath } from "../ingress-BfetZbMO.js"; //#region src/channels/fallback.ts /** * Build an inert fallback destination for a `ChannelHost` to resolve. * * The Host skips unavailable destinations, advances after confirmed failures, * and stops after a delivered or uncertain result to avoid duplicates. */ function fallback(surfaces) { return { channelKey: "fallback", version: 1, address: { surfaces }, label: surfaces.map((surface) => surface.label).join(", then ") }; } /** * Advance only when a failed destination has not started reading the answer. * * Replaying an arbitrarily large consumed prefix requires an arbitrarily large * buffer. Instead, each attempt gets a cancellation-shielded view of the same * source. Once an attempt asks for its first chunk, its result is terminal and * the source is never handed to another destination. */ async function streamWithFallbackBeforeRead(resolve, destinations, chunks, options) { const reader = chunks.getReader(); let drained = false; function attempt() { let startedReading = false; return { chunks: new ReadableStream({ async pull(controller) { startedReading = true; const result = await reader.read(); if (result.done) { drained = true; controller.close(); return; } controller.enqueue(result.value); }, cancel() {} }, { highWaterMark: 0 }), startedReading: () => startedReading }; } try { for (let index = 0; index < destinations.length - 1; index += 1) { const destination = destinations[index]; if (!await resolve.isAvailable(destination)) continue; const current = attempt(); const result = await resolve.stream(destination, current.chunks, options); if (result.status !== "failed" || current.startedReading()) return result; } const final = attempt(); return await resolve.stream(destinations.at(-1), final.chunks, options); } finally { if (!drained) await reader.cancel().catch(() => {}); reader.releaseLock(); } } /** Build the ordinary Channel installed under the reserved fallback key. */ function fallbackChannel(resolve) { async function run(surface, operation) { const destinations = compositeDestinations(surface); if (!destinations) return unsupported("FALLBACK_SURFACE_INVALID", "Fallback surface must contain at least one valid destination"); for (let index = 0; index < destinations.length - 1; index += 1) { const destination = destinations[index]; if (await resolve.isAvailable(destination)) { const result = await operation(destination); if (result.status !== "failed") return result; } } return operation(destinations.at(-1)); } return { deliver(surface, message, options) { return run(surface, (destination) => resolve.deliver(destination, message, options)); }, async stream(surface, chunks, options) { const destinations = compositeDestinations(surface); if (!destinations) { await chunks.cancel().catch(() => {}); return unsupported("FALLBACK_SURFACE_INVALID", "Fallback surface must contain at least one valid destination"); } return streamWithFallbackBeforeRead(resolve, destinations, chunks, options); }, requestApproval(surface, options) { return run(surface, (destination) => resolve.requestApproval(destination, options)); }, async isAvailable(surface) { const destinations = compositeDestinations(surface); if (!destinations) return true; for (const destination of destinations) if (await resolve.isAvailable(destination)) return true; return false; } }; } //#endregion //#region src/channels/fanout.ts function teeAll(source, count) { const branches = []; let rest = source; for (let index = 0; index < count - 1; index += 1) { const [branch, remainder] = rest.tee(); branches.push(branch); rest = remainder; } branches.push(rest); return branches; } /** Build an inert fanout destination for a `ChannelHost` to resolve. */ function fanout(surfaces) { return { channelKey: "fanout", version: 1, address: { surfaces }, label: surfaces.map((surface) => surface.label).join(" and ") }; } /** Build the ordinary Channel installed under the reserved fanout key. */ function fanoutChannel(resolve) { async function run(surface, operation) { const destinations = compositeDestinations(surface); if (!destinations) return unsupported("FANOUT_SURFACE_INVALID", "Fanout surface must contain at least one valid destination"); return combine(await Promise.all(destinations.map(operation))); } function combine(results) { if (results.every((result) => result.status === "delivered")) return { status: "delivered" }; if (results.every((result) => result.status === "failed")) return { status: "failed", retryable: results.every((result) => result.status === "failed" && result.retryable), error: { code: "FANOUT_DELIVERY_FAILED", message: "Every fanout destination rejected the delivery" } }; return { status: "uncertain", error: { code: "FANOUT_DELIVERY_UNCERTAIN", message: "Fanout delivery was partial or had an uncertain destination outcome" } }; } return { deliver(surface, message, options) { return run(surface, (destination) => resolve.deliver(destination, message, options)); }, async stream(surface, chunks, options) { const destinations = compositeDestinations(surface); if (!destinations) { await chunks.cancel().catch(() => {}); return unsupported("FANOUT_SURFACE_INVALID", "Fanout surface must contain at least one valid destination"); } const branches = teeAll(chunks, destinations.length); return combine(await Promise.all(destinations.map(async (destination, index) => { const branch = branches[index]; try { return await resolve.stream(destination, branch, options); } finally { branch.cancel().catch(() => {}); } }))); }, requestApproval(surface, options) { return run(surface, (destination) => resolve.requestApproval(destination, options)); }, async isAvailable(surface) { const destinations = compositeDestinations(surface); if (!destinations) return true; return (await Promise.all(destinations.map((destination) => resolve.isAvailable(destination)))).every(Boolean); } }; } //#endregion //#region src/channels/host/index.ts const POLICY_KEYS = /* @__PURE__ */ new Set(["fallback", "fanout"]); var _channels = /* @__PURE__ */ new WeakMap(); var _defaultRoute = /* @__PURE__ */ new WeakMap(); var _findUser = /* @__PURE__ */ new WeakMap(); var _onRoute = /* @__PURE__ */ new WeakMap(); var _onMessage = /* @__PURE__ */ new WeakMap(); var _onApprovalResponse = /* @__PURE__ */ new WeakMap(); var _ChannelHost_brand = /* @__PURE__ */ new WeakSet(); /** * Authenticates and normalizes ingress through configured Channel adapters, * resolves outbound surfaces, and awaits the application's durable handoff. */ var ChannelHost = class { constructor(options) { _classPrivateMethodInitSpec(this, _ChannelHost_brand); _classPrivateFieldInitSpec(this, _channels, void 0); _classPrivateFieldInitSpec(this, _defaultRoute, void 0); _classPrivateFieldInitSpec(this, _findUser, void 0); _classPrivateFieldInitSpec(this, _onRoute, void 0); _classPrivateFieldInitSpec(this, _onMessage, void 0); _classPrivateFieldInitSpec(this, _onApprovalResponse, void 0); for (const channelKey of Object.keys(options.channels)) if (POLICY_KEYS.has(channelKey)) throw new Error(`Channel key "${channelKey}" is reserved for a delivery policy`); const channels = { ...options.channels }; _classPrivateFieldSet2(_channels, this, channels); channels.fallback = fallbackChannel(this); channels.fanout = fanoutChannel(this); _classPrivateFieldSet2(_defaultRoute, this, options.defaultRoute); _classPrivateFieldSet2(_findUser, this, options.findUser); _classPrivateFieldSet2(_onRoute, this, options.onRoute); _classPrivateFieldSet2(_onMessage, this, options.onMessage); _classPrivateFieldSet2(_onApprovalResponse, this, options.onApprovalResponse); } async handleRequest(request) { for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) { const ingress = channel.ingress; if (!ingress) continue; try { const result = await ingress.receive(request); if (!result) continue; for (const envelope of result.events) await _assertClassBrand(_ChannelHost_brand, this, _dispatch).call(this, channelKey, channel, envelope); return result.response; } catch { return new Response("Failed to handle Channel event", { status: 500 }); } } } async handleEmail(email) { for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) { const ingress = channel.emailIngress; if (!ingress) continue; const result = await ingress.receive(email); if (!result) continue; for (const envelope of result.events) await _assertClassBrand(_ChannelHost_brand, this, _dispatch).call(this, channelKey, channel, envelope); return true; } return false; } /** Deliver through the configured Channel or composite named by the surface. */ deliver(surface, message, options) { return _assertClassBrand(_ChannelHost_brand, this, _outbound).call(this, surface, (channel, destination) => { if (!channel.deliver) return Promise.resolve(unsupported("CHANNEL_DELIVERY_UNSUPPORTED", `Channel "${destination.channelKey}" does not support delivery`)); return channel.deliver(destination, message, options); }); } /** * Deliver a progressively generated answer to the Channel or composite * named by the surface. * * A Channel that can stream consumes the stream itself. A Channel that * cannot never learns it was a stream, because the Host collects the answer * and calls `deliver` once. */ async stream(surface, chunks, options = {}) { if (!isChannelMessageSurface(surface)) { await chunks.cancel().catch(() => {}); return invalidSurface(); } const channel = _assertClassBrand(_ChannelHost_brand, this, _configuredChannel).call(this, surface.channelKey); if (!channel.stream && !channel.deliver) { await chunks.cancel().catch(() => {}); return unsupported("CHANNEL_DELIVERY_UNSUPPORTED", `Channel "${surface.channelKey}" does not support delivery`); } if (channel.stream) return channel.stream(surface, chunks, options); return collectAndDeliver(channel, surface, chunks, options); } /** Request approval through the Channel or composite named by the surface. */ requestApproval(surface, options) { return _assertClassBrand(_ChannelHost_brand, this, _outbound).call(this, surface, (channel, destination) => { if (!channel.requestApproval) return Promise.resolve(unsupported("CHANNEL_APPROVAL_UNSUPPORTED", `Channel "${destination.channelKey}" does not support approval requests`)); return channel.requestApproval(destination, options); }); } /** Return the identity's configured Channel destination, when supported. */ contactSurface(identity) { const surface = (Object.prototype.hasOwnProperty.call(_classPrivateFieldGet2(_channels, this), identity.channelKey) ? _classPrivateFieldGet2(_channels, this)[identity.channelKey] : void 0)?.contactSurface?.(identity); return surface ? stampSurface(identity.channelKey, surface) : null; } /** Resolve whether a surface can currently be selected without delivery. */ async isAvailable(surface) { if (!isChannelMessageSurface(surface)) return false; return _assertClassBrand(_ChannelHost_brand, this, _configuredChannel).call(this, surface.channelKey).isAvailable?.(surface) ?? true; } }; async function _outbound(surface, operation) { if (!isChannelMessageSurface(surface)) return invalidSurface(); return operation(_assertClassBrand(_ChannelHost_brand, this, _configuredChannel).call(this, surface.channelKey), surface); } function _configuredChannel(channelKey) { const channel = Object.prototype.hasOwnProperty.call(_classPrivateFieldGet2(_channels, this), channelKey) ? _classPrivateFieldGet2(_channels, this)[channelKey] : void 0; if (!channel) throw new Error(`Channel message surface names unknown configured Channel key "${channelKey}"`); return channel; } async function _dispatch(channelKey, channel, envelope) { const rawEvent = envelope.event; const event = stampEvent(channelKey, rawEvent); const route = await _assertClassBrand(_ChannelHost_brand, this, _route).call(this, channelKey, channel, event, envelope.raw); const dispatchId = await createDispatchId(channelKey, event.eventId); await _classPrivateFieldGet2(_onRoute, this)?.call(this, { channelKey, event, route, dispatchId }); if (route === null) return; if (event.type === "message") { if (!_classPrivateFieldGet2(_onMessage, this)) throw new Error(`Channel "${channelKey}" received a message without an onMessage callback`); await _classPrivateFieldGet2(_onMessage, this).call(this, { channelKey, route, dispatchId, message: event }); return; } if (!_classPrivateFieldGet2(_onApprovalResponse, this)) throw new Error(`Channel "${channelKey}" received an approval response without an onApprovalResponse callback`); await _classPrivateFieldGet2(_onApprovalResponse, this).call(this, { channelKey, route, dispatchId, response: event }); } async function _route(channelKey, channel, event, raw) { const context = _assertClassBrand(_ChannelHost_brand, this, _routeContext).call(this, event); const route = channel.route ? await channel.route(event, raw, context) : _classPrivateFieldGet2(_defaultRoute, this) ? await _classPrivateFieldGet2(_defaultRoute, this).call(this, event, raw, context) : event.thread.id; if (route === void 0) throw new Error(`Channel route for "${channelKey}" returned undefined; return null to ignore an event`); if (route !== null && typeof route !== "string") throw new Error(`Channel route for "${channelKey}" must return a string or null`); return route; } function _routeContext(event) { let linkedUser; return { findUser: () => { if (!linkedUser) { const identity = event.actor?.identity; const findUser = _classPrivateFieldGet2(_findUser, this); linkedUser = identity && findUser ? Promise.resolve().then(() => findUser(identity)) : Promise.resolve(null); } return linkedUser; } }; } /** * Serve a Channel that cannot stream by collecting the answer first. * * A generation that failed part-way still delivers what it produced, because * losing the partial answer helps nobody, but the result is downgraded to * `uncertain` since the reader received an incomplete answer. */ function invalidSurface() { return unsupported("CHANNEL_SURFACE_INVALID", "Cannot resolve an invalid Channel message surface"); } async function collectAndDeliver(channel, surface, stream, options) { const collected = await collectText(stream); if (collected.interrupted && collected.text.length === 0) return { status: "failed", retryable: false, error: { code: "CHANNEL_STREAM_INTERRUPTED", message: "The stream ended before producing any content to deliver" } }; const result = await channel.deliver(surface, { ...options.title !== void 0 && { title: options.title }, markdown: collected.text }, options.delivery ? { delivery: options.delivery } : void 0); if (!collected.interrupted || result.status !== "delivered") return result; return { status: "uncertain", ...result.reference !== void 0 && { reference: result.reference }, error: { code: "CHANNEL_STREAM_INTERRUPTED", message: "An incomplete answer was delivered because the stream ended early" } }; } function stampSurface(channelKey, surface) { return { ...surface, channelKey }; } function stampIdentity(channelKey, identity) { return { ...identity, channelKey }; } function stampEvent(channelKey, event) { return { ...event, ...event.replySurface && { replySurface: stampSurface(channelKey, event.replySurface) }, ...event.actor && { actor: { ...event.actor, ...event.actor.identity && { identity: stampIdentity(channelKey, event.actor.identity) } } } }; } /** Hash an unambiguous tuple so dispatch identities remain safe to carry. */ async function createDispatchId(channelKey, eventId) { const identity = new TextEncoder().encode(JSON.stringify([channelKey, eventId])); const digest = await crypto.subtle.digest("SHA-256", identity); return `sha256:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`; } //#endregion //#region src/channels/identity.ts const DEFAULT_IDENTITY_SCOPE = "default"; function normalizeIdentity(identity) { return { channelKey: identity.channelKey, scope: identity.scope ?? DEFAULT_IDENTITY_SCOPE, subject: identity.subject }; } /** Build a stable key for comparing or indexing a Channel identity. */ function identityKey(identity) { const normalized = normalizeIdentity(identity); return JSON.stringify([ normalized.channelKey, normalized.scope, normalized.subject ]); } /** Raised when an operation would join identities already assigned to users. */ var UserIdentityConflictError = class extends Error { constructor(conflicts, attemptedUserId) { super(attemptedUserId ? `A channel identity is already linked to a different user than ${attemptedUserId}` : "Both channel identities are already linked to users"); this.conflicts = conflicts; this.attemptedUserId = attemptedUserId; this.code = "USER_IDENTITY_CONFLICT"; this.name = "UserIdentityConflictError"; } }; const USERS_TABLE = "cf_channels_users_v1"; const LINKS_TABLE = "cf_channels_user_identity_links_v1"; function requireNonEmpty(label, value) { if (value.length === 0) throw new TypeError(`${label} must be a non-empty string`); } function validatedIdentity(identity) { const normalized = normalizeIdentity(identity); requireNonEmpty("identity.channelKey", normalized.channelKey); requireNonEmpty("identity.scope", normalized.scope); requireNonEmpty("identity.subject", normalized.subject); return normalized; } function sameIdentity(first, second) { return identityKey(first) === identityKey(second); } function compareIdentities(first, second) { return first.channelKey.localeCompare(second.channelKey) || first.scope.localeCompare(second.scope) || first.subject.localeCompare(second.subject); } /** Build the off-the-shelf user identity store over application-owned SQLite. */ function createUserIdentityStore(storage, options = {}) { const createUserId = options.createUserId ?? (() => crypto.randomUUID()); storage.sql.exec(` CREATE TABLE IF NOT EXISTS ${USERS_TABLE} ( user_id TEXT PRIMARY KEY ) WITHOUT ROWID `); storage.sql.exec(` CREATE TABLE IF NOT EXISTS ${LINKS_TABLE} ( channel_key TEXT NOT NULL, scope TEXT NOT NULL, subject TEXT NOT NULL, user_id TEXT NOT NULL, PRIMARY KEY (channel_key, scope, subject) ) WITHOUT ROWID `); storage.sql.exec(` CREATE INDEX IF NOT EXISTS ${LINKS_TABLE}_user ON ${LINKS_TABLE} (user_id, channel_key, scope, subject) `); function linkedUserId(identity) { return storage.sql.exec(`SELECT user_id FROM ${LINKS_TABLE} WHERE channel_key = ? AND scope = ? AND subject = ?`, identity.channelKey, identity.scope, identity.subject).toArray()[0]?.user_id ?? null; } function readUser(userId) { const user = storage.sql.exec(`SELECT user_id FROM ${USERS_TABLE} WHERE user_id = ?`, userId).toArray()[0]; if (!user) return null; const channelIdentities = storage.sql.exec(`SELECT channel_key AS channelKey, scope, subject FROM ${LINKS_TABLE} WHERE user_id = ? ORDER BY channel_key, scope, subject`, userId).toArray().sort(compareIdentities); return { id: user.user_id, channelIdentities }; } function insertUser(userId) { return storage.sql.exec(`INSERT INTO ${USERS_TABLE} (user_id) VALUES (?) ON CONFLICT (user_id) DO NOTHING RETURNING user_id`, userId).toArray()[0] !== void 0; } function createUser() { for (let attempt = 0; attempt < 10; attempt += 1) { const userId = createUserId(); requireNonEmpty("createUserId() result", userId); if (insertUser(userId)) return userId; } throw new Error("createUserId() repeatedly returned existing user IDs"); } function insertLink(userId, identity) { storage.sql.exec(`INSERT INTO ${LINKS_TABLE} (channel_key, scope, subject, user_id) VALUES (?, ?, ?, ?)`, identity.channelKey, identity.scope, identity.subject, userId); } return { async link(userId, identity) { requireNonEmpty("userId", userId); const normalized = validatedIdentity(identity); return storage.transactionSync(() => { const existingUserId = linkedUserId(normalized); if (existingUserId && existingUserId !== userId) throw new UserIdentityConflictError([{ channelIdentity: normalized, userId: existingUserId }], userId); insertUser(userId); if (!existingUserId) insertLink(userId, normalized); const user = readUser(userId); if (!user) throw new Error("Linked user identity could not be read"); return user; }); }, async findUser(identity) { const userId = linkedUserId(validatedIdentity(identity)); return userId ? readUser(userId) : null; }, async getUser(userId) { requireNonEmpty("userId", userId); return readUser(userId); }, async listUsers() { return storage.sql.exec(`SELECT user_id FROM ${USERS_TABLE} ORDER BY user_id`).toArray().map(({ user_id }) => readUser(user_id)).filter((user) => user !== null); }, async linkChannelIdentities(first, second) { const normalizedFirst = validatedIdentity(first); const normalizedSecond = validatedIdentity(second); if (sameIdentity(normalizedFirst, normalizedSecond)) throw new TypeError("Channel identities must be distinct"); return storage.transactionSync(() => { const firstUserId = linkedUserId(normalizedFirst); const secondUserId = linkedUserId(normalizedSecond); if (firstUserId && secondUserId) throw new UserIdentityConflictError([{ channelIdentity: normalizedFirst, userId: firstUserId }, { channelIdentity: normalizedSecond, userId: secondUserId }]); const userId = firstUserId ?? secondUserId ?? createUser(); if (!firstUserId) insertLink(userId, normalizedFirst); if (!secondUserId) insertLink(userId, normalizedSecond); const user = readUser(userId); if (!user) throw new Error("Linked user identity could not be read"); return user; }); } }; } /** Join two Channel identities through a `UserIdentityStore`. */ function linkChannelIdentities(store, first, second) { return store.linkChannelIdentities(first, second); } //#endregion //#region src/channels/routes.ts /** * Common deterministic routing policies for normalized Channel events. * * Each one maps an event to a namespaced application route. Deciding whether * an event is relevant at all is application policy: write that in your own * `route` function and return `null` to ignore the event. */ const routes = { /** * Give every event its own application route. Useful to kick off a * new conversation for each ingress, and where you don't want subsequent * messages routed back to that same conversation. **/ perEvent(event) { return `event:${event.eventId}`; }, /** Send every event in the same thread to the same conversation */ perThread(event) { return `thread:${event.thread.id}`; }, /** * Prefer the sender's own channel identity, then delegate to another route * policy. Without a fallback, an event carrying no identity is ignored. * * This groups events that carry the *same* identity. It never infers that * two different identities belong to one person: that is an explicit * application decision, exposed to routing through `byUser`. */ byIdentity(fallback) { return (event, raw, context) => { const identity = event.actor?.identity; if (identity) return `identity:${identityKey(identity)}`; return fallback ? fallback(event, raw, context) : null; }; }, /** Prefer an explicitly linked user, then delegate to another route policy. */ byUser(fallback) { return async (event, raw, context) => { const user = await context.findUser(); return user ? `user:${user.id}` : fallback(event, raw, context); }; } }; //#endregion export { ChannelHost, UserIdentityConflictError, consumeChunks, createUserIdentityStore, fallback, fallbackChannel, fanout, fanoutChannel, identityKey, isChannelMessageSurface, linkChannelIdentities, matchesPath, routes }; //# sourceMappingURL=index.js.map