Cache : corrections de relecture — la ligne MCP et les descriptions d'outils partagent un seul format

mcpPromptLine relisait le préfixe « [MCP: <serveur>] » des descriptions en le
réécrivant à la main : changer le format dans mcpTools aurait fait disparaître
la ligne en silence. Un nom de serveur contenant « ] » y était aussi coupé.

- mcpTagDescription pose le préfixe, mcpToolServer le relit : un seul endroit.
- mcpToolServer retient la coupure dont la forme assainie est celle du nom
  d'outil — « a] b » reste « a] b ».
- Les tests qui appellent EnabledTools isolent $LOKI_HOME : la configuration
  MCP de la machine de test (et ses connexions) ne fausse plus le cas « sans
  outil MCP » ni le budget du préambule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5.5 committed 2026-10-04 01:34:36 +02:00
1 parent 7dd319a058
commit eaf5e313d9
3 files changed
+48 -15

No files matched your search

+3
View File
@@ -49,6 +49,9 @@ import (
const promptCharBudget = 11000 // ~2750 tokens, tout allumé
func TestSystemPromptStaysLean(t *testing.T) {
// $LOKI_HOME isolé : les outils MCP configurés sur la machine de test (et
// leur ligne au préambule) ne comptent pas dans le budget de Loki.
testHome(t)
caps := Caps{Agent: true, Internet: true, Mem: MemAlways}
tools := EnabledTools(caps)
sp := baseSystemPrompt(caps, tools)
+36 -14
View File
@@ -307,7 +307,7 @@ func mcpTools() []Tool {
}
// Préfixe le serveur d'origine dans la description : aide le modèle à
// choisir entre outils similaires de serveurs différents.
desc = "[MCP: " + name + "] " + desc
desc = mcpTagDescription(name, desc)
out = append(out, Tool{
Type: "function",
Function: ToolFunction{
@@ -449,6 +449,39 @@ func flattenMCPContent(res *mcpsdk.CallToolResult) string {
return text
}
// mcpTagDescription préfixe la description d'un outil MCP par le nom d'origine
// de son serveur, que le nom d'outil (assaini) ne garde pas. mcpToolServer le
// relit pour la ligne du préambule : ce format n'existe qu'ici, pour que les
// deux ne puissent pas diverger en silence.
func mcpTagDescription(server, desc string) string {
return "[MCP: " + server + "] " + desc
}
// mcpToolServer retrouve le serveur d'origine d'un outil MCP annoncé, d'après
// le préfixe posé par mcpTagDescription. Le nom brut d'un serveur peut lui-même
// contenir « ] » : on retient la première coupure dont la forme assainie est
// bien celle qui figure dans le nom de l'outil.
func mcpToolServer(t Tool) (string, bool) {
name := t.Function.Name
if !strings.HasPrefix(name, "mcp__") {
return "", false
}
rest, ok := strings.CutPrefix(t.Function.Description, "[MCP: ")
if !ok {
return "", false
}
for i := 0; ; {
j := strings.Index(rest[i:], "] ")
if j < 0 {
return "", false
}
if server := rest[:i+j]; strings.HasPrefix(name, "mcp__"+mcpSanitize(server)+"__") {
return server, true
}
i += j + 1
}
}
// mcpPromptLine renvoie une ligne système listant les serveurs MCP annoncés et
// leur nombre d'outils, pour situer le modèle. Elle se DÉDUIT des outils
// réellement envoyés ce tour-ci (EnabledTools), pas de l'état du pool.
@@ -463,20 +496,9 @@ func flattenMCPContent(res *mcpsdk.CallToolResult) string {
func mcpPromptLine(tools []Tool) string {
counts := map[string]int{}
for _, t := range tools {
if !strings.HasPrefix(t.Function.Name, "mcp__") {
continue
if server, ok := mcpToolServer(t); ok {
counts[server]++
}
// mcpTools préfixe chaque description par « [MCP: <serveur>] » : le nom
// d'origine du serveur, que le nom d'outil (assaini) ne garde pas.
rest, ok := strings.CutPrefix(t.Function.Description, "[MCP: ")
if !ok {
continue
}
server, _, ok := strings.Cut(rest, "] ")
if !ok {
continue
}
counts[server]++
}
if len(counts) == 0 {
return ""
+9 -1
View File
@@ -8,7 +8,7 @@ import (
func mcpTestTool(server, tool string) Tool {
return Tool{Type: "function", Function: ToolFunction{
Name: mcpExposedName(server, tool),
Description: "[MCP: " + server + "] décrit " + tool,
Description: mcpTagDescription(server, "décrit "+tool),
}}
}
@@ -32,6 +32,10 @@ func TestMCPPromptLineDeriveDesOutils(t *testing.T) {
""},
{"description vide après le préfixe", []Tool{{Type: "function", Function: ToolFunction{Name: "mcp__x__y", Description: "[MCP: x] "}}},
"x (1)"},
{"nom de serveur contenant « ] »", []Tool{mcpTestTool("a] b", "t"), mcpTestTool("a] b", "u")},
"a] b (2)"},
{"préfixe d'un autre serveur que celui du nom d'outil", []Tool{{Type: "function", Function: ToolFunction{Name: "mcp__x__y", Description: "[MCP: z] y"}}},
""},
}
for _, c := range cases {
got := mcpPromptLine(c.tools)
@@ -51,6 +55,10 @@ func TestMCPPromptLineDeriveDesOutils(t *testing.T) {
// planner (aucun outil MCP) n'a pas de ligne MCP, le fil principal l'a dès le
// premier tour.
func TestBaseSystemPromptLigneMCPSuitLesOutils(t *testing.T) {
// $LOKI_HOME isolé : EnabledTools lit la configuration MCP, et celle de la
// machine de test (serveurs réels, connexions comprises) fausserait le cas
// « sans outil MCP ».
testHome(t)
caps := Caps{Agent: true, Mem: MemOff}
with := append(EnabledTools(caps), mcpTestTool("github", "issues"))
if sp := baseSystemPrompt(caps, with); !strings.Contains(sp, "MCP servers connected") || !strings.Contains(sp, "github (1)") {