Files

314 lines
11 KiB
C++

/* Bantam: tiny, powerful, DIY AI agent — modern C++20 port
* Created by Luxferre in 2026, released into the public domain.
*
* This is a C++20 rewrite of the pure-C port (bantam-c). It talks to any
* OpenAI-compatible /chat/completions API and implements the same agentic
* loop, tools, session management, markdown terminal rendering and
* raw-terminal line editor.
*
* 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 modern C++20 with zero external bloat.
*
* Feature macros: we rely on POSIX.1-2008 interfaces. */
#ifndef BANTAM_HPP
#define BANTAM_HPP
#include <cstdarg>
#include <cstdio>
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
/* ------------------------------------------------------------------ */
/* Minimal RFC 8259 JSON value, parser and serializer (json.cpp). */
/* ------------------------------------------------------------------ */
enum class JType { Null, Bool, Int, Float, Str, Arr, Obj };
struct Json {
JType type = JType::Null;
bool bval = false;
int64_t ival = 0;
double dval = 0.0;
std::string sval;
std::vector<Json> aval;
std::vector<std::pair<std::string, Json>> oval;
Json();
~Json();
Json(const Json &);
Json(Json &&) noexcept;
Json &operator=(const Json &);
Json &operator=(Json &&) noexcept;
Json(std::nullptr_t);
Json(bool b);
Json(int n);
Json(int64_t n);
Json(double d);
Json(const char *s);
Json(std::string s);
Json(std::string_view s);
static Json object();
static Json array();
bool is_null() const { return type == JType::Null; }
bool is_bool() const { return type == JType::Bool; }
bool is_number_integer() const { return type == JType::Int; }
bool is_number() const { return type == JType::Int || type == JType::Float; }
bool is_string() const { return type == JType::Str; }
bool is_array() const { return type == JType::Arr; }
bool is_object() const { return type == JType::Obj; }
bool empty() const;
size_t size() const;
bool contains(std::string_view k) const;
Json &operator[](std::string_view k);
const Json &operator[](std::string_view k) const;
Json &at(size_t i);
const Json &at(size_t i) const;
void push_back(Json v);
std::string str(std::string_view def = "") const;
int64_t as_int64(int64_t def = 0) const;
int as_int(int def = 0) const;
std::string dump(int indent = -1) const;
static bool parse(std::string_view src, Json &out);
};
/* Markdown renderer state (code-fence tracking). */
struct MdState {
bool in_code = false;
};
/* ------------------------------------------------------------------ */
/* Tool call / message / usage / config structures. */
/* ------------------------------------------------------------------ */
struct ToolCall {
std::string id;
std::string name;
std::string arguments; /* raw JSON text of the arguments object */
};
struct Message {
std::string role;
std::string content; /* empty when absent */
std::string reasoning; /* empty when absent */
std::vector<ToolCall> calls;
std::string tool_call_id; /* empty when absent (tool role only) */
int calls_shown = 0;
};
struct Messages {
std::vector<Message> items;
};
struct Usage {
int prompt_tokens = 0;
int completion_tokens = 0;
int total_tokens = 0;
int cached_tokens = 0;
int context_tokens = 0;
std::string model;
};
struct Cfg {
std::string endpoint;
std::string model;
std::string api_key;
std::string color;
double temperature = 0.7;
int timeout = 300;
int shell_timeout = 120;
int max_al_iterations = 1000;
int stream = 1; /* 0/1 */
int force_stream = -1; /* -1 = auto, 0/1 = force (compaction) */
int context_window = 262144;
int max_tool_res = 65536;
bool context_window_set = false;
std::map<std::string, std::string> raw; /* raw key/value map */
};
struct Session {
std::string id;
std::string created;
std::string summary;
Messages messages;
};
/* ------------------------------------------------------------------ */
/* Globals. */
/* ------------------------------------------------------------------ */
extern int g_col; /* color enabled (0/1) */
extern volatile int g_interrupted; /* SIGINT flag */
/* ------------------------------------------------------------------ */
/* util.cpp */
/* ------------------------------------------------------------------ */
void setup_signal_handler();
int is_terminal(int fd);
int to_int_val(std::string_view s, int def);
bool read_file_str(std::string_view path, std::string &out);
bool write_file_str(std::string_view path, std::string_view data);
std::string sfmt(const char *fmt, ...);
void str_appendf(std::string &s, const char *fmt, ...);
/* UTF-8 helpers. */
int utf8_decode(const unsigned char *p, size_t avail, size_t *plen);
std::string_view utf8_truncate(std::string_view s, size_t max_bytes);
std::string filter_text(std::string_view s);
int color_enabled(const Cfg &cfg);
template<class R>
inline std::string cstr(std::string_view text, const R& codes) {
if (!g_col || codes.empty() || text.empty()) return std::string(text);
std::string s;
s.reserve(text.size() + 16);
s += "\033[";
for (size_t i = 0; i < codes.size(); i++) {
if (i) s += ';';
int v = (int)codes[i];
if (v <= 0) {
s += '0';
} else {
char rev[12];
int rn = 0;
while (v > 0 && rn < 12) { rev[rn++] = (char)('0' + (v % 10)); v /= 10; }
while (rn > 0) s += rev[--rn];
}
}
s += 'm';
s += text;
s += "\033[0m";
return s;
}
template<class R>
inline void cprint(std::string_view text, const R& codes) {
if (text.empty()) return;
if (!g_col || codes.empty()) {
std::fwrite(text.data(), 1, text.size(), stdout);
return;
}
char seq[32];
size_t pos = 0;
seq[pos++] = '\033';
seq[pos++] = '[';
for (size_t i = 0; i < codes.size() && pos + 12 < sizeof(seq); i++) {
if (i) seq[pos++] = ';';
int v = (int)codes[i];
if (v <= 0) {
seq[pos++] = '0';
} else {
char rev[12];
int rn = 0;
while (v > 0 && rn < 12) { rev[rn++] = (char)('0' + (v % 10)); v /= 10; }
while (rn > 0) seq[pos++] = rev[--rn];
}
}
seq[pos++] = 'm';
std::fwrite(seq, 1, pos, stdout);
std::fwrite(text.data(), 1, text.size(), stdout);
std::fwrite("\033[0m", 1, 4, stdout);
}
int visible_len(std::string_view s);
int rune_width(int cp);
void mkdir_p(std::string_view dir);
std::string project_id();
std::string bantam_tmp_dir(std::string_view sess_id = "");
std::string tool_res_dir(std::string_view sess_id = "");
std::string md5_hex(std::string_view data);
/* ------------------------------------------------------------------ */
/* config.cpp */
/* ------------------------------------------------------------------ */
const std::string *cfg_raw_get(const Cfg &c, std::string_view k);
bool internal_key(std::string_view k);
Cfg get_cfg(std::string_view path);
int set_cfg(std::string_view path, std::string_view key, std::string_view val);
std::string config_path();
int fetch_context_window(Cfg &cfg);
std::string list_models(Cfg &cfg, int *errout);
std::string tools_dir(const Cfg &c);
std::string skills_dir(const Cfg &c);
/* ------------------------------------------------------------------ */
/* json.cpp — JSON engine, request building & response parsing. */
/* ------------------------------------------------------------------ */
void serialize_message_json(Json &obj, const Message &m);
void parse_message_json(const Json &obj, Message &m);
std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream);
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage);
/* ------------------------------------------------------------------ */
/* llm.cpp — HTTP client (libcurl), headers, backoff, dispatch. */
/* ------------------------------------------------------------------ */
void http_cleanup();
int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code,
std::string &errmsg);
int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
std::string &errmsg, volatile int *interrupted);
/* ------------------------------------------------------------------ */
/* tools.cpp */
/* ------------------------------------------------------------------ */
std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
int timeout, volatile int *interrupted);
std::string read_file_or_dir(std::string_view path, int offset, int limit,
int *style);
std::string write_file(std::string_view path, long line_offset, long byte_offset,
long del_lines, long del_bytes,
std::string_view content, int *err);
std::string offload_tool_result(std::string_view res, int max_res,
std::vector<std::string> &tmps);
void cleanup_temps(std::vector<std::string> &tmps);
/* ------------------------------------------------------------------ */
/* session.cpp */
/* ------------------------------------------------------------------ */
void save_session(const Messages &msgs, std::string_view sid,
std::string *out_id, std::string *out_summary);
std::vector<Session> sessions_load();
std::unique_ptr<Messages> load_session(std::string_view sid, std::string *err);
std::unique_ptr<Session> find_project_session();
void autosave(const Messages &msgs);
std::unique_ptr<Messages> compact(Cfg &cfg, const Messages &msgs,
std::string *summary, std::string *err,
volatile int *interrupted);
std::string summary_of(const Messages &msgs);
/* ------------------------------------------------------------------ */
/* render.cpp — terminal markdown rendering. */
/* ------------------------------------------------------------------ */
void render_md_print(std::string_view text);
void render_md_line_print(std::string_view line, MdState &st);
void render_table_print(const std::vector<std::string> &lines);
bool is_table_line(std::string_view line);
/* ------------------------------------------------------------------ */
/* editor.cpp — GNU readline line editor + history (+ term helpers). */
/* ------------------------------------------------------------------ */
void load_history();
void save_history();
void add_history(std::string_view s);
std::string read_line(std::string_view prompt, bool &ok);
std::string read_plain(std::string_view prompt, bool &ok);
int term_width();
#endif /* BANTAM_HPP */