post inicial
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user