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,038 lines (1,031 loc) • 38.3 kB
JavaScript
import {
ProcessStats,
TelemetryNodeClient,
createTelemetryClient,
pipeConsoleToTelemetryClient
} from "../../chunk-MLK5YGGI.mjs";
import {
TransportClientBase
} from "../../chunk-HZQWVS5S.mjs";
import {
canon
} from "../../chunk-6CBODJTF.mjs";
import {
RollingBuffer
} from "../../chunk-LPKEDFLM.mjs";
import {
LiveKitNodeClient,
eouProviders,
llmProviders,
prepareAgentConfig,
sttProviders,
ttsProviders,
vadProviders
} from "../../chunk-XNMZQAOR.mjs";
import {
pluginEventInputSchema,
toMethodName
} from "../../chunk-X455LF5V.mjs";
import {
AsyncQueue,
deepClone
} from "../../chunk-D2T23PCX.mjs";
import {
importServerBuild
} from "../../chunk-CUJAJGIJ.mjs";
import {
attempt,
failure,
isLifeError,
lifeError,
newId,
success,
toPublic
} from "../../chunk-ZHBK6UTM.mjs";
import {
__name
} from "../../chunk-2D3UJWOA.mjs";
// server/agent-process/process.ts
import { createBirpc } from "birpc";
// agent/server/class.ts
import z2 from "zod";
// plugins/server/class.ts
import z from "zod";
var PluginServer = class {
static {
__name(this, "PluginServer");
}
def;
agent;
config;
context;
telemetry;
queue = new AsyncQueue();
eventsListeners = /* @__PURE__ */ new Map();
contextListeners = /* @__PURE__ */ new Map();
streamHandlersQueues = [];
externalInterceptHandlers = [];
#internalAccessor;
#waitProcessingResolvers = /* @__PURE__ */ new Map();
#waitResultResolvers = /* @__PURE__ */ new Map();
#waitProcessingHistory = new RollingBuffer(1e3);
#waitResultHistory = new RollingBuffer(1e3);
#handlersStates = /* @__PURE__ */ new Map();
constructor({
agent,
definition,
config,
context = {}
}) {
this.def = definition;
this.agent = agent;
const { error: errConfig, data: parsedConfig } = definition.config.schema.safeParse(config);
if (errConfig)
throw lifeError({
code: "Validation",
message: `Invalid config provided to plugin server '${definition.name}'.`,
cause: errConfig
});
this.config = parsedConfig;
const { error: errContext, data: parsedContext } = definition.context.schema.safeParse(context);
if (errContext)
throw lifeError({
code: "Validation",
message: `Invalid context provided to plugin server '${definition.name}'.`,
cause: errContext
});
this.context = parsedContext;
this.telemetry = createTelemetryClient("plugin.server", {
agentId: agent.id,
agentSha: agent.sha,
agentName: agent.def.name,
agentConfig: agent.config,
transportProviderName: agent.config.transport.provider,
llmProviderName: agent.config.models.llm.provider,
sttProviderName: agent.config.models.stt.provider,
eouProviderName: agent.config.models.eou.provider,
ttsProviderName: agent.config.models.tts.provider,
vadProviderName: agent.config.models.vad.provider,
pluginName: definition.name,
pluginServerConfig: this.config
});
this.#initClientRPC();
const [errAccessor, accessor] = this.getAccessor({
type: "server",
name: this.def.name
});
if (errAccessor) throw errAccessor;
this.#internalAccessor = accessor;
}
/**
* Produces an accessor object, exposing methods to interact safely with the plugin
* from a given source (plugin, handler, client).
* @param source - The source accessing the plugin.
* @param handlerAccess - The access mode for the handler.
* @returns An accessor object.
*/
getAccessor(source, handlerAccess) {
const [errClone, clonedConfig] = attempt(() => deepClone(this.config));
if (errClone) return failure(errClone);
const contextOnChange = /* @__PURE__ */ __name(((selector, callback) => {
const id = newId("listener");
this.contextListeners.set(id, { id, callback, selector });
return success(() => this.contextListeners.delete(id));
}), "contextOnChange");
const contextGet = /* @__PURE__ */ __name((() => attempt(() => deepClone(this.context))), "contextGet");
const contextSet = /* @__PURE__ */ __name(((valueOrUpdater) => {
const [errOld, oldContext] = attempt(() => deepClone(this.context));
if (errOld) return failure(errOld);
if (typeof valueOrUpdater === "function") this.context = valueOrUpdater(oldContext);
else this.context = valueOrUpdater;
Promise.all([
// Notify context change listeners
Array.from(this.contextListeners.values()).map(async (listener) => {
try {
const newSelectedValue = listener.selector(this.context);
const oldSelectedValue = listener.selector(oldContext);
const [errEqual, equal] = canon.equal(newSelectedValue, oldSelectedValue);
if (errEqual) return failure(errEqual);
if (equal) await listener.callback(deepClone(this.context), deepClone(oldContext));
} catch (error) {
this.telemetry.log.error({
message: `Error while notifying context listeners in plugin '${this.def.name}'.`,
error
});
}
}),
// Send new context value updates via RPC
this.agent.transport.call({
name: `plugin.${this.def.name}.context.changed`,
schema: {
input: z.object({ value: z.any(), timestamp: z.number() })
},
input: { value: this.context, timestamp: Date.now() }
})
]);
return success();
}), "contextSet");
const eventsEmit = /* @__PURE__ */ __name(((event) => {
const { error: errEvent, data: parsedEvent } = pluginEventInputSchema.safeParse(event);
if (errEvent)
return failure({
code: "Validation",
message: "Invalid event shape for event.",
cause: errEvent
});
const isInternal = source.type === "server" && source.name === this.def.name;
if (!isInternal && parsedEvent.name.startsWith("plugin."))
return failure({
code: "Validation",
message: "Internal events cannot be emitted from outside the plugin."
});
const eventDef = this.def.events?.find((e) => e.name === parsedEvent.name);
if (!eventDef)
return failure({
code: "Validation",
message: `Event of type '${parsedEvent.name}' not found.`
});
let parsedData = null;
if (eventDef.dataSchema) {
const { error: errData, data } = eventDef.dataSchema.safeParse(parsedEvent.data);
parsedData = data;
if (errData)
return failure({
code: "Validation",
message: `Invalid event data shape for '${parsedEvent.name}' event.`,
cause: errData
});
}
const outputEvent = {
id: newId("event"),
name: parsedEvent.name,
urgent: parsedEvent.urgent,
data: parsedData,
created: { at: Date.now(), by: source },
edited: false,
dropped: false,
contextChanges: []
};
if (outputEvent.urgent) this.queue.pushFirst(outputEvent);
else this.queue.push(outputEvent);
return success(outputEvent.id);
}), "eventsEmit");
const eventsOn = /* @__PURE__ */ __name(((selector, callback, includeDropped = false) => {
const id = newId("listener");
this.eventsListeners.set(id, { id, callback, selector, includeDropped });
return success(() => this.eventsListeners.delete(id));
}), "eventsOn");
const eventsOnce = /* @__PURE__ */ __name(((selector, callback, includeDropped = false) => {
const [errOn, unsubscribe] = eventsOn(
selector,
async (event) => {
unsubscribe?.();
await callback(event);
},
includeDropped
);
if (errOn) return failure(errOn);
return success(unsubscribe);
}), "eventsOnce");
const eventsWaitForProcessing = /* @__PURE__ */ __name((async (eventId) => {
let resolver;
const cleanupResolver = /* @__PURE__ */ __name(() => {
if (!resolver) return;
this.#waitProcessingResolvers.get(eventId)?.delete(resolver);
if (this.#waitProcessingResolvers.get(eventId)?.size === 0)
this.#waitProcessingResolvers.delete(eventId);
}, "cleanupResolver");
try {
if (this.#waitProcessingHistory.get().includes(eventId)) return success();
const resolverPromise = new Promise(
(r) => resolver = /* @__PURE__ */ __name(() => void r(success()), "resolver")
);
if (!resolver) throw new Error("Resolver not found. Shouldn't happen.");
if (!this.#waitProcessingResolvers.has(eventId))
this.#waitProcessingResolvers.set(eventId, /* @__PURE__ */ new Set());
this.#waitProcessingResolvers.get(eventId)?.add(resolver);
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => {
resolve(
failure({
code: "Timeout",
message: `Waiting for event '${eventId}' processing timed out.`,
isPublic: true
})
);
}, 15e3);
});
const [err] = await Promise.race([resolverPromise, timeoutPromise]);
if (err) return failure(err);
this.#waitProcessingHistory.add(eventId);
return success();
} catch (error) {
return failure({ code: "Unknown", cause: error });
} finally {
cleanupResolver();
}
}), "eventsWaitForProcessing");
const eventsWaitForResult = /* @__PURE__ */ __name((async (eventId, handlerName) => {
let resolver;
const cleanupResolver = /* @__PURE__ */ __name(() => {
if (!resolver) return;
this.#waitResultResolvers.get(eventId)?.get(handlerName)?.delete(resolver);
if (this.#waitResultResolvers.get(eventId)?.get(handlerName)?.size === 0)
this.#waitResultResolvers.get(eventId)?.delete(handlerName);
if (this.#waitResultResolvers.get(eventId)?.size === 0)
this.#waitResultResolvers.delete(eventId);
}, "cleanupResolver");
try {
const historyItem = this.#waitResultHistory.get().find((h) => h.eventId === eventId && h.handlerName === handlerName);
if (historyItem) return historyItem.result;
const resolverPromise = new Promise((r) => resolver = r);
if (!resolver) throw new Error("Resolver not found. Shouldn't happen.");
if (!this.#waitResultResolvers.has(eventId))
this.#waitResultResolvers.set(eventId, /* @__PURE__ */ new Map());
if (!this.#waitResultResolvers.get(eventId)?.get(handlerName))
this.#waitResultResolvers.get(eventId)?.set(handlerName, /* @__PURE__ */ new Set());
this.#waitResultResolvers.get(eventId)?.get(handlerName)?.add(resolver);
const timeoutPromise = new Promise((resolve) => {
setTimeout(() => {
resolve(
failure({
code: "Timeout",
message: `Waiting for event '${eventId}' result timed out.`,
isPublic: true
})
);
}, 15e3);
});
const result = await Promise.race([resolverPromise, timeoutPromise]);
this.#waitResultHistory.add({ eventId, handlerName, result });
return result;
} catch (error) {
return failure({ code: "Unknown", cause: error });
} finally {
cleanupResolver();
}
}), "eventsWaitForResult");
if (source.type === "handler") {
return success({
config: clonedConfig,
context: {
get: contextGet,
...handlerAccess === "write" ? { set: contextSet } : {},
onChange: contextOnChange
},
events: {
emit: eventsEmit,
waitForResult: eventsWaitForResult,
waitForProcessing: eventsWaitForProcessing
}
});
}
if (source.type === "server") {
return success({
config: clonedConfig,
context: {
get: contextGet,
onChange: contextOnChange
},
events: {
emit: eventsEmit,
on: eventsOn,
once: eventsOnce,
waitForResult: eventsWaitForResult,
waitForProcessing: eventsWaitForProcessing
}
});
}
if (source.type === "client") {
return success({
config: clonedConfig,
context: {
get: contextGet,
onChange: contextOnChange
},
events: {
emit: eventsEmit,
on: eventsOn,
once: eventsOnce,
waitForResult: eventsWaitForResult,
waitForProcessing: eventsWaitForProcessing
}
});
}
return failure({
code: "NotFound",
message: `Unknown source type '${source.type}'.`
});
}
/**
* Produces a map of dependencies' accessors, see `getAccessor()`.
* @param source - The source accessing the dependencies.
* @returns A map of dependencies' accessors.
*/
getDependenciesAccessors(source) {
const dependenciesAccessors = {};
for (const dependencyDef of this.def.dependencies ?? []) {
const depServer = this.agent.plugins?.[toMethodName(dependencyDef.name)];
if (!depServer)
return failure({
code: "NotFound",
message: `Failed to obtain server instance for dependency plugin '${dependencyDef.name}'. Shouldn't happen.`
});
const [errAccessor, accessor] = depServer.getAccessor(source);
if (errAccessor) return failure(errAccessor);
dependenciesAccessors[dependencyDef.name] = accessor;
}
return success(dependenciesAccessors);
}
async start() {
return await this.telemetry.trace(`plugin.${this.def.name}.start()`, async () => {
const streamHandlers = this.def.handlers.filter((h) => h.mode === "stream");
for (const handler of streamHandlers) {
const queue = new AsyncQueue();
this.streamHandlersQueues.push(queue);
(async () => {
for await (const event of queue) await this.#executeHandler(handler, event);
})();
}
const interceptHandlers = this.def.handlers.filter((h) => h.mode === "intercept");
for (const handler of interceptHandlers) {
for (const dependencyDef of this.def.dependencies ?? []) {
const dependency = this.agent.plugins?.[dependencyDef.name];
if (!dependency) {
this.telemetry.log.error(
lifeError({
code: "NotFound",
message: `Failed to obtain server instance for dependency plugin '${dependencyDef.name}'. Shouldn't happen.`
})
);
continue;
}
dependency.externalInterceptHandlers.push({
dependent: this,
handler
});
}
}
(async () => {
for await (const event of this.queue) {
try {
for (const { handler, dependent } of this.externalInterceptHandlers) {
await this.#executeHandler(handler, event, dependent, (result) => {
if (result.type === "drop") {
if (event.dropped)
event.dropped = {
at: Date.now(),
by: {
plugin: this.def.name,
handler: handler.name
},
reason: result.reason
};
} else if (result.type === "next") {
const [errClone, oldData] = attempt(() => deepClone(event.data));
if (errClone) return failure(errClone);
if (!event.edited) event.edited = [];
event.edited.push({
at: Date.now(),
by: {
plugin: this.def.name,
handler: handler.name
},
reason: result.reason,
dataBefore: oldData,
dataAfter: result.data
});
event.data = result.data;
}
});
if (event.dropped) break;
}
if (!event.dropped) {
const blockHandlers = this.def.handlers.filter((h) => h.mode === "block");
for (const handler of blockHandlers) {
const [errOld, oldContext] = attempt(() => deepClone(this.context));
if (errOld) {
this.telemetry.log.error({ error: errOld });
continue;
}
await this.#executeHandler(handler, event);
const [errEqual, equal] = canon.equal(
this.context,
oldContext
);
if (errEqual) {
this.telemetry.log.error({ error: errEqual });
continue;
}
if (!equal) {
if (!event.contextChanges) event.contextChanges = [];
event.contextChanges.push({
at: Date.now(),
byHandler: handler.name,
valueBefore: oldContext,
valueAfter: this.context
});
}
}
for (const queue of this.streamHandlersQueues) queue.push(event);
}
await Promise.all(
Array.from(this.eventsListeners.values()).map(
async ({ id, callback, selector, includeDropped }) => {
if (this.#eventMatchesSelector(event, selector)) {
if (!event.dropped || includeDropped) {
if (callback === "remote") {
const [err] = await this.agent.transport.call({
name: `plugin.${this.def.name}.events.callback`,
schema: {
input: z.object({
listenerId: z.string(),
event: pluginEventInputSchema
})
},
input: { listenerId: id, event }
});
if (err)
this.telemetry.log.error({
message: `Error while steaming event to remote listener in plugin '${this.def.name}'.`,
error: err
});
} else await callback(event);
}
}
}
)
);
const waiters = this.#waitProcessingResolvers.get(event.id) ?? [];
for (const resolver of waiters) resolver();
} catch (error) {
this.telemetry.log.error({
message: `Unknown error while executing event in plugin '${this.def.name}'.`,
error
});
}
}
})();
const [errEmit, eventId] = this.#emitInternalEvent({
name: "plugin.start",
data: { isRestart: this.agent.isRestart, restartCount: 0 }
});
if (errEmit) return failure(errEmit);
const [errWaitForResult] = await this.#internalAccessor.events.waitForProcessing(eventId);
if (errWaitForResult) return failure(errWaitForResult);
return success();
});
}
async stop() {
return await this.telemetry.trace(`plugin.${this.def.name}.stop()`, async () => {
const [errEmit, eventId] = this.#emitInternalEvent({ name: "plugin.stop", urgent: true });
if (errEmit) return failure(errEmit);
const [errWaitForResult] = await this.#internalAccessor.events.waitForProcessing(eventId);
if (errWaitForResult) return failure(errWaitForResult);
this.queue.stop();
for (const queue of this.streamHandlersQueues) queue.stop();
this.#handlersStates.clear();
return success();
});
}
#getHandlerState(handler, accessor) {
const savedState = this.#handlersStates.get(handler.name);
if (savedState) return savedState;
const initialState = typeof handler.state === "function" ? handler.state({ config: accessor.config }) : handler.state ?? {};
this.#handlersStates.set(handler.name, initialState);
return initialState;
}
#emitInternalEvent(event) {
const [errEmit, eventId] = this.#internalAccessor.events.emit(event);
if (errEmit) return failure(errEmit);
return success(eventId);
}
async #executeHandler(handler, event, dependency, onInterceptResult) {
const result = await (async () => {
const [errClone, clonedEvent] = attempt(() => deepClone(event));
if (errClone) return failure(errClone);
const handlerAccess = handler.mode === "block" ? "write" : "read";
const [errAccessor, accessor] = this.getAccessor(
{
type: "handler",
plugin: this.def.name,
handler: handler.name,
event: event.name
},
handlerAccess
);
if (errAccessor) return failure(errAccessor);
const state = this.#getHandlerState(handler, accessor);
const [errDependency, dependencyAccessor] = dependency ? dependency.getAccessor({ type: "server", name: dependency.def.name }) : success({});
if (errDependency) return failure(errDependency);
const [errDependencies, dependenciesAccessor] = this.getDependenciesAccessors({
type: "handler",
plugin: this.def.name,
handler: handler.name,
event: event.name
});
if (errDependencies) return failure(errDependencies);
const drop = /* @__PURE__ */ __name((reason) => onInterceptResult?.({ type: "drop", reason }), "drop");
const next = /* @__PURE__ */ __name((data, reason) => onInterceptResult?.({ type: "next", data, reason }), "next");
return await this.telemetry.trace(
`plugin.${this.def.name}.handler.${handler.name}`,
async (span) => attempt(
async () => await handler.onEvent({
event: clonedEvent,
plugin: toPublic(accessor),
state,
telemetry: span,
agent: toPublic(this.agent),
...handler.mode === "intercept" ? { drop, next, dependency: dependencyAccessor } : { dependencies: dependenciesAccessor }
})
)
);
})();
if (result[0]) {
this.telemetry.log.error({
message: `Error while executing ${handler.mode} handler '${handler.name}' in plugin '${this.def.name}'.`,
error: result[0]
});
this.#emitInternalEvent({ name: "plugin.error", data: { error: result[0], event } });
}
const waiters = this.#waitResultResolvers.get(event.id)?.get(handler.name) ?? [];
for (const resolver of waiters) resolver(result);
}
#initClientRPC() {
const [errAccessor, accessor] = this.getAccessor({
type: "client",
name: this.def.name
});
if (errAccessor) return failure(errAccessor);
this.agent.transport.register({
name: `plugin.${this.def.name}.context.get`,
schema: { output: z.object().loose() },
execute: /* @__PURE__ */ __name(() => success({ value: this.context, timestamp: Date.now() }), "execute")
});
this.agent.transport.register({
name: `plugin.${this.def.name}.events.emit`,
schema: {
input: pluginEventInputSchema,
output: z.object({ id: z.string() })
},
execute: /* @__PURE__ */ __name(async (input) => {
const [err, id] = await accessor.events.emit(input);
if (err) return failure(err);
return success({ id });
}, "execute")
});
this.agent.transport.register({
name: `plugin.${this.def.name}.events.subscribe`,
schema: {
input: z.object({
listenerId: z.string(),
selector: z.any(),
includeDropped: z.boolean().prefault(false)
})
},
execute: /* @__PURE__ */ __name((input) => {
const { listenerId, selector, includeDropped } = input;
this.eventsListeners.set(listenerId, {
id: listenerId,
callback: "remote",
selector,
includeDropped
});
return success();
}, "execute")
});
this.agent.transport.register({
name: `plugin.${this.def.name}.events.unsubscribe`,
schema: { input: z.object({ listenerId: z.string() }) },
execute: /* @__PURE__ */ __name((input) => {
const { listenerId } = input;
this.eventsListeners.delete(listenerId);
return success();
}, "execute")
});
this.agent.transport.register({
name: `plugin.${this.def.name}.events.waitForProcessing`,
schema: { input: z.object({ eventId: z.string() }) },
execute: /* @__PURE__ */ __name(async (input) => await accessor.events.waitForProcessing(input.eventId), "execute")
});
this.agent.transport.register({
name: `plugin.${this.def.name}.events.waitForResult`,
schema: {
input: z.object({ eventId: z.string(), handlerName: z.string() }),
output: z.object().loose()
},
execute: /* @__PURE__ */ __name(async (input) => await accessor.events.waitForResult(
input.eventId,
input.handlerName
), "execute")
});
}
#eventMatchesSelector(event, selector) {
const isAllSelector = selector === "*";
const isArraySelectorAndIncludesEvent = Array.isArray(selector) && selector.includes(event.name);
const isObjectSelectorAndIncludesEvent = typeof selector === "object" && "include" in selector && (selector.include === "*" || selector.include.includes(event.name));
const isObjectSelectorAndExcludesEvent = typeof selector === "object" && "exclude" in selector && selector.exclude?.includes(event.name);
return isAllSelector || isArraySelectorAndIncludesEvent || isObjectSelectorAndIncludesEvent && !isObjectSelectorAndExcludesEvent;
}
};
// shared/ensure-server.ts
function ensureServer(featureName) {
const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
if (isBrowser) {
throw new Error(`\u274C "${featureName}" is a server-only and must not run in the browser.`);
}
}
__name(ensureServer, "ensureServer");
// transport/client/node.ts
ensureServer("transport.client.node");
var nodeTransportProviders = {
livekit: LiveKitNodeClient
};
var TransportNodeClient = class extends TransportClientBase {
static {
__name(this, "TransportNodeClient");
}
constructor({
config,
obfuscateErrors = false,
telemetry: telemetry2 = null
}) {
const ProviderClass = nodeTransportProviders[config.provider];
super({ provider: new ProviderClass(config), obfuscateErrors, telemetry: telemetry2 });
}
};
// agent/server/class.ts
var AgentServer = class {
static {
__name(this, "AgentServer");
}
def;
id;
sha;
config;
transport;
storage = null;
models;
plugins = {};
scope;
isRestart;
telemetry;
#initialPluginsContexts;
constructor({
id,
sha,
definition,
scope,
config,
pluginsContexts,
isRestart
}) {
this.id = id;
this.sha = sha;
this.def = definition;
this.scope = scope ?? this.def.scope.schema.parse({});
this.config = config;
this.isRestart = isRestart ?? false;
this.#initialPluginsContexts = pluginsContexts ?? {};
this.telemetry = createTelemetryClient("agent.server", {
agentId: id,
agentSha: this.sha,
agentName: this.def.name,
agentConfig: this.config,
transportProviderName: this.config.transport.provider,
llmProviderName: this.config.models.llm.provider,
sttProviderName: this.config.models.stt.provider,
eouProviderName: this.config.models.eou.provider,
ttsProviderName: this.config.models.tts.provider,
vadProviderName: this.config.models.vad.provider
});
this.transport = new TransportNodeClient({
config: this.config.transport,
obfuscateErrors: true,
telemetry: this.telemetry
});
const vadProvider = vadProviders[this.config.models.vad.provider];
const sttProvider = sttProviders[this.config.models.stt.provider];
const eouProvider = eouProviders[this.config.models.eou.provider];
const llmProvider = llmProviders[this.config.models.llm.provider];
const ttsProvider = ttsProviders[this.config.models.tts.provider];
this.models = {
vad: new vadProvider.class(this.config.models.vad),
stt: new sttProvider.class(this.config.models.stt),
eou: new eouProvider.class(this.config.models.eou),
llm: new llmProvider.class(this.config.models.llm),
tts: new ttsProvider.class(this.config.models.tts)
};
this.#initializePlugins();
}
#initializePlugins() {
const pluginNames = this.def.plugins.map((p) => p.name);
const duplicates = pluginNames.filter((name, index) => pluginNames.indexOf(name) !== index);
if (duplicates.length > 0) {
const uniqueDuplicates = [...new Set(duplicates)];
throw lifeError({
code: "Validation",
message: `Two or more plugins are named "${uniqueDuplicates.join('", "')}". Plugin names must be unique. (agent: '${this.def.name}')`
});
}
for (const plugin of this.def.plugins) {
for (const dependency of plugin.dependencies) {
const depPlugin = this.def.plugins.find((p) => p.name === dependency.name);
if (!depPlugin) {
throw lifeError({
code: "Validation",
message: `Plugin '${plugin.name}' depends on plugin '${dependency.name}', but '${dependency.name}' is not registered. (agent: '${this.def.name}')`
});
}
}
}
this.transport.register({
name: "agent.has-plugin-server",
schema: {
input: z2.object({ pluginName: z2.string() }),
output: z2.object({ hasServer: z2.boolean() })
},
execute: /* @__PURE__ */ __name(({ pluginName }) => {
const plugin = this.plugins?.[pluginName];
if (!plugin) return success({ hasServer: false });
return success({ hasServer: true });
}, "execute")
});
}
async start() {
return await this.telemetry.trace("AgentServer.start()", async () => {
try {
for (const definition of this.def.plugins) {
const { error: errorContext, data: parsedContext } = definition.context.schema.safeParse(
this.#initialPluginsContexts?.[definition.name] ?? {}
);
if (errorContext) {
return failure({
code: "Validation",
message: `Invalid initial context value for plugin '${definition.name}'.`,
cause: errorContext
});
}
const { error: errorConfig, data: parsedConfig } = definition.config.schema.safeParse(
this.def.pluginConfigs?.[definition.name] ?? {}
);
if (errorConfig) {
return failure({
code: "Validation",
message: `Invalid config value for plugin '${definition.name}'.`,
cause: errorConfig
});
}
const [errCreate, instance] = attempt(
() => new PluginServer({
agent: this,
definition,
config: parsedConfig,
context: parsedContext
})
);
if (errCreate) return failure(errCreate);
this.plugins[definition.name] = instance;
}
const result = await Promise.all(Object.values(this.plugins).map((p) => p.start()));
const err = result.find((r) => r[0])?.[0];
if (err) return failure(err);
return success();
} catch (error) {
return failure({ code: "Unknown", cause: error });
}
});
}
async stop() {
return await this.telemetry.trace("AgentServer.stop()", async (span) => {
try {
await Promise.all(
Object.entries(this.plugins).map(
([pluginId, plugin]) => plugin.stop().catch(
(error) => span.log.error({ message: `Error stopping plugin ${pluginId}:`, error })
)
)
);
const [errLeave] = await this.transport.leaveRoom();
if (errLeave) return failure(errLeave);
return success();
} catch (error) {
return failure({ code: "Unknown", cause: error });
}
});
}
};
// server/agent-process/process.ts
var processStats = new ProcessStats();
var agentServer = null;
var telemetry = null;
var rpc = createBirpc(
{
init(params) {
telemetry = createTelemetryClient("agent.process", { agentId: params.agentId });
pipeConsoleToTelemetryClient(telemetry);
return success();
},
//
async start(params) {
try {
const [errIndex, buildIndex] = await importServerBuild({
projectDirectory: process.cwd(),
noCache: true
});
if (errIndex) return failure(errIndex);
const build = buildIndex?.[params.name];
if (!build)
return failure({ code: "NotFound", message: `Agent '${params.name}' not found.` });
const [errConfig, config] = prepareAgentConfig(
build.definition.config,
build.globalConfigs
);
if (errConfig) return failure(errConfig);
const [errCreate, instance] = attempt(
() => new AgentServer({
id: params.id,
definition: build.definition,
scope: params.scope,
sha: build.sha,
config: config.server,
pluginsContexts: params.pluginsContexts,
isRestart: params.isRestart
})
);
if (errCreate) return failure(errCreate);
agentServer = instance;
const [err] = await agentServer.start();
if (err) return failure(err);
for (const pluginDef of build.definition.plugins) {
const [errAccessor, accessor] = agentServer.plugins[pluginDef.name]?.getAccessor({
type: "server",
name: pluginDef.name
}) ?? failure({ code: "NotFound", message: `Plugin '${pluginDef.name}' not found.` });
if (errAccessor) return failure(errAccessor);
accessor.context.onChange(
(c) => c,
async (c) => {
if (!agentServer) return;
const [error] = await rpc.syncContext({
agentId: agentServer.id,
pluginName: pluginDef.name,
context: c,
timestamp: Date.now()
});
if (error)
telemetry?.log.error({
message: `Failed to sync for plugin '${pluginDef.name}' in agent '${agentServer.def.name}' process.`,
error
});
}
);
}
await agentServer.transport.joinRoom(params.transportRoom.name, params.transportRoom.token);
await rpc.ready();
telemetry?.log.info({ message: "Agent started successfully." });
return success();
} catch (error) {
return failure({ code: "Unknown", cause: error });
}
},
async stop() {
try {
if (agentServer) {
const [err] = await agentServer.stop();
if (err) return failure(err);
agentServer = null;
}
return success();
} catch (error) {
return failure({ code: "Unknown", cause: error });
}
},
// Simple ping to check if process is responsive
async ping() {
return await success();
},
// Get detailed stats from the child process
async getProcessStats() {
try {
return await processStats.get();
} catch (error) {
return failure({ code: "Unknown", cause: error });
}
}
},
{
post: /* @__PURE__ */ __name((data) => process.send?.(data), "post"),
on: /* @__PURE__ */ __name((fn) => process.on("message", fn), "on"),
serialize: /* @__PURE__ */ __name((data) => {
const [error, result] = canon.serialize(data);
if (error) {
throw lifeError({
code: "Validation",
message: "Failed to serialize data from agent process to server. The message has been discarded.",
attributes: {
agentId: agentServer?.id,
agentName: agentServer?.def.name,
data
},
cause: error
});
}
return result;
}, "serialize"),
deserialize: /* @__PURE__ */ __name((data) => {
const [error, result] = canon.deserialize(data);
if (error) {
throw lifeError({
code: "Validation",
message: "Failed to deserialize data from agent process to server. The message has been discarded.",
attributes: {
agentId: agentServer?.id,
agentName: agentServer?.def.name,
data
},
cause: error
});
}
return result;
}, "deserialize"),
onFunctionError: /* @__PURE__ */ __name((error) => {
telemetry?.log.error(
isLifeError(error) ? error : lifeError({
code: "Unknown",
cause: error
})
);
}, "onFunctionError"),
onGeneralError: /* @__PURE__ */ __name((error) => {
telemetry?.log.error(
isLifeError(error) ? error : lifeError({
code: "Unknown",
cause: error
})
);
}, "onGeneralError"),
// Disable Birpc timeout
onTimeoutError: /* @__PURE__ */ __name(() => true, "onTimeoutError"),
timeout: -1
}
);
TelemetryNodeClient.registerGlobalConsumer({
start: /* @__PURE__ */ __name(async (queue) => {
for await (const signal of queue) rpc.syncTelemetry(signal);
}, "start")
});
process.on("uncaughtException", async (error) => {
telemetry?.log.error({ error });
await telemetry?.flushConsumers(1e3);
process.exit(1);
});
process.on("unhandledRejection", async (reason) => {
telemetry?.log.error({
message: reason instanceof Error ? reason.message : String(reason),
error: reason instanceof Error ? reason : void 0
});
await telemetry?.flushConsumers(1e3);
process.exit(1);
});
//# sourceMappingURL=process.mjs.map