mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Mode Code : git sur le dépôt cloné, budget de build, sous-agents moins rigides
Trois trous relevés en comparant avec OpenFox. - git_status / git_diff tournaient à la racine de la discussion, qui n'est souvent pas un dépôt : git_clone range le projet dans un sous-dossier, et le vérificateur — dont la consigne commence par git_diff — tombait sur « not a git repository ». Ils visent maintenant le sous-dossier demandé (dir), la racine si c'est un dépôt, sinon l'unique dépôt présent ; plusieurs : on demande de choisir. - Budget d'outils triplé en mode Code : lire, éditer, compiler, relancer les tests dépasse vite 24 appels sans tourner en rond, et le troisième rappel (« n'appelle plus d'outil ») coupait le build au milieu. - Sous-agents : température 0.6 au lieu de 0 (en glouton, Qwen avec réflexion boucle vite ; le TEMP du preset l'emporte toujours), et seul le texte écrit après le dernier outil revient au builder. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2e0356cd6c
commit
dfafca6fb2
5 files changed
+104
-8
No files matched your search
@@ -26,7 +26,9 @@ func gitStatusTool() Tool {
|
||||
Description: "git status of the working folder (branch + changed files, porcelain format).",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]any{},
|
||||
"properties": map[string]any{
|
||||
"dir": map[string]any{"type": "string", "description": "Repository subfolder (default: auto-detected)"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -42,6 +44,7 @@ func gitDiffTool() Tool {
|
||||
"type": "object",
|
||||
"properties": map[string]any{
|
||||
"file": map[string]any{"type": "string", "description": "Limit the diff to this path"},
|
||||
"dir": map[string]any{"type": "string", "description": "Repository subfolder (default: auto-detected)"},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -66,15 +69,56 @@ func gitCloneTool() Tool {
|
||||
}
|
||||
}
|
||||
|
||||
// gitRepoDir : où lancer git_status / git_diff. La discussion n'est souvent PAS
|
||||
// un dépôt : git_clone range le dépôt dans un sous-dossier, et git, lancé à la
|
||||
// racine, échouait (« not a git repository ») — alors que le vérificateur a
|
||||
// pour consigne de commencer par git_diff. Ordre : le sous-dossier demandé
|
||||
// (dir), la racine si c'est un dépôt, sinon l'unique sous-dossier qui en est
|
||||
// un. Plusieurs candidats : on demande de choisir.
|
||||
func gitRepoDir(dir string) (string, error) {
|
||||
base := agentCwd()
|
||||
if dir = strings.TrimSpace(dir); dir != "" {
|
||||
if strings.Contains(dir, "..") || filepath.IsAbs(dir) {
|
||||
return "", fmt.Errorf("dossier invalide : %s", dir)
|
||||
}
|
||||
return filepath.Join(base, dir), nil
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(base, ".git")); err == nil {
|
||||
return base, nil
|
||||
}
|
||||
entries, _ := os.ReadDir(base)
|
||||
var repos []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(base, e.Name(), ".git")); err == nil {
|
||||
repos = append(repos, e.Name())
|
||||
}
|
||||
}
|
||||
switch len(repos) {
|
||||
case 0:
|
||||
return base, nil // git dira lui-même « not a git repository »
|
||||
case 1:
|
||||
return filepath.Join(base, repos[0]), nil
|
||||
}
|
||||
return "", fmt.Errorf("plusieurs dépôts ici (%s) : précise dir", strings.Join(repos, ", "))
|
||||
}
|
||||
|
||||
// runGit exécute git dans le dossier de la discussion, sortie bornée.
|
||||
func runGit(ctx context.Context, timeout time.Duration, args ...string) string {
|
||||
return runGitIn(ctx, agentCwd(), timeout, args...)
|
||||
}
|
||||
|
||||
// runGitIn exécute git dans dir, sortie bornée.
|
||||
func runGitIn(ctx context.Context, dir string, timeout time.Duration, args ...string) string {
|
||||
if _, err := exec.LookPath("git"); err != nil {
|
||||
return "[erreur] git n'est pas installé sur cette machine"
|
||||
}
|
||||
cctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(cctx, "git", args...)
|
||||
cmd.Dir = agentCwd()
|
||||
cmd.Dir = dir
|
||||
// Jamais d'invite interactive (mot de passe, hôte inconnu) : un process
|
||||
// serveur n'a personne pour y répondre, il faut échouer vite et le dire.
|
||||
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "GIT_SSH_COMMAND=ssh -oBatchMode=yes")
|
||||
@@ -95,24 +139,32 @@ func runGit(ctx context.Context, timeout time.Duration, args ...string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func toolGitStatus(ctx context.Context) string {
|
||||
return runGit(ctx, 20*time.Second, "status", "--porcelain=v1", "--branch")
|
||||
func toolGitStatus(ctx context.Context, args map[string]any) string {
|
||||
dir, err := gitRepoDir(str(args["dir"]))
|
||||
if err != nil {
|
||||
return "[erreur] " + err.Error()
|
||||
}
|
||||
return runGitIn(ctx, dir, 20*time.Second, "status", "--porcelain=v1", "--branch")
|
||||
}
|
||||
|
||||
func toolGitDiff(ctx context.Context, args map[string]any) string {
|
||||
dir, err := gitRepoDir(str(args["dir"]))
|
||||
if err != nil {
|
||||
return "[erreur] " + err.Error()
|
||||
}
|
||||
// Un seul appel qui montre TOUT ce qui a changé : index + arbre de travail.
|
||||
// HEAD peut ne pas exister (dépôt tout neuf) : on retombe sur le diff simple.
|
||||
gitArgs := []string{"diff", "HEAD"}
|
||||
if file, _ := args["file"].(string); strings.TrimSpace(file) != "" {
|
||||
gitArgs = append(gitArgs, "--", file)
|
||||
}
|
||||
out := runGit(ctx, 30*time.Second, gitArgs...)
|
||||
out := runGitIn(ctx, dir, 30*time.Second, gitArgs...)
|
||||
if strings.HasPrefix(out, "[erreur]") && strings.Contains(out, "HEAD") {
|
||||
fallback := []string{"diff"}
|
||||
if file, _ := args["file"].(string); strings.TrimSpace(file) != "" {
|
||||
fallback = append(fallback, "--", file)
|
||||
}
|
||||
out = runGit(ctx, 30*time.Second, fallback...)
|
||||
out = runGitIn(ctx, dir, 30*time.Second, fallback...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -30,6 +30,9 @@ import (
|
||||
// permettrait au builder de se décerner son propre satisfecit.
|
||||
var subagentRoles = []string{"explorer", "code-reviewer", "planner"}
|
||||
|
||||
// subagentTemperature : voir toolSubagent.
|
||||
const subagentTemperature = 0.6
|
||||
|
||||
func isSubagentRole(r string) bool {
|
||||
for _, x := range subagentRoles {
|
||||
if x == r {
|
||||
@@ -102,7 +105,14 @@ func toolSubagent(ctx context.Context, args map[string]any, parent Caps) string
|
||||
var out strings.Builder
|
||||
// Trace jetable : aucun forwardStream, rien de persisté. L'utilisateur voit
|
||||
// la bulle de l'outil `subagent` et son rapport, pas les dix lectures.
|
||||
if _, err := runChat(ctx, msgs, 0, caps, func(ev StreamEvent) bool {
|
||||
// Température 0.6 et non 0 : en glouton, Qwen avec réflexion tourne vite en
|
||||
// boucle (OpenFox l'a relevé). Le TEMP du preset, s'il est posé, l'emporte.
|
||||
// Seul le texte écrit APRÈS le dernier outil revient au builder : la
|
||||
// narration entre deux lectures n'est pas le rapport.
|
||||
if _, err := runChat(ctx, msgs, subagentTemperature, caps, func(ev StreamEvent) bool {
|
||||
if ev.ToolUsed != nil {
|
||||
out.Reset()
|
||||
}
|
||||
if ev.Content != "" {
|
||||
out.WriteString(ev.Content)
|
||||
}
|
||||
|
||||
@@ -157,3 +157,28 @@ func TestEditCRLFMultiligne(t *testing.T) {
|
||||
t.Fatalf("résultat inattendu : %q", b)
|
||||
}
|
||||
}
|
||||
|
||||
// git_status / git_diff trouvent le dépôt cloné dans un sous-dossier de la
|
||||
// discussion, et demandent de choisir quand il y en a plusieurs.
|
||||
func TestGitRepoDirDetecteLeClone(t *testing.T) {
|
||||
ws := withWorkspace(t)
|
||||
if d, err := gitRepoDir(""); err != nil || d != agentCwd() {
|
||||
t.Fatalf("sans dépôt : %q, %v", d, err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(agentCwd(), "projet", ".git"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d, err := gitRepoDir(""); err != nil || filepath.Base(d) != "projet" {
|
||||
t.Fatalf("un clone : %q, %v (ws %s)", d, err, ws)
|
||||
}
|
||||
_ = os.MkdirAll(filepath.Join(agentCwd(), "autre", ".git"), 0o755)
|
||||
if _, err := gitRepoDir(""); err == nil {
|
||||
t.Fatal("deux dépôts : il fallait demander dir")
|
||||
}
|
||||
if d, err := gitRepoDir("autre"); err != nil || filepath.Base(d) != "autre" {
|
||||
t.Fatalf("dir explicite : %q, %v", d, err)
|
||||
}
|
||||
if _, err := gitRepoDir("../dehors"); err == nil {
|
||||
t.Fatal("dir hors de la discussion accepté")
|
||||
}
|
||||
}
|
||||
@@ -89,3 +89,6 @@ func logBudget(calls, budget, sent int) {
|
||||
fmt.Fprintf(os.Stderr, "[agent] %d appels d'outils sur ce tour (palier=%d) — rappel n°%d envoyé au modèle\n",
|
||||
calls, budget, sent)
|
||||
}
|
||||
|
||||
// codeBudgetFactor : multiplicateur du palier en mode Code (voir runChat).
|
||||
const codeBudgetFactor = 3
|
||||
@@ -959,6 +959,12 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
// demande de conclure — de la pression, pas une barrière. Sans ça, un modèle
|
||||
// qui tourne en rond n'avait rien en face de lui sauf le bouton stop.
|
||||
toolRuns, budgetNudges, budget := 0, 0, agentBudget()
|
||||
// Mode Code : lire, éditer, compiler, relancer les tests — un vrai tour de
|
||||
// build dépasse vite 24 appels sans tourner en rond, et le troisième rappel
|
||||
// (« n'appelle plus d'outil ») coupait le travail au milieu. Palier triplé.
|
||||
if caps.Code {
|
||||
budget *= codeBudgetFactor
|
||||
}
|
||||
for iter := 0; ; iter++ {
|
||||
// Ajout en cours de réponse : entre deux étapes (après un appel d'outil ou
|
||||
// une relance), on injecte les messages mis en file par l'utilisateur. Pas
|
||||
@@ -1705,7 +1711,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
|
||||
case "bash_tail":
|
||||
result = toolBashTail(args)
|
||||
case "git_status":
|
||||
result = toolGitStatus(ctx)
|
||||
result = toolGitStatus(ctx, args)
|
||||
case "git_diff":
|
||||
result = toolGitDiff(ctx, args)
|
||||
case "git_clone":
|
||||
|
||||
Reference in new issue
Block a user