Files
2026-09-27 16:04:02 -06:00

161 lines
4.2 KiB
Go

package tools
import (
"bytes"
"fmt"
"os"
"path/filepath"
"time"
)
// Límites de salida enviada al modelo (§85). La salida completa se guarda
// en .agent/tmp/ cuando los supera, para poder consultarla por partes.
const (
inlineLimit = 16 << 10 // hasta aquí la salida va entera al modelo
headKeep = 8 << 10 // si se supera: primeros 8 KiB...
tailKeep = 4 << 10 // ...y últimos 4 KiB
maxSpillSize = 256 << 20 // tope del archivo de desbordamiento
)
// spillWriter captura un flujo de salida. Mantiene en memoria hasta
// inlineLimit bytes; si se supera, vuelca todo a un archivo temporal y
// conserva en memoria solo el inicio y una ventana circular del final.
//
// Así un "journalctl" o un "find /" enorme no agota la memoria ni el
// contexto del modelo, y aun así se conserva el resultado completo.
type spillWriter struct {
dir string // directorio para el archivo de desbordamiento
prefix string
head bytes.Buffer
tail []byte // ventana circular de tailKeep bytes
tailN int // bytes escritos en tail (hasta tailKeep)
tailAt int
total int64
file *os.File
fileErr error
fileBytes int64
}
func newSpillWriter(dir, prefix string) *spillWriter {
return &spillWriter{dir: dir, prefix: prefix, tail: make([]byte, tailKeep)}
}
func (w *spillWriter) Write(p []byte) (int, error) {
n := len(p)
w.total += int64(n)
// Ventana circular del final.
for _, b := range p {
w.tail[w.tailAt] = b
w.tailAt = (w.tailAt + 1) % tailKeep
if w.tailN < tailKeep {
w.tailN++
}
}
if w.file == nil && w.fileErr == nil && w.head.Len()+len(p) > inlineLimit {
w.openSpill()
}
if w.file != nil {
if w.fileBytes < maxSpillSize {
chunk := p
if rem := maxSpillSize - w.fileBytes; int64(len(chunk)) > rem {
chunk = chunk[:rem]
}
m, _ := w.file.Write(chunk)
w.fileBytes += int64(m)
}
if w.head.Len() < headKeep {
room := headKeep - w.head.Len()
w.head.Write(p[:min(room, len(p))])
}
return n, nil
}
if w.head.Len() < inlineLimit {
room := inlineLimit - w.head.Len()
w.head.Write(p[:min(room, len(p))])
}
return n, nil
}
// openSpill crea el archivo y vuelca lo acumulado hasta ahora.
func (w *spillWriter) openSpill() {
if err := os.MkdirAll(w.dir, 0o700); err != nil {
w.fileErr = err
return
}
name := fmt.Sprintf("%s-%s-*.out", time.Now().Format("20060102-150405"), w.prefix)
f, err := os.CreateTemp(w.dir, name)
if err != nil {
w.fileErr = err
return
}
w.file = f
m, _ := f.Write(w.head.Bytes())
w.fileBytes = int64(m)
// A partir de aquí head solo guarda los primeros headKeep bytes.
if w.head.Len() > headKeep {
b := append([]byte(nil), w.head.Bytes()[:headKeep]...)
w.head.Reset()
w.head.Write(b)
}
}
// Close cierra el archivo de desbordamiento, si existe.
func (w *spillWriter) Close() {
if w.file != nil {
w.file.Close()
}
}
// Text devuelve la representación para el modelo: completa si cabe, o
// inicio + aviso + final, indicando dónde está el resultado completo.
func (w *spillWriter) Text() string {
if w.total <= inlineLimit {
return w.head.String()
}
omitted := w.total - int64(w.head.Len()) - int64(w.tailN)
where := "not saved"
switch {
case w.file != nil:
where = "full output saved to " + w.file.Name()
if w.fileBytes < w.total {
where += fmt.Sprintf(" (first %d bytes only)", w.fileBytes)
}
case w.fileErr != nil:
where = "could not save full output: " + w.fileErr.Error()
}
return fmt.Sprintf("%s\n\n[... %d bytes omitted of %d total; %s; use read_file with offset/limit or grep to inspect ...]\n\n%s",
w.head.String(), max(omitted, 0), w.total, where, w.tailString())
}
// SpillPath devuelve la ruta del archivo completo, si se creó.
func (w *spillWriter) SpillPath() string {
if w.file == nil {
return ""
}
return w.file.Name()
}
func (w *spillWriter) tailString() string {
if w.tailN < tailKeep {
return string(w.tail[:w.tailN])
}
return string(w.tail[w.tailAt:]) + string(w.tail[:w.tailAt])
}
// truncateText recorta textos ya en memoria (p.ej. read_file).
func truncateText(s string, limit int) (string, bool) {
if len(s) <= limit {
return s, false
}
return s[:limit], true
}
// tmpDir es el directorio de salidas extensas dentro del proyecto.
func tmpDir(root string) string {
return filepath.Join(root, ".agent", "tmp")
}