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") }