// Bantam agent: tiny, powerful, DIY // Created by Luxferre in 2026, released into the public domain package main import ( "bufio" "bytes" "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os" "os/exec" "os/signal" "path/filepath" "regexp" "sort" "strconv" "strings" "time" "unicode/utf8" ) const MAX_DEPTH = 5 // subagent recursion depth limit var ( COL bool // ANSI color enabled hist []string // line history (mirrors ~/.bantam_history) histF string // history file path stdin *bufio.Reader // stdin reader for the REPL ) type Cfg struct { Endpoint string Model string APIKey string Temperature float64 Timeout int ShellTimeout int MaxALIterations int Stream bool Color string ContextWindow int Raw map[string]string } var defCfg = Cfg{"https://opencode.ai/zen/v1", "big-pickle", "-", 0.7, 300, 120, 1000, true, "auto", 200000, nil} func atoiD(s string, d int) int { if v, e := strconv.Atoi(strings.TrimSpace(s)); e == nil { return v } return d } func queryModelsContextWindow(cfg *Cfg) int { client := &http.Client{Timeout: 3 * time.Second} req, err := http.NewRequest("GET", strings.TrimRight(cfg.Endpoint, "/")+"/models", nil) if err != nil { return 0 } req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Bantam/1.0)") if cfg.APIKey != "" && cfg.APIKey != "-" { req.Header.Set("Authorization", "Bearer "+cfg.APIKey) } resp, err := client.Do(req) if err != nil || resp.StatusCode >= 400 { return 0 } defer resp.Body.Close() var res struct { Data []map[string]any `json:"data"` Models []map[string]any `json:"models"` } if json.NewDecoder(resp.Body).Decode(&res) != nil { return 0 } list := res.Data if len(list) == 0 { list = res.Models } for _, item := range list { id, _ := item["id"].(string) if id == cfg.Model || strings.EqualFold(id, cfg.Model) { for _, key := range []string{"context_window", "context_length", "max_context_length", "max_model_len", "context_size", "max_tokens", "max_input_tokens"} { if val, ok := item[key]; ok { switch v := val.(type) { case float64: if v > 0 { return int(v) } case string: if n := atoiD(v, 0); n > 0 { return n } } } } } } return 0 } func fetchContextWindow(cfg *Cfg) int { if cw := queryModelsContextWindow(cfg); cw > 0 { return cw } if v, ok := cfg.Raw["context_window"]; ok { return atoiD(v, 200000) } return 200000 } func getCfg(path string) Cfg { cfg := defCfg cfg.Raw = map[string]string{ "endpoint": cfg.Endpoint, "model": cfg.Model, "temperature": fmt.Sprintf("%v", cfg.Temperature), "api_key": cfg.APIKey, "stream": strconv.FormatBool(cfg.Stream), "color": cfg.Color, "timeout": strconv.Itoa(cfg.Timeout), "shell_timeout": strconv.Itoa(cfg.ShellTimeout), "max_al_iterations": strconv.Itoa(cfg.MaxALIterations), "context_window": strconv.Itoa(cfg.ContextWindow), } if d, err := os.ReadFile(path); err == nil { for _, ln := range strings.Split(string(d), "\n") { ln = strings.TrimSpace(ln) if ln == "" || ln[0] == '#' || !strings.Contains(ln, "=") { continue } k, v, _ := strings.Cut(ln, "=") k, v = strings.TrimSpace(k), strings.TrimSpace(v) cfg.Raw[k] = v switch k { case "endpoint": cfg.Endpoint = v case "model": cfg.Model = v case "api_key": cfg.APIKey = v case "temperature": if f, e := strconv.ParseFloat(v, 64); e == nil { cfg.Temperature = f } case "timeout": cfg.Timeout = atoiD(v, cfg.Timeout) case "shell_timeout": cfg.ShellTimeout = atoiD(v, cfg.ShellTimeout) case "max_al_iterations": cfg.MaxALIterations = atoiD(v, cfg.MaxALIterations) case "stream": cfg.Stream = v == "true" || v == "1" || v == "yes" case "color": cfg.Color = v case "context_window": cfg.ContextWindow = atoiD(v, cfg.ContextWindow) } } } if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("OPENAI_API_KEY") != "" { cfg.APIKey = os.Getenv("OPENAI_API_KEY") cfg.Raw["api_key"] = cfg.APIKey } return cfg } func setCfg(path, key, val string) error { var lines []string found := false if d, err := os.ReadFile(path); err == nil { for _, ln := range strings.Split(string(d), "\n") { trimmed := strings.TrimSpace(ln) if !strings.HasPrefix(trimmed, "#") && strings.Contains(trimmed, "=") { k, _, _ := strings.Cut(trimmed, "=") if strings.TrimSpace(k) == key { lines = append(lines, key+"="+val) found = true continue } } lines = append(lines, ln) } } if !found { if len(lines) > 0 && lines[len(lines)-1] == "" { lines[len(lines)-1] = key + "=" + val lines = append(lines, "") } else { lines = append(lines, key+"="+val) } } return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644) } const defaultSystemPrompt = `You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools: - shell_exec: run a shell command; returns its output and exit code. - run_subagent: delegate a sub-task to a child agent; returns its reply. Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Delegate large or independent sub-tasks to run_subagent. Stop as soon as the goal is met and report concisely: results, not process. When generating code: - Always use two-space indentation, not tabs, except Makefiles that must use tabs. - No whitespace between keywords and opening braces in C-like languages. - Write optimally and with as few third-party dependencies as possible. - Always test. - No emojis in code or documentation. - Respect AGENTS.md, GEMINI.md, CLAUDE.md contents in the project.` func prompt(path string) string { if d, err := os.ReadFile(path); err == nil { return strings.TrimSpace(string(d)) } return defaultSystemPrompt } func c(t string, cs ...int) string { if !COL || len(cs) == 0 { return t } s := make([]string, len(cs)) for i, x := range cs { s[i] = strconv.Itoa(x) } return "\033[" + strings.Join(s, ";") + "m" + t + "\033[0m" } func col(cfg Cfg) bool { if os.Getenv("NO_COLOR") != "" || os.Getenv("BANTAM_NO_COLOR") != "" { return false } switch strings.ToLower(cfg.Color) { case "always": return true case "never": return false } return isTerminal(int(os.Stdout.Fd())) } type mdState struct { inCode bool lang string } var ( reCode = regexp.MustCompile("`([^`]+)`") reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) reBI1 = regexp.MustCompile(`\*\*\*(.*?)\*\*\*`) reBI2 = regexp.MustCompile(`___(.*?)___`) reB1 = regexp.MustCompile(`\*\*(.*?)\*\*`) reB2 = regexp.MustCompile(`__(.*?)__`) reI1 = regexp.MustCompile(`\*(.*?)\*`) reI2 = regexp.MustCompile(`_(.*?)_`) reS = regexp.MustCompile(`~~(.*?)~~`) reOrd = regexp.MustCompile(`^(\d+\.)\s+(.*)`) ) func renderInline(s string) string { if !COL { return s } var codes []string s = reCode.ReplaceAllStringFunc(s, func(m string) string { codes = append(codes, c(m[1:len(m)-1], 33)) return fmt.Sprintf("\x00CD%d\x00", len(codes)-1) }) s = reLink.ReplaceAllStringFunc(s, func(m string) string { sm := reLink.FindStringSubmatch(m) if len(sm) == 3 { return c(sm[1], 4, 36) + " " + c("("+sm[2]+")", 2) } return m }) s = reBI1.ReplaceAllStringFunc(s, func(m string) string { return c(m[3:len(m)-3], 1, 3) }) s = reBI2.ReplaceAllStringFunc(s, func(m string) string { return c(m[3:len(m)-3], 1, 3) }) s = reB1.ReplaceAllStringFunc(s, func(m string) string { return c(m[2:len(m)-2], 1) }) s = reB2.ReplaceAllStringFunc(s, func(m string) string { return c(m[2:len(m)-2], 1) }) s = reI1.ReplaceAllStringFunc(s, func(m string) string { return c(m[1:len(m)-1], 3) }) s = reI2.ReplaceAllStringFunc(s, func(m string) string { return c(m[1:len(m)-1], 3) }) s = reS.ReplaceAllStringFunc(s, func(m string) string { return c(m[2:len(m)-2], 9) }) for i, code := range codes { s = strings.ReplaceAll(s, fmt.Sprintf("\x00CD%d\x00", i), code) } return s } func renderMDLine(line string, st *mdState) string { if !COL { return line } trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "```") { if !st.inCode { st.inCode = true st.lang = strings.TrimSpace(strings.TrimPrefix(trimmed, "```")) title := "" if st.lang != "" { title = " [ " + st.lang + " ]" } return c("───"+title+"──────────────────────────────────────────", 2) } st.inCode = false st.lang = "" return c("───────────────────────────────────────────────────", 2) } if st.inCode { return c(" ", 2) + c(line, 32) } if trimmed == "---" || trimmed == "***" || trimmed == "___" || trimmed == "----" || trimmed == "------" { return c("───────────────────────────────────────────────────", 2) } if strings.HasPrefix(trimmed, "#") { lvl := 0 for lvl < len(trimmed) && trimmed[lvl] == '#' { lvl++ } if lvl < len(trimmed) && trimmed[lvl] == ' ' { htext := strings.TrimSpace(trimmed[lvl:]) switch lvl { case 1: return c("■ ", 35) + c(htext, 1, 37) case 2: return c("▲ ", 34) + c(htext, 1, 36) case 3: return c("● ", 32) + c(htext, 1, 32) case 4: return c("◆ ", 33) + c(htext, 1, 33) default: return c(htext, 1) } } } if strings.HasPrefix(trimmed, ">") { qtext := strings.TrimSpace(strings.TrimPrefix(trimmed, ">")) return c("▎ ", 34) + c(renderInline(qtext), 3) } if strings.HasPrefix(trimmed, "- [ ] ") || strings.HasPrefix(trimmed, "* [ ] ") { return strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) + c("☐ ", 33) + renderInline(trimmed[6:]) } if strings.HasPrefix(trimmed, "- [x] ") || strings.HasPrefix(trimmed, "* [x] ") || strings.HasPrefix(trimmed, "- [X] ") || strings.HasPrefix(trimmed, "* [X] ") { return strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) + c("☑ ", 32) + renderInline(trimmed[6:]) } if strings.HasPrefix(trimmed, "- ") || strings.HasPrefix(trimmed, "* ") || strings.HasPrefix(trimmed, "+ ") { indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) return indent + c("• ", 36) + renderInline(trimmed[2:]) } if m := reOrd.FindStringSubmatch(trimmed); len(m) == 3 { indent := strings.Repeat(" ", len(line)-len(strings.TrimLeft(line, " "))) return indent + c(m[1]+" ", 33) + renderInline(m[2]) } if strings.HasPrefix(trimmed, "|") && strings.HasSuffix(trimmed, "|") { return renderInline(line) } return renderInline(line) } func isTableSep(cells []string) bool { if len(cells) == 0 { return false } for _, cell := range cells { c := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(cell), "-", ""), ":", "") if c != "" { return false } } return true } func parseTableCells(line string) []string { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "|") { trimmed = trimmed[1:] } if strings.HasSuffix(trimmed, "|") { trimmed = trimmed[:len(trimmed)-1] } parts := strings.Split(trimmed, "|") cells := make([]string, len(parts)) for i, p := range parts { cells[i] = strings.TrimSpace(p) } return cells } func isTableLine(line string) bool { trimmed := strings.TrimSpace(line) if !strings.Contains(trimmed, "|") { return false } return strings.HasPrefix(trimmed, "|") || strings.HasSuffix(trimmed, "|") } func wrapCell(s string, width int) []string { if width <= 0 { return []string{""} } s = strings.ReplaceAll(s, "
", "\n") s = strings.ReplaceAll(s, "
", "\n") s = strings.ReplaceAll(s, "
", "\n") var lines []string for _, p := range strings.Split(s, "\n") { p = strings.TrimSpace(p) if p == "" { lines = append(lines, "") continue } if visibleLen(p) <= width { lines = append(lines, p) continue } words := strings.Fields(p) if len(words) == 0 { lines = append(lines, "") continue } var curLine string var activeEsc string updateActiveEsc := func(token string) { for i := 0; i < len(token); { if token[i] == 0x1b { j := i + 1 if j < len(token) && token[j] == '[' { j++ for j < len(token) && !(token[j] >= 0x40 && token[j] <= 0x7e) { j++ } if j < len(token) { j++ } } esc := token[i:j] if esc == "\033[0m" || esc == "\033[m" { activeEsc = "" } else { activeEsc = esc } i = j } else { i++ } } } flushLine := func() { if curLine != "" { out := curLine if COL && activeEsc != "" && !strings.HasSuffix(out, "\033[0m") { out += "\033[0m" } lines = append(lines, out) curLine = "" } } for _, word := range words { wLen := visibleLen(word) if wLen > width { flushLine() var chunk strings.Builder cLen := 0 for i := 0; i < len(word); { if word[i] == 0x1b { j := i + 1 if j < len(word) && word[j] == '[' { j++ for j < len(word) && !(word[j] >= 0x40 && word[j] <= 0x7e) { j++ } if j < len(word) { j++ } } chunk.WriteString(word[i:j]) updateActiveEsc(word[i:j]) i = j continue } r, size := utf8.DecodeRuneInString(word[i:]) if cLen >= width { if COL && activeEsc != "" { chunk.WriteString("\033[0m") } lines = append(lines, chunk.String()) chunk.Reset() if COL && activeEsc != "" { chunk.WriteString(activeEsc) } cLen = 0 } chunk.WriteRune(r) cLen++ i += size } if chunk.Len() > 0 { curLine = chunk.String() } continue } cLen := visibleLen(curLine) if curLine == "" { curLine = word updateActiveEsc(word) } else if cLen+1+wLen <= width { curLine += " " + word updateActiveEsc(word) } else { flushLine() if COL && activeEsc != "" { curLine = activeEsc + word } else { curLine = word } updateActiveEsc(word) } } flushLine() } if len(lines) == 0 { lines = []string{""} } return lines } func renderTable(lines []string) []string { if len(lines) == 0 { return nil } var rows [][]string var headerRow []string hasHeader := false for _, ln := range lines { cells := parseTableCells(ln) if isTableSep(cells) { if len(rows) > 0 && !hasHeader { headerRow = rows[len(rows)-1] rows = rows[:len(rows)-1] hasHeader = true } continue } rows = append(rows, cells) } if !hasHeader && len(lines) < 2 { var out []string var st mdState for _, ln := range lines { out = append(out, renderMDLine(ln, &st)) } return out } numCols := len(headerRow) for _, r := range rows { if len(r) > numCols { numCols = len(r) } } if numCols == 0 { return nil } if hasHeader { for len(headerRow) < numCols { headerRow = append(headerRow, "") } } for i := range rows { for len(rows[i]) < numCols { rows[i] = append(rows[i], "") } } colWidths := make([]int, numCols) for i := 0; i < numCols; i++ { if hasHeader { vl := visibleLen(headerRow[i]) if vl > colWidths[i] { colWidths[i] = vl } } for _, r := range rows { vl := visibleLen(renderInline(r[i])) if vl > colWidths[i] { colWidths[i] = vl } } if colWidths[i] < 3 { colWidths[i] = 3 } } maxTableWidth := termWidth() if maxTableWidth < 20 { maxTableWidth = 80 } overhead := 3*numCols + 1 availContent := maxTableWidth - overhead if availContent < numCols*3 { availContent = numCols * 3 } tot := 0 for _, w := range colWidths { tot += w } for tot > availContent { maxIdx := 0 maxVal := colWidths[0] for i := 1; i < numCols; i++ { if colWidths[i] > maxVal { maxVal = colWidths[i] maxIdx = i } } if maxVal <= 3 { break } colWidths[maxIdx]-- tot-- } var res []string var topParts []string for _, w := range colWidths { topParts = append(topParts, strings.Repeat("─", w+2)) } res = append(res, c("┌"+strings.Join(topParts, "┬")+"┐", 2)) if hasHeader { headerCols := make([][]string, numCols) maxHeaderLines := 1 for i, h := range headerRow { wrapped := wrapCell(h, colWidths[i]) if len(wrapped) > maxHeaderLines { maxHeaderLines = len(wrapped) } headerCols[i] = wrapped } for lineIdx := 0; lineIdx < maxHeaderLines; lineIdx++ { var hCells []string for i := 0; i < numCols; i++ { txt := "" if lineIdx < len(headerCols[i]) { txt = headerCols[i][lineIdx] } rh := c(txt, 1, 36) pad := strings.Repeat(" ", colWidths[i]-visibleLen(txt)) hCells = append(hCells, " "+rh+pad+" ") } res = append(res, c("│", 2)+strings.Join(hCells, c("│", 2))+c("│", 2)) } var midParts []string for _, w := range colWidths { midParts = append(midParts, strings.Repeat("─", w+2)) } res = append(res, c("├"+strings.Join(midParts, "┼")+"┤", 2)) } for _, r := range rows { rowCols := make([][]string, numCols) maxRowLines := 1 for i, cell := range r { rc := renderInline(cell) wrapped := wrapCell(rc, colWidths[i]) if len(wrapped) > maxRowLines { maxRowLines = len(wrapped) } rowCols[i] = wrapped } for lineIdx := 0; lineIdx < maxRowLines; lineIdx++ { var rCells []string for i := 0; i < numCols; i++ { txt := "" if lineIdx < len(rowCols[i]) { txt = rowCols[i][lineIdx] } pad := strings.Repeat(" ", colWidths[i]-visibleLen(txt)) rCells = append(rCells, " "+txt+pad+" ") } res = append(res, c("│", 2)+strings.Join(rCells, c("│", 2))+c("│", 2)) } } var botParts []string for _, w := range colWidths { botParts = append(botParts, strings.Repeat("─", w+2)) } res = append(res, c("└"+strings.Join(botParts, "┴")+"┘", 2)) return res } func renderMD(text string) string { if !COL { return text } lines := strings.Split(text, "\n") var out []string var st mdState var tbl []string flushTable := func() { if len(tbl) > 0 { out = append(out, renderTable(tbl)...) tbl = nil } } for _, ln := range lines { trimmed := strings.TrimSpace(ln) if strings.HasPrefix(trimmed, "```") { flushTable() out = append(out, renderMDLine(ln, &st)) continue } if st.inCode { out = append(out, renderMDLine(ln, &st)) continue } if isTableLine(ln) { tbl = append(tbl, ln) continue } flushTable() out = append(out, renderMDLine(ln, &st)) } flushTable() return strings.Join(out, "\n") } type Message struct { Role string `json:"role"` Content *string `json:"content"` ReasoningContent string `json:"reasoning_content,omitempty"` ToolCalls []ToolCall `json:"tool_calls,omitempty"` ToolCallID string `json:"tool_call_id,omitempty"` } type ToolCall struct { ID string `json:"id"` Type string `json:"type"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } var TOOLS = []map[string]any{ {"type": "function", "function": map[string]any{"name": "shell_exec", "description": "Run a shell command, return output and exit code.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"command": map[string]any{"type": "string"}}, "required": []string{"command"}}}}, {"type": "function", "function": map[string]any{"name": "run_subagent", "description": "Run a child agent with a prompt.", "parameters": map[string]any{"type": "object", "properties": map[string]any{"prompt": map[string]any{"type": "string"}}, "required": []string{"prompt"}}}}, } func strp(s string) *string { return &s } type Usage struct { PromptTokens int `json:"prompt_tokens"` CompletionTokens int `json:"completion_tokens"` TotalTokens int `json:"total_tokens"` PromptTokensDetails struct { CachedTokens int `json:"cached_tokens"` } `json:"prompt_tokens_details"` CachedTokens int `json:"cached_tokens"` } func (u Usage) Cached() int { if u.PromptTokensDetails.CachedTokens > 0 { return u.PromptTokensDetails.CachedTokens } return u.CachedTokens } func estTokens(msgs []Message) int { chars := 0 for _, m := range msgs { if m.Content != nil { chars += len(*m.Content) } chars += len(m.ReasoningContent) for _, tc := range m.ToolCalls { chars += len(tc.Function.Name) + len(tc.Function.Arguments) } } if chars == 0 { return 0 } t := chars / 4 if t == 0 { t = 1 } return t } func formatUsage(u Usage, cw int) string { if cw <= 0 { cw = 200000 } pct := float64(u.PromptTokens) * 100.0 / float64(cw) cached := u.Cached() if cached > 0 { uncached := u.PromptTokens - cached if uncached < 0 { uncached = 0 } return fmt.Sprintf("[tokens: %d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, cached, uncached, u.CompletionTokens, u.PromptTokens, cw, pct) } return fmt.Sprintf("[tokens: %d prompt + %d completion | context: %d/%d (%.1f%%)]", u.PromptTokens, u.CompletionTokens, u.PromptTokens, cw, pct) } type streamDelta struct { Choices []struct { Delta struct { ReasoningContent string `json:"reasoning_content"` Reasoning string `json:"reasoning"` Content string `json:"content"` ToolCalls []struct { Index int `json:"index"` ID string `json:"id"` Function struct { Name string `json:"name"` Arguments string `json:"arguments"` } `json:"function"` } `json:"tool_calls"` } `json:"delta"` } `json:"choices"` Usage *Usage `json:"usage"` } func sanitizeMessages(msgs []Message) { for i := range msgs { if msgs[i].Role == "assistant" && len(msgs[i].ToolCalls) > 0 { for j := range msgs[i].ToolCalls { tc := &msgs[i].ToolCalls[j] astr := tc.Function.Arguments var a map[string]any if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil { fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr}) tc.Function.Arguments = string(fixed) } } } } } func isInvalidAssistantErr(err error) bool { s := err.Error() return strings.Contains(s, "Invalid assistant message") || strings.Contains(s, "content or tool_calls must be set") } func llm(ctx context.Context, cfg *Cfg, msgs []Message, tools []map[string]any) (Message, Usage, error) { if err := ctx.Err(); err != nil { return Message{}, Usage{}, err } sanitizeMessages(msgs) p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": msgs, "stream": cfg.Stream} if tools != nil { p["tools"] = tools } if cfg.Stream { p["stream_options"] = map[string]any{"include_usage": true} } for k, v := range cfg.Raw { switch k { case "endpoint", "api_key", "timeout", "shell_timeout", "max_al_iterations", "color", "context_window": continue default: var jv any if err := json.Unmarshal([]byte(v), &jv); err == nil { p[k] = jv } else { p[k] = v } } } body, _ := json.Marshal(p) client := &http.Client{Transport: &http.Transport{ DialContext: (&net.Dialer{Timeout: time.Duration(cfg.Timeout) * time.Second}).DialContext, ResponseHeaderTimeout: time.Duration(cfg.Timeout) * time.Second, }} defer client.CloseIdleConnections() fib := []int{1, 1, 2, 3, 5, 8, 13, 21, 34} pend := c("...requesting...", 1, 2) var resp *http.Response var err error for i := 0; i <= len(fib); i++ { if err := ctx.Err(); err != nil { if COL { fmt.Print("\r\033[K") } return Message{}, Usage{}, err } if COL { fmt.Print("\r" + pend) } else { fmt.Println(pend) } req, _ := http.NewRequestWithContext(ctx, "POST", strings.TrimRight(cfg.Endpoint, "/")+"/chat/completions", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Bantam/1.0)") if cfg.APIKey != "" && cfg.APIKey != "-" { req.Header.Set("Authorization", "Bearer "+cfg.APIKey) } resp, err = client.Do(req) var is4xxClientErr bool if err == nil && resp.StatusCode >= 400 { b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) resp.Body.Close() err = fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b))) if resp.StatusCode < 500 && resp.StatusCode != 408 && resp.StatusCode != 429 { is4xxClientErr = true } resp = nil } if err == nil { break } if COL { fmt.Print("\r\033[K") } if errors.Is(err, context.Canceled) || ctx.Err() != nil { return Message{}, Usage{}, ctx.Err() } if is4xxClientErr { return Message{}, Usage{}, err } if i < len(fib) { fmt.Println(c(fmt.Sprintf("[network error: %v, retrying in %ds...]", err, fib[i]), 31)) select { case <-ctx.Done(): return Message{}, Usage{}, ctx.Err() case <-time.After(time.Duration(fib[i]) * time.Second): } } } if err != nil { if COL { fmt.Print("\r\033[K") } return Message{}, Usage{}, err } defer resp.Body.Close() if COL { fmt.Print("\r\033[K") } if !cfg.Stream { var cr struct { Choices []struct { Message struct { Message Reasoning string `json:"reasoning"` } `json:"message"` } `json:"choices"` Usage Usage `json:"usage"` } if err := json.NewDecoder(resp.Body).Decode(&cr); err != nil { if errors.Is(err, context.Canceled) || ctx.Err() != nil { return Message{}, Usage{}, ctx.Err() } return Message{}, Usage{}, err } if len(cr.Choices) == 0 { return Message{}, Usage{}, errors.New("empty choices in LLM response") } m := cr.Choices[0].Message.Message if m.ReasoningContent == "" { m.ReasoningContent = cr.Choices[0].Message.Reasoning } u := cr.Usage if u.PromptTokens == 0 { u.PromptTokens = estTokens(msgs) u.CompletionTokens = estTokens([]Message{m}) u.TotalTokens = u.PromptTokens + u.CompletionTokens } return m, u, nil } m, u, err := parseStream(ctx, resp.Body) if err == nil && u.PromptTokens == 0 { u.PromptTokens = estTokens(msgs) u.CompletionTokens = estTokens([]Message{m}) u.TotalTokens = u.PromptTokens + u.CompletionTokens } return m, u, err } func parseStream(ctx context.Context, r io.Reader) (Message, Usage, error) { var content, reas string var rh, ch bool var lineBuf string var mdSt mdState var tblBuf []string var lastUsage Usage tcs := map[int]*ToolCall{} var order []int flushTable := func() { if len(tblBuf) > 0 { for _, tln := range renderTable(tblBuf) { fmt.Println(tln) } tblBuf = nil } } sc := bufio.NewScanner(r) sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024) for sc.Scan() { if err := ctx.Err(); err != nil { return Message{}, lastUsage, err } ln := strings.TrimSpace(sc.Text()) if !strings.HasPrefix(ln, "data:") { continue } data := strings.TrimSpace(ln[5:]) if data == "[DONE]" { break } var d streamDelta if json.Unmarshal([]byte(data), &d) != nil { continue } if d.Usage != nil && (d.Usage.PromptTokens > 0 || d.Usage.TotalTokens > 0) { lastUsage = *d.Usage } if len(d.Choices) == 0 { continue } dl := d.Choices[0].Delta rc := dl.ReasoningContent if rc == "" { rc = dl.Reasoning } if rc != "" { if !rh { fmt.Println(c("--- reasoning start ---", 36)); rh = true } fmt.Print(c(rc, 2)) reas += rc } if dl.Content != "" { if rh && !ch { fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n\n") } ch = true content += dl.Content lineBuf += dl.Content for { idx := strings.IndexByte(lineBuf, '\n') if idx == -1 { break } curLine := lineBuf[:idx] lineBuf = lineBuf[idx+1:] trimmed := strings.TrimSpace(curLine) if strings.HasPrefix(trimmed, "```") { flushTable() fmt.Println(renderMDLine(curLine, &mdSt)) } else if mdSt.inCode { fmt.Println(renderMDLine(curLine, &mdSt)) } else if isTableLine(curLine) { tblBuf = append(tblBuf, curLine) } else { flushTable() fmt.Println(renderMDLine(curLine, &mdSt)) } } } for _, tc := range dl.ToolCalls { t, ok := tcs[tc.Index] if !ok { t = &ToolCall{Type: "function"} tcs[tc.Index] = t order = append(order, tc.Index) } if tc.ID != "" { t.ID = tc.ID } if tc.Function.Name != "" { t.Function.Name += tc.Function.Name } if tc.Function.Arguments != "" { t.Function.Arguments += tc.Function.Arguments } } } if err := ctx.Err(); err != nil { return Message{}, lastUsage, err } flushTable() if lineBuf != "" { if !mdSt.inCode && isTableLine(lineBuf) { tblBuf = append(tblBuf, lineBuf) flushTable() } else { fmt.Println(renderMDLine(lineBuf, &mdSt)) } } if rh && !ch { fmt.Print("\n" + c("--- reasoning end ---", 36) + "\n") } m := Message{Role: "assistant"} if content != "" { m.Content = strp(content) } if reas != "" { m.ReasoningContent = reas } if len(tcs) > 0 { m.ToolCalls = make([]ToolCall, 0, len(order)) for _, idx := range order { m.ToolCalls = append(m.ToolCalls, *tcs[idx]) } } if lastUsage.TotalTokens == 0 && lastUsage.PromptTokens > 0 { lastUsage.TotalTokens = lastUsage.PromptTokens + lastUsage.CompletionTokens } return m, lastUsage, sc.Err() } func shell(ctx context.Context, cmd string, timeout int) string { cmdCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() c := exec.CommandContext(cmdCtx, "sh", "-c", cmd) c.WaitDelay = 100 * time.Millisecond out, err := c.CombinedOutput() res := strings.TrimSpace(string(out)) if ctx.Err() != nil { return "[interrupted]\n\nexit: -1" } if cmdCtx.Err() == context.DeadlineExceeded { return fmt.Sprintf("%s\n\n[shell timeout after %ds]\nexit: -1", res, timeout) } code := 0 if err != nil { if ee, ok := err.(*exec.ExitError); ok { code = ee.ExitCode() } else { code = -1 } } return fmt.Sprintf("%s\n\nexit: %d", res, code) } func last(msgs []Message) string { for i := len(msgs) - 1; i >= 0; i-- { if msgs[i].Role == "assistant" && msgs[i].Content != nil && *msgs[i].Content != "" { return *msgs[i].Content } } return "" } func AL(ctx context.Context, cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, Usage, error) { done := false var turnUsage Usage 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) if err != nil { if errors.Is(err, context.Canceled) || ctx.Err() != nil { return msgs, turnUsage, err } if isInvalidAssistantErr(err) { stripped := false for j := len(msgs) - 1; j >= 0; j-- { if msgs[j].Role == "assistant" { fmt.Println(c("[stripped malformed assistant message]", 33)) msgs = append(msgs[:j], msgs[j+1:]...) stripped = true break } } if stripped { continue } } return msgs, turnUsage, err } turnUsage.PromptTokens = u.PromptTokens turnUsage.CompletionTokens += u.CompletionTokens turnUsage.TotalTokens += u.TotalTokens if u.Cached() > 0 { turnUsage.CachedTokens = u.Cached() } for j := range m.ToolCalls { tc := &m.ToolCalls[j] astr := tc.Function.Arguments var a map[string]any if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil { fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr}) tc.Function.Arguments = string(fixed) } } msgs = append(msgs, m) if !cfg.Stream { if m.ReasoningContent != "" { fmt.Println(c("--- reasoning start ---", 36) + "\n" + c(m.ReasoningContent, 2) + "\n" + c("--- reasoning end ---", 36)) } if m.Content != nil { fmt.Println(renderMD(*m.Content)) } } if len(m.ToolCalls) == 0 { done = true; break } for _, tc := range m.ToolCalls { if err := ctx.Err(); err != nil { return msgs, turnUsage, err } fn, astr := tc.Function.Name, tc.Function.Arguments fmt.Println(c(fmt.Sprintf("[tool call: %s(%s)]", fn, astr), 33)) res, sty := "", 2 var a map[string]any if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil { res, sty = fmt.Sprintf("[tool error: invalid JSON args for %s: %v. Raw: %q]", fn, err, astr), 31 } else { switch fn { case "shell_exec": cmd, _ := a["command"].(string) res = shell(ctx, cmd, cfg.ShellTimeout) if err := ctx.Err(); err != nil { return msgs, turnUsage, err } case "run_subagent": pr, _ := a["prompt"].(string) if depth >= MAX_DEPTH { res, sty = fmt.Sprintf("[subagent depth limit (%d) reached, child not spawned]", MAX_DEPTH), 31 } else { subMsgs := []Message{ {Role: "system", Content: strp(sp + "\n\nImportant: this is a child agent")}, {Role: "user", Content: strp(pr)}, } if subr, subu, err := AL(ctx, cfg, subMsgs, sp, depth+1); err != nil { if errors.Is(err, context.Canceled) || ctx.Err() != nil { return msgs, turnUsage, err } res, sty = "[subagent error: "+err.Error()+"]", 31 } else { turnUsage.CompletionTokens += subu.CompletionTokens turnUsage.TotalTokens += subu.TotalTokens res, sty = last(subr), 2 } } default: res, sty = "Unknown tool: " + fn, 31 } } fmt.Println(c("[tool result: "+fn+"]", 32) + "\n" + c(res, sty) + "\n") msgs = append(msgs, Message{Role: "tool", ToolCallID: tc.ID, Content: strp(res)}) } } if !done { msgs = append(msgs, Message{Role: "assistant", Content: strp(fmt.Sprintf("[max AL iterations (%d) reached]", cfg.MaxALIterations))}) } return msgs, turnUsage, nil } func homeDir() string { if h, err := os.UserHomeDir(); err == nil && h != "" { return h } return "." } func sdir() string { d := filepath.Join(homeDir(), ".bantam", "sessions") os.MkdirAll(d, 0755) return d } type Session struct { ID string `json:"id"` Created string `json:"created"` Summary string `json:"summary"` Messages []Message `json:"messages"` } func summary(msgs []Message) string { for _, m := range msgs { if m.Role == "user" && m.Content != nil && strings.TrimSpace(*m.Content) != "" { t := strings.Join(strings.Fields(*m.Content), " ") if len(t) > 80 { t = t[:80] + "..." } return t } } return "(empty session)" } func fileExists(p string) bool { _, err := os.Stat(p) return err == nil } func saveSession(msgs []Message) (string, string) { d := sdir() base := time.Now().Format("20060102-150405") sid, path := base, filepath.Join(d, base+".json") for i := 1; fileExists(path); i++ { sid = fmt.Sprintf("%s-%d", base, i) path = filepath.Join(d, sid+".json") } s := Session{sid, time.Now().Format("2006-01-02 15:04:05"), summary(msgs), msgs} b, _ := json.MarshalIndent(s, "", " ") os.WriteFile(path, b, 0644) return sid, s.Summary } func sessions() []Session { out := []Session{} d := filepath.Join(homeDir(), ".bantam", "sessions") entries, err := os.ReadDir(d) if err != nil { return out } for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".json") { continue } b, err := os.ReadFile(filepath.Join(d, e.Name())) if err != nil { continue } var s Session if json.Unmarshal(b, &s) != nil { continue } if s.ID == "" { s.ID = strings.TrimSuffix(e.Name(), ".json") } out = append(out, s) } sort.Slice(out, func(i, j int) bool { return out[i].ID > out[j].ID }) return out } func loadSession(sid string) ([]Message, error) { ss := sessions() for i := range ss { if ss[i].ID == sid { return ss[i].Messages, nil } } var pref []*Session for i := range ss { if strings.HasPrefix(ss[i].ID, sid) { pref = append(pref, &ss[i]) } } if len(pref) == 1 { return pref[0].Messages, nil } if len(pref) > 1 { names := make([]string, len(pref)) for i, s := range pref { names[i] = s.ID } return nil, fmt.Errorf("ambiguous prefix: %s", strings.Join(names, ", ")) } return nil, fmt.Errorf("session not found: %s", sid) } func autosave(msgs []Message) { s := Session{"autosave", time.Now().Format("2006-01-02 15:04:05"), summary(msgs), msgs} b, _ := json.MarshalIndent(s, "", " ") os.WriteFile(filepath.Join(sdir(), "autosave.json"), b, 0644) } const compactionPrompt = "You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely, preserving all important facts, decisions, code snippets, tool outputs, errors, and current task state so work can seamlessly continue. Output only the summary." func compact(ctx context.Context, cfg *Cfg, msgs []Message) ([]Message, string, error) { if len(msgs) == 0 || msgs[0].Role != "system" { return msgs, "", errors.New("session has no system message") } if len(msgs) <= 1 { return msgs, "", errors.New("nothing to compact") } cMsgs := append(append([]Message{}, msgs...), Message{ Role: "user", Content: strp(compactionPrompt), }) cc := *cfg cc.Stream = false m, _, err := llm(ctx, &cc, cMsgs, nil) if err != nil { return msgs, "", err } s := "" if m.Content != nil { s = *m.Content } if s == "" { s = m.ReasoningContent } s = strings.TrimSpace(s) if s == "" { return msgs, "", errors.New("LLM returned an empty summary") } newMsgs := []Message{ {Role: "system", Content: msgs[0].Content}, {Role: "user", Content: strp("Summary of the previous conversation:\n" + s + "\n\nPlease continue from here.")}, } return newMsgs, s, nil } func summarize(ctx context.Context, cfg *Cfg, msgs []Message) (string, error) { _, s, err := compact(ctx, cfg, msgs) return s, err } func loadHistory() { histF = filepath.Join(homeDir(), ".bantam_history") b, err := os.ReadFile(histF) if err != nil { return } for _, ln := range strings.Split(string(b), "\n") { if ln = strings.TrimRight(ln, "\r"); ln != "" { hist = append(hist, ln) } } } func addHistory(s string) { if s == "" || (len(hist) > 0 && hist[len(hist)-1] == s) { return } hist = append(hist, s) } func saveHistory() { os.WriteFile(histF, []byte(strings.Join(hist, "\n")+"\n"), 0644) } func visibleLen(s string) int { n := 0 for i := 0; i < len(s); { switch s[i] { case 0x1b: j := i + 1 if j < len(s) && s[j] == '[' { j++ for j < len(s) && !(s[j] >= 0x40 && s[j] <= 0x7e) { j++ } if j < len(s) { j++ } i = j } else { i++ } case 0x01, 0x02: i++ default: _, size := utf8.DecodeRuneInString(s[i:]) n++ i += size } } return n } type editor struct { prompt string buf []rune pos int hist []string hpos int draft string crow int } func textPos(promptLen, W int, s string, pos int) (row, col int) { row, col = 0, promptLen pend := false for i, r := range []rune(s) { if i == pos { return row, col } if r == '\n' { row++ col = 0 pend = false continue } if pend { row++ col = 0 pend = false } if col == W-1 { pend = true } else { col++ } } return row, col } func (e *editor) draw() { W := termWidth() s := string(e.buf) P := visibleLen(e.prompt) er, _ := textPos(P, W, s, len([]rune(s))) pr, pc := textPos(P, W, s, e.pos) if e.crow > 0 { fmt.Printf("\033[%dA", e.crow) } fmt.Print("\r\033[J" + e.prompt + s) if up := er - pr; up > 0 { fmt.Printf("\033[%dA", up) } fmt.Print("\r") if pc > 0 { fmt.Printf("\033[%dC", pc) } e.crow = pr } func (e *editor) histNav(up bool) { if up { if len(e.hist) == 0 { return } if e.hpos < 0 { e.draft = string(e.buf) e.hpos = len(e.hist) - 1 } else if e.hpos > 0 { e.hpos-- } e.buf = []rune(e.hist[e.hpos]) } else { if e.hpos < 0 { return } e.hpos++ if e.hpos >= len(e.hist) { e.hpos = -1 e.buf = []rune(e.draft) } else { e.buf = []rune(e.hist[e.hpos]) } } e.pos = len(e.buf) } func readPlain(prompt string) (string, bool) { fmt.Print(prompt) line, err := stdin.ReadString('\n') if err != nil && line == "" { return "", false } return strings.TrimRight(line, "\r\n"), true } func readLine(prompt string) (string, bool) { if !isTerminal(int(os.Stdin.Fd())) { return readPlain(prompt) } restore, err := makeRaw(int(os.Stdin.Fd())) if err != nil { return readPlain(prompt) } defer restore() e := &editor{prompt: prompt, hpos: -1, hist: hist} for { e.draw() rn, _, err := stdin.ReadRune() if err != nil { fmt.Print("\r\n") return "", false } switch rn { case '\r': if stdin.Buffered() > 0 { if b, _ := stdin.Peek(1); len(b) > 0 && b[0] == '\n' { stdin.ReadByte() } } W := termWidth() s := string(e.buf) P := visibleLen(e.prompt) er, _ := textPos(P, W, s, len([]rune(s))) pr, _ := textPos(P, W, s, e.pos) if down := er - pr; down > 0 { fmt.Printf("\033[%dB", down) } fmt.Print("\r\n") return string(e.buf), true case '\n': e.buf = append(e.buf, 0) copy(e.buf[e.pos+1:], e.buf[e.pos:]) e.buf[e.pos] = '\n' e.pos++ case 0x03: fmt.Print("^C\r\n") return "", true case 0x04: if len(e.buf) == 0 { fmt.Print("\r\n") return "", false } case 0x7f, 0x08: if e.pos > 0 { e.buf = append(e.buf[:e.pos-1], e.buf[e.pos:]...) e.pos-- } case 0x1b: b1, err1 := stdin.ReadByte() b2, err2 := stdin.ReadByte() if err1 != nil || err2 != nil { continue } if b1 == '[' { switch b2 { case 'A': e.histNav(true) case 'B': e.histNav(false) case 'C': if e.pos < len(e.buf) { e.pos++ } case 'D': if e.pos > 0 { e.pos-- } } } default: if rn >= 32 { e.buf = append(e.buf, 0) copy(e.buf[e.pos+1:], e.buf[e.pos:]) e.buf[e.pos] = rn e.pos++ } } } } func main() { sp := prompt("system.txt") cfg := getCfg("model.cfg") cfg.ContextWindow = fetchContextWindow(&cfg) COL = col(cfg) stdin = bufio.NewReader(os.Stdin) msgs := []Message{{Role: "system", Content: strp(sp)}} if len(os.Args) > 1 && os.Args[1] != "" { p := os.Args[1] data, err := os.ReadFile(p) if err != nil { fmt.Println(c("Error: file '"+p+"' not found.", 31)) os.Exit(1) } u := strings.TrimSpace(string(data)) if strings.HasPrefix(u, "!") { cmd := strings.TrimSpace(strings.TrimPrefix(u, "!")) if cmd != "" { astr, _ := json.Marshal(map[string]string{"command": cmd}) fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33)) sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) res := shell(sigCtx, cmd, cfg.ShellTimeout) cancel() fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2)) } return } msgs = append(msgs, Message{Role: "user", Content: strp(u)}) sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) var usg Usage msgs, usg, err = AL(sigCtx, &cfg, msgs, sp, 0) cancel() if err != nil { if errors.Is(err, context.Canceled) || sigCtx.Err() != nil { fmt.Println(c("\n[interrupted]", 33)) } else { fmt.Println(c("[error: "+err.Error()+"]", 31)) } os.Exit(1) } fmt.Println(c(formatUsage(usg, cfg.ContextWindow), 2)) autosave(msgs) return } loadHistory() fmt.Println(c("Bantam Agent ready", 1, 32) + c(" (Ctrl+J = new line)", 2)) fmt.Println(c(fmt.Sprintf("endpoint: %s model: %s temp: %v context: %d", cfg.Endpoint, cfg.Model, cfg.Temperature, cfg.ContextWindow), 2)) for { u, ok := readLine(c("> ", 1, 36)) if !ok { fmt.Println() break } u = strings.TrimSpace(u) if u == "" { continue } addHistory(u) switch { case u == "/quit": goto done case u == "/clear": msgs = []Message{{Role: "system", Content: strp(sp)}} autosave(msgs) continue case u == "/save": sid, sm := saveSession(msgs) fmt.Println(c("[session saved: "+sid+"]", 32) + " " + c(sm, 2)) continue case u == "/list": ss := sessions() if len(ss) == 0 { fmt.Println(c("No sessions saved yet.", 33)) continue } for _, s := range ss { mk := c(" (autosave)", 33) if s.ID != "autosave" { mk = "" } fmt.Println(c(s.ID, 32) + mk + c(fmt.Sprintf(" %s [%d msgs]", s.Created, len(s.Messages)), 2)) fmt.Println(" " + c(s.Summary, 2)) } continue case strings.HasPrefix(u, "/load"): parts := strings.Fields(u) if len(parts) < 2 { fmt.Println(c("Usage: /load ", 31)) continue } lm, err := loadSession(parts[1]) if err != nil { fmt.Println(c("Session not found: "+err.Error(), 31)) continue } msgs = lm autosave(msgs) fmt.Println(c("[session loaded: "+parts[1]+"]", 32) + " " + c(summary(msgs), 2)) continue case u == "/compact": if len(msgs) <= 1 { fmt.Println(c("Nothing to compact yet.", 33)) continue } fmt.Println(c("[compacting conversation...]", 33)) sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) nm, sm, err := compact(sigCtx, &cfg, msgs) cancel() if err != nil { if errors.Is(err, context.Canceled) || sigCtx.Err() != nil { fmt.Println(c("\n[interrupted]", 33)) } else { fmt.Println(c("[compact failed: "+err.Error()+"]", 31)) } continue } msgs = nm autosave(msgs) fmt.Println(c(fmt.Sprintf("[compacted to %d messages]", len(msgs)), 32)) fmt.Println(c("--- summary ---", 33) + "\n" + c(sm, 2)) continue case strings.HasPrefix(u, "/cfg"): parts := strings.SplitN(u, " ", 3) if len(parts) == 2 { k := strings.TrimSpace(parts[1]) if v, ok := cfg.Raw[k]; ok { fmt.Println(c(k+"="+v, 32)) } else { fmt.Println(c(k+" not set", 31)) } } else if len(parts) >= 3 { k, v := strings.TrimSpace(parts[1]), strings.TrimSpace(parts[2]) if err := setCfg("model.cfg", k, v); err != nil { fmt.Println(c("[cfg error: "+err.Error()+"]", 31)) continue } cfg = getCfg("model.cfg") if k == "model" || k == "endpoint" || k == "api_key" { cfg.ContextWindow = fetchContextWindow(&cfg) } COL = col(cfg) fmt.Println(c(fmt.Sprintf("[config updated: %s=%s]", k, v), 32)) } else { fmt.Println(c("Usage: /cfg [val]", 31)) } continue case strings.HasPrefix(u, "!"): cmd := strings.TrimSpace(strings.TrimPrefix(u, "!")) if cmd != "" { astr, _ := json.Marshal(map[string]string{"command": cmd}) fmt.Println(c(fmt.Sprintf("[tool call: shell_exec(%s)]", string(astr)), 33)) sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) res := shell(sigCtx, cmd, cfg.ShellTimeout) cancel() fmt.Println(c("[tool result: shell_exec]", 32) + "\n" + c(res, 2)) } continue case u == "/help": fmt.Println(c("Bantam commands:", 1, 36)) for _, kv := range [][2]string{{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save", "save session"}, {"/list", "list sessions"}, {"/load ", "load session"}, {"/compact", "compact context"}, {"/cfg [v]", "get/set config"}, {"!", "run shell command directly"}, {"/help", "show help"}} { fmt.Println(c(fmt.Sprintf(" %-15s", kv[0]), 1, 32) + kv[1]) } continue } turnMsgs := append([]Message{}, msgs...) turnMsgs = append(turnMsgs, Message{Role: "user", Content: strp(u)}) sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) resMsgs, usg, err := AL(sigCtx, &cfg, turnMsgs, sp, 0) cancel() if err != nil { if errors.Is(err, context.Canceled) || sigCtx.Err() != nil { fmt.Println(c("\n[interrupted]", 33)) } else { fmt.Println(c("[error: "+err.Error()+"]", 31)) } continue } msgs = resMsgs autosave(msgs) fmt.Println(c(formatUsage(usg, cfg.ContextWindow), 2)) pct := float64(usg.PromptTokens) * 100.0 / float64(cfg.ContextWindow) if pct >= 60.0 && len(msgs) > 1 { fmt.Print(c(fmt.Sprintf("Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: ", pct, usg.PromptTokens, cfg.ContextWindow), 33)) ans, ok := readPlain("") if ok { ans = strings.TrimSpace(strings.ToLower(ans)) if ans == "" || ans == "y" || ans == "yes" { fmt.Println(c("[compacting conversation...]", 33)) sigCtx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) nm, sm, err := compact(sigCtx, &cfg, msgs) cancel() if err != nil { fmt.Println(c("[compact failed: "+err.Error()+"]", 31)) } else { msgs = nm autosave(msgs) fmt.Println(c(fmt.Sprintf("[compacted to %d messages]", len(msgs)), 32)) fmt.Println(c("--- summary ---", 33) + "\n" + c(sm, 2)) } } } } } done: autosave(msgs) saveHistory() }