Optimize startup, HTTP/SSE streaming, and fix macOS libedit bindings

This commit is contained in:
Luxferre
2026-10-14 15:30:12 +03:00
parent 105d283e01
commit a1d5f4bf48
10 changed files with 497 additions and 222 deletions
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+79 -28
View File
@@ -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();
+185 -83
View File
@@ -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);
+15 -12
View File
@@ -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
View File
@@ -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;
}
+42 -20
View File
@@ -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';
}
+49 -12
View File
@@ -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);