mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-23 23:18:13 +02:00
Journal : outils coalescés, curseur de replay borné, export allégé
Trois maux d'une conversation agentique longue : - Chaque appel d'outil s'écrivait en de nombreux événements (annonce, frappe du corps, arguments) jusqu'au done=true qui porte déjà l'état final. Le journal enflait jusqu'à maxLogEvents et tronquait les plus VIEUX événements : les premiers messages disparaissaient à l'affichage. Seul le done=true est conservé au compactage. - Un événement non-outil (stats, raisonnement) glissé entre l'annonce et le résultat faisait émettre l'outil DEUX fois au replay et à l'export Markdown. L'annonce reste en attente jusqu'au done. - Ouvrir une session plus ancienne avec le curseur de la précédente (Seq plus élevés) sautait tout : conversation vide. Curseur au-delà du dernier Seq → on repart du début. - L'export JSON embarquait les images en base64 (fichier énorme) : les pièces jointes sont réduites à leur descriptif. Repris de l'amont AJEAN v0.12.7, avec ses tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7fwdmmVbvLHznF9v1npzN
This commit is contained in:
1 parent
3edd356f34
commit
475d523e4a
4 files changed
+218
-6
No files matched your search
@@ -103,12 +103,78 @@ func (c *Conversation) ExportJSON(o exportOpts) ([]byte, error) {
|
||||
Version: Version,
|
||||
ExportedAt: time.Now().Format(time.RFC3339),
|
||||
CtxUsed: ctxUsed,
|
||||
Messages: msgs,
|
||||
Messages: stripInlineImages(msgs),
|
||||
Log: filterLog(log, o),
|
||||
}
|
||||
return json.MarshalIndent(p, "", " ")
|
||||
}
|
||||
|
||||
// stripInlineImages remplace les images en base64 (data URI) portées par les
|
||||
// messages multimodaux (vision : pièces jointes images + outil see_image) par un
|
||||
// court marqueur. Sans ça, l'export JSON d'une conversation avec images embarquait
|
||||
// plusieurs Mo de base64 par image — un fichier énorme et illisible. Le Markdown,
|
||||
// lui, n'était pas concerné : il ne cite que les NOMS de fichiers. Le journal
|
||||
// d'affichage non plus (il ne porte pas les images). On ne touche donc qu'ici, et
|
||||
// sur une COPIE : les messages appartiennent à la conversation vivante.
|
||||
func stripInlineImages(msgs []Message) []Message {
|
||||
out := make([]Message, len(msgs))
|
||||
for i, m := range msgs {
|
||||
out[i] = m
|
||||
out[i].Content = stripInlineImagesValue(m.Content)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func stripInlineImagesValue(v any) any {
|
||||
// Une partie image (format OpenAI) : {"type":"image_url","image_url":{"url":"data:…"}}.
|
||||
// On copie la partie et on élide l'URL data: en gardant l'en-tête (type MIME) lisible.
|
||||
elide := func(part map[string]any) map[string]any {
|
||||
cp := make(map[string]any, len(part))
|
||||
for k, val := range part {
|
||||
cp[k] = val
|
||||
}
|
||||
if iu, ok := cp["image_url"].(map[string]any); ok {
|
||||
c2 := make(map[string]any, len(iu))
|
||||
for k, val := range iu {
|
||||
c2[k] = val
|
||||
}
|
||||
if url, ok := c2["url"].(string); ok && strings.HasPrefix(url, "data:") {
|
||||
c2["url"] = elideDataURI(url)
|
||||
}
|
||||
cp["image_url"] = c2
|
||||
}
|
||||
return cp
|
||||
}
|
||||
switch parts := v.(type) {
|
||||
case []map[string]any: // message fraîchement produit
|
||||
out := make([]map[string]any, len(parts))
|
||||
for i, p := range parts {
|
||||
out[i] = elide(p)
|
||||
}
|
||||
return out
|
||||
case []any: // message relu depuis la base (JSON) : []any de maps
|
||||
out := make([]any, len(parts))
|
||||
for i, p := range parts {
|
||||
if mp, ok := p.(map[string]any); ok {
|
||||
out[i] = elide(mp)
|
||||
} else {
|
||||
out[i] = p
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
return v // simple chaîne (cas courant) : rien à faire
|
||||
}
|
||||
|
||||
// elideDataURI garde l'en-tête « data:<mime>;base64, » (lisible) et remplace le
|
||||
// corps base64 par sa taille, pour que l'export reste informatif sans le poids.
|
||||
func elideDataURI(url string) string {
|
||||
if i := strings.Index(url, ","); i >= 0 {
|
||||
return url[:i+1] + fmt.Sprintf("<élidé — %s>", humanBytes(int64(len(url)-i-1)))
|
||||
}
|
||||
return "<image élidée>"
|
||||
}
|
||||
|
||||
// filterLog applique au journal les MÊMES cases que le Markdown. Les deux
|
||||
// formats avaient auparavant des options distinctes (le JSON proposait un
|
||||
// obscur « journal d'affichage » à la place des trois autres) : personne ne
|
||||
|
||||
Reference in new issue
Block a user