mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 17:57:37 +02:00
Mémoire : un mode par projet, et un quatrième — « recherche »
Repris d'AJEAN 0.13.12. PAR PROJET Le mode mémoire était un réglage global de config.env. Il vit désormais sur le projet (champ mem_mode) : un chantier de code peut couper la mémoire pendant qu'un projet perso la garde proactive. Un projet sans réglage propre — tous ceux d'avant — retombe sur l'ancien MEM_MODE global : rien ne change tant qu'on n'y touche pas. L'API /api/memory et `loki memory` agissent sur le projet actif ; l'UI le dit, et se recharge déjà au changement de projet. AUTO, EN DEUX SAVEURS - « auto, injectée » (always, le défaut) : l'index des pages est en tête de conversation. La consigne dit maintenant d'y lire directement la bonne page ; mem_search ne sert plus qu'à chercher par contenu. Avant, on injectait l'index ET on exigeait une recherche préalable — un appel d'outil par tour pour retrouver ce que le modèle avait sous les yeux. - « auto, recherche » (nouveau) : rien d'injecté, l'IA cherche avant chaque tâche. Contexte plus léger, démarrage plus rapide. memProactive regroupe les deux là où seul « proactif » compte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4a1054c1b7
commit
2c36fa6d2c
8 files changed
+144
-43
No files matched your search
@@ -377,47 +377,69 @@ func MemDelete(name string) error {
|
||||
}
|
||||
|
||||
// MemMode gouverne l'accès de l'IA à sa mémoire persistante, indépendamment du
|
||||
// mode agent (shell). Trois modes :
|
||||
// - MemOff : mémoire coupée (aucun outil mem_*, aucune consigne).
|
||||
// - MemOnDemand : outils mem_* disponibles, mais l'IA ne les utilise QUE si
|
||||
// mode agent (shell). Réglé PAR PROJET (voir projectMemMode). Quatre modes :
|
||||
// - MemOff : mémoire coupée (aucun outil mem_*, aucune consigne).
|
||||
// - MemOnDemand : outils mem_* disponibles, mais l'IA ne les utilise QUE si
|
||||
// l'utilisateur le demande explicitement (pas de recherche/écriture spontanée).
|
||||
// - MemAlways : comportement proactif historique (cherche avant de répondre, sauve d'elle-même).
|
||||
// - MemAlways : proactif, INDEX INJECTÉ — l'index MEMORY.md (titres +
|
||||
// accroches) est en tête de conversation, l'IA lit directement la bonne page.
|
||||
// - MemSearchFirst : proactif, RECHERCHE D'ABORD — rien n'est injecté, l'IA
|
||||
// appelle mem_search avant toute tâche. Prompt léger, démarrage plus rapide.
|
||||
type MemMode string
|
||||
|
||||
const (
|
||||
MemOff MemMode = "off"
|
||||
MemOnDemand MemMode = "ondemand"
|
||||
MemAlways MemMode = "always"
|
||||
MemOff MemMode = "off"
|
||||
MemOnDemand MemMode = "ondemand"
|
||||
MemAlways MemMode = "always"
|
||||
MemSearchFirst MemMode = "search"
|
||||
)
|
||||
|
||||
// memMode lit MEM_MODE dans config.env. Défaut = always (préserve le comportement
|
||||
// actuel). Toute valeur inconnue retombe sur always.
|
||||
func memMode() MemMode {
|
||||
switch strings.ToLower(strings.TrimSpace(ReadConfig()["MEM_MODE"])) {
|
||||
// memProactive : l'IA gère sa mémoire d'elle-même (cherche/sauve sans qu'on le
|
||||
// demande). Vrai pour les deux modes auto (injecté ou recherche).
|
||||
func memProactive(m MemMode) bool { return m == MemAlways || m == MemSearchFirst }
|
||||
|
||||
// normalizeMemMode ramène une valeur de config à un mode connu. Défaut = always
|
||||
// (préserve le comportement historique). "auto" est un alias d'always.
|
||||
func normalizeMemMode(v string) MemMode {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case "off":
|
||||
return MemOff
|
||||
case "ondemand":
|
||||
return MemOnDemand
|
||||
case "search":
|
||||
return MemSearchFirst
|
||||
default: // "always", "auto", "" et inconnus
|
||||
return MemAlways
|
||||
}
|
||||
}
|
||||
|
||||
// setMemMode persiste le mode mémoire dans config.env.
|
||||
func setMemMode(m MemMode) error {
|
||||
return SetConfigKey("MEM_MODE", string(m))
|
||||
// memMode renvoie le mode mémoire du PROJET ACTIF (AJEAN 0.13.12). Repli : un
|
||||
// projet sans réglage propre (tous ceux d'avant) retombe sur l'ancien réglage
|
||||
// global MEM_MODE de config.env — always par défaut. Rien ne change donc pour
|
||||
// les projets existants tant qu'on ne touche pas à leur mode.
|
||||
func memMode() MemMode {
|
||||
if pm := projectMemMode(activeProjectSlug()); pm != "" {
|
||||
return normalizeMemMode(pm)
|
||||
}
|
||||
return normalizeMemMode(ReadConfig()["MEM_MODE"])
|
||||
}
|
||||
|
||||
// cmdMemory : loki memory [off|ondemand|always|status]
|
||||
// setMemMode persiste le mode mémoire du PROJET ACTIF.
|
||||
func setMemMode(m MemMode) error {
|
||||
return setProjectMemMode(activeProjectSlug(), string(m))
|
||||
}
|
||||
|
||||
// cmdMemory : loki memory [off|ondemand|always|search|status]
|
||||
func cmdMemory(args []string) error {
|
||||
sub := ""
|
||||
if len(args) > 0 {
|
||||
sub = strings.ToLower(strings.TrimSpace(args[0]))
|
||||
}
|
||||
label := map[MemMode]string{
|
||||
MemOff: "désactivée (l'IA n'a aucun accès mémoire)",
|
||||
MemOnDemand: "sur demande (outils dispo, utilisés seulement si tu le demandes)",
|
||||
MemAlways: "auto (l'IA cherche et sauve d'elle-même)",
|
||||
MemOff: "désactivée (l'IA n'a aucun accès mémoire)",
|
||||
MemOnDemand: "sur demande (outils dispo, utilisés seulement si tu le demandes)",
|
||||
MemAlways: "auto/injectée (index en tête de conversation, l'IA lit directement la bonne page)",
|
||||
MemSearchFirst: "auto/recherche (rien d'injecté, l'IA cherche avant toute tâche — prompt léger)",
|
||||
}
|
||||
switch sub {
|
||||
case "off":
|
||||
@@ -428,20 +450,24 @@ func cmdMemory(args []string) error {
|
||||
if err := setMemMode(MemOnDemand); err != nil {
|
||||
return err
|
||||
}
|
||||
case "always":
|
||||
case "always", "auto", "inject", "injected":
|
||||
if err := setMemMode(MemAlways); err != nil {
|
||||
return err
|
||||
}
|
||||
case "search", "recherche", "lazy":
|
||||
if err := setMemMode(MemSearchFirst); err != nil {
|
||||
return err
|
||||
}
|
||||
case "", "status":
|
||||
m := memMode()
|
||||
fmt.Printf("%s mode: %s — %s\n", cyan("Mémoire"), bold(string(m)), label[m])
|
||||
fmt.Printf("%s projet %s, mode: %s — %s\n", cyan("Mémoire"), bold(projectName(activeProjectSlug())), bold(string(m)), label[m])
|
||||
pages := MemList()
|
||||
fmt.Printf(" %d page(s) sous %s\n", len(pages), memoryDir())
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("usage: loki memory [off|ondemand|always|status]")
|
||||
return fmt.Errorf("usage: loki memory [off|ondemand|always|search|status]")
|
||||
}
|
||||
m := memMode()
|
||||
fmt.Printf("%s mémoire : %s — %s\n", green("[ok]"), bold(string(m)), label[m])
|
||||
fmt.Printf("%s mémoire du projet %s : %s — %s\n", green("[ok]"), bold(projectName(activeProjectSlug())), bold(string(m)), label[m])
|
||||
return nil
|
||||
}
|
||||
@@ -56,7 +56,7 @@ func baseSystemPrompt(caps Caps) string {
|
||||
b.WriteString("\n\n" + rp)
|
||||
}
|
||||
}
|
||||
if caps.Mem == MemAlways {
|
||||
if memProactive(caps.Mem) {
|
||||
b.WriteString(" You evolve with every conversation: you actively maintain a persistent memory so nothing useful is lost between sessions.")
|
||||
}
|
||||
// PAS de catalogue d'outils ici : leurs schémas, envoyés dans la même requête,
|
||||
@@ -76,7 +76,15 @@ func baseSystemPrompt(caps Caps) string {
|
||||
case MemAlways:
|
||||
b.WriteString("\nManaging your memory is part of the job, not optional:\n")
|
||||
b.WriteString("- Save anything worth keeping (a preference, fact, decision, how-to) with mem_add, or mem_edit to update a page — on your own, without being asked.\n")
|
||||
b.WriteString("- Before any task or answer, call mem_search first, then mem_read the best page. Do this even when the request has new specifics (a name, a place, a value): your saved method still applies, only the parameter changes.\n")
|
||||
// Mode INJECTÉ : l'index est en tête de contexte (mem_index.go), la
|
||||
// recherche devient facultative — l'IA lit directement la bonne page.
|
||||
b.WriteString("- The memory index (page list) is in your context: when a page looks relevant, mem_read it directly. mem_search is only for finding something by content.\n")
|
||||
case MemSearchFirst:
|
||||
// Mode RECHERCHE : rien d'injecté, prompt léger — mais l'IA DOIT chercher
|
||||
// elle-même, sinon elle ignore ce que sa mémoire contient.
|
||||
b.WriteString("\nManaging your memory is part of the job, not optional:\n")
|
||||
b.WriteString("- Save anything worth keeping (a preference, fact, decision, how-to) with mem_add, or mem_edit to update a page — on your own, without being asked.\n")
|
||||
b.WriteString("- Before any task or answer, call mem_search first, then mem_read the best page. Do this even when the request has new specifics (a name, a place, a value): your saved method still applies, only the parameter changes. Nothing is preloaded: a search is the only way to know what you already know.\n")
|
||||
case MemOnDemand:
|
||||
b.WriteString("\nMemory is ON-DEMAND: you have the mem_* tools but do NOT read or write memory on your own. Call mem_search/mem_read only when the user explicitly asks you to recall or look something up, and mem_add/mem_edit only when the user explicitly asks you to remember something. Otherwise leave memory untouched and answer directly.\n")
|
||||
}
|
||||
@@ -96,7 +104,7 @@ func baseSystemPrompt(caps Caps) string {
|
||||
// disent pas. Une ligne, pas trois : le budget du préambule est compté
|
||||
// (TestSystemPromptStaysLean).
|
||||
b.WriteString("You can schedule work for yourself with task_create — only for what must recur or happen later, never a one-off you can do now. A script from your scripts folder runs as a task with no model at all.\n")
|
||||
if caps.Mem == MemAlways {
|
||||
if caps.Mem == MemSearchFirst {
|
||||
b.WriteString("Before answering anything about yourself or this machine, call mem_search first — even trivial-seeming questions. A tool check never replaces it: memory may hold context the tool won't reveal.\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package loki
|
||||
|
||||
import "testing"
|
||||
|
||||
// Chaque projet garde son propre mode mémoire ; un projet sans réglage retombe
|
||||
// sur l'ancien global MEM_MODE.
|
||||
func TestMemModePerProject(t *testing.T) {
|
||||
testHome(t)
|
||||
ensureDefaultProject()
|
||||
if err := SetConfigKey("MEM_MODE", "ondemand"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if memMode() != MemOnDemand {
|
||||
t.Fatalf("sans réglage propre, repli global attendu, got %q", memMode())
|
||||
}
|
||||
b, err := createProject("Chantier")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := setActiveProject(b.Slug); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := setMemMode(MemSearchFirst); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if memMode() != MemSearchFirst {
|
||||
t.Fatalf("mode du projet actif attendu, got %q", memMode())
|
||||
}
|
||||
if err := setActiveProject(defaultProjectSlug); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if memMode() != MemOnDemand {
|
||||
t.Fatalf("l'autre projet ne doit pas hériter du réglage, got %q", memMode())
|
||||
}
|
||||
if setProjectMemMode(b.Slug, "n'importe quoi") == nil {
|
||||
t.Fatal("un mode inconnu doit être refusé")
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,9 @@ type Project struct {
|
||||
// de conversation (projectContextMessage) pour qu'elle sache d'emblée sur quoi
|
||||
// elle travaille, sans avoir à le lui réexpliquer à chaque nouvelle discussion.
|
||||
Desc string `json:"desc,omitempty"`
|
||||
// MemMode = mode mémoire PROPRE à ce projet (off/ondemand/always/search).
|
||||
// Vide = pas encore réglé → repli sur l'ancien global MEM_MODE (voir memMode).
|
||||
MemMode string `json:"mem_mode,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -186,6 +189,36 @@ func projectDesc(slug string) string {
|
||||
// setProjectDesc enregistre la description d'un projet, tronquée à une taille
|
||||
// raisonnable : c'est un contexte injecté à CHAQUE conversation, pas un cahier des
|
||||
// charges. Une chaîne vide efface la description.
|
||||
// projectMemMode renvoie le mode mémoire propre à un projet ("" si non réglé, ce
|
||||
// qui déclenche le repli global dans memMode).
|
||||
func projectMemMode(slug string) string {
|
||||
for _, p := range listProjects() {
|
||||
if p.Slug == slug {
|
||||
return p.MemMode
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setProjectMemMode enregistre le mode mémoire d'un projet. Une valeur vide
|
||||
// efface le réglage propre (retour au repli global).
|
||||
func setProjectMemMode(slug, mode string) error {
|
||||
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||
switch mode {
|
||||
case "", "off", "ondemand", "always", "search":
|
||||
default:
|
||||
return fmt.Errorf("mode mémoire invalide: %s", mode)
|
||||
}
|
||||
list := listProjects()
|
||||
for i := range list {
|
||||
if list[i].Slug == slug {
|
||||
list[i].MemMode = mode
|
||||
return saveProjects(list)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("projet introuvable")
|
||||
}
|
||||
|
||||
func setProjectDesc(slug, desc string) error {
|
||||
desc = strings.TrimSpace(desc)
|
||||
const max = 2000
|
||||
|
||||
@@ -2584,9 +2584,10 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<div id="mem-block" class="gated-group">
|
||||
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span>mode</span>
|
||||
<label class="switchrow"><span>mode <small class="dim">(projet actif)</small></span>
|
||||
<select id="mem-mode" onchange="setMemMode()" class="sctl">
|
||||
<option value="always">auto</option>
|
||||
<option value="always">auto, injectée</option>
|
||||
<option value="search">auto, recherche</option>
|
||||
<option value="ondemand">sur demande</option>
|
||||
<option value="off">désactivée</option>
|
||||
</select>
|
||||
@@ -4542,9 +4543,10 @@ async function toggleCompact(){
|
||||
const on=document.getElementById('compact-toggle').checked;
|
||||
await jpost('/api/agent/compact',{on});
|
||||
}
|
||||
// Mode mémoire (3 états) — indépendant du mode agent.
|
||||
// Mode mémoire (4 états), réglé PAR PROJET — chaque projet garde le sien.
|
||||
const MEM_DESC={
|
||||
always:'L\'IA cherche dans sa mémoire avant de répondre et sauve d\'elle-même ce qui mérite d\'être retenu.',
|
||||
always:'L\'index des pages est placé en tête de conversation : l\'IA voit d\'emblée ce qu\'elle sait, lit directement la bonne page et sauve d\'elle-même ce qui mérite d\'être retenu. Réglage propre au projet actif.',
|
||||
search:'Rien n\'est injecté : l\'IA cherche dans sa mémoire avant chaque tâche. Contexte plus léger, démarrage plus rapide. Réglage propre au projet actif.',
|
||||
ondemand:'Les outils mémoire existent mais l\'IA ne les utilise QUE si tu le demandes (« souviens-toi de… », « qu\'avais-tu retenu sur… »).',
|
||||
off:'Mémoire coupée : aucun accès en lecture ni écriture, l\'IA répond sans mémoire.'
|
||||
};
|
||||
|
||||
@@ -376,9 +376,10 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<div id="mem-block" class="gated-group">
|
||||
<div class="subhead">Mémoire<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span id="mem-mode-desc" class="tip"></span></span></div>
|
||||
<div class="slist">
|
||||
<label class="switchrow"><span>mode</span>
|
||||
<label class="switchrow"><span>mode <small class="dim">(projet actif)</small></span>
|
||||
<select id="mem-mode" onchange="setMemMode()" class="sctl">
|
||||
<option value="always">auto</option>
|
||||
<option value="always">auto, injectée</option>
|
||||
<option value="search">auto, recherche</option>
|
||||
<option value="ondemand">sur demande</option>
|
||||
<option value="off">désactivée</option>
|
||||
</select>
|
||||
|
||||
@@ -310,9 +310,10 @@ async function toggleCompact(){
|
||||
const on=document.getElementById('compact-toggle').checked;
|
||||
await jpost('/api/agent/compact',{on});
|
||||
}
|
||||
// Mode mémoire (3 états) — indépendant du mode agent.
|
||||
// Mode mémoire (4 états), réglé PAR PROJET — chaque projet garde le sien.
|
||||
const MEM_DESC={
|
||||
always:'L\'IA cherche dans sa mémoire avant de répondre et sauve d\'elle-même ce qui mérite d\'être retenu.',
|
||||
always:'L\'index des pages est placé en tête de conversation : l\'IA voit d\'emblée ce qu\'elle sait, lit directement la bonne page et sauve d\'elle-même ce qui mérite d\'être retenu. Réglage propre au projet actif.',
|
||||
search:'Rien n\'est injecté : l\'IA cherche dans sa mémoire avant chaque tâche. Contexte plus léger, démarrage plus rapide. Réglage propre au projet actif.',
|
||||
ondemand:'Les outils mémoire existent mais l\'IA ne les utilise QUE si tu le demandes (« souviens-toi de… », « qu\'avais-tu retenu sur… »).',
|
||||
off:'Mémoire coupée : aucun accès en lecture ni écriture, l\'IA répond sans mémoire.'
|
||||
};
|
||||
|
||||
@@ -549,16 +549,8 @@ func handleMemoryMode(w http.ResponseWriter, r *http.Request) {
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
// On normalise via memMode() en réinjectant la valeur : toute entrée
|
||||
// inconnue retombe sur "always", donc on valide en passant par le parseur.
|
||||
m := MemAlways
|
||||
switch MemMode(strings.ToLower(strings.TrimSpace(req.Mode))) {
|
||||
case MemOff:
|
||||
m = MemOff
|
||||
case MemOnDemand:
|
||||
m = MemOnDemand
|
||||
case MemAlways:
|
||||
m = MemAlways
|
||||
}
|
||||
if err := setMemMode(m); err != nil {
|
||||
// Réglage du PROJET ACTIF (AJEAN 0.13.12).
|
||||
if err := setMemMode(normalizeMemMode(req.Mode)); err != nil {
|
||||
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user