Optimize startup, HTTP/SSE streaming, and fix macOS libedit bindings
This commit is contained in:
@@ -12,9 +12,9 @@
|
||||
|
||||
CXX = c++
|
||||
STRIP = strip
|
||||
CXXFLAGS = -std=c++20 -s -Wall -Wextra -Wpedantic -O2 -march=native -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
|
||||
CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_XOPEN_SOURCE=700
|
||||
LDFLAGS = -s
|
||||
CXXFLAGS = -std=c++20 -Wall -Wextra -Wpedantic -O2 -march=native -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
|
||||
CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_XOPEN_SOURCE=700 -D_DARWIN_C_SOURCE
|
||||
LDFLAGS =
|
||||
LDLIBS = -L/usr/local/lib -lcurl -lreadline
|
||||
|
||||
# Location of the AddressSanitizer/UBSan runtime libraries plus a replacement
|
||||
|
||||
+37
-4
@@ -138,6 +138,7 @@ struct Cfg {
|
||||
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 */
|
||||
};
|
||||
|
||||
@@ -180,17 +181,48 @@ inline std::string cstr(std::string_view text, const R& codes) {
|
||||
s += "\033[";
|
||||
for (size_t i = 0; i < codes.size(); i++) {
|
||||
if (i) s += ';';
|
||||
s += sfmt("%d", (int)codes[i]);
|
||||
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 += 'm';
|
||||
s += text;
|
||||
s += "\033[0m";
|
||||
return s;
|
||||
}
|
||||
template<class R>
|
||||
inline void cprint(std::string_view text, const R& codes) {
|
||||
std::string w = cstr(text, codes);
|
||||
std::fputs(w.c_str(), stdout);
|
||||
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);
|
||||
@@ -226,6 +258,7 @@ 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,
|
||||
|
||||
+6
-4
@@ -87,7 +87,7 @@ static void parse_cfg_file(std::string_view path, Cfg &c, std::set<std::string>
|
||||
else if (k == "max_al_iterations") c.max_al_iterations = to_int_val(v, c.max_al_iterations);
|
||||
else if (k == "stream") c.stream = (v == "true" || v == "1" || v == "yes");
|
||||
else if (k == "color") c.color = std::string(v);
|
||||
else if (k == "context_window") c.context_window = to_int_val(v, c.context_window);
|
||||
else if (k == "context_window") { c.context_window = to_int_val(v, c.context_window); c.context_window_set = true; }
|
||||
else if (k == "max_tool_res") c.max_tool_res = to_int_val(v, c.max_tool_res);
|
||||
}
|
||||
}
|
||||
@@ -111,7 +111,7 @@ static void apply_env_cfg(Cfg &c, const std::set<std::string> &file_keys) {
|
||||
if ((e = ev("timeout", "BANTAM_TIMEOUT"))) { c.timeout = to_int_val(e, c.timeout); cfg_raw_set(c, "timeout", e); }
|
||||
if ((e = ev("shell_timeout", "BANTAM_SHELL_TIMEOUT"))) { c.shell_timeout = to_int_val(e, c.shell_timeout); cfg_raw_set(c, "shell_timeout", e); }
|
||||
if ((e = ev("max_al_iterations", "BANTAM_MAX_AL_ITERATIONS"))) { c.max_al_iterations = to_int_val(e, c.max_al_iterations); cfg_raw_set(c, "max_al_iterations", e); }
|
||||
if ((e = ev("context_window", "BANTAM_CONTEXT_WINDOW"))) { c.context_window = to_int_val(e, c.context_window); cfg_raw_set(c, "context_window", e); }
|
||||
if ((e = ev("context_window", "BANTAM_CONTEXT_WINDOW"))) { c.context_window = to_int_val(e, c.context_window); c.context_window_set = true; cfg_raw_set(c, "context_window", e); }
|
||||
if ((e = ev("max_tool_res", "BANTAM_MAX_TOOL_RES"))) { c.max_tool_res = to_int_val(e, c.max_tool_res); cfg_raw_set(c, "max_tool_res", e); }
|
||||
if ((e = ev("reasoning_effort", "BANTAM_REASONING_EFFORT"))) cfg_raw_set(c, "reasoning_effort", e);
|
||||
if ((e = ev("bantam_tools_dir", "BANTAM_TOOLS_DIR"))) cfg_raw_set(c, "bantam_tools_dir", e);
|
||||
@@ -211,6 +211,7 @@ static int query_models_context_window(Cfg &cfg) {
|
||||
}
|
||||
|
||||
int fetch_context_window(Cfg &cfg) {
|
||||
if (cfg.context_window_set && cfg.context_window > 0) return cfg.context_window;
|
||||
static std::map<std::string, int> cache;
|
||||
std::string key = cfg.endpoint + std::string(1, '\0') + cfg.model;
|
||||
auto it = cache.find(key);
|
||||
@@ -218,8 +219,9 @@ int fetch_context_window(Cfg &cfg) {
|
||||
int cw = query_models_context_window(cfg);
|
||||
if (cw > 0) { cache[key] = cw; return cw; }
|
||||
const std::string *v = cfg_raw_get(cfg, "context_window");
|
||||
if (v && !v->empty()) return to_int_val(*v, 262144);
|
||||
return 262144;
|
||||
int fallback = (v && !v->empty()) ? to_int_val(*v, 262144) : 262144;
|
||||
cache[key] = fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
std::string list_models(Cfg &cfg, int *errout) {
|
||||
|
||||
+34
-14
@@ -55,13 +55,41 @@ std::string read_plain(std::string_view prompt, bool &ok) {
|
||||
}
|
||||
|
||||
/* ----------------------------- readline editor ------------------------ */
|
||||
static void ensure_readline_init() {
|
||||
static bool initialized = false;
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
rl_initialize();
|
||||
#if !defined(__APPLE__) && defined(RL_READLINE_VERSION) && (RL_READLINE_VERSION >= 0x0500)
|
||||
/* On GNU Readline (Linux, FreeBSD), terminal raw mode clears ICRNL so
|
||||
* Enter sends '\r' (rl_newline = submit) while Ctrl+J sends '\n'
|
||||
* (rl_insert = literal newline). On macOS / BSD libedit (EditLine),
|
||||
* ICRNL remains enabled in termios (translating Enter '\r' into '\n') and
|
||||
* '\n' is the sole accept-line key (ed-newline), so rebinding '\n' to
|
||||
* rl_insert prevents submitting any prompt. */
|
||||
if (!rl_library_version || std::strncmp(rl_library_version, "EditLine", 8) != 0) {
|
||||
rl_bind_key('\n', rl_insert);
|
||||
rl_bind_key('\r', rl_newline);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string read_line(std::string_view prompt, bool &ok) {
|
||||
if (!is_terminal(STDIN_FILENO)) return read_plain(prompt, ok);
|
||||
/* Enter (\r) submits the line; Ctrl+J inserts a literal newline so a
|
||||
* multi-line prompt can be composed before submitting. */
|
||||
rl_bind_key('\n', rl_insert);
|
||||
ensure_readline_init();
|
||||
char *line = readline(prompt.empty() ? "" : std::string(prompt).c_str());
|
||||
if (!line) { ok = false; return ""; }
|
||||
setup_signal_handler();
|
||||
if (!line) {
|
||||
if (g_interrupted) {
|
||||
g_interrupted = 0;
|
||||
std::fputc('\n', stdout);
|
||||
std::fflush(stdout);
|
||||
ok = true;
|
||||
return "";
|
||||
}
|
||||
ok = false;
|
||||
return "";
|
||||
}
|
||||
std::string r(line);
|
||||
free(line);
|
||||
ok = true;
|
||||
@@ -74,19 +102,11 @@ int is_terminal(int fd) { return isatty(fd) != 0; }
|
||||
int term_width() {
|
||||
#ifdef TIOCGWINSZ
|
||||
struct winsize ws{};
|
||||
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col;
|
||||
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col;
|
||||
if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col;
|
||||
if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col;
|
||||
#endif
|
||||
const char *c = std::getenv("COLUMNS");
|
||||
if (c && *c) { int n = std::atoi(c); if (n > 0) return n; }
|
||||
FILE *p = popen("tput cols 2>/dev/null", "r");
|
||||
if (p) {
|
||||
char buf[32];
|
||||
if (fgets(buf, sizeof buf, p)) {
|
||||
int n = std::atoi(buf);
|
||||
pclose(p);
|
||||
if (n > 0) return n;
|
||||
} else pclose(p);
|
||||
}
|
||||
return 80;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* and non-streaming response parsing. */
|
||||
#include "bantam.hpp"
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
@@ -118,6 +119,19 @@ static void dump_str(std::string &out, std::string_view s) {
|
||||
const unsigned char *p = (const unsigned char *)s.data();
|
||||
size_t avail = s.size();
|
||||
while (avail > 0) {
|
||||
/* Fast path: scan contiguous unescaped printable ASCII. */
|
||||
size_t run = 0;
|
||||
while (run < avail) {
|
||||
unsigned char b = p[run];
|
||||
if (b >= 0x20 && b < 0x80 && b != '"' && b != '\\') run++;
|
||||
else break;
|
||||
}
|
||||
if (run > 0) {
|
||||
out.append((const char *)p, run);
|
||||
p += run;
|
||||
avail -= run;
|
||||
if (avail == 0) break;
|
||||
}
|
||||
unsigned char c = p[0];
|
||||
if (c < 0x80) {
|
||||
p++;
|
||||
@@ -130,10 +144,12 @@ static void dump_str(std::string &out, std::string_view s) {
|
||||
case '\n': out += "\\n"; break;
|
||||
case '\r': out += "\\r"; break;
|
||||
case '\t': out += "\\t"; break;
|
||||
default:
|
||||
if (c < 0x20) str_appendf(out, "\\u%04x", (unsigned)c);
|
||||
else out += (char)c;
|
||||
default: {
|
||||
char ubuf[7];
|
||||
std::snprintf(ubuf, sizeof ubuf, "\\u%04x", (unsigned)c);
|
||||
out.append(ubuf, 6);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
size_t pl = 0;
|
||||
@@ -160,12 +176,22 @@ static void dump_rec(std::string &out, const Json &j, int indent, int depth) {
|
||||
case JType::Bool:
|
||||
out += (j.bval ? "true" : "false");
|
||||
break;
|
||||
case JType::Int:
|
||||
str_appendf(out, "%lld", (long long)j.ival);
|
||||
case JType::Int: {
|
||||
char ibuf[32];
|
||||
auto [ptr, ec] = std::to_chars(ibuf, ibuf + sizeof ibuf, j.ival);
|
||||
if (ec == std::errc()) out.append(ibuf, (size_t)(ptr - ibuf));
|
||||
else str_appendf(out, "%lld", (long long)j.ival);
|
||||
break;
|
||||
}
|
||||
case JType::Float:
|
||||
if (!std::isfinite(j.dval)) out += "null";
|
||||
else str_appendf(out, "%.15g", j.dval);
|
||||
if (!std::isfinite(j.dval)) {
|
||||
out += "null";
|
||||
} else {
|
||||
char fbuf[32];
|
||||
int n = std::snprintf(fbuf, sizeof fbuf, "%.15g", j.dval);
|
||||
if (n > 0 && (size_t)n < sizeof fbuf) out.append(fbuf, (size_t)n);
|
||||
else str_appendf(out, "%.15g", j.dval);
|
||||
}
|
||||
break;
|
||||
case JType::Str:
|
||||
dump_str(out, j.sval);
|
||||
@@ -217,6 +243,7 @@ static void dump_rec(std::string &out, const Json &j, int indent, int depth) {
|
||||
|
||||
std::string Json::dump(int indent) const {
|
||||
std::string out;
|
||||
out.reserve(256);
|
||||
dump_rec(out, *this, indent, 0);
|
||||
return out;
|
||||
}
|
||||
@@ -278,16 +305,25 @@ struct JParser {
|
||||
if (pos >= src.size() || src[pos] != '"') return false;
|
||||
pos++;
|
||||
out.clear();
|
||||
while (pos < src.size()) {
|
||||
unsigned char c = (unsigned char)src[pos++];
|
||||
const char *base = src.data();
|
||||
size_t n = src.size();
|
||||
while (pos < n) {
|
||||
/* Fast path: scan contiguous unescaped bytes >= 0x20. */
|
||||
size_t run_start = pos;
|
||||
while (pos < n) {
|
||||
unsigned char b = (unsigned char)base[pos];
|
||||
if (b == '"' || b == '\\' || b < 0x20) break;
|
||||
pos++;
|
||||
}
|
||||
if (pos > run_start) {
|
||||
out.append(base + run_start, pos - run_start);
|
||||
}
|
||||
if (pos >= n) return false;
|
||||
unsigned char c = (unsigned char)base[pos++];
|
||||
if (c == '"') return true;
|
||||
if (c < 0x20) return false;
|
||||
if (c != '\\') {
|
||||
out += (char)c;
|
||||
continue;
|
||||
}
|
||||
if (pos >= src.size()) return false;
|
||||
char esc = src[pos++];
|
||||
if (pos >= n) return false;
|
||||
char esc = base[pos++];
|
||||
switch (esc) {
|
||||
case '"': out += '"'; break;
|
||||
case '\\': out += '\\'; break;
|
||||
@@ -301,7 +337,7 @@ struct JParser {
|
||||
uint32_t cp = 0;
|
||||
if (!parse_hex4(cp)) return false;
|
||||
if (cp >= 0xD800 && cp <= 0xDBFF) {
|
||||
if (pos + 6 <= src.size() && src[pos] == '\\' && src[pos + 1] == 'u') {
|
||||
if (pos + 6 <= n && base[pos] == '\\' && base[pos + 1] == 'u') {
|
||||
size_t saved = pos;
|
||||
pos += 2;
|
||||
uint32_t low = 0;
|
||||
@@ -353,20 +389,32 @@ struct JParser {
|
||||
if (pos >= src.size() || src[pos] < '0' || src[pos] > '9') return false;
|
||||
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') pos++;
|
||||
}
|
||||
std::string num_str(src.substr(start, pos - start));
|
||||
const char *first = src.data() + start;
|
||||
const char *last = src.data() + pos;
|
||||
if (!is_float) {
|
||||
errno = 0;
|
||||
char *end = nullptr;
|
||||
long long iv = std::strtoll(num_str.c_str(), &end, 10);
|
||||
if (errno == 0 && end == num_str.c_str() + num_str.size()) {
|
||||
out = Json((int64_t)iv);
|
||||
int64_t iv = 0;
|
||||
auto [ptr, ec] = std::from_chars(first, last, iv);
|
||||
if (ec == std::errc() && ptr == last) {
|
||||
out = Json(iv);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
char stack_num[64];
|
||||
std::string heap_num;
|
||||
const char *cstr = nullptr;
|
||||
size_t nlen = pos - start;
|
||||
if (nlen < sizeof stack_num) {
|
||||
std::memcpy(stack_num, first, nlen);
|
||||
stack_num[nlen] = '\0';
|
||||
cstr = stack_num;
|
||||
} else {
|
||||
heap_num.assign(first, nlen);
|
||||
cstr = heap_num.c_str();
|
||||
}
|
||||
errno = 0;
|
||||
char *end = nullptr;
|
||||
double dv = std::strtod(num_str.c_str(), &end);
|
||||
if (end != num_str.c_str() + num_str.size() || !std::isfinite(dv)) return false;
|
||||
double dv = std::strtod(cstr, &end);
|
||||
if (end != cstr + nlen || !std::isfinite(dv)) return false;
|
||||
out = Json(dv);
|
||||
return true;
|
||||
}
|
||||
@@ -554,6 +602,7 @@ std::string build_request_json(const Cfg &cfg, const Messages &msgs,
|
||||
root["model"] = cfg.model;
|
||||
root["temperature"] = cfg.temperature;
|
||||
Json arr = Json::array();
|
||||
arr.aval.reserve(msgs.items.size());
|
||||
for (auto &m : msgs.items) {
|
||||
Json mo;
|
||||
serialize_message_json(mo, m);
|
||||
@@ -562,10 +611,12 @@ std::string build_request_json(const Cfg &cfg, const Messages &msgs,
|
||||
root["messages"] = std::move(arr);
|
||||
root["stream"] = stream;
|
||||
if (use_tools) {
|
||||
Json tools;
|
||||
if (Json::parse(TOOLS_JSON, tools)) {
|
||||
root["tools"] = std::move(tools);
|
||||
}
|
||||
static const Json kTools = []() {
|
||||
Json t;
|
||||
Json::parse(TOOLS_JSON, t);
|
||||
return t;
|
||||
}();
|
||||
root["tools"] = kTools;
|
||||
}
|
||||
if (stream) {
|
||||
Json so = Json::object();
|
||||
|
||||
@@ -18,15 +18,29 @@ static size_t mem_cb(char *ptr, size_t sz, size_t nm, void *ud) {
|
||||
return total;
|
||||
}
|
||||
|
||||
/* ----------------------------- curl RAII ------------------------------ */
|
||||
struct CurlHandle {
|
||||
CURL *c;
|
||||
CurlHandle() : c(curl_easy_init()) {}
|
||||
~CurlHandle() { if (c) curl_easy_cleanup(c); }
|
||||
CurlHandle(const CurlHandle &) = delete;
|
||||
CurlHandle &operator=(const CurlHandle &) = delete;
|
||||
explicit operator bool() const { return c != nullptr; }
|
||||
};
|
||||
static int xferinfo_cb(void *clientp, curl_off_t, curl_off_t, curl_off_t, curl_off_t) {
|
||||
volatile int *interrupted = (volatile int *)clientp;
|
||||
return (interrupted && *interrupted) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ----------------------------- curl handle & headers ------------------ */
|
||||
static CURL *g_shared_curl = nullptr;
|
||||
|
||||
static CURL *get_shared_curl() {
|
||||
if (!g_shared_curl) {
|
||||
g_shared_curl = curl_easy_init();
|
||||
} else {
|
||||
curl_easy_reset(g_shared_curl);
|
||||
}
|
||||
return g_shared_curl;
|
||||
}
|
||||
|
||||
void http_cleanup() {
|
||||
if (g_shared_curl) {
|
||||
curl_easy_cleanup(g_shared_curl);
|
||||
g_shared_curl = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
struct CurlHeaders {
|
||||
curl_slist *list = nullptr;
|
||||
@@ -53,8 +67,23 @@ static void set_proxy(CURL *c) {
|
||||
}
|
||||
}
|
||||
|
||||
static void apply_common_curl_opts(CURL *c, volatile int *interrupted) {
|
||||
curl_easy_setopt(c, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_NOPROGRESS, 0L);
|
||||
curl_easy_setopt(c, CURLOPT_XFERINFOFUNCTION, xferinfo_cb);
|
||||
curl_easy_setopt(c, CURLOPT_XFERINFODATA, (void *)interrupted);
|
||||
curl_easy_setopt(c, CURLOPT_TCP_KEEPALIVE, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_TCP_KEEPIDLE, 30L);
|
||||
curl_easy_setopt(c, CURLOPT_TCP_KEEPINTVL, 15L);
|
||||
curl_easy_setopt(c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp");
|
||||
set_proxy(c);
|
||||
}
|
||||
|
||||
static void apply_llm_headers(CurlHeaders &h, const Cfg &cfg) {
|
||||
h.append("Content-Type: application/json");
|
||||
/* Suppress Expect: 100-continue so large POST bodies are sent immediately
|
||||
* without waiting for a 100 Continue round-trip or hanging behind proxies. */
|
||||
h.append("Expect:");
|
||||
if (!cfg.api_key.empty() && cfg.api_key != "-") {
|
||||
h.append(sfmt("Authorization: Bearer %s", cfg.api_key.c_str()));
|
||||
}
|
||||
@@ -63,21 +92,32 @@ static void apply_llm_headers(CurlHeaders &h, const Cfg &cfg) {
|
||||
/* ----------------------------- http_get ------------------------------- */
|
||||
int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code,
|
||||
std::string &errmsg) {
|
||||
CurlHandle curl;
|
||||
if (!curl) { errmsg = "curl init failed"; return -1; }
|
||||
CURL *c = get_shared_curl();
|
||||
if (!c) { errmsg = "curl init failed"; return -1; }
|
||||
std::string url_s(url);
|
||||
curl_easy_setopt(curl.c, CURLOPT_URL, url_s.c_str());
|
||||
curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, mem_cb);
|
||||
curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &body);
|
||||
set_proxy(curl.c);
|
||||
long t = cfg.timeout > 0 ? cfg.timeout : 30;
|
||||
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, t);
|
||||
curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, t);
|
||||
curl_easy_setopt(curl.c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp");
|
||||
CURLcode res = curl_easy_perform(curl.c);
|
||||
curl_easy_getinfo(curl.c, CURLINFO_RESPONSE_CODE, &code);
|
||||
CurlHeaders headers;
|
||||
if (!cfg.api_key.empty() && cfg.api_key != "-") {
|
||||
headers.append(sfmt("Authorization: Bearer %s", cfg.api_key.c_str()));
|
||||
}
|
||||
apply_common_curl_opts(c, &g_interrupted);
|
||||
curl_easy_setopt(c, CURLOPT_URL, url_s.c_str());
|
||||
curl_easy_setopt(c, CURLOPT_HTTPGET, 1L);
|
||||
if (headers.list) curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers.list);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, mem_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &body);
|
||||
long cfg_t = cfg.timeout > 0 ? cfg.timeout : 15;
|
||||
long conn_t = (cfg_t < 10) ? cfg_t : 10;
|
||||
long tot_t = (cfg_t < 15) ? cfg_t : 15;
|
||||
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, conn_t);
|
||||
curl_easy_setopt(c, CURLOPT_TIMEOUT, tot_t);
|
||||
curl_easy_setopt(c, CURLOPT_LOW_SPEED_LIMIT, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_LOW_SPEED_TIME, tot_t);
|
||||
CURLcode res = curl_easy_perform(c);
|
||||
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code);
|
||||
if (res != CURLE_OK) {
|
||||
errmsg = curl_easy_strerror(res);
|
||||
errmsg = (res == CURLE_ABORTED_BY_CALLBACK || g_interrupted)
|
||||
? "interrupted"
|
||||
: curl_easy_strerror(res);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -113,11 +153,14 @@ static int est_tokens_msg(const Message &m) {
|
||||
|
||||
/* ----------------------------- streaming parser ----------------------- */
|
||||
struct StreamCtx {
|
||||
std::string content, reas, line_buf, live_printed, buf, raw;
|
||||
std::string content, reas, line_buf, buf, raw;
|
||||
std::vector<std::string> tbl;
|
||||
Usage last_usage;
|
||||
std::vector<ToolCall> tcs;
|
||||
std::vector<std::string> tcs_filt_args;
|
||||
std::vector<size_t> tcs_filt_pos;
|
||||
std::vector<int> tcs_idx;
|
||||
size_t live_printed_len = 0;
|
||||
int tool_calls_shown = 0;
|
||||
int tty = 0;
|
||||
int live_idx = -1;
|
||||
@@ -128,6 +171,32 @@ struct StreamCtx {
|
||||
volatile int *interrupted = nullptr;
|
||||
};
|
||||
|
||||
static void advance_filtered_args(StreamCtx *sc, int slot, bool flush_all) {
|
||||
const std::string &raw_args = sc->tcs[slot].arguments;
|
||||
size_t pos = sc->tcs_filt_pos[slot];
|
||||
if (pos >= raw_args.size()) return;
|
||||
std::string_view rem = std::string_view(raw_args).substr(pos);
|
||||
if (!flush_all) {
|
||||
/* Hold back an incomplete trailing multi-byte UTF-8 sequence until the
|
||||
* next chunk arrives. */
|
||||
size_t back = 0;
|
||||
while (back < 3 && back < rem.size()) {
|
||||
unsigned char b = (unsigned char)rem[rem.size() - 1 - back];
|
||||
if ((b & 0x80) == 0) break; /* ASCII */
|
||||
if ((b & 0xC0) == 0xC0) { /* UTF-8 lead byte */
|
||||
size_t need = ((b & 0xE0) == 0xC0) ? 2 : ((b & 0xF0) == 0xE0) ? 3 : 4;
|
||||
if (back + 1 < need) rem.remove_suffix(back + 1);
|
||||
break;
|
||||
}
|
||||
back++;
|
||||
}
|
||||
}
|
||||
if (!rem.empty()) {
|
||||
sc->tcs_filt_args[slot] += filter_text(rem);
|
||||
sc->tcs_filt_pos[slot] = pos + rem.size();
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_upsert_tc(StreamCtx *sc, int idx, std::string_view id,
|
||||
std::string_view name, std::string_view args) {
|
||||
int slot = -1;
|
||||
@@ -136,30 +205,54 @@ static void stream_upsert_tc(StreamCtx *sc, int idx, std::string_view id,
|
||||
if (slot < 0) {
|
||||
slot = (int)sc->tcs.size();
|
||||
sc->tcs.emplace_back();
|
||||
sc->tcs_filt_args.emplace_back();
|
||||
sc->tcs_filt_pos.push_back(0);
|
||||
sc->tcs_idx.push_back(idx);
|
||||
}
|
||||
if (!id.empty()) sc->tcs[slot].id = std::string(id);
|
||||
if (!name.empty()) sc->tcs[slot].name += std::string(name);
|
||||
if (!args.empty()) sc->tcs[slot].arguments += std::string(args);
|
||||
if (!args.empty()) {
|
||||
sc->tcs[slot].arguments += std::string(args);
|
||||
advance_filtered_args(sc, slot, false);
|
||||
}
|
||||
}
|
||||
|
||||
static void live_target(StreamCtx *sc, int slot, bool closed, std::string &out) {
|
||||
ToolCall &t = sc->tcs[slot];
|
||||
str_appendf(out, "[tool call: %s", t.name.c_str());
|
||||
if (closed || !t.arguments.empty()) {
|
||||
std::string fa = filter_text(t.arguments);
|
||||
str_appendf(out, "(%s", fa.c_str());
|
||||
if (closed) out += ")]";
|
||||
static void print_live_delta(StreamCtx *sc, int slot, bool closed) {
|
||||
if (closed) advance_filtered_args(sc, slot, true);
|
||||
const ToolCall &t = sc->tcs[slot];
|
||||
const std::string &fa = sc->tcs_filt_args[slot];
|
||||
bool has_args = closed || !t.arguments.empty();
|
||||
size_t prefix_len = 12 + t.name.size(); /* "[tool call: " + name */
|
||||
size_t total_len = prefix_len + (has_args ? (1 + fa.size() + (closed ? 2 : 0)) : 0);
|
||||
size_t cur = sc->live_printed_len;
|
||||
if (total_len <= cur) return;
|
||||
|
||||
std::string delta;
|
||||
delta.reserve(total_len - cur);
|
||||
auto emit_part = [&](std::string_view part, size_t part_start) {
|
||||
size_t part_end = part_start + part.size();
|
||||
if (part_end <= cur) return;
|
||||
size_t off = (cur > part_start) ? (cur - part_start) : 0;
|
||||
delta.append(part.substr(off));
|
||||
};
|
||||
size_t off = 0;
|
||||
emit_part("[tool call: ", off); off += 12;
|
||||
emit_part(t.name, off); off += t.name.size();
|
||||
if (has_args) {
|
||||
emit_part("(", off); off += 1;
|
||||
emit_part(fa, off); off += fa.size();
|
||||
if (closed) {
|
||||
emit_part(")]", off); off += 2;
|
||||
}
|
||||
}
|
||||
cprint(delta, std::array{33});
|
||||
sc->live_printed_len = total_len;
|
||||
}
|
||||
|
||||
static void finish_live(StreamCtx *sc, bool closed) {
|
||||
if (sc->live_idx < 0) return;
|
||||
std::string target;
|
||||
live_target(sc, sc->live_idx, closed, target);
|
||||
if (target.size() > sc->live_printed.size()) {
|
||||
cprint(target.substr(sc->live_printed.size()), std::array{33});
|
||||
}
|
||||
sc->live_printed.clear();
|
||||
print_live_delta(sc, sc->live_idx, closed);
|
||||
sc->live_printed_len = 0;
|
||||
sc->live_idx = -1;
|
||||
/* The tool call marker must always terminate its own line so the
|
||||
* following [tool result: ...] header is separated from it by a
|
||||
@@ -172,13 +265,8 @@ static void step_live(StreamCtx *sc) {
|
||||
if (!sc->tty || sc->tcs.empty()) return;
|
||||
int cur = (int)sc->tcs.size() - 1;
|
||||
if (sc->live_idx != -1 && sc->live_idx != cur) finish_live(sc, true);
|
||||
if (sc->live_idx == -1) { sc->live_idx = cur; sc->live_printed.clear(); }
|
||||
std::string target;
|
||||
live_target(sc, cur, false, target);
|
||||
if (target.size() > sc->live_printed.size()) {
|
||||
cprint(target.substr(sc->live_printed.size()), std::array{33});
|
||||
sc->live_printed += target.substr(sc->live_printed.size());
|
||||
}
|
||||
if (sc->live_idx == -1) { sc->live_idx = cur; sc->live_printed_len = 0; }
|
||||
print_live_delta(sc, cur, false);
|
||||
sc->tool_calls_shown = 1;
|
||||
}
|
||||
|
||||
@@ -196,7 +284,7 @@ static void stream_maybe_erase(StreamCtx *sc) {
|
||||
}
|
||||
}
|
||||
|
||||
static void stream_handle_data(StreamCtx *sc, const std::string &data) {
|
||||
static void stream_handle_data(StreamCtx *sc, std::string_view data) {
|
||||
if (data == "[DONE]") { sc->done = 1; return; }
|
||||
Json d_mut;
|
||||
if (!Json::parse(data, d_mut) || !d_mut.is_object()) return;
|
||||
@@ -253,21 +341,23 @@ static void stream_handle_data(StreamCtx *sc, const std::string &data) {
|
||||
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
|
||||
sc->content += ct;
|
||||
sc->line_buf += ct;
|
||||
size_t lpos = 0;
|
||||
while (true) {
|
||||
auto nl = sc->line_buf.find('\n');
|
||||
auto nl = sc->line_buf.find('\n', lpos);
|
||||
if (nl == std::string::npos) break;
|
||||
std::string cur = sc->line_buf.substr(0, nl);
|
||||
sc->line_buf.erase(0, nl + 1);
|
||||
std::string_view cur_sv = std::string_view(sc->line_buf).substr(lpos, nl - lpos);
|
||||
lpos = nl + 1;
|
||||
/* Inside a code block every line is rendered verbatim as code; only
|
||||
* outside a code block do table-shaped lines get accumulated for
|
||||
* box-drawing table rendering. */
|
||||
if (sc->md.in_code || !is_table_line(cur)) {
|
||||
if (sc->md.in_code || !is_table_line(cur_sv)) {
|
||||
flush_tbl(sc);
|
||||
render_md_line_print(cur, sc->md);
|
||||
render_md_line_print(cur_sv, sc->md);
|
||||
} else {
|
||||
sc->tbl.push_back(cur);
|
||||
sc->tbl.emplace_back(cur_sv);
|
||||
}
|
||||
}
|
||||
if (lpos > 0) sc->line_buf.erase(0, lpos);
|
||||
}
|
||||
const Json &tcs = delta["tool_calls"];
|
||||
if (tcs.is_array()) {
|
||||
@@ -297,22 +387,26 @@ static int stream_feed(StreamCtx *sc, const char *data, size_t len,
|
||||
volatile int *interrupted) {
|
||||
sc->interrupted = interrupted;
|
||||
sc->buf.append(data, len);
|
||||
size_t pos = 0;
|
||||
while (true) {
|
||||
auto nl = sc->buf.find('\n');
|
||||
auto nl = sc->buf.find('\n', pos);
|
||||
if (nl == std::string::npos) break;
|
||||
std::string line = sc->buf.substr(0, nl);
|
||||
std::string_view line = std::string_view(sc->buf).substr(pos, nl - pos);
|
||||
pos = nl + 1;
|
||||
while (!line.empty() && (line.back() == '\r' || line.back() == ' ' || line.back() == '\t'))
|
||||
line.pop_back();
|
||||
if (line.rfind("data:", 0) == 0) {
|
||||
std::string_view dd = line;
|
||||
dd.remove_prefix(5);
|
||||
while (!dd.empty() && dd.front() == ' ') dd.remove_prefix(1);
|
||||
stream_handle_data(sc, std::string(dd));
|
||||
line.remove_suffix(1);
|
||||
if (line.starts_with("data:")) {
|
||||
line.remove_prefix(5);
|
||||
while (!line.empty() && line.front() == ' ') line.remove_prefix(1);
|
||||
stream_handle_data(sc, line);
|
||||
}
|
||||
sc->buf.erase(0, nl + 1);
|
||||
if (sc->done) break;
|
||||
if (*interrupted) return -1;
|
||||
if (*interrupted) {
|
||||
sc->buf.erase(0, pos);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (pos > 0) sc->buf.erase(0, pos);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -336,9 +430,9 @@ static void stream_result(StreamCtx *sc, Message &out, Usage &usage) {
|
||||
out = Message{};
|
||||
out.role = "assistant";
|
||||
out.calls_shown = sc->tool_calls_shown;
|
||||
if (!sc->content.empty()) out.content = sc->content;
|
||||
if (!sc->reas.empty()) out.reasoning = sc->reas;
|
||||
for (auto &t : sc->tcs) out.calls.push_back(t);
|
||||
if (!sc->content.empty()) out.content = std::move(sc->content);
|
||||
if (!sc->reas.empty()) out.reasoning = std::move(sc->reas);
|
||||
for (auto &t : sc->tcs) out.calls.push_back(std::move(t));
|
||||
usage = sc->last_usage;
|
||||
if (usage.total_tokens == 0 && usage.prompt_tokens > 0)
|
||||
usage.total_tokens = usage.prompt_tokens + usage.completion_tokens;
|
||||
@@ -349,7 +443,11 @@ static size_t stream_write_cb(char *ptr, size_t sz, size_t nm, void *ud) {
|
||||
StreamCtx *sc = (StreamCtx *)ud;
|
||||
size_t total = sz * nm;
|
||||
if (*sc->interrupted) return 0;
|
||||
sc->raw.append((const char *)ptr, total);
|
||||
/* Cap raw error buffer at 4096 bytes; http_err_body only uses 4000 bytes. */
|
||||
if (sc->raw.size() < 4096) {
|
||||
size_t rem = 4096 - sc->raw.size();
|
||||
sc->raw.append((const char *)ptr, total < rem ? total : rem);
|
||||
}
|
||||
int r = stream_feed(sc, ptr, total, sc->interrupted);
|
||||
if (r != 0 || *sc->interrupted) return 0;
|
||||
return total;
|
||||
@@ -388,20 +486,23 @@ int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
|
||||
int nf = 9;
|
||||
|
||||
std::string pend = cstr("...requesting...", std::array{1, 2});
|
||||
CurlHandle curl;
|
||||
if (!curl) { errmsg = "curl init failed"; return -1; }
|
||||
curl_easy_setopt(curl.c, CURLOPT_USERAGENT, "Mozilla/5.0 Bantam/nightly-cpp");
|
||||
CURL *c = get_shared_curl();
|
||||
if (!c) { errmsg = "curl init failed"; return -1; }
|
||||
CurlHeaders headers;
|
||||
apply_llm_headers(headers, cfg);
|
||||
|
||||
curl_easy_setopt(curl.c, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl.c, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl.c, CURLOPT_POSTFIELDS, payload.c_str());
|
||||
curl_easy_setopt(curl.c, CURLOPT_POSTFIELDSIZE, (long)payload.size());
|
||||
curl_easy_setopt(curl.c, CURLOPT_HTTPHEADER, headers.list);
|
||||
set_proxy(curl.c);
|
||||
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 30));
|
||||
if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 300));
|
||||
apply_common_curl_opts(c, interrupted);
|
||||
curl_easy_setopt(c, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(c, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDS, payload.c_str());
|
||||
curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)payload.size());
|
||||
curl_easy_setopt(c, CURLOPT_HTTPHEADER, headers.list);
|
||||
long t = (long)(cfg.timeout > 0 ? cfg.timeout : 300);
|
||||
long conn_t = (t < 20) ? t : 20;
|
||||
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, conn_t);
|
||||
curl_easy_setopt(c, CURLOPT_LOW_SPEED_LIMIT, 1L);
|
||||
curl_easy_setopt(c, CURLOPT_LOW_SPEED_TIME, t);
|
||||
if (!stream) curl_easy_setopt(c, CURLOPT_TIMEOUT, t);
|
||||
|
||||
int attempt, rc = -1;
|
||||
for (attempt = 0; attempt <= nf; attempt++) {
|
||||
@@ -416,20 +517,21 @@ int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
|
||||
sc.tty = is_terminal(STDOUT_FILENO);
|
||||
sc.interrupted = interrupted;
|
||||
sc.erase_pending = 1;
|
||||
curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, stream_write_cb);
|
||||
curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &sc);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, stream_write_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &sc);
|
||||
} else {
|
||||
curl_easy_setopt(curl.c, CURLOPT_WRITEFUNCTION, mem_cb);
|
||||
curl_easy_setopt(curl.c, CURLOPT_WRITEDATA, &mbody);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, mem_cb);
|
||||
curl_easy_setopt(c, CURLOPT_WRITEDATA, &mbody);
|
||||
}
|
||||
res = curl_easy_perform(curl.c);
|
||||
if (*interrupted) {
|
||||
res = curl_easy_perform(c);
|
||||
if (*interrupted || res == CURLE_ABORTED_BY_CALLBACK) {
|
||||
*interrupted = 1;
|
||||
if (g_col && (!stream || sc.erase_pending)) printf("\r\033[K");
|
||||
errmsg = "interrupted";
|
||||
rc = -1;
|
||||
break;
|
||||
}
|
||||
curl_easy_getinfo(curl.c, CURLINFO_RESPONSE_CODE, &code);
|
||||
curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &code);
|
||||
int transport_err = (res != CURLE_OK);
|
||||
int is4xx = (!transport_err && code >= 400 && code < 500 && code != 408 && code != 429);
|
||||
|
||||
|
||||
@@ -180,12 +180,13 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
|
||||
for (size_t tci = 0; tci < m.calls.size(); tci++) {
|
||||
if (*interrupted) { rc = -1; break; }
|
||||
ToolCall &tc = m.calls[tci];
|
||||
std::string astr = filter_text(tc.arguments);
|
||||
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());
|
||||
@@ -196,11 +197,11 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
|
||||
if (fn == "bash" || fn == "shell_exec") {
|
||||
std::string cmd = get_str(a, "command");
|
||||
std::string workdir = get_str(a, "workdir");
|
||||
std::string fcmd = filter_text(cmd);
|
||||
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(fcmd, fwd, to, interrupted);
|
||||
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);
|
||||
@@ -250,17 +251,17 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
|
||||
sty = 31;
|
||||
}
|
||||
}
|
||||
std::string fres = filter_text(res);
|
||||
res = offload_tool_result(fres, cfg.max_tool_res, tmps);
|
||||
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());
|
||||
std::string body = cstr(res, std::array{sty});
|
||||
printf("%s\n\n", body.c_str());
|
||||
cprint(res, std::array{sty});
|
||||
printf("\n\n");
|
||||
}
|
||||
Message tm; tm.role = "tool"; tm.tool_call_id = tc.id; tm.content = res;
|
||||
msgs.items.push_back(tm);
|
||||
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;
|
||||
@@ -374,16 +375,15 @@ static void do_compact(Cfg &cfg, Messages &msgs) {
|
||||
/* ----------------------------- main ----------------------------------- */
|
||||
struct CurlGlobalGuard {
|
||||
CurlGlobalGuard() { curl_global_init(CURL_GLOBAL_DEFAULT); }
|
||||
~CurlGlobalGuard() { curl_global_cleanup(); }
|
||||
~CurlGlobalGuard() { http_cleanup(); curl_global_cleanup(); }
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
CurlGlobalGuard curl_guard;
|
||||
setup_signal_handler();
|
||||
Cfg cfg = get_cfg(config_path());
|
||||
cfg.context_window = fetch_context_window(cfg);
|
||||
std::string sp = build_system_prompt(cfg);
|
||||
g_col = color_enabled(cfg);
|
||||
std::string sp = build_system_prompt(cfg);
|
||||
Messages msgs;
|
||||
{
|
||||
Message sys; sys.role = "system"; sys.content = sp;
|
||||
@@ -405,6 +405,7 @@ int main(int argc, char **argv) {
|
||||
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;
|
||||
@@ -443,6 +444,8 @@ int main(int argc, char **argv) {
|
||||
{
|
||||
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});
|
||||
|
||||
+47
-42
@@ -1,5 +1,6 @@
|
||||
/* session.cpp — session persistence, loading, compaction. */
|
||||
#include "bantam.hpp"
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -24,6 +25,29 @@ static std::string sessions_dir() {
|
||||
return d;
|
||||
}
|
||||
|
||||
static bool load_session_file(const std::string &full, Session &s) {
|
||||
std::string buf;
|
||||
if (!read_file_str(full, buf)) return false;
|
||||
Json root_mut;
|
||||
if (!Json::parse(buf, root_mut) || !root_mut.is_object()) return false;
|
||||
const Json &root = root_mut;
|
||||
s = Session{};
|
||||
s.id = root["id"].str();
|
||||
s.created = root["created"].str();
|
||||
s.summary = root["summary"].str();
|
||||
const Json &marr = root["messages"];
|
||||
if (marr.is_array()) {
|
||||
s.messages.items.reserve(marr.aval.size());
|
||||
for (auto &mo : marr.aval) {
|
||||
Message m;
|
||||
parse_message_json(mo, m);
|
||||
s.messages.items.push_back(std::move(m));
|
||||
}
|
||||
}
|
||||
if (s.summary.empty()) s.summary = "(no summary)";
|
||||
return true;
|
||||
}
|
||||
|
||||
void save_session(const Messages &msgs, std::string_view sid,
|
||||
std::string *out_id, std::string *out_summary) {
|
||||
std::string id = (!sid.empty()) ? std::string(sid) : project_id();
|
||||
@@ -40,6 +64,7 @@ void save_session(const Messages &msgs, std::string_view sid,
|
||||
std::string sum = summary_of(msgs);
|
||||
root["summary"] = sum;
|
||||
Json arr = Json::array();
|
||||
arr.aval.reserve(msgs.items.size());
|
||||
for (auto &m : msgs.items) {
|
||||
Json mo;
|
||||
serialize_message_json(mo, m);
|
||||
@@ -63,63 +88,46 @@ std::vector<Session> sessions_load() {
|
||||
std::string nm = de->d_name;
|
||||
if (nm == "." || nm == "..") continue;
|
||||
if (nm.size() < 5 || nm.substr(nm.size() - 5) != ".json") continue;
|
||||
files.push_back(nm);
|
||||
files.push_back(std::move(nm));
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
}
|
||||
for (size_t i = 1; i < files.size(); i++) {
|
||||
for (size_t j = i; j > 0 && files[j - 1] > files[j]; j--) {
|
||||
std::swap(files[j - 1], files[j]);
|
||||
}
|
||||
}
|
||||
std::sort(files.begin(), files.end());
|
||||
ss.reserve(files.size());
|
||||
for (auto &nm : files) {
|
||||
std::string full = d + "/" + nm;
|
||||
std::string buf;
|
||||
if (!read_file_str(full, buf)) continue;
|
||||
Json root_mut;
|
||||
if (!Json::parse(buf, root_mut) || !root_mut.is_object()) continue;
|
||||
const Json &root = root_mut;
|
||||
Session s;
|
||||
s.id = root["id"].str();
|
||||
s.created = root["created"].str();
|
||||
s.summary = root["summary"].str();
|
||||
const Json &marr = root["messages"];
|
||||
if (marr.is_array()) {
|
||||
for (auto &mo : marr.aval) {
|
||||
Message m;
|
||||
parse_message_json(mo, m);
|
||||
s.messages.items.push_back(std::move(m));
|
||||
}
|
||||
if (load_session_file(full, s)) {
|
||||
ss.push_back(std::move(s));
|
||||
}
|
||||
if (s.summary.empty()) s.summary = "(no summary)";
|
||||
ss.push_back(std::move(s));
|
||||
}
|
||||
/* sort by id descending (newest first) */
|
||||
for (size_t i = 1; i < ss.size(); i++) {
|
||||
for (size_t j = i; j > 0 && ss[j - 1].id < ss[j].id; j--) {
|
||||
std::swap(ss[j - 1], ss[j]);
|
||||
}
|
||||
}
|
||||
std::sort(ss.begin(), ss.end(), [](const Session &a, const Session &b) {
|
||||
return a.id > b.id;
|
||||
});
|
||||
return ss;
|
||||
}
|
||||
|
||||
std::unique_ptr<Messages> load_session(std::string_view sid, std::string *err) {
|
||||
if (!sid.empty() && sid.find('/') == std::string_view::npos) {
|
||||
std::string exact = sessions_dir() + "/" + std::string(sid) + ".json";
|
||||
Session s;
|
||||
if (load_session_file(exact, s) && s.id == sid) {
|
||||
return std::make_unique<Messages>(std::move(s.messages));
|
||||
}
|
||||
}
|
||||
auto ss = sessions_load();
|
||||
for (auto &s : ss) {
|
||||
if (s.id == sid) {
|
||||
auto res = std::make_unique<Messages>();
|
||||
for (auto &m : s.messages.items) res->items.push_back(m);
|
||||
return res;
|
||||
return std::make_unique<Messages>(std::move(s.messages));
|
||||
}
|
||||
}
|
||||
/* prefix match */
|
||||
std::vector<Session *> pref;
|
||||
for (auto &s : ss) if (s.id.rfind(sid, 0) == 0) pref.push_back(&s);
|
||||
for (auto &s : ss) if (s.id.starts_with(sid)) pref.push_back(&s);
|
||||
if (pref.size() == 1) {
|
||||
auto res = std::make_unique<Messages>();
|
||||
for (auto &m : pref[0]->messages.items) res->items.push_back(m);
|
||||
return res;
|
||||
return std::make_unique<Messages>(std::move(pref[0]->messages));
|
||||
}
|
||||
if (pref.size() > 1) {
|
||||
std::string names;
|
||||
@@ -134,13 +142,10 @@ std::unique_ptr<Messages> load_session(std::string_view sid, std::string *err) {
|
||||
|
||||
std::unique_ptr<Session> find_project_session() {
|
||||
std::string pid = project_id();
|
||||
auto ss = sessions_load();
|
||||
for (auto &s : ss) {
|
||||
if (s.id == pid) {
|
||||
auto found = std::make_unique<Session>();
|
||||
*found = std::move(s);
|
||||
return found;
|
||||
}
|
||||
std::string path = sessions_dir() + "/" + pid + ".json";
|
||||
auto found = std::make_unique<Session>();
|
||||
if (load_session_file(path, *found) && found->id == pid) {
|
||||
return found;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/* tools.cpp — bash/read/write tools and oversized result offloading. */
|
||||
#include "bantam.hpp"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
@@ -20,6 +21,8 @@
|
||||
* be located anywhere on PATH. Never returns a hardcoded bash path. */
|
||||
static const char *resolve_shell() {
|
||||
static char buf[4096];
|
||||
static const char *cached = nullptr;
|
||||
if (cached) return cached;
|
||||
const char *path = getenv("PATH");
|
||||
if (path && *path) {
|
||||
std::string p(path);
|
||||
@@ -34,19 +37,22 @@ static const char *resolve_shell() {
|
||||
if (stat(cand.c_str(), &st) == 0 && S_ISREG(st.st_mode) &&
|
||||
(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0) {
|
||||
snprintf(buf, sizeof buf, "%s", cand.c_str());
|
||||
return buf;
|
||||
cached = buf;
|
||||
return cached;
|
||||
}
|
||||
if (end == std::string::npos) break;
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
return "/bin/sh";
|
||||
cached = "/bin/sh";
|
||||
return cached;
|
||||
}
|
||||
|
||||
std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
||||
int timeout, volatile int *interrupted) {
|
||||
std::string fcmd = filter_text(cmd);
|
||||
if (timeout <= 0) timeout = 120;
|
||||
const char *sh = resolve_shell();
|
||||
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) != 0) return "[tool error: pipe failed]";
|
||||
@@ -57,13 +63,14 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
||||
return "[tool error: fork failed]";
|
||||
}
|
||||
if (pid == 0) {
|
||||
/* child */
|
||||
/* child: create a dedicated process group so timeout/SIGINT kills the
|
||||
* entire process tree spawned by the shell. */
|
||||
setpgid(0, 0);
|
||||
dup2(pipefd[1], STDOUT_FILENO);
|
||||
dup2(pipefd[1], STDERR_FILENO);
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
if (!workdir.empty()) chdir(std::string(workdir).c_str());
|
||||
const char *sh = resolve_shell();
|
||||
const char *shname = (strrchr(sh, '/') ? strrchr(sh, '/') + 1 : sh);
|
||||
execl(sh, shname, "-c", fcmd.c_str(), (char *)nullptr);
|
||||
_exit(127);
|
||||
@@ -81,9 +88,11 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
||||
if (*interrupted) break;
|
||||
int remain = (int)(deadline - time(nullptr));
|
||||
if (remain <= 0) { killed = true; break; }
|
||||
int pr = poll(&pfd, 1, remain * 1000);
|
||||
int wait_ms = remain * 1000;
|
||||
if (wait_ms > 200) wait_ms = 200;
|
||||
int pr = poll(&pfd, 1, wait_ms);
|
||||
if (pr < 0) { if (errno == EINTR) continue; break; }
|
||||
if (pr == 0) { killed = true; break; }
|
||||
if (pr == 0) continue;
|
||||
char buf[4096];
|
||||
ssize_t n = read(pipefd[0], buf, sizeof buf);
|
||||
if (n > 0) out.append(buf, (size_t)n);
|
||||
@@ -92,6 +101,7 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
||||
else break;
|
||||
}
|
||||
if (*interrupted || killed) {
|
||||
kill(-pid, SIGKILL);
|
||||
kill(pid, SIGKILL);
|
||||
/* drain remaining output briefly */
|
||||
struct pollfd dpfd; dpfd.fd = pipefd[0]; dpfd.events = POLLIN;
|
||||
@@ -141,29 +151,41 @@ std::string read_file_or_dir(std::string_view path, int offset, int limit, int *
|
||||
return sfmt("[tool error: read %s: %s]", p.c_str(), strerror(errno));
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
std::vector<std::string> raw_names;
|
||||
struct DirEnt {
|
||||
std::string name;
|
||||
int is_dir; /* -1 = unknown, 0 = non-dir, 1 = dir */
|
||||
};
|
||||
std::vector<DirEnt> entries;
|
||||
{
|
||||
DIR *dir = opendir(p.c_str());
|
||||
if (!dir) { *style = 31; return "[tool error: read (directory listing failed)]"; }
|
||||
struct dirent *de;
|
||||
while ((de = readdir(dir))) {
|
||||
std::string nm = de->d_name;
|
||||
if (nm == "." || nm == "..") continue;
|
||||
raw_names.push_back(std::move(nm));
|
||||
if (de->d_name[0] == '.' &&
|
||||
(de->d_name[1] == '\0' || (de->d_name[1] == '.' && de->d_name[2] == '\0')))
|
||||
continue;
|
||||
int isdir = -1;
|
||||
#if defined(DT_DIR) && defined(DT_UNKNOWN) && defined(DT_LNK)
|
||||
if (de->d_type != DT_UNKNOWN && de->d_type != DT_LNK) {
|
||||
isdir = (de->d_type == DT_DIR) ? 1 : 0;
|
||||
}
|
||||
#endif
|
||||
entries.push_back(DirEnt{de->d_name, isdir});
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
for (size_t i = 1; i < raw_names.size(); i++) {
|
||||
for (size_t j = i; j > 0 && raw_names[j - 1] > raw_names[j]; j--) {
|
||||
std::swap(raw_names[j - 1], raw_names[j]);
|
||||
}
|
||||
}
|
||||
std::sort(entries.begin(), entries.end(), [](const DirEnt &a, const DirEnt &b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
std::string b;
|
||||
for (auto &nm : raw_names) {
|
||||
std::string full = p + "/" + nm;
|
||||
struct stat ds;
|
||||
bool isdir = (stat(full.c_str(), &ds) == 0 && S_ISDIR(ds.st_mode));
|
||||
b += nm;
|
||||
for (auto &ent : entries) {
|
||||
bool isdir = (ent.is_dir == 1);
|
||||
if (ent.is_dir < 0) {
|
||||
std::string full = p + "/" + ent.name;
|
||||
struct stat ds;
|
||||
isdir = (stat(full.c_str(), &ds) == 0 && S_ISDIR(ds.st_mode));
|
||||
}
|
||||
b += ent.name;
|
||||
if (isdir) b += '/';
|
||||
b += '\n';
|
||||
}
|
||||
|
||||
@@ -12,11 +12,15 @@
|
||||
|
||||
/* ----------------------------- formatting ----------------------------- */
|
||||
static std::string vfmt(const char *fmt, va_list ap) {
|
||||
char stack_buf[512];
|
||||
va_list cp;
|
||||
va_copy(cp, ap);
|
||||
int need = std::vsnprintf(nullptr, 0, fmt, cp);
|
||||
int need = std::vsnprintf(stack_buf, sizeof stack_buf, fmt, cp);
|
||||
va_end(cp);
|
||||
if (need < 0) return {};
|
||||
if ((size_t)need < sizeof stack_buf) {
|
||||
return std::string(stack_buf, (size_t)need);
|
||||
}
|
||||
std::string s;
|
||||
s.resize((size_t)need);
|
||||
std::vsnprintf(s.data(), (size_t)need + 1, fmt, ap);
|
||||
@@ -30,11 +34,22 @@ std::string sfmt(const char *fmt, ...) {
|
||||
return s;
|
||||
}
|
||||
void str_appendf(std::string &s, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
char stack_buf[512];
|
||||
va_list ap, cp;
|
||||
va_start(ap, fmt);
|
||||
std::string more = vfmt(fmt, ap);
|
||||
va_copy(cp, ap);
|
||||
int need = std::vsnprintf(stack_buf, sizeof stack_buf, fmt, cp);
|
||||
va_end(cp);
|
||||
if (need > 0) {
|
||||
if ((size_t)need < sizeof stack_buf) {
|
||||
s.append(stack_buf, (size_t)need);
|
||||
} else {
|
||||
size_t old_sz = s.size();
|
||||
s.resize(old_sz + (size_t)need);
|
||||
std::vsnprintf(s.data() + old_sz, (size_t)need + 1, fmt, ap);
|
||||
}
|
||||
}
|
||||
va_end(ap);
|
||||
s += more;
|
||||
}
|
||||
|
||||
/* ----------------------------- UTF-8 ---------------------------------- */
|
||||
@@ -102,6 +117,19 @@ std::string filter_text(std::string_view s) {
|
||||
const unsigned char *p = (const unsigned char *)s.data();
|
||||
size_t avail = s.size();
|
||||
while (avail > 0) {
|
||||
/* Fast path: scan contiguous printable ASCII plus '\t' and '\n'. */
|
||||
size_t run = 0;
|
||||
while (run < avail) {
|
||||
unsigned char b = p[run];
|
||||
if ((b >= 0x20 && b <= 0x7e) || b == '\n' || b == '\t') run++;
|
||||
else break;
|
||||
}
|
||||
if (run > 0) {
|
||||
out.append((const char *)p, run);
|
||||
p += run;
|
||||
avail -= run;
|
||||
if (avail == 0) break;
|
||||
}
|
||||
size_t pl;
|
||||
int cp = utf8_decode(p, avail, &pl);
|
||||
if (cp == 0xFFFD && pl == 1) { p++; avail--; continue; }
|
||||
@@ -183,10 +211,12 @@ int rune_width(int cp) {
|
||||
void mkdir_p(std::string_view dir) {
|
||||
if (dir.empty()) return;
|
||||
std::string path(dir);
|
||||
struct stat st;
|
||||
if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode)) return;
|
||||
for (size_t i = 1; i < path.size(); i++) {
|
||||
if (path[i] == '/') {
|
||||
path[i] = '\0';
|
||||
mkdir(path.c_str(), 0755);
|
||||
if (stat(path.c_str(), &st) != 0) mkdir(path.c_str(), 0755);
|
||||
path[i] = '/';
|
||||
}
|
||||
}
|
||||
@@ -218,12 +248,15 @@ std::string tool_res_dir(std::string_view sess_id) {
|
||||
return p;
|
||||
}
|
||||
std::string project_id() {
|
||||
char cwd[4096];
|
||||
if (!getcwd(cwd, sizeof cwd)) std::strcpy(cwd, ".");
|
||||
char *abs = realpath(cwd, nullptr);
|
||||
std::string src = abs ? abs : cwd;
|
||||
if (abs) free(abs);
|
||||
return md5_hex(src);
|
||||
static const std::string cached_id = []() {
|
||||
char cwd[4096];
|
||||
if (!getcwd(cwd, sizeof cwd)) std::strcpy(cwd, ".");
|
||||
char *abs = realpath(cwd, nullptr);
|
||||
std::string src = abs ? abs : cwd;
|
||||
if (abs) free(abs);
|
||||
return md5_hex(src);
|
||||
}();
|
||||
return cached_id;
|
||||
}
|
||||
|
||||
/* ----------------------------- MD5 ------------------------------------ */
|
||||
@@ -322,7 +355,11 @@ bool read_file_str(std::string_view path, std::string &out) {
|
||||
std::string p(path);
|
||||
FILE *f = std::fopen(p.c_str(), "rb");
|
||||
if (!f) return false;
|
||||
char buf[4096];
|
||||
struct stat st;
|
||||
if (fstat(fileno(f), &st) == 0 && S_ISREG(st.st_mode) && st.st_size > 0) {
|
||||
out.reserve((size_t)st.st_size);
|
||||
}
|
||||
char buf[16384];
|
||||
size_t n;
|
||||
while ((n = std::fread(buf, 1, sizeof buf, f)) > 0) {
|
||||
out.append(buf, n);
|
||||
|
||||
Reference in New Issue
Block a user