UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

439 lines (438 loc) 15.3 kB
import { r as normalizeOptionalString } from "./string-utils-BtCofrRx.js"; import "./embedding-defaults-BP3wPc9o.js"; import { fileURLToPath } from "node:url"; import path from "node:path"; import { fork } from "node:child_process"; //#region packages/memory-host-sdk/src/host/embedding-vectors.ts /** Replace invalid coordinates and L2-normalize non-empty vectors. */ function sanitizeAndNormalizeEmbedding(vec) { const sanitized = vec.map((value) => Number.isFinite(value) ? value : 0); const magnitude = Math.sqrt(sanitized.reduce((sum, value) => sum + value * value, 0)); if (magnitude < 1e-10) return sanitized; return sanitized.map((value) => value / magnitude); } //#endregion //#region packages/memory-host-sdk/src/host/embedding-worker-errors.ts /** Stable error codes emitted by the local embedding worker supervisor. */ const LOCAL_EMBEDDING_WORKER_ERROR_CODES = { exited: "LOCAL_EMBEDDING_WORKER_EXITED", processError: "LOCAL_EMBEDDING_WORKER_PROCESS_ERROR", ipcError: "LOCAL_EMBEDDING_WORKER_IPC_ERROR" }; new Set(Object.values(LOCAL_EMBEDDING_WORKER_ERROR_CODES)); /** Create a local embedding worker failure with stable metadata fields. */ function createLocalEmbeddingWorkerFailureError(params) { return Object.assign(new Error(params.message), { code: params.code, reason: params.reason, ...params.exitCode !== void 0 ? { exitCode: params.exitCode } : {}, ...params.signal !== void 0 ? { signal: params.signal } : {}, ...params.cause !== void 0 ? { cause: params.cause } : {} }); } //#endregion //#region packages/memory-host-sdk/src/host/embeddings-worker.ts /** Resolve the worker child script for source, package, and bundled runtime layouts. */ function resolveDefaultWorkerScriptPath() { const currentPath = fileURLToPath(import.meta.url); const extension = path.extname(currentPath); const currentName = path.basename(currentPath); const sibling = extension === ".ts" ? "embeddings-worker-child.ts" : currentName.startsWith("embeddings-worker.") ? "embeddings-worker-child.js" : "memory-core-local-embedding-worker.js"; return path.join(path.dirname(currentPath), sibling); } /** Keep only local embedding options that are safe and necessary to send over IPC. */ function serializeLocalEmbeddingOptions(options, runtimeOptions) { return { config: {}, provider: "local", model: options.model, fallback: "none", local: { ...options.local, ...runtimeOptions?.nodeLlamaCppImportUrl ? { nodeLlamaCppImportUrl: runtimeOptions.nodeLlamaCppImportUrl } : {} } }; } /** Create a typed failure for unexpected worker process exits. */ function createWorkerExitError(code, signal) { return createLocalEmbeddingWorkerFailureError({ message: `Local embedding worker exited unexpectedly (${signal ? `signal ${signal}` : `exit code ${code ?? "unknown"}`})`, code: LOCAL_EMBEDDING_WORKER_ERROR_CODES.exited, reason: signal ? "signal" : "exit", exitCode: code, signal }); } /** Convert worker response errors into Error objects while preserving worker error codes. */ function createWorkerResponseError(error) { if (typeof error.error === "object" && error.error) { const message = error.error.message || "Local embedding worker failed"; const workerError = new Error(message); if (error.error.code) workerError.code = error.error.code; return workerError; } return new Error(error.error || "Local embedding worker failed"); } const WORKER_UNSAFE_EXEC_ARGV_FLAGS = new Set(["--inspect", "--inspect-brk"]); const WORKER_UNSAFE_EXEC_ARGV_FLAGS_WITH_VALUE = new Set([ "--eval", "-e", "--print", "-p", "--input-type", "--inspect-port" ]); const WORKER_UNSAFE_EXEC_ARGV_OPTION_PREFIXES = [ "--eval=", "--print=", "--input-type=", "--inspect=", "--inspect-brk=", "--inspect-port=" ]; const WORKER_CLOSE_GRACE_MS = 250; /** Drop execArgv flags that would make forked workers debug/eval stateful or unsafe. */ function resolveWorkerExecArgv() { const args = []; let skipNext = false; for (const arg of process.execArgv) { if (skipNext) { skipNext = false; continue; } if (WORKER_UNSAFE_EXEC_ARGV_FLAGS.has(arg)) continue; if (WORKER_UNSAFE_EXEC_ARGV_FLAGS_WITH_VALUE.has(arg)) { skipNext = true; continue; } if (WORKER_UNSAFE_EXEC_ARGV_OPTION_PREFIXES.some((prefix) => arg.startsWith(prefix))) continue; args.push(arg); } return args; } /** IPC client that serializes local embedding calls through one child process. */ var LocalEmbeddingWorkerClient = class { constructor(scriptPath) { this.scriptPath = scriptPath; this.child = null; this.nextRequestId = 1; this.pending = /* @__PURE__ */ new Map(); } /** Start or reuse the child worker and initialize its provider. */ async initialize(options) { await this.send({ type: "initialize", options }); } /** Request one query embedding from the child worker. */ async embedQuery(options, text, callOptions) { const result = await this.send({ type: "embedQuery", options, text }, callOptions); return Array.isArray(result) ? result : []; } /** Request a batch of embeddings from the child worker. */ async embedBatch(options, texts, callOptions) { const result = await this.send({ type: "embedBatch", options, texts }, callOptions); return Array.isArray(result) ? result : []; } /** Ask the child to close gracefully, then force shutdown after a short grace period. */ async close() { if (!this.child) return; let timeout; const closeRequest = this.send({ type: "close" }).then(() => "closed"); const closeTimeout = new Promise((resolve) => { timeout = setTimeout(() => resolve("timeout"), WORKER_CLOSE_GRACE_MS); timeout.unref?.(); }); try { if (await Promise.race([closeRequest, closeTimeout]) === "timeout") closeRequest.catch(() => {}); } finally { if (timeout) clearTimeout(timeout); this.shutdownChild(); } } /** Ensure the child process exists and has lifecycle failure handlers installed. */ ensureChild() { if (this.child?.connected) return this.child; const child = fork(this.scriptPath, [], { execArgv: resolveWorkerExecArgv(), serialization: "json", stdio: [ "ignore", "ignore", "ignore", "ipc" ] }); child.on("message", (message) => this.handleMessage(message)); child.on("exit", (code, signal) => { if (this.child === child) this.child = null; this.rejectPending(createWorkerExitError(code, signal)); }); child.on("error", (err) => { if (this.child === child) this.child = null; this.rejectPending(createLocalEmbeddingWorkerFailureError({ message: `Local embedding worker process failed: ${err.message}`, code: LOCAL_EMBEDDING_WORKER_ERROR_CODES.processError, reason: "process-error", cause: err })); }); this.child = child; return child; } /** Send one request over IPC and bind its abort signal to child shutdown. */ async send(request, options) { options?.signal?.throwIfAborted(); const child = this.ensureChild(); const id = this.nextRequestId++; const payload = { ...request, id }; return await new Promise((resolve, reject) => { const pending = { resolve, reject }; if (options?.signal) { const abort = () => { this.pending.delete(id); this.shutdownChild(); reject(toLintErrorObject$1(options.signal?.reason ?? /* @__PURE__ */ new Error("Local embedding request aborted"), "Non-Error rejection")); }; options.signal.addEventListener("abort", abort, { once: true }); pending.abort = () => options.signal?.removeEventListener("abort", abort); } this.pending.set(id, pending); child.send(payload, (err) => { if (err) { this.pending.delete(id); pending.abort?.(); reject(createLocalEmbeddingWorkerFailureError({ message: `Local embedding worker IPC failed: ${err.message}`, code: LOCAL_EMBEDDING_WORKER_ERROR_CODES.ipcError, reason: "ipc", cause: err })); } }); }); } /** Route one worker response to the matching pending request. */ handleMessage(message) { const response = message; if (typeof response.id !== "number") return; const pending = this.pending.get(response.id); if (!pending) return; this.pending.delete(response.id); pending.abort?.(); if (response.ok) { pending.resolve(response.value); return; } pending.reject(createWorkerResponseError(response)); } /** Disconnect and kill the current child process if it is still alive. */ shutdownChild() { const child = this.child; this.child = null; if (!child) return; if (child.connected) child.disconnect(); if (!child.killed) child.kill(); } /** Reject all pending requests after child process failure. */ rejectPending(err) { const pending = [...this.pending.values()]; this.pending.clear(); for (const entry of pending) { entry.abort?.(); entry.reject(err); } } }; /** Create the public local embedding provider backed by the child worker client. */ async function createLocalEmbeddingWorkerProvider(options, runtimeOptions) { const modelPath = normalizeOptionalString(options.local?.modelPath) || "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf"; const workerOptions = serializeLocalEmbeddingOptions(options, runtimeOptions); const client = new LocalEmbeddingWorkerClient(runtimeOptions?.workerScriptPath ?? resolveDefaultWorkerScriptPath()); try { await client.initialize(workerOptions); } catch (err) { await client.close().catch(() => {}); throw err; } let closed = false; const throwIfClosed = () => { if (closed) throw new Error("Local embedding provider has been closed"); }; return { id: "local", model: modelPath, embedQuery: async (text, callOptions) => { throwIfClosed(); return await client.embedQuery(workerOptions, text, callOptions); }, embedBatch: async (texts, callOptions) => { throwIfClosed(); return await client.embedBatch(workerOptions, texts, callOptions); }, close: async () => { if (closed) return; closed = true; await client.close(); } }; } /** Convert abort reasons or arbitrary thrown values into lint-safe Error objects. */ function toLintErrorObject$1(value, fallbackMessage) { if (value instanceof Error) return value; if (typeof value === "string") return new Error(value); const error = new Error(fallbackMessage, { cause: value }); if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value); return error; } //#endregion //#region packages/memory-host-sdk/src/host/node-llama.ts const NODE_LLAMA_CPP_MODULE = "node-llama-cpp"; /** Dynamically import node-llama-cpp so the optional dependency is loaded only when needed. */ async function importNodeLlamaCpp(moduleSpecifier = NODE_LLAMA_CPP_MODULE) { return import(moduleSpecifier); } //#endregion //#region packages/memory-host-sdk/src/host/embeddings.ts async function disposeResources(resources) { let firstError; for (const resource of resources) try { await resource?.dispose?.(); } catch (err) { firstError ??= err; } if (firstError) throw toLintErrorObject(firstError, "Non-Error thrown"); } async function createLocalEmbeddingProvider(options, runtimeOptions) { return await createLocalEmbeddingWorkerProvider(options, runtimeOptions); } async function createLocalEmbeddingProviderInProcess(options) { const modelPath = normalizeOptionalString(options.local?.modelPath) || "hf:ggml-org/embeddinggemma-300m-qat-q8_0-GGUF/embeddinggemma-300m-qat-Q8_0.gguf"; const modelCacheDir = normalizeOptionalString(options.local?.modelCacheDir); const nodeLlamaCppImportUrl = normalizeOptionalString(options.local?.nodeLlamaCppImportUrl); const contextSize = options.local?.contextSize ?? 4096; const { getLlama, resolveModelFile, LlamaLogLevel } = await importNodeLlamaCpp(nodeLlamaCppImportUrl); let llama = null; let embeddingModel = null; let embeddingContext = null; let initPromise = null; let initAbortController = null; let closePromise = null; let closed = false; const throwIfClosed = () => { if (closed) throw new Error("Local embedding provider has been closed"); }; const disposeAndThrowIfClosed = async (resource) => { if (!closed) return resource; await disposeResources([resource]); throwIfClosed(); return resource; }; const ensureContext = async () => { throwIfClosed(); if (embeddingContext) return embeddingContext; if (initPromise) return initPromise; initPromise = (async () => { const abortController = new AbortController(); initAbortController = abortController; try { if (!llama) llama = await disposeAndThrowIfClosed(await getLlama({ logLevel: LlamaLogLevel.error })); if (!embeddingModel) { const resolved = await resolveModelFile(modelPath, { ...modelCacheDir ? { directory: modelCacheDir } : {}, signal: abortController.signal }); throwIfClosed(); embeddingModel = await disposeAndThrowIfClosed(await llama.loadModel({ modelPath: resolved, loadSignal: abortController.signal })); } if (!embeddingContext) embeddingContext = await disposeAndThrowIfClosed(await embeddingModel.createEmbeddingContext({ contextSize, createSignal: abortController.signal })); return embeddingContext; } catch (err) { initPromise = null; throw err; } finally { if (initAbortController === abortController) initAbortController = null; } })(); return initPromise; }; return { id: "local", model: modelPath, embedQuery: async (text, optionsValue) => { throwIfClosed(); optionsValue?.signal?.throwIfAborted(); const ctx = await ensureContext(); throwIfClosed(); optionsValue?.signal?.throwIfAborted(); const embedding = await ctx.getEmbeddingFor(text); return sanitizeAndNormalizeEmbedding(Array.from(embedding.vector)); }, embedBatch: async (texts, optionsLocal) => { throwIfClosed(); optionsLocal?.signal?.throwIfAborted(); const ctx = await ensureContext(); throwIfClosed(); optionsLocal?.signal?.throwIfAborted(); const embeddings = []; for (const text of texts) { throwIfClosed(); optionsLocal?.signal?.throwIfAborted(); const embedding = await ctx.getEmbeddingFor(text); embeddings.push(sanitizeAndNormalizeEmbedding(Array.from(embedding.vector))); } return embeddings; }, close: async () => { if (closePromise) return closePromise; closed = true; initAbortController?.abort(); initAbortController = null; closePromise = (async () => { const context = embeddingContext; const model = embeddingModel; const runtime = llama; embeddingContext = null; embeddingModel = null; llama = null; initPromise = null; await disposeResources([ context, model, runtime ]); })(); return closePromise; } }; } function toLintErrorObject(value, fallbackMessage) { if (value instanceof Error) return value; if (typeof value === "string") return new Error(value); const error = new Error(fallbackMessage, { cause: value }); if (typeof value === "object" && value !== null || typeof value === "function") Object.assign(error, value); return error; } //#endregion export { createLocalEmbeddingProviderInProcess as n, sanitizeAndNormalizeEmbedding as r, createLocalEmbeddingProvider as t };