mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
Le builder délègue à un rôle en lecture seule (explorer, code-reviewer, planner) sans passer par /tool/exec : src/chat/subagent.rs porte toolSubagent. Mêmes refus avant tout appel au moteur (rôle inconnu, verifier compris, tâche vide, récursion), contexte isolé servi par le Go (POST /internal/v1/turn/subagent : prompt du rôle + briefing machine, outils, capacités ; subagentView et subagentFraming partagés), tour imbriqué run_turn avec l'endpoint, le raisonnement, l'écho et l'arrêt du tour qui délègue, température 0,6, travail annexe (second slot, effacé à la fin), télémétrie à part. Seul le texte écrit après le dernier outil revient, rogné, préfixé du rôle, tronqué à 6000 runes. Le tour du rôle est porté par SubHost, qui délègue tout à son GoTurnHost sauf un nouvel appel à subagent ; les outils de la façade passent dans GoTurnHost::exec_tool_base, que seul le tour principal complète de subagent (le type du tour imbriqué ne se contient pas lui-même). Parité : journal tiré du Go contre un faux moteur scripté (parity_subagent_test.go → tests/parity_subagent/subagent.json : refus, réponse directe, rapport après deux outils, texte vide relancé, rapport tronqué, cadrage du planner, moteur qui refuse ; requêtes du rôle), rejoué par un tour complet de la façade (tests/lot6.rs) ; TestSubagentVueInterne côté Go. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
86 lines
3.1 KiB
Go
86 lines
3.1 KiB
Go
package loki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Les garde-fous de l'outil subagent doivent répondre SANS appeler le modèle :
|
|
// un rôle inconnu, une tâche vide ou un appel depuis un sous-agent sont refusés
|
|
// à l'entrée (sinon chaque faute de frappe coûte une génération complète).
|
|
func TestSubagentRefusesBeforeAnyInference(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
args map[string]any
|
|
parent Caps
|
|
want string
|
|
}{
|
|
{"rôle inconnu", map[string]any{"role": "architecte", "task": "x"}, Caps{Code: true}, "rôle inconnu"},
|
|
{"rôle verifier refusé", map[string]any{"role": "verifier", "task": "x"}, Caps{Code: true}, "rôle inconnu"},
|
|
{"tâche vide", map[string]any{"role": "explorer", "task": " "}, Caps{Code: true}, "tâche vide"},
|
|
{"pas de récursion", map[string]any{"role": "explorer", "task": "x"}, Caps{Code: true, Role: "explorer"}, "ne peut pas en appeler un autre"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := toolSubagent(context.Background(), tc.args, tc.parent)
|
|
if !strings.HasPrefix(got, "[erreur]") || !strings.Contains(got, tc.want) {
|
|
t.Fatalf("attendu un refus contenant %q, obtenu %q", tc.want, got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Un rôle délégué travaille en LECTURE SEULE : ni write/edit (les modifications
|
|
// restent dans le fil principal), ni subagent (pas de récursion), et seul le
|
|
// planner pose des critères.
|
|
func TestSubagentRolesAreReadOnly(t *testing.T) {
|
|
forbidden := map[string]bool{"write": true, "edit": true, "subagent": true, "mem_add": true, "mem_edit": true, "web_search": true}
|
|
for _, role := range subagentRoles {
|
|
names := map[string]bool{}
|
|
for _, tool := range EnabledTools(Caps{Agent: true, Code: true, Role: role}) {
|
|
names[tool.Function.Name] = true
|
|
}
|
|
for f := range forbidden {
|
|
if names[f] {
|
|
t.Fatalf("le rôle %s ne devrait pas avoir l'outil %s", role, f)
|
|
}
|
|
}
|
|
if !names["read"] || !names["grep"] {
|
|
t.Fatalf("le rôle %s devrait pouvoir lire le code", role)
|
|
}
|
|
if got := names["criteria"]; got != (role == "planner") {
|
|
t.Fatalf("critères pour %s : %v (attendu %v)", role, got, role == "planner")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lot 47 : la façade demande au Go le contexte isolé d'un rôle délégué —
|
|
// le même prompt système, les mêmes outils et capacités que toolSubagent.
|
|
func TestSubagentVueInterne(t *testing.T) {
|
|
testHome(t)
|
|
t.Setenv(internalTokenEnv, jetonTest)
|
|
w := appelOAIInterne(t, "POST", "/internal/v1/turn/subagent", `{"role":"planner"}`)
|
|
if w.Code != 200 {
|
|
t.Fatalf("vue : %d %s", w.Code, w.Body.String())
|
|
}
|
|
var v struct {
|
|
System string `json:"system"`
|
|
Tools []Tool `json:"tools"`
|
|
Caps Caps `json:"caps"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
caps, system := subagentView("planner")
|
|
if v.System != system || v.Caps != caps || len(v.Tools) != len(EnabledTools(caps)) {
|
|
t.Fatalf("vue : %+v", v.Caps)
|
|
}
|
|
for _, body := range []string{`{"role":"verifier"}`, `{"role":""}`, `{`} {
|
|
if w := appelOAIInterne(t, "POST", "/internal/v1/turn/subagent", body); w.Code != 400 {
|
|
t.Fatalf("%s : %d", body, w.Code)
|
|
}
|
|
}
|
|
}
|