654 lines
30 KiB
C++
654 lines
30 KiB
C++
/* main.cpp — entry point, agentic loop, REPL, command dispatch. */
|
|
#include "bantam.hpp"
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cerrno>
|
|
#include <climits>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <curl/curl.h>
|
|
#include <dirent.h>
|
|
#include <span>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <vector>
|
|
|
|
/* ----------------------------- usage / pct ---------------------------- */
|
|
static int ctx_tokens(Usage &u) {
|
|
if (u.context_tokens > u.prompt_tokens) return u.context_tokens;
|
|
return u.prompt_tokens;
|
|
}
|
|
static std::string format_usage(Usage &u, int cw) {
|
|
int base = ctx_tokens(u);
|
|
if (cw <= 0) cw = 262144;
|
|
double pct = (double)base * 100.0 / (double)cw;
|
|
std::string s;
|
|
if (!u.model.empty()) str_appendf(s, "[%s: ", u.model.c_str());
|
|
else s += "[";
|
|
if (u.cached_tokens > 0) {
|
|
int uncached = u.prompt_tokens - u.cached_tokens;
|
|
if (uncached < 0) uncached = 0;
|
|
str_appendf(s, "%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]",
|
|
u.prompt_tokens, u.cached_tokens, uncached, u.completion_tokens, base, cw, pct);
|
|
} else {
|
|
str_appendf(s, "%d prompt + %d completion | context: %d/%d (%.1f%%)]",
|
|
u.prompt_tokens, u.completion_tokens, base, cw, pct);
|
|
}
|
|
return s;
|
|
}
|
|
static double context_pct(Usage &u, int cw) {
|
|
if (cw <= 0) cw = 262144;
|
|
return (double)ctx_tokens(u) * 100.0 / (double)cw;
|
|
}
|
|
|
|
/* ----------------------------- invalid-assistant detection ------------- */
|
|
static bool is_invalid_assistant_err(const char *err) {
|
|
if (!err) return false;
|
|
std::string low = err;
|
|
for (char &c : low) c = (char)std::tolower((unsigned char)c);
|
|
return low.find("invalid assistant message") != std::string::npos
|
|
|| low.find("content or tool_calls must be set") != std::string::npos
|
|
|| low.find("tool_calls must be set") != std::string::npos
|
|
|| low.find("content must be set") != std::string::npos;
|
|
}
|
|
|
|
/* ----------------------------- sanitize args -------------------------- */
|
|
static std::string sanitize_arguments(std::string_view args) {
|
|
std::string f = filter_text(args);
|
|
Json o;
|
|
if (Json::parse(f, o) && o.is_object()) return f;
|
|
Json w = Json::object();
|
|
w["invalid_raw"] = f;
|
|
return w.dump();
|
|
}
|
|
|
|
/* ----------------------------- system prompt -------------------------- */
|
|
static const char *default_system_prompt =
|
|
"You are Bantam, a tiny, powerful AI agent. Solve the user's task using three tools:\n"
|
|
"- bash: run a bash command; returns its output and exit code.\n"
|
|
"- read: read a file or directory from the local filesystem; returns numbered lines or directory entries.\n"
|
|
"- write: write content to a file; if lineOffset/byteOffset and delLines/delBytes are all omitted it overwrites the entire file, otherwise it writes at the given line or byte offset (optionally deleting lines or bytes first); line-based edits are preferred; returns status.\n\n"
|
|
"Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.\n"
|
|
"Use the target system's filesystem deliberately:\n"
|
|
"- Use only $TMPDIR/bantam as the temporary directory for intermediate or scratch files (logs, downloads, temporary build outputs, etc.); create it if it does not exist.\n"
|
|
"- Create permanent artifacts in the current working directory unless the user explicitly instructs otherwise.\n"
|
|
"- Tool results larger than $MAX_TOOL_RES bytes are not inserted into the context in full: they are saved under $TMPDIR/bantam/toolres and replaced by a short note giving the file path and its total byte length. Read such a file with the read tool, paging through it with line offsets (using offset and limit) instead of loading it all at once.\n\n"
|
|
"When generating code:\n"
|
|
"- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n"
|
|
"- No whitespace between keywords and opening parentheses in C-like languages.\n"
|
|
"- Write optimally and with as few third-party dependencies as possible.\n"
|
|
"- Always test.\n"
|
|
"- No emojis in code or documentation.\n"
|
|
"- Respect AGENTS.md contents in the project.";
|
|
|
|
static std::string build_system_prompt(Cfg &cfg) {
|
|
std::string tmp = bantam_tmp_dir();
|
|
std::string sp = default_system_prompt;
|
|
size_t found = 0;
|
|
while ((found = sp.find("$TMPDIR/bantam", found)) != std::string::npos) {
|
|
sp.replace(found, std::strlen("$TMPDIR/bantam"), tmp);
|
|
found += tmp.size();
|
|
}
|
|
char mtr[32]; std::snprintf(mtr, sizeof mtr, "%d", cfg.max_tool_res);
|
|
auto fm = sp.find("$MAX_TOOL_RES");
|
|
if (fm != std::string::npos) sp.replace(fm, std::strlen("$MAX_TOOL_RES"), mtr);
|
|
std::string out = sp;
|
|
std::string td = tools_dir(cfg);
|
|
if (!td.empty()) { out += "\n\nExtra shell tools can be found at "; out += td; }
|
|
std::string sd = skills_dir(cfg);
|
|
if (!sd.empty()) { out += "\n\nSkills may be discovered and invoked from "; out += sd; }
|
|
return out;
|
|
}
|
|
|
|
/* ----------------------------- json arg helpers ----------------------- */
|
|
static std::string get_str(const Json &j, const char *k) {
|
|
return j[k].str();
|
|
}
|
|
static int get_int(const Json &j, const char *k, int def = 0) {
|
|
return j[k].as_int(def);
|
|
}
|
|
static std::string tool_path_arg(const Json &a) {
|
|
std::string v = get_str(a, "filePath");
|
|
if (v.empty()) v = get_str(a, "path");
|
|
return v;
|
|
}
|
|
static bool trim_is_empty(std::string_view s) {
|
|
size_t i = 0;
|
|
while (i < s.size() && (s[i] == ' ' || s[i] == '\n' || s[i] == '\r' || s[i] == '\t')) i++;
|
|
return i == s.size();
|
|
}
|
|
static std::string last_role(Messages &msgs) {
|
|
if (msgs.items.empty()) return "";
|
|
return msgs.items.back().role;
|
|
}
|
|
|
|
/* ----------------------------- AL loop -------------------------------- */
|
|
static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, std::string *err) {
|
|
int done = 0;
|
|
std::vector<std::string> tmps;
|
|
int rc = 0;
|
|
for (int i = 0; i < cfg.max_al_iterations && !done; i++) {
|
|
if (*interrupted) { rc = -1; break; }
|
|
Message m; Usage u;
|
|
std::string lerr;
|
|
int lrc = llm(cfg, msgs, true, m, u, lerr, interrupted);
|
|
if (lrc != 0) {
|
|
if (*interrupted) { rc = -1; break; }
|
|
if (is_invalid_assistant_err(lerr.c_str())) {
|
|
int stripped = 0;
|
|
for (int j = (int)msgs.items.size() - 1; j >= 0; j--) {
|
|
if (msgs.items[j].role == "assistant") {
|
|
cprint("[stripped malformed assistant message]\n", std::array{33});
|
|
msgs.items.erase(msgs.items.begin() + j);
|
|
stripped = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (stripped) continue;
|
|
}
|
|
if (err) *err = std::move(lerr);
|
|
rc = -1; break;
|
|
}
|
|
out.prompt_tokens = u.prompt_tokens;
|
|
out.completion_tokens += u.completion_tokens;
|
|
out.total_tokens += u.total_tokens;
|
|
out.context_tokens = u.context_tokens;
|
|
if (u.cached_tokens > 0) out.cached_tokens = u.cached_tokens;
|
|
if (!u.model.empty()) out.model = u.model;
|
|
for (auto &tc : m.calls) {
|
|
tc.arguments = sanitize_arguments(tc.arguments);
|
|
}
|
|
msgs.items.push_back(m);
|
|
if (!cfg.stream) {
|
|
if (!m.reasoning.empty()) {
|
|
cprint("--- reasoning start ---\n", std::array{36});
|
|
cprint(m.reasoning, std::array{2});
|
|
cprint("\n--- reasoning end ---\n", std::array{36});
|
|
}
|
|
if (!m.content.empty()) { render_md_print(m.content); printf("\n"); }
|
|
}
|
|
if (m.calls.empty()) {
|
|
if (!m.reasoning.empty() && trim_is_empty(m.content)) {
|
|
cprint("[auto continue: response was reasoning-only]\n", std::array{33});
|
|
Message cont; cont.role = "user"; cont.content = "continue";
|
|
msgs.items.push_back(cont);
|
|
continue;
|
|
}
|
|
done = 1; break;
|
|
}
|
|
for (size_t tci = 0; tci < m.calls.size(); tci++) {
|
|
if (*interrupted) { rc = -1; break; }
|
|
ToolCall &tc = m.calls[tci];
|
|
const std::string &astr = tc.arguments; /* already filtered by sanitize_arguments */
|
|
if (!m.calls_shown) {
|
|
cprint(sfmt("[tool call: %s(%s)]", tc.name.c_str(), astr.c_str()), std::array{33});
|
|
printf("\n");
|
|
}
|
|
std::string res; int sty = 2;
|
|
bool res_already_filtered = false;
|
|
Json a_mut;
|
|
if (!Json::parse(astr, a_mut) || !a_mut.is_object()) {
|
|
res = sfmt("[tool error: invalid JSON args for %s: parse error. Raw: %s]", tc.name.c_str(), astr.c_str());
|
|
sty = 31;
|
|
} else {
|
|
const Json &a = a_mut;
|
|
std::string fn = tc.name;
|
|
if (fn == "bash" || fn == "shell_exec") {
|
|
std::string cmd = get_str(a, "command");
|
|
std::string workdir = get_str(a, "workdir");
|
|
std::string fwd = filter_text(workdir);
|
|
int to = cfg.shell_timeout;
|
|
if (a.contains("timeout")) { int n = get_int(a, "timeout"); if (n > 0) to = (n > 1000) ? n / 1000 : n; }
|
|
res = shell_with_workdir(cmd, fwd, to, interrupted);
|
|
res_already_filtered = true;
|
|
} else if (fn == "read") {
|
|
std::string p = tool_path_arg(a);
|
|
std::string fp = filter_text(p);
|
|
int off = get_int(a, "offset", 1); if (off < 1) off = 1;
|
|
int lim = get_int(a, "limit", 2000); if (lim <= 0) lim = 2000;
|
|
res = read_file_or_dir(fp, off, lim, &sty);
|
|
} else if (fn == "write" || fn == "write_file") {
|
|
std::string p = tool_path_arg(a);
|
|
std::string fp = filter_text(p);
|
|
std::string cv = get_str(a, "content");
|
|
bool has_content = a.contains("content") && a["content"].is_string();
|
|
bool has_line = a.contains("lineOffset") && !a["lineOffset"].is_null();
|
|
bool has_byte = a.contains("byteOffset") && !a["byteOffset"].is_null();
|
|
bool has_del_lines = a.contains("delLines") && !a["delLines"].is_null();
|
|
bool has_del_bytes = a.contains("delBytes") && !a["delBytes"].is_null();
|
|
long line_off = has_line ? (long)get_int(a, "lineOffset") : -1L;
|
|
long byte_off = has_byte ? (long)get_int(a, "byteOffset") : -1L;
|
|
long del_lines = has_del_lines ? (long)get_int(a, "delLines") : -1L;
|
|
long del_bytes = has_del_bytes ? (long)get_int(a, "delBytes") : -1L;
|
|
if (line_off < 1) line_off = -1L;
|
|
if (byte_off < 0) byte_off = -1L;
|
|
if (del_lines < 0) del_lines = -1L;
|
|
if (del_bytes < 0) del_bytes = -1L;
|
|
if (fp.empty()) { res = "[tool error: write requires 'filePath' parameter]"; sty = 31; }
|
|
else if (!has_content) { res = "[tool error: write requires 'content' parameter]"; sty = 31; }
|
|
else if (!has_line && !has_byte && !has_del_lines && !has_del_bytes) {
|
|
/* No offset/deletion parameters: overwrite the whole file. */
|
|
std::string c = filter_text(cv);
|
|
/* Ensure the written file ends with a single trailing newline. */
|
|
if (!c.empty() && c.back() != '\n') c.push_back('\n');
|
|
auto sl = fp.find_last_of('/');
|
|
if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl));
|
|
else mkdir_p(".");
|
|
if (!write_file_str(fp, c)) { res = sfmt("[tool error: write %s: %s]", fp.c_str(), strerror(errno)); sty = 31; }
|
|
else { res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
|
|
} else {
|
|
int werr = 0; std::string c = filter_text(cv);
|
|
/* Line-based edits keep the text-file trailing-newline convention;
|
|
* byte-based edits are exact byte operations and must not have a
|
|
* newline injected. */
|
|
if (!(has_byte || has_del_bytes) && !c.empty() && c.back() != '\n') c.push_back('\n');
|
|
res = write_file(fp, line_off, byte_off, del_lines, del_bytes, c, &werr);
|
|
if (werr) sty = 31;
|
|
}
|
|
} else {
|
|
res = sfmt("Unknown tool: %s", fn.c_str());
|
|
sty = 31;
|
|
}
|
|
}
|
|
if (!res_already_filtered) res = filter_text(res);
|
|
res = offload_tool_result(res, cfg.max_tool_res, tmps);
|
|
{
|
|
std::string hdr = cstr("[tool result: ", std::array{32});
|
|
std::string fn2 = cstr(tc.name, std::array{32});
|
|
printf("%s%s]\n", hdr.c_str(), fn2.c_str());
|
|
cprint(res, std::array{sty});
|
|
printf("\n\n");
|
|
}
|
|
Message tm; tm.role = "tool"; tm.tool_call_id = tc.id; tm.content = std::move(res);
|
|
msgs.items.push_back(std::move(tm));
|
|
if (*interrupted) { rc = -1; break; }
|
|
}
|
|
if (rc != 0) break;
|
|
}
|
|
if (!done && rc == 0) {
|
|
Message maxm; maxm.role = "assistant";
|
|
maxm.content = sfmt("[max AL iterations (%d) reached]", cfg.max_al_iterations);
|
|
msgs.items.push_back(maxm);
|
|
}
|
|
cleanup_temps(tmps);
|
|
return rc;
|
|
}
|
|
|
|
/* ----------------------------- run_turn ------------------------------- */
|
|
static int run_turn(Cfg &cfg, Messages &msgs, Usage &usage, std::string *err, volatile int *interrupted) {
|
|
*interrupted = 0;
|
|
usage = Usage{};
|
|
int rc = AL(cfg, msgs, usage, interrupted, err);
|
|
if (last_role(msgs) == "tool") {
|
|
Message cont; cont.role = "user"; cont.content = "continue";
|
|
msgs.items.push_back(cont);
|
|
cprint("[auto continue: last message was a tool result]\n", std::array{33});
|
|
rc = AL(cfg, msgs, usage, interrupted, err);
|
|
}
|
|
if (rc != 0) {
|
|
if (*interrupted) { cprint("\n[interrupted]\n", std::array{33}); }
|
|
else { cprint("\n[error: ", std::array{31}); cprint((!err || err->empty()) ? "error" : *err, std::array{31}); cprint("]\n", std::array{31}); }
|
|
}
|
|
return rc;
|
|
}
|
|
|
|
/* ----------------------------- direct shell --------------------------- */
|
|
static void run_direct_shell(std::string_view cmd, int timeout) {
|
|
std::string fcmd = filter_text(cmd);
|
|
cprint(sfmt("[tool call: bash({\"command\":\"%s\"})]", fcmd.c_str()), std::array{33});
|
|
printf("\n");
|
|
std::string res = shell_with_workdir(fcmd, "", timeout, &g_interrupted);
|
|
printf("%s\n", cstr("[tool result: bash]", std::array{32}).c_str());
|
|
printf("%s\n", cstr(res, std::array{2}).c_str());
|
|
}
|
|
|
|
/* ----------------------------- skills --------------------------------- */
|
|
static std::string skill_prompt(std::string_view u, Cfg &cfg) {
|
|
std::string_view rest = u.substr(6);
|
|
while (!rest.empty() && rest.front() == ' ') rest = rest.substr(1);
|
|
if (rest.empty()) return "";
|
|
std::string name(rest);
|
|
std::string prompt;
|
|
auto sp = name.find(' ');
|
|
if (sp != std::string::npos) { prompt = name.substr(sp + 1); name = name.substr(0, sp); }
|
|
std::string sd = skills_dir(cfg);
|
|
std::string path;
|
|
if (!sd.empty()) {
|
|
path = sfmt("%s/%s/SKILL.md", sd.c_str(), name.c_str());
|
|
} else if (name.find("SKILL.md") != std::string::npos) {
|
|
path = name;
|
|
} else {
|
|
path = name + "/SKILL.md";
|
|
}
|
|
struct stat st;
|
|
if (stat(path.c_str(), &st) != 0)
|
|
return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
|
|
if (S_ISDIR(st.st_mode))
|
|
return sfmt("[cannot read skill \"%s\": Is a directory]", name.c_str());
|
|
std::string data;
|
|
if (!read_file_str(path, data))
|
|
return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
|
|
while (!data.empty() && (data.back() == '\n' || data.back() == '\r')) data.pop_back();
|
|
std::string out = data;
|
|
if (!prompt.empty()) { out += "\n\n"; out += prompt; }
|
|
return out;
|
|
}
|
|
static void list_skills(Cfg &cfg) {
|
|
std::string sd = skills_dir(cfg);
|
|
if (sd.empty()) { cprint("No skills directory configured (set BANTAM_SKILLS_DIR or bantam_skills_dir).\n", std::array{33}); return; }
|
|
DIR *d = opendir(sd.c_str());
|
|
if (!d) { cprint(sfmt("[skill error: cannot read skills dir: %s]", strerror(errno)), std::array{31}); return; }
|
|
std::string names; int cnt = 0; struct dirent *de;
|
|
while ((de = readdir(d))) {
|
|
if (std::strcmp(de->d_name, ".") == 0 || std::strcmp(de->d_name, "..") == 0) continue;
|
|
std::string full = sfmt("%s/%s", sd.c_str(), de->d_name);
|
|
struct stat st; if (stat(full.c_str(), &st) != 0 || !S_ISDIR(st.st_mode)) continue;
|
|
std::string sk = sfmt("%s/%s/SKILL.md", sd.c_str(), de->d_name);
|
|
if (access(sk.c_str(), F_OK) == 0) { str_appendf(names, " %s\n", de->d_name); cnt++; }
|
|
}
|
|
closedir(d);
|
|
if (cnt == 0) { cprint("No skills found in ", std::array{33}); cprint(sd, std::array{33}); cprint("\n", std::array{33}); return; }
|
|
cprint("Available skills in ", std::array{1, 36}); cprint(sd, std::array{1, 36}); cprint(":\n", std::array{1, 36});
|
|
fputs(names.c_str(), stdout);
|
|
}
|
|
|
|
/* ----------------------------- compact -------------------------------- */
|
|
static void do_compact(Cfg &cfg, Messages &msgs) {
|
|
if (msgs.items.size() <= 1) { cprint("Nothing to compact yet.\n", std::array{33}); return; }
|
|
cprint("[compacting conversation...]\n", std::array{33});
|
|
std::string sum, e;
|
|
auto nm = compact(cfg, msgs, &sum, &e, &g_interrupted);
|
|
int interrupted = g_interrupted;
|
|
if (!nm) {
|
|
if (interrupted) cprint("\n[interrupted]\n", std::array{33});
|
|
else { cprint(sfmt("\n[compact failed: %s]", e.empty() ? "compact failed" : e.c_str()), std::array{31}); printf("\n"); }
|
|
return;
|
|
}
|
|
msgs = std::move(*nm);
|
|
autosave(msgs);
|
|
printf("%s%d messages]\n", cstr("[compacted to ", std::array{32}).c_str(), (int)msgs.items.size());
|
|
printf("%s", cstr("--- summary ---\n", std::array{33}).c_str());
|
|
printf("%s\n", cstr(sum, std::array{2}).c_str());
|
|
}
|
|
|
|
/* ----------------------------- main ----------------------------------- */
|
|
struct CurlGlobalGuard {
|
|
CurlGlobalGuard() { curl_global_init(CURL_GLOBAL_DEFAULT); }
|
|
~CurlGlobalGuard() { http_cleanup(); curl_global_cleanup(); }
|
|
};
|
|
|
|
int main(int argc, char **argv) {
|
|
CurlGlobalGuard curl_guard;
|
|
setup_signal_handler();
|
|
Cfg cfg = get_cfg(config_path());
|
|
g_col = color_enabled(cfg);
|
|
std::string sp = build_system_prompt(cfg);
|
|
Messages msgs;
|
|
{
|
|
Message sys; sys.role = "system"; sys.content = sp;
|
|
msgs.items.push_back(sys);
|
|
}
|
|
|
|
if (argc > 1 && argv[1] && argv[1][0] != '\0') {
|
|
std::string p = argv[1];
|
|
struct stat st;
|
|
if (stat(p.c_str(), &st) != 0 || S_ISDIR(st.st_mode)) {
|
|
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
|
|
return 1;
|
|
}
|
|
std::string u;
|
|
if (!read_file_str(p, u)) {
|
|
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
|
|
return 1;
|
|
}
|
|
size_t a = 0; while (a < u.size() && (u[a] == ' ' || u[a] == '\n' || u[a] == '\r' || u[a] == '\t')) a++;
|
|
u = u.substr(a);
|
|
if (!u.empty() && u[0] == '!') { run_direct_shell(u.substr(1), cfg.shell_timeout); return 0; }
|
|
cfg.context_window = fetch_context_window(cfg);
|
|
Message um; um.role = "user"; um.content = u;
|
|
msgs.items.push_back(um);
|
|
Usage usg; std::string err;
|
|
run_turn(cfg, msgs, usg, &err, &g_interrupted);
|
|
if (!err.empty()) return 1;
|
|
cprint(format_usage(usg, cfg.context_window), std::array{2}); printf("\n");
|
|
autosave(msgs);
|
|
return 0;
|
|
}
|
|
|
|
load_history();
|
|
auto sess = find_project_session();
|
|
if (sess && sess->messages.items.size() > 1) {
|
|
cprint("[session found for this directory]\n", std::array{33});
|
|
cprint(" ", std::array{33}); cprint(sess->id, std::array{32}); printf("\n");
|
|
cprint(" ", std::array{33}); cprint(sess->summary, std::array{2}); printf("\n");
|
|
std::string created = sfmt(" created %s [%d messages]\n", sess->created.c_str(), (int)sess->messages.items.size());
|
|
cprint(created, std::array{2});
|
|
printf(" Resume this session? [Y/n]: "); fflush(stdout);
|
|
bool ok; std::string ans = read_plain("", ok);
|
|
if (ok) {
|
|
std::string a2 = ans;
|
|
size_t ai = 0; while (ai < a2.size() && (a2[ai] == ' ' || a2[ai] == '\n' || a2[ai] == '\r' || a2[ai] == '\t')) ai++;
|
|
a2 = a2.substr(ai);
|
|
while (!a2.empty() && (a2.back() == '\n' || a2.back() == '\r' || a2.back() == ' ')) a2.pop_back();
|
|
if (a2.empty() || a2[0] == 'y' || a2[0] == 'Y') {
|
|
msgs = std::move(sess->messages);
|
|
autosave(msgs);
|
|
printf("%s%s]\n", cstr("[session resumed: ", std::array{32}).c_str(), sess->id.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::string *re_ptr = cfg_raw_get(cfg, "reasoning_effort");
|
|
std::string re = (re_ptr && !re_ptr->empty()) ? *re_ptr : "high";
|
|
{
|
|
cprint("Bantam Agent ready", std::array{1, 32}); printf(" ");
|
|
cprint(" (Enter submits; Ctrl+J or paste = new line)\n", std::array{2});
|
|
fflush(stdout);
|
|
cfg.context_window = fetch_context_window(cfg);
|
|
std::string info = sfmt("endpoint: %s model: %s temp: %g reasoning: %s context: %d\n",
|
|
cfg.endpoint.c_str(), cfg.model.c_str(), cfg.temperature, re.c_str(), cfg.context_window);
|
|
cprint(info, std::array{2});
|
|
}
|
|
|
|
for (;;) {
|
|
g_interrupted = 0;
|
|
bool ok;
|
|
std::string u = read_line("> ", ok);
|
|
if (!ok) { printf("\n"); break; }
|
|
std::string ut = u;
|
|
size_t a = 0; while (a < ut.size() && (ut[a] == ' ' || ut[a] == '\n' || ut[a] == '\r' || ut[a] == '\t')) a++;
|
|
ut = ut.substr(a);
|
|
if (ut.empty()) continue;
|
|
add_history(u);
|
|
/* command aliases */
|
|
for (int ci = 0; ci < 2; ci++) {
|
|
const char *cmd = (ci == 0) ? "model" : "endpoint";
|
|
std::string slash = sfmt("/%s", cmd);
|
|
if (u == slash) {
|
|
u = sfmt("/cfg %s", cmd);
|
|
} else if (u.size() > slash.size() && u.rfind(slash, 0) == 0 && u[slash.size()] == ' ') {
|
|
u = sfmt("/cfg %s %s", cmd, u.c_str() + slash.size() + 1);
|
|
}
|
|
}
|
|
if (u == "/quit") break;
|
|
if (u == "/clear") {
|
|
msgs = Messages{};
|
|
Message s2; s2.role = "system"; s2.content = build_system_prompt(cfg);
|
|
msgs.items.push_back(s2);
|
|
autosave(msgs); continue;
|
|
}
|
|
if (u == "/save" || u.starts_with("/save ")) {
|
|
std::string name = u.substr(5);
|
|
size_t ni = 0; while (ni < name.size() && name[ni] == ' ') ni++;
|
|
name = name.substr(ni);
|
|
std::string sid, sm;
|
|
save_session(msgs, name, &sid, &sm);
|
|
printf("%s%s]\n", cstr("[session saved: ", std::array{32}).c_str(), sid.c_str());
|
|
cprint(" ", std::array{32}); cprint(sm, std::array{2}); printf("\n");
|
|
continue;
|
|
}
|
|
if (u == "/continue" || u == "/cont") {
|
|
std::string pid = project_id();
|
|
std::string lerr;
|
|
auto lm = load_session(pid, &lerr);
|
|
if (!lm) { cprint("No session found for current project: ", std::array{31}); cprint(lerr, std::array{31}); printf("\n"); continue; }
|
|
msgs = std::move(*lm);
|
|
autosave(msgs);
|
|
printf("%s%s]\n", cstr("[session continued: ", std::array{32}).c_str(), pid.c_str());
|
|
std::string sm = summary_of(msgs);
|
|
cprint(" ", std::array{32}); cprint(sm, std::array{32}); printf("\n");
|
|
continue;
|
|
}
|
|
if (u == "/list") {
|
|
auto ss = sessions_load();
|
|
if (ss.empty()) { cprint("No sessions saved yet.\n", std::array{33}); continue; }
|
|
std::string pid = project_id();
|
|
for (auto &s : ss) {
|
|
printf("%s", cstr(s.id, std::array{32}).c_str());
|
|
if (s.id == pid) cprint(" (current project)", std::array{33});
|
|
std::string meta = sfmt(" %s [%d msgs]\n", s.created.c_str(), (int)s.messages.items.size());
|
|
cprint(meta, std::array{2});
|
|
cprint(" ", std::array{32}); cprint(s.summary, std::array{2}); printf("\n");
|
|
}
|
|
continue;
|
|
}
|
|
if (u == "/load" || u.starts_with("/load ")) {
|
|
std::vector<std::string> parts;
|
|
{
|
|
std::string_view sv(u);
|
|
size_t i = 0;
|
|
while (i < sv.size()) {
|
|
while (i < sv.size() && sv[i] == ' ') i++;
|
|
if (i >= sv.size()) break;
|
|
size_t j = i;
|
|
while (j < sv.size() && sv[j] != ' ') j++;
|
|
parts.push_back(std::string(sv.substr(i, j - i)));
|
|
i = j;
|
|
}
|
|
}
|
|
if (parts.size() < 2) { cprint("Usage: /load <session-id>\n", std::array{31}); continue; }
|
|
std::string lerr;
|
|
auto lm = load_session(parts[1], &lerr);
|
|
if (!lm) { cprint("Session not found: ", std::array{31}); cprint(lerr, std::array{31}); printf("\n"); continue; }
|
|
msgs = std::move(*lm);
|
|
autosave(msgs);
|
|
printf("%s%s]\n", cstr("[session loaded: ", std::array{32}).c_str(), parts[1].c_str());
|
|
std::string sm = summary_of(msgs);
|
|
cprint(" ", std::array{32}); cprint(sm, std::array{32}); printf("\n");
|
|
continue;
|
|
}
|
|
if (u == "/compact") { do_compact(cfg, msgs); continue; }
|
|
if (u == "/cfg" || u.starts_with("/cfg ")) {
|
|
/* tokens after "/cfg " */
|
|
std::string rest = u.substr(4);
|
|
while (!rest.empty() && rest.front() == ' ') rest = rest.substr(1);
|
|
std::string sub, key, val;
|
|
{
|
|
size_t i = 0;
|
|
while (i < rest.size() && rest[i] != ' ') i++;
|
|
sub = rest.substr(0, i);
|
|
size_t j = i;
|
|
while (j < rest.size() && rest[j] == ' ') j++;
|
|
size_t ks = j;
|
|
while (j < rest.size() && rest[j] != ' ') j++;
|
|
key = rest.substr(ks, j - ks);
|
|
size_t vs = j;
|
|
while (vs < rest.size() && rest[vs] == ' ') vs++;
|
|
val = rest.substr(vs);
|
|
}
|
|
if (sub.empty()) { cprint("Usage: /cfg <key> [value]\n", std::array{31}); continue; }
|
|
if (sub == "get") {
|
|
if (key.empty()) { cprint("Usage: /cfg get <key>\n", std::array{31}); continue; }
|
|
const std::string *v = cfg_raw_get(cfg, key);
|
|
if (v && !v->empty()) printf("%s=%s\n", cstr(key, std::array{32}).c_str(), v->c_str());
|
|
else printf("%s not set\n", cstr(key, std::array{31}).c_str());
|
|
} else if (sub == "set") {
|
|
if (key.empty()) { cprint("Usage: /cfg set <key> <value>\n", std::array{31}); continue; }
|
|
if (set_cfg(config_path(), key, val) != 0) { cprint("[cfg error]\n", std::array{31}); continue; }
|
|
cfg = get_cfg(config_path());
|
|
if (key == "model" || key == "endpoint" || key == "api_key")
|
|
cfg.context_window = fetch_context_window(cfg);
|
|
g_col = color_enabled(cfg);
|
|
printf("%s%s=%s]\n", cstr("[config updated: ", std::array{32}).c_str(), key.c_str(), val.c_str());
|
|
} else if (key.empty()) {
|
|
/* single token: treat as get */
|
|
const std::string *v = cfg_raw_get(cfg, sub);
|
|
if (v && !v->empty()) printf("%s=%s\n", cstr(sub, std::array{32}).c_str(), v->c_str());
|
|
else printf("%s not set\n", cstr(sub, std::array{31}).c_str());
|
|
} else {
|
|
/* legacy "/cfg key value": treat as set */
|
|
if (set_cfg(config_path(), sub, key) != 0) { cprint("[cfg error]\n", std::array{31}); continue; }
|
|
cfg = get_cfg(config_path());
|
|
if (sub == "model" || sub == "endpoint" || sub == "api_key")
|
|
cfg.context_window = fetch_context_window(cfg);
|
|
g_col = color_enabled(cfg);
|
|
printf("%s%s=%s]\n", cstr("[config updated: ", std::array{32}).c_str(), sub.c_str(), key.c_str());
|
|
}
|
|
continue;
|
|
}
|
|
if (u == "/skill" || u.starts_with("/skill ")) {
|
|
std::string rest = u.substr(6);
|
|
while (!rest.empty() && rest.front() == ' ') rest = rest.substr(1);
|
|
if (rest.empty()) { list_skills(cfg); continue; }
|
|
std::string composed = skill_prompt(u, cfg);
|
|
if (composed.empty()) { cprint("/skill usage: /skill <name> [prompt]\n", std::array{31}); continue; }
|
|
if (composed.starts_with("[cannot read skill")) {
|
|
cprint(composed, std::array{31}); printf("\n"); continue;
|
|
}
|
|
u = composed;
|
|
/* fall through to a normal LLM turn */
|
|
} else if (u == "/models") {
|
|
int e2; std::string ml = list_models(cfg, &e2);
|
|
if (e2 != 0) { cprint("[error querying /models: ", std::array{31}); cprint(ml, std::array{31}); printf("]\n"); continue; }
|
|
cprint("Supported models at ", std::array{1, 36}); cprint(cfg.endpoint, std::array{1, 36}); cprint(":\n", std::array{1, 36});
|
|
render_md_print(ml);
|
|
continue;
|
|
} else if (!u.empty() && u[0] == '!') {
|
|
run_direct_shell(u.substr(1), cfg.shell_timeout); continue;
|
|
} else if (u == "/help") {
|
|
cprint("Bantam commands:\n", std::array{1, 36});
|
|
const char *cmds[][2] = {
|
|
{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save [name]", "save session (default: project MD5)"},
|
|
{"/continue", "continue project session (alias: /cont)"}, {"/list", "list sessions"}, {"/load <id>", "load session"},
|
|
{"/compact", "compact context"}, {"/cfg <k> [v]", "get/set config"}, {"/model [m]", "alias for /cfg model"},
|
|
{"/endpoint [e]", "alias for /cfg endpoint"}, {"!<cmd>", "run shell command directly"}, {"/models", "list models at endpoint"},
|
|
{"/skill <n> [p]", "load SKILL.md and run as prompt"}, {"/help", "show help"}};
|
|
for (int i = 0; i < 14; i++) {
|
|
std::string label = sfmt(" %-18s", cmds[i][0]);
|
|
cprint(label, std::array{1, 32});
|
|
cprint(cmds[i][1], std::array{1, 32});
|
|
printf("\n");
|
|
}
|
|
continue;
|
|
}
|
|
/* normal LLM turn */
|
|
{
|
|
Message um; um.role = "user"; um.content = u;
|
|
msgs.items.push_back(um);
|
|
Usage usg; std::string err;
|
|
run_turn(cfg, msgs, usg, &err, &g_interrupted);
|
|
if (!err.empty()) continue;
|
|
cprint(format_usage(usg, cfg.context_window), std::array{2}); printf("\n");
|
|
double pct = context_pct(usg, cfg.context_window);
|
|
if (pct >= 60.0 && msgs.items.size() > 1) {
|
|
std::string q = sfmt("Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: ",
|
|
pct, ctx_tokens(usg), cfg.context_window);
|
|
cprint(q, std::array{33});
|
|
bool ok2; std::string ans = read_plain("", ok2);
|
|
if (ok2) {
|
|
std::string a2 = ans;
|
|
size_t ai = 0; while (ai < a2.size() && (a2[ai] == ' ' || a2[ai] == '\n' || a2[ai] == '\r' || a2[ai] == '\t')) ai++;
|
|
a2 = a2.substr(ai);
|
|
while (!a2.empty() && (a2.back() == '\n' || a2.back() == '\r' || a2.back() == ' ')) a2.pop_back();
|
|
if (a2.empty() || a2[0] == 'y' || a2[0] == 'Y') do_compact(cfg, msgs);
|
|
}
|
|
}
|
|
autosave(msgs);
|
|
}
|
|
}
|
|
autosave(msgs);
|
|
save_history();
|
|
return 0;
|
|
}
|