context overflow prevention
This commit is contained in:
@@ -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)}}
|
||||
|
||||
Reference in New Issue
Block a user