UNPKG

agents

Version:

A home for your AI agents

518 lines (517 loc) 19.5 kB
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