v0.2.11 : fix stall reasoning (prompt agent court) + outil edit + filet de sécurité nudge/drop_reasoning

This commit is contained in:
nathaninline committed 2026-06-26 16:42:33 +02:00
1 parent 4c8aafa898
commit 7a145258d3
5 files changed
+56 -34

No files matched your search

+7
View File
@@ -73,6 +73,13 @@ func cmdChat(args []string) error {
fmt.Printf("\n%s\n", red("[erreur] "+ev.Err.Error()))
case ev.Stats != nil:
stats = ev.Stats
case ev.DropReasoning:
// Le tour a « pensé sans agir » : on relance. Impossible d'effacer le
// texte déjà imprimé en terminal — on referme juste la ligne reasoning.
if inReason {
fmt.Print("\n")
inReason = false
}
case ev.ToolUsed != nil:
if ev.ToolUsed.Done || ev.ToolUsed.Typing {
break // résultat / frappe live affichés côté web ; en terminal on garde l'annonce seule
+26 -1
View File
@@ -209,13 +209,18 @@ func EnabledTools(caps Caps) []Tool {
}
// StreamEvent is what a ChatCallback receives for each piece of streamed output.
// Exactly one of {Content, Reasoning, ToolUsed, Stats, Err} is set per call.
// Exactly one of {Content, Reasoning, ToolUsed, Stats, Err, DropReasoning} is set
// per call.
type StreamEvent struct {
Content string
Reasoning string
ToolUsed *ToolUsedEvent
Stats *StatsEvent
Err error
// DropReasoning demande à l'UI de retirer la dernière bulle de raisonnement :
// le modèle a « pensé sans agir » et on relance le tour, ce raisonnement-là
// est mort-né et ne doit pas rester à l'écran (sinon double raisonnement).
DropReasoning bool
}
type ToolUsedEvent struct {
Name string
@@ -346,6 +351,11 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
// several times, it's stuck — break instead of spinning to the iteration cap.
lastSig := ""
repeatSig := 0
// Garde-fou « pensé sans agir » : certains modèles à reasoning planifient un
// appel d'outil dans leur <think> puis émettent le token de fin SANS l'émettre
// (ni réponse, ni tool_call). On relance alors UNE fois le tour avec un nudge
// explicite au lieu d'afficher « pas de réponse ».
nudged := false
for iter := 0; iter < 8; iter++ {
payload := map[string]any{
"model": "jean",
@@ -695,6 +705,21 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
// (empty content, e.g. it stopped right after a tool result), say so
// instead of leaving the user staring at a silent, finished chat.
if strings.TrimSpace(assistantContent.String()) == "" {
// Filet de sécurité (le vrai fix est le prompt court, voir baseSystemPrompt) :
// si un modèle « pense sans agir » malgré tout, on le relance UNE fois avec
// une consigne impérative au lieu d'afficher « pas de réponse ».
if len(tools) > 0 && !disableTools && !nudged {
nudged = true
// Le raisonnement de ce tour avorté ne mène à rien : on demande à
// l'UI de l'effacer avant de relancer, pour ne pas afficher deux
// blocs de réflexion successifs.
cb(StreamEvent{DropReasoning: true})
messages = append(messages, Message{
Role: "user",
Content: "Tu as réfléchi mais tu n'as ni appelé d'outil ni répondu. Agis MAINTENANT : appelle directement l'outil approprié (par ex. mem_search/mem_read/bash), ou donne ta réponse finale si tu as déjà l'information. N'explique pas, agis.",
})
continue
}
cb(StreamEvent{Content: "_(le modèle n'a pas produit de réponse — finish: " + finishReason + ")_"})
}
return extra, nil
+12 -33
View File
@@ -30,44 +30,23 @@ func baseSystemPrompt(caps Caps) string {
if !caps.Agent {
return ""
}
cwd, _ := os.Getwd()
var b strings.Builder
b.WriteString("You are an expert coding assistant operating inside jean, a coding agent harness. You help users by reading files, executing commands, editing code, writing new files, and leveraging persistent memory to remember things across sessions.\n\n")
b.WriteString("Available tools:\n")
// Prompt VOLONTAIREMENT court. Un préambule verbeux (longue liste de
// « guidelines », surtout des méta-instructions sur la réflexion) fait
// sur-raisonner les modèles à reasoning (Qwen3) : ils émettent leur <think>
// puis le token de fin SANS appeler d'outil (~25-45 % de tours « morts »
// mesurés). Une version courte et directe ramène ça à 0 %. NE PAS regonfler.
b.WriteString("You are jean, an expert assistant operating directly on this machine with real tools.\n\n")
b.WriteString("Tools:\n")
for _, l := range []string{
"bash: Execute a shell command on this machine (ls, grep, find, cat, sed, read logs, run scripts…) and return stdout, stderr and the exit code. This is your real access to the system.",
"edit: Modify a file on disk by exact replacement (old → new; old must match exactly once). Use this to patch a file instead of rewriting it whole.",
"mem_search: Search your memory (Markdown pages under MEMORY/). Returns a ranked list of {file, title, snippet}.",
"mem_read: Read a memory page (Markdown file), line-numbered, with optional offset/limit.",
"mem_add: Create a new memory page (one topic per page, descriptive kebab-case name, first line is a short # title).",
"mem_edit: Edit a memory page by exact replacement (old → new; old must match exactly once).",
"bash — run a shell command on this machine (inspect files, processes, logs, run scripts).",
"edit — patch a file by exact replacement (old → new, old must be unique).",
"mem_search / mem_read / mem_add / mem_edit — your persistent Markdown memory under MEMORY/.",
} {
b.WriteString("- " + l + "\n")
}
b.WriteString("\nGuidelines:\n")
for _, l := range []string{
"Reason briefly (1-2 sentences) then ACT. As soon as an action is possible, call the tool immediately — don't announce it, do it.",
"Never repeat the same reasoning step or the same tool call twice. If you hesitate (\"maybe I should check…\"), stop thinking and call the tool now.",
"Use bash for any system question (files, processes, network, OS) instead of assuming — you have real access, don't ask permission to inspect.",
"Use bash to read files (cat, sed -n); to change a file, use the edit tool (old → new) instead of rewriting it whole with bash.",
"Use mem_search FIRST whenever the user asks about something you might already know (preferences, ongoing projects, past decisions). Open the most relevant page with mem_read; snippets are hints, not the answer.",
"Save anything worth keeping across sessions with mem_add; update an existing page with mem_edit. Don't save ephemeral conversation state.",
"Prefer targeted, non-interactive commands; chain them with && when they depend on each other.",
"Avoid destructive commands (rm -rf, mkfs, dd…) unless the user explicitly asks.",
"If a command fails, read stderr and the exit code, then fix it — don't re-run the same command unchanged.",
"Never invent a tool: you only have bash, edit and the mem_* tools. Anything else (reading code, web access) goes through bash.",
"When the task is done, give a short final answer and stop. Don't chain useless tool calls.",
"Be concise in your responses.",
"Show file paths clearly when working with files.",
} {
b.WriteString("- " + l + "\n")
}
b.WriteString("\nCurrent date: " + time.Now().Format("2006-01-02"))
b.WriteString("\nCurrent working directory: " + cwd)
b.WriteString("\nWhen the user asks about something you might already know (preferences, projects, past decisions), call mem_search first, then mem_read the most relevant page. For anything about the system or files, use bash instead of guessing. Act immediately — call the right tool, or answer if you already have the info. Never end your turn after only thinking. Be concise.\n")
b.WriteString("\nDate: " + time.Now().Format("2006-01-02"))
return b.String()
}
+8
View File
@@ -954,6 +954,14 @@ async function send(){
if(tu.name==='mem_add' || tu.name==='mem_edit') loadMem();
}
}
if(d.drop_reasoning){
// Le modèle a « pensé sans agir » : on relance le tour. Le
// raisonnement mort-né est retiré pour ne pas afficher un double bloc.
if(reasonEl){
const i=turnCollapsibles.indexOf(reasonEl); if(i>=0) turnCollapsibles.splice(i,1);
reasonEl.remove(); reasonEl=null; fullReason='';
}
}
if(d.reasoning_content){
// Start a fresh buffer per bubble — otherwise a reasoning block that
// follows a tool call re-renders ALL previously accumulated reasoning
+3
View File
@@ -680,6 +680,9 @@ func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any)
if ev.Stats != nil {
return emit(map[string]any{"stats": ev.Stats})
}
if ev.DropReasoning {
return emit(map[string]any{"drop_reasoning": true})
}
if ev.Reasoning != "" {
return emit(map[string]any{"reasoning_content": ev.Reasoning})
}