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, 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 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:
    # 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.

  1. Configure .bantam.cfg with 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
    
  2. Interactive mode:

    ./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 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 (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 <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 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 * .
    • !<cmd> — 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:

    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>.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:

    ./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; 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, 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 <dir>" 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_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'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 from the current working directory. 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 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/json
  • Expect: (empty) — suppresses the Expect: 100-continue round-trip so large POST bodies are sent immediately and do not hang behind some proxies.
  • User-Agent: Mozilla/5.0 Bantam/nightly-cpp
  • Authorization: Bearer <api_key> — added only when api_key is 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 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 <line>: <content> format; if a directory, returns a list of directory entries (subdirectories include a trailing /).

write tool

  • Parameters: filePath (string, required; also accepts path); content (string, required, may be empty); lineOffset (integer, optional, 1-indexed line number to start writing from — takes precedence over byteOffset); byteOffset (integer, optional, byte offset — used only when lineOffset is not given); delLines (integer, optional, number of lines to delete starting at the insertion point — takes precedence over delBytes); delBytes (integer, optional, number of bytes to delete — used only when delLines is not given).
  • Action: if none of lineOffset, byteOffset, delLines and delBytes are provided, the entire file is overwritten with content (the file and any necessary parent directories are created if missing). Otherwise content is written at the resolved line/byte offset, optionally deleting delLines lines or delBytes bytes first (splicing prefix + content + suffix). Line numbers are 1-indexed and match the read tool's numbering; a lineOffset past the end of the file appends at the end. (Also accepted as legacy write_file; legacy offset/del_bytes are no longer emitted but the byteOffset/delBytes byte-based mode covers the same behavior.)

bash tool

  • Parameters: command (string, required); workdir (string, optional).
  • Action: run command through the system shell (bash resolved from PATH, falling back to /bin/sh if bash is unavailable) via -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 <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.

S
Description
Tiny, powerful, DIY agent (C++ version)
https://bantam.luxferre.top Readme
536 KiB
Languages
C++ 98.1%
Makefile 1.9%