UNPKG

agents

Version:

A home for your AI agents

1,186 lines (1,185 loc) • 47.3 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 LifecycleCapability } from "../../capability-DBAKc6UZ.js"; import { t as TextSegmentJoiner } from "../../text-segment-joiner-BtAFQSA_.js"; import { n as Streams } from "../../streams-lsHMJhRW.js"; import { t as applyChunks } from "../../apply-chunks-1FheRb9W.js"; import { a as toParticipant, i as participantRoute, o as unsupported } from "../../internal-kmw70xwR.js"; import { t as WEB_IDENTITY_HEADER } from "../../protocol-y2sSY584.js"; import { t as matchesPath } from "../../ingress-EFbrS547.js"; //#region src/experimental/channels/harness-conversations.ts function isTurnEvent(event) { return event.type !== "conversation-create" && event.type !== "conversation-fork" && event.type !== "conversation-reset"; } const PREFIX = "channels:harness:"; var _harness$1 = /* @__PURE__ */ new WeakMap(); var _host$1 = /* @__PURE__ */ new WeakMap(); var _links = /* @__PURE__ */ new WeakMap(); var _HarnessConversations_brand = /* @__PURE__ */ new WeakSet(); /** * Serves harness sessions as Channels conversations: the conversation id is * the session id, a turn is one operation (its id is the inbound message's * event id), and a run is one response. Operations that join a running run * (steers) share its response. * * The harness owns the transcript. Turn records live in storage; each * watched session's transcript is held in memory, from the watch's state * and the messages that follow it. */ var HarnessConversations = class { constructor(harness, host) { _classPrivateMethodInitSpec(this, _HarnessConversations_brand); _classPrivateFieldInitSpec(this, _harness$1, void 0); _classPrivateFieldInitSpec(this, _host$1, void 0); _classPrivateFieldInitSpec(this, _links, /* @__PURE__ */ new Map()); _classPrivateFieldSet2(_harness$1, this, harness); _classPrivateFieldSet2(_host$1, this, host); } /** The conversation a surface joins when it names none. */ defaultConversation() { return _classPrivateFieldGet2(_harness$1, this).session().id; } /** Follow every session a surface has joined. Call from `onStart`. */ async attachAll() { const sessions = _classPrivateFieldGet2(_host$1, this).kv().list({ prefix: `${PREFIX}session:` }); await Promise.all([...sessions].map(([key]) => _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, key.slice(`${PREFIX}session:`.length)).catch((error) => console.error("Failed to follow a harness session", error)))); } async snapshot(conversationId) { return { messages: [...(await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, conversationId)).messages], turns: _assertClassBrand(_HarnessConversations_brand, this, _turns).call(this, conversationId).filter(isOpen) }; } async onEvent(event, origin) { const conversationId = origin.conversationId; const link = await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, conversationId); switch (event.type) { case "message": if (!_assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, event.eventId)) await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, { turnId: event.eventId, startedBy: event.eventId, messageId: event.message.id, status: "queued" }); await link.session.submit({ parts: inputParts(event.message), messageId: event.message.id, from: { participantId: origin.participant.id } }, { operationId: event.eventId }); return; case "tool-result": case "approval-response": { const turn = _assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, event.turnId); if (!turn) return; const operationId = event.eventId; _classPrivateFieldGet2(_host$1, this).kv().put(aliasKey(conversationId, operationId), turn.turnId); await link.session.submit({ ...event.type === "tool-result" ? { type: "tool-result", toolCallId: event.toolCallId, result: event.result } : { type: "approval", approvalId: event.approvalId, approved: event.approved, ...event.reason !== void 0 && { reason: event.reason } }, from: { participantId: origin.participant.id } }, { operationId }); return; } case "cancel": { const turn = _assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, event.turnId); await link.session.abort(turn?.operation ?? event.turnId); return; } } } /** Every session, as conversations. */ async list() { return (await _classPrivateFieldGet2(_harness$1, this).sessions.list()).map(({ id, parent, busy }) => ({ id, busy, ...parent !== void 0 && { parent } })); } /** Create a session; a conversation operation. */ async create() { const session = await _classPrivateFieldGet2(_harness$1, this).sessions.create(); await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, session.id); return session.id; } /** Fork a session; a conversation operation. */ async fork(from) { const session = await _classPrivateFieldGet2(_harness$1, this).sessions.fork(from); await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, session.id); return session.id; } /** Reset a session's context; the watch reports it as a `reset` event. */ async reset(conversationId, handoff) { await (await _assertClassBrand(_HarnessConversations_brand, this, _link).call(this, conversationId)).session.reset(handoff); } }; function _link(conversationId) { let link = _classPrivateFieldGet2(_links, this).get(conversationId); if (!link) { const attached = _assertClassBrand(_HarnessConversations_brand, this, _attach).call(this, conversationId).then((linked) => { linked.watch.closed.then(() => { if (_classPrivateFieldGet2(_links, this).get(conversationId) === attached) _classPrivateFieldGet2(_links, this).delete(conversationId); }); return linked; }, (error) => { _classPrivateFieldGet2(_links, this).delete(conversationId); throw error; }); link = attached; _classPrivateFieldGet2(_links, this).set(conversationId, link); } return link; } async function _attach(conversationId) { const session = _classPrivateFieldGet2(_harness$1, this).session(conversationId); const watch = await session.watch(); _classPrivateFieldGet2(_host$1, this).kv().put(`${PREFIX}session:${conversationId}`, true); const link = new SessionLink(session, watch); await _assertClassBrand(_HarnessConversations_brand, this, _resume).call(this, conversationId, link); watch.start((events) => _assertClassBrand(_HarnessConversations_brand, this, _onEvents).call(this, conversationId, link, events)); return link; } /** Catch up from the watch's state, after an attach or a restart. */ async function _resume(conversationId, link) { const { state } = link.watch; const live = new Set(state.pending.map((p) => p.operationId)); for (const status of state.pending) await _assertClassBrand(_HarnessConversations_brand, this, _operation).call(this, conversationId, link, status); if (state.run) { for (const id of state.run.operations) live.add(id); await _assertClassBrand(_HarnessConversations_brand, this, _runStart).call(this, conversationId, link, state.run.operations); for (const chunk of state.run.partial ?? []) link.append(chunk); } for (const turn of _assertClassBrand(_HarnessConversations_brand, this, _turns).call(this, conversationId)) { const operationId = turn.operation ?? turn.turnId; if (turn.status === "settled" || live.has(operationId)) continue; link.session.wait(operationId).then((result) => _assertClassBrand(_HarnessConversations_brand, this, _operation).call(this, conversationId, link, result).catch((error) => console.error("Failed to settle a turn", error)), (error) => console.error("Failed to settle a turn", error)); } } async function _onEvents(conversationId, link, events) { for (const event of events) try { await _assertClassBrand(_HarnessConversations_brand, this, _onEvent).call(this, conversationId, link, event); } catch (error) { console.error("Harness event failed", event.type, error); } } async function _onEvent(conversationId, link, event) { switch (event.type) { case "operation": return _assertClassBrand(_HarnessConversations_brand, this, _operation).call(this, conversationId, link, event.status); case "run-start": return _assertClassBrand(_HarnessConversations_brand, this, _runStart).call(this, conversationId, link, event.operations); case "chunk": link.append(event.chunk); return; case "message": link.upsert(event.message); await _classPrivateFieldGet2(_host$1, this).publishMessages(conversationId, [event.message]); return; case "run-end": link.endResponse(); return; case "reset": link.endResponse(); link.messages = []; await _classPrivateFieldGet2(_host$1, this).reset(conversationId); return; } } async function _runStart(conversationId, link, operations) { link.endResponse(); const queued = _assertClassBrand(_HarnessConversations_brand, this, _turns).call(this, conversationId).find((t) => t.status === "queued"); const first = operations[0] ?? queued?.operation ?? queued?.turnId; if (first === void 0) return; const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, first); if (!turn) return; link.response = await _classPrivateFieldGet2(_host$1, this).openResponse(conversationId, turn.turnId); const last = link.messages.at(-1); link.extends = turn.turnId !== first && last?.role === "assistant" ? last.id : void 0; for (const id of operations.length ? operations : [first]) await _assertClassBrand(_HarnessConversations_brand, this, _join).call(this, conversationId, link, id); } async function _operation(conversationId, link, status) { const { operationId } = status; const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, operationId); switch (status.status) { case "queued": if (!turn) await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, { turnId: operationId, startedBy: operationId, messageId: operationId, status: "queued" }); else if (turn.turnId !== operationId && turn.status === "settled") await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, { turnId: turn.turnId, startedBy: turn.startedBy, messageId: turn.messageId, operation: operationId, status: "queued" }); return; case "placed": return _assertClassBrand(_HarnessConversations_brand, this, _join).call(this, conversationId, link, operationId); case "done": return _assertClassBrand(_HarnessConversations_brand, this, _settle).call(this, conversationId, link, operationId, { outcome: "completed" }); case "unanswered": { const stopped = status.reason === "aborted" || status.reason === "withdrawn"; const refusedAnswer = turn !== void 0 && turn.turnId !== operationId && !stopped; return _assertClassBrand(_HarnessConversations_brand, this, _settle).call(this, conversationId, link, operationId, stopped ? { outcome: "aborted" } : refusedAnswer ? { outcome: "completed" } : { outcome: "failed", error: `Not answered${status.reason ? `: ${status.reason}` : ""}` }); } } } async function _join(conversationId, link, operationId) { const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, operationId); if (!turn || !link.response) return; const answer = turn.turnId !== operationId; if (turn.status === "settled" && !answer) return; if (turn.status === "running" && turn.responseId === link.response.id) return; await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, { turnId: turn.turnId, startedBy: turn.startedBy, messageId: turn.messageId, ...answer && { operation: operationId }, status: "running", responseId: link.response.id, ...link.extends !== void 0 && { extends: link.extends } }); } async function _settle(conversationId, link, operationId, end) { const turn = _assertClassBrand(_HarnessConversations_brand, this, _turn).call(this, conversationId, operationId); if (!turn || turn.status === "settled") return; if ((turn.operation ?? turn.turnId) !== operationId) return; const start = link.messages.findIndex((m) => m.id === turn.messageId); const messageIds = []; for (const message of start === -1 ? [] : link.messages.slice(start + 1)) { if (message.role === "user") break; messageIds.push(message.id); } const waiting = end.outcome === "completed" && link.messages.some((message) => messageIds.includes(message.id) && awaitsInput(message)); await _assertClassBrand(_HarnessConversations_brand, this, _save).call(this, conversationId, { turnId: turn.turnId, startedBy: turn.startedBy, messageId: turn.messageId, ...turn.operation !== void 0 && { operation: turn.operation }, status: "settled", messageIds, ...waiting ? { outcome: "awaiting-input" } : end }); } async function _save(conversationId, turn) { _classPrivateFieldGet2(_host$1, this).kv().put(turnKey(conversationId, turn.turnId), turn); await _classPrivateFieldGet2(_host$1, this).publishTurn(conversationId, turn); } function _turnRecord(conversationId, turnId) { return _classPrivateFieldGet2(_host$1, this).kv().get(turnKey(conversationId, turnId)); } /** The turn an operation belongs to: its own, or the one it answers. */ function _turn(conversationId, operationId) { const turnId = _classPrivateFieldGet2(_host$1, this).kv().get(aliasKey(conversationId, operationId)) ?? operationId; return _assertClassBrand(_HarnessConversations_brand, this, _turnRecord).call(this, conversationId, turnId); } function _turns(conversationId) { const prefix = turnKey(conversationId, ""); return [..._classPrivateFieldGet2(_host$1, this).kv().list({ prefix })].map(([, turn]) => turn); } /** One watched session: its transcript and the response of its run. */ var SessionLink = class { constructor(session, watch) { this.session = session; this.watch = watch; this.messages = [...watch.state.messages]; } upsert(message) { const index = this.messages.findIndex((m) => m.id === message.id); if (index === -1) this.messages.push(message); else this.messages[index] = message; } append(chunk) { try { this.response?.append(chunk); } catch (error) { console.warn("Dropped a harness chunk", chunk.type, error); } } endResponse() { this.response?.end(); this.response = void 0; this.extends = void 0; } }; function turnKey(conversationId, turnId) { return `${PREFIX}turn:${JSON.stringify(conversationId)}:${turnId}`; } function aliasKey(conversationId, operationId) { return `${PREFIX}alias:${JSON.stringify(conversationId)}:${operationId}`; } /** Whether the message has a tool call waiting for a result or approval. */ function awaitsInput(message) { return message.parts.some((part) => part.type === "tool" && (part.state === "input-available" || part.state === "approval-requested")); } function isOpen(turn) { return turn.status !== "settled" || turn.outcome === "awaiting-input"; } function inputParts(message) { return message.parts.flatMap((part) => { if (part.type === "text") return [{ type: "text", text: part.text }]; if (part.type === "file") return [{ type: "file", mediaType: part.mediaType, url: part.url, ...part.filename !== void 0 && { filename: part.filename } }]; return []; }); } //#endregion //#region src/experimental/channels/response.ts /** A chunk that breaks the response grammar. Nothing was recorded. */ var ResponseGrammarError = class extends Error { constructor(message) { super(message); this.name = "ResponseGrammarError"; } }; function responseWriter(writer) { const open = /* @__PURE__ */ new Set(); function check(chunk) { const [kind, id, step] = partOf(chunk); if (!kind) return void 0; const key = `${kind}:${id}`; if (step === "start") { if (open.has(key)) fail(`${kind} "${id}" is already open`); return () => open.add(key); } if (step !== "close" && !open.has(key)) fail(`${kind} "${id}" is not open`); return step === "delta" ? void 0 : () => open.delete(key); } const response = { id: writer.streamId, append(chunk) { const apply = check(chunk); writer.append(chunk); apply?.(); }, end() { writer.close(); }, async pipe(chunks) { try { for await (const chunk of chunks) response.append(chunk); } catch (error) { writer.error("interrupted"); throw error; } response.end(); } }; return response; } function partOf(chunk) { switch (chunk.type) { case "text-start": return [ "text", chunk.id, "start" ]; case "text-delta": return [ "text", chunk.id, "delta" ]; case "text-end": return [ "text", chunk.id, "end" ]; case "reasoning-start": return [ "reasoning", chunk.id, "start" ]; case "reasoning-delta": return [ "reasoning", chunk.id, "delta" ]; case "reasoning-end": return [ "reasoning", chunk.id, "end" ]; case "tool-input-start": return [ "tool input", chunk.toolCallId, "start" ]; case "tool-input-delta": return [ "tool input", chunk.toolCallId, "delta" ]; case "tool-input-available": case "tool-input-error": return [ "tool input", chunk.toolCallId, "close" ]; default: return [void 0]; } } function fail(message) { throw new ResponseGrammarError(message); } //#endregion //#region src/experimental/channels/conversations.ts const TAG_PREFIX = "channels:"; const GENERIC_FAILURE = "The turn failed."; const SEEN_EVENTS_LIMIT = 1e3; var _websockets = /* @__PURE__ */ new WeakMap(); var _harness = /* @__PURE__ */ new WeakMap(); var _channels$1 = /* @__PURE__ */ new WeakMap(); var _queues = /* @__PURE__ */ new WeakMap(); var _seen = /* @__PURE__ */ new WeakMap(); var _Channels_brand = /* @__PURE__ */ new WeakSet(); /** * Serves an agent harness's sessions as conversations: carries inbound * events to the harness, and responses, turn statuses and transcript * updates back to every channel showing the conversation. */ var Channels = class Channels extends LifecycleCapability { /** * Channels for any shared-interface harness, with their own Streams and * (when a channel needs them) WebSockets. Lifecycle does not install one * capability from another yet, so install them alongside: * * ```ts * readonly channels = Channels.forHarness(harness, { * channels: { web: new WebChannel() } * }); * readonly lifecycle = Lifecycle.install(this) * .use(this.harness) * .use(this.channels.streams) * .use(this.channels) * .use(this.channels.websockets); * * receive(event: GatewayEvent, origin: GatewayOrigin) { * return this.channels.receive(event, origin); * } * ``` */ static forHarness(harness, options) { return new Channels({ harness, channels: options.channels, streams: options.streams ?? new Streams(), ...options.websockets && { websockets: options.websockets } }); } constructor(options) { super("channels"); _classPrivateMethodInitSpec(this, _Channels_brand); _classPrivateFieldInitSpec(this, _websockets, void 0); _classPrivateFieldInitSpec(this, _harness, void 0); _classPrivateFieldInitSpec(this, _channels$1, void 0); _classPrivateFieldInitSpec(this, _queues, /* @__PURE__ */ new Map()); _classPrivateFieldInitSpec(this, _seen, /* @__PURE__ */ new Map()); this.streams = options.streams; _classPrivateFieldSet2(_websockets, this, options.websockets); _classPrivateFieldSet2(_harness, this, new HarnessConversations(options.harness, { kv: () => this.lifecycle.storage.kv, openResponse: (conversationId, turnId) => _assertClassBrand(_Channels_brand, this, _openResponse).call(this, conversationId, turnId), publishTurn: (conversationId, turn) => _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, { type: "turn", turn: statusOf(turn) }), publishMessages: (conversationId, messages) => _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, { type: "messages", messages }), reset: (conversationId) => _assertClassBrand(_Channels_brand, this, _reset).call(this, conversationId) })); _classPrivateFieldSet2(_channels$1, this, Object.entries(options.channels)); for (const [channelKey, channel] of _classPrivateFieldGet2(_channels$1, this)) channel.mount(_assertClassBrand(_Channels_brand, this, _host).call(this, channelKey)); } /** * The WebSockets the channels share. Install it with Lifecycle. Throws * when no channel uses WebSockets. */ get websockets() { if (!_classPrivateFieldGet2(_websockets, this)) throw new Error("No channel uses WebSockets"); return _classPrivateFieldGet2(_websockets, this); } /** Mark responses left streaming by an earlier instance as interrupted. */ async onStart() { const streaming = await this.streams.list({ state: "streaming", limit: 1e3 }); for (const status of streaming) { const conversationId = conversationOf(status.tag); if (conversationId === void 0) continue; (await this.streams.open(status.streamId)).error("interrupted"); await _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, { type: "response-end", responseId: status.streamId, ending: "interrupted" }); } await _classPrivateFieldGet2(_harness, this).attachAll(); } /** * Deliver an inbound event from the gateway, through the same path as * the agent's own channels. The agent calls this from its RPC method. */ async receive(event, from) { const origin = { conversationId: from.conversationId ?? _classPrivateFieldGet2(_harness, this).defaultConversation(), participant: from.participant, surface: from.surface }; if (isConversationOperation(event.type)) throw new Error(`${event.type} is not supported on this surface`); const { surface } = origin; this.lifecycle.storage.kv.put(surfaceKey(origin.conversationId, surface.channelKey, surface.address), surface); await _assertClassBrand(_Channels_brand, this, _dispatch$1).call(this, event.type !== "approval-response" || "turnId" in event ? event : await _assertClassBrand(_Channels_brand, this, _withTurn).call(this, event, origin), origin); } }; /** Open a new response for a turn. Each run opens one. */ async function _openResponse(conversationId, turnId) { return responseWriter(await this.streams.open(crypto.randomUUID(), { tag: TAG_PREFIX + conversationId, metadata: { turnId } })); } /** * After the harness reset a session: delete the conversation's responses, * stopping live producers, then push an empty snapshot. */ async function _reset(conversationId) { const streams = this.streams; const tag = TAG_PREFIX + conversationId; for (;;) { const batch = await streams.list({ tag }); if (batch.length === 0) break; for (const { streamId, state } of batch) { if (state === "streaming") (await streams.open(streamId)).close(); await streams.delete(streamId); } } const prefix = JSON.stringify([conversationId]).slice(0, -1) + ","; for (const key of _classPrivateFieldGet2(_seen, this).keys()) if (key.startsWith(prefix)) _classPrivateFieldGet2(_seen, this).delete(key); await _assertClassBrand(_Channels_brand, this, _publish).call(this, conversationId, { type: "reset" }); } async function _publish(conversationId, update) { await Promise.all(_classPrivateFieldGet2(_channels$1, this).map(async ([channelKey, channel]) => { try { await channel.publish(conversationId, update); } catch (error) { console.error(`Channel "${channelKey}" failed to publish`, error); } })); } function _host(channelKey) { const kv = () => this.lifecycle.storage.kv; const stateKey = (key) => `${TAG_PREFIX}state:${JSON.stringify([channelKey, key])}`; return { channelKey, websockets: (create) => _classPrivateFieldGet2(_websockets, this) ?? _classPrivateFieldSet2(_websockets, this, create()), surfaces: async (conversationId) => { const prefix = surfaceKey(conversationId, channelKey); return [...kv().list({ prefix })].map(([, surface]) => surface); }, state: { get: (key) => kv().get(stateKey(key)), put: (key, value) => kv().put(stateKey(key), value), delete: (key) => void kv().delete(stateKey(key)) }, snapshot: async (conversationId) => { const snapshot = await _assertClassBrand(_Channels_brand, this, _runInHost).call(this, () => _classPrivateFieldGet2(_harness, this).snapshot(conversationId)); return { ...snapshot, turns: snapshot.turns.map(statusOf) }; }, defaultConversation: () => _classPrivateFieldGet2(_harness, this).defaultConversation(), operations: CONVERSATION_OPERATIONS, listConversations: () => _assertClassBrand(_Channels_brand, this, _runInHost).call(this, () => _classPrivateFieldGet2(_harness, this).list()), dispatch: (event, origin) => _assertClassBrand(_Channels_brand, this, _dispatch$1).call(this, event, origin), readResponse: (conversationId, responseId, options) => _assertClassBrand(_Channels_brand, this, _readResponse).call(this, conversationId, responseId, options) }; } /** Events take effect in arrival order, one at a time per conversation. */ function _dispatch$1(event, origin) { const key = JSON.stringify([origin.conversationId, event.eventId]); const seen = _classPrivateFieldGet2(_seen, this).get(key); if (seen) return seen; const previous = _classPrivateFieldGet2(_queues, this).get(origin.conversationId); const run = async () => { let result = {}; try { result = await _assertClassBrand(_Channels_brand, this, _runInHost).call(this, async () => { if (isTurnEvent(event)) { await _classPrivateFieldGet2(_harness, this).onEvent(event, origin); return {}; } return _assertClassBrand(_Channels_brand, this, _operate).call(this, event, origin); }); } catch (error) { _classPrivateFieldGet2(_seen, this).delete(key); throw error; } if (event.type === "message") await _assertClassBrand(_Channels_brand, this, _publish).call(this, origin.conversationId, { type: "messages", messages: [event.message], origin, eventId: event.eventId }); return result; }; const result = previous ? previous.then(run, run) : run(); _classPrivateFieldGet2(_seen, this).set(key, result); if (_classPrivateFieldGet2(_seen, this).size > SEEN_EVENTS_LIMIT) _classPrivateFieldGet2(_seen, this).delete(_classPrivateFieldGet2(_seen, this).keys().next().value); const settled = result.catch(() => {}); _classPrivateFieldGet2(_queues, this).set(origin.conversationId, settled); settled.then(() => { if (_classPrivateFieldGet2(_queues, this).get(origin.conversationId) === settled) _classPrivateFieldGet2(_queues, this).delete(origin.conversationId); }); return result; } /** * Act on the conversation itself. Any participant may create, fork or * reset: the agent object is the authorization boundary, and the gateway * decides who reaches it. */ async function _operate(event, origin) { const harness = _classPrivateFieldGet2(_harness, this); switch (event.type) { case "conversation-create": return { conversationId: await harness.create() }; case "conversation-fork": return { conversationId: await harness.fork(origin.conversationId) }; case "conversation-reset": await harness.reset(origin.conversationId, event.handoff); return {}; default: throw new Error(`${event.type} is not a conversation operation`); } } /** Name the turn awaiting an approval that arrived without one. */ async function _withTurn(event, origin) { const { messages, turns } = await _assertClassBrand(_Channels_brand, this, _runInHost).call(this, () => _classPrivateFieldGet2(_harness, this).snapshot(origin.conversationId)); const message = messages.find((m) => m.parts.some((part) => part.type === "tool" && part.state === "approval-requested" && part.approval?.id === event.approvalId)); const turn = turns.find((t) => t.status === "settled" && message !== void 0 && t.messageIds.includes(message.id)); if (!turn) throw new Error("No turn is waiting for this approval"); return { ...event, turnId: turn.turnId }; } async function _readResponse(conversationId, responseId, options) { const streams = this.streams; if ((await streams.status(responseId))?.tag !== TAG_PREFIX + conversationId) return "not-found"; let cursor = options.from ?? 0; for await (const batch of streams.readBatches(responseId, { from: cursor, signal: options.signal, onUpToDate: () => options.onCaughtUp(cursor) })) { options.onChunks(batch[0].seq, batch.map((item) => item.chunk)); cursor = batch[batch.length - 1].seq + 1; } return (await streams.status(responseId))?.state === "errored" ? "interrupted" : "ended"; } async function _runInHost(fn) { return await this.lifecycle.runInHostContext(fn); } /** * Where a surface is recorded. Without an address, the prefix for all of a * channel's surfaces in the conversation. */ function surfaceKey(conversationId, channelKey, address) { const scope = `${TAG_PREFIX}surface:${JSON.stringify([conversationId, channelKey])}:`; return address === void 0 ? scope : scope + JSON.stringify(address); } const CONVERSATION_OPERATIONS = [ "conversation-create", "conversation-fork", "conversation-reset" ]; function isConversationOperation(type) { return CONVERSATION_OPERATIONS.includes(type); } /** A turn status with only its protocol fields. */ function statusOf(turn) { const { turnId, startedBy } = turn; switch (turn.status) { case "queued": return { turnId, startedBy, status: "queued" }; case "running": { const { responseId, extends: extended } = turn; return { turnId, startedBy, status: "running", responseId, ...extended !== void 0 && { extends: extended } }; } case "settled": { const { messageIds } = turn; return turn.outcome === "failed" ? { turnId, startedBy, status: "settled", outcome: "failed", messageIds, error: turn.error ?? GENERIC_FAILURE } : { turnId, startedBy, status: "settled", outcome: turn.outcome, messageIds }; } } } function conversationOf(tag) { return tag?.startsWith(TAG_PREFIX) ? tag.slice(9) : void 0; } //#endregion //#region src/experimental/channels/stream.ts /** * Read a stream to completion, then finalize exactly once. * * `onFinish` runs whether the stream closed normally, errored because the * generation failed, or stopped because `onChunk` threw. A Channel that * finalizes here cannot lose a terminal provider call to an early ending. */ async function consumeChunks(chunks, consumer) { const reader = chunks.getReader(); let outcome = { interrupted: false }; let cancellation; try { while (true) { const { done, value } = await reader.read(); if (done) break; await consumer.onChunk(value); } } catch (error) { outcome = { interrupted: true, error }; cancellation = reader.cancel().catch(() => {}); } finally { reader.releaseLock(); } try { return await consumer.onFinish(outcome); } finally { await cancellation; } } /** * Turn a response's chunks into answer text, one chunk at a time. Deltas of * one part join directly; a new text part or tool activity in between adds a * space. Metadata and data never split text. Returns the text each chunk adds. */ function createTextCollector() { const joiner = new TextSegmentJoiner(); return (chunk) => { if (chunk.type === "metadata" || chunk.type === "data" || chunk.type === "text-end") return ""; let added = ""; for (const event of joiner.pushChunk(chunk.type === "text-delta" ? { type: "text-delta", text: chunk.delta } : { type: chunk.type })) if (event.type === "text") added += event.text; return added; }; } /** * Collect a response's text into one Markdown answer. * * The result reports interruption instead of throwing, because a Channel that * has partial text still has to decide what to deliver. */ function collectText(chunks) { let text = ""; const collect = createTextCollector(); return consumeChunks(chunks, { onChunk(chunk) { text += collect(chunk); }, onFinish: (outcome) => ({ text, interrupted: outcome.interrupted }) }); } /** A finished message as a response's chunks. */ function messageChunks(markdown) { return new ReadableStream({ start(controller) { controller.enqueue({ type: "text-start", id: "message" }); controller.enqueue({ type: "text-delta", id: "message", delta: markdown }); controller.enqueue({ type: "text-end", id: "message" }); controller.close(); } }); } //#endregion //#region src/experimental/channels/surface.ts function isSurfaceValue(value) { if (value === null || typeof value === "boolean" || typeof value === "string") return true; if (typeof value === "number") return Number.isFinite(value); if (Array.isArray(value)) return value.every(isSurfaceValue); return typeof value === "object" && Object.values(value).every(isSurfaceValue); } /** Validate the common envelope before provider-specific address parsing. */ function isChannelMessageSurface(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; const surface = value; return typeof surface.channelKey === "string" && surface.channelKey.length > 0 && surface.version === 1 && isSurfaceValue(surface.address) && typeof surface.label === "string" && surface.label.trim().length > 0; } //#endregion //#region src/experimental/channels/gateway.ts var _channels = /* @__PURE__ */ new WeakMap(); var _agent = /* @__PURE__ */ new WeakMap(); var _onRoute = /* @__PURE__ */ new WeakMap(); var _ChannelGateway_brand = /* @__PURE__ */ new WeakSet(); /** * The Worker's entry point for Channels. Authenticates and normalizes * webhooks, resolves WebSocket upgrades, routes each to the agent object * its route names, and sends to surfaces. * * The gateway is the trust boundary. It alone decides who a sender is and * tells the agent, so an agent serving Channels must be reachable only * through it: an app that forwards an upgrade to the agent some other way * (another fetch handler, `routeAgentRequest`, RPC) lets the caller pick * its own participant and conversation. * * The agent object is the authorization boundary. Whoever a Channel routes * to an object may list, join, create, fork and reset every conversation in * it. To keep people apart, route them to different objects; by default * each participant gets an object of their own. */ var ChannelGateway = class { constructor(options) { _classPrivateMethodInitSpec(this, _ChannelGateway_brand); _classPrivateFieldInitSpec(this, _channels, void 0); _classPrivateFieldInitSpec(this, _agent, void 0); _classPrivateFieldInitSpec(this, _onRoute, void 0); _classPrivateFieldSet2(_channels, this, { ...options.channels }); for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) if ((channel.ingress || channel.emailIngress) && !channel.participant) throw new Error(`Channel "${channelKey}" takes ingress but has no participant; say who its senders are`); _classPrivateFieldSet2(_agent, this, options.agent); _classPrivateFieldSet2(_onRoute, this, options.onRoute); } /** Serve a request if it is for Channels, or return undefined. */ async fetch(request) { if (request.headers.get("Upgrade")?.toLowerCase() === "websocket") { const web = await _assertClassBrand(_ChannelGateway_brand, this, _upgrade).call(this, request); if (web) return web; } return _assertClassBrand(_ChannelGateway_brand, this, _webhook).call(this, request); } 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(_ChannelGateway_brand, this, _dispatch).call(this, channelKey, channel, envelope); return true; } return false; } /** Deliver through the configured Channel named by the surface. */ deliver(surface, message, options) { return _assertClassBrand(_ChannelGateway_brand, this, _outbound).call(this, surface, (channel, destination) => { if (channel.deliver) return channel.deliver(destination, message, options); if (channel.stream) return channel.stream(destination, messageChunks(message.markdown), { ...message.title !== void 0 && { title: message.title }, ...options }); return Promise.resolve(unsupported("CHANNEL_DELIVERY_UNSUPPORTED", `Channel "${destination.channelKey}" does not support delivery`)); }); } /** * Deliver a progressively generated answer to the Channel * 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(); } let channel; try { channel = _assertClassBrand(_ChannelGateway_brand, this, _configuredChannel).call(this, surface.channelKey); } catch (error) { await chunks.cancel().catch(() => {}); throw error; } 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); } /** 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; } }; /** * Forward a WebSocket upgrade to its agent with the resolved identity. * Any identity header the client sent is replaced. An upgrade no Channel * takes is left to the Worker, header and all, so the Worker must not * forward it to a Channels agent. */ async function _upgrade(request) { for (const [channelKey, channel] of Object.entries(_classPrivateFieldGet2(_channels, this))) { const upgrade = channel.upgrade; const match = upgrade?.match(request); if (!upgrade || !match) continue; return _assertClassBrand(_ChannelGateway_brand, this, _forwardUpgrade).call(this, channelKey, upgrade, match, request); } } async function _forwardUpgrade(channelKey, upgrade, match, request) { const source = `Channel "${channelKey}"`; const participant = toParticipant(await upgrade.participant(request), source); if (!participant) return new Response("Unauthorized", { status: 401 }); const route = checkRoute(await upgrade.route(request, participant), source); if (route === null) return new Response("Forbidden", { status: 403 }); const identity = { route, ...match.conversationId !== void 0 && { conversationId: match.conversationId }, participant }; const headers = new Headers(request.headers); headers.set(WEB_IDENTITY_HEADER, JSON.stringify(identity)); return _classPrivateFieldGet2(_agent, this).call(this, route).fetch(new Request(request, { headers })); } async function _webhook(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(_ChannelGateway_brand, this, _dispatch).call(this, channelKey, channel, envelope); return result.response; } catch { return new Response("Failed to handle Channel event", { status: 500 }); } } } async function _outbound(surface, operation) { if (!isChannelMessageSurface(surface)) return invalidSurface(); return operation(_assertClassBrand(_ChannelGateway_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 source = `Channel "${channelKey}"`; const participant = toParticipant(await channel.participant(event, envelope.raw), source); const route = participant ? checkRoute(channel.route ? await channel.route(event, envelope.raw, participant) : participantRoute(participant), source) : null; const dispatchId = await createDispatchId(channelKey, event.eventId); await _classPrivateFieldGet2(_onRoute, this)?.call(this, { channelKey, event, participant, route, dispatchId }); if (!participant || route === null) return; if (!event.replySurface) throw new Error(`Channel "${channelKey}" produced an event without a reply surface`); await _classPrivateFieldGet2(_agent, this).call(this, route).receive(toInboundEvent(event, dispatchId), { route, participant, surface: event.replySurface }); } /** Check what an application's `route` callback returned. */ function checkRoute(route, source) { if (route === void 0) throw new Error(`${source} route returned undefined; return null to ignore an event`); if (route !== null && typeof route !== "string") throw new Error(`${source} route must return a string or null`); return route; } /** The provider's event as an inbound event, keyed by its dispatch id. */ function toInboundEvent(event, dispatchId) { if (event.type === "approval-response") return { type: "approval-response", eventId: dispatchId, approvalId: event.approvalId, approved: event.decision === "approve" }; const { message } = event; const parts = [{ type: "text", text: message.markdown ?? message.text }]; for (const attachment of message.attachments ?? []) if (attachment.url && attachment.mediaType) parts.push({ type: "file", url: attachment.url, mediaType: attachment.mediaType, ...attachment.name !== void 0 && { filename: attachment.name } }); return { type: "message", eventId: dispatchId, message: { id: dispatchId, role: "user", parts } }; } function invalidSurface() { return unsupported("CHANNEL_SURFACE_INVALID", "Cannot resolve an invalid Channel message surface"); } /** * 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. */ 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/experimental/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 ]); } //#endregion //#region src/experimental/channels/routes.ts /** * Common deterministic routing policies for normalized Channel events. * * Each one maps an event to a namespaced agent object. That object is the * authorization boundary: anyone routed to it may use every conversation in * it. Deciding whether an event is relevant at all is application policy: * write that in your own `route` function and return `null` to ignore it. */ const routes = { /** * Give each participant an agent object of their own. The default for every * Channel, so that a group chat never shares an object by accident. * * A surface that names no conversation, such as a Slack thread or an * email, joins the object's default conversation, so a participant keeps * one history wherever they write from. */ perParticipant(_event, _raw, participant) { return participantRoute(participant); }, /** * 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 object, whoever sent * it. Everyone in the thread can then use that object's conversations. */ perThread(event) { return `thread:${event.thread.id}`; } }; //#endregion export { ChannelGateway, Channels, applyChunks, consumeChunks, identityKey, isChannelMessageSurface, matchesPath, routes }; //# sourceMappingURL=index.js.map