feat(agent): per-tenant Karpathy-style memory vault
User explicit ask: 'maybe with a miniature hindsight or a Karpathy
vault behind it as its memory system for each tenant.'
- src/lib/agentMemory.ts: EA2-backed vault per user. Vault root is
flow.kind=value at flow/memory_vault_<email_slug>; each memory is a
flow.kind=value doc linked via defines edge with role=presentation.
- agentMemory.remember/listAll/recall — recall is a simple keyword
overlap score with recency tiebreak.
- agentTools: new 'remember' and 'recall' tools.
matchers: 'remember that ...', 'note ...', 'save ...' /
'recall ...', 'remind me ...', 'what do you know about ...'
- routeAgentInput: every user turn is best-effort persisted as an
episodic memory (fire-and-forget). When no command matches, the
router does a vault recall and surfaces related notes before
showing the help message.
Vault key: deterministic per-user. Owner_email stored on the vault
root + on each memory's config.memory. No raw IDs visible to the user.
This commit is contained in:
@@ -2,6 +2,7 @@ import { api } from "./api";
|
||||
import { wizardApi } from "./wizardApi";
|
||||
import { chatApi } from "./chatApi";
|
||||
import { curatedPublishedFlows, fetchStartableFlows } from "./flowCuration";
|
||||
import { agentMemory, type Memory } from "./agentMemory";
|
||||
|
||||
export interface ToolResult {
|
||||
ok: boolean;
|
||||
@@ -151,11 +152,41 @@ const TOOLS: ToolDef[] = [
|
||||
return { ok: true, display: `Here's what's published:\n${lines}` };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "remember",
|
||||
description: "Stash a note in your per-tenant memory vault",
|
||||
matcher: /^(remember|note|save)\s+(that\s+)?(.+)$/i,
|
||||
async run(input, ctx) {
|
||||
const m = input.match(/^(remember|note|save)\s+(that\s+)?(.+)$/i);
|
||||
const content = m?.[3]?.trim() || "";
|
||||
if (!content) return { ok: false, display: "Tell me what to remember." };
|
||||
const key = await agentMemory.remember(ctx.userEmail, content, ["user-said"]);
|
||||
return key
|
||||
? { ok: true, display: `Got it — I'll remember: "${content}".` }
|
||||
: { ok: false, display: "Couldn't save that to your vault. Try again." };
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "recall",
|
||||
description: "Pull related notes from your memory vault",
|
||||
matcher: /^(recall|remind me|what do you know|what did i say)\s*(about|of|on)?\s*(.*)$/i,
|
||||
async run(input, ctx) {
|
||||
const m = input.match(/^(recall|remind me|what do you know|what did i say)\s*(about|of|on)?\s*(.*)$/i);
|
||||
const query = m?.[3]?.trim() || "";
|
||||
const memories = await agentMemory.recall(ctx.userEmail, query, 5);
|
||||
if (!memories.length) return { ok: true, display: query ? `Nothing in your vault about "${query}" yet.` : "Your vault is empty." };
|
||||
const lines = memories.map((mem) => `• ${mem.content}`).join("\n");
|
||||
return { ok: true, display: `From your vault${query ? ` about "${query}"` : ""}:\n${lines}` };
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export async function routeAgentInput(text: string, ctx: ToolContext): Promise<ToolResult> {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return { ok: false, display: "Tell me what to do." };
|
||||
// Best-effort: every turn becomes an episodic memory in the vault. We never
|
||||
// block the response on it so a vault outage doesn't break the assistant.
|
||||
agentMemory.remember(ctx.userEmail, trimmed, ["turn"]).catch(() => {});
|
||||
for (const tool of TOOLS) {
|
||||
if (tool.matcher.test(trimmed)) {
|
||||
try {
|
||||
@@ -165,6 +196,18 @@ export async function routeAgentInput(text: string, ctx: ToolContext): Promise<T
|
||||
}
|
||||
}
|
||||
}
|
||||
// If nothing matched, try to recall related context from the vault before
|
||||
// falling back to the help message.
|
||||
try {
|
||||
const hints: Memory[] = await agentMemory.recall(ctx.userEmail, trimmed, 3);
|
||||
if (hints.length > 0) {
|
||||
const lines = hints.map((h) => `• ${h.content}`).join("\n");
|
||||
return {
|
||||
ok: true,
|
||||
display: `I don't have a command that matches that yet, but your vault has related notes:\n${lines}\n\nTry: 'open mission', 'list processes', 'remember that ...', 'recall about ...', 'create a new process'.`,
|
||||
};
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
return {
|
||||
ok: false,
|
||||
display:
|
||||
|
||||
Reference in New Issue
Block a user