Files
bantam-cpp/README.md
T

411 lines
19 KiB
Markdown
Raw Normal View History

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.
- 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
```
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
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 /
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
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
- 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).
- 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
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.