v0.2.1 : anti-boucle + system prompt facon pi

- Nouveau system prompt structure facon pi (identite -> methode/guidelines
  -> date -> outils + conseils -> contexte machine/skills), injecte via
  InjectSkills. Corrige les boucles de raisonnement infinies.
- Plafond de reflexion par defaut (REASONING_BUDGET=2048 au lieu de -1) :
  coupe une reflexion qui derape au lieu de tourner a l'infini.
- Garde-fou anti-boucle dans runChat : stop si le modele relance
  exactement le meme appel d'outil plusieurs tours de suite.
- Descriptions d'outils enrichies (run_shell, skill).
- Bump version 0.2.0 -> 0.2.1.
This commit is contained in:
nathaninline committed 2026-06-21 09:14:08 +02:00
1 parent e606206d90
commit ca5d3f9197
6 files changed
+78 -10

No files matched your search

+1
View File
@@ -160,6 +160,7 @@ Tout vit sous **`$JEAN_HOME`** (défaut `/etc/jean` sur Linux/macOS, `%ProgramDa
| `CUDA_VISIBLE_DEVICES` | GPU à utiliser (réglé par `jean gpu`) | tous |
| `KV_TYPE` (`_K`/`_V`) | quantization du cache KV | — |
| `REASONING` | passthrough du mode raisonnement | — |
| `REASONING_BUDGET` | plafond de tokens de réflexion (anti-boucle) ; `-1` = illimité | `2048` |
| `EXTRA_ARGS` | ajouté tel quel à `llama-server` | — |
La clé API (quand elle est définie avec `jean set-api-key`) est stockée dans `$JEAN_HOME/.api_key`, séparément de `config.env`.
+25
View File
@@ -86,6 +86,8 @@ func runShellTool() Tool {
// skills are enabled). Merges with an existing system message if present.
func InjectSkills(msgs []Message) []Message {
var parts []string
// Always-on decisive-agent preamble (anti-loop). See baseSystemPrompt.
parts = append(parts, baseSystemPrompt())
if mp := machineSystemPrompt(); mp != "" {
parts = append(parts, mp)
}
@@ -234,6 +236,10 @@ func runChat(messages []Message, temperature float64, cb ChatCallback) error {
// retry the same turn once with tools removed so the model answers in plain
// text from the tool results already gathered, instead of dying mid-chat.
disableTools := false
// Anti-loop net: if the model re-emits the exact same tool call (name+args)
// several times, it's stuck — break instead of spinning to the iteration cap.
lastSig := ""
repeatSig := 0
for iter := 0; iter < 8; iter++ {
payload := map[string]any{
"model": "jean",
@@ -447,6 +453,25 @@ func runChat(messages []Message, temperature float64, cb ChatCallback) error {
assistant.Content = s
}
messages = append(messages, assistant)
// Loop guard: same exact call(s) as last turn? Count it; on the 3rd
// identical turn, stop so we don't churn the same command forever.
sig := strings.Builder{}
for _, tc := range tcs {
sig.WriteString(tc.Function.Name)
sig.WriteByte('\x00')
sig.WriteString(tc.Function.Arguments)
sig.WriteByte('\n')
}
if s := sig.String(); s == lastSig {
repeatSig++
if repeatSig >= 2 {
cb(StreamEvent{Content: "\n\n[stop: appel d'outil répété en boucle — " + tcs[0].Function.Name + "]"})
return nil
}
} else {
lastSig = s
repeatSig = 0
}
// 2. Execute each tool locally and append a "tool" reply.
for _, tc := range tcs {
var args map[string]any
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"strings"
)
const Version = "0.2.0"
const Version = "0.2.1"
func main() {
args := os.Args[1:]
+6 -5
View File
@@ -61,11 +61,12 @@ func cmdServe(args []string) error {
llmArgs = append(llmArgs, "-ctv", vtv)
}
if r := cfg["REASONING"]; r != "" {
// budget -1 = thinking illimité (défaut). NE PAS forcer 0 : sur llama.cpp
// vanilla, 0 = "immediate end" → coupe le raisonnement net (le fork
// ik_llama.cpp, lui, l'ignore, d'où l'ancien comportement trompeur).
// Configurable via REASONING_BUDGET (ex: 2048 pour plafonner).
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "-1"))
// budget plafonné par défaut (2048) : une réflexion qui part en boucle est
// coupée net au lieu de tourner à l'infini — c'est le comportement de pi.
// NE PAS forcer 0 : sur llama.cpp vanilla, 0 = "immediate end" → coupe tout
// le raisonnement (le fork ik_llama.cpp l'ignore). Mettre -1 pour illimité.
// Configurable via REASONING_BUDGET.
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "2048"))
}
// API_KEY protège le serveur quand il est exposé sur internet : llama-server
// exige alors l'en-tête "Authorization: Bearer <clé>". La clé est lue depuis
+11 -3
View File
@@ -162,12 +162,20 @@ func skillsSystemPrompt() string {
}
list := ListSkills()
var b strings.Builder
b.WriteString(`Skills = guides Markdown que tu gères toi-même via le tool skill(action, name, content) : read=lire, write=créer/remplacer, append=ajouter à la fin sans réécrire, delete=supprimer. De ta propre initiative, crée ou enrichis un skill quand tu découvres une solution réutilisable (problème enfin résolu, procédure, astuce) ; sa 1re ligne est un titre court (#) servant de description. N'utilise read que si la question concerne un skill listé.`)
b.WriteString("Outil disponible — skill(action, name, content) : gère tes guides Markdown réutilisables. action: read=lire, write=créer/remplacer, append=ajouter à la fin, delete=supprimer.\n")
b.WriteString("Conseils skill :\n")
for _, l := range []string{
"De ta propre initiative, crée ou enrichis un skill quand tu découvres une solution réutilisable (problème résolu, procédure, astuce).",
"La 1re ligne du contenu est un titre court (#) qui sert de description.",
"N'utilise read que si la question concerne un skill listé ci-dessous — sinon n'appelle pas l'outil.",
} {
b.WriteString("- " + l + "\n")
}
if len(list) == 0 {
b.WriteString("\nAucun skill pour l'instant.")
b.WriteString("Skills : aucun pour l'instant.")
return b.String()
}
b.WriteString("\nSkills :\n")
b.WriteString("Skills :\n")
for _, s := range list {
fmt.Fprintf(&b, "- %s: %s\n", s.Name, s.Desc)
}
+34 -1
View File
@@ -37,6 +37,30 @@ func setToolsEnabled(on bool) error {
return nil
}
// baseSystemPrompt is the always-on system preamble. Structured like pi's
// proven prompt (identity → method → guidelines → concision → date): a concrete,
// procedural prompt gives the model rails so it stops deliberating forever
// ("Wait, let me check… Wait, I'll just run it…") and commits to an action.
// The per-tool "Outil disponible" sections live in machine/skills prompts so
// they only appear when the matching feature is on.
func baseSystemPrompt() string {
var b strings.Builder
b.WriteString("Tu es un assistant expert opérant dans jean, un agent qui agit directement sur cette machine. Tu aides l'utilisateur en inspectant le système, en exécutant des commandes et en gérant des skills réutilisables.\n\n")
b.WriteString("Méthode :\n")
for _, l := range []string{
"Raisonne brièvement (1 à 2 phrases) puis AGIS. Dès qu'une action est possible, appelle l'outil immédiatement — ne l'annonce pas, fais-le.",
"Ne répète JAMAIS deux fois la même étape de réflexion ni le même appel d'outil. Si tu hésites (« je devrais peut-être vérifier… »), arrête de réfléchir et appelle l'outil maintenant.",
"Pour toute question système (fichiers, process, réseau, OS), utilise tes outils au lieu de supposer — tu as un accès réel, ne demande pas la permission pour inspecter.",
"Quand la tâche est faite, donne une réponse finale courte et arrête-toi. N'enchaîne pas d'outils inutiles.",
"Sois concis dans tes réponses.",
"Affiche clairement les chemins de fichiers quand tu travailles dessus.",
} {
b.WriteString("- " + l + "\n")
}
b.WriteString("\nDate du jour : " + time.Now().Format("2006-01-02"))
return b.String()
}
// machineSystemPrompt returns a short briefing about the host the model is
// running on, so that when machine access is enabled it knows *which* machine
// run_shell acts upon (and doesn't claim it has no access to "your PC").
@@ -56,7 +80,16 @@ func machineSystemPrompt() string {
cwd, _ := os.Getwd()
var b strings.Builder
b.WriteString("Accès machine actif : le tool run_shell exécute des commandes sur cette machine — c'est ton accès réel, ne dis jamais que tu n'y as pas accès. Pour toute question système (OS, disque, process, fichiers, réseau), utilise-le au lieu de supposer.\n")
b.WriteString("Outil disponible — run_shell(command, timeout) : exécute une commande shell sur CETTE machine et renvoie stdout, stderr et le code de sortie. C'est ton accès réel ; ne dis jamais que tu n'y as pas accès.\n")
b.WriteString("Conseils run_shell :\n")
for _, l := range []string{
"Sers-t'en pour toute question système (OS, disque, process, fichiers, réseau, lire un script) plutôt que de supposer.",
"Préfère des commandes ciblées et non-interactives ; chaîne-les avec && quand elles sont dépendantes.",
"Évite les commandes destructrices (rm -rf, mkfs, dd…) sauf demande explicite de l'utilisateur.",
"Si une commande échoue, lis stderr et le code de sortie, puis corrige — ne relance pas la même commande à l'identique.",
} {
b.WriteString("- " + l + "\n")
}
b.WriteString(fmt.Sprintf("Machine : hôte=%s, %s/%s", host, runtime.GOOS, runtime.GOARCH))
if who != "" {
b.WriteString(", user=" + who)