refactor: add memory event tools, update SDK types, CLI hooks, and provider config

This commit is contained in:
kartik-mem0
2026-04-06 13:53:42 +05:30
parent 458576dd0c
commit 87157c1fe7
14 changed files with 251 additions and 34 deletions
+1 -2
View File
@@ -1420,9 +1420,8 @@ export function registerCliCommands(
});
},
{
commands: ["mem0"],
descriptors: [
{ name: "mem0", description: "Mem0 memory plugin commands" },
{ name: "mem0", description: "Mem0 memory plugin commands", hasSubcommands: true },
],
},
);
+6 -6
View File
@@ -16,6 +16,7 @@
* - Dual mode: platform or open-source (self-hosted)
*/
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type {
@@ -90,12 +91,10 @@ export { createProvider } from "./providers.ts";
// Plugin Definition
// ============================================================================
const memoryPlugin = {
const memoryPlugin = definePluginEntry({
id: "openclaw-mem0",
name: "Memory (Mem0)",
description: "Mem0 memory backend — Mem0 platform or self-hosted open-source",
kind: "memory" as const,
configSchema: mem0ConfigSchema,
register(api: OpenClawPluginApi) {
// Read auth from openclaw.json plugin config (picks up post-startup login).
@@ -234,6 +233,7 @@ const memoryPlugin = {
api,
provider,
cfg,
backend,
resolveUserId: _resolveUserId,
effectiveUserId: _effectiveUserId,
agentUserId: _agentUserId,
@@ -301,7 +301,7 @@ const memoryPlugin = {
},
});
},
};
});
// ============================================================================
// Lifecycle Hook Registration
@@ -582,11 +582,11 @@ function registerHooks(
// Track last seen session ID to detect actual new sessions (not every turn)
let lastRecallSessionId: string | undefined;
// Auto-recall: inject relevant memories before agent starts
// Auto-recall: inject relevant memories before prompt is built
if (cfg.autoRecall) {
const RECALL_TIMEOUT_MS = 8_000;
api.on("before_agent_start", async (event: any, ctx: any) => {
api.on("before_prompt_build", async (event: any, ctx: any) => {
if (!event.prompt || event.prompt.length < 5) return;
// Skip non-interactive triggers (cron, heartbeat, automation)
+30 -3
View File
@@ -9,19 +9,46 @@ declare module "openclaw/plugin-sdk" {
};
resolvePath(p: string): string;
registerTool(
definition: Record<string, unknown>,
metadata?: Record<string, unknown>,
definition: {
name: string;
description: string;
parameters: unknown;
execute: (
toolCallId: string,
params: Record<string, unknown>,
) => Promise<{ content: Array<{ type: string; text: string }>; [key: string]: unknown }>;
[key: string]: unknown;
},
metadata?: { optional?: boolean; [key: string]: unknown },
): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(
handler: (context: { program: any }) => void,
options?: Record<string, unknown>,
): void;
registerCommand?(definition: Record<string, unknown>): void;
registerService(service: {
id: string;
start: () => void;
start: (...args: any[]) => void;
stop: () => void;
}): void;
[key: string]: unknown;
}
}
declare module "openclaw/plugin-sdk/plugin-entry" {
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
export interface PluginEntry {
id: string;
name: string;
description?: string;
register(api: OpenClawPluginApi): void;
}
export function definePluginEntry<T extends PluginEntry>(entry: T): T;
}
declare module "openclaw/plugin-sdk/core" {
export * from "openclaw/plugin-sdk";
}
+1 -1
View File
@@ -7,7 +7,7 @@
"contracts": {
"tools": [
"memory_search", "memory_add", "memory_get", "memory_list",
"memory_update", "memory_delete"
"memory_update", "memory_delete", "memory_event_list", "memory_event_status"
]
},
"providerAuthEnvVars": {
+30 -18
View File
@@ -81,6 +81,7 @@ class PlatformProvider implements Mem0Provider {
constructor(
private readonly apiKey: string,
private readonly baseUrl?: string,
private readonly orgId?: string,
private readonly projectId?: string,
) {}
@@ -97,11 +98,17 @@ class PlatformProvider implements Mem0Provider {
private async _init(): Promise<void> {
const { default: MemoryClient } = await import("mem0ai");
const opts: { apiKey: string; org_id?: string; project_id?: string } = {
const opts: {
apiKey: string;
host?: string;
organizationId?: string;
projectId?: string;
} = {
apiKey: this.apiKey,
};
if (this.orgId) opts.org_id = this.orgId;
if (this.projectId) opts.project_id = this.projectId;
if (this.baseUrl) opts.host = this.baseUrl;
if (this.orgId) opts.organizationId = this.orgId;
if (this.projectId) opts.projectId = this.projectId;
this.client = new MemoryClient(opts);
}
@@ -133,20 +140,11 @@ class PlatformProvider implements Mem0Provider {
async search(query: string, options: SearchOptions): Promise<MemoryItem[]> {
await this.ensureClient();
// Base filters: always scope by user_id, optionally by run_id
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
// Merge agent-provided filters (created_at ranges, metadata, etc.)
// with base filters. Agent filters extend, never override user scoping.
const mergedFilters = options.filters
? { AND: [baseFilters, options.filters] }
: baseFilters;
const opts: Record<string, unknown> = {
api_version: "v2",
filters: mergedFilters,
user_id: options.user_id,
};
if (options.run_id) opts.run_id = options.run_id;
if (options.top_k != null) opts.top_k = options.top_k;
if (options.threshold != null) opts.threshold = options.threshold;
if (options.keyword_search != null)
@@ -155,6 +153,14 @@ class PlatformProvider implements Mem0Provider {
if (options.filter_memories != null)
opts.filter_memories = options.filter_memories;
if (options.categories != null) opts.categories = options.categories;
const baseFilters: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) baseFilters.run_id = options.run_id;
if (options.filters) {
opts.filters = { AND: [baseFilters, options.filters] };
} else {
opts.filters = baseFilters;
}
const results = await this.client.search(query, opts);
return normalizeSearchResults(results);
@@ -168,10 +174,16 @@ class PlatformProvider implements Mem0Provider {
async getAll(options: ListOptions): Promise<MemoryItem[]> {
await this.ensureClient();
const opts: Record<string, unknown> = { user_id: options.user_id };
if (options.run_id) opts.run_id = options.run_id;
const opts: Record<string, unknown> = {
api_version: "v2",
user_id: options.user_id,
filters: { user_id: options.user_id },
};
if (options.run_id) {
opts.run_id = options.run_id;
(opts.filters as Record<string, unknown>).run_id = options.run_id;
}
if (options.page_size != null) opts.page_size = options.page_size;
if (options.source) opts.source = options.source;
const results = await this.client.getAll(opts);
if (Array.isArray(results)) return results.map(normalizeMemoryItem);
@@ -419,7 +431,7 @@ export function createProvider(
);
}
return new PlatformProvider(cfg.apiKey!, cfg.orgId, cfg.projectId);
return new PlatformProvider(cfg.apiKey!, cfg.baseUrl, cfg.orgId, cfg.projectId);
}
// ============================================================================
+7
View File
@@ -0,0 +1,7 @@
/**
* Test shim for openclaw/plugin-sdk/plugin-entry.
* At runtime this is resolved from the OpenClaw gateway.
*/
export function definePluginEntry<T>(entry: T): T {
return entry;
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Test shim for openclaw/plugin-sdk.
* At runtime this is resolved from the OpenClaw gateway.
*/
export interface OpenClawPluginApi {
pluginConfig: Record<string, unknown>;
logger: {
info(msg: string): void;
warn(msg: string): void;
error(msg: string): void;
debug(msg: string): void;
};
resolvePath(p: string): string;
registerTool(definition: Record<string, unknown>, metadata?: Record<string, unknown>): void;
on(event: string, handler: (event: any, ctx: any) => any): void;
registerCli(handler: (context: { program: any }) => void, options?: Record<string, unknown>): void;
registerService(service: { id: string; start: (...args: any[]) => void; stop: () => void }): void;
[key: string]: unknown;
}
+8 -2
View File
@@ -15,6 +15,8 @@ import { createMemoryGetTool } from "../tools/memory-get.ts";
import { createMemoryDeleteTool } from "../tools/memory-delete.ts";
import { createMemoryListTool } from "../tools/memory-list.ts";
import { createMemoryUpdateTool } from "../tools/memory-update.ts";
import { createMemoryEventListTool } from "../tools/memory-event-list.ts";
import { createMemoryEventStatusTool } from "../tools/memory-event-status.ts";
// ---------------------------------------------------------------------------
// Mock helper
@@ -79,10 +81,10 @@ function createMockToolDeps(overrides = {}): ToolDeps {
// ---------------------------------------------------------------------------
describe("registerAllTools", () => {
it("calls api.registerTool exactly 6 times", () => {
it("calls api.registerTool exactly 8 times", () => {
const ctx = createMockToolDeps();
registerAllTools(ctx);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(6);
expect(ctx.api.registerTool).toHaveBeenCalledTimes(8);
});
it("registers tools with the correct names", () => {
@@ -102,6 +104,8 @@ describe("registerAllTools", () => {
"memory_list",
"memory_update",
"memory_delete",
"memory_event_list",
"memory_event_status",
]);
});
@@ -127,6 +131,8 @@ describe("tool factory shape", () => {
{ fn: createMemoryGetTool, expectedName: "memory_get" },
{ fn: createMemoryDeleteTool, expectedName: "memory_delete" },
{ fn: createMemoryListTool, expectedName: "memory_list" },
{ fn: createMemoryEventListTool, expectedName: "memory_event_list" },
{ fn: createMemoryEventStatusTool, expectedName: "memory_event_status" },
];
for (const { fn, expectedName } of factories) {
+6
View File
@@ -1,5 +1,6 @@
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import type { Mem0Config, Mem0Provider, AddOptions, SearchOptions } from "../types.ts";
import type { Backend } from "../backend/base.ts";
import { createMemorySearchTool } from "./memory-search.ts";
import { createMemoryAddTool } from "./memory-add.ts";
@@ -7,11 +8,14 @@ import { createMemoryGetTool } from "./memory-get.ts";
import { createMemoryListTool } from "./memory-list.ts";
import { createMemoryUpdateTool } from "./memory-update.ts";
import { createMemoryDeleteTool } from "./memory-delete.ts";
import { createMemoryEventListTool } from "./memory-event-list.ts";
import { createMemoryEventStatusTool } from "./memory-event-status.ts";
export interface ToolDeps {
api: OpenClawPluginApi;
provider: Mem0Provider;
cfg: Mem0Config;
backend?: Backend;
resolveUserId: (opts: { agentId?: string; userId?: string }) => string;
effectiveUserId: (sessionKey?: string) => string;
agentUserId: (id: string) => string;
@@ -31,4 +35,6 @@ export function registerAllTools(deps: ToolDeps): void {
api.registerTool(createMemoryListTool(deps));
api.registerTool(createMemoryUpdateTool(deps));
api.registerTool(createMemoryDeleteTool(deps));
api.registerTool(createMemoryEventListTool(deps));
api.registerTool(createMemoryEventStatusTool(deps));
}
+60
View File
@@ -0,0 +1,60 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryEventListTool(deps: ToolDeps) {
return {
name: "memory_event_list",
label: "Memory Event List",
description:
"List recent background processing events from the Mem0 Platform. Use to check whether memory operations (add, update, delete) were processed successfully.",
parameters: Type.Object({}),
async execute(_toolCallId: string, _params: Record<string, unknown>) {
const start = Date.now();
try {
if (!deps.backend) {
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: 0, error: "not_platform" });
return {
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
details: { error: "not_platform" },
};
}
const results = await deps.backend.listEvents();
if (!results.length) {
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: 0 });
return {
content: [{ type: "text", text: "No events found." }],
details: { count: 0 },
};
}
const rows = results.map((ev) => {
const evId = String(ev.id ?? "");
const evType = String(ev.event_type ?? "—");
const status = String(ev.status ?? "—");
const latency =
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
return { id: evId, type: evType, status, latency, created };
});
const text = rows
.map((r) => `- ${r.id} | ${r.type} | ${r.status} | ${r.latency} | ${r.created}`)
.join("\n");
deps.captureToolEvent("memory_event_list", { success: true, latency_ms: Date.now() - start, count: results.length });
return {
content: [{ type: "text", text: `${results.length} event(s):\n${text}` }],
details: { count: results.length, events: rows },
};
} catch (err) {
deps.captureToolEvent("memory_event_list", { success: false, latency_ms: Date.now() - start, error: String(err) });
return {
content: [{ type: "text", text: `Failed to list events: ${String(err)}` }],
details: { error: String(err) },
};
}
},
};
}
+63
View File
@@ -0,0 +1,63 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryEventStatusTool(deps: ToolDeps) {
return {
name: "memory_event_status",
label: "Memory Event Status",
description:
"Get detailed status of a specific background processing event. Use to verify whether a memory add/update/delete was processed, view latency, and inspect results.",
parameters: Type.Object({
event_id: Type.String({ description: "The event ID to check" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { event_id: eventId } = params as { event_id: string };
const start = Date.now();
try {
if (!deps.backend) {
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: 0, error: "not_platform" });
return {
content: [{ type: "text", text: "Event tracking is only available in platform mode." }],
details: { error: "not_platform" },
};
}
const ev = await deps.backend.getEvent(eventId);
const status = String(ev.status ?? "—");
const evType = String(ev.event_type ?? "—");
const latency =
typeof ev.latency === "number" ? `${Math.round(ev.latency as number)}ms` : "—";
const created = String(ev.created_at ?? "—").slice(0, 19).replace("T", " ");
const updated = String(ev.updated_at ?? "—").slice(0, 19).replace("T", " ");
let text = `Event: ${eventId}\nType: ${evType}\nStatus: ${status}\nLatency: ${latency}\nCreated: ${created}\nUpdated: ${updated}`;
const results = ev.results as Record<string, unknown>[] | undefined;
if (results && Array.isArray(results) && results.length) {
const resultLines = results.map((r) => {
const memId = String(r.id ?? "").slice(0, 8);
const data = r.data as Record<string, unknown> | undefined;
const memory = data?.memory ?? "";
const evName = String(r.event ?? "");
return `- [${evName}] ${memory} (${memId})`;
});
text += `\n\nResults (${results.length}):\n${resultLines.join("\n")}`;
}
deps.captureToolEvent("memory_event_status", { success: true, latency_ms: Date.now() - start });
return {
content: [{ type: "text", text }],
details: { event: ev },
};
} catch (err) {
deps.captureToolEvent("memory_event_status", { success: false, latency_ms: Date.now() - start, error: String(err) });
return {
content: [{ type: "text", text: `Failed to get event: ${String(err)}` }],
details: { error: String(err) },
};
}
},
};
}
+1 -1
View File
@@ -19,6 +19,6 @@
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts"],
"include": ["index.ts", "types.ts", "providers.ts", "config.ts", "filtering.ts", "isolation.ts", "openclaw-plugin-sdk.d.ts", "backend/**/*.ts", "tools/**/*.ts", "cli/**/*.ts", "skill-loader.ts", "recall.ts", "dream-gate.ts", "telemetry.ts", "fs-safe.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}
+1 -1
View File
@@ -7,5 +7,5 @@ export default defineConfig({
dts: true,
sourcemap: true,
clean: true,
external: [/^node:/, "fs", "os", "path", "url", "readline", "module"],
external: [/^node:/, /^openclaw\//, "fs", "os", "path", "url", "readline", "module"],
});
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
alias: {
// OpenClaw SDK modules are resolved from the gateway at runtime.
// During unit tests we provide lightweight stubs.
"openclaw/plugin-sdk/plugin-entry": new URL(
"./test-shims/plugin-entry.ts",
import.meta.url,
).pathname,
"openclaw/plugin-sdk": new URL(
"./test-shims/plugin-sdk.ts",
import.meta.url,
).pathname,
},
},
});