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. Chiffré comme les discussions quand le // chiffrement mémoire est actif (bkToolRes figure dans encryptedBuckets). // // 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 « . ». 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)) // Chiffrement actif mais mémoire verrouillée : putStoreBytes refuse d'écrire // en clair, et l'appelant envoie alors le résultat entier dans le flux. if id == "" || putStoreBytes(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 := getStoreBytesErr(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() { // Mémoire verrouillée : l'index des discussions est illisible et reviendrait // vide — tout passerait pour orphelin. On attend le déverrouillage. if memEncActive() && !memUnlocked() { return } 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 }) }