UNPKG

life

Version:

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

1 lines 77.2 kB
{"version":3,"sources":["../../../server/agent-process/process.ts","../../../agent/server/class.ts","../../../plugins/server/class.ts","../../../shared/ensure-server.ts","../../../transport/client/node.ts"],"sourcesContent":["import { createBirpc } from \"birpc\";\nimport { AgentServer } from \"@/agent/server/class\";\nimport { prepareAgentConfig } from \"@/agent/server/config\";\nimport { importServerBuild } from \"@/exports/build/server\";\nimport type { AsyncQueue } from \"@/shared/async-queue\";\nimport { canon, type SerializableValue } from \"@/shared/canon\";\nimport { isLifeError, lifeError } from \"@/shared/error\";\nimport * as op from \"@/shared/operation\";\nimport { ProcessStats } from \"@/shared/process-stats\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { createTelemetryClient, TelemetryNodeClient } from \"@/telemetry/clients/node\";\nimport { pipeConsoleToTelemetryClient } from \"@/telemetry/helpers/patch-console\";\nimport type { TelemetrySignal } from \"@/telemetry/types\";\nimport type { ChildMethods, ParentMethods } from \"./types\";\n\n// Keep track of process stats\nconst processStats = new ProcessStats();\n\n// Holds the agent server instance created in start()\nlet agentServer: AgentServer | null = null;\n\n// Telemetry client\nlet telemetry: TelemetryClient | null = null;\n\nconst rpc = createBirpc<ParentMethods, ChildMethods>(\n {\n init(params) {\n // Initialize telemetry client\n telemetry = createTelemetryClient(\"agent.process\", { agentId: params.agentId });\n // Forward console.* methods to the process telemetry client\n pipeConsoleToTelemetryClient(telemetry);\n // Return success\n return op.success();\n },\n //\n async start(params) {\n try {\n // Retrieve the agent definition\n const [errIndex, buildIndex] = await importServerBuild({\n projectDirectory: process.cwd(),\n noCache: true,\n });\n if (errIndex) return op.failure(errIndex);\n const build = buildIndex?.[params.name as keyof typeof buildIndex];\n if (!build)\n return op.failure({ code: \"NotFound\", message: `Agent '${params.name}' not found.` });\n\n // Obtain the final agent config\n const [errConfig, config] = prepareAgentConfig(\n build.definition.config,\n build.globalConfigs,\n );\n if (errConfig) return op.failure(errConfig);\n\n // Create the agent server\n const [errCreate, instance] = op.attempt(\n () =>\n new AgentServer({\n id: params.id,\n definition: build.definition,\n scope: params.scope,\n sha: build.sha,\n config: config.server,\n pluginsContexts: params.pluginsContexts,\n isRestart: params.isRestart,\n }),\n );\n if (errCreate) return op.failure(errCreate);\n agentServer = instance;\n\n // Start the agent server\n const [err] = await agentServer.start();\n if (err) return op.failure(err);\n\n // Stream plugin context changes to parent (for restoration after crash)\n for (const pluginDef of build.definition.plugins) {\n const [errAccessor, accessor] =\n agentServer.plugins[pluginDef.name]?.getAccessor({\n type: \"server\",\n name: pluginDef.name,\n }) ??\n op.failure({ code: \"NotFound\", message: `Plugin '${pluginDef.name}' not found.` });\n if (errAccessor) return op.failure(errAccessor);\n accessor.context.onChange(\n (c) => c,\n async (c) => {\n if (!agentServer) return;\n const [error] = await rpc.syncContext({\n agentId: agentServer.id,\n pluginName: pluginDef.name,\n context: c as SerializableValue,\n timestamp: Date.now(),\n });\n if (error)\n telemetry?.log.error({\n message: `Failed to sync for plugin '${pluginDef.name}' in agent '${agentServer.def.name}' process.`,\n error,\n });\n },\n );\n }\n\n // Register transport room\n await agentServer.transport.joinRoom(params.transportRoom.name, params.transportRoom.token);\n\n // Notify parent that agent is ready\n await rpc.ready();\n\n // Return that the agent server was started successfully\n telemetry?.log.info({ message: \"Agent started successfully.\" });\n return op.success();\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n },\n\n async stop() {\n try {\n if (agentServer) {\n const [err] = await agentServer.stop();\n if (err) return op.failure(err);\n agentServer = null;\n }\n return op.success();\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n },\n\n // Simple ping to check if process is responsive\n async ping() {\n return await op.success();\n },\n\n // Get detailed stats from the child process\n async getProcessStats() {\n try {\n return await processStats.get();\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n },\n },\n {\n post: (data) => process.send?.(data),\n on: (fn) => process.on(\"message\", fn),\n serialize: (data) => {\n const [error, result] = canon.serialize(data);\n if (error) {\n throw lifeError({\n code: \"Validation\",\n message:\n \"Failed to serialize data from agent process to server. The message has been discarded.\",\n attributes: {\n agentId: agentServer?.id,\n agentName: agentServer?.def.name,\n data,\n },\n cause: error,\n });\n }\n return result;\n },\n deserialize: (data) => {\n const [error, result] = canon.deserialize(data);\n if (error) {\n throw lifeError({\n code: \"Validation\",\n message:\n \"Failed to deserialize data from agent process to server. The message has been discarded.\",\n attributes: {\n agentId: agentServer?.id,\n agentName: agentServer?.def.name,\n data,\n },\n cause: error,\n });\n }\n return result;\n },\n onFunctionError: (error) => {\n telemetry?.log.error(\n isLifeError(error)\n ? error\n : lifeError({\n code: \"Unknown\",\n cause: error,\n }),\n );\n },\n onGeneralError: (error) => {\n telemetry?.log.error(\n isLifeError(error)\n ? error\n : lifeError({\n code: \"Unknown\",\n cause: error,\n }),\n );\n },\n // Disable Birpc timeout\n onTimeoutError: () => true,\n timeout: -1,\n },\n);\n\n// Register telemetry consumer to forward all signals to parent\nTelemetryNodeClient.registerGlobalConsumer({\n start: async (queue: AsyncQueue<TelemetrySignal>) => {\n for await (const signal of queue) rpc.syncTelemetry(signal);\n },\n});\n\n// Handle uncaught errors\nprocess.on(\"uncaughtException\", async (error) => {\n telemetry?.log.error({ error });\n // Flush telemetry before exiting to ensure error is sent to parent\n await telemetry?.flushConsumers(1000);\n process.exit(1);\n});\nprocess.on(\"unhandledRejection\", async (reason) => {\n telemetry?.log.error({\n message: reason instanceof Error ? reason.message : String(reason),\n error: reason instanceof Error ? reason : undefined,\n });\n // Flush telemetry before exiting to ensure error is sent to parent\n await telemetry?.flushConsumers(1000);\n process.exit(1);\n});\n","import z from \"zod\";\nimport { type EOUProvider, eouProviders } from \"@/models/eou\";\nimport { type LLMProvider, llmProviders } from \"@/models/llm\";\nimport { type STTProvider, sttProviders } from \"@/models/stt\";\nimport { type TTSProvider, ttsProviders } from \"@/models/tts\";\nimport { type VADProvider, vadProviders } from \"@/models/vad\";\nimport { PluginServer } from \"@/plugins/server/class\";\nimport type { PluginDefinition } from \"@/plugins/server/types\";\nimport type { SerializableValue } from \"@/shared/canon\";\nimport { lifeError } from \"@/shared/error\";\nimport * as op from \"@/shared/operation\";\nimport type { Todo } from \"@/shared/types\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { createTelemetryClient } from \"@/telemetry/clients/node\";\nimport { TransportNodeClient } from \"@/transport/client/node\";\nimport type { agentServerConfig } from \"./config\";\nimport type { AgentDefinition, AgentScope } from \"./types\";\n\nexport class AgentServer {\n readonly def: AgentDefinition;\n readonly id: string;\n readonly sha: string;\n readonly config: z.output<typeof agentServerConfig.schema>;\n readonly transport: TransportNodeClient;\n readonly storage = null;\n readonly models: {\n vad: InstanceType<VADProvider>;\n stt: InstanceType<STTProvider>;\n eou: InstanceType<EOUProvider>;\n llm: InstanceType<LLMProvider>;\n tts: InstanceType<TTSProvider>;\n };\n readonly plugins: Record<string, PluginServer<PluginDefinition>> = {};\n readonly scope: AgentScope<AgentDefinition[\"scope\"]>;\n readonly isRestart: boolean;\n readonly telemetry: TelemetryClient;\n readonly #initialPluginsContexts: Record<string, SerializableValue>;\n\n constructor({\n id,\n sha,\n definition,\n scope,\n config,\n pluginsContexts,\n isRestart,\n }: {\n id: string;\n sha: string;\n definition: AgentDefinition;\n config: z.output<typeof agentServerConfig.schema>;\n scope?: AgentScope<AgentDefinition[\"scope\"]>;\n isRestart?: boolean;\n pluginsContexts?: Record<string, SerializableValue>;\n }) {\n this.id = id;\n this.sha = sha;\n this.def = definition;\n this.scope = scope ?? this.def.scope.schema.parse({});\n this.config = config;\n this.isRestart = isRestart ?? false;\n this.#initialPluginsContexts = pluginsContexts ?? {};\n\n // Initialize telemetry\n this.telemetry = createTelemetryClient(\"agent.server\", {\n agentId: id,\n agentSha: this.sha,\n agentName: this.def.name,\n agentConfig: this.config,\n transportProviderName: this.config.transport.provider,\n llmProviderName: this.config.models.llm.provider,\n sttProviderName: this.config.models.stt.provider,\n eouProviderName: this.config.models.eou.provider,\n ttsProviderName: this.config.models.tts.provider,\n vadProviderName: this.config.models.vad.provider,\n });\n\n // Initialize transport\n this.transport = new TransportNodeClient({\n config: this.config.transport,\n obfuscateErrors: true,\n telemetry: this.telemetry,\n });\n\n // Initialize storage\n // TODO\n\n // Initialize models\n const vadProvider = vadProviders[this.config.models.vad.provider];\n const sttProvider = sttProviders[this.config.models.stt.provider];\n const eouProvider = eouProviders[this.config.models.eou.provider];\n const llmProvider = llmProviders[this.config.models.llm.provider];\n const ttsProvider = ttsProviders[this.config.models.tts.provider];\n this.models = {\n vad: new vadProvider.class(this.config.models.vad),\n stt: new sttProvider.class(this.config.models.stt),\n eou: new eouProvider.class(this.config.models.eou as Todo),\n llm: new llmProvider.class(this.config.models.llm as Todo),\n tts: new ttsProvider.class(this.config.models.tts),\n };\n\n // Validate plugins\n this.#initializePlugins();\n }\n\n #initializePlugins() {\n // Validate plugins have unique names\n const pluginNames = this.def.plugins.map((p) => p.name);\n const duplicates = pluginNames.filter((name, index) => pluginNames.indexOf(name) !== index);\n if (duplicates.length > 0) {\n const uniqueDuplicates = [...new Set(duplicates)];\n throw lifeError({\n code: \"Validation\",\n message: `Two or more plugins are named \"${uniqueDuplicates.join('\", \"')}\". Plugin names must be unique. (agent: '${this.def.name}')`,\n });\n }\n\n // Validate plugin dependencies\n for (const plugin of this.def.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 throw lifeError({\n code: \"Validation\",\n message: `Plugin '${plugin.name}' depends on plugin '${dependency.name}', but '${dependency.name}' is not registered. (agent: '${this.def.name}')`,\n });\n }\n }\n }\n\n // Expose 'has-plugin-server' via RPC\n this.transport.register({\n name: \"agent.has-plugin-server\",\n schema: {\n input: z.object({ pluginName: z.string() }),\n output: z.object({ hasServer: z.boolean() }),\n },\n execute: ({ pluginName }) => {\n const plugin = this.plugins?.[pluginName];\n if (!plugin) return op.success({ hasServer: false });\n return op.success({ hasServer: true });\n },\n });\n }\n\n async start() {\n return await this.telemetry.trace(\"AgentServer.start()\", async () => {\n try {\n // Create plugin servers\n for (const definition of this.def.plugins) {\n // Parse the initial context\n const { error: errorContext, data: parsedContext } = definition.context.schema.safeParse(\n this.#initialPluginsContexts?.[definition.name] ?? {},\n );\n if (errorContext) {\n return op.failure({\n code: \"Validation\",\n message: `Invalid initial context value for plugin '${definition.name}'.`,\n cause: errorContext,\n });\n }\n\n // Parse the config\n const { error: errorConfig, data: parsedConfig } = definition.config.schema.safeParse(\n this.def.pluginConfigs?.[definition.name] ?? {},\n );\n if (errorConfig) {\n return op.failure({\n code: \"Validation\",\n message: `Invalid config value for plugin '${definition.name}'.`,\n cause: errorConfig,\n });\n }\n\n const [errCreate, instance] = op.attempt(\n () =>\n new PluginServer({\n agent: this,\n definition,\n config: parsedConfig,\n context: parsedContext as SerializableValue,\n }),\n );\n if (errCreate) return op.failure(errCreate);\n this.plugins[definition.name] = instance;\n }\n\n // Start all plugin servers\n const result = await Promise.all(Object.values(this.plugins).map((p) => p.start()));\n const err = result.find((r) => r[0])?.[0];\n if (err) return op.failure(err);\n\n // Return that the agent server was started successfully\n return op.success();\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n });\n }\n\n async stop() {\n return await this.telemetry.trace(\"AgentServer.stop()\", async (span) => {\n try {\n // Stop all plugins\n await Promise.all(\n Object.entries(this.plugins).map(([pluginId, plugin]) =>\n plugin\n .stop()\n .catch((error) =>\n span.log.error({ message: `Error stopping plugin ${pluginId}:`, error }),\n ),\n ),\n );\n\n // Disconnect transport\n const [errLeave] = await this.transport.leaveRoom();\n if (errLeave) return op.failure(errLeave);\n\n // Return that the agent server was stopped successfully\n return op.success();\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n }\n });\n }\n}\n\n// notify: (\n// { emit, context },\n// params: {\n// source: \"user\" | \"application\";\n// behavior: \"inform\" | \"interrupt\" | \"decide\";\n// message: string;\n// },\n// ) => {\n// // Insert the notification message\n// const message = `${params.source === \"user\" ? \"User\" : \"Application\"} update: ${params.message}`;\n// emit({\n// type: \"operation.message\",\n// data: {\n// id: generateId(),\n// role: params.source === \"user\" ? \"user\" : \"system\",\n// message,\n// },\n// });\n\n// // If the behavior is \"discrete\", return\n// if (params.behavior === \"inform\") return;\n// // Else, if the behavior is interrupt, run continue\n// else if (params.behavior === \"interrupt\")\n// emit({\n// type: \"operation.continue\",\n// data: {\n// messages: context.messages,\n// insertPolicy: \"abrupt-interrupt\",\n// allowInterruption: true,\n// },\n// });\n// // Else, if the behavior is decide, decide whether to make the notification interrupt or not\n// else if (params.behavior === \"decide\") {\n// emit({\n// type: \"operation.decide\",\n// data: { messages: [], insertPolicy: \"abrupt-interrupt\", allowInterruption: true },\n// });\n// }\n// },\n// ask: ({ emit, context }, message: string) => {\n// emit({\n// type: \"operation.message\",\n// data: { id: generateId(), role: \"user\", message: message },\n// });\n// emit({ type: \"operation.continue\", data: { messages: context.messages } });\n// },\n// prompt: ({ emit, context }, message: string) => {\n// emit({\n// type: \"operation.message\",\n// data: { id: generateId(), role: \"system\", message: message },\n// });\n// emit({ type: \"operation.continue\", data: { messages: context.messages } });\n// },\n","import z from \"zod\";\nimport type { AgentServer } from \"@/agent/server/class\";\nimport {\n type PluginAccessor,\n type PluginConfig,\n type PluginContext,\n type PluginContextListener,\n type PluginDefinition,\n type PluginDependenciesAccessor,\n type PluginEvent,\n type PluginEventSource,\n type PluginEventsListener,\n type PluginEventsSelector,\n type PluginHandlerDefinition,\n type PluginInternalEvent,\n pluginEventInputSchema,\n} from \"@/plugins/server/types\";\nimport { AsyncQueue } from \"@/shared/async-queue\";\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 { RollingBuffer } from \"@/shared/rolling-buffer\";\nimport type { Any, Todo } from \"@/shared/types\";\nimport type { TelemetryClient } from \"@/telemetry/clients/base\";\nimport { createTelemetryClient } from \"@/telemetry/clients/node\";\n\nexport class PluginServer<const PluginDef extends PluginDefinition> {\n readonly def: PluginDef;\n readonly agent: AgentServer;\n readonly config: PluginConfig<PluginDef[\"config\"], \"output\">;\n context: PluginContext<PluginDef[\"context\"], \"output\">;\n readonly telemetry: TelemetryClient;\n readonly queue = new AsyncQueue<PluginEvent<PluginDef[\"events\"], \"output\">>();\n readonly eventsListeners = new Map<string, PluginEventsListener>();\n readonly contextListeners = new Map<string, PluginContextListener>();\n readonly streamHandlersQueues: AsyncQueue<PluginEvent<PluginDef[\"events\"], \"output\">>[] = [];\n readonly externalInterceptHandlers: {\n dependent: PluginServer<PluginDefinition>;\n handler: PluginHandlerDefinition;\n }[] = [];\n readonly #internalAccessor: PluginAccessor<\n PluginDef,\n { type: \"server\"; name: PluginDef[\"name\"] }\n >;\n readonly #waitProcessingResolvers = new Map<string, Set<() => void>>();\n readonly #waitResultResolvers = new Map<\n string,\n Map<string, Set<(result: op.OperationResult<unknown>) => void>>\n >();\n readonly #waitProcessingHistory = new RollingBuffer<string>(1000);\n readonly #waitResultHistory = new RollingBuffer<{\n eventId: string;\n handlerName: string;\n result: op.OperationResult<unknown>;\n }>(1000);\n readonly #handlersStates = new Map<string, unknown>();\n\n constructor({\n agent,\n definition,\n config,\n context = {},\n }: {\n agent: AgentServer;\n definition: PluginDef;\n config: PluginConfig<PluginDef[\"config\"], \"input\">;\n context: SerializableValue;\n }) {\n this.def = definition;\n this.agent = agent;\n\n // Validate config\n const { error: errConfig, data: parsedConfig } = definition.config.schema.safeParse(config);\n if (errConfig)\n throw lifeError({\n code: \"Validation\",\n message: `Invalid config provided to plugin server '${definition.name}'.`,\n cause: errConfig,\n });\n this.config = parsedConfig as PluginConfig<PluginDef[\"config\"], \"output\">;\n\n // Validate context\n const { error: errContext, data: parsedContext } = definition.context.schema.safeParse(context);\n if (errContext)\n throw lifeError({\n code: \"Validation\",\n message: `Invalid context provided to plugin server '${definition.name}'.`,\n cause: errContext,\n });\n this.context = parsedContext as PluginContext<PluginDef[\"context\"], \"output\">;\n\n // Initialize telemetry\n this.telemetry = createTelemetryClient(\"plugin.server\", {\n agentId: agent.id,\n agentSha: agent.sha,\n agentName: agent.def.name,\n agentConfig: agent.config,\n transportProviderName: agent.config.transport.provider,\n llmProviderName: agent.config.models.llm.provider,\n sttProviderName: agent.config.models.stt.provider,\n eouProviderName: agent.config.models.eou.provider,\n ttsProviderName: agent.config.models.tts.provider,\n vadProviderName: agent.config.models.vad.provider,\n pluginName: definition.name,\n pluginServerConfig: this.config,\n });\n\n // Initialize client RPC interface\n this.#initClientRPC();\n\n // Instantiate an internal accessor instance to be used outside of handlers\n const [errAccessor, accessor] = this.getAccessor({\n type: \"server\",\n name: this.def.name,\n });\n if (errAccessor) throw errAccessor;\n this.#internalAccessor = accessor;\n }\n\n /**\n * Produces an accessor object, exposing methods to interact safely with the plugin\n * from a given source (plugin, handler, client).\n * @param source - The source accessing the plugin.\n * @param handlerAccess - The access mode for the handler.\n * @returns An accessor object.\n */\n getAccessor<Source extends PluginEventSource, HandlerAccess extends \"read\" | \"write\" = \"read\">(\n source: Source,\n handlerAccess?: HandlerAccess,\n ) {\n // config\n const [errClone, clonedConfig] = op.attempt(() => deepClone(this.config));\n if (errClone) return op.failure(errClone);\n\n // context.onChange()\n const contextOnChange = ((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<PluginDef, { type: \"server\"; name: string }>[\"context\"][\"onChange\"];\n\n // context.get()\n const contextGet = (() => op.attempt(() => deepClone(this.context))) satisfies PluginAccessor<\n PluginDef,\n { type: \"server\"; name: string }\n >[\"context\"][\"get\"];\n\n // context.set()\n const contextSet = ((valueOrUpdater) => {\n // Snapshot the old context value\n const [errOld, oldContext] = op.attempt(() => deepClone(this.context));\n if (errOld) return op.failure(errOld);\n\n // Set the new context value\n if (typeof valueOrUpdater === \"function\") this.context = valueOrUpdater(oldContext);\n else this.context = valueOrUpdater;\n\n Promise.all([\n // Notify context change listeners\n Array.from(this.contextListeners.values()).map(async (listener) => {\n try {\n const newSelectedValue = listener.selector(this.context) as SerializableValue;\n const oldSelectedValue = listener.selector(oldContext) as SerializableValue;\n // Only call if value actually changed\n const [errEqual, equal] = canon.equal(newSelectedValue, oldSelectedValue);\n if (errEqual) return op.failure(errEqual);\n if (equal) await listener.callback(deepClone(this.context), deepClone(oldContext));\n } catch (error) {\n this.telemetry.log.error({\n message: `Error while notifying context listeners in plugin '${this.def.name}'.`,\n error,\n });\n }\n }),\n // Send new context value updates via RPC\n this.agent.transport.call({\n name: `plugin.${this.def.name}.context.changed`,\n schema: {\n input: z.object({ value: z.any(), timestamp: z.number() }),\n },\n input: { value: this.context, timestamp: Date.now() },\n }),\n ]);\n\n return op.success();\n }) satisfies PluginAccessor<\n PluginDef,\n { type: \"server\"; name: string },\n \"write\"\n >[\"context\"][\"set\"];\n\n // events.emit()\n const eventsEmit = ((event) => {\n // Validate event shape\n const { error: errEvent, data: parsedEvent } = pluginEventInputSchema.safeParse(event);\n if (errEvent)\n return op.failure({\n code: \"Validation\",\n message: \"Invalid event shape for event.\",\n cause: errEvent,\n });\n\n // If not internal, ensure the event is not an internal event\n const isInternal = source.type === \"server\" && source.name === this.def.name;\n if (!isInternal && parsedEvent.name.startsWith(\"plugin.\"))\n return op.failure({\n code: \"Validation\",\n message: \"Internal events cannot be emitted from outside the plugin.\",\n });\n\n // Ensure the event type exists\n const eventDef = this.def.events?.find((e) => e.name === parsedEvent.name);\n if (!eventDef)\n return op.failure({\n code: \"Validation\",\n message: `Event of type '${parsedEvent.name}' not found.`,\n });\n\n // Validate the event data\n let parsedData: unknown | null = null;\n if (eventDef.dataSchema) {\n const { error: errData, data } = eventDef.dataSchema.safeParse(parsedEvent.data);\n parsedData = data;\n if (errData)\n return op.failure({\n code: \"Validation\",\n message: `Invalid event data shape for '${parsedEvent.name}' event.`,\n cause: errData,\n });\n }\n\n // Generate an id for the event\n const outputEvent: PluginEvent<PluginDef[\"events\"], \"output\"> = {\n id: newId(\"event\"),\n name: parsedEvent.name,\n urgent: parsedEvent.urgent,\n data: parsedData as Any,\n created: { at: Date.now(), by: source },\n edited: false,\n dropped: false,\n contextChanges: [],\n };\n\n // Append to queue\n if (outputEvent.urgent) this.queue.pushFirst(outputEvent);\n else this.queue.push(outputEvent);\n\n // Return the id\n return op.success(outputEvent.id);\n }) satisfies PluginAccessor<PluginDef, { type: \"server\"; name: string }>[\"events\"][\"emit\"];\n\n // events.on()\n const eventsOn = ((selector, callback, includeDropped = false) => {\n const id = newId(\"listener\");\n this.eventsListeners.set(id, { id, callback, selector, includeDropped });\n return op.success(() => this.eventsListeners.delete(id));\n }) satisfies PluginAccessor<PluginDef, { type: \"server\"; name: string }>[\"events\"][\"on\"];\n\n // events.once()\n const eventsOnce = ((selector, callback, includeDropped = false) => {\n const [errOn, unsubscribe] = eventsOn(\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<PluginDef, { type: \"server\"; name: string }>[\"events\"][\"once\"];\n\n // events.waitForProcessing()\n const eventsWaitForProcessing = (async (eventId) => {\n // Holds resolver, and a cleanup function\n let resolver: (() => void) | undefined;\n const cleanupResolver = () => {\n if (!resolver) return;\n this.#waitProcessingResolvers.get(eventId)?.delete(resolver);\n if (this.#waitProcessingResolvers.get(eventId)?.size === 0)\n this.#waitProcessingResolvers.delete(eventId);\n };\n\n try {\n // If the event was already processed recently, return success immediately\n if (this.#waitProcessingHistory.get().includes(eventId)) return op.success();\n\n // Else register a resolver, and wait until resolve or timeout\n const resolverPromise = new Promise<op.OperationResult<void>>(\n (r) => (resolver = () => void r(op.success())),\n );\n if (!resolver) throw new Error(\"Resolver not found. Shouldn't happen.\");\n if (!this.#waitProcessingResolvers.has(eventId))\n this.#waitProcessingResolvers.set(eventId, new Set());\n this.#waitProcessingResolvers.get(eventId)?.add(resolver);\n\n const timeoutPromise = new Promise<op.OperationResult<void>>((resolve) => {\n setTimeout(() => {\n resolve(\n op.failure({\n code: \"Timeout\",\n message: `Waiting for event '${eventId}' processing timed out.`,\n isPublic: true,\n }),\n );\n }, 15_000);\n });\n\n const [err] = await Promise.race([resolverPromise, timeoutPromise]);\n\n // Return the error if any\n if (err) return op.failure(err);\n\n // Append the event id to the processing history\n this.#waitProcessingHistory.add(eventId);\n\n // Return success\n return op.success();\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n } finally {\n cleanupResolver();\n }\n }) satisfies PluginAccessor<\n PluginDef,\n { type: \"server\"; name: string }\n >[\"events\"][\"waitForProcessing\"];\n\n // events.waitForResult()\n const eventsWaitForResult = (async (eventId, handlerName) => {\n // Holds resolver, and a cleanup function\n let resolver: ((result: op.OperationResult<Any>) => void) | undefined;\n const cleanupResolver = () => {\n if (!resolver) return;\n this.#waitResultResolvers.get(eventId)?.get(handlerName)?.delete(resolver);\n if (this.#waitResultResolvers.get(eventId)?.get(handlerName)?.size === 0)\n this.#waitResultResolvers.get(eventId)?.delete(handlerName);\n if (this.#waitResultResolvers.get(eventId)?.size === 0)\n this.#waitResultResolvers.delete(eventId);\n };\n\n try {\n // If the event was already processed recently, return result immediately\n const historyItem = this.#waitResultHistory\n .get()\n .find((h) => h.eventId === eventId && h.handlerName === handlerName);\n if (historyItem) return historyItem.result;\n\n // Else register a resolver and wait until resolve or timeout\n const resolverPromise = new Promise<op.OperationResult<Any>>((r) => (resolver = r));\n if (!resolver) throw new Error(\"Resolver not found. Shouldn't happen.\");\n if (!this.#waitResultResolvers.has(eventId))\n this.#waitResultResolvers.set(eventId, new Map());\n if (!this.#waitResultResolvers.get(eventId)?.get(handlerName))\n this.#waitResultResolvers.get(eventId)?.set(handlerName, new Set());\n this.#waitResultResolvers.get(eventId)?.get(handlerName)?.add(resolver);\n\n const timeoutPromise = new Promise<op.OperationResult<void>>((resolve) => {\n setTimeout(() => {\n resolve(\n op.failure({\n code: \"Timeout\",\n message: `Waiting for event '${eventId}' result timed out.`,\n isPublic: true,\n }),\n );\n }, 15_000);\n });\n\n const result = await Promise.race([resolverPromise, timeoutPromise]);\n\n // Append the result to the history\n this.#waitResultHistory.add({ eventId, handlerName, result });\n\n // Return result\n return result;\n } catch (error) {\n return op.failure({ code: \"Unknown\", cause: error });\n } finally {\n cleanupResolver();\n }\n }) satisfies PluginAccessor<\n PluginDef,\n { type: \"server\"; name: string }\n >[\"events\"][\"waitForResult\"];\n\n if (source.type === \"handler\") {\n return op.success({\n config: clonedConfig,\n context: {\n get: contextGet,\n ...(handlerAccess === \"write\" ? { set: contextSet } : {}),\n onChange: contextOnChange,\n },\n events: {\n emit: eventsEmit,\n waitForResult: eventsWaitForResult,\n waitForProcessing: eventsWaitForProcessing,\n },\n } as unknown as PluginAccessor<PluginDef, Source, HandlerAccess>);\n }\n\n if (source.type === \"server\") {\n return op.success({\n config: clonedConfig,\n context: {\n get: contextGet,\n onChange: contextOnChange,\n },\n events: {\n emit: eventsEmit,\n on: eventsOn,\n once: eventsOnce,\n waitForResult: eventsWaitForResult,\n waitForProcessing: eventsWaitForProcessing,\n },\n } as unknown as PluginAccessor<PluginDef, Source, HandlerAccess>);\n }\n\n if (source.type === \"client\") {\n return op.success({\n config: clonedConfig,\n context: {\n get: contextGet,\n onChange: contextOnChange,\n },\n events: {\n emit: eventsEmit,\n on: eventsOn,\n once: eventsOnce,\n waitForResult: eventsWaitForResult,\n waitForProcessing: eventsWaitForProcessing,\n },\n } as unknown as PluginAccessor<PluginDef, Source, HandlerAccess>);\n }\n\n return op.failure({\n code: \"NotFound\",\n message: `Unknown source type '${(source as { type: string }).type}'.`,\n });\n }\n\n /**\n * Produces a map of dependencies' accessors, see `getAccessor()`.\n * @param source - The source accessing the dependencies.\n * @returns A map of dependencies' accessors.\n */\n getDependenciesAccessors<Source extends PluginEventSource>(source: Source) {\n const dependenciesAccessors = {} as PluginDependenciesAccessor<\n PluginDef[\"dependencies\"],\n Source\n >;\n for (const dependencyDef of this.def.dependencies ?? []) {\n const depServer = this.agent.plugins?.[toMethodName(dependencyDef.name)];\n if (!depServer)\n return op.failure({\n code: \"NotFound\",\n message: `Failed to obtain server instance for dependency plugin '${dependencyDef.name}'. Shouldn't happen.`,\n });\n const [errAccessor, accessor] = depServer.getAccessor(source);\n if (errAccessor) return op.failure(errAccessor);\n // @ts-expect-error\n dependenciesAccessors[dependencyDef.name] = accessor;\n }\n\n return op.success(dependenciesAccessors);\n }\n\n async start() {\n return await this.telemetry.trace(`plugin.${this.def.name}.start()`, async () => {\n // 1. Initialize 'stream()' handlers sub-queues\n const streamHandlers = this.def.handlers.filter((h) => h.mode === \"stream\");\n for (const handler of streamHandlers) {\n const queue = new AsyncQueue<PluginEvent<PluginDef[\"events\"], \"output\">>();\n this.streamHandlersQueues.push(queue);\n (async () => {\n for await (const event of queue) await this.#executeHandler(handler, event);\n })();\n }\n\n // 2. Register 'intercept' handlers onto dependency plugins\n const interceptHandlers = this.def.handlers.filter((h) => h.mode === \"intercept\");\n for (const handler of interceptHandlers) {\n for (const dependencyDef of this.def.dependencies ?? []) {\n const dependency = this.agent.plugins?.[dependencyDef.name];\n if (!dependency) {\n this.telemetry.log.error(\n lifeError({\n code: \"NotFound\",\n message: `Failed to obtain server instance for dependency plugin '${dependencyDef.name}'. Shouldn't happen.`,\n }),\n );\n continue;\n }\n dependency.externalInterceptHandlers.push({\n dependent: this as unknown as PluginServer<PluginDefinition>,\n handler,\n });\n }\n }\n\n // 3. Run the synchronous event loop with 'block' handlers and remote 'intercept' handlers\n (async () => {\n for await (const event of this.queue) {\n try {\n // Run external 'intercept' handlers\n for (const { handler, dependent } of this.externalInterceptHandlers) {\n // biome-ignore lint/performance/noAwaitInLoops: sequential execution expected here\n await this.#executeHandler(handler, event, dependent, (result) => {\n if (result.type === \"drop\") {\n if (event.dropped)\n event.dropped = {\n at: Date.now(),\n by: {\n plugin: this.def.name,\n handler: handler.name,\n },\n reason: result.reason,\n };\n } else if (result.type === \"next\") {\n const [errClone, oldData] = op.attempt(() => deepClone(event.data));\n if (errClone) return op.failure(errClone);\n if (!event.edited) event.edited = [];\n event.edited.push({\n at: Date.now(),\n by: {\n plugin: this.def.name,\n handler: handler.name,\n },\n reason: result.reason,\n dataBefore: oldData,\n dataAfter: result.data,\n });\n event.data = result.data;\n }\n });\n if (event.dropped) break;\n }\n\n // If the event was not dropped\n if (!event.dropped) {\n // Run the 'block' handlers\n const blockHandlers = this.def.handlers.filter((h) => h.mode === \"block\");\n for (const handler of blockHandlers) {\n // Take a snapshot of the context data before executing the handler\n const [errOld, oldContext] = op.attempt(() => deepClone(this.context));\n if (errOld) {\n this.telemetry.log.error({ error: errOld });\n continue;\n }\n // Execute the handler\n // biome-ignore lint/performance/noAwaitInLoops: sequential execution expected here\n await this.#executeHandler(handler, event);\n // If the context has changed, record the change\n const [errEqual, equal] = canon.equal(\n this.context as SerializableValue,\n oldContext as SerializableValue,\n );\n if (errEqual) {\n this.telemetry.log.error({ error: errEqual });\n continue;\n }\n if (!equal) {\n if (!event.contextChanges) event.contextChanges = [];\n event.contextChanges.push({\n at: Date.now(),\n byHandler: handler.name,\n valueBefore: oldContext,\n valueAfter: this.context,\n });\n }\n }\n // Feed the 'stream' handlers' queues\n for (const queue of this.streamHandlersQueues) queue.push(event);\n }\n\n // Notify events listeners\n await Promise.all(\n Array.from(this.eventsListeners.values()).map(\n async ({ id, callback, selector, includeDropped }) => {\n // If the event matches the listener selector\n if (this.#eventMatchesSelector(event, selector)) {\n // And the event was not dropped or the listener includes dropped events\n if (!event.dropped || includeDropped) {\n // If remote, send the event via RPC\n if (callback === \"remote\") {\n const [err] = await this.agent.transport.call({\n name: `plugin.${this.def.name}.events.callback`,\n schema: {\n input: z.object({\n listenerId: z.string(),\n event: pluginEventInputSchema,\n }),\n },\n input: { listenerId: id, event },\n });\n if (err)\n this.telemetry.log.error({\n message: `Error while steaming event to remote listener in plugin '${this.def.name}'.`,\n error: err,\n });\n }\n // Else, call the local callback\n else await callback(event);\n }\n }\n },\n ),\n );\n\n // Resolve processing waiters\n const waiters = this.#waitProcessingResolvers.get(event.id) ?? [];\n for (const resolver of waiters) resolver();\n } catch (error) {\n this.telemetry.log.error({\n message: `Unknown error while executing event in plugin '${this.def.name}'.`,\n error,\n });\n }\n }\n })();\n\n // 4. Start the queue (send the 'plugin.start' event)\n const [errEmit, eventId] = this.#emitInternalEvent({\n name: \"plugin.start\",\n data: { isRestart: this.agent.isRestart, restartCount: 0 },\n });\n if (errEmit) return op.failure(errEmit);\n const [errWaitForResult] = await this.#internalAccessor.events.waitForProcessing(eventId);\n if (errWaitForResult) return op.failure(errWaitForResult);\n\n return op.success();\n });\n }\n\n async stop() {\n return await this.telemetry.trace(`plugin.${this.def.name}.stop()`, async () => {\n // 1. Send the 'plugin.stop' event at the front of the queue\n const [errEmit, eventId] = this.#emitInternalEvent({ name: \"plugin.stop\", urgent: true });\n if (errEmit) return op.failure(errEmit);\n const [errWaitForResult] = await this.#internalAccessor.events.waitForProcessing(eventId);\n if (errWaitForResult) return op.failure(errWaitForResult);\n\n // 2. Stop the main queue and 'stream' handlers queues\n this.queue.stop();\n for (const queue of this.streamHandlersQueues) queue.stop();\n\n // 3. Clear handlers state\n this.#handlersStates.clear();\n\n // 4. Return that the plugin was stopped successfully\n return op.success();\n });\n }\n\n #getHandlerState(\n handler: PluginDef[\"handlers\"][number],\n accessor: PluginAccessor<\n PluginDef,\n { type: \"handler\"; plugin: string; handler: string; event: string }\n >,\n ) {\n const savedState = this.#handlersStates.get(handler.name);\n if (savedState) return savedState;\n const initialState =\n typeof handler.state === \"function\"\n ? handler.state({ config: accessor.config })\n : (handler.state ?? {});\n this.#handlersStates.set(handler.name, initialState);\n return initialState;\n }\n\n #emitInternalEvent(event: PluginInternalEvent<\"input\">) {\n // Any is used here, because emit() type excludes internal events\n const [errEmit, eventId] = this.#internalAccessor.events.emit(event as Any);\n if (errEmit) return op.failure(errEmit);\n return op.success(eventId);\n }\n\n async #executeHandler<Event extends PluginEvent<PluginDef[\"events\"], \"output\">>(\n handler: PluginDef[\"handlers\"][number],\n event: Event,\n dependency?: PluginServer<PluginDefinition>,\n onInterceptResult?: (\n result:\n | { type: \"drop\"; reason: string }\n | { type: \"next\"; data: Event[\"data\"]; reason: string },\n ) => void,\n ) {\n // Run the handler onEvent function\n const result = await (async () => {\n const [errClone, clonedEvent] = op.attempt(() => deepClone(event));\n if (errClone) return op.failure(errClone);\n const handlerAccess = handler.mode === \"block\" ? \"write\" : \"read\";\n const [errAccessor, accessor] = this.getAccessor(\n {\n type: \"handler\",\n plugin: this.def.name,\n handler: handler.name,\n event: event.name,\n },\n handlerAccess,\n );\n if (errAccessor) return op.failure(errAccessor);\n const state = this.#getHandlerState(handler, accessor);\n const [errDependency, dependencyAccessor] = dependency\n ? dependency.getAccessor({ type: \"server\", name: dependency.def.name })\n : op.success({});\n if (errDependency) return op.failure(errDependency);\n const [errDependencies, dependenciesAccessor] = this.getDependenciesAccessors({\n type: \"handler\",\n plugin: this.def.name,\n handler: handler.name,\n event: event.name,\n });\n if (errDependencies) return op.failure(errDependencies);\n const drop = (reason: string) => onInterceptResult?.({ type: \"drop\", reason });\n const next = (data: PluginEvent<PluginDef[\"events\"], \"output\">[\"data\"], reason: string) =>\n onInterceptResult?.({ type: \"next\", data, reason });\n return await this.telemetry.trace(\n `plugin.${this.def.name}.handler.${handler.name}`,\n async (span) =>\n op.attempt(\n async () =>\n await handler.onEvent({\n event: clonedEvent,\n plugin: op.toPublic(accessor),\n state,\n telemetry: span,\n agent: op.toPublic(this.agent),\n ...(handler.mode === \"intercept\"\n ? { drop, next, dependency: dependencyAccessor }\n : { dependencies: dependenciesAccessor }),\n } as Todo),\n ),\n );\n })();\n\n // Log the error if any, and emit a plugin.error event\n if (result[0]) {\n this.telemetry.log.error({\n message: `Error while executing ${handler.mode} handler '${handler.name}' in plugin '${this.def.name}'.`,\n error: result[0],\n });\n this.#emitInternalEvent({ name: \"plugin.error\", data: { error: result[0], event } });\n }\n\n // Resolve result waiters\n const waiters = this.#waitResultResolvers.get(event.id)?.get(handler.name) ?? [];\n for (const resolver of waiters) resolver(result);\n }\n\n #initClientRPC() {\n const [errAccessor, accessor] = this.getAccessor({\n type: \"client\",\n name: this.def.name,\n });\n if (errAccessor) return op.failure(errAccessor);\n\n // Expose context via RPC\n this.agent.transport.register({\n name: `plugin.${this.def.name}.context.get`,\n schema: { output: z.object().loose() },\n execute: () => op.success({ value: this.context, timestamp: Date.now() }),\n });\n\n // Expose 'events.emit()' via RPC\n this.agent.transport.register({\n name: `plugin.${this.def.name}.events.emit`,\n schema: {\n input: pluginEventInputSchema,\n output: z.object({ id: z.string() }),\n },\n execute: async (input) => {\n const [err, id] = await accessor.events.emit(input as Any);\n if (err) return op.failure(err);\n return op.success({ id });\n },\n });\n\n // Setup events subscriptions via RPC\n this.agent.transport.register({\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 execute: (input) => {\n const { listenerId, selector, includeDropped } = input;\n this.eventsListeners.set(listenerId, {\n id: listenerId,\n callback: \"remote\",\n selector,\n includeDropped,\n });\n return op.success();\n },\n });\n this.agent.transport.register({\n name: `plugin.${this.def.name}.events.unsubscribe`,\n schema: { input: z