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
JavaScript
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