# Bantam: tiny, powerful, DIY AI agent (C++ port) ## About Bantam is a minimalist, dependency-light AI agent that talks to any OpenAI-compatible `/chat/completions` API. This directory is a faithful, **modern C++20 port** of the pure-C reference implementation (`bantam-c`), which itself tracks the Go reference implementation (`main.go` + `term_*.go`, module `code.luxferre.top/luxferre/bantam`). It provides the same agentic loop — autonomous tool execution, direct shell interaction, file writing/editing, 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, and conversation compaction — implemented in C++20 using standard, safe containers and RAII. The terminal markdown renderer (inline styles, headings, lists, block quotes, fenced code, and box-drawing tables) is implemented natively, so no external Markdown library is required. Interactive line editing is handled by **GNU readline**, which provides the prompt, history and Emacs-style key bindings, so no hand-rolled terminal code is needed. The whole philosophy of Bantam rests on two principles: 1. The structure must be as simple as possible for anyone to reimplement the agent from a plain algorithm description. 2. The agent only needs a minimal set of tools: `bash` to run shell commands, `read` to inspect files and directories, and `write` to write/edit files. ### Design goals relative to the C port - Use standard, safe C++ containers (`std::string`, `std::vector`, `std::map`, `std::span`) instead of raw memory areas. - Eliminate hand-rolled allocation/string code; rely on RAII so there is far less room for memory-allocation related vulnerabilities. - Depend only on **libcurl** (HTTP) and **GNU readline** (interactive line editing); JSON parsing/serialization and Markdown rendering are implemented natively in C++20 with no external or vendored library bloat, and the unsafe json-c C API is no longer used. ## Repository layout ``` bantam.hpp all shared declarations (structs, prototypes, native Json type) util.cpp string building, UTF-8, prompt-injection filter, color, IDs, MD5 config.cpp config file parsing, env precedence, /models discovery json.cpp native RFC 8259 JSON engine, request building + response parsing llm.cpp libcurl HTTP client, OpenCode headers, backoff, SSE streaming tools.cpp bash/read/write tools, oversized-result offloading session.cpp message copy, session save/load, compaction render.cpp terminal markdown renderer (native, box-drawing tables) editor.cpp GNU readline line editor + history (+ term helpers) main.cpp REPL, agentic loop, tool dispatch, commands Makefile POSIX-compatible build README.md this document ``` ## Prerequisites - A C++20 compiler (`c++` / `g++` / `clang++`) and a POSIX `make` (`bmake`, `gmake`, `smake`, …). - **libcurl** development packages, available via the standard include / library search paths. The JSON layer is implemented natively in `json.cpp` and needs no separate dependency. - **GNU readline** development packages (`libreadline-dev` on Debian/Ubuntu, `readline` on Fedora/macOS) for the interactive prompt, history and line editing. On common systems: ```bash # Debian / Ubuntu apt-get install libreadline-dev # Fedora / RHEL dnf install readline-devel # macOS (Homebrew) brew install readline ``` - An OpenAI-compatible API endpoint (or an API key for one). On common systems: ```bash # Debian / Ubuntu apt-get install libcurl4-openssl-dev build-essential # Fedora / RHEL dnf install libcurl-devel # FreeBSD pkg install curl # macOS (Homebrew) brew install curl ``` If the libcurl headers or libraries live in a non-standard prefix, override them at build time, e.g.: ```bash make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl" ``` ## Building ```bash make # builds & strips ./bantam (c++ -std=c++20 -O2 -march=native -s -Wall -Wextra -Wpedantic) make 32bit # builds & strips a 32-bit x86 ./bantam (-m32) with large file support make clean # remove build artifacts make install # install to $(DESTDIR)/usr/local/bin/bantam ``` The default `make` produces a single binary, `bantam`, with no runtime dependencies beyond libc, libcurl and libreadline. ## Running All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if present) from the current working directory. 1. Configure `model.cfg` (or `.bantam.cfg`, which takes priority) with your API settings: ```ini endpoint=https://api.kilo.ai/api/openrouter model=openrouter/free temperature=0.7 api_key=your_api_key_here stream=true context_window=200000 max_tool_res=65536 reasoning_effort=high ``` 2. Interactive mode: ```bash ./bantam ``` 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. Bracketed paste is enabled (`ESC[?2004h`) while editing, so a multi-line block pasted from the clipboard is inserted as literal line breaks and only the final **Enter** submits it. The C++ port ships its own raw-mode line editor, working everywhere with no third-party dependencies. It supports arrow keys and Up/Down for history, Backspace / Delete / Ctrl+D (on an empty line) to delete, and Emacs-style editing combos: **Ctrl+A** (start of line), **Ctrl+E** (end of line), **Ctrl+B** / **Ctrl+F** (move by character), **Ctrl+W** (delete previous word), **Ctrl+K** (kill to end of line), **Ctrl+U** (kill to start of line), **Ctrl+Left** / **Ctrl+Right** (move by word), and **Home** / **End** keys. **Ctrl+C** clears the line / interrupts an in-flight run, and **Ctrl+D** on an empty line exits. After every interaction, Bantam displays token usage (prompt tokens, cached / uncached breakdown when supported by the provider, completion tokens, and context-window utilization): ```text [openrouter/free: 1420 prompt (1000 cached, 420 uncached) + 85 completion | context: 1420/262144 (0.5%)] ``` Sessions are saved under `~/.bantam/sessions/` and can be managed with these commands: - `/save [name]` — save the entire conversation to a session file (named `name`, or defaulting to the MD5 hash of the current project directory) and generate its summary. - `/continue` (alias `/cont`) — continue/autoload the session corresponding to the current project directory. - `/list` — list saved sessions (newest first) with their ids, timestamps, message counts and summaries (marking the current project's session). - `/load ` — 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 to the conversation to derive the summary, then the conversation is reset to `[system, summary-user-message]`. - `/cfg [val]` — inspect or update a configuration parameter live (writes to `.bantam.cfg`). - `/model [val]` — alias for `/cfg model` (inspect or set the model). - `/endpoint [val]` — alias for `/cfg endpoint` (inspect or set the API endpoint). - `/skill` — with no argument, list every skill under the configured skills directory (or report that none are configured); with ` [prompt]`, load `//SKILL.md` (or an absolute path when no skills dir is set) and send its contents prefixed to the optional `prompt` as the next user turn. - `/models` — query the `/models` path on the current inference endpoint and print a plain list of supported model IDs, marking the currently configured model with a leading `* `. - `!` — execute a shell command directly through `bash` without adding the result to the conversation context. - `/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: ```text Context usage is at 62.4% (163840 / 262144 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/.json` after every turn, on `/clear`, `/load`, `/compact`, `/continue`, and on exit — so you can always run `/continue` (or `/cont`) to resume where you left off. 3. File input mode: ```bash ./bantam prompt.txt ``` If the file's first line (or the argument) starts with `!`, the rest is run as a direct shell command through `bash` and the program exits (the output is not added to the conversation). ## Model configuration parameters (shared with the Go and C implementations; `.bantam.cfg` takes priority over `model.cfg` when both exist — both are plain `key=value` with `#` comments. Values defined in `.bantam.cfg` override `BANTAM_*` environment variables; when `.bantam.cfg` is absent, `BANTAM_*` environment variables override `model.cfg` and built-in defaults.) - `endpoint` (base OpenAI-compatible API URL, default `https://api.kilo.ai/api/openrouter`; falls back to `BANTAM_ENDPOINT`). - `model` (model name, default `openrouter/free`; falls back to `BANTAM_MODEL`). - `temperature` (model temperature, default `0.7`; falls back to `BANTAM_TEMP` or `BANTAM_TEMPERATURE`). - `api_key` (API key / Bearer token, optional; falls back to `BANTAM_API_KEY`; set to `-` or left blank in the config file to explicitly defer to `BANTAM_API_KEY`). - `stream` (stream response tokens in real-time, default `true`; falls back to `BANTAM_STREAM`). - `color` (ANSI coloring: `auto` (TTY-detected, default), `always`, or `never`; disabled by `NO_COLOR` / `BANTAM_NO_COLOR` env vars, falls back to `BANTAM_COLOR`). - `timeout` (HTTP timeout in seconds for LLM API calls, default `300`; bounds connection setup and time-to-first-byte so long streaming responses are not cut off mid-stream; falls back to `BANTAM_TIMEOUT`). - `shell_timeout` (timeout in seconds for `bash` shell commands, default `120`; falls back to `BANTAM_SHELL_TIMEOUT`). - `max_al_iterations` (max tool-call loop iterations per `AL()` invocation, default `1000`; falls back to `BANTAM_MAX_AL_ITERATIONS`). - `context_window` (context window size in tokens, auto-discovered from `/models` API if available, fallback to this setting, default `262144`; falls back to `BANTAM_CONTEXT_WINDOW`). - `max_tool_res` (maximum number of bytes of a tool result that may be sent into the model context directly, default `65536`; larger results are written to a unique file under `$TMPDIR/bantam/toolres` and replaced by a short instruction naming the file and its line count so it can be read in parts using the `read` tool, and the file is deleted once the round ends; falls back to `BANTAM_MAX_TOOL_RES`). - `reasoning_effort` (reasoning effort level, forwarded to chat completions API, default `high`; falls back to `BANTAM_REASONING_EFFORT`). - `bantam_tools_dir` (optional path to a directory of extra shell tools; when set, Bantam appends `"Extra shell tools can be found at "` to the system prompt at startup. Falls back to `BANTAM_TOOLS_DIR`. Agent-internal hint, not forwarded to the API.) - `bantam_skills_dir` (optional path to a directory of reusable *skills*, each in its own subdirectory as `/SKILL.md`. When set, Bantam appends `"Skills may be discovered and invoked from "` to the system prompt and the `/skill [prompt]` command loads a skill's `SKILL.md` and runs it as a prompt. Falls back to `BANTAM_SKILLS_DIR`. Agent-internal hint, not forwarded to the API.) The C++ port supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables. When enabled, the interactive console uses a subtle ANSI palette: the pending-request status `...requesting...` is dimmed 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. ## The agentic loop 1. **Initialization**: read the config file (`.bantam.cfg` if present, else `model.cfg`). Discover the context-window size from the `/models` endpoint (or fall back to `context_window`, default `262144` tokens). Prepare the messages array starting with the built-in system prompt. 2. **Input processing**: take the user prompt (command-line file parameter or interactive stdin). If prefixed with `!`, execute the command directly via `bash` without appending to the conversation context. Otherwise append `{"role": "user", "content": prompt}` and invoke `AL(cfg, messages)`. 3. **Agentic loop (`AL`)**: - Send `messages` and tool definitions (`bash`, `read`, `write`) to the OpenAI-compatible `/chat/completions` API with custom request headers (see below) and `stream_options: {"include_usage": true}`. - Support context cancellation (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`). A `4xx` error other than the recoverable invalid-assistant-message case fast-fails. - 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 (`bash`, `read`, or `write`; also accepting legacy `shell_exec` and `write_file`), 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 the user to compact. - Compaction appends a compaction prompt as a user message to derive the summary, invokes the LLM, and then resets the conversation to the system prompt plus a `user` message carrying the resulting summary. 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), Bantam strips the last `assistant`-role message from the session and retries the call. ### Custom request headers All endpoints receive custom request headers mimicking OpenCode: `User-Agent: opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14`, `x-opencode-client: cli`, `x-opencode-project: global`, `x-opencode-session: `, and a fresh `x-opencode-request: ` per call (the 12-hex timestamp prefix in `x-opencode-request` is the bitwise inverse of `x-opencode-session`). The `Authorization: Bearer ` header is added whenever `api_key` is set and not `-`. ### Prompt-injection defense Tool arguments and tool outputs are sanitized by filtering out non-printable and space-like Unicode characters (keeping ASCII space, tab, newline, and printable Unicode), protecting against indirect prompt injection before anything is sent back to the model. ## Tool call definitions ### `read` tool - Parameters: `filePath` (string, required; also accepts `path`); `offset` (integer, optional, 1-indexed, default 1); `limit` (integer, optional, default 2000). - Action: if the target is a regular file, returns numbered lines in `: ` format; if a directory, returns a list of directory entries (subdirectories include a trailing `/`). ### `write` tool - Parameters: `filePath` (string, required; also accepts `path`); `offset` (integer, optional, byte offset); `del_bytes` (integer, optional); `content` (string, required, may be empty). - Action: if `offset` and `del_bytes` are both omitted, the entire file is overwritten with `content` (the file and any necessary parent directories are created if missing); otherwise `content` is written at `offset`, optionally deleting `del_bytes` bytes first (splicing prefix + content + suffix and never appending to the end of an existing file). (Also accepted as legacy `write_file`.) ### `bash` tool - Parameters: `command` (string, required); `workdir` (string, optional). - Action: run `command` via `bash -c` subject to `shell_timeout` (default `120s`) and return `output + '\nexit: ' + exit_code`. (Also accepted as legacy `shell_exec`.) ## Extra tools and skills Because Bantam's built-in tools include `bash`, helper scripts can be invoked directly by the agent through `bash`. Point Bantam at a directory of helpers with the `bantam_tools_dir` key (or `BANTAM_TOOLS_DIR`); when set, the line `Extra shell tools can be found at ` is appended to the system prompt. A *skill* is a reusable instruction bundle: a directory containing a `SKILL.md` file, pointed to by `bantam_skills_dir` (or `BANTAM_SKILLS_DIR`), and run with `/skill [prompt]`. ## Memory safety The C++ port leans on the language's standard library and RAII to minimize memory-management hazards: all dynamic text uses `std::string`, message collections use `std::vector`, and curl header lists and streaming state are wrapped in RAII types (`CurlHeaders`, `StreamCtx`) that free themselves on scope exit. There is no hand-rolled allocator and no manual `free` of message or tool-call data, so use-after-free and double-free are structurally unlikely. A lightweight, native RFC 8259 JSON engine (`struct Json` in `json.cpp`) handles all JSON parsing and serialization using standard C++ containers instead of an unsafe C API or a heavy header-only template library. ## Credits Created by Luxferre in 2026, released into the public domain with no warranties.