refactor: consolidate agent/editor plugins under integrations/ (#5491)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Kartik
2026-06-12 10:31:35 +05:30
committed by GitHub
co-authored by Claude
parent c676c2c458
commit 2c796d144f
295 changed files with 94 additions and 81 deletions
+41
View File
@@ -0,0 +1,41 @@
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";
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;
buildAddOptions: (userIdOverride?: string, runId?: string, sessionKey?: string) => AddOptions;
buildSearchOptions: (userIdOverride?: string, limit?: number, runId?: string, sessionKey?: string) => SearchOptions;
getCurrentSessionId: () => string | undefined;
skillsActive: boolean;
captureToolEvent: (toolName: string, properties: Record<string, unknown>) => void;
}
export function registerAllTools(deps: ToolDeps): void {
const { api } = deps;
const nonOptional = { optional: false };
api.registerTool(createMemorySearchTool(deps), nonOptional);
api.registerTool(createMemoryAddTool(deps), nonOptional);
api.registerTool(createMemoryGetTool(deps), nonOptional);
api.registerTool(createMemoryListTool(deps), nonOptional);
api.registerTool(createMemoryUpdateTool(deps), nonOptional);
api.registerTool(createMemoryDeleteTool(deps), nonOptional);
api.registerTool(createMemoryEventListTool(deps), nonOptional);
api.registerTool(createMemoryEventStatusTool(deps), nonOptional);
}
+105
View File
@@ -0,0 +1,105 @@
import { Type } from "@sinclair/typebox";
import type { AddOptions } from "../types.ts";
import { isSubagentSession } from "../isolation.ts";
import { isNoiseMessage, stripNoiseFromContent } from "../filtering.ts";
// v3.0.0: resolveCategories/ttlToExpirationDate removed - expiration_date/immutable no longer supported
import type { ToolDeps } from "./index.ts";
export function createMemoryAddTool(deps: ToolDeps) {
const { api, provider, resolveUserId, getCurrentSessionId, buildAddOptions, buildSearchOptions, skillsActive } = deps;
return {
name: "memory_add",
label: "Memory Add",
description: "Save important information in long-term memory via Mem0. Use for preferences, facts, decisions, and anything worth remembering.",
parameters: Type.Object({
text: Type.Optional(Type.String({ description: "Single fact to remember" })),
facts: Type.Optional(Type.Array(Type.String(), { description: "Array of facts to store. ALL must share the same category." })),
category: Type.Optional(Type.String({ description: 'Category: "identity", "preference", "decision", "rule", "project", "configuration", "technical", "relationship"' })),
importance: Type.Optional(Type.Number({ description: "Importance (0.0-1.0), omit for category default" })),
userId: Type.Optional(Type.String({ description: "User ID to scope this memory" })),
agentId: Type.Optional(Type.String({ description: "Agent ID namespace" })),
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Additional metadata" })),
longTerm: Type.Optional(Type.Boolean({ description: "Long-term (default: true). Set false for session-scoped." })),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const p = params as {
text?: string; facts?: string[]; category?: string; importance?: number;
userId?: string; agentId?: string; metadata?: Record<string, unknown>; longTerm?: boolean;
};
const rawFacts: string[] = p.facts?.length ? p.facts : (p.text ? [p.text] : []);
if (rawFacts.length === 0) {
return { content: [{ type: "text", text: "No facts provided. Pass 'text' or 'facts' array." }], details: { error: "missing_facts" } };
}
// Filter out noise and clean the facts before storing
const allFacts = rawFacts
.map((f) => stripNoiseFromContent(f))
.filter((f) => f.length > 0 && !isNoiseMessage(f));
if (allFacts.length === 0) {
return { content: [{ type: "text", text: "All provided facts were filtered as noise. Nothing stored." }], details: { error: "all_noise" } };
}
const start = Date.now();
try {
const currentSessionId = getCurrentSessionId();
if (isSubagentSession(currentSessionId)) {
return { content: [{ type: "text", text: "Memory storage is not available in subagent sessions." }], details: { error: "subagent_blocked" } };
}
const uid = resolveUserId({ agentId: p.agentId, userId: p.userId });
const runId = !(p.longTerm ?? true) && currentSessionId ? currentSessionId : undefined;
if (skillsActive) {
const rawMetadata = p.metadata;
const category = p.category ?? rawMetadata?.category as string | undefined;
const importance = p.importance ?? rawMetadata?.importance as number | undefined;
const parsedMetadata: Record<string, unknown> = {
...(rawMetadata ?? {}),
...(category && { category }),
...(importance !== undefined && { importance }),
};
const addOpts: AddOptions = {
user_id: uid, source: "OPENCLAW", infer: false,
deduced_memories: allFacts, metadata: parsedMetadata ?? {},
};
if (runId) addOpts.run_id = runId;
const result = await provider.add([{ role: "user", content: allFacts.join("\n") }], addOpts);
const count = result.results?.length ?? 0;
api.logger.info(`openclaw-mem0: stored ${count} memor${count === 1 ? "y" : "ies"} (infer=false, category=${category ?? "none"})`);
deps.captureToolEvent("memory_add", { success: true, latency_ms: Date.now() - start, fact_count: allFacts.length, mode: "skills" });
return {
content: [{ type: "text", text: `Stored ${allFacts.length} fact(s) [${category ?? "uncategorized"}]: ${allFacts.map(f => `"${f.slice(0, 60)}${f.length > 60 ? "..." : ""}"`).join(", ")}` }],
details: { action: "stored", mode: "skills", category, factCount: allFacts.length, results: result.results },
};
}
const combinedText = allFacts.join("\n");
const result = await provider.add([{ role: "user", content: combinedText }], buildAddOptions(uid, runId, currentSessionId));
const added = result.results?.filter((r) => r.event === "ADD") ?? [];
const updated = result.results?.filter((r) => r.event === "UPDATE") ?? [];
const summary = [];
if (added.length > 0) summary.push(`${added.length} added`);
if (updated.length > 0) summary.push(`${updated.length} updated`);
if (summary.length === 0) summary.push("No new memories extracted");
deps.captureToolEvent("memory_add", { success: true, latency_ms: Date.now() - start, fact_count: allFacts.length });
return {
content: [{ type: "text", text: `Stored: ${summary.join(", ")}. ${result.results?.map((r) => `[${r.event}] ${r.memory}`).join("; ") ?? ""}` }],
details: { action: "stored", results: result.results },
};
} catch (err) {
deps.captureToolEvent("memory_add", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory add failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
@@ -0,0 +1,76 @@
import { Type } from "@sinclair/typebox";
import { isSubagentSession } from "../isolation.ts";
import type { ToolDeps } from "./index.ts";
export function createMemoryDeleteTool(deps: ToolDeps) {
const { api, provider, resolveUserId, getCurrentSessionId, buildSearchOptions } = deps;
return {
name: "memory_delete",
label: "Memory Delete",
description: "Delete memories. Provide memoryId, query to search-and-delete, or all:true for bulk deletion (requires confirm:true).",
parameters: Type.Object({
memoryId: Type.Optional(Type.String({ description: "Specific memory ID to delete" })),
query: Type.Optional(Type.String({ description: "Search query to find and delete" })),
agentId: Type.Optional(Type.String({ description: "Agent ID to scope deletion" })),
all: Type.Optional(Type.Boolean({ description: "Delete ALL memories. Requires confirm: true." })),
confirm: Type.Optional(Type.Boolean({ description: "Safety gate for bulk operations" })),
userId: Type.Optional(Type.String({ description: "User ID scope" })),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId, query, agentId, all, confirm, userId } = params as {
memoryId?: string; query?: string; agentId?: string;
all?: boolean; confirm?: boolean; userId?: string;
};
const start = Date.now();
try {
if (isSubagentSession(getCurrentSessionId())) {
return { content: [{ type: "text", text: "Memory deletion is not available in subagent sessions." }], details: { error: "subagent_blocked" } };
}
if (memoryId) {
await provider.delete(memoryId);
deps.captureToolEvent("memory_delete", { success: true, latency_ms: Date.now() - start, delete_mode: "single" });
return { content: [{ type: "text", text: `Memory ${memoryId} deleted.` }], details: { action: "deleted", id: memoryId } };
}
if (query) {
const uid = resolveUserId({ agentId, userId });
const results = await provider.search(query, buildSearchOptions(uid, 5));
if (!results || results.length === 0) {
return { content: [{ type: "text", text: "No matching memories found." }], details: { found: 0 } };
}
if (results.length === 1 || (results[0].score ?? 0) > 0.9) {
await provider.delete(results[0].id);
return { content: [{ type: "text", text: `Deleted: "${results[0].memory}"` }], details: { action: "deleted", id: results[0].id } };
}
const list = results.map((r) =>
`- [${r.id}] ${r.memory.slice(0, 80)}${r.memory.length > 80 ? "..." : ""} (${((r.score ?? 0) * 100).toFixed(0)}%)`
).join("\n");
return {
content: [{ type: "text", text: `Found ${results.length} candidates. Specify memoryId:\n${list}` }],
details: { action: "candidates", candidates: results.map((r) => ({ id: r.id, memory: r.memory, score: r.score })) },
};
}
if (all) {
if (!confirm) {
return { content: [{ type: "text", text: "Bulk deletion requires confirm: true." }], details: { error: "confirmation_required" } };
}
const uid = resolveUserId({ agentId, userId });
await provider.deleteAll(uid);
deps.captureToolEvent("memory_delete", { success: true, latency_ms: Date.now() - start, delete_mode: "all" });
api.logger.info(`openclaw-mem0: deleted all memories for user ${uid}`);
return { content: [{ type: "text", text: `All memories deleted for user "${uid}".` }], details: { action: "deleted_all", user_id: uid } };
}
return { content: [{ type: "text", text: "Provide memoryId, query, or all:true." }], details: { error: "missing_param" } };
} catch (err) {
deps.captureToolEvent("memory_delete", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory delete failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
@@ -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) },
};
}
},
};
}
@@ -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) },
};
}
},
};
}
+31
View File
@@ -0,0 +1,31 @@
import { Type } from "@sinclair/typebox";
import type { ToolDeps } from "./index.ts";
export function createMemoryGetTool(deps: ToolDeps) {
const { provider } = deps;
return {
name: "memory_get",
label: "Memory Get",
description: "Retrieve a specific memory by its ID from Mem0.",
parameters: Type.Object({
memoryId: Type.String({ description: "The memory ID to retrieve" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId } = params as { memoryId: string };
const start = Date.now();
try {
const memory = await provider.get(memoryId);
deps.captureToolEvent("memory_get", { success: true, latency_ms: Date.now() - start });
return {
content: [{ type: "text", text: `Memory ${memory.id}:\n${memory.memory}\n\nCreated: ${memory.created_at ?? "unknown"}\nUpdated: ${memory.updated_at ?? "unknown"}` }],
details: { memory },
};
} catch (err) {
deps.captureToolEvent("memory_get", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory get failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
@@ -0,0 +1,65 @@
import { Type } from "@sinclair/typebox";
import type { MemoryItem } from "../types.ts";
import type { ToolDeps } from "./index.ts";
export function createMemoryListTool(deps: ToolDeps) {
const { provider, resolveUserId, getCurrentSessionId } = deps;
return {
name: "memory_list",
label: "Memory List",
description: "List all stored memories for a user or agent.",
parameters: Type.Object({
userId: Type.Optional(Type.String({ description: "User ID (default: configured)" })),
agentId: Type.Optional(Type.String({ description: "Agent ID namespace" })),
scope: Type.Optional(
Type.Union([Type.Literal("session"), Type.Literal("long-term"), Type.Literal("all")], {
description: 'Scope: "all" (default), "session", or "long-term"',
}),
),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { userId, agentId, scope = "all" } = params as {
userId?: string; agentId?: string; scope?: "session" | "long-term" | "all";
};
const start = Date.now();
try {
let memories: MemoryItem[] = [];
const uid = resolveUserId({ agentId, userId });
const currentSessionId = getCurrentSessionId();
if (scope === "session") {
if (currentSessionId) memories = await provider.getAll({ user_id: uid, run_id: currentSessionId, source: "OPENCLAW" });
} else if (scope === "long-term") {
memories = await provider.getAll({ user_id: uid, source: "OPENCLAW" });
} else {
const longTerm = await provider.getAll({ user_id: uid, source: "OPENCLAW" });
let session: MemoryItem[] = [];
if (currentSessionId) session = await provider.getAll({ user_id: uid, run_id: currentSessionId, source: "OPENCLAW" });
const seen = new Set(longTerm.map((r) => r.id));
memories = [...longTerm, ...session.filter((r) => !seen.has(r.id))];
}
deps.captureToolEvent("memory_list", { success: true, latency_ms: Date.now() - start, result_count: memories.length });
if (!memories || memories.length === 0) {
return { content: [{ type: "text", text: "No memories stored yet." }], details: { count: 0 } };
}
const text = memories.map((r, i) => `${i + 1}. ${r.memory} (id: ${r.id})`).join("\n");
return {
content: [{ type: "text", text: `${memories.length} memories:\n\n${text}` }],
details: {
count: memories.length,
memories: memories.map((r) => ({ id: r.id, memory: r.memory, categories: r.categories, created_at: r.created_at })),
},
};
} catch (err) {
deps.captureToolEvent("memory_list", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory list failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
@@ -0,0 +1,87 @@
import { Type } from "@sinclair/typebox";
import type { MemoryItem, SearchOptions } from "../types.ts";
import type { ToolDeps } from "./index.ts";
export function createMemorySearchTool(deps: ToolDeps) {
const { cfg, provider, resolveUserId, buildSearchOptions, getCurrentSessionId } = deps;
return {
name: "memory_search",
label: "Memory Search",
description: "Search through long-term memories stored in Mem0.",
parameters: Type.Object({
query: Type.String({ description: "Search query" }),
limit: Type.Optional(Type.Number({ description: `Max results (default: ${cfg.topK})` })),
userId: Type.Optional(Type.String({ description: "User ID to scope search" })),
agentId: Type.Optional(Type.String({ description: "Agent ID to search a specific agent's memories" })),
scope: Type.Optional(
Type.Union([Type.Literal("session"), Type.Literal("long-term"), Type.Literal("all")], {
description: 'Scope: "all" (default), "session", or "long-term"',
}),
),
categories: Type.Optional(Type.Array(Type.String(), { description: "Filter by category" })),
filters: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Advanced filters" })),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const {
query, limit, userId, agentId, scope = "all",
categories: filterCategories, filters: agentFilters,
} = params as {
query: string; limit?: number; userId?: string; agentId?: string;
scope?: "session" | "long-term" | "all"; categories?: string[];
filters?: Record<string, unknown>;
};
const start = Date.now();
try {
let results: MemoryItem[] = [];
const uid = resolveUserId({ agentId, userId });
const currentSessionId = getCurrentSessionId();
const applyFilters = (opts: SearchOptions): SearchOptions => {
if (filterCategories?.length) opts.categories = filterCategories;
if (agentFilters) opts.filters = agentFilters;
return opts;
};
if (scope === "session") {
if (currentSessionId) {
results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, undefined, currentSessionId)));
}
} else if (scope === "long-term") {
results = await provider.search(query, applyFilters(buildSearchOptions(uid, limit)));
} else {
const longTerm = await provider.search(query, applyFilters(buildSearchOptions(uid, limit)));
let session: MemoryItem[] = [];
if (currentSessionId) {
session = await provider.search(query, applyFilters(buildSearchOptions(uid, limit, undefined, currentSessionId)));
}
const seen = new Set(longTerm.map((r) => r.id));
results = [...longTerm, ...session.filter((r) => !seen.has(r.id))];
}
deps.captureToolEvent("memory_search", { success: true, latency_ms: Date.now() - start, result_count: results.length });
if (!results || results.length === 0) {
return { content: [{ type: "text", text: "No relevant memories found." }], details: { count: 0 } };
}
const text = results.map((r, i) =>
`${i + 1}. ${r.memory} (score: ${((r.score ?? 0) * 100).toFixed(0)}%, id: ${r.id})`
).join("\n");
return {
content: [{ type: "text", text: `Found ${results.length} memories:\n\n${text}` }],
details: {
count: results.length,
memories: results.map((r) => ({ id: r.id, memory: r.memory, score: r.score, categories: r.categories, created_at: r.created_at })),
},
};
} catch (err) {
deps.captureToolEvent("memory_search", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory search failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}
@@ -0,0 +1,36 @@
import { Type } from "@sinclair/typebox";
import { isSubagentSession } from "../isolation.ts";
import type { ToolDeps } from "./index.ts";
export function createMemoryUpdateTool(deps: ToolDeps) {
const { api, provider, getCurrentSessionId } = deps;
return {
name: "memory_update",
label: "Memory Update",
description: "Update an existing memory's text in place. Atomic and preserves history.",
parameters: Type.Object({
memoryId: Type.String({ description: "The memory ID to update" }),
text: Type.String({ description: "The new text (replaces old)" }),
}),
async execute(_toolCallId: string, params: Record<string, unknown>) {
const { memoryId, text } = params as { memoryId: string; text: string };
const start = Date.now();
try {
if (isSubagentSession(getCurrentSessionId())) {
return { content: [{ type: "text", text: "Memory update is not available in subagent sessions." }], details: { error: "subagent_blocked" } };
}
await provider.update(memoryId, text);
deps.captureToolEvent("memory_update", { success: true, latency_ms: Date.now() - start });
return {
content: [{ type: "text", text: `Updated memory ${memoryId}: "${text.slice(0, 80)}${text.length > 80 ? "..." : ""}"` }],
details: { action: "updated", id: memoryId },
};
} catch (err) {
deps.captureToolEvent("memory_update", { success: false, latency_ms: Date.now() - start, error: String(err) });
return { content: [{ type: "text", text: `Memory update failed: ${String(err)}` }], details: { error: String(err) } };
}
},
};
}