agents
Version:
A home for your AI agents
1,186 lines (1,185 loc) • 47.3 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 { 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