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:
- The structure must be as simple as possible for anyone to reimplement the agent from a plain algorithm description.
- The agent only needs a minimal set of tools:
bashto run shell commands,readto inspect files and directories, andwriteto 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, custom 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 POSIXmake(bmake,gmake,smake, …). - libcurl development packages, available via the standard include /
library search paths. The JSON layer is implemented natively in
json.cppand needs no separate dependency. - GNU readline development packages (
libreadline-devon Debian/Ubuntu,readlineon Fedora/macOS) for the interactive prompt, history and line editing. On common systems:# 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:
# 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.:
make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl"
Building
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.
Note: on macOS, if you also have a GNU toolchain installed, you may
have to use make STRIP=/usr/bin/strip to achieve full ABI compatibility.
Running
Bantam reads .bantam.cfg from the current working directory.
-
Configure
.bantam.cfgwith your API settings: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 -
Interactive mode:
./bantamIn 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 interactive prompt is provided by GNU readline, which is a third-party dependency (see Prerequisites). 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):
[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 (namedname, 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 <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 to the conversation to derive the summary, then the conversation is reset to[system, summary-user-message]./cfg <param> [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<name> [prompt], load<skills_dir>/<name>/SKILL.md(or an absolute path when no skills dir is set) and send its contents prefixed to the optionalpromptas the next user turn./models— query the/modelspath on the current inference endpoint and print a plain list of supported model IDs, marking the currently configured model with a leading*.!<cmd>— execute a shell command directly throughbashwithout 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:
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/<project-md5>.jsonafter every turn, on/clear,/load,/compact,/continue, and on exit — so you can always run/continue(or/cont) to resume where you left off. -
File input mode:
./bantam prompt.txtIf the file's first line (or the argument) starts with
!, the rest is run as a direct shell command throughbashand the program exits (the output is not added to the conversation).
Model configuration parameters
(shared with the Go and C implementations; the configuration file is
.bantam.cfg — a plain key=value file with # comments. Values defined in
.bantam.cfg override BANTAM_* environment variables; when .bantam.cfg
is absent, BANTAM_* environment variables override the built-in defaults.)
-
endpoint(base OpenAI-compatible API URL, defaulthttps://api.kilo.ai/api/openrouter; falls back toBANTAM_ENDPOINT). -
model(model name, defaultopenrouter/free; falls back toBANTAM_MODEL). -
temperature(model temperature, default0.7; falls back toBANTAM_TEMPorBANTAM_TEMPERATURE). -
api_key(API key / Bearer token, optional; falls back toBANTAM_API_KEY; set to-or left blank in the config file to explicitly defer toBANTAM_API_KEY). -
stream(stream response tokens in real-time, defaulttrue; falls back toBANTAM_STREAM). -
color(ANSI coloring:auto(TTY-detected, default),always, ornever; disabled byNO_COLOR/BANTAM_NO_COLORenv vars, falls back toBANTAM_COLOR). -
timeout(HTTP timeout in seconds for LLM API calls, default300; bounds connection setup and time-to-first-byte so long streaming responses are not cut off mid-stream; falls back toBANTAM_TIMEOUT). -
shell_timeout(timeout in seconds forbashshell commands, default120; falls back toBANTAM_SHELL_TIMEOUT). -
max_al_iterations(max tool-call loop iterations perAL()invocation, default1000; falls back toBANTAM_MAX_AL_ITERATIONS). -
context_window(context window size in tokens, auto-discovered from/modelsAPI if available, fallback to this setting, default262144; falls back toBANTAM_CONTEXT_WINDOW). -
max_tool_res(maximum number of bytes of a tool result that may be sent into the model context directly, default65536; larger results are written to a unique file under$TMPDIR/bantam/toolresand replaced by a short instruction naming the file and its line count so it can be read in parts using thereadtool, and the file is deleted once the round ends; falls back toBANTAM_MAX_TOOL_RES). -
reasoning_effort(reasoning effort level, forwarded to chat completions API, defaulthigh; falls back toBANTAM_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 <dir>"to the system prompt at startup. Falls back toBANTAM_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_name>/SKILL.md. When set, Bantam appends"Skills may be discovered and invoked from <dir>"to the system prompt and the/skill <skill_name> [prompt]command loads a skill'sSKILL.mdand runs it as a prompt. Falls back toBANTAM_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
- Initialization: read the config file
.bantam.cfgfrom the current working directory. Discover the context-window size from the/modelsendpoint (or fall back tocontext_window, default262144tokens). Prepare the messages array starting with the built-in system prompt. - Input processing: take the user prompt (command-line file parameter or
interactive stdin). If prefixed with
!, execute the command directly viabashwithout appending to the conversation context. Otherwise append{"role": "user", "content": prompt}and invokeAL(cfg, messages). - Agentic loop (
AL):- Send
messagesand tool definitions (bash,read,write) to the OpenAI-compatible/chat/completionsAPI with custom request headers (see below) andstream_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). A4xxerror 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). Iftool_callsexist, trace the call ([tool call: name(args)]), validate JSON arguments, execute the requested tool (bash,read, orwrite; also accepting legacyshell_execandwrite_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_iterationsis reached, return the updated messages list and turn usage stats.
- Send
- 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
usermessage 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 requests to the /chat/completions and /models endpoints are sent with a
minimal, dependency-free header set (see apply_llm_headers / http_get in
llm.cpp):
Content-Type: application/jsonExpect:(empty) — suppresses theExpect: 100-continueround-trip so large POST bodies are sent immediately and do not hang behind some proxies.User-Agent: Mozilla/5.0 Bantam/nightly-cppAuthorization: Bearer <api_key>— added only whenapi_keyis set and is not-(a real key, not the placeholder).
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 acceptspath);offset(integer, optional, 1-indexed, default 1);limit(integer, optional, default 2000). - Action: if the target is a regular file, returns numbered lines in
<line>: <content>format; if a directory, returns a list of directory entries (subdirectories include a trailing/).
write tool
- Parameters:
filePath(string, required; also acceptspath);content(string, required, may be empty);lineOffset(integer, optional, 1-indexed line number to start writing from — takes precedence overbyteOffset);byteOffset(integer, optional, byte offset — used only whenlineOffsetis not given);delLines(integer, optional, number of lines to delete starting at the insertion point — takes precedence overdelBytes);delBytes(integer, optional, number of bytes to delete — used only whendelLinesis not given). - Action: if none of
lineOffset,byteOffset,delLinesanddelBytesare provided, the entire file is overwritten withcontent(the file and any necessary parent directories are created if missing). Otherwisecontentis written at the resolved line/byte offset, optionally deletingdelLineslines ordelBytesbytes first (splicing prefix + content + suffix). Line numbers are 1-indexed and match thereadtool's numbering; alineOffsetpast the end of the file appends at the end. (Also accepted as legacywrite_file; legacyoffset/del_bytesare no longer emitted but thebyteOffset/delBytesbyte-based mode covers the same behavior.)
bash tool
- Parameters:
command(string, required);workdir(string, optional). - Action: run
commandthrough the system shell (bashresolved fromPATH, falling back to/bin/shif bash is unavailable) via-c, subject toshell_timeout(default120s) and returnoutput + '\nexit: ' + exit_code. (Also accepted as legacyshell_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 <dir> 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 <name> [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.