139 lines
5.3 KiB
TypeScript
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)}`;
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}
|