2026-08-08 23:10:10 +03:00
2026-08-08 23:10:10 +03:00
2026-08-16 08:41:12 +03:00
2026-08-15 08:30:40 +03:00
2026-08-17 09:53:02 +03:00
2026-08-09 10:37:07 +00:00
2026-08-15 08:27:24 +03:00
2026-08-08 23:10:10 +03:00
2026-08-15 08:27:24 +03:00
2026-08-08 23:10:10 +03:00

Bantam: tiny, powerful, DIY AI agent

About

Bantam is a minimalist, dependency-free AI agent specification with reference implementations in Go (main.go + term_*.go, module code.luxferre.top/luxferre/bantam) and Perl 5 as MicroBantam (mb, under 100 SLOC). It provides an agentic loop capable of autonomous tool execution, direct shell interaction, real-time response streaming, markdown terminal rendering with box-drawing tables, Fibonacci backoff network resilience, context window auto-discovery, token usage tracking with prompt cache breakdowns, prefix-cache-friendly conversation compaction, and subagent delegation using any OpenAI-compatible completions API.

The entire philosophy of Bantam is built upon two principles:

  1. The structure must be as simple as possible for anyone to be able to reimplement the agent from a plain algorithm description.
  2. The agent only needs to provide two tools: a tool to call shell commands and a tool to call itself. In theory, this should be sufficient to give LLMs the ability to handle tasks of any complexity.

Because of the second principle, Bantam itself was named after Victorinox Bantam Alox, a small and lightweight Swiss army knife with only two tools.

Usage

Prerequisites

  • Go 1.21+ or Perl 5.14+ (standard library / core modules only)
  • An OpenAI-compatible API endpoint (or OpenAI API key)

Installation (Go)

go install code.luxferre.top/luxferre/bantam@latest

This installs the bantam binary into $(go env GOPATH)/bin (make sure it is on your PATH). To build from a local checkout instead:

go build ./...        # produces ./bantam
# or run without building:
go run . prompt.txt

The Go port is a single main.go plus four platform files (term_linux.go, term_bsd.go, term_windows.go, term_other.go) for the built-in raw-terminal line editor — zero external dependencies.

Running Bantam

All implementations read the same model.cfg and system.txt from the current working directory.

  1. Configure model.cfg with your API settings:

    endpoint=https://opencode.ai/zen/v1
    model=deepseek-v4-flash-free
    temperature=0.7
    api_key=your_api_key_here
    stream=true
    context_window=200000
    
  2. Interactive mode:

    bantam            # Go (or: go run .)
    ./mb              # MicroBantam (Perl 5)
    

    In interactive mode, prompts can span multiple lines: press Ctrl+J to insert a real line break (the cursor moves to the next line), then Enter to submit the whole multi-line prompt. The Go port ships its own raw-mode line editor (arrow keys move the cursor, Up/Down browse history, Backspace edits, Ctrl+C clears line / interrupts in-flight run, Ctrl+D exits), working everywhere without third-party dependencies.

    After every interaction, Bantam displays token usage (prompt tokens, cached/uncached breakdown when supported by the provider, completion tokens, and context window utilization):

    [tokens: 1420 prompt (1000 cached, 420 uncached) + 85 completion | context: 1420/200000 (0.7%)]
    

    Sessions are saved under ~/.bantam/sessions/ and can be managed with these commands:

    • /save — save the entire conversation to a new session file (auto-id like 20260808-190038) and generate its summary
    • /list — list saved sessions (newest first) with their ids, timestamps, message counts and summaries
    • /load <id> — load a saved session (exact id or unique prefix) and continue from there
    • /compact — compact context down to the system message and a concise summary using the LLM; the compaction prompt is appended directly to the existing message prefix to guarantee a 100% prompt cache hit
    • /cfg <param> [val] — inspect or update a configuration parameter in model.cfg live
    • !<cmd> — execute a shell command directly through shell_exec without adding the result to the conversation context (Go port)
    • /help — show all supported commands
    • /clear — reset the conversation to just the system prompt
    • /quit — exit

    When context window usage reaches 60% or higher, Bantam automatically offers to compact the conversation:

    Context usage is at 62.4% (124800 / 200000 tokens). Compact conversation? [Y/n]: 
    

    Pressing Ctrl+C during an active inference run or long-running shell execution cleanly interrupts the turn without appending partial or malformed responses to the conversation context.

    The current conversation is also auto-saved to ~/.bantam/sessions/autosave.json after every turn, on /clear, /load, /compact, and on exit — so you can always /load autosave to resume where you left off.

  3. File input mode:

    bantam prompt.txt              # Go
    ./mb prompt.txt                # MicroBantam (Perl 5)
    

Rules of Bantam (The Algorithm)

Using these rules, everyone can build their own copy of Bantam from scratch in little time in any language that supports file access, shell and HTTP(S) calls.

High-Level Overview

  1. Initialization: Read system.txt and model.cfg. Discover context window size from the /models endpoint (or fallback to context_window from model.cfg or 200,000 tokens). Prepare an array of messages starting with the system prompt {"role": "system", "content": system_prompt}.
  2. Input Processing: Take user prompt (via command-line file parameter or interactive stdin). If prefixed with !, execute the command directly via shell_exec without appending to conversation context. Otherwise, append {"role": "user", "content": prompt}, and invoke AL(cfg, messages).
  3. Agentic Loop (AL):
    • Send messages and tool definitions to the OpenAI-compatible /chat/completions API endpoint with custom User-Agent headers and stream_options: {"include_usage": true}.
    • Support context cancellation (e.g. on SIGINT / Ctrl+C) to cleanly abort in-flight requests without appending incomplete messages.
    • On network or HTTP failure, retry using Fibonacci backoff delays (1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s).
    • If stream=true, parse SSE data chunks (data: {...}) in real-time to stream reasoning content (reasoning_content) and response text directly to stdout, bracketing the reasoning block with --- reasoning start --- / --- reasoning end --- markers, rendering Markdown and tables constrained to terminal width.
    • Reconstruct the assistant message and track usage tokens (prompt_tokens, completion_tokens, cached tokens). If tool_calls exist, trace the call ([tool call: name(args)]), validate JSON arguments, execute the requested tool (shell_exec or run_subagent), trace the result ([tool result: name]), append the tool response {"role": "tool", "tool_call_id": id, "content": result}, and repeat the loop.
    • If no tool calls remain or max_al_iterations is reached, return the updated messages list and turn usage stats.
  4. Post-Turn Reporting & Compaction:
    • Display token usage and context window percentage.
    • If context usage is >= 60%, prompt user to compact.
    • Compaction appends "You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely..." as a user message to the conversation, invokes the LLM (ensuring zero prompt cache misses), and resets the conversation to the system prompt and the resulting summary.

Main program

  1. Read system prompt from system.txt (default if missing).
  2. Read model parameters from model.cfg (key=value format) and discover context window size.
  3. Prepare a new message list with the system prompt (role: "system").
  4. Read the first command-line parameter. If non-empty, read user prompt from the specified file. If prefixed with !, execute the shell command directly via shell_exec and exit. Otherwise, append to messages (role: "user"), run AL(cfg, messages), display token usage, and exit.
  5. Read user prompt from standard input (with readline line editing and history in ~/.bantam_history; Ctrl+J inserts a real newline into the line being edited). If equal to /quit or EOF, exit. If equal to /clear, reset messages to step 3 and return to step 5. If equal to /save, write the whole messages array to ~/.bantam/sessions/<id>.json (with an auto-generated summary) and return to step 5. If equal to /list, print saved sessions and their summaries and return to step 5. If starting with /load, replace messages with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to /compact, ask the LLM to summarize the conversation by appending the compaction prompt to preserve KV cache, replace messages with [system, summary-user-message], and return to step 5. If starting with /cfg, display the current value (/cfg <param>) or update model.cfg live (/cfg <param> <val>) and return to step 5. If starting with !, execute the command directly via shell_exec without adding the result to messages and return to step 5. If equal to /help, print the command list and return to step 5. After every user turn and on exit, auto-save messages to ~/.bantam/sessions/autosave.json.
  6. Append user prompt to messages (role: "user"), run AL(cfg, messages), display token usage, check 60% context threshold for auto-compaction, and go to step 5.

Agentic loop (AL(cfg, messages)) function

  1. Call OpenAI-compatible Completions API (POST {endpoint}/chat/completions) forwarding relevant parameters from cfg (model, temperature, stream, reasoning_effort, etc., excluding internal agent configs like endpoint, api_key, timeout, shell_timeout, max_al_iterations, color, context_window) and optional api_key bearer header.
    • Set custom User-Agent header (Mozilla/5.0 (compatible; Bantam/1.0)) to avoid gateway 403 blocks.
    • Retry network/HTTP errors with Fibonacci backoff delays (1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s).
    • If stream=true, parse SSE stream (data: {...}) for real-time reasoning and text output, bracketing reasoning with --- reasoning start --- / --- reasoning end --- markers.
  2. Append the assistant's response message object to messages. If non-streaming and response has reasoning tokens (reasoning_content or reasoning), output them wrapped in --- reasoning start --- / --- reasoning end --- markers.
  3. If there are pending tool_calls in the assistant response:
    • For each tool call, output a trace log ([tool call: name(args)]).
    • Validate JSON arguments. If invalid, format a tool-error response so the LLM can self-correct.
    • Execute tool action (shell_exec or run_subagent).
    • Output a trace log of the result ([tool result: name]).
    • Append tool result message (role: "tool", tool_call_id, content: result string) to messages.
    • Loop back to step 1.
  4. If no pending tool calls (or if max_al_iterations is reached), stop and return messages and accumulated usage.

If the API rejects the request with an Invalid assistant message: content or tool_calls must be set error (usually caused by a previously cut-off stream that left an empty assistant message in the session), all implementations strip the last assistant-role message from the session and retry the call.

Model configuration parameters

(shared by all implementations; model.cfg is plain key=value with # comments)

  • endpoint (base OpenAI-compatible API URL, default https://opencode.ai/zen/v1)
  • model (model name, e.g. gpt-4o)
  • temperature (model temperature, default 0.7)
  • api_key (API key / Bearer token, optional; fall back to OPENAI_API_KEY env var)
  • stream (stream response tokens in real-time, default true)
  • color (ANSI coloring: auto (TTY-detected, default), always, or never; also disabled by NO_COLOR/BANTAM_NO_COLOR env vars)
  • timeout (HTTP timeout in seconds for LLM API calls, default 300; in the Go port it bounds connection setup and time-to-first-byte, so long streaming responses are not cut off mid-stream)
  • shell_timeout (timeout in seconds for shell_exec commands, default 120)
  • max_al_iterations (max tool-call loop iterations per AL() invocation, default 1000)
  • context_window (context window size in tokens, auto-discovered from /models API if available, fallback to this setting, default 200000)

The Go port's built-in editor tracks the cursor with its own column math (terminal auto-wrap aware) and redraws from the first line of the buffer, so wrapped input stays clean at any terminal width.

When enabled, the interactive console uses a subtle ANSI palette: the pending-request status ...requesting... is darkened bold, reasoning markers are cyan, reasoning text is dim, [tool call: ...] traces are yellow, [tool result: ...] headers are green, tool result bodies are dim (red for tool errors/unknown tools), and errors/network retries are red. Tool result payloads fed back to the LLM are never colored.

Tool call definitions

run_subagent tool

  • Parameters: prompt (string)
  • Return value: string
  • Action: run AL(cfg, [{"role": "system", "content": system_prompt + "\n\nImportant: this is a child agent"}, {"role": "user", "content": prompt}]) subject to recursion depth limit (MAX_DEPTH = 5) and return the text content of the last assistant-role message.

shell_exec tool

  • Parameters: command (string)
  • Return value: string
  • Action: run shell command specified in command subject to shell_timeout (default 120s) and return output + '\n\nexit: ' + exit_code string.

MicroBantam

MicroBantam (mb) is a compressed Perl 5 reference implementation of the same agent in under 100 SLOC, written to stay readable while keeping the full agentic core. It reads the same model.cfg and system.txt from the current working directory.

Features

  • Full agentic loop: LLM calls, shell_exec / run_subagent tool execution with JSON argument validation (invalid args are fed back so the model can self-correct), and the 5-level subagent recursion depth limit
  • A ...requesting... in-flight indicator: in-place on a TTY (\r overwrite, erased on completion), a plain line when output is piped
  • Session management: /save, /list, /load <id> (exact id only, no prefix matching), /cfg <param> [val], and auto-save to ~/.bantam/sessions/autosave.json after every turn and on exit; session ids get -1, -2, ... suffixes on same-second collisions
  • Interactive mode (/quit, /clear, /save, /list, /load <id>, /cfg, /help) and file input mode
  • Same built-in default system prompt and OPENAI_API_KEY fallback as the Go implementation
  • Automatic recovery from Invalid assistant message: content or tool_calls must be set API errors: strips the last assistant message and retries

What it drops

  • Streaming (requests are non-streaming; stream is ignored)
  • ANSI coloring/styling (color is ignored)
  • Line editing, Ctrl+J multi-line prompts and history (plain single-line prompts)
  • Fibonacci backoff network retries (a failed request aborts with an API error message)
  • /compact context summarization

Running MicroBantam

./mb            # interactive (or: perl mb)
./mb prompt.txt # file input mode

Repository layout

  • main.go, term_linux.go, term_bsd.go, term_windows.go, term_other.go — Go implementation (stdlib only, module code.luxferre.top/luxferre/bantam)
  • mb — MicroBantam, compressed Perl 5 implementation (core modules only, under 100 SLOC)
  • model.cfg, system.txt — shared configuration and system prompt
  • README.md — this document

Extra tools

The extras/ directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's only built-in tools are shell_exec and run_subagent, these helpers can be invoked directly by the agent through shell_exec to give it real-world capabilities (web search, live weather) that the base model alone does not have. They are plain /bin/sh scripts depending only on curl (and jq where noted), so the agent can discover and run them just like any other command.

extras/websearch

A simple web search tool for Bantam built on Exa's MCP server (Streamable HTTP). It speaks the JSON-RPC MCP protocol (initialize -> notifications/initialized -> tools/call with web_search_exa) and prints the formatted result text. Usage:

extras/websearch "your query"           # default 5 results
extras/websearch "your query" 10        # custom number of results

Environment:

  • EXA_MCP_ENDPOINT — endpoint URL (default https://mcp.exa.ai/mcp).
  • EXA_API_KEY — optional; when set, it is passed as an ?api_key= query parameter so no extra HTTP headers are added beyond Content-Type.

Dependencies: curl, jq.

extras/weather

A wrapper around the wttr.in weather API that queries current conditions and forecasts for any location. It supports the graphical ANSI terminal view (default), one-line presets (1-4), custom %-notation formats, and the rich JSON document (?format=j1). Usage:

extras/weather                          # auto-detect location from request IP
extras/weather London                   # graphical report
extras/weather -f 3 "New York"          # one-line preset
extras/weather -f "%l: %c %t" Paris     # custom one-line format
extras/weather -u u -L de Berlin        # USCS units, German output
extras/weather -j Tokyo                 # raw JSON (pretty-printed via jq)

Options include -l/--location, -u/--units (m/u/M), -L/--lang, -f/--format, -j/--json, -0/-1/-2 (view depth), -q/--quiet, -A (force ANSI) and -h/--help. Environment overrides: WTTRAPI (default https://wttr.in) and WEATHER_TIMEOUT (default 20s).

Dependencies: curl, jq (only required for the JSON format).

FAQ

Does Bantam support AGENTS.md etc?

The default system prompt instructs the agent to respect AGENTS.md/GEMINI.md/CLAUDE.md files.

Is there any common config place for Bantam?

No, loading model.cfg and system.txt is deliberately only supported from the current working directory. This allows natural separation of configs and system prompts per project. In case there's no system.txt inside the project, the concise and sensible default system prompt will be loaded. In case there's no model.cfg inside the project, Bantam will use the free Big Pickle model from OpenCode Zen with the temperature 0.7. Big Pickle has been chosen as the default because it has no set expiration date, unlike other OpenCode's keyless tiers.

Why no MCP support?

If you need MCP server tools, there's nothing a simple shell wrapper cannot solve in this case.

Why no sandboxing?

Same philosophy as the Pi agent: there's nothing a simple chroot environment cannot solve in case sandboxing is really necessary.

Any advanced authentication schemes or header injection?

You can pair Bantam with the Dynagate LLM gateway to achieve all that.

How to run on mobiles?

On Android, Bantam (Go) and MicroBantam (mb) are easily runnable within the Termux environment. The Go implementation is preferred for performance reasons.

On iOS/iPadOS, the easiest way to use Bantam is to run MicroBantam (mb) inside iSH. Some terminal features may not be available (run with rlwrap to bring them back), but the agent itself is fully functional.

Credits

Created by Luxferre in 2026, released into the public domain with no warranties.

S
Description
Tiny, powerful, DIY agent
Readme
541 KiB
Languages
Go 83.8%
Shell 10.1%
Perl 6.1%