Files
mem0/integrations/dsh-mem0/src/index.ts
T

139 lines
5.3 KiB
TypeScript

/**
* dsh-mem0: Mem0 long-term memory as a native DeepSeek Harness (Cordis) plugin.
*
* Registers two agent-callable tools backed by the Mem0 SDK:
* - `search_memory` recalls facts relevant to a query
* - `add_memory` stores a fact for future sessions
*
* A plugin is a Cordis module that exports `apply(ctx, config)`. Declaring
* `inject = ['tools']` holds the plugin until the harness tool registry exists;
* tools registered via `ctx.tools.register(...)` are auto-unregistered when the
* plugin unmounts (Cordis revertible effects).
*/
import type { Context } from "@deepseek-ai/cordis";
import { defineTool } from "@deepseek-ai/dsh-tools";
import { MemoryClient } from "mem0ai";
import { formatMemoryList, formatAddResult } from "./formatting.ts";
import { truncateOutput } from "./output.ts";
import { resolveSearchFilters, resolveAddParams } from "./scoping.ts";
export const name = "mem0";
export const inject = ["tools"];
// Tags writes so Mem0's backend attributes them to this integration in
// telemetry. The backend keeps recognized values via its KNOWN_EVENT_SOURCES
// allowlist; unknown values bucket into "OTHERS", so "DEEPSEEK_HARNESS" must be
// added to that allowlist for usage to surface by name (a one-line backend PR,
// same pattern as the ZAPIER / STRANDS sources).
const SOURCE = "DEEPSEEK_HARNESS";
const DEFAULT_SEARCH_LIMIT = 10;
export interface Config {
/** Mem0 API key. Defaults to the MEM0_API_KEY env var. */
apiKey?: string;
/** Default entity that owns the memories (Mem0 user scope). */
userId: string;
/** Optional Mem0 Platform base-URL override (on-prem / dedicated); defaults to api.mem0.ai. Not a switch to self-hosted OSS. */
host?: string;
}
// Both tools return a single text string; the render is identical, so lift it
// into one shared declaration instead of repeating the block per tool.
const textOutput = {
schema: { type: "string" } as const,
render: (_args: unknown, value: string) => [
{ type: "text" as const, text: value },
],
};
// Optional per-call scoping params, shared by both tools. A single harness
// install can serve more than one entity, so the model may override the
// mount-time default per call (see scoping.ts).
const scopeParams = {
userId: {
type: "string",
description:
"Entity that owns the memory. Defaults to the plugin's configured userId; set this only to read or write another user's memories.",
},
agentId: {
type: "string",
description: "Optional agent scope, to partition memories by agent.",
},
runId: {
type: "string",
description: "Optional run/session scope, to partition memories by session.",
},
} as const;
export function apply(ctx: Context, config: Config): void {
const apiKey = config.apiKey ?? process.env.MEM0_API_KEY;
if (!apiKey) {
throw new Error("dsh-mem0: set config.apiKey or the MEM0_API_KEY env var");
}
const userId = config.userId;
if (!userId) {
throw new Error("dsh-mem0: config.userId is required");
}
const client = new MemoryClient({
apiKey,
...(config.host ? { host: config.host } : {}),
});
// Recall. The platform rejects top-level entity params on search, so scope
// goes inside `filters` (unlike add below, which takes them top-level).
ctx.tools.register(
defineTool({
name: "search_memory",
description:
"Search the user's long-term Mem0 memory for facts relevant to a query. Use proactively before answering anything that may depend on what the user told you earlier.",
parameters: {
query: { type: "string", description: "What to recall.", required: true },
limit: {
type: "integer",
description: `Max results to return (default ${DEFAULT_SEARCH_LIMIT}).`,
},
...scopeParams,
},
output: textOutput,
async execute({ query, limit, userId: u, agentId, runId }) {
const filters = resolveSearchFilters({ userId: u, agentId, runId }, userId);
try {
const topK = limit && limit > 0 ? limit : DEFAULT_SEARCH_LIMIT;
const { results } = await client.search(query, { filters, topK });
return truncateOutput(formatMemoryList(results ?? []));
} catch (err) {
return `search_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
}),
);
// Write. `source` tags the memory for telemetry attribution.
ctx.tools.register(
defineTool({
name: "add_memory",
description:
"Store a fact in the user's long-term Mem0 memory for later sessions. Extraction runs asynchronously server-side, so a stored fact may take a moment to become searchable; do not immediately search to confirm the write.",
parameters: {
text: { type: "string", description: "The fact to remember.", required: true },
...scopeParams,
},
output: textOutput,
async execute({ text, userId: u, agentId, runId }) {
const addParams = resolveAddParams({ userId: u, agentId, runId }, userId);
try {
const result = await client.add([{ role: "user", content: text }], {
...addParams,
source: SOURCE,
});
return truncateOutput(formatAddResult(result));
} catch (err) {
return `add_memory failed: ${err instanceof Error ? err.message : String(err)}`;
}
},
}),
);
}