Files
Loki/internal/loki/tool_results.go
T
MichaelandClaude Opus 5.5 ae0e181711 Outils : aperçu dans le flux, « voir plus » à la demande, coupes annoncées
Repris d'AJEAN 0.15.1 → 0.15.7 (tool_results.go adapté : pas de
chiffrement chez nous).

VOIR PLUS
Chaque résultat d'outil partait en entier dans le flux (jusqu'à 12 000
caractères) et dans le journal rejoué à chaque ouverture : un long fil
d'agent en transportait des centaines. Le flux ne porte plus qu'un aperçu
de 1 600 caractères, la taille réelle (le « ~N tok » de la bulle reste
juste) et un id. Le résultat complet est rangé dans le bucket `toolres`,
sous « <discussion>.<aléa> » — un id propre, pas le tool_call_id que
certains parseurs recyclent. « voir plus » le charge au clic via
/api/chat/tool-result et lève le plafond de hauteur du bloc ; il se range
avec « copier » dans une barre au coin du résultat. Les résultats partent
avec leur discussion (convDelete) ; les orphelins sont nettoyés toutes les
200 écritures.

COUPES ANNONCÉES
- Terminal : une sortie de plus de 8 000 caractères était coupée en
  silence ; le modèle croyait tout voir. Elle commence désormais par
  « [sortie tronquée : N caractères au total, seuls les 8000 derniers sont
  gardés] ».
- MCP : l'amont transmet désormais la réponse en entier. On s'en approche
  sans renoncer au garde-fou (65k de contexte) : le plafond suit la
  fenêtre (CTX caractères, ≈ un quart de la fenêtre, jamais sous les
  12 000 d'avant), et la coupe dit la taille réelle.

Vérifié dans le navigateur avec un faux moteur qui appelle bash : aperçu
de 1 600 caractères, « voir plus » charge les 8 099, mention de coupe en
tête.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:08:46 +02:00

114 lines
3.1 KiB
Go

package loki
import (
"crypto/rand"
"encoding/hex"
"strings"
"sync/atomic"
bolt "go.etcd.io/bbolt"
)
// tool_results.go — résultats COMPLETS des outils, pour le bouton « voir plus ».
// Repris d'AJEAN 0.15.5 / 0.15.7, sans le chiffrement (Loki n'en a pas).
//
// Le flux n'envoie à l'UI qu'un aperçu (toolPreviewChars) ; le reste se charge
// au clic. Il ne peut pas être cherché dans conv.Messages : pendant un tour
// d'agent, les messages tool n'y sont versés qu'à la FIN du tour, et après un
// compactage les anciens ont disparu de la vue modèle. On garde donc chaque
// résultat coupé dans un bucket dédié, sous un id PROPRE (pas le tool_call_id,
// que certains parseurs réutilisent d'un tour à l'autre).
//
// Rangement PAR DISCUSSION : la clé est « <id de discussion>.<aléa> ». Les
// résultats vivent aussi longtemps que leur discussion (supprimés avec elle,
// voir deleteToolResultsFor) ; les orphelins sont nettoyés de temps en temps.
const (
bkToolRes = "toolres"
toolResPruneGap = 200 // nettoyage tenté toutes les N écritures
)
var toolResWrites atomic.Int64
func toolResID(sid string) string {
var b [12]byte
if _, err := rand.Read(b[:]); err != nil {
return ""
}
if sid = strings.TrimSpace(sid); sid == "" {
sid = "nosession"
}
return sid + "." + hex.EncodeToString(b[:])
}
// saveToolResult enregistre un résultat complet de la discussion ACTIVE et
// renvoie son id ("" en cas d'échec : l'appelant envoie alors le résultat
// entier dans le flux).
func saveToolResult(result string) string {
id := toolResID(getStr(bkChat, ckActive))
if id == "" || putBytes(bkToolRes, id, []byte(result)) != nil {
return ""
}
if toolResWrites.Add(1)%toolResPruneGap == 0 {
go pruneToolResults()
}
return id
}
// loadToolResult relit un résultat enregistré.
func loadToolResult(id string) (string, bool) {
if strings.ContainsAny(id, "/\\") {
return "", false
}
b, err := getBytesErr(bkToolRes, id)
if err != nil || b == nil {
return "", false
}
return string(b), true
}
// deleteToolResultsFor supprime les résultats d'une discussion supprimée.
func deleteToolResultsFor(sid string) {
if sid == "" {
return
}
prefix := sid + "."
_ = update(bkToolRes, func(b *bolt.Bucket) error {
c := b.Cursor()
var keys [][]byte
for k, _ := c.Seek([]byte(prefix)); k != nil && strings.HasPrefix(string(k), prefix); k, _ = c.Next() {
keys = append(keys, append([]byte(nil), k...))
}
for _, k := range keys {
if err := b.Delete(k); err != nil {
return err
}
}
return nil
})
}
// pruneToolResults supprime les résultats de discussions qui n'existent plus.
func pruneToolResults() {
alive := map[string]bool{getStr(bkChat, ckActive): true}
for _, m := range convIndex() {
alive[m.ID] = true
}
_ = update(bkToolRes, func(b *bolt.Bucket) error {
var orphans []string
_ = b.ForEach(func(k, _ []byte) error {
key := string(k)
if i := strings.LastIndexByte(key, '.'); i > 0 && key[:i] != "nosession" && !alive[key[:i]] {
orphans = append(orphans, key)
}
return nil
})
for _, k := range orphans {
if err := b.Delete([]byte(k)); err != nil {
return err
}
}
return nil
})
}