2026-10-14 00:01:03 +03:00
# 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.
2026-10-14 08:36:39 +03:00
- 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.
2026-10-14 00:01:03 +03:00
## Repository layout
```
2026-10-14 08:36:39 +03:00
bantam.hpp all shared declarations (structs, prototypes, native Json type)
2026-10-14 00:01:03 +03:00
util.cpp string building, UTF-8, prompt-injection filter, color, IDs, MD5
config.cpp config file parsing, env precedence, /models discovery
2026-10-14 08:36:39 +03:00
json.cpp native RFC 8259 JSON engine, request building + response parsing
2026-10-14 00:01:03 +03:00
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 /
2026-10-14 08:36:39 +03:00
library search paths. The JSON layer is implemented natively in `json.cpp`
and needs no separate dependency.
2026-10-14 00:01:03 +03:00
- **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
2026-10-14 08:36:39 +03:00
make # builds & strips ./bantam (c++ -std=c++20 -O2 -march=native -s -Wall -Wextra -Wpedantic)
2026-10-14 08:38:40 +03:00
make 32bit # builds & strips a 32-bit x86 ./bantam (-m32) with large file support
2026-10-14 00:01:03 +03:00
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 <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:
` ``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/<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:
` ``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 <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` 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: <ses_...>`, and a fresh ` x-opencode-request: <msg_...>`
per call (the 12-hex timestamp prefix in ` x-opencode-request` is the bitwise
inverse of ` x-opencode-session`). The ` Authorization: Bearer <api_key>` 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
` <line>: <content>` format; if a directory, returns a list of directory
entries (subdirectories include a trailing ` /`).
### ` write` tool
2026-10-14 10:58:08 +03:00
- 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.)
2026-10-14 00:01:03 +03:00
### ` bash` tool
- Parameters: ` command` (string, required); ` workdir` (string, optional).
2026-10-14 10:58:08 +03:00
- 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`
2026-10-14 00:01:03 +03:00
(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
2026-10-14 08:36:39 +03:00
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.
2026-10-14 00:01:03 +03:00
## Credits
Created by Luxferre in 2026, released into the public domain with no
warranties.