feat(memory): clôture du sujet mémoire — bascule adaptative live + panneau UI (§14.5.5)
Pièce 1 (backend) : state.rs câble AdaptiveMemoryRecall via build_memory_recall, piloté par le profil embedder chargé depuis embedder.json (fallback none). Défaut none ⇒ NaiveMemoryRecall nu (comportement inchangé, StubEmbedder jamais touché, zéro dépendance lourde). Instance de recall partagée (RecallMemory + LaunchAgent). Chargement du profil isolé sur un runtime dédié (évite le block_on imbriqué). Pièce 2 (frontend) : feature mémoire complète en miroir de skills — MemoryGateway (port) + TauriMemoryGateway + MockMemoryGateway, types domaine Memory/MemoryIndexEntry/MemoryType, MemoryPanel/MemoryEditor/useMemory, onglet sidebar « Memory » dans ProjectsView. CRUD par slug, liens [[slug]] résolus. Le sujet mémoire est clos : CRUD .md + index + rappel adaptatif + injection à l'activation des agents + UI de gestion. Embedder concret ONNX/HTTP reste un follow-up (défaut none = pleinement fonctionnel sans dépendance). Tests: backend 57 binaires verts, frontend 285 tests verts, typecheck OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,10 @@ import type {
|
||||
LayoutList,
|
||||
LayoutOperation,
|
||||
LayoutTree,
|
||||
Memory,
|
||||
MemoryIndexEntry,
|
||||
MemoryLink,
|
||||
MemoryType,
|
||||
Project,
|
||||
ProfileAvailability,
|
||||
Skill,
|
||||
@ -32,9 +36,11 @@ import type {
|
||||
AgentGateway,
|
||||
ConversationDetails,
|
||||
CreateAgentInput,
|
||||
CreateMemoryInput,
|
||||
CreateSkillInput,
|
||||
CreateTemplateInput,
|
||||
Gateways,
|
||||
MemoryGateway,
|
||||
GitGateway,
|
||||
LayoutGateway,
|
||||
OpenTerminalOptions,
|
||||
@ -1178,6 +1184,107 @@ export class MockSkillGateway implements SkillGateway {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stateful in-memory memory gateway (L14).
|
||||
*
|
||||
* Notes are partitioned by `projectId` and keyed by their immutable **slug**
|
||||
* (derived from the name on create). `readIndex` projects the notes into the
|
||||
* recall index; `resolveLinks` scans `[[slug]]` wikilinks in a note's content
|
||||
* and keeps only those targeting an existing note; `recall` returns a simple
|
||||
* truncation of the index (no embeddings in the mock).
|
||||
*/
|
||||
export class MockMemoryGateway implements MemoryGateway {
|
||||
private byProject = new Map<string, Map<string, Memory>>();
|
||||
|
||||
private bucket(projectId: string): Map<string, Memory> {
|
||||
let store = this.byProject.get(projectId);
|
||||
if (!store) {
|
||||
store = new Map();
|
||||
this.byProject.set(projectId, store);
|
||||
}
|
||||
return store;
|
||||
}
|
||||
|
||||
private notFound(slug: string, projectId: string): GatewayError {
|
||||
return {
|
||||
code: "NOT_FOUND",
|
||||
message: `memory ${slug} not found in project ${projectId}`,
|
||||
};
|
||||
}
|
||||
|
||||
async listMemories(projectId: string): Promise<Memory[]> {
|
||||
return structuredClone([...this.bucket(projectId).values()]);
|
||||
}
|
||||
|
||||
async getMemory(projectId: string, slug: string): Promise<Memory> {
|
||||
const note = this.bucket(projectId).get(slug);
|
||||
if (!note) throw this.notFound(slug, projectId);
|
||||
return structuredClone(note);
|
||||
}
|
||||
|
||||
async createMemory(input: CreateMemoryInput): Promise<Memory> {
|
||||
const note: Memory = {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
type: input.type,
|
||||
content: input.content,
|
||||
};
|
||||
this.bucket(input.projectId).set(slugify(input.name), note);
|
||||
return structuredClone(note);
|
||||
}
|
||||
|
||||
async updateMemory(
|
||||
projectId: string,
|
||||
slug: string,
|
||||
description: string,
|
||||
type: MemoryType,
|
||||
content: string,
|
||||
): Promise<Memory> {
|
||||
const store = this.bucket(projectId);
|
||||
const note = store.get(slug);
|
||||
if (!note) throw this.notFound(slug, projectId);
|
||||
const updated: Memory = { ...note, description, type, content };
|
||||
store.set(slug, updated);
|
||||
return structuredClone(updated);
|
||||
}
|
||||
|
||||
async deleteMemory(projectId: string, slug: string): Promise<void> {
|
||||
const store = this.bucket(projectId);
|
||||
if (!store.delete(slug)) throw this.notFound(slug, projectId);
|
||||
}
|
||||
|
||||
async readIndex(projectId: string): Promise<MemoryIndexEntry[]> {
|
||||
return [...this.bucket(projectId).entries()].map(([slug, note]) => ({
|
||||
slug,
|
||||
title: note.name,
|
||||
hook: note.description,
|
||||
type: note.type,
|
||||
}));
|
||||
}
|
||||
|
||||
async resolveLinks(projectId: string, slug: string): Promise<MemoryLink[]> {
|
||||
const store = this.bucket(projectId);
|
||||
const note = store.get(slug);
|
||||
if (!note) throw this.notFound(slug, projectId);
|
||||
const targets = new Set<MemoryLink>();
|
||||
for (const match of note.content.matchAll(/\[\[([^\]]+)\]\]/g)) {
|
||||
const target = match[1].trim();
|
||||
if (target !== slug && store.has(target)) targets.add(target);
|
||||
}
|
||||
return [...targets];
|
||||
}
|
||||
|
||||
async recall(
|
||||
projectId: string,
|
||||
_text: string,
|
||||
tokenBudget: number,
|
||||
): Promise<MemoryIndexEntry[]> {
|
||||
const index = await this.readIndex(projectId);
|
||||
const limit = Math.max(0, Math.floor(tokenBudget / 16));
|
||||
return index.slice(0, limit);
|
||||
}
|
||||
}
|
||||
|
||||
/** Builds the full set of mock gateways. */
|
||||
export function createMockGateways(): Gateways {
|
||||
const agentGateway = new MockAgentGateway();
|
||||
@ -1192,6 +1299,7 @@ export function createMockGateways(): Gateways {
|
||||
profile: new MockProfileGateway(),
|
||||
template: new MockTemplateGateway(agentGateway),
|
||||
skill: new MockSkillGateway(agentGateway),
|
||||
memory: new MockMemoryGateway(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -12,11 +12,12 @@ import { createMockGateways, MockSystemGateway } from "./index";
|
||||
const gateways: Gateways = createMockGateways();
|
||||
|
||||
describe("createMockGateways", () => {
|
||||
it("exposes all ten gateways", () => {
|
||||
it("exposes all eleven gateways", () => {
|
||||
expect(Object.keys(gateways).sort()).toEqual([
|
||||
"agent",
|
||||
"git",
|
||||
"layout",
|
||||
"memory",
|
||||
"profile",
|
||||
"project",
|
||||
"remote",
|
||||
|
||||
Reference in New Issue
Block a user