agents
Version:
A home for your AI agents
518 lines (517 loc) • 19.5 kB
JavaScript
import { camelCaseToKebabCase, isInternalJsStubProp } from "./utils.js";
import "./types.js";
import { i as capnWebTransportUrl, n as CAPNWEB_TRANSPORT_SEND } from "./transport-protocol-BxyngQ_R.js";
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, t as _classPrivateFieldGet2 } from "./classPrivateFieldGet2-DZBYAB34.js";
import { PartySocket } from "partysocket";
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
//#region src/websockets/capnweb-socket.ts
/** Local root the host calls to deliver frames. */
var Inbox = class extends RpcTarget {
constructor(socket) {
super();
this.socket = socket;
}
message(value) {
this.socket.dispatchEvent(new MessageEvent("message", { data: value }));
}
};
var _socket = /* @__PURE__ */ new WeakMap();
var _root = /* @__PURE__ */ new WeakMap();
/**
* A WebSocket-shaped client for the Cap'n Web connection transport.
*
* It has the constructor, `readyState`, `send()`, `close()`, and events of
* a `WebSocket`, so PartySocket (and therefore `useAgent` and
* `AgentClient`) can drive it as a drop-in socket implementation and keep
* owning reconnection, buffering, and backoff. Frames sent here travel
* through the host's pipe method; frames from the host arrive as
* `message` events.
*
* @experimental The transport is experimental.
*/
var CapnWebSocket = class CapnWebSocket extends EventTarget {
constructor(url, protocols) {
super();
this.CONNECTING = 0;
this.OPEN = 1;
this.CLOSING = 2;
this.CLOSED = 3;
this.readyState = CapnWebSocket.CONNECTING;
this.binaryType = "arraybuffer";
this.bufferedAmount = 0;
this.extensions = "";
this.protocol = "";
this.onopen = null;
this.onmessage = null;
this.onclose = null;
this.onerror = null;
_classPrivateFieldInitSpec(this, _socket, void 0);
_classPrivateFieldInitSpec(this, _root, void 0);
this.url = capnWebTransportUrl(url);
_classPrivateFieldSet2(_socket, this, protocols ? new WebSocket(this.url, protocols) : new WebSocket(this.url));
_classPrivateFieldSet2(_root, this, newWebSocketRpcSession(_classPrivateFieldGet2(_socket, this), new Inbox(this)));
_classPrivateFieldGet2(_socket, this).addEventListener("open", () => {
this.readyState = CapnWebSocket.OPEN;
this.protocol = _classPrivateFieldGet2(_socket, this).protocol;
this.dispatchEvent(new Event("open"));
});
_classPrivateFieldGet2(_socket, this).addEventListener("close", (event) => {
this.readyState = CapnWebSocket.CLOSED;
this.dispatchEvent(new CloseEvent("close", {
code: event.code,
reason: event.reason,
wasClean: event.wasClean
}));
});
_classPrivateFieldGet2(_socket, this).addEventListener("error", () => {
this.dispatchEvent(new Event("error"));
});
}
dispatchEvent(event) {
const handler = Reflect.get(this, `on${event.type}`);
if (typeof handler === "function") Reflect.apply(handler, this, [event]);
return super.dispatchEvent(event);
}
/**
* Invoke one of the host's native callables on the session root. The
* result keeps Cap'n Web semantics: an `RpcTarget` arrives as a live
* stub, a `ReadableStream` streams, and chained calls pipeline.
*/
invoke(method, args) {
if (this.readyState !== CapnWebSocket.OPEN) return Promise.reject(/* @__PURE__ */ new Error("Connection closed"));
const root = _classPrivateFieldGet2(_root, this);
return Promise.resolve(root[method](...args));
}
send(data) {
if (this.readyState !== CapnWebSocket.OPEN) throw new DOMException("WebSocket is not open", "InvalidStateError");
_classPrivateFieldGet2(_root, this)[CAPNWEB_TRANSPORT_SEND](data).catch(() => {
this.dispatchEvent(new Event("error"));
});
}
close(code, reason) {
if (this.readyState >= CapnWebSocket.CLOSING) return;
this.readyState = CapnWebSocket.CLOSING;
try {
_classPrivateFieldGet2(_root, this)[Symbol.dispose]();
} catch {}
_classPrivateFieldGet2(_socket, this).close(code, reason);
}
};
CapnWebSocket.CONNECTING = 0;
CapnWebSocket.OPEN = 1;
CapnWebSocket.CLOSING = 2;
CapnWebSocket.CLOSED = 3;
/**
* A `CapnWebSocket` subclass that reports each instance it constructs.
* PartySocket instantiates the class it is given; this is how a client
* gets hold of the live socket to call `invoke()` without reaching into
* PartySocket internals.
*/
function boundCapnWebSocket(onCreate) {
return class BoundCapnWebSocket extends CapnWebSocket {
constructor(url, protocols) {
super(url, protocols);
onCreate(this);
}
};
}
//#endregion
//#region src/client.ts
var AgentConnectionError = class extends Error {
constructor(event) {
const reason = event.reason || `WebSocket closed with code ${event.code}`;
super(`Agent connection closed: ${reason}`);
this.name = "AgentConnectionError";
this.code = event.code;
this.reason = event.reason;
this.wasClean = event.wasClean;
}
};
function isTerminalCloseEvent(event) {
return event.code === 1008 || event.code >= 4e3 && event.code <= 4999;
}
/**
* Default timeout (in milliseconds) applied to non-streaming RPC calls
* that don't pass an explicit `timeout`. Acts as a backstop so calls
* whose response is lost (e.g. the connection drops mid-flight) reject
* instead of hanging forever. Override per client via
* `defaultCallTimeout`, or per call via `timeout` (0 disables).
*/
const DEFAULT_CALL_TIMEOUT_MS = 3e4;
/**
* Normalize `call()` options. The legacy shape is the stream callbacks
* themselves (`{ onChunk, onDone, onError }`); the current shape nests
* them under `stream` beside `timeout`.
*/
function splitCallOptions(options) {
return options !== void 0 && ("onChunk" in options || "onDone" in options || "onError" in options) ? {
stream: options,
timeout: void 0
} : {
stream: options?.stream,
timeout: options?.timeout
};
}
/**
* One native call on the Cap'n Web transport, with the timeout and
* stream-callback contract `call()` has on the JSON wire. The timeout
* covers the whole call, draining included: on expiry the stream reader is
* cancelled and the promise rejects once. A `ReadableStream` result is
* drained into the stream callbacks when they are given; any other value,
* live stubs included, passes straight through.
*/
async function nativeCall(socket, method, args, options, defaultTimeout) {
const { stream, timeout } = splitCallOptions(options);
const effectiveTimeout = timeout !== void 0 ? timeout : stream ? void 0 : defaultTimeout;
let reader;
let timer;
const work = (async () => {
const result = await socket.invoke(method, args);
if (stream && result instanceof ReadableStream) {
reader = result.getReader();
for (;;) {
const { done, value } = await reader.read();
if (done) break;
stream.onChunk?.(value);
}
stream.onDone?.(void 0);
return;
}
return result;
})();
const expiry = effectiveTimeout ? new Promise((_, reject) => {
timer = setTimeout(() => {
reader?.cancel().catch(() => {});
reject(/* @__PURE__ */ new Error(`RPC call to ${method} timed out after ${effectiveTimeout}ms`));
}, effectiveTimeout);
}) : void 0;
try {
return await (expiry ? Promise.race([work, expiry]) : work);
} catch (error) {
work.catch(() => {});
stream?.onError?.(error instanceof Error ? error.message : String(error));
throw error;
} finally {
if (timer) clearTimeout(timer);
}
}
var _pending = /* @__PURE__ */ new WeakMap();
/**
* Native calls issued while the Cap'n Web socket is still connecting or
* between reconnects. They must not fall back to JSON frames — that would
* change which methods are reachable and lose return-by-reference — so
* they wait here and run on the next open socket, or reject when the
* connection ends for good. Their timeout starts when they are issued.
*/
var NativeCallQueue = class {
constructor() {
_classPrivateFieldInitSpec(this, _pending, []);
}
enqueue(method, args, options, defaultTimeout) {
return new Promise((resolve, reject) => {
const { stream, timeout } = splitCallOptions(options);
const effectiveTimeout = timeout !== void 0 ? timeout : stream ? void 0 : defaultTimeout;
const startedAt = Date.now();
const entry = {
run: (socket) => {
const remaining = effectiveTimeout === void 0 ? void 0 : Math.max(1, effectiveTimeout - (Date.now() - startedAt));
nativeCall(socket, method, args, remaining === void 0 ? options : {
stream,
timeout: remaining
}, defaultTimeout).then(resolve, reject);
},
reject: (error) => {
stream?.onError?.(error.message);
reject(error);
}
};
_classPrivateFieldGet2(_pending, this).push(entry);
if (effectiveTimeout) setTimeout(() => {
const index = _classPrivateFieldGet2(_pending, this).indexOf(entry);
if (index === -1) return;
_classPrivateFieldGet2(_pending, this).splice(index, 1);
entry.reject(/* @__PURE__ */ new Error(`RPC call to ${method} timed out after ${effectiveTimeout}ms`));
}, effectiveTimeout);
});
}
/** Run everything queued on a socket that just opened. */
flush(socket) {
const pending = _classPrivateFieldGet2(_pending, this);
_classPrivateFieldSet2(_pending, this, []);
for (const entry of pending) entry.run(socket);
}
/** Reject everything queued; the connection will not come back. */
rejectAll(reason) {
const pending = _classPrivateFieldGet2(_pending, this);
_classPrivateFieldSet2(_pending, this, []);
const error = new Error(reason);
for (const entry of pending) entry.reject(error);
}
};
/**
* Creates a proxy that wraps RPC method calls.
* Internal JS methods (toJSON, then, etc.) return undefined to avoid
* triggering RPC calls during serialization (e.g., console.log)
*/
function createStubProxy(call) {
return new Proxy({}, { get: (_target, method) => {
if (isInternalJsStubProp(method)) return;
return (...args) => call(method, args);
} });
}
var _capnWeb = /* @__PURE__ */ new WeakMap();
var _nativeQueue = /* @__PURE__ */ new WeakMap();
/**
* WebSocket client for connecting to an Agent
*/
var AgentClient = class extends PartySocket {
/**
* @deprecated Use agentFetch instead
*/
static fetch(_opts) {
throw new Error("AgentClient.fetch is not implemented, use agentFetch instead");
}
/**
* Promise that resolves when identity has been received from the server.
* Useful for waiting before making calls that depend on knowing the instance.
* Resets on connection close so it can be awaited again after reconnect.
*/
get ready() {
return this._readyPromise;
}
_resetReady() {
this._readyPromise = new Promise((resolve) => {
this._resolveReady = resolve;
});
}
constructor(options) {
const capnWeb = { current: null };
const socketClass = options.transport === "capnweb" ? { WebSocket: boundCapnWebSocket((socket) => {
capnWeb.current = socket;
}) } : {};
const agentNamespace = camelCaseToKebabCase(options.agent);
const shouldReconnectOnClose = options.shouldReconnectOnClose;
const classifyReconnect = (event) => (shouldReconnectOnClose?.(event) ?? true) && !isTerminalCloseEvent(event);
const socketOptions = options.basePath ? {
basePath: options.basePath,
path: options.path,
...options,
...socketClass,
shouldReconnectOnClose: classifyReconnect
} : {
party: agentNamespace,
prefix: "agents",
room: options.name || "default",
path: options.path,
...options,
...socketClass,
shouldReconnectOnClose: classifyReconnect
};
super(socketOptions);
this.state = void 0;
this.identified = false;
this.connectionError = null;
this._pendingCalls = /* @__PURE__ */ new Map();
this._previousName = null;
this._previousAgent = null;
_classPrivateFieldInitSpec(this, _capnWeb, void 0);
_classPrivateFieldInitSpec(this, _nativeQueue, new NativeCallQueue());
_classPrivateFieldSet2(_capnWeb, this, capnWeb);
this.agent = agentNamespace;
this.name = options.name || "default";
this.options = options;
this._resetReady();
this.addEventListener("message", (event) => {
if (typeof event.data === "string") {
let parsedMessage;
try {
parsedMessage = JSON.parse(event.data);
} catch (_error) {
return;
}
if (parsedMessage.type === "cf_agent_identity") {
const oldName = this._previousName;
const oldAgent = this._previousAgent;
const newName = parsedMessage.name;
const newAgent = parsedMessage.agent;
this.identified = true;
this._resolveReady();
if (oldName !== null && oldAgent !== null && (oldName !== newName || oldAgent !== newAgent)) if (this.options.onIdentityChange) this.options.onIdentityChange(oldName, newName, oldAgent, newAgent);
else {
const agentChanged = oldAgent !== newAgent;
const nameChanged = oldName !== newName;
let changeDescription = "";
if (agentChanged && nameChanged) changeDescription = `agent "${oldAgent}" → "${newAgent}", instance "${oldName}" → "${newName}"`;
else if (agentChanged) changeDescription = `agent "${oldAgent}" → "${newAgent}"`;
else changeDescription = `instance "${oldName}" → "${newName}"`;
console.warn(`[agents] Identity changed on reconnect: ${changeDescription}. This can happen with server-side routing (e.g., basePath with getAgentByName) where the instance is determined by auth/session. Provide onIdentityChange callback to handle this explicitly, or ignore if this is expected for your routing pattern.`);
}
this._previousName = newName;
this._previousAgent = newAgent;
this.name = newName;
this.agent = newAgent;
this.options.onIdentity?.(newName, newAgent);
return;
}
if (parsedMessage.type === "cf_agent_state") {
this.state = parsedMessage.state;
this.options.onStateUpdate?.(parsedMessage.state, "server");
return;
}
if (parsedMessage.type === "cf_agent_state_error") {
this.options.onStateUpdateError?.(parsedMessage.error);
return;
}
if (parsedMessage.type === "rpc") {
const response = parsedMessage;
const pending = this._pendingCalls.get(response.id);
if (!pending) {
console.warn(`[AgentClient] Discarded an RPC response with no matching pending call (id "${response.id}"). The call likely timed out or was rejected when its connection closed before the response arrived.`);
return;
}
if (!response.success) {
pending.reject(new Error(response.error));
this._pendingCalls.delete(response.id);
pending.stream?.onError?.(response.error);
return;
}
if ("done" in response) if (response.done) {
pending.resolve(response.result);
this._pendingCalls.delete(response.id);
pending.stream?.onDone?.(response.result);
} else pending.stream?.onChunk?.(response.result);
else {
pending.resolve(response.result);
this._pendingCalls.delete(response.id);
}
}
}
});
this.addEventListener("open", () => {
this.connectionError = null;
for (const pending of this._pendingCalls.values()) pending.transmitted = true;
const native = _classPrivateFieldGet2(_capnWeb, this).current;
if (native) _classPrivateFieldGet2(_nativeQueue, this).flush(native);
});
this.addEventListener("close", (event) => {
const terminalClose = isTerminalCloseEvent(event);
this.identified = false;
this._resetReady();
if (this.shouldReconnect) this._rejectPendingCalls("Connection closed", { onlyTransmitted: true });
else {
this._rejectPendingCalls("Connection closed");
_classPrivateFieldGet2(_nativeQueue, this).rejectAll("Connection closed");
if (terminalClose) {
const error = new AgentConnectionError(event);
this.connectionError = error;
this.options.onConnectionError?.(error);
}
}
});
this.call = this._callImpl.bind(this);
this.stub = createStubProxy((method, args) => this._callImpl(method, args));
}
/**
* Reject pending RPC calls with the given reason.
* With `onlyTransmitted`, calls still sitting in the send buffer are
* kept pending (they'll be flushed on reconnect).
*/
_rejectPendingCalls(reason, { onlyTransmitted = false } = {}) {
const error = new Error(reason);
for (const [id, pending] of this._pendingCalls) {
if (onlyTransmitted && !pending.transmitted) continue;
this._pendingCalls.delete(id);
pending.reject(error);
pending.stream?.onError?.(reason);
}
}
setState(state) {
this.send(JSON.stringify({
state,
type: "cf_agent_state"
}));
this.state = state;
this.options.onStateUpdate?.(state, "client");
}
/**
* Close the connection and immediately reject all pending RPC calls.
* This provides immediate feedback on intentional close rather than
* waiting for the WebSocket close handshake to complete.
*
* Note: Any calls made after `close()` will be rejected when the
* underlying WebSocket close event fires.
*/
close(code, reason) {
this._rejectPendingCalls("Connection closed");
super.close(code, reason);
}
/**
* Call a method on the Agent.
* When AgentT is provided, method names are inferred from the agent's methods.
* Falls back to untyped string-based calls when AgentT is not provided.
*/
async _callImpl(method, args = [], options) {
if (this.options.transport === "capnweb") {
const timeout = this.options.defaultCallTimeout ?? 3e4;
const native = _classPrivateFieldGet2(_capnWeb, this).current;
if (native && this.readyState === this.OPEN) return nativeCall(native, method, args, options, timeout);
if (this.connectionError && this.readyState === this.CLOSED) throw new Error("Connection closed");
return _classPrivateFieldGet2(_nativeQueue, this).enqueue(method, args, options, timeout);
}
if (this.connectionError && this.readyState === this.CLOSED) throw new Error("Connection closed");
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();
let timeoutId;
const { stream: streamOptions, timeout } = splitCallOptions(options);
const effectiveTimeout = timeout !== void 0 ? timeout : streamOptions ? void 0 : this.options.defaultCallTimeout ?? 3e4;
if (effectiveTimeout) timeoutId = setTimeout(() => {
const pending = this._pendingCalls.get(id);
this._pendingCalls.delete(id);
const errorMessage = `RPC call to ${method} timed out after ${effectiveTimeout}ms`;
pending?.stream?.onError?.(errorMessage);
reject(new Error(errorMessage));
}, effectiveTimeout);
this._pendingCalls.set(id, {
reject: (e) => {
if (timeoutId) clearTimeout(timeoutId);
reject(e);
},
resolve: (value) => {
if (timeoutId) clearTimeout(timeoutId);
resolve(value);
},
stream: streamOptions,
transmitted: this.readyState === this.OPEN
});
const request = {
args,
id,
method,
type: "rpc"
};
this.send(JSON.stringify(request));
});
}
};
/**
* Make an HTTP request to an Agent
* @param opts Connection options
* @param init Request initialization options
* @returns Promise resolving to a Response
*/
function agentFetch(opts, init) {
const agentNamespace = camelCaseToKebabCase(opts.agent);
if (opts.basePath) return PartySocket.fetch({
basePath: opts.basePath,
...opts
}, init);
return PartySocket.fetch({
party: agentNamespace,
prefix: "agents",
room: opts.name || "default",
...opts
}, init);
}
//#endregion
export { agentFetch as a, nativeCall as c, NativeCallQueue as i, splitCallOptions as l, AgentConnectionError as n, createStubProxy as o, DEFAULT_CALL_TIMEOUT_MS as r, isTerminalCloseEvent as s, AgentClient as t, boundCapnWebSocket as u };
//# sourceMappingURL=client-C9DAjZIY.js.map