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 lines • 87 kB
Source Map (JSON)
{"version":3,"sources":["../shared/hmr.ts","../plugins/client/class.ts","../transport/client/browser.ts","../agent/client/atoms/info.ts","../agent/client/atoms/define.ts","../agent/client/atoms/index.ts","../agent/client/class.ts","../telemetry/helpers/formatting/browser.ts","../client/api.ts","../client/client.ts","../client/options.ts","../client/create.ts","../agent/client/types.ts"],"sourcesContent":["// biome-ignore-all lint/suspicious/noExplicitAny: runtime only\n\ntype RawHMR = {\n accept?: (...args: unknown[]) => void;\n dispose?: (cb: () => void) => void;\n addDisposeHandler?: (cb: () => void) => void;\n};\n\nconst rawHMR: RawHMR | null =\n // - Vite / Bun / modern dev servers\n (typeof import.meta !== \"undefined\" && (import.meta as any).hot) ||\n // - Webpack ESM\n (typeof import.meta !== \"undefined\" && (import.meta as any).webpackHot) ||\n // - Webpack CJS\n (globalThis as any)?.module?.hot ||\n // - None\n null;\n\ntype HMR = RawHMR & { active: boolean };\n\nexport const hmr: HMR = Object.freeze(\n rawHMR\n ? {\n active: true,\n accept: rawHMR.accept ? (...args: unknown[]) => rawHMR.accept?.(...args) : undefined,\n dispose:\n rawHMR.dispose || rawHMR.addDisposeHandler\n ? (cb: () => void) => (rawHMR.dispose || rawHMR.addDisposeHandler)?.(cb)\n : undefined,\n }\n : { active: false },\n);\n","import z from \"zod\";\nimport type { AgentClient } from \"@/agent/client/class\";\nimport type { AgentClientDefinition } from \"@/agent/client/types\";\nimport { canon, type SerializableValue } from \"@/shared/canon\";\nimport { deepClone } from \"@/shared/deep-clone\";\nimport { lifeError } from \"@/shared/error\";\nimport { toMethodName } from \"@/shared/method-name\";\nimport * as op from \"@/shared/operation\";\nimport { newId } from \"@/shared/prefixed-id\";\nimport type { Any } from \"@/shared/types\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { createTelemetryClient } from \"@/telemetry/clients/browser\";\nimport { type PluginAccessor, type PluginContext, pluginEventInputSchema } from \"../server/types\";\nimport type {\n PluginClientAccessor,\n PluginClientAtoms,\n PluginClientConfig,\n PluginClientContextListener,\n PluginClientDefinition,\n PluginClientDependenciesAccessor,\n PluginClientEventsListener,\n PluginClientExtension,\n} from \"./types\";\n\nexport class PluginClient<const PluginClientDef extends PluginClientDefinition> {\n readonly def: PluginClientDef;\n readonly #config: PluginClientConfig<PluginClientDef[\"config\"], \"output\">;\n readonly #agent: AgentClient<AgentClientDefinition>;\n readonly #telemetry: TelemetryClient;\n readonly #atoms: PluginClientAtoms<PluginClientDef[\"atoms\"]>;\n readonly #extension: PluginClientExtension<PluginClientDef[\"class\"]>;\n readonly #eventsListeners = new Map<string, PluginClientEventsListener>();\n readonly #contextListeners = new Map<string, PluginClientContextListener>();\n #contextValue = {} as PluginContext<PluginClientDef[\"$serverDef\"][\"context\"], \"output\">;\n #lastContextValueTimestamp = 0;\n\n constructor(\n definition: PluginClientDef,\n config: PluginClientConfig<PluginClientDef[\"config\"], \"input\">,\n agent: AgentClient<AgentClientDefinition>,\n ) {\n this.def = definition;\n this.#agent = agent;\n\n // Validate config\n const { error: errConfig, data: parsedConfig } = this.def.config.schema.safeParse(config);\n if (errConfig) {\n throw lifeError({\n code: \"Validation\",\n message: `Invalid config provided to plugin client '${this.def.name}'.`,\n cause: errConfig,\n });\n }\n this.#config = parsedConfig as PluginClientConfig<PluginClientDef[\"config\"], \"output\">;\n\n // Initialize telemetry\n this.#telemetry = createTelemetryClient(\"plugin.client\", {\n agentId: agent.id,\n agentName: agent.def.name,\n agentConfig: agent.config,\n transportProviderName: agent.config.transport.provider,\n pluginName: definition.name,\n pluginClientConfig: this.#config,\n });\n\n // Instantiate extension\n const [errAccessor, accessor] = this.getAccessor();\n if (errAccessor) throw errAccessor;\n this.#extension = new (definition.class({\n plugin: op.toPublic(accessor),\n agent: op.toPublic(this.#agent),\n dependencies: this.getDependenciesAccessor(),\n telemetry: this.#telemetry,\n }))();\n\n // Compute atoms\n const atomsArr = definition.atoms({\n plugin: op.toPublic(accessor),\n agent: op.toPublic(this.#agent),\n dependencies: this.getDependenciesAccessor(),\n telemetry: this.#telemetry,\n });\n const atomsMap = Object.fromEntries(atomsArr.map((atom) => [atom.name, atom.create]));\n this.#atoms = atomsMap as PluginClientAtoms<PluginClientDef[\"atoms\"]>;\n }\n\n getAccessor() {\n // client.config\n const [errClone, cloneConfig] = op.attempt(() => deepClone(this.#config));\n if (errClone) return op.failure({ code: \"Unknown\", cause: errClone });\n\n // server.context.get()\n const serverContextGet = (() =>\n op.attempt(() => deepClone(this.#contextValue))) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"context\"][\"get\"];\n\n // server.context.onChange()\n const serverContextOnChange = ((selector, callback) => {\n const id = newId(\"listener\");\n this.#contextListeners.set(id, { id, callback, selector });\n return op.success(() => this.#contextListeners.delete(id));\n }) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"context\"][\"onChange\"];\n\n // server.events.emit()\n const serverEventsEmit = (async (event) => {\n const [err, data] = await this.#agent.transport.call({\n name: `plugin.${this.def.name}.events.emit`,\n schema: {\n input: pluginEventInputSchema,\n output: z.object({ id: z.string() }),\n },\n input: event as unknown as z.infer<typeof pluginEventInputSchema>,\n });\n if (err) return op.failure(err);\n return op.success(data.id);\n }) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"events\"][\"emit\"];\n\n // server.events.on()\n const serverEventsOn = ((selector, callback, includeDropped = false) => {\n // Append listener, and ask plugin's server to stream selected event\n const id = newId(\"listener\");\n this.#eventsListeners.set(id, { id, selector, callback, includeDropped });\n this.#agent.transport.call({\n name: `plugin.${this.def.name}.events.subscribe`,\n schema: {\n input: z.object({\n listenerId: z.string(),\n selector: z.any(),\n includeDropped: z.boolean().prefault(false),\n }),\n },\n input: { listenerId: id, selector, includeDropped },\n });\n\n // Return unsubscribe function\n return op.success(() => {\n this.#agent.transport.call({\n name: `plugin.${this.def.name}.events.unsubscribe`,\n schema: { input: z.object({ listenerId: z.string() }) },\n input: { listenerId: id },\n });\n this.#eventsListeners.delete(id);\n });\n }) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"events\"][\"on\"];\n\n // server.events.once()\n const serverEventsOnce = ((selector, callback, includeDropped = false) => {\n const [errOn, unsubscribe] = serverEventsOn(\n selector,\n async (event) => {\n unsubscribe?.();\n await callback(event);\n },\n includeDropped,\n );\n if (errOn) return op.failure(errOn);\n return op.success(unsubscribe);\n }) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"events\"][\"once\"];\n\n // server.events.waitForProcessing()\n const serverEventsWaitForProcessing = (async (eventId) =>\n await this.#agent.transport.call({\n name: `plugin.${this.def.name}.events.waitForProcessing`,\n schema: { input: z.object({ eventId: z.string() }) },\n input: { eventId },\n })) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"events\"][\"waitForProcessing\"];\n\n // server.events.waitForResult()\n const serverEventsWaitForResult = (async (eventId, handlerName) =>\n // @ts-expect-error\n await this.#agent.transport.call({\n name: `plugin.${this.def.name}.events.waitForResult`,\n schema: {\n input: z.object({ eventId: z.string(), handlerName: z.string() }),\n output: z.object().loose(),\n },\n input: { eventId, handlerName },\n })) satisfies PluginAccessor<\n PluginClientDef[\"$serverDef\"],\n { type: \"client\"; name: string }\n >[\"events\"][\"waitForResult\"];\n\n return op.success(\n Object.assign(this.#extension ?? ({} as InstanceType<ReturnType<PluginClientDef[\"class\"]>>), {\n config: cloneConfig,\n atoms: this.#atoms,\n server: {\n context: {\n onChange: serverContextOnChange,\n get: serverContextGet,\n },\n events: {\n emit: serverEventsEmit,\n on: serverEventsOn,\n once: serverEventsOnce,\n waitForProcessing: serverEventsWaitForProcessing,\n waitForResult: serverEventsWaitForResult as Any,\n },\n },\n }) satisfies PluginClientAccessor<PluginClientDef, Any>,\n );\n }\n\n getDependenciesAccessor() {\n const dependenciesAccessors = {} as PluginClientDependenciesAccessor<\n PluginClientDef[\"dependencies\"]\n >;\n for (const dependencyDef of this.def.dependencies ?? []) {\n // @ts-expect-error\n const accessor = this.#agent[toMethodName(dependencyDef.name)];\n if (!accessor)\n return op.failure({\n code: \"NotFound\",\n message: `Failed to obtain client instance for dependency plugin '${dependencyDef.name}'. Shouldn't happen.`,\n });\n // @ts-expect-error\n dependenciesAccessors[dependencyDef.name] = accessor;\n }\n return op.success(dependenciesAccessors);\n }\n\n async start() {\n // Figure whether the plugin client has a server\n const [errPing, dataPing] = await this.#agent.transport.call({\n name: \"agent.has-plugin-server\",\n schema: {\n input: z.object({ pluginName: z.string() }),\n output: z.object({\n hasServer: z.boolean(),\n }),\n },\n input: { pluginName: this.def.name },\n });\n if (errPing) return op.failure(errPing);\n\n // Return early if the plugin client has no server\n if (!dataPing.hasServer) return op.success();\n\n // Listen to context value changes\n this.#agent.transport.register({\n name: `plugin.${this.def.name}.context.changed`,\n schema: { input: z.object({ value: z.any(), timestamp: z.number() }) },\n execute: async ({ value, timestamp }) => await this.#setContextValue(value, timestamp),\n });\n\n // Consume events listeners callbacks\n this.#agent.transport.register({\n name: `plugin.${this.def.name}.events.callback`,\n schema: {\n input: z.object({ listenerId: z.string(), event: pluginEventInputSchema }),\n },\n execute: async ({ listenerId, event }) => {\n await this.#eventsListeners.get(listenerId)?.callback(event as never);\n return op.success();\n },\n });\n\n // Fetch initial context value\n const [err, data] = await this.#agent.transport.call({\n name: `plugin.${this.def.name}.context.get`,\n schema: { output: z.object({ value: z.any(), timestamp: z.number() }) },\n });\n if (err) return op.failure(err);\n return await this.#setContextValue(data.value, data.timestamp);\n }\n\n async #setContextValue(\n value: PluginContext<PluginClientDef[\"$serverDef\"][\"context\"], \"output\">,\n timestamp: number,\n ) {\n // Clone the old context value\n const oldContextValue = deepClone(this.#contextValue);\n\n // Ensure the new value is newer than the last one\n if (timestamp < this.#lastContextValueTimestamp) return op.success();\n\n // Update the last context value timestamp and value\n this.#lastContextValueTimestamp = timestamp;\n this.#contextValue = value;\n\n // Notify listeners if the value they select has changed\n await Promise.all(\n Array.from(this.#contextListeners.values()).map(async (listener) => {\n const newSelectedValue = listener.selector(this.#contextValue) as SerializableValue;\n const oldSelectedValue = listener.selector(oldContextValue) as SerializableValue;\n\n // Check if the value actually changed\n const [errEqual, equal] = canon.equal(newSelectedValue, oldSelectedValue);\n if (errEqual) return op.failure(errEqual);\n if (equal) return op.success();\n\n // Call the listener if changed\n return await op.attempt(\n async () =>\n await listener.callback(deepClone(this.#contextValue), deepClone(oldContextValue)),\n );\n }),\n );\n return op.success();\n }\n}\n","import type { z } from \"zod\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { TransportClientBase } from \"../base\";\nimport type { transportBrowserConfig } from \"../config/browser\";\nimport { LiveKitBrowserClient } from \"../providers/livekit/browser\";\n\n// Providers\nexport const clientTransportProviders = {\n livekit: LiveKitBrowserClient,\n} as const;\n\n// Transport\nexport class TransportBrowserClient extends TransportClientBase {\n constructor({\n config,\n obfuscateErrors = false,\n telemetry = null,\n }: {\n config: z.output<typeof transportBrowserConfig.schema>;\n obfuscateErrors?: boolean;\n telemetry?: TelemetryClient | null;\n }) {\n const ProviderClass = clientTransportProviders[config.provider];\n super({ provider: new ProviderClass(config), obfuscateErrors, telemetry });\n }\n}\n","import { atom, onMount } from \"nanostores\";\nimport z from \"zod\";\nimport type { definition } from \"@/server/api/definition\";\nimport { lifeError } from \"@/shared/error\";\nimport { defineAgentAtom } from \"./define\";\n\ntype AgentInfoResponse = z.infer<(typeof definition)[\"agent.info\"][\"outputDataSchema\"]>;\n\nconst atomConfigSchema = z.object({\n pollingMs: z.number().min(1000).max(30_000).prefault(5000),\n});\n\nexport const agentInfoAtomDef = defineAgentAtom(({ agent }) => ({\n name: \"info\",\n create: (config: z.input<typeof atomConfigSchema>) => {\n // Validate config\n const { error: errConfig, data: parsedConfig } = atomConfigSchema.safeParse(config ?? {});\n if (errConfig)\n throw lifeError({ code: \"Validation\", message: \"Invalid config provided to atom.\" });\n // Create the store and the refresh function\n const store = atom<AgentInfoResponse | null>(null);\n const refresh = async () => {\n try {\n const [error, data] = await agent.info();\n if (error) throw error;\n store.set(data);\n } catch (error) {\n throw lifeError({ code: \"Unknown\", cause: error });\n }\n };\n onMount(store, () => {\n // Fetch immediately on mount\n refresh();\n // Set up polling interval\n const intervalId = setInterval(() => refresh(), parsedConfig.pollingMs);\n // Cleanup on unmount\n return () => clearInterval(intervalId);\n });\n return { store, refresh };\n },\n}));\n","import type { ReadableAtom, WritableAtom } from \"nanostores\";\nimport type { AgentClientDefinition } from \"@/agent/client/types\";\nimport type { AgentClient } from \"@/exports/client\";\nimport type { Any } from \"@/shared/types\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\n\nexport type AgentClientAtom = {\n store: WritableAtom | ReadableAtom;\n refresh: () => Promise<void>;\n};\n\nexport type AgentAtomDefinition<Name extends string = string> = (params: {\n agent: AgentClient<AgentClientDefinition>;\n telemetry: TelemetryClient;\n}) => {\n name: Name;\n create: (...params: Any[]) => AgentClientAtom;\n};\n\nexport function defineAgentAtom<\n const Name extends string,\n const AtomDef extends AgentAtomDefinition<Name>,\n>(atomDef: AtomDef) {\n return atomDef;\n}\n","import type { AgentAtomDefinition } from \"./define\";\nimport { agentInfoAtomDef } from \"./info\";\n\nexport const createAgentClientAtoms = (params: Parameters<AgentAtomDefinition>[0]) => ({\n info: agentInfoAtomDef(params),\n});\n\nexport type AgentClientAtoms = ReturnType<typeof createAgentClientAtoms>;\n","import type z from \"zod\";\nimport type { LifeClient } from \"@/client/client\";\nimport { PluginClient } from \"@/plugins/client/class\";\nimport type { PluginClientDefinition } from \"@/plugins/client/types\";\nimport { toMethodName } from \"@/shared/method-name\";\nimport * as op from \"@/shared/operation\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { createTelemetryClient } from \"@/telemetry/clients/browser\";\nimport { TransportBrowserClient } from \"@/transport/client/browser\";\nimport type { agentClientConfig } from \"../client/config\";\nimport type { AgentScope } from \"../server/types\";\nimport { type AgentClientAtoms, createAgentClientAtoms } from \"./atoms\";\nimport type { AgentClientDefinition, AgentClientPluginsDefinition } from \"./types\";\n\nexport class AgentClient<const AgentDef extends AgentClientDefinition> {\n readonly def: AgentDef;\n readonly id: string;\n readonly name: string;\n readonly atoms: AgentClientAtoms;\n readonly config: z.output<typeof agentClientConfig.schema>;\n readonly transport: TransportBrowserClient;\n\n readonly #life: LifeClient;\n readonly #telemetry: TelemetryClient;\n readonly #plugins: Record<string, PluginClient<PluginClientDefinition>> = {};\n\n #sessionToken?: string;\n #transportRoom?: { name: string; token: string };\n #scope?: AgentScope<AgentDef[\"$serverDef\"][\"scope\"]>;\n\n isStarted = false;\n\n constructor(params: {\n id: string;\n definition: AgentDef;\n config: z.output<typeof agentClientConfig.schema>;\n life: LifeClient;\n }) {\n this.def = params.definition;\n this.id = params.id;\n this.name = params.definition.name;\n this.config = params.config; // Already parsed by the server\n this.#life = params.life;\n\n // Initialize telemetry\n this.#telemetry = createTelemetryClient(\"agent.client\", {\n agentId: this.id,\n agentName: this.name,\n agentConfig: this.config,\n transportProviderName: this.config.transport.provider,\n });\n\n // Initialize transport\n this.transport = new TransportBrowserClient({\n config: this.config.transport,\n telemetry: this.#telemetry,\n });\n\n // Initialize atoms\n this.atoms = createAgentClientAtoms({ agent: this, telemetry: this.#telemetry });\n\n // Initialize plugins\n const [errInitialize] = this.#initializePlugins(this.def.plugins);\n if (errInitialize) throw errInitialize;\n }\n\n #initializePlugins(plugins: AgentClientPluginsDefinition) {\n return this.#telemetry.trace(\"#initializePlugins()\", () => {\n try {\n // Validate plugins have unique names\n const pluginNames = plugins.map((plugin) => plugin.name);\n const duplicates = pluginNames.filter((name, index) => pluginNames.indexOf(name) !== index);\n if (duplicates.length > 0) {\n const uniqueDuplicates = [...new Set(duplicates)];\n return op.failure({\n code: \"Validation\",\n message: `Two or more plugins are named '${uniqueDuplicates.join(\"', '\")}'. Plugin names must be unique. (agent: '${this.name}')`,\n });\n }\n\n // Validate plugin dependencies\n for (const plugin of plugins) {\n for (const dependency of plugin.dependencies) {\n // - Ensure the plugin is provided\n const depPlugin = this.def.plugins.find((p) => p.name === dependency.name);\n if (!depPlugin) {\n return op.failure({\n code: \"Validation\",\n message: `Plugin '${plugin.name}' depends on plugin '${dependency.name}', but '${dependency.name}' is not registered. (agent: '${this.name}')`,\n });\n }\n }\n }\n\n // Instantiate plugins\n const initResults = plugins.map((plugin) => {\n // Create plugin instance\n const [errPlugin, pluginInstance] = op.attempt(\n () =>\n new PluginClient(\n plugin,\n (this.def.pluginConfigs[plugin.name] ?? {}) as Record<string, unknown>,\n this,\n ),\n );\n if (errPlugin) {\n return op.failure({\n code: \"Unknown\",\n message: `Failed to initialize plugin '${name}'.`,\n cause: errPlugin,\n });\n }\n this.#plugins[plugin.name] = pluginInstance;\n\n // Assign plugin instance to agent client\n const [errAccessor, accessor] = pluginInstance.getAccessor();\n if (errAccessor) return op.failure(errAccessor);\n this[toMethodName(plugin.name) as keyof typeof this] =\n accessor as this[keyof typeof this];\n\n return op.success();\n });\n\n // Log all failures\n for (const result of initResults) {\n const [error] = result;\n if (error) this.#telemetry.log.error({ error });\n }\n\n return op.success();\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while initializing plugins.\",\n cause: error,\n });\n }\n });\n }\n\n /**\n * Start the agent and join the transport room\n * @returns Server response on successful start\n * @throws Error if the agent fails to start\n */\n async start(scope: AgentScope<AgentDef[\"$serverDef\"][\"scope\"]>) {\n return await this.#telemetry.trace(\"start()\", async (span) => {\n const [error] = await this.#start(scope);\n if (error) {\n span.log.error({ error });\n return op.failure(error);\n }\n return op.success();\n });\n }\n\n // Private method, doesn't log to telemetry\n async #start(scope: AgentScope<AgentDef[\"$serverDef\"][\"scope\"]>) {\n return await this.#telemetry.trace(\"#start()\", async () => {\n try {\n // Send a call to the server to start the agent\n const [errStart, data] = await this.#life.api.call(\"agent.start\", { id: this.id, scope });\n if (errStart) return op.failure(errStart);\n this.#sessionToken = data.sessionToken;\n this.#transportRoom = data.transportRoom;\n this.#scope = scope;\n\n // Join the transport room\n const [errJoin] = await this.transport.joinRoom(\n this.#transportRoom.name,\n this.#transportRoom.token,\n );\n if (errJoin) return op.failure(errJoin);\n\n // Start plugins\n const results = await Promise.all(Object.values(this.#plugins).map((p) => p.start()));\n const errors = results.map((r) => r[0]).filter(Boolean);\n for (const error of errors)\n this.#telemetry.log.error({ message: \"Failed to start plugin.\", error });\n\n this.isStarted = true;\n\n return op.success();\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while starting agent.\",\n cause: error,\n });\n }\n });\n }\n\n /**\n * Stop the agent and leave the transport room\n * @returns Server response on successful stop\n * @throws Error if the agent fails to stop\n */\n async stop() {\n return await this.#telemetry.trace(\"stop()\", async (span) => {\n const [error] = await this.#stop();\n if (error) {\n span.log.error({ error });\n return op.failure(error);\n }\n return op.success();\n });\n }\n\n // Private method, doesn't log to telemetry\n async #stop() {\n return await this.#telemetry.trace(\"#stop()\", async () => {\n try {\n // Ensure sessionToken is set\n if (!this.#sessionToken) {\n return op.failure({ code: \"Conflict\", message: \"Agent is not started.\" });\n }\n\n // Send a call to the server to stop the agent and leave the transport room\n const [apiResult, roomResult] = await Promise.all([\n this.#life.api.call(\"agent.stop\", {\n id: this.id,\n sessionToken: this.#sessionToken,\n }),\n this.transport.leaveRoom(),\n ]);\n\n // Return error if any occurs\n if (apiResult[0]) return op.failure(apiResult[0]);\n if (roomResult[0]) return op.failure(roomResult[0]);\n\n this.isStarted = false;\n\n return op.success();\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while stopping agent.\",\n cause: error,\n });\n }\n });\n }\n\n /**\n * Restart the agent by stopping and starting it\n */\n async restart() {\n return await this.#telemetry.trace(\"restart()\", async (span) => {\n const [error] = await this.#restart();\n if (error) {\n span.log.error({ error });\n return op.failure(error);\n }\n return op.success();\n });\n }\n\n // Private method, doesn't log to telemetry\n async #restart() {\n return await this.#telemetry.trace(\"#restart()\", async () => {\n try {\n const [errStop] = await this.#stop();\n if (errStop) return op.failure(errStop);\n if (!this.#scope) {\n return op.failure({ code: \"Conflict\", message: \"Agent is not started.\" });\n }\n const [errStart] = await this.#start(this.#scope);\n if (errStart) return op.failure(errStart);\n return op.success();\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while restarting agent.\",\n cause: error,\n });\n }\n });\n }\n\n async enableVoiceIn() {\n const [errEnableMicrophone] = await this.transport.enableMicrophone();\n if (errEnableMicrophone) return op.failure(errEnableMicrophone);\n return op.success();\n }\n\n async enableVoiceOut() {\n // Turn on audio output\n // TODO\n\n // Play audio from the transport\n const [errPlayAudio] = await this.transport.playAudio();\n if (errPlayAudio) return op.failure(errPlayAudio);\n return op.success();\n }\n\n async disableVoiceIn() {\n // TODO\n return await op.success();\n }\n\n async disableVoiceOut() {\n // TODO\n return await op.success();\n }\n\n /**\n * Get agent information from the server\n * @returns Agent information including status and metrics\n * @throws Error if unable to retrieve agent info\n */\n async info() {\n return await this.#telemetry.trace(\"info()\", async (span) => {\n const [error, data] = await this.#info();\n if (error) {\n span.log.error({ error });\n return op.failure(error);\n }\n return op.success(data);\n });\n }\n\n // Private method, doesn't log to telemetry\n async #info() {\n return await this.#telemetry.trace(\"#info()\", async () => {\n try {\n // Ensure sessionToken is set\n if (!this.#sessionToken) {\n return op.failure({ code: \"Conflict\", message: \"Agent is not started.\" });\n }\n\n // Send a call to the server to get agent information\n const [err, data] = await this.#life.api.call(\"agent.info\", {\n id: this.id,\n sessionToken: this.#sessionToken,\n });\n\n // Return error if any occurs\n if (err) return op.failure(err);\n\n return op.success(data);\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while getting agent info.\",\n cause: error,\n });\n }\n });\n }\n}\n","import { FlattenMap, originalPositionFor, TraceMap } from \"@jridgewell/trace-mapping\";\nimport ErrorStackParser from \"error-stack-parser\";\nimport z from \"zod\";\nimport { deepClone } from \"@/shared/deep-clone\";\nimport { isLifeError } from \"@/shared/error\";\nimport { telemetryBrowserScopesDefinition } from \"@/telemetry/clients/browser\";\nimport type { TelemetryLog } from \"@/telemetry/types\";\n\nconst sourceMapCache = new Map<string, TraceMap | null>();\n\nasync function getSourceMap(file: string): Promise<TraceMap | null> {\n if (sourceMapCache.has(file)) return sourceMapCache.get(file) ?? null;\n try {\n const mapUrl = `${file}.map`;\n const r = await fetch(mapUrl, { credentials: \"same-origin\" });\n if (!r.ok) return sourceMapCache.set(file, null).get(file) ?? null;\n const json = await r.json();\n\n // Try TraceMap first, fall back to FlattenMap for sectioned source maps\n let map: TraceMap;\n try {\n map = new TraceMap(json);\n } catch (error) {\n // If TraceMap fails with sectioned source map error, use FlattenMap\n if (error instanceof Error && error.message.includes(\"sectioned source map\")) {\n map = new FlattenMap(json);\n } else {\n throw error;\n }\n }\n\n return sourceMapCache.set(file, map).get(file) ?? null;\n } catch (error) {\n console.error(\"Failed to get source map for\", file, error);\n return sourceMapCache.set(file, null).get(file) ?? null;\n }\n}\n\n/** Returns a pretty, source-mapped stack string for an Error (browser dev). */\nexport async function prettifyErrorStack<T extends Error>(err_: T): Promise<T> {\n const err = deepClone(err_);\n const frames = safeParse(err);\n const lines: string[] = [];\n\n for (const f of frames) {\n const file = f.fileName?.startsWith(\"async \") ? f.fileName.slice(6) : f.fileName;\n const line = f.lineNumber;\n const col = f.columnNumber;\n\n let ref = \"<unknown>\";\n if (file && line != null && col != null) {\n // biome-ignore lint/performance/noAwaitInLoops: sequential fine here\n const map = await getSourceMap(file);\n if (map) {\n const pos = originalPositionFor(map, { line, column: col, bias: 1 });\n if (pos.source && pos.line != null && pos.column != null) {\n ref = `${pos.source}:${pos.line}:${pos.column}`;\n }\n }\n if (!ref) ref = `${file}:${line}:${col}`;\n }\n\n lines.push(f.functionName ? ` at ${f.functionName} (${ref})` : ` at ${ref}`);\n }\n err.stack = lines.join(\"\\n\");\n return err;\n}\n\nfunction safeParse(err: Error) {\n try {\n return ErrorStackParser.parse(err);\n } catch {\n return [\n {\n fileName: undefined,\n lineNumber: undefined,\n columnNumber: undefined,\n functionName: err.name || \"Error\",\n },\n ];\n }\n}\n\n// ---\nexport async function formatErrorForBrowser(error: Error | unknown) {\n let code = \"\";\n let message = \"\";\n let stack = \"\";\n const after: string[] = [];\n let processed = false;\n\n // Format LifeError\n if (isLifeError(error)) {\n code = `LifeError (${error.code})`;\n message = error.message;\n stack = (await prettifyErrorStack(error)).stack ?? \"\";\n\n if (error.cause) {\n // Append the error after the LifeError\n const formatted = await formatErrorForBrowser(error.cause);\n after.push(formatted.content, ...formatted.after);\n\n // If the cause has a stack and the error is \"Unknown\", hide the error stack (redundant)\n const typedCause = error.cause as { stack?: string };\n if (error.code === \"Unknown\" && typedCause?.stack) stack = \"\";\n }\n\n processed = true;\n }\n\n // Format ZodError\n else if (error instanceof z.ZodError) {\n code = \"ZodError\";\n message = z.prettifyError(error);\n stack = (await prettifyErrorStack(error)).stack ?? \"\";\n processed = true;\n }\n\n // Format other errors\n if (!processed && error instanceof Error) {\n // Try to infer the code\n if (\"name\" in error && typeof error.name === \"string\") code = error.name;\n else if (\"code\" in error && typeof error.code === \"string\") code = error.code;\n\n // Try to infer the message\n if (\"message\" in error && typeof error.message === \"string\") message = error.message;\n else if (\"reason\" in error && typeof error.reason === \"string\") message = error.reason;\n\n // Try to infer the stack\n if (\"stack\" in error && typeof error.stack === \"string\") {\n stack = (await prettifyErrorStack(error)).stack ?? \"\";\n }\n\n // Remove first line of stack if it includes the error message\n stack =\n stack\n ?.split(\"\\n\")\n ?.filter((line) => !line.includes(error.message.trim()))\n ?.join(\"\\n\") ?? \"\";\n\n // If no code, message, or stack is present, use the default\n if (!code) code = \"Unknown Error\";\n if (!message) message = \"An unknown error occurred.\";\n if (!stack) stack = \"\";\n }\n\n // If a cause is present, format it as other as well\n if (error instanceof Error && error.cause) {\n const formatted = await formatErrorForBrowser(error.cause);\n after.push(formatted.content, ...formatted.after);\n }\n\n return {\n content: `${code}${code ? \": \" : \"\"}${message}${message ? \" \" : \"\"}${stack ? `\\n${stack}` : \"\"}`,\n after,\n };\n}\n\nexport async function formatLogForBrowser(log: TelemetryLog) {\n // Get prefix and color based on level\n let prefix: string;\n if (log.level === \"fatal\") prefix = \"✘\";\n else if (log.level === \"error\") prefix = \"✘\";\n else if (log.level === \"warn\") prefix = \"▲\";\n else if (log.level === \"info\") prefix = \"⦿\";\n else prefix = \"∴\";\n\n // Format the log scope\n const scopeDefinition =\n telemetryBrowserScopesDefinition?.[log.scope as keyof typeof telemetryBrowserScopesDefinition];\n const scopeDisplayName =\n scopeDefinition?.displayName instanceof Function\n ? // biome-ignore lint/suspicious/noExplicitAny: fine here\n scopeDefinition.displayName(log.attributes as any)\n : scopeDefinition?.displayName;\n const scope = `[${scopeDisplayName ?? \"Unknown\"}]`;\n\n // Format the log message\n const message = log.message || \"\";\n\n // Build the log header with browser console CSS styling\n const header = `${prefix} ${scope}${message ? ` ${message}` : \"\"}`;\n\n // Format the log error content (if any)\n const formatted = await formatErrorForBrowser(log.error);\n const errors = [formatted.content, ...formatted.after];\n\n return [header, ...errors];\n}\n","import type z from \"zod\";\nimport type { definition } from \"@/server/api/definition\";\nimport type {\n LifeApiCallDefinition,\n LifeApiCastDefinition,\n LifeApiDefinition,\n LifeApiStreamDefinition,\n} from \"@/server/api/types\";\nimport { canon } from \"@/shared/canon\";\nimport { lifeError } from \"@/shared/error\";\nimport * as op from \"@/shared/operation\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\n\n// Helper types to extract input/output schemas\ntype InferInput<T> = T extends { inputDataSchema: z.ZodType }\n ? z.input<T[\"inputDataSchema\"]>\n : undefined;\n\ntype InferOutput<T> = T extends { outputDataSchema: z.ZodType }\n ? z.output<T[\"outputDataSchema\"]>\n : undefined;\n\n// Extract handlers by type\ntype CallHandlers<T extends LifeApiDefinition> = {\n [K in keyof T as T[K] extends LifeApiCallDefinition ? K : never]: T[K];\n};\n\ntype CastHandlers<T extends LifeApiDefinition> = {\n [K in keyof T as T[K] extends LifeApiCastDefinition ? K : never]: T[K];\n};\n\ntype StreamHandlers<T extends LifeApiDefinition> = {\n [K in keyof T as T[K] extends LifeApiStreamDefinition ? K : never]: T[K];\n};\n\ntype UnsubscribeFunction = () => void;\n\n// Constants\nconst WS_PROTOCOL_REGEX = /^http/;\nconst TRAILING_SLASH_REGEX = /\\/$/;\nconst SUBSCRIPTION_ID_LENGTH = 16;\n\nexport class LifeServerApiClient<Def extends LifeApiDefinition = typeof definition> {\n readonly #telemetry: TelemetryClient;\n readonly #serverUrl: string;\n readonly #serverToken?: string;\n #ws?: WebSocket;\n readonly #subscriptions = new Map<string, (data: unknown) => void>();\n #wsReconnectTimeout?: NodeJS.Timeout;\n\n constructor(params: { telemetry: TelemetryClient; serverUrl: string; serverToken?: string }) {\n this.#telemetry = params.telemetry;\n this.#serverUrl = params.serverUrl.replace(TRAILING_SLASH_REGEX, \"\");\n this.#serverToken = params.serverToken;\n }\n\n private ensureWebSocket(): Promise<WebSocket> {\n if (this.#ws?.readyState === WebSocket.OPEN) return Promise.resolve(this.#ws);\n\n return new Promise((resolve, reject) => {\n const wsUrl = `${this.#serverUrl.replace(WS_PROTOCOL_REGEX, \"ws\")}/api/ws`;\n this.#ws = new WebSocket(wsUrl);\n\n this.#ws.onopen = () => {\n if (this.#ws) resolve(this.#ws);\n };\n\n this.#ws.onerror = () => {\n reject(lifeError({ code: \"Upstream\", message: \"WebSocket connection failed\" }));\n };\n\n this.#ws.onmessage = (event) => {\n try {\n const [err, message] = canon.parse(event.data);\n if (err) return;\n if (!message || typeof message !== \"object\" || message === null) return;\n if (!(\"subscriptionId\" in message) || typeof message.subscriptionId !== \"string\") return;\n const callback = this.#subscriptions.get(message.subscriptionId);\n callback?.(message.data);\n } catch {\n // Silently ignore parse errors\n }\n };\n\n this.#ws.onclose = () => {\n if (this.#subscriptions.size > 0) {\n this.#wsReconnectTimeout = setTimeout(() => {\n this.ensureWebSocket().catch(() => {\n // Retry silently\n });\n }, 5000);\n }\n };\n });\n }\n\n async call<K extends keyof CallHandlers<Def>>(handlerId: K, input?: InferInput<Def[K]>) {\n return await this.#telemetry.trace(\"api.call()\", async () => {\n const url = `${this.#serverUrl}/api/http`;\n const headers: Record<string, string> = { \"Content-Type\": \"application/json\" };\n\n if (this.#serverToken) headers.Authorization = `Bearer ${this.#serverToken}`;\n\n try {\n const [errCanon, body] = canon.stringify({\n handlerId: handlerId as string,\n serverToken: this.#serverToken,\n data: input,\n });\n if (errCanon) return op.failure(errCanon);\n\n const response = await fetch(url, {\n method: \"POST\",\n headers,\n body,\n });\n\n if (!response.ok) {\n try {\n const result = canon.parse(await response.text()) as op.OperationResult<unknown>;\n return op.failure(\n result?.[0] ?? {\n code: \"Upstream\",\n message: `API call failed: ${response.statusText}`,\n },\n );\n } catch {\n return op.failure({\n code: \"Upstream\",\n message: `API call failed: ${response.statusText}`,\n });\n }\n }\n\n const text = await response.text();\n const [err, data] = canon.parse(text) as op.OperationResult<unknown>;\n if (err) return op.failure(err);\n\n return op.success(data as InferOutput<Def[K]>);\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n });\n }\n\n cast<K extends keyof CastHandlers<Def>>(\n handlerId: K,\n input?: InferInput<Def[K]>,\n ): Promise<op.OperationResult<void>> {\n return this.#telemetry.trace(\"api.cast()\", async () => {\n const ws = await this.ensureWebSocket();\n const [errCanon, body] = canon.stringify({\n type: \"cast\",\n handlerId: handlerId as string,\n serverToken: this.#serverToken,\n data: input,\n });\n if (errCanon) return op.failure(errCanon);\n return await op.attempt(async () => ws.send(body));\n });\n }\n\n subscribe<K extends keyof StreamHandlers<Def>>(\n handlerId: K,\n callback: (data: InferOutput<Def[K]>) => void,\n input?: InferInput<Def[K]>,\n ): op.OperationResult<UnsubscribeFunction> {\n return this.#telemetry.trace(\"api.subscribe()\", () => {\n try {\n const subscriptionId = `sub_${Date.now()}_${Math.random()\n .toString(36)\n .substring(2, 2 + SUBSCRIPTION_ID_LENGTH)}`;\n\n this.#subscriptions.set(subscriptionId, callback as (data: unknown) => void);\n\n // Start connection asynchronously\n this.ensureWebSocket()\n .then((ws) => {\n const [errCanon, body] = canon.stringify({\n type: \"stream\",\n action: \"subscribe\",\n handlerId: handlerId as string,\n subscriptionId,\n serverToken: this.#serverToken,\n data: input,\n });\n if (errCanon) return op.failure(errCanon);\n ws.send(body);\n })\n .catch(() => {\n this.#subscriptions.delete(subscriptionId);\n });\n\n const unsubscribe: UnsubscribeFunction = () => {\n this.#subscriptions.delete(subscriptionId);\n if (this.#ws?.readyState === WebSocket.OPEN) {\n const [errCanon, body] = canon.stringify({\n type: \"stream\",\n action: \"unsubscribe\",\n handlerId: handlerId as string,\n subscriptionId,\n serverToken: this.#serverToken,\n });\n if (errCanon) return op.failure(errCanon);\n this.#ws.send(body);\n }\n };\n\n return op.success(unsubscribe);\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n });\n }\n\n disconnect(): void {\n if (this.#wsReconnectTimeout) {\n clearTimeout(this.#wsReconnectTimeout);\n this.#wsReconnectTimeout = undefined;\n }\n this.#subscriptions.clear();\n if (this.#ws) {\n this.#ws.close();\n this.#ws = undefined;\n }\n }\n}\n","import { AgentClient } from \"@/agent/client/class\";\nimport type { AgentClientDefinition, AgentClientFromBuild } from \"@/agent/client/types\";\nimport { type ClientBuild, importClientBuild } from \"@/exports/build/client\";\nimport * as op from \"@/shared/operation\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { createTelemetryClient, TelemetryBrowserClient } from \"@/telemetry/clients/browser\";\nimport { formatLogForBrowser } from \"@/telemetry/helpers/formatting/browser\";\nimport { logLevelPriority } from \"@/telemetry/helpers/log-level-priority\";\nimport type { TelemetryLogLevel } from \"@/telemetry/types\";\nimport { LifeServerApiClient } from \"./api\";\nimport type { LifeClientOptions } from \"./options\";\n\n// Stream formatted telemetry logs to the terminal\nTelemetryBrowserClient.registerGlobalConsumer({\n async start(queue) {\n for await (const item of queue) {\n if (item.type !== \"log\") continue;\n const logLevel = (globalThis?.process?.env?.LOG_LEVEL as TelemetryLogLevel) ?? \"info\";\n\n // Ignore logs lower than the requested log level\n const priority = logLevelPriority(item.level);\n if (priority < logLevelPriority(logLevel as TelemetryLogLevel)) continue;\n\n // Format and print the log\n try {\n const contents = (await formatLogForBrowser(item)).filter(Boolean);\n let consoleFn: (line: string) => void;\n if (priority >= logLevelPriority(\"error\")) consoleFn = console.error;\n else if (priority >= logLevelPriority(\"warn\")) consoleFn = console.warn;\n else consoleFn = console.log;\n for (let i = 0; i < contents.length; i++)\n consoleFn(\n `Life.js (${item.id.slice(0, 6)}, ${i + 1}/${contents.length})\\n${contents[i]}`,\n );\n } catch {\n console.log(item.message);\n }\n }\n },\n});\n\nexport class LifeClient {\n readonly options: LifeClientOptions;\n readonly #agents = new Map<string, AgentClient<AgentClientDefinition>>();\n readonly #telemetry: TelemetryClient;\n api: LifeServerApiClient;\n\n constructor(options: LifeClientOptions) {\n this.options = options;\n\n // Initialize telemetry\n this.#telemetry = createTelemetryClient(\"client\", {});\n\n // Initialize API client\n this.api = new LifeServerApiClient({\n telemetry: this.#telemetry,\n serverUrl: this.options.serverUrl,\n serverToken: this.options.serverToken,\n });\n }\n\n /**\n * Create a new agent instance on the server\n * @param name - Agent name/type to create\n * @param scope - Agent scope configuration\n * @returns AgentClient instance if creation successful\n */\n async createAgent<Name extends keyof ClientBuild>(name: Name, options: { id?: string } = {}) {\n return await this.#telemetry.trace(\"createAgent()\", async (span) => {\n const [error, agent] = await this.#createAgent(name, options);\n if (error) {\n span.log.error({ error });\n return op.failure(error);\n }\n return op.success(agent);\n });\n }\n\n // Private method, doesn't log to telemetry\n async #createAgent<Name extends keyof ClientBuild>(name: Name, options: { id?: string } = {}) {\n return await this.#telemetry.trace(\"#createAgent()\", async () => {\n try {\n // Load the client build if not already loaded\n const [errIndex, buildIndex] = await importClientBuild();\n if (errIndex) return op.failure(errIndex);\n const build = buildIndex[name as keyof ClientBuild];\n if (!build) {\n return op.failure({\n code: \"NotFound\",\n message: `Agent '${String(name)}' not found in client build.`,\n });\n }\n\n // Send a call to the server to create the agent\n const [err, data] = await this.api.call(\"agent.create\", { name, id: options.id });\n if (err) return op.failure(err);\n\n // Create agent client with proper definition and plugins from build\n const agentClient = new AgentClient({\n id: data.id,\n definition: build.definition,\n life: this,\n config: data.clientConfig ?? {},\n });\n this.#agents.set(data.id, agentClient);\n\n // Return the agent client\n return op.success(op.toPublic(agentClient) as AgentClientFromBuild<Name>);\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while creating agent.\",\n cause: error,\n });\n }\n });\n }\n\n /**\n * Get an existing agent client instance\n * @param id - Agent ID\n * @returns AgentClient instance or undefined\n */\n getAgent<Name extends keyof ClientBuild>(name: Name, options: { id?: string } = {}) {\n return this.#telemetry.trace(\"getAgent()\", (span) => {\n const [error, agent] = this.#getAgent<Name>(name, options);\n if (error) {\n span.log.error({ error });\n return op.failure(error);\n }\n return op.success(agent);\n });\n }\n\n // Private method, doesn't log to telemetry\n #getAgent<Name extends keyof ClientBuild>(name: Name, options: { id?: string } = {}) {\n return this.#telemetry.trace(\"#getAgent()\", () => {\n try {\n // If ID is provided, get the agent from the map\n if (options.id) {\n const agent = this.#agents.get(options.id);\n if (!agent) return op.success(undefined);\n return op.success(op.toPublic(agent) as unknown as AgentClientFromBuild<Name>);\n }\n\n // Otherwise, retrieve the agent by name\n const agents = Array.from(this.#agents.values()).filter((a) => a.def.name === String(name));\n if (!agents.length) return op.success(undefined);\n\n // If many agents exist for this same name, return failure\n if (agents.length > 1)\n return op.failure({\n code: \"Conflict\",\n message: `Multiple agents found for name '${String(name)}'. Use an ID to get a specific agent.`,\n });\n\n // Else, return the first agent\n return op.success(op.toPublic(agents[0]) as unknown as AgentClientFromBuild<Name>);\n } catch (error) {\n return op.failure({\n code: \"Unknown\",\n message: \"Unknown error while getting agent.\",\n cause: error,\n });\n }\n });\n }\n\n /**\n * Get or create an agent instance\n * @param name - Agent name/type\n * @param scope - Agent scope configuration\n * @returns AgentClient instance\n */\n async getOrCreateAgent<Name extends keyof ClientBuild>(\n name: Name,\n options: { id?: string } = {},\n ) {\n return await this.#telemetry.trace(\"getOrCreateAgent()\", async (span) => {\n const [error, agent] = await this.#getOrCreateAgent(name, options);\n if