Navigateur piloté, images redressées : reprises d'AJEAN

L'amont (AJEAN 0.15) donne à l'IA le contrôle d'un navigateur ; Loki
embarquait déjà un Chromium pour `web_screenshot` sans jamais le piloter.

Contrôle du navigateur (computer_use.go, computer_cdp.go, browser_grid.go,
portés d'AJEAN) : l'IA ouvre une page en CDP, en reçoit les éléments
interactifs NUMÉROTÉS et agit par numéro (browser_open/snapshot/find/click/
type/key/scroll). Aucune vision requise — un petit modèle texte s'en sort.
Avec un projecteur chargé s'ajoutent browser_screenshot (image quadrillée)
et browser_click_xy. Interrupteur dédié (Réglages, `loki computer`,
/api/computer), sous le mode agent : cliquer et taper dans une page sont des
actions réelles, même niveau de confiance que bash.

Adaptation au conteneur : chromePath cherche D'ABORD le Chromium de
Playwright (PLAYWRIGHT_BROWSERS_PATH, /opt/pw-browsers), le seul navigateur
de l'image — sinon la fonctionnalité se déclarait absente là où le
navigateur est présent. LOKI_CHROME force un chemin, LOKI_CU_HEADFUL ouvre
une vraie fenêtre.

Préparation des images (web_upload_orient.go, porté d'AJEAN) : l'orientation
EXIF est cuite dans les pixels — le projecteur l'ignore et voyait les photos
de téléphone couchées — et le grand côté ramené sous 1568 px. Appliquée aux
pièces jointes, à see_image et aux captures.

Dédup des appels d'outils : bash, bash_bg, bash_tail, see_image et les
browser_* ne sont plus court-circuités sur un appel identique. Relancer la
même commande après avoir modifié un fichier est légitime, et un outil qui
porte une image dans un message à part renvoyait « [déjà fait] » SANS
l'image — le modèle tournait en boucle.

Mode code : le builder ne publie plus de lui-même (pas de commit/push/reset/
rebase ni de redémarrage de service sans demande explicite), reprise de la
leçon d'AJEAN 0.15.4 ; l'inspection en lecture seule reste encouragée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
This commit is contained in:
Claude committed 2026-09-22 13:19:37 +00:00
1 parent b5904907e8
commit 00b2350fb9
21 files changed
+2503 -19

No files matched your search

+58 -2
View File
@@ -202,6 +202,9 @@ type Caps struct {
// "verifier" (passe de vérification, seule autorisée à marquer un critère
// passed), "explorer", "code-reviewer". Voir code_roles.go.
Role string
// ComputerUse = pilotage du navigateur du conteneur (outils browser_*).
// Requiert aussi Agent (mêmes actions réelles que bash). Voir computer_use.go.
ComputerUse bool
}
// globalCaps reads the machine-wide config — the default when a request doesn't
@@ -219,7 +222,7 @@ func globalCaps() Caps {
if !agent {
return Caps{Agent: false, Internet: false, Mem: MemOff}
}
return Caps{Agent: true, Internet: internetEnabled() && crawlReachable(), Mem: memMode()}
return Caps{Agent: true, Internet: internetEnabled() && crawlReachable(), Mem: memMode(), ComputerUse: computerUseEnabled()}
}
// InjectSkills prepends context system messages to msgs: the decisive-agent
@@ -369,6 +372,14 @@ func EnabledTools(caps Caps) []Tool {
if caps.Agent && (caps.Internet || caps.Code) && screenshotAvailable() {
tools = append(tools, webScreenshotTool())
}
// Pilotage de navigateur (browser_*) : l'IA ouvre une page, lit ses éléments
// interactifs NUMÉROTÉS et agit par numéro. Ce sont des actions réelles sur
// le web (cliquer, taper, valider un formulaire) : même niveau de confiance
// que bash, donc mode agent requis, plus son propre interrupteur (Réglages).
// Voir computer_use.go.
if caps.Agent && caps.ComputerUse {
tools = append(tools, computerUseTools()...)
}
// Outils MCP : serveurs tiers configurés par le propriétaire de la machine.
// Comme bash, ils exécutent du code arbitraire côté hôte → réservés au mode
// agent. La découverte est paresseuse et cachée (voir mcp_client.go).
@@ -458,6 +469,33 @@ func shownResult(s string) string {
return s
}
// dedupableTool indique si un appel RIGOUREUSEMENT identique (même outil, mêmes
// arguments) doit être court-circuité au lieu d'être rejoué. Vrai pour les outils
// où un ré-appel identique n'a pas de sens ou produit une fausse erreur (rejouer
// une écriture déjà appliquée → « old introuvable »).
//
// FAUX pour bash : relancer la MÊME commande est un usage courant et légitime —
// l'IA crée un fichier, le lance, le modifie, puis le RELANCE avec la même ligne ;
// le résultat change parce que le fichier a changé. La dédup la bloquait par un
// « [déjà fait] » exaspérant. bash a des effets de bord : on le laisse toujours
// s'exécuter. Même raison pour bash_bg / bash_tail (suivre un job, c'est relire
// la même sortie qui, elle, avance).
//
// FAUX aussi pour see_image et tout le pilotage de navigateur (browser_*) :
// - leur résultat dépend de l'ÉTAT VIVANT de la page (un browser_snapshot sans
// argument a forcément la même clé de dédup à chaque appel, alors que la page
// a changé) — les dédupliquer fige l'IA sur un vieux cliché ;
// - surtout, browser_screenshot et see_image portent leur IMAGE dans un message
// à part (visionImg) que le chemin de dédup ne rejoue PAS : l'IA recevrait
// « [déjà fait] » SANS l'image et tournerait en boucle.
func dedupableTool(name string) bool {
switch name {
case "bash", "bash_bg", "bash_tail", "see_image":
return false
}
return !strings.HasPrefix(name, "browser_")
}
// repeatedCallResult construit ce qu'on renvoie quand le modèle redemande un
// appel RIGOUREUSEMENT identique (même outil, mêmes arguments) dans le même tour.
// L'appel n'est jamais rejoué — on répond depuis le résultat mémorisé.
@@ -1228,7 +1266,7 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
// même écriture ; la rejouer produisait une fausse erreur (« old
// introuvable », puisque le remplacement est déjà fait).
callKey := tc.Function.Name + "\x00" + tc.Function.Arguments
if prev, seen := doneCalls[callKey]; seen {
if prev, seen := doneCalls[callKey]; seen && dedupableTool(tc.Function.Name) {
repeatCount[callKey]++
result = repeatedCallResult(prev, repeatCount[callKey])
cb(StreamEvent{ToolUsed: &ToolUsedEvent{Name: tc.Function.Name, Label: label, Result: shownResult(result), Done: true}})
@@ -1381,6 +1419,24 @@ func runChat(ctx context.Context, messages []Message, temperature float64, caps
result = capWebOutput(toolWebGrep(args))
case "web_screenshot":
result = toolWebScreenshot(args, caps)
case "browser_open":
result = capCUOutput(toolCUOpen(args))
case "browser_snapshot":
result = capCUOutput(toolCUSnapshot())
case "browser_find":
result = capCUOutput(toolCUFind(args))
case "browser_click":
result = capCUOutput(toolCUClick(args))
case "browser_click_xy":
result = capCUOutput(toolCUClickXY(args))
case "browser_type":
result = capCUOutput(toolCUType(args))
case "browser_key":
result = capCUOutput(toolCUKey(args))
case "browser_scroll":
result = capCUOutput(toolCUScroll(args))
case "browser_screenshot":
result, visionImg = toolCUScreenshot()
default:
if isMCPTool(tc.Function.Name) {
result = mcpCall(tc.Function.Name, args)