mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 07:28:13 +02:00
v0.2.11 : fix stall reasoning (prompt agent court) + outil edit + filet de sécurité nudge/drop_reasoning
This commit is contained in:
1 parent
4c8aafa898
commit
7a145258d3
5 files changed
+56
-34
No files matched your search
@@ -73,6 +73,13 @@ func cmdChat(args []string) error {
|
|||||||
fmt.Printf("\n%s\n", red("[erreur] "+ev.Err.Error()))
|
fmt.Printf("\n%s\n", red("[erreur] "+ev.Err.Error()))
|
||||||
case ev.Stats != nil:
|
case ev.Stats != nil:
|
||||||
stats = ev.Stats
|
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:
|
case ev.ToolUsed != nil:
|
||||||
if ev.ToolUsed.Done || ev.ToolUsed.Typing {
|
if ev.ToolUsed.Done || ev.ToolUsed.Typing {
|
||||||
break // résultat / frappe live affichés côté web ; en terminal on garde l'annonce seule
|
break // résultat / frappe live affichés côté web ; en terminal on garde l'annonce seule
|
||||||
|
|||||||
@@ -209,13 +209,18 @@ func EnabledTools(caps Caps) []Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// StreamEvent is what a ChatCallback receives for each piece of streamed output.
|
// 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 {
|
type StreamEvent struct {
|
||||||
Content string
|
Content string
|
||||||
Reasoning string
|
Reasoning string
|
||||||
ToolUsed *ToolUsedEvent
|
ToolUsed *ToolUsedEvent
|
||||||
Stats *StatsEvent
|
Stats *StatsEvent
|
||||||
Err error
|
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 {
|
type ToolUsedEvent struct {
|
||||||
Name string
|
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.
|
// several times, it's stuck — break instead of spinning to the iteration cap.
|
||||||
lastSig := ""
|
lastSig := ""
|
||||||
repeatSig := 0
|
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++ {
|
for iter := 0; iter < 8; iter++ {
|
||||||
payload := map[string]any{
|
payload := map[string]any{
|
||||||
"model": "jean",
|
"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
|
// (empty content, e.g. it stopped right after a tool result), say so
|
||||||
// instead of leaving the user staring at a silent, finished chat.
|
// instead of leaving the user staring at a silent, finished chat.
|
||||||
if strings.TrimSpace(assistantContent.String()) == "" {
|
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 + ")_"})
|
cb(StreamEvent{Content: "_(le modèle n'a pas produit de réponse — finish: " + finishReason + ")_"})
|
||||||
}
|
}
|
||||||
return extra, nil
|
return extra, nil
|
||||||
|
|||||||
@@ -30,44 +30,23 @@ func baseSystemPrompt(caps Caps) string {
|
|||||||
if !caps.Agent {
|
if !caps.Agent {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
cwd, _ := os.Getwd()
|
|
||||||
|
|
||||||
var b strings.Builder
|
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")
|
// Prompt VOLONTAIREMENT court. Un préambule verbeux (longue liste de
|
||||||
|
// « guidelines », surtout des méta-instructions sur la réflexion) fait
|
||||||
b.WriteString("Available tools:\n")
|
// 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{
|
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.",
|
"bash — run a shell command on this machine (inspect files, processes, logs, run scripts).",
|
||||||
"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.",
|
"edit — patch a file by exact replacement (old → new, old must be unique).",
|
||||||
"mem_search: Search your memory (Markdown pages under MEMORY/). Returns a ranked list of {file, title, snippet}.",
|
"mem_search / mem_read / mem_add / mem_edit — your persistent Markdown memory under MEMORY/.",
|
||||||
"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).",
|
|
||||||
} {
|
} {
|
||||||
b.WriteString("- " + l + "\n")
|
b.WriteString("- " + l + "\n")
|
||||||
}
|
}
|
||||||
|
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("\nGuidelines:\n")
|
b.WriteString("\nDate: " + time.Now().Format("2006-01-02"))
|
||||||
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)
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -954,6 +954,14 @@ async function send(){
|
|||||||
if(tu.name==='mem_add' || tu.name==='mem_edit') loadMem();
|
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){
|
if(d.reasoning_content){
|
||||||
// Start a fresh buffer per bubble — otherwise a reasoning block that
|
// Start a fresh buffer per bubble — otherwise a reasoning block that
|
||||||
// follows a tool call re-renders ALL previously accumulated reasoning
|
// follows a tool call re-renders ALL previously accumulated reasoning
|
||||||
|
|||||||
@@ -680,6 +680,9 @@ func runChatStream(ctx context.Context, body chatReq, emit func(map[string]any)
|
|||||||
if ev.Stats != nil {
|
if ev.Stats != nil {
|
||||||
return emit(map[string]any{"stats": ev.Stats})
|
return emit(map[string]any{"stats": ev.Stats})
|
||||||
}
|
}
|
||||||
|
if ev.DropReasoning {
|
||||||
|
return emit(map[string]any{"drop_reasoning": true})
|
||||||
|
}
|
||||||
if ev.Reasoning != "" {
|
if ev.Reasoning != "" {
|
||||||
return emit(map[string]any{"reasoning_content": ev.Reasoning})
|
return emit(map[string]any{"reasoning_content": ev.Reasoning})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user