UNPKG

life

Version:

Life.js is the first fullstack framework to build agentic web applications. It is minimal, extensible, and typesafe. Well, everything you love.

1,198 lines (1,183 loc) 39.7 kB
import { TelemetryBrowserClient, createTelemetryClient, logLevelPriority, telemetryBrowserScopesDefinition } from "./chunk-5BLN2MK4.mjs"; import { importClientBuild } from "./chunk-VF3CTIUM.mjs"; import { TransportClientBase } from "./chunk-HZQWVS5S.mjs"; import { canon } from "./chunk-6CBODJTF.mjs"; import { pluginEventInputSchema, toMethodName } from "./chunk-X455LF5V.mjs"; import { LiveKitBrowserClient, deepClone } from "./chunk-D2T23PCX.mjs"; import { attempt, failure, isLifeError, lifeError, newId, success, toPublic } from "./chunk-ZHBK6UTM.mjs"; import { __name } from "./chunk-2D3UJWOA.mjs"; // shared/hmr.ts var rawHMR = ( // - Vite / Bun / modern dev servers typeof import.meta !== "undefined" && import.meta.hot || // - Webpack ESM typeof import.meta !== "undefined" && import.meta.webpackHot || // - Webpack CJS globalThis?.module?.hot || // - None null ); var hmr = Object.freeze( rawHMR ? { active: true, accept: rawHMR.accept ? (...args) => rawHMR.accept?.(...args) : void 0, dispose: rawHMR.dispose || rawHMR.addDisposeHandler ? (cb) => (rawHMR.dispose || rawHMR.addDisposeHandler)?.(cb) : void 0 } : { active: false } ); // plugins/client/class.ts import z from "zod"; var PluginClient = class { static { __name(this, "PluginClient"); } def; #config; #agent; #telemetry; #atoms; #extension; #eventsListeners = /* @__PURE__ */ new Map(); #contextListeners = /* @__PURE__ */ new Map(); #contextValue = {}; #lastContextValueTimestamp = 0; constructor(definition, config, agent) { this.def = definition; this.#agent = agent; const { error: errConfig, data: parsedConfig } = this.def.config.schema.safeParse(config); if (errConfig) { throw lifeError({ code: "Validation", message: `Invalid config provided to plugin client '${this.def.name}'.`, cause: errConfig }); } this.#config = parsedConfig; this.#telemetry = createTelemetryClient("plugin.client", { agentId: agent.id, agentName: agent.def.name, agentConfig: agent.config, transportProviderName: agent.config.transport.provider, pluginName: definition.name, pluginClientConfig: this.#config }); const [errAccessor, accessor] = this.getAccessor(); if (errAccessor) throw errAccessor; this.#extension = new (definition.class({ plugin: toPublic(accessor), agent: toPublic(this.#agent), dependencies: this.getDependenciesAccessor(), telemetry: this.#telemetry }))(); const atomsArr = definition.atoms({ plugin: toPublic(accessor), agent: toPublic(this.#agent), dependencies: this.getDependenciesAccessor(), telemetry: this.#telemetry }); const atomsMap = Object.fromEntries(atomsArr.map((atom2) => [atom2.name, atom2.create])); this.#atoms = atomsMap; } getAccessor() { const [errClone, cloneConfig] = attempt(() => deepClone(this.#config)); if (errClone) return failure({ code: "Unknown", cause: errClone }); const serverContextGet = /* @__PURE__ */ __name((() => attempt(() => deepClone(this.#contextValue))), "serverContextGet"); const serverContextOnChange = /* @__PURE__ */ __name(((selector, callback) => { const id = newId("listener"); this.#contextListeners.set(id, { id, callback, selector }); return success(() => this.#contextListeners.delete(id)); }), "serverContextOnChange"); const serverEventsEmit = /* @__PURE__ */ __name((async (event) => { const [err, data] = await this.#agent.transport.call({ name: `plugin.${this.def.name}.events.emit`, schema: { input: pluginEventInputSchema, output: z.object({ id: z.string() }) }, input: event }); if (err) return failure(err); return success(data.id); }), "serverEventsEmit"); const serverEventsOn = /* @__PURE__ */ __name(((selector, callback, includeDropped = false) => { const id = newId("listener"); this.#eventsListeners.set(id, { id, selector, callback, includeDropped }); this.#agent.transport.call({ name: `plugin.${this.def.name}.events.subscribe`, schema: { input: z.object({ listenerId: z.string(), selector: z.any(), includeDropped: z.boolean().prefault(false) }) }, input: { listenerId: id, selector, includeDropped } }); return success(() => { this.#agent.transport.call({ name: `plugin.${this.def.name}.events.unsubscribe`, schema: { input: z.object({ listenerId: z.string() }) }, input: { listenerId: id } }); this.#eventsListeners.delete(id); }); }), "serverEventsOn"); const serverEventsOnce = /* @__PURE__ */ __name(((selector, callback, includeDropped = false) => { const [errOn, unsubscribe] = serverEventsOn( selector, async (event) => { unsubscribe?.(); await callback(event); }, includeDropped ); if (errOn) return failure(errOn); return success(unsubscribe); }), "serverEventsOnce"); const serverEventsWaitForProcessing = /* @__PURE__ */ __name((async (eventId) => await this.#agent.transport.call({ name: `plugin.${this.def.name}.events.waitForProcessing`, schema: { input: z.object({ eventId: z.string() }) }, input: { eventId } })), "serverEventsWaitForProcessing"); const serverEventsWaitForResult = /* @__PURE__ */ __name((async (eventId, handlerName) => ( // @ts-expect-error await this.#agent.transport.call({ name: `plugin.${this.def.name}.events.waitForResult`, schema: { input: z.object({ eventId: z.string(), handlerName: z.string() }), output: z.object().loose() }, input: { eventId, handlerName } }) )), "serverEventsWaitForResult"); return success( Object.assign(this.#extension ?? {}, { config: cloneConfig, atoms: this.#atoms, server: { context: { onChange: serverContextOnChange, get: serverContextGet }, events: { emit: serverEventsEmit, on: serverEventsOn, once: serverEventsOnce, waitForProcessing: serverEventsWaitForProcessing, waitForResult: serverEventsWaitForResult } } }) ); } getDependenciesAccessor() { const dependenciesAccessors = {}; for (const dependencyDef of this.def.dependencies ?? []) { const accessor = this.#agent[toMethodName(dependencyDef.name)]; if (!accessor) return failure({ code: "NotFound", message: `Failed to obtain client instance for dependency plugin '${dependencyDef.name}'. Shouldn't happen.` }); dependenciesAccessors[dependencyDef.name] = accessor; } return success(dependenciesAccessors); } async start() { const [errPing, dataPing] = await this.#agent.transport.call({ name: "agent.has-plugin-server", schema: { input: z.object({ pluginName: z.string() }), output: z.object({ hasServer: z.boolean() }) }, input: { pluginName: this.def.name } }); if (errPing) return failure(errPing); if (!dataPing.hasServer) return success(); this.#agent.transport.register({ name: `plugin.${this.def.name}.context.changed`, schema: { input: z.object({ value: z.any(), timestamp: z.number() }) }, execute: /* @__PURE__ */ __name(async ({ value, timestamp }) => await this.#setContextValue(value, timestamp), "execute") }); this.#agent.transport.register({ name: `plugin.${this.def.name}.events.callback`, schema: { input: z.object({ listenerId: z.string(), event: pluginEventInputSchema }) }, execute: /* @__PURE__ */ __name(async ({ listenerId, event }) => { await this.#eventsListeners.get(listenerId)?.callback(event); return success(); }, "execute") }); const [err, data] = await this.#agent.transport.call({ name: `plugin.${this.def.name}.context.get`, schema: { output: z.object({ value: z.any(), timestamp: z.number() }) } }); if (err) return failure(err); return await this.#setContextValue(data.value, data.timestamp); } async #setContextValue(value, timestamp) { const oldContextValue = deepClone(this.#contextValue); if (timestamp < this.#lastContextValueTimestamp) return success(); this.#lastContextValueTimestamp = timestamp; this.#contextValue = value; await Promise.all( Array.from(this.#contextListeners.values()).map(async (listener) => { const newSelectedValue = listener.selector(this.#contextValue); const oldSelectedValue = listener.selector(oldContextValue); const [errEqual, equal] = canon.equal(newSelectedValue, oldSelectedValue); if (errEqual) return failure(errEqual); if (equal) return success(); return await attempt( async () => await listener.callback(deepClone(this.#contextValue), deepClone(oldContextValue)) ); }) ); return success(); } }; // transport/client/browser.ts var clientTransportProviders = { livekit: LiveKitBrowserClient }; var TransportBrowserClient = class extends TransportClientBase { static { __name(this, "TransportBrowserClient"); } constructor({ config, obfuscateErrors = false, telemetry = null }) { const ProviderClass = clientTransportProviders[config.provider]; super({ provider: new ProviderClass(config), obfuscateErrors, telemetry }); } }; // agent/client/atoms/info.ts import { atom, onMount } from "nanostores"; import z2 from "zod"; // agent/client/atoms/define.ts function defineAgentAtom(atomDef) { return atomDef; } __name(defineAgentAtom, "defineAgentAtom"); // agent/client/atoms/info.ts var atomConfigSchema = z2.object({ pollingMs: z2.number().min(1e3).max(3e4).prefault(5e3) }); var agentInfoAtomDef = defineAgentAtom(({ agent }) => ({ name: "info", create: /* @__PURE__ */ __name((config) => { const { error: errConfig, data: parsedConfig } = atomConfigSchema.safeParse(config ?? {}); if (errConfig) throw lifeError({ code: "Validation", message: "Invalid config provided to atom." }); const store = atom(null); const refresh = /* @__PURE__ */ __name(async () => { try { const [error, data] = await agent.info(); if (error) throw error; store.set(data); } catch (error) { throw lifeError({ code: "Unknown", cause: error }); } }, "refresh"); onMount(store, () => { refresh(); const intervalId = setInterval(() => refresh(), parsedConfig.pollingMs); return () => clearInterval(intervalId); }); return { store, refresh }; }, "create") })); // agent/client/atoms/index.ts var createAgentClientAtoms = /* @__PURE__ */ __name((params) => ({ info: agentInfoAtomDef(params) }), "createAgentClientAtoms"); // agent/client/class.ts var AgentClient = class { static { __name(this, "AgentClient"); } def; id; name; atoms; config; transport; #life; #telemetry; #plugins = {}; #sessionToken; #transportRoom; #scope; isStarted = false; constructor(params) { this.def = params.definition; this.id = params.id; this.name = params.definition.name; this.config = params.config; this.#life = params.life; this.#telemetry = createTelemetryClient("agent.client", { agentId: this.id, agentName: this.name, agentConfig: this.config, transportProviderName: this.config.transport.provider }); this.transport = new TransportBrowserClient({ config: this.config.transport, telemetry: this.#telemetry }); this.atoms = createAgentClientAtoms({ agent: this, telemetry: this.#telemetry }); const [errInitialize] = this.#initializePlugins(this.def.plugins); if (errInitialize) throw errInitialize; } #initializePlugins(plugins) { return this.#telemetry.trace("#initializePlugins()", () => { try { const pluginNames = plugins.map((plugin) => plugin.name); const duplicates = pluginNames.filter((name2, index) => pluginNames.indexOf(name2) !== index); if (duplicates.length > 0) { const uniqueDuplicates = [...new Set(duplicates)]; return failure({ code: "Validation", message: `Two or more plugins are named '${uniqueDuplicates.join("', '")}'. Plugin names must be unique. (agent: '${this.name}')` }); } for (const plugin of plugins) { for (const dependency of plugin.dependencies) { const depPlugin = this.def.plugins.find((p) => p.name === dependency.name); if (!depPlugin) { return failure({ code: "Validation", message: `Plugin '${plugin.name}' depends on plugin '${dependency.name}', but '${dependency.name}' is not registered. (agent: '${this.name}')` }); } } } const initResults = plugins.map((plugin) => { const [errPlugin, pluginInstance] = attempt( () => new PluginClient( plugin, this.def.pluginConfigs[plugin.name] ?? {}, this ) ); if (errPlugin) { return failure({ code: "Unknown", message: `Failed to initialize plugin '${name}'.`, cause: errPlugin }); } this.#plugins[plugin.name] = pluginInstance; const [errAccessor, accessor] = pluginInstance.getAccessor(); if (errAccessor) return failure(errAccessor); this[toMethodName(plugin.name)] = accessor; return success(); }); for (const result of initResults) { const [error] = result; if (error) this.#telemetry.log.error({ error }); } return success(); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while initializing plugins.", cause: error }); } }); } /** * Start the agent and join the transport room * @returns Server response on successful start * @throws Error if the agent fails to start */ async start(scope) { return await this.#telemetry.trace("start()", async (span) => { const [error] = await this.#start(scope); if (error) { span.log.error({ error }); return failure(error); } return success(); }); } // Private method, doesn't log to telemetry async #start(scope) { return await this.#telemetry.trace("#start()", async () => { try { const [errStart, data] = await this.#life.api.call("agent.start", { id: this.id, scope }); if (errStart) return failure(errStart); this.#sessionToken = data.sessionToken; this.#transportRoom = data.transportRoom; this.#scope = scope; const [errJoin] = await this.transport.joinRoom( this.#transportRoom.name, this.#transportRoom.token ); if (errJoin) return failure(errJoin); const results = await Promise.all(Object.values(this.#plugins).map((p) => p.start())); const errors = results.map((r) => r[0]).filter(Boolean); for (const error of errors) this.#telemetry.log.error({ message: "Failed to start plugin.", error }); this.isStarted = true; return success(); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while starting agent.", cause: error }); } }); } /** * Stop the agent and leave the transport room * @returns Server response on successful stop * @throws Error if the agent fails to stop */ async stop() { return await this.#telemetry.trace("stop()", async (span) => { const [error] = await this.#stop(); if (error) { span.log.error({ error }); return failure(error); } return success(); }); } // Private method, doesn't log to telemetry async #stop() { return await this.#telemetry.trace("#stop()", async () => { try { if (!this.#sessionToken) { return failure({ code: "Conflict", message: "Agent is not started." }); } const [apiResult, roomResult] = await Promise.all([ this.#life.api.call("agent.stop", { id: this.id, sessionToken: this.#sessionToken }), this.transport.leaveRoom() ]); if (apiResult[0]) return failure(apiResult[0]); if (roomResult[0]) return failure(roomResult[0]); this.isStarted = false; return success(); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while stopping agent.", cause: error }); } }); } /** * Restart the agent by stopping and starting it */ async restart() { return await this.#telemetry.trace("restart()", async (span) => { const [error] = await this.#restart(); if (error) { span.log.error({ error }); return failure(error); } return success(); }); } // Private method, doesn't log to telemetry async #restart() { return await this.#telemetry.trace("#restart()", async () => { try { const [errStop] = await this.#stop(); if (errStop) return failure(errStop); if (!this.#scope) { return failure({ code: "Conflict", message: "Agent is not started." }); } const [errStart] = await this.#start(this.#scope); if (errStart) return failure(errStart); return success(); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while restarting agent.", cause: error }); } }); } async enableVoiceIn() { const [errEnableMicrophone] = await this.transport.enableMicrophone(); if (errEnableMicrophone) return failure(errEnableMicrophone); return success(); } async enableVoiceOut() { const [errPlayAudio] = await this.transport.playAudio(); if (errPlayAudio) return failure(errPlayAudio); return success(); } async disableVoiceIn() { return await success(); } async disableVoiceOut() { return await success(); } /** * Get agent information from the server * @returns Agent information including status and metrics * @throws Error if unable to retrieve agent info */ async info() { return await this.#telemetry.trace("info()", async (span) => { const [error, data] = await this.#info(); if (error) { span.log.error({ error }); return failure(error); } return success(data); }); } // Private method, doesn't log to telemetry async #info() { return await this.#telemetry.trace("#info()", async () => { try { if (!this.#sessionToken) { return failure({ code: "Conflict", message: "Agent is not started." }); } const [err, data] = await this.#life.api.call("agent.info", { id: this.id, sessionToken: this.#sessionToken }); if (err) return failure(err); return success(data); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while getting agent info.", cause: error }); } }); } }; // telemetry/helpers/formatting/browser.ts import { FlattenMap, originalPositionFor, TraceMap } from "@jridgewell/trace-mapping"; import ErrorStackParser from "error-stack-parser"; import z3 from "zod"; var sourceMapCache = /* @__PURE__ */ new Map(); async function getSourceMap(file) { if (sourceMapCache.has(file)) return sourceMapCache.get(file) ?? null; try { const mapUrl = `${file}.map`; const r = await fetch(mapUrl, { credentials: "same-origin" }); if (!r.ok) return sourceMapCache.set(file, null).get(file) ?? null; const json = await r.json(); let map; try { map = new TraceMap(json); } catch (error) { if (error instanceof Error && error.message.includes("sectioned source map")) { map = new FlattenMap(json); } else { throw error; } } return sourceMapCache.set(file, map).get(file) ?? null; } catch (error) { console.error("Failed to get source map for", file, error); return sourceMapCache.set(file, null).get(file) ?? null; } } __name(getSourceMap, "getSourceMap"); async function prettifyErrorStack(err_) { const err = deepClone(err_); const frames = safeParse(err); const lines = []; for (const f of frames) { const file = f.fileName?.startsWith("async ") ? f.fileName.slice(6) : f.fileName; const line = f.lineNumber; const col = f.columnNumber; let ref = "<unknown>"; if (file && line != null && col != null) { const map = await getSourceMap(file); if (map) { const pos = originalPositionFor(map, { line, column: col, bias: 1 }); if (pos.source && pos.line != null && pos.column != null) { ref = `${pos.source}:${pos.line}:${pos.column}`; } } if (!ref) ref = `${file}:${line}:${col}`; } lines.push(f.functionName ? ` at ${f.functionName} (${ref})` : ` at ${ref}`); } err.stack = lines.join("\n"); return err; } __name(prettifyErrorStack, "prettifyErrorStack"); function safeParse(err) { try { return ErrorStackParser.parse(err); } catch { return [ { fileName: void 0, lineNumber: void 0, columnNumber: void 0, functionName: err.name || "Error" } ]; } } __name(safeParse, "safeParse"); async function formatErrorForBrowser(error) { let code = ""; let message = ""; let stack = ""; const after = []; let processed = false; if (isLifeError(error)) { code = `LifeError (${error.code})`; message = error.message; stack = (await prettifyErrorStack(error)).stack ?? ""; if (error.cause) { const formatted = await formatErrorForBrowser(error.cause); after.push(formatted.content, ...formatted.after); const typedCause = error.cause; if (error.code === "Unknown" && typedCause?.stack) stack = ""; } processed = true; } else if (error instanceof z3.ZodError) { code = "ZodError"; message = z3.prettifyError(error); stack = (await prettifyErrorStack(error)).stack ?? ""; processed = true; } if (!processed && error instanceof Error) { if ("name" in error && typeof error.name === "string") code = error.name; else if ("code" in error && typeof error.code === "string") code = error.code; if ("message" in error && typeof error.message === "string") message = error.message; else if ("reason" in error && typeof error.reason === "string") message = error.reason; if ("stack" in error && typeof error.stack === "string") { stack = (await prettifyErrorStack(error)).stack ?? ""; } stack = stack?.split("\n")?.filter((line) => !line.includes(error.message.trim()))?.join("\n") ?? ""; if (!code) code = "Unknown Error"; if (!message) message = "An unknown error occurred."; if (!stack) stack = ""; } if (error instanceof Error && error.cause) { const formatted = await formatErrorForBrowser(error.cause); after.push(formatted.content, ...formatted.after); } return { content: `${code}${code ? ": " : ""}${message}${message ? " " : ""}${stack ? ` ${stack}` : ""}`, after }; } __name(formatErrorForBrowser, "formatErrorForBrowser"); async function formatLogForBrowser(log) { let prefix; if (log.level === "fatal") prefix = "\u2718"; else if (log.level === "error") prefix = "\u2718"; else if (log.level === "warn") prefix = "\u25B2"; else if (log.level === "info") prefix = "\u29BF"; else prefix = "\u2234"; const scopeDefinition = telemetryBrowserScopesDefinition?.[log.scope]; const scopeDisplayName = scopeDefinition?.displayName instanceof Function ? ( // biome-ignore lint/suspicious/noExplicitAny: fine here scopeDefinition.displayName(log.attributes) ) : scopeDefinition?.displayName; const scope = `[${scopeDisplayName ?? "Unknown"}]`; const message = log.message || ""; const header = `${prefix} ${scope}${message ? ` ${message}` : ""}`; const formatted = await formatErrorForBrowser(log.error); const errors = [formatted.content, ...formatted.after]; return [header, ...errors]; } __name(formatLogForBrowser, "formatLogForBrowser"); // client/api.ts var WS_PROTOCOL_REGEX = /^http/; var TRAILING_SLASH_REGEX = /\/$/; var SUBSCRIPTION_ID_LENGTH = 16; var LifeServerApiClient = class { static { __name(this, "LifeServerApiClient"); } #telemetry; #serverUrl; #serverToken; #ws; #subscriptions = /* @__PURE__ */ new Map(); #wsReconnectTimeout; constructor(params) { this.#telemetry = params.telemetry; this.#serverUrl = params.serverUrl.replace(TRAILING_SLASH_REGEX, ""); this.#serverToken = params.serverToken; } ensureWebSocket() { if (this.#ws?.readyState === WebSocket.OPEN) return Promise.resolve(this.#ws); return new Promise((resolve, reject) => { const wsUrl = `${this.#serverUrl.replace(WS_PROTOCOL_REGEX, "ws")}/api/ws`; this.#ws = new WebSocket(wsUrl); this.#ws.onopen = () => { if (this.#ws) resolve(this.#ws); }; this.#ws.onerror = () => { reject(lifeError({ code: "Upstream", message: "WebSocket connection failed" })); }; this.#ws.onmessage = (event) => { try { const [err, message] = canon.parse(event.data); if (err) return; if (!message || typeof message !== "object" || message === null) return; if (!("subscriptionId" in message) || typeof message.subscriptionId !== "string") return; const callback = this.#subscriptions.get(message.subscriptionId); callback?.(message.data); } catch { } }; this.#ws.onclose = () => { if (this.#subscriptions.size > 0) { this.#wsReconnectTimeout = setTimeout(() => { this.ensureWebSocket().catch(() => { }); }, 5e3); } }; }); } async call(handlerId, input) { return await this.#telemetry.trace("api.call()", async () => { const url = `${this.#serverUrl}/api/http`; const headers = { "Content-Type": "application/json" }; if (this.#serverToken) headers.Authorization = `Bearer ${this.#serverToken}`; try { const [errCanon, body] = canon.stringify({ handlerId, serverToken: this.#serverToken, data: input }); if (errCanon) return failure(errCanon); const response = await fetch(url, { method: "POST", headers, body }); if (!response.ok) { try { const result = canon.parse(await response.text()); return failure( result?.[0] ?? { code: "Upstream", message: `API call failed: ${response.statusText}` } ); } catch { return failure({ code: "Upstream", message: `API call failed: ${response.statusText}` }); } } const text = await response.text(); const [err, data] = canon.parse(text); if (err) return failure(err); return success(data); } catch (error) { return failure({ code: "Unknown", cause: error }); } }); } cast(handlerId, input) { return this.#telemetry.trace("api.cast()", async () => { const ws = await this.ensureWebSocket(); const [errCanon, body] = canon.stringify({ type: "cast", handlerId, serverToken: this.#serverToken, data: input }); if (errCanon) return failure(errCanon); return await attempt(async () => ws.send(body)); }); } subscribe(handlerId, callback, input) { return this.#telemetry.trace("api.subscribe()", () => { try { const subscriptionId = `sub_${Date.now()}_${Math.random().toString(36).substring(2, 2 + SUBSCRIPTION_ID_LENGTH)}`; this.#subscriptions.set(subscriptionId, callback); this.ensureWebSocket().then((ws) => { const [errCanon, body] = canon.stringify({ type: "stream", action: "subscribe", handlerId, subscriptionId, serverToken: this.#serverToken, data: input }); if (errCanon) return failure(errCanon); ws.send(body); }).catch(() => { this.#subscriptions.delete(subscriptionId); }); const unsubscribe = /* @__PURE__ */ __name(() => { this.#subscriptions.delete(subscriptionId); if (this.#ws?.readyState === WebSocket.OPEN) { const [errCanon, body] = canon.stringify({ type: "stream", action: "unsubscribe", handlerId, subscriptionId, serverToken: this.#serverToken }); if (errCanon) return failure(errCanon); this.#ws.send(body); } }, "unsubscribe"); return success(unsubscribe); } catch (error) { return failure({ code: "Unknown", cause: error }); } }); } disconnect() { if (this.#wsReconnectTimeout) { clearTimeout(this.#wsReconnectTimeout); this.#wsReconnectTimeout = void 0; } this.#subscriptions.clear(); if (this.#ws) { this.#ws.close(); this.#ws = void 0; } } }; // client/client.ts TelemetryBrowserClient.registerGlobalConsumer({ async start(queue) { for await (const item of queue) { if (item.type !== "log") continue; const logLevel = globalThis?.process?.env?.LOG_LEVEL ?? "info"; const priority = logLevelPriority(item.level); if (priority < logLevelPriority(logLevel)) continue; try { const contents = (await formatLogForBrowser(item)).filter(Boolean); let consoleFn; if (priority >= logLevelPriority("error")) consoleFn = console.error; else if (priority >= logLevelPriority("warn")) consoleFn = console.warn; else consoleFn = console.log; for (let i = 0; i < contents.length; i++) consoleFn( `Life.js (${item.id.slice(0, 6)}, ${i + 1}/${contents.length}) ${contents[i]}` ); } catch { console.log(item.message); } } } }); var LifeClient = class { static { __name(this, "LifeClient"); } options; #agents = /* @__PURE__ */ new Map(); #telemetry; api; constructor(options) { this.options = options; this.#telemetry = createTelemetryClient("client", {}); this.api = new LifeServerApiClient({ telemetry: this.#telemetry, serverUrl: this.options.serverUrl, serverToken: this.options.serverToken }); } /** * Create a new agent instance on the server * @param name - Agent name/type to create * @param scope - Agent scope configuration * @returns AgentClient instance if creation successful */ async createAgent(name2, options = {}) { return await this.#telemetry.trace("createAgent()", async (span) => { const [error, agent] = await this.#createAgent(name2, options); if (error) { span.log.error({ error }); return failure(error); } return success(agent); }); } // Private method, doesn't log to telemetry async #createAgent(name2, options = {}) { return await this.#telemetry.trace("#createAgent()", async () => { try { const [errIndex, buildIndex] = await importClientBuild(); if (errIndex) return failure(errIndex); const build = buildIndex[name2]; if (!build) { return failure({ code: "NotFound", message: `Agent '${String(name2)}' not found in client build.` }); } const [err, data] = await this.api.call("agent.create", { name: name2, id: options.id }); if (err) return failure(err); const agentClient = new AgentClient({ id: data.id, definition: build.definition, life: this, config: data.clientConfig ?? {} }); this.#agents.set(data.id, agentClient); return success(toPublic(agentClient)); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while creating agent.", cause: error }); } }); } /** * Get an existing agent client instance * @param id - Agent ID * @returns AgentClient instance or undefined */ getAgent(name2, options = {}) { return this.#telemetry.trace("getAgent()", (span) => { const [error, agent] = this.#getAgent(name2, options); if (error) { span.log.error({ error }); return failure(error); } return success(agent); }); } // Private method, doesn't log to telemetry #getAgent(name2, options = {}) { return this.#telemetry.trace("#getAgent()", () => { try { if (options.id) { const agent = this.#agents.get(options.id); if (!agent) return success(void 0); return success(toPublic(agent)); } const agents = Array.from(this.#agents.values()).filter((a) => a.def.name === String(name2)); if (!agents.length) return success(void 0); if (agents.length > 1) return failure({ code: "Conflict", message: `Multiple agents found for name '${String(name2)}'. Use an ID to get a specific agent.` }); return success(toPublic(agents[0])); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while getting agent.", cause: error }); } }); } /** * Get or create an agent instance * @param name - Agent name/type * @param scope - Agent scope configuration * @returns AgentClient instance */ async getOrCreateAgent(name2, options = {}) { return await this.#telemetry.trace("getOrCreateAgent()", async (span) => { const [error, agent] = await this.#getOrCreateAgent(name2, options); if (error) { span.log.error({ error }); return failure(error); } return success(agent); }); } // Private method, doesn't log to telemetry async #getOrCreateAgent(name2, options = {}) { return await this.#telemetry.trace("#getOrCreateAgent()", async () => { try { const [error, agent] = this.getAgent(name2, options); if (error && error.code !== "Conflict") return failure(error); if (agent) return success(agent); const [err, newAgent] = await this.#createAgent(name2, options); if (err) return failure(err); return success(newAgent); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while getting or creating agent.", cause: error }); } }); } /** * List all created agent instances * @returns Array of { name: string, id: string } */ listAgents() { return this.#telemetry.trace("listAgents()", (span) => { const [error, agents] = this.#listAgents(); if (error) { span.log.error({ error }); return failure(error); } return success(agents); }); } // Private method, doesn't log to telemetry #listAgents() { return this.#telemetry.trace("#listAgents()", () => { try { return success( Array.from(this.#agents.values()).map((a) => ({ name: a.name, id: a.id })) ); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while listing agents.", cause: error }); } }); } /** * Get server information * @returns Server info response */ async info() { return await this.#telemetry.trace("info()", async (span) => { const [error, data] = await this.#info(); if (error) { span.log.error({ error }); return failure(error); } return success(data); }); } // Private method, doesn't log to telemetry async #info() { return await this.#telemetry.trace("info()", async () => { try { const [err, data] = await this.api.call("server.info"); if (err) return failure(err); return success(data); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while getting server info.", cause: error }); } }); } /** * Check if the server is responsive * @returns True if server responds with "pong" */ async ping() { return await this.#telemetry.trace("ping()", async (span) => { const [error, data] = await this.#ping(); if (error) { span.log.error({ error }); return failure(error); } return success(data); }); } // Private method, doesn't log to telemetry async #ping() { return await this.#telemetry.trace("#ping()", async () => { try { const [err, data] = await this.api.call("server.ping"); if (err) return failure(err); if (data !== "pong") return failure({ code: "Validation", message: `Ping failed. Received wrong response: '${data}'.` }); return success("pong"); } catch (error) { return failure({ code: "Unknown", message: "Unknown error while pinging server.", cause: error }); } }); } }; // client/options.ts import z4 from "zod"; var lifeClientOptionsSchema = z4.object({ serverUrl: z4.string().prefault("http://localhost:3003"), serverToken: z4.string().optional() }); // client/create.ts var getClientCache = /* @__PURE__ */ __name(() => { if (!globalThis.__LIFE_CLIENT_CACHE__) globalThis.__LIFE_CLIENT_CACHE__ = /* @__PURE__ */ new Map(); return globalThis.__LIFE_CLIENT_CACHE__; }, "getClientCache"); var getCacheKey = /* @__PURE__ */ __name((options) => `${options.serverUrl}::${options.serverToken ?? ""}`, "getCacheKey"); var createLifeClient = /* @__PURE__ */ __name((options = {}) => { const { error: errOptions, data: parsedOptions } = lifeClientOptionsSchema.safeParse(options); if (errOptions) { throw lifeError({ code: "Validation", message: "Invalid options provided to LifeClient.", cause: errOptions }); } if (typeof window === "undefined") return { options: parsedOptions }; const cache = getClientCache(); const key = getCacheKey(parsedOptions); const client = cache.get(key); if (client) return client; const newClient = toPublic(new LifeClient(parsedOptions)); cache.set(key, newClient); return newClient; }, "createLifeClient"); hmr.accept?.(); // agent/client/types.ts var parseAgentClientParam = /* @__PURE__ */ __name((agent) => agent, "parseAgentClientParam"); export { createLifeClient, parseAgentClientParam }; //# sourceMappingURL=chunk-T6NMMENO.mjs.map