init commit
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
# 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), **GNU readline** (interactive line
|
||||
editing) and the vendored single-header **nlohmann/json** library (vendored
|
||||
under `nlohmann/json.hpp`); the unsafe json-c C API is no longer used.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
bantam.hpp all shared declarations (structs, prototypes)
|
||||
util.cpp string building, UTF-8, prompt-injection filter, color, IDs, MD5
|
||||
config.cpp config file parsing, env precedence, /models discovery
|
||||
json.cpp request building + non-stream response parsing (nlohmann/json)
|
||||
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
|
||||
nlohmann/ vendored nlohmann/json single-header (v3.12.0)
|
||||
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 a vendored single-header
|
||||
(`nlohmann/json.hpp`) 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 ./bantam (c++ -std=c++20 -O2 -s -Wall -Wextra -Wpedantic)
|
||||
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`); `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 <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. The vendored nlohmann/json library is used for all JSON parsing and
|
||||
serialization instead of the unsafe json-c C API.
|
||||
|
||||
## Credits
|
||||
|
||||
Created by Luxferre in 2026, released into the public domain with no
|
||||
warranties.
|
||||
Reference in New Issue
Block a user