mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
perf: lazy load memory embedding runtime
This commit is contained in:
@@ -802,7 +802,9 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
|
||||
expect(codexPluginsManifestKeys).toEqual([...CODEX_PLUGINS_CONFIG_KEYS].toSorted());
|
||||
expect(codexPluginsProperties.additionalProperties).toBe(false);
|
||||
for (const key of CODEX_PLUGINS_CONFIG_KEYS) {
|
||||
expect(manifest.uiHints[`codexPlugins.${key}`]).toBeTruthy();
|
||||
expect(manifest.uiHints[`codexPlugins.${key}`]).toMatchObject({
|
||||
label: expect.any(String),
|
||||
});
|
||||
}
|
||||
const pluginEntryProperties = (
|
||||
codexPluginsProperties.properties.plugins as {
|
||||
|
||||
@@ -9,13 +9,8 @@
|
||||
import { Buffer } from "node:buffer";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type * as LanceDB from "@lancedb/lancedb";
|
||||
import OpenAI from "openai";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import {
|
||||
getMemoryEmbeddingProvider,
|
||||
type MemoryEmbeddingProvider,
|
||||
} from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import { resolveDefaultAgentId } from "openclaw/plugin-sdk/memory-host-core";
|
||||
import type { MemoryEmbeddingProvider } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
@@ -64,6 +59,40 @@ type AutoCaptureCursor = {
|
||||
lastMessageFingerprint?: string;
|
||||
};
|
||||
|
||||
type OpenAiEmbeddingClient = {
|
||||
post<T>(
|
||||
path: string,
|
||||
options: { body: unknown; timeout?: number; maxRetries?: number },
|
||||
): Promise<T>;
|
||||
};
|
||||
|
||||
let openAiModulePromise: Promise<typeof import("openai")> | undefined;
|
||||
function loadOpenAiModule(): Promise<typeof import("openai")> {
|
||||
openAiModulePromise ??= import("openai");
|
||||
return openAiModulePromise;
|
||||
}
|
||||
|
||||
let memoryEmbeddingProviderModulePromise:
|
||||
| Promise<typeof import("openclaw/plugin-sdk/memory-core-host-engine-embeddings")>
|
||||
| undefined;
|
||||
function loadMemoryEmbeddingProviderModule(): Promise<
|
||||
typeof import("openclaw/plugin-sdk/memory-core-host-engine-embeddings")
|
||||
> {
|
||||
memoryEmbeddingProviderModulePromise ??=
|
||||
import("openclaw/plugin-sdk/memory-core-host-engine-embeddings");
|
||||
return memoryEmbeddingProviderModulePromise;
|
||||
}
|
||||
|
||||
let memoryHostCoreModulePromise:
|
||||
| Promise<typeof import("openclaw/plugin-sdk/memory-host-core")>
|
||||
| undefined;
|
||||
function loadMemoryHostCoreModule(): Promise<
|
||||
typeof import("openclaw/plugin-sdk/memory-host-core")
|
||||
> {
|
||||
memoryHostCoreModulePromise ??= import("openclaw/plugin-sdk/memory-host-core");
|
||||
return memoryHostCoreModulePromise;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
@@ -314,7 +343,7 @@ type Embeddings = {
|
||||
};
|
||||
|
||||
class OpenAiCompatibleEmbeddings implements Embeddings {
|
||||
private client: OpenAI;
|
||||
private clientPromise: Promise<OpenAiEmbeddingClient>;
|
||||
|
||||
constructor(
|
||||
apiKey: string,
|
||||
@@ -322,11 +351,13 @@ class OpenAiCompatibleEmbeddings implements Embeddings {
|
||||
baseUrl?: string,
|
||||
private dimensions?: number,
|
||||
) {
|
||||
this.client = new OpenAI({ apiKey, baseURL: baseUrl });
|
||||
this.clientPromise = loadOpenAiModule().then(
|
||||
({ default: OpenAI }) => new OpenAI({ apiKey, baseURL: baseUrl }) as OpenAiEmbeddingClient,
|
||||
);
|
||||
}
|
||||
|
||||
async embed(text: string, options?: { timeoutMs?: number }): Promise<number[]> {
|
||||
const params: OpenAI.EmbeddingCreateParams = {
|
||||
const params: Record<string, unknown> = {
|
||||
model: this.model,
|
||||
input: text,
|
||||
};
|
||||
@@ -338,7 +369,9 @@ class OpenAiCompatibleEmbeddings implements Embeddings {
|
||||
// omitted, then decodes the response. Several compatible providers either
|
||||
// reject encoding_format or always return float arrays, so use the generic
|
||||
// transport and normalize the response ourselves.
|
||||
const response = await this.client.post<EmbeddingCreateResponse>("/embeddings", {
|
||||
const response = await (
|
||||
await this.clientPromise
|
||||
).post<EmbeddingCreateResponse>("/embeddings", {
|
||||
body: params,
|
||||
...(options?.timeoutMs ? { timeout: options.timeoutMs, maxRetries: 0 } : {}),
|
||||
});
|
||||
@@ -367,10 +400,12 @@ class ProviderAdapterEmbeddings implements Embeddings {
|
||||
private async createProvider(): Promise<MemoryEmbeddingProvider> {
|
||||
const cfg = (this.api.runtime.config?.current?.() ?? this.api.config) as OpenClawConfig;
|
||||
const providerId = this.embedding.provider;
|
||||
const { getMemoryEmbeddingProvider } = await loadMemoryEmbeddingProviderModule();
|
||||
const adapter = getMemoryEmbeddingProvider(providerId, cfg);
|
||||
if (!adapter) {
|
||||
throw new Error(`Unknown memory embedding provider: ${providerId}`);
|
||||
}
|
||||
const { resolveDefaultAgentId } = await loadMemoryHostCoreModule();
|
||||
const defaultAgentId = resolveDefaultAgentId(cfg);
|
||||
const agentDir = this.api.runtime.agent.resolveAgentDir(cfg, defaultAgentId);
|
||||
const remote =
|
||||
|
||||
@@ -1463,14 +1463,14 @@ describe("slack prepareSlackMessage inbound contract", () => {
|
||||
opts: { source: "message" },
|
||||
});
|
||||
|
||||
expect(root).toBeTruthy();
|
||||
expect(followUp).toBeTruthy();
|
||||
assertPrepared(root, "root message");
|
||||
assertPrepared(followUp, "follow-up message");
|
||||
// Without the seeding fix, root would land on `agent:main:slack:channel:c0agg76cp1s`
|
||||
// while followUp would land on `:thread:<rootTs>`, splitting the conversation
|
||||
// across two sessions. Both must share one session key.
|
||||
expect(root!.ctxPayload.SessionKey).toBe(expectedSessionKey);
|
||||
expect(followUp!.ctxPayload.SessionKey).toBe(expectedSessionKey);
|
||||
expect(new Set([root!.ctxPayload.SessionKey, followUp!.ctxPayload.SessionKey]).size).toBe(1);
|
||||
expect(root.ctxPayload.SessionKey).toBe(expectedSessionKey);
|
||||
expect(followUp.ctxPayload.SessionKey).toBe(expectedSessionKey);
|
||||
expect(new Set([root.ctxPayload.SessionKey, followUp.ctxPayload.SessionKey]).size).toBe(1);
|
||||
});
|
||||
|
||||
it("treats Slack user-group mentions as explicit mentions when the bot is a member", async () => {
|
||||
|
||||
@@ -817,12 +817,6 @@ describe("resolveTelegramFetch", () => {
|
||||
const seventhDispatcher = getDispatcherFromUndiciCall(7);
|
||||
const eighthDispatcher = getDispatcherFromUndiciCall(8);
|
||||
|
||||
expect(firstDispatcher).toBeDefined();
|
||||
expect(secondDispatcher).toBeDefined();
|
||||
expect(sixthDispatcher).toBeDefined();
|
||||
expect(seventhDispatcher).toBeDefined();
|
||||
expect(eighthDispatcher).toBeDefined();
|
||||
|
||||
expect(firstDispatcher).not.toBe(secondDispatcher);
|
||||
expect(secondDispatcher).toBe(sixthDispatcher);
|
||||
expect(seventhDispatcher).toBe(firstDispatcher);
|
||||
|
||||
@@ -171,8 +171,7 @@ describe("resolveMemoryBackendConfig", () => {
|
||||
} as OpenClawConfig;
|
||||
const resolved = resolveMemoryBackendConfig({ cfg, agentId: "main" });
|
||||
const custom = resolved.qmd?.collections.find((c) => c.name.startsWith("custom-notes"));
|
||||
expect(custom).toBeDefined();
|
||||
expect(custom?.path).toBe(path.resolve("/workspace/root", "notes"));
|
||||
expect(custom).toMatchObject({ path: path.resolve("/workspace/root", "notes") });
|
||||
});
|
||||
|
||||
it("scopes qmd collection names per agent", () => {
|
||||
|
||||
@@ -44,7 +44,10 @@ describe("package withRemoteHttpResponse", () => {
|
||||
...deps,
|
||||
});
|
||||
|
||||
expect(deps.calls[0]).toBeDefined();
|
||||
expect(deps.calls[0]).not.toHaveProperty("mode");
|
||||
expect(deps.calls).toEqual([
|
||||
expect.not.objectContaining({
|
||||
mode: expect.any(String),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +124,6 @@ describe("buildSessionEntry", () => {
|
||||
// Content line 0 → JSONL line 4 (the first user message)
|
||||
// Content line 1 → JSONL line 6 (the assistant message)
|
||||
// Content line 2 → JSONL line 7 (the second user message)
|
||||
expect(entry!.lineMap).toBeDefined();
|
||||
expect(entry!.lineMap).toEqual([4, 6, 7]);
|
||||
});
|
||||
|
||||
|
||||
@@ -578,8 +578,12 @@ liveGatewayDescribe("OpenClaw SDK live Gateway e2e", () => {
|
||||
|
||||
try {
|
||||
await oc.connect();
|
||||
await expect(oc.agents.list()).resolves.toBeDefined();
|
||||
await expect(oc.models.status({ probe: false })).resolves.toBeDefined();
|
||||
await expect(oc.agents.list()).resolves.toEqual(
|
||||
expect.objectContaining({ agents: expect.any(Array) }),
|
||||
);
|
||||
await expect(oc.models.status({ probe: false })).resolves.toEqual(
|
||||
expect.objectContaining({ providers: expect.any(Array) }),
|
||||
);
|
||||
|
||||
const agent = await oc.agents.get(process.env.OPENCLAW_SDK_LIVE_AGENT_ID ?? "main");
|
||||
const run = await agent.run({
|
||||
|
||||
@@ -135,7 +135,6 @@ describe("acp translator stable lifecycle handlers", () => {
|
||||
|
||||
const result = await agent.initialize(createInitializeRequest());
|
||||
const capabilities = result.agentCapabilities;
|
||||
expect(capabilities).toBeDefined();
|
||||
if (!capabilities) {
|
||||
throw new Error("initialize response did not include agent capabilities");
|
||||
}
|
||||
|
||||
@@ -1091,10 +1091,12 @@ describe("compactEmbeddedPiSession hooks (ownsCompaction engine)", () => {
|
||||
]
|
||||
>;
|
||||
const runtimeContext = contextEngineCompactCalls[0]?.[0]?.runtimeContext;
|
||||
expect(runtimeContext).toBeDefined();
|
||||
if (!runtimeContext) {
|
||||
throw new Error("expected compaction runtime context");
|
||||
}
|
||||
|
||||
await expect(
|
||||
runtimeContext?.llm?.complete?.({
|
||||
runtimeContext.llm?.complete?.({
|
||||
messages: [{ role: "user", content: "summarize" }],
|
||||
agentId: "other-agent",
|
||||
}),
|
||||
|
||||
@@ -482,9 +482,10 @@ describe("config io write prepare", () => {
|
||||
auth: { mode: "token" },
|
||||
});
|
||||
const channels = persisted.channels as Record<string, Record<string, unknown>> | undefined;
|
||||
expect(channels?.imessage).toBeDefined();
|
||||
expect(channels?.imessage).toMatchObject({
|
||||
cliPath: "/usr/local/bin/imsg",
|
||||
});
|
||||
expect(channels?.imessage).not.toHaveProperty("runtimeOnlyDefault");
|
||||
expect(channels?.imessage?.cliPath).toBe("/usr/local/bin/imsg");
|
||||
});
|
||||
|
||||
it("does not reintroduce legacy nested dm.policy defaults in the persisted candidate", () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ describe("movePathWithCopyFallback", () => {
|
||||
}),
|
||||
).rejects.toThrow("Hardlinked source file is not allowed");
|
||||
|
||||
await expect(fs.stat(sourceFile)).resolves.toBeTruthy();
|
||||
await expect(fs.readFile(sourceFile, "utf8")).resolves.toBe("hello");
|
||||
await expect(fs.stat(targetDir)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user