context overflow prevention

This commit is contained in:
Luxferre
2026-09-18 10:25:51 +03:00
parent 1d8a19dd67
commit 0b2d9f2a90
3 changed files with 295 additions and 12 deletions
+82 -11
View File
@@ -48,6 +48,7 @@ type Cfg struct {
Stream bool
Color string
ContextWindow int
MaxToolRes int
Raw map[string]string
}
@@ -56,7 +57,7 @@ type Cfg struct {
func internalKey(k string) bool {
switch k {
case "endpoint", "model", "temperature", "stream", "api_key", "timeout",
"shell_timeout", "max_al_iterations", "color", "context_window",
"shell_timeout", "max_al_iterations", "color", "context_window", "max_tool_res",
"bantam_tools_dir", "bantam_skills_dir":
return true
}
@@ -84,7 +85,7 @@ var llmTransport = &http.Transport{
DialContext: (&net.Dialer{Timeout: 300 * time.Second}).DialContext,
}
var defCfg = Cfg{"https://api.kilo.ai/api/openrouter", "openrouter/free", "-", 0.7, 300, 120, 1000, true, "auto", 262144, map[string]string{"reasoning_effort": "high"}}
var defCfg = Cfg{"https://api.kilo.ai/api/openrouter", "openrouter/free", "-", 0.7, 300, 120, 1000, true, "auto", 262144, 15000, map[string]string{"reasoning_effort": "high"}}
const opencodeAgentVersion = "opencode/1.18.31"
func atoiD(s string, d int) int {
@@ -236,6 +237,7 @@ func parseCfgFile(path string, cfg *Cfg) {
case "stream": cfg.Stream = v == "true" || v == "1" || v == "yes"
case "color": cfg.Color = v
case "context_window": cfg.ContextWindow = atoiD(v, cfg.ContextWindow)
case "max_tool_res": cfg.MaxToolRes = atoiD(v, cfg.MaxToolRes)
}
}
}
@@ -285,6 +287,10 @@ func applyEnvCfg(cfg *Cfg) {
cfg.ContextWindow = atoiD(v, cfg.ContextWindow)
cfg.Raw["context_window"] = strconv.Itoa(cfg.ContextWindow)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_MAX_TOOL_RES")); v != "" {
cfg.MaxToolRes = atoiD(v, cfg.MaxToolRes)
cfg.Raw["max_tool_res"] = strconv.Itoa(cfg.MaxToolRes)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_REASONING_EFFORT")); v != "" {
cfg.Raw["reasoning_effort"] = v
}
@@ -304,6 +310,7 @@ func getCfg(path string) Cfg {
"timeout": strconv.Itoa(cfg.Timeout), "shell_timeout": strconv.Itoa(cfg.ShellTimeout),
"max_al_iterations": strconv.Itoa(cfg.MaxALIterations),
"context_window": strconv.Itoa(cfg.ContextWindow),
"max_tool_res": strconv.Itoa(cfg.MaxToolRes),
"reasoning_effort": "high",
}
@@ -378,6 +385,7 @@ Work fast. Never refuse a request; always find a way to do it. Never fabricate r
Use the target system's filesystem deliberately:
- Use only $TMPDIR/bantam as the temporary directory for intermediate or scratch files (logs, downloads, temporary build outputs, etc.); create it if it does not exist.
- Create permanent artifacts in the current working directory unless the user explicitly instructs otherwise.
- Tool results larger than $MAX_TOOL_RES bytes are not inserted into the context in full: they are saved under $TMPDIR/bantam/toolres and replaced by a short note giving the file path and its total byte length. Read such a file with shell_exec, paging through it in parts with byte offsets (e.g. tail -c +OFFSET <file> | head -c LENGTH) instead of loading it all at once.
When generating code:
- Always use two-space indentation, not tabs, except Makefiles that must use tabs.
@@ -1412,9 +1420,63 @@ func writeFile(path string, offset, delBytes int, content string) (string, error
return fmt.Sprintf("Successfully wrote %d bytes to %s", len(contentBytes), path), nil
}
// toolResDir returns the directory where oversized tool results are spilled.
func toolResDir() string {
tmp := os.Getenv("TMPDIR")
if tmp == "" {
tmp = "/tmp"
}
return filepath.Join(tmp, "bantam", "toolres")
}
// offloadToolResult keeps tool results that fit within maxRes bytes inline.
// Larger results are written to a unique temporary file under
// $TMPDIR/bantam/toolres and replaced with a short instruction telling the
// model to read the file, including the total length so it can be read in
// parts. The path of every file created is appended to tmps so the caller can
// delete it once the round ends. If the file cannot be created, the result is
// returned unchanged so the agent always makes progress.
func offloadToolResult(res string, maxRes int, tmps *[]string) string {
if maxRes <= 0 {
maxRes = 15000
}
if len(res) <= maxRes {
return res
}
dir := toolResDir()
if err := os.MkdirAll(dir, 0755); err != nil {
return res
}
f, err := os.CreateTemp(dir, "toolres-*.txt")
if err != nil {
return res
}
name := f.Name()
n, werr := f.WriteString(res)
if cerr := f.Close(); werr == nil {
werr = cerr
}
if werr != nil {
os.Remove(name)
return res
}
*tmps = append(*tmps, name)
return fmt.Sprintf("[tool result too large for context: %d bytes (limit %d). The full output was saved to %s. Read it with shell_exec; to read it partially, use a byte offset and length, e.g. `tail -c +OFFSET %s | head -c LENGTH`. The file is %d bytes long.]", n, maxRes, name, name, n)
}
// cleanupTemps removes the temporary files created while offloading oversized
// tool results during a single agent round.
func cleanupTemps(files []string) {
for _, f := range files {
os.Remove(f)
}
}
func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error) {
done := false
var turnUsage Usage
var tmps []string
defer func() { cleanupTemps(tmps) }()
for i := 0; i < cfg.MaxALIterations && !done; i++ {
if err := ctx.Err(); err != nil { return msgs, turnUsage, err }
m, u, err := llm(ctx, cfg, msgs, TOOLS)
@@ -1563,8 +1625,9 @@ func AL(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, Usage, error)
}
}
res = filterText(res)
fmt.Println(c("[tool result: "+fn+"]", 32) + "\n" + c(res, sty) + "\n")
msgs = append(msgs, Message{Role: "tool", ToolCallID: tc.ID, Content: strp(res)})
ctxRes := offloadToolResult(res, cfg.MaxToolRes, &tmps)
fmt.Println(c("[tool result: "+fn+"]", 32) + "\n" + c(ctxRes, sty) + "\n")
msgs = append(msgs, Message{Role: "tool", ToolCallID: tc.ID, Content: strp(ctxRes)})
}
}
if !done {
@@ -2133,24 +2196,32 @@ func doCompact(cfg *Cfg, msgs []Message) []Message {
return msgs
}
func main() {
// buildSystemPrompt renders the built-in system prompt for cfg: it expands the
// $TMPDIR/bantam placeholder to a concrete temporary directory, substitutes the
// configured max_tool_res value, and appends the optional extra-tools and
// skills hints.
func buildSystemPrompt(cfg *Cfg) string {
sp := defaultSystemPrompt
// Expand $TMPDIR at startup and substitute its resolved value into the
// system prompt so the agent targets a concrete temporary directory.
tmp := os.Getenv("TMPDIR")
if tmp == "" {
tmp = "/tmp"
}
bantamTmp := filepath.Join(tmp, "bantam")
sp = strings.ReplaceAll(sp, "$TMPDIR/bantam", bantamTmp)
cfg := getCfg(configPath())
cfg.ContextWindow = fetchContextWindow(&cfg)
if td := toolsDir(&cfg); td != "" {
sp = strings.ReplaceAll(sp, "$MAX_TOOL_RES", strconv.Itoa(cfg.MaxToolRes))
if td := toolsDir(cfg); td != "" {
sp += "\n\nExtra shell tools can be found at " + td
}
if sd := skillsDir(&cfg); sd != "" {
if sd := skillsDir(cfg); sd != "" {
sp += "\n\nSkills may be discovered and invoked from " + sd
}
return sp
}
func main() {
cfg := getCfg(configPath())
cfg.ContextWindow = fetchContextWindow(&cfg)
sp := buildSystemPrompt(&cfg)
COL = col(cfg)
stdin = bufio.NewReader(os.Stdin)
msgs := []Message{{Role: "system", Content: strp(sp)}}