Mémoire en notes Markdown + presets de configuration (repris d'AJEAN)

Les deux dernières idées d'AJEAN (github.com/nathaninline/jean).

1. Mémoire en notes Markdown ($DATA_DIR/MEMORY), écrites par l'agent via
   deux outils (memory_search / memory_save). Trois modes : off,
   ondemand (défaut) et always. Face au RAG vectoriel : aucun modèle
   d'embedding requis, notes lisibles et éditables à la main, et rien de
   mémorisé à l'insu — la recherche par mots-clés ne rappelle que du
   pertinent, là où le RAG ressortait une demande sans rapport.
   Le RAG disparaît de l'UI : un seul concept de mémoire.
   En mode Plan, seule la consultation est proposée, pas l'écriture.

2. Presets : jeux de réglages nommés, enregistrés et rappelés en un
   clic (sélecteur + « + Preset » dans l'en-tête des Réglages).

Au passage, un vrai bug : ConfigPatch ne déclarait ni keep_alive, ni
skills_enabled, ni ponytail — Pydantic ignore les champs non déclarés,
donc ces trois réglages ne se sauvegardaient JAMAIS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVay7z3y7q2gEe54ByAE6N
This commit is contained in:
Claude committed 2026-08-03 05:57:49 +00:00
1 parent 236e875cf9
commit 174a3d4a35
7 files changed
+463 -5

No files matched your search

+39
View File
@@ -293,6 +293,7 @@ export interface AgentConfig {
embed_model: string;
skills_enabled: boolean;
ponytail: boolean;
memory_mode: "off" | "ondemand" | "always";
keep_alive: string;
}
@@ -406,6 +407,44 @@ export async function getConfig(model?: string): Promise<{
return res.json();
}
// ── Presets de configuration ─────────────────────────────────────────────
export async function listPresets(): Promise<string[]> {
const res = await fetch("/api/config/presets");
return (await res.json()).presets;
}
export async function savePreset(name: string, model?: string): Promise<string[]> {
const query = model ? `?model=${encodeURIComponent(model)}` : "";
const res = await fetch(`/api/config/presets${query}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!res.ok) throw await apiError(res, "preset refusé");
return (await res.json()).presets;
}
export async function applyPreset(
name: string,
model?: string
): Promise<AgentConfig> {
const query = model ? `?model=${encodeURIComponent(model)}` : "";
const res = await fetch(`/api/config/presets/apply${query}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name }),
});
if (!res.ok) throw await apiError(res, "preset introuvable");
return (await res.json()).config;
}
export async function deletePreset(name: string): Promise<string[]> {
const res = await fetch(`/api/config/presets?name=${encodeURIComponent(name)}`, {
method: "DELETE",
});
return (await res.json()).presets;
}
export async function saveConfig(
patch: Partial<AgentConfig>,
model?: string
+96 -2
View File
@@ -1,12 +1,15 @@
import { useEffect, useState } from "react";
import { useStore } from "../store/useStore";
import {
applyPreset,
deleteModel,
getBenchScores,
getHardware,
listMcp,
listPresets,
pullModel,
runBench,
savePreset,
testMcp,
updateMcp,
} from "../api/client";
@@ -125,6 +128,7 @@ export function SettingsView() {
</div>
</div>
<div className="flex items-center gap-3">
<PresetBar model={selectedModel} onApplied={refreshConfig} />
<div className="flex items-center gap-1.5 text-[13px] text-ok">
<span className="h-2 w-2 border-2 border-line bg-ok" />
{status?.connected ? "Ollama connecté" : "Ollama déconnecté"}
@@ -459,8 +463,6 @@ export function SettingsView() {
"Méthodes expertes selon la tâche (débogage, web…)"] as const,
["self_review", "Auto-critique",
"Relit et révise la réponse — plus lent"] as const,
["rag_enabled", "Mémoire entre sessions",
"Rappelle d'AUTRES discussions ici. Chaque discussion garde toujours sa propre mémoire."] as const,
]).map(([key, label, desc], i) => (
<ToggleRow
key={key}
@@ -471,6 +473,29 @@ export function SettingsView() {
onClick={() => set(key, !draft[key])}
/>
))}
<div className="flex items-center gap-3 border-t-2 border-line-soft py-2">
<span className="flex-1">
<span className="block text-[14px] text-ink">Mémoire</span>
<span className="block text-[12px] text-muted-2">
Notes Markdown que l'agent écrit lui-même, relisibles
dans <code>/data/MEMORY</code>
</span>
</span>
<select
value={draft.memory_mode}
onChange={(e) =>
set(
"memory_mode",
e.target.value as AgentConfig["memory_mode"]
)
}
className="h-8 border-[3px] border-line bg-card px-2 text-[13px] text-ink"
>
<option value="off">Désactivée</option>
<option value="ondemand">Sur demande</option>
<option value="always">Automatique</option>
</select>
</div>
</Advanced>
<div className="mt-2 flex items-center gap-3 border-t-2 border-line-soft pt-3">
<span className="flex-1">
@@ -898,6 +923,75 @@ function Toggle({ on, onClick }: { on: boolean; onClick: () => void }) {
);
}
/** Presets : jeux de réglages nommés, enregistrés et rappelés en un clic. */
function PresetBar({
model,
onApplied,
}: {
model: string;
onApplied: () => void;
}) {
const [presets, setPresets] = useState<string[]>([]);
const [busy, setBusy] = useState(false);
useEffect(() => {
listPresets().then(setPresets).catch(() => {});
}, []);
const apply = async (name: string) => {
if (!name) return;
setBusy(true);
try {
await applyPreset(name, model || undefined);
onApplied();
} finally {
setBusy(false);
}
};
const create = async () => {
const name = window.prompt("Nom du preset (réglages actuels) :");
if (!name?.trim()) return;
setBusy(true);
try {
setPresets(await savePreset(name.trim(), model || undefined));
} catch (e) {
window.alert(e instanceof Error ? e.message : "preset refusé");
} finally {
setBusy(false);
}
};
return (
<div className="flex items-center gap-1.5">
<select
value=""
disabled={busy || presets.length === 0}
onChange={(e) => apply(e.target.value)}
className="h-[34px] border-[3px] border-line bg-card px-2 text-[13px] text-ink disabled:opacity-40"
title="Appliquer un preset"
>
<option value="">
{presets.length ? "Preset…" : "Aucun preset"}
</option>
{presets.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
<button
onClick={create}
disabled={busy}
className="h-[34px] border-[3px] border-line bg-card px-2.5 text-[13px] text-muted-2 disabled:opacity-40"
title="Enregistrer les réglages actuels comme preset"
>
+ Preset
</button>
</div>
);
}
/** Repli « Avancé » : sort les réglages rares du chemin principal.
*
* <details> natif : pas d'état à gérer, repliement mémorisé par le navigateur