mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-14 18:58:43 +02:00
Deux reprises d'AJEAN autour des tâches planifiées. Dossier de scripts (/data/scripts) : un dossier durable À CÔTÉ de memory, hors du workspace. Le workspace est jetable — supprimer une discussion emporte ses fichiers — donc un script qu'on veut garder n'y avait pas sa place. Le briefing machine l'annonce à l'IA, qui y écrit et y lance ses scripts normalement. Tâche « script seul » (Task.Kind/Script) : le planificateur lance le script sans charger le modèle ni consommer un token, sa sortie devient le compte-rendu, et l'UI l'affiche comme n'importe quelle tâche. Elle tourne hors du verrou de génération — d'où un registre à part pour l'afficher « en cours » et l'arrêter (/api/tasks/stop), et un « tester » qui n'attend ni le verrou ni le moteur. Sélecteur Consigne IA / Script seul dans la modale. Outils task_list/create/update/delete : l'IA se donne elle-même rappels et veilles récurrentes, cloisonnés par projet (dans un projet, elle ne voit et ne pilote que ses tâches). Le budget du préambule passe de 8600 à 11000 caractères, avec le palier documenté dans le test. Dossier mémoire réservé à ses outils : bash, write et edit ne le touchent plus (guardToolOnly*). Un `cat memory/…` contournait l'index MEMORY.md, et c'est la condition d'un chiffrement de la mémoire à venir. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
95 lines
4.4 KiB
Go
95 lines
4.4 KiB
Go
package loki
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Budget du préambule envoyé À CHAQUE TOUR : prompt système + schémas des outils.
|
|
// Il est payé sur toute la conversation et sort du contexte utile.
|
|
//
|
|
// Ce test existe pour empêcher le regonflement. Le prompt a déjà été raccourci
|
|
// une fois pour une raison de comportement (un préambule verbeux fait
|
|
// sur-raisonner les modèles à reasoning, qui finissent leur tour sans appeler
|
|
// d'outil), puis une seconde fois pour le coût. Les deux fois, il avait
|
|
// regrossi ligne par ligne, chacune paraissant anodine.
|
|
//
|
|
// Approximation 1 tok ≈ 4 caractères : suffisante pour une alerte, et sans
|
|
// dépendance à un tokenizer.
|
|
//
|
|
// Le budget n'a été relevé que pour de VRAIS outils nouveaux, jamais pour des
|
|
// lignes de prompt — et chaque palier dit lequel :
|
|
//
|
|
// 7500 → 7900 : recall + recall_search (chat_recall.go), ~720 car. Le compactage
|
|
// cesse d'être destructif : le modèle ne relance plus les lectures
|
|
// et recherches déjà faites. Il récupère bien plus qu'il ne coûte.
|
|
// 7900 → 8600 : tracker (tracker.go), ~620 car. Un seul schéma pour les quatre
|
|
// actions — quatre outils auraient coûté quatre fois ça.
|
|
// 8600 → 11000 : les quatre outils task_* (tasks_tools.go), ~1290 car. L'IA se
|
|
// donne elle-même ses rappels et ses veilles ; sans eux il fallait
|
|
// passer par l'interface. Descriptions déjà au plus court, et un
|
|
// seul schéma par action (create/update/delete ne partagent pas
|
|
// assez d'arguments pour fusionner sans rendre chacun ambigu).
|
|
// Les ~500 car restants couvrent web_screenshot, déclaré dès que
|
|
// Playwright est présent — c'est le cas dans l'image, pas sur le
|
|
// runner de CI : le budget doit tenir pour l'image, la vraie.
|
|
// (Le pilotage de navigateur, lui, est HORS de ce décompte : ses
|
|
// schémas n'existent que si on arme son interrupteur.)
|
|
//
|
|
// Les ~200 car de prompt qui les accompagnent sont le seul écart
|
|
// assumé à la règle « pas de relèvement pour du prompt » : un outil
|
|
// qu'aucune ligne ne mentionne n'est jamais appelé de lui-même, et
|
|
// la ligne dit ce que les schémas ne disent pas (quand se planifier,
|
|
// et qu'un script planifié ne charge aucun modèle). Elle a été
|
|
// ramenée de trois lignes à une.
|
|
//
|
|
// Descriptions déjà réduites au strict nécessaire. La consigne d'origine tient
|
|
// pour la suite : ce budget ne se relève pas pour du prompt.
|
|
const promptCharBudget = 11000 // ~2750 tokens, tout allumé
|
|
|
|
func TestSystemPromptStaysLean(t *testing.T) {
|
|
caps := Caps{Agent: true, Internet: true, Mem: MemAlways}
|
|
sp := baseSystemPrompt(caps)
|
|
tb, err := json.Marshal(EnabledTools(caps))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
total := len(sp) + len(tb)
|
|
t.Logf("prompt %d car (~%d tok) + outils %d car (~%d tok) = ~%d tok",
|
|
len(sp), len(sp)/4, len(tb), len(tb)/4, total/4)
|
|
if total > promptCharBudget {
|
|
t.Fatalf("préambule à %d car (~%d tok), budget %d car (~%d tok).\n"+
|
|
"Avant de relever le budget : les schémas d'outils pèsent le double du prompt,\n"+
|
|
"et une consigne écrite dans un schéma n'a pas à être répétée dans le prompt.",
|
|
total, total/4, promptCharBudget, promptCharBudget/4)
|
|
}
|
|
}
|
|
|
|
// Le prompt ne doit pas redevenir un catalogue d'outils : leurs schémas partent
|
|
// dans la MÊME requête et les décrivent déjà un par un.
|
|
func TestSystemPromptDoesNotRelistTools(t *testing.T) {
|
|
sp := baseSystemPrompt(Caps{Agent: true, Internet: true, Mem: MemAlways})
|
|
for _, line := range strings.Split(sp, "\n") {
|
|
l := strings.TrimSpace(line)
|
|
if !strings.HasPrefix(l, "- ") {
|
|
continue
|
|
}
|
|
// Une puce qui commence par un nom d'outil = un catalogue qui revient.
|
|
for _, tool := range []string{"bash ", "write ", "edit ", "mem_", "web_"} {
|
|
if strings.HasPrefix(l[2:], tool) {
|
|
t.Fatalf("le prompt réénumère un outil : %q", l)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sans agent ni mémoire, aucun préambule : un modèle de chat simple à qui on
|
|
// ordonne d'appeler des outils invente des appels textuels qui fuient dans la
|
|
// réponse.
|
|
func TestSystemPromptEmptyWithoutTools(t *testing.T) {
|
|
if sp := baseSystemPrompt(Caps{Mem: MemOff}); sp != "" {
|
|
t.Fatalf("préambule non vide sans outils : %q", sp)
|
|
}
|
|
}
|