From a745d2399e4ca61d9d40fef50a04c5148352f07a Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Fri, 2 Oct 2026 23:32:29 +0200 Subject: [PATCH] =?UTF-8?q?Agent=20:=20read=5Ffile,=20str=5Freplace,=20old?= =?UTF-8?q?=5Fstring=E2=80=A6=20traduits=20vers=20les=20vrais=20outils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Généralisation du transformSubAgentAliases d'OpenFox. Les petits modèles ont appris d'autres agents : read_file, str_replace, run_command, ou path / old_string au lieu de file / old. L'appel tombait sur « outil inconnu » ou sur un argument manquant, et le tour se perdait en allers-retours. Le nom et les arguments sont traduits vers l'outil réel quand la cible est disponible dans ce tour, avant que l'appel soit rangé dans l'historique (le modèle y relit l'appel tel qu'exécuté). Un sous-agent appelé comme un outil (« explorer », « code_reviewer ») devient subagent{role, task}. Un appel déjà correct, ou dont la cible n'est pas offerte, reste intact. Co-Authored-By: Claude Opus 5.5 --- internal/loki/code_alias.go | 102 +++++++++++++++++++++++++++++++ internal/loki/code_alias_test.go | 56 +++++++++++++++++ internal/loki/llm_client.go | 5 ++ 3 files changed, 163 insertions(+) create mode 100644 internal/loki/code_alias.go create mode 100644 internal/loki/code_alias_test.go diff --git a/internal/loki/code_alias.go b/internal/loki/code_alias.go new file mode 100644 index 0000000..06075a5 --- /dev/null +++ b/internal/loki/code_alias.go @@ -0,0 +1,102 @@ +package loki + +// code_alias.go — réparation des noms d'outils et d'arguments hallucinés. +// +// Les petits modèles ont appris d'autres agents : ils appellent read_file, +// str_replace, run_command, ou passent path / old_string au lieu de file / +// old. L'appel tombait sur « outil inconnu » ou sur un argument manquant, et +// le tour se perdait en allers-retours. On traduit vers l'outil RÉEL quand la +// cible existe dans ce tour (généralisation du transformSubAgentAliases +// d'OpenFox : un outil nommé d'après un sous-agent devient subagent{role}). + +import ( + "encoding/json" + "strings" +) + +// toolNameAliases : nom halluciné → outil de Loki. +var toolNameAliases = map[string]string{ + "read_file": "read", "view": "read", "cat": "read", "open_file": "read", + "write_file": "write", "create_file": "write", + "edit_file": "edit", "str_replace": "edit", "replace_in_file": "edit", "str_replace_editor": "edit", + "run_command": "bash", "shell": "bash", "run_shell": "bash", "execute_command": "bash", "terminal": "bash", + "search": "grep", "grep_search": "grep", "search_files": "grep", + "find_files": "glob", "list_files": "glob", "file_search": "glob", +} + +// toolArgAliases : par outil, argument halluciné → argument attendu. +var toolArgAliases = map[string]map[string]string{ + "read": {"path": "file", "file_path": "file", "filename": "file", "filepath": "file"}, + "write": {"path": "file", "file_path": "file", "filename": "file", "filepath": "file", "contents": "content", "text": "content"}, + "edit": {"path": "file", "file_path": "file", "filename": "file", "filepath": "file", + "old_string": "old", "old_str": "old", "old_text": "old", "search": "old", + "new_string": "new", "new_str": "new", "new_text": "new", "replace": "new"}, + "bash": {"cmd": "command"}, + "bash_bg": {"cmd": "command"}, + "grep": {"query": "pattern", "regex": "pattern"}, + "glob": {"query": "pattern"}, +} + +// repairToolCall corrige en place le nom et les arguments d'un appel quand ils +// visent, sous un autre nom, un outil disponible dans ce tour. Renvoie true si +// l'appel a été modifié. Les arguments doivent déjà être du JSON valide. +func repairToolCall(tc *ToolCall, tools []Tool) bool { + have := make(map[string]bool, len(tools)) + for _, t := range tools { + have[t.Function.Name] = true + } + name := tc.Function.Name + var args map[string]any + if err := json.Unmarshal([]byte(tc.Function.Arguments), &args); err != nil || args == nil { + args = map[string]any{} + } + changed := false + if !have[name] { + lower := strings.ToLower(strings.TrimSpace(name)) + if t := toolNameAliases[lower]; t != "" && have[t] { + name, changed = t, true + } else if have["subagent"] { + // Un sous-agent appelé comme un outil (« explorer », « code_reviewer »). + role := strings.ReplaceAll(lower, "_", "-") + for _, r := range subagentRoles { + if role == r { + if _, ok := args["role"]; !ok { + args["role"] = r + } + if _, ok := args["task"]; !ok { + for _, k := range []string{"prompt", "query", "question", "description"} { + if v, ok := args[k].(string); ok && v != "" { + args["task"] = v + delete(args, k) + break + } + } + } + name, changed = "subagent", true + break + } + } + } + } + for from, to := range toolArgAliases[name] { + v, ok := args[from] + if !ok { + continue + } + if _, exists := args[to]; !exists { + args[to] = v + changed = true + } + delete(args, from) + } + if !changed { + return false + } + b, err := json.Marshal(args) + if err != nil { + return false + } + tc.Function.Name = name + tc.Function.Arguments = string(b) + return true +} diff --git a/internal/loki/code_alias_test.go b/internal/loki/code_alias_test.go new file mode 100644 index 0000000..8936f9d --- /dev/null +++ b/internal/loki/code_alias_test.go @@ -0,0 +1,56 @@ +package loki + +import ( + "encoding/json" + "testing" +) + +func outils(noms ...string) []Tool { + var ts []Tool + for _, n := range noms { + ts = append(ts, Tool{Type: "function", Function: ToolFunction{Name: n}}) + } + return ts +} + +// Noms et arguments appris d'autres agents : traduits vers les outils de Loki. +func TestReparationDesAppelsHallucines(t *testing.T) { + tools := outils("read", "edit", "bash", "subagent") + cas := []struct { + name, args, wantName string + wantArgs map[string]any + }{ + {"read_file", `{"path":"a.go"}`, "read", map[string]any{"file": "a.go"}}, + {"str_replace", `{"file_path":"a.go","old_string":"x","new_string":"y"}`, "edit", map[string]any{"file": "a.go", "old": "x", "new": "y"}}, + {"run_command", `{"cmd":"ls"}`, "bash", map[string]any{"command": "ls"}}, + {"edit", `{"file":"a.go","old_str":"x","new":"y"}`, "edit", map[string]any{"file": "a.go", "old": "x", "new": "y"}}, + {"code_reviewer", `{"prompt":"relis"}`, "subagent", map[string]any{"role": "code-reviewer", "task": "relis"}}, + } + for _, c := range cas { + tc := ToolCall{Function: ToolCallFunc{Name: c.name, Arguments: c.args}} + if !repairToolCall(&tc, tools) { + t.Errorf("%s : non réparé", c.name) + continue + } + var got map[string]any + _ = json.Unmarshal([]byte(tc.Function.Arguments), &got) + if tc.Function.Name != c.wantName || len(got) != len(c.wantArgs) { + t.Errorf("%s → %s %v", c.name, tc.Function.Name, got) + continue + } + for k, v := range c.wantArgs { + if got[k] != v { + t.Errorf("%s : %s=%v, attendu %v", c.name, k, got[k], v) + } + } + } + // Outil cible absent de ce tour, ou appel déjà correct : intact. + for _, tc := range []ToolCall{ + {Function: ToolCallFunc{Name: "write_file", Arguments: `{"path":"a"}`}}, + {Function: ToolCallFunc{Name: "read", Arguments: `{"file":"a"}`}}, + } { + if repairToolCall(&tc, tools) { + t.Errorf("modifié à tort : %+v", tc) + } + } +} diff --git a/internal/loki/llm_client.go b/internal/loki/llm_client.go index c11381c..a92cd51 100644 --- a/internal/loki/llm_client.go +++ b/internal/loki/llm_client.go @@ -1530,6 +1530,11 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps badArgs[tc.ID] = true } tc.Function.Arguments = "{}" + } else { + // Nom ou argument appris d'un autre agent (read_file, old_string…) : + // traduit vers l'outil réel, AVANT de ranger l'appel dans + // l'historique — le modèle y relit l'appel tel qu'exécuté. + repairToolCall(&tc, tools) } tcs = append(tcs, tc) }