feat(canvas): Agent + Hubs + view-as personas (#1)
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

This commit was merged in pull request #1.
This commit is contained in:
2026-06-14 20:38:32 +00:00
parent 89fca578d9
commit 21031ef1c4
7 changed files with 894 additions and 1 deletions
+85
View File
@@ -0,0 +1,85 @@
// Per-tenant agent memory (mini-hindsight). Stored in localStorage, keyed by
// tenant + actor.user_id. Three fact kinds: world, experience, opinion.
// Survives reloads; cleared on Settings → "Forget all".
export type FactKind = "world" | "experience" | "opinion";
export interface Fact {
id: string;
kind: FactKind;
content: string;
tags: string[];
createdAt: number;
}
const NS = "fm.mc.agent.mem.v1";
function key(tenant: string, userId: string): string {
return `${NS}:${tenant}:${userId}`;
}
function load(tenant: string, userId: string): Fact[] {
if (typeof localStorage === "undefined") return [];
try {
const raw = localStorage.getItem(key(tenant, userId));
if (!raw) return [];
return JSON.parse(raw) as Fact[];
} catch {
return [];
}
}
function save(tenant: string, userId: string, facts: Fact[]): void {
if (typeof localStorage === "undefined") return;
try {
localStorage.setItem(key(tenant, userId), JSON.stringify(facts.slice(-500)));
} catch {
/* quota: drop silently */
}
}
export const agentMemory = {
list(tenant: string, userId: string): Fact[] {
return load(tenant, userId);
},
retain(tenant: string, userId: string, kind: FactKind, content: string, tags: string[] = []): Fact {
const facts = load(tenant, userId);
const fact: Fact = {
id: `f_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
kind,
content: content.trim(),
tags,
createdAt: Date.now(),
};
facts.push(fact);
save(tenant, userId, facts);
return fact;
},
recall(tenant: string, userId: string, query: string, limit = 8): Fact[] {
const facts = load(tenant, userId);
if (!query.trim()) return facts.slice(-limit).reverse();
const q = query.toLowerCase();
const tokens = q.split(/\s+/).filter(Boolean);
const scored = facts.map((f) => {
const hay = `${f.content} ${f.tags.join(" ")}`.toLowerCase();
let score = 0;
for (const t of tokens) {
if (hay.includes(t)) score += 1;
}
return { f, score };
});
return scored
.filter((s) => s.score > 0)
.sort((a, b) => b.score - a.score || b.f.createdAt - a.f.createdAt)
.slice(0, limit)
.map((s) => s.f);
},
forget(tenant: string, userId: string): void {
if (typeof localStorage === "undefined") return;
try {
localStorage.removeItem(key(tenant, userId));
} catch {
/* swallow */
}
},
};