resolve shell from PATH (fallback /bin/sh); rewrite write tool with lineOffset/byteOffset + delLines/delBytes
This commit is contained in:
@@ -357,20 +357,28 @@ anything is sent back to the model.
|
|||||||
|
|
||||||
### `write` tool
|
### `write` tool
|
||||||
|
|
||||||
- Parameters: `filePath` (string, required; also accepts `path`); `offset`
|
- Parameters: `filePath` (string, required; also accepts `path`); `content`
|
||||||
(integer, optional, byte offset); `del_bytes` (integer, optional); `content`
|
(string, required, may be empty); `lineOffset` (integer, optional, 1-indexed
|
||||||
(string, required, may be empty).
|
line number to start writing from — takes precedence over `byteOffset`);
|
||||||
- Action: if `offset` and `del_bytes` are both omitted, the entire file is
|
`byteOffset` (integer, optional, byte offset — used only when `lineOffset` is
|
||||||
overwritten with `content` (the file and any necessary parent directories
|
not given); `delLines` (integer, optional, number of lines to delete starting
|
||||||
are created if missing); otherwise `content` is written at `offset`,
|
at the insertion point — takes precedence over `delBytes`); `delBytes`
|
||||||
optionally deleting `del_bytes` bytes first (splicing prefix + content +
|
(integer, optional, number of bytes to delete — used only when `delLines` is
|
||||||
suffix and never appending to the end of an existing file). (Also accepted as
|
not given).
|
||||||
legacy `write_file`.)
|
- Action: if none of `lineOffset`, `byteOffset`, `delLines` and `delBytes` are
|
||||||
|
provided, the entire file is overwritten with `content` (the file and any
|
||||||
|
necessary parent directories are created if missing). Otherwise `content` is
|
||||||
|
written at the resolved line/byte offset, optionally deleting `delLines`
|
||||||
|
lines or `delBytes` bytes first (splicing prefix + content + suffix). Line
|
||||||
|
numbers are 1-indexed and match the `read` tool's numbering; a `lineOffset`
|
||||||
|
past the end of the file appends at the end. (Also accepted as legacy
|
||||||
|
`write_file`; legacy `offset`/`del_bytes` are no longer emitted but the
|
||||||
|
`byteOffset`/`delBytes` byte-based mode covers the same behavior.)
|
||||||
|
|
||||||
### `bash` tool
|
### `bash` tool
|
||||||
|
|
||||||
- Parameters: `command` (string, required); `workdir` (string, optional).
|
- Parameters: `command` (string, required); `workdir` (string, optional).
|
||||||
- Action: run `command` via `bash -c` subject to `shell_timeout`
|
- Action: run `command` through the system shell (`bash` resolved from `PATH`, falling back to `/bin/sh` if bash is unavailable) via `-c`, subject to `shell_timeout`
|
||||||
(default `120s`) and return `output + '\nexit: ' + exit_code`. (Also
|
(default `120s`) and return `output + '\nexit: ' + exit_code`. (Also
|
||||||
accepted as legacy `shell_exec`.)
|
accepted as legacy `shell_exec`.)
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -225,7 +225,7 @@ bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage)
|
|||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* llm.cpp — HTTP client (libcurl), headers, backoff, dispatch. */
|
/* llm.cpp — HTTP client (libcurl), headers, backoff, dispatch. */
|
||||||
/* ------------------------------------------------------------------ */
|
/* -------------------------------------------------------------- */
|
||||||
int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code,
|
int http_get(Cfg &cfg, std::string_view url, std::string &body, long &code,
|
||||||
std::string &errmsg);
|
std::string &errmsg);
|
||||||
int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
|
int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
|
||||||
@@ -238,8 +238,9 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
|||||||
int timeout, volatile int *interrupted);
|
int timeout, volatile int *interrupted);
|
||||||
std::string read_file_or_dir(std::string_view path, int offset, int limit,
|
std::string read_file_or_dir(std::string_view path, int offset, int limit,
|
||||||
int *style);
|
int *style);
|
||||||
std::string write_file(std::string_view path, int offset, int del_bytes,
|
std::string write_file(std::string_view path, long line_offset, long byte_offset,
|
||||||
std::string_view content, int *err);
|
long del_lines, long del_bytes,
|
||||||
|
std::string_view content, int *err);
|
||||||
std::string offload_tool_result(std::string_view res, int max_res,
|
std::string offload_tool_result(std::string_view res, int max_res,
|
||||||
std::vector<std::string> &tmps);
|
std::vector<std::string> &tmps);
|
||||||
void cleanup_temps(std::vector<std::string> &tmps);
|
void cleanup_temps(std::vector<std::string> &tmps);
|
||||||
|
|||||||
@@ -401,30 +401,6 @@ struct JParser {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c == '[') {
|
if (c == '[') {
|
||||||
pos++;
|
|
||||||
skip_ws();
|
|
||||||
out = Json::array();
|
|
||||||
if (pos < src.size() && src[pos] == ']') {
|
|
||||||
pos++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
while (true) {
|
|
||||||
Json elem;
|
|
||||||
if (!parse_value(elem, depth + 1)) return false;
|
|
||||||
out.aval.push_back(std::move(elem));
|
|
||||||
skip_ws();
|
|
||||||
if (pos < src.size() && src[pos] == ',') {
|
|
||||||
pos++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (pos < src.size() && src[pos] == ']') {
|
|
||||||
pos++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (c == '{') {
|
|
||||||
pos++;
|
pos++;
|
||||||
skip_ws();
|
skip_ws();
|
||||||
out = Json::object();
|
out = Json::object();
|
||||||
@@ -491,12 +467,14 @@ static const char *TOOLS_JSON = R"JSON([
|
|||||||
},"required":["filePath"]}}},
|
},"required":["filePath"]}}},
|
||||||
{"type":"function","function":{
|
{"type":"function","function":{
|
||||||
"name":"write",
|
"name":"write",
|
||||||
"description":"Write content to a file. If offset and del_bytes are both omitted, the entire file is overwritten with the new content; otherwise content is written at the given byte offset, optionally deleting bytes first.",
|
"description":"Write content to a file. If none of lineOffset, byteOffset, delLines and delBytes are provided, the entire file is overwritten with the new content (the file and any necessary parent directories are created if missing); otherwise content is written at the given line or byte offset, optionally deleting lines or bytes first. Line-based editing is primary.",
|
||||||
"parameters":{"type":"object","properties":{
|
"parameters":{"type":"object","properties":{
|
||||||
"filePath":{"type":"string","description":"The path to the file to write"},
|
"filePath":{"type":"string","description":"The path to the file to write"},
|
||||||
"content":{"type":"string","description":"The content to write to the file"},
|
"content":{"type":"string","description":"The content to write to the file"},
|
||||||
"offset":{"type":"integer","description":"Byte offset to start writing from. If omitted together with del_bytes, the whole file is overwritten instead. Defaults to 0 (start of file)."},
|
"lineOffset":{"type":"integer","description":"1-indexed line number where writing begins (1 = first line). Takes precedence over byteOffset when both are given."},
|
||||||
"del_bytes":{"type":"integer","description":"Bytes to delete starting at offset. If omitted together with offset, the whole file is overwritten instead."}
|
"byteOffset":{"type":"integer","description":"Byte offset where writing begins. Used only when lineOffset is not given."},
|
||||||
|
"delLines":{"type":"integer","description":"Number of lines to delete starting at the insertion point. Takes precedence over delBytes when both are given."},
|
||||||
|
"delBytes":{"type":"integer","description":"Number of bytes to delete starting at the insertion point. Used only when delLines is not given."}
|
||||||
},"required":["filePath","content"]}}}
|
},"required":["filePath","content"]}}}
|
||||||
])JSON";
|
])JSON";
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ static const char *default_system_prompt =
|
|||||||
"You are Bantam, a tiny, powerful AI agent. Solve the user's task using three tools:\n"
|
"You are Bantam, a tiny, powerful AI agent. Solve the user's task using three tools:\n"
|
||||||
"- bash: run a bash command; returns its output and exit code.\n"
|
"- bash: run a bash command; returns its output and exit code.\n"
|
||||||
"- read: read a file or directory from the local filesystem; returns numbered lines or directory entries.\n"
|
"- read: read a file or directory from the local filesystem; returns numbered lines or directory entries.\n"
|
||||||
"- write: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.\n\n"
|
"- write: write content to a file; if lineOffset/byteOffset and delLines/delBytes are all omitted it overwrites the entire file, otherwise it writes at the given line or byte offset (optionally deleting lines or bytes first); line-based edits are preferred; returns status.\n\n"
|
||||||
"Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.\n"
|
"Work fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.\n"
|
||||||
"Use the target system's filesystem deliberately:\n"
|
"Use the target system's filesystem deliberately:\n"
|
||||||
"- Use only $TMPDIR/bantam as the temporary directory for intermediate or scratch files (logs, downloads, temporary build outputs, etc.); create it if it does not exist.\n"
|
"- Use only $TMPDIR/bantam as the temporary directory for intermediate or scratch files (logs, downloads, temporary build outputs, etc.); create it if it does not exist.\n"
|
||||||
@@ -212,13 +212,22 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
|
|||||||
std::string fp = filter_text(p);
|
std::string fp = filter_text(p);
|
||||||
std::string cv = get_str(a, "content");
|
std::string cv = get_str(a, "content");
|
||||||
bool has_content = a.contains("content") && a["content"].is_string();
|
bool has_content = a.contains("content") && a["content"].is_string();
|
||||||
bool has_offset = a.contains("offset") && !a["offset"].is_null();
|
bool has_line = a.contains("lineOffset") && !a["lineOffset"].is_null();
|
||||||
bool has_del = a.contains("del_bytes") && !a["del_bytes"].is_null();
|
bool has_byte = a.contains("byteOffset") && !a["byteOffset"].is_null();
|
||||||
int offset = has_offset ? get_int(a, "offset") : 0; if (offset < 0) offset = 0;
|
bool has_del_lines = a.contains("delLines") && !a["delLines"].is_null();
|
||||||
int del = has_del ? get_int(a, "del_bytes") : 0;
|
bool has_del_bytes = a.contains("delBytes") && !a["delBytes"].is_null();
|
||||||
|
long line_off = has_line ? (long)get_int(a, "lineOffset") : -1L;
|
||||||
|
long byte_off = has_byte ? (long)get_int(a, "byteOffset") : -1L;
|
||||||
|
long del_lines = has_del_lines ? (long)get_int(a, "delLines") : -1L;
|
||||||
|
long del_bytes = has_del_bytes ? (long)get_int(a, "delBytes") : -1L;
|
||||||
|
if (line_off < 1) line_off = -1L;
|
||||||
|
if (byte_off < 0) byte_off = -1L;
|
||||||
|
if (del_lines < 0) del_lines = -1L;
|
||||||
|
if (del_bytes < 0) del_bytes = -1L;
|
||||||
if (fp.empty()) { res = "[tool error: write requires 'filePath' parameter]"; sty = 31; }
|
if (fp.empty()) { res = "[tool error: write requires 'filePath' parameter]"; sty = 31; }
|
||||||
else if (!has_content) { res = "[tool error: write requires 'content' parameter]"; sty = 31; }
|
else if (!has_content) { res = "[tool error: write requires 'content' parameter]"; sty = 31; }
|
||||||
else if (!has_offset && !has_del) {
|
else if (!has_line && !has_byte && !has_del_lines && !has_del_bytes) {
|
||||||
|
/* No offset/deletion parameters: overwrite the whole file. */
|
||||||
std::string c = filter_text(cv);
|
std::string c = filter_text(cv);
|
||||||
auto sl = fp.find_last_of('/');
|
auto sl = fp.find_last_of('/');
|
||||||
if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl));
|
if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl));
|
||||||
@@ -227,7 +236,7 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
|
|||||||
else { res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
|
else { res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
|
||||||
} else {
|
} else {
|
||||||
int werr = 0; std::string c = filter_text(cv);
|
int werr = 0; std::string c = filter_text(cv);
|
||||||
res = write_file(fp, offset, del, c, &werr);
|
res = write_file(fp, line_off, byte_off, del_lines, del_bytes, c, &werr);
|
||||||
if (werr) sty = 31;
|
if (werr) sty = 31;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -15,6 +15,34 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
/* ----------------------------- shell ---------------------------------- */
|
/* ----------------------------- shell ---------------------------------- */
|
||||||
|
/* Resolve the Bourne-again shell from the system PATH. Returns a static
|
||||||
|
* buffer holding the absolute path to `bash`, or "/bin/sh" when bash cannot
|
||||||
|
* be located anywhere on PATH. Never returns a hardcoded bash path. */
|
||||||
|
static const char *resolve_shell() {
|
||||||
|
static char buf[4096];
|
||||||
|
const char *path = getenv("PATH");
|
||||||
|
if (path && *path) {
|
||||||
|
std::string p(path);
|
||||||
|
size_t start = 0;
|
||||||
|
while (start <= p.size()) {
|
||||||
|
size_t end = p.find(':', start);
|
||||||
|
std::string dir = (end == std::string::npos) ? p.substr(start)
|
||||||
|
: p.substr(start, end - start);
|
||||||
|
if (!dir.empty() && dir.back() != '/') dir += '/';
|
||||||
|
std::string cand = dir + "bash";
|
||||||
|
struct stat st;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
if (end == std::string::npos) break;
|
||||||
|
start = end + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "/bin/sh";
|
||||||
|
}
|
||||||
|
|
||||||
std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
||||||
int timeout, volatile int *interrupted) {
|
int timeout, volatile int *interrupted) {
|
||||||
std::string fcmd = filter_text(cmd);
|
std::string fcmd = filter_text(cmd);
|
||||||
@@ -35,7 +63,9 @@ std::string shell_with_workdir(std::string_view cmd, std::string_view workdir,
|
|||||||
close(pipefd[0]);
|
close(pipefd[0]);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
if (!workdir.empty()) chdir(std::string(workdir).c_str());
|
if (!workdir.empty()) chdir(std::string(workdir).c_str());
|
||||||
execl("/bin/bash", "bash", "-c", fcmd.c_str(), (char *)nullptr);
|
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);
|
_exit(127);
|
||||||
}
|
}
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
@@ -171,7 +201,38 @@ std::string read_file_or_dir(std::string_view path, int offset, int limit, int *
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* ----------------------------- write ---------------------------------- */
|
/* ----------------------------- write ---------------------------------- */
|
||||||
std::string write_file(std::string_view path, int offset, int del_bytes,
|
/* Convert a 1-indexed line number to the byte offset of the start of that
|
||||||
|
* line within `data`. Line 1 begins at byte 0; a line number past the end of
|
||||||
|
* the file maps to data.size() (i.e. append at end of file). */
|
||||||
|
static size_t line_to_byte_offset(std::string_view data, long line_no) {
|
||||||
|
if (line_no <= 1) return 0;
|
||||||
|
size_t pos = 0;
|
||||||
|
long cur = 1;
|
||||||
|
while (pos < data.size()) {
|
||||||
|
if (data[pos] == '\n') {
|
||||||
|
cur++;
|
||||||
|
if (cur == line_no) return pos + 1;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
return data.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Byte length of `nlines` complete lines beginning at byte offset `start`.
|
||||||
|
* Counts the newlines terminating each line and stops at end of data. */
|
||||||
|
static size_t lines_to_byte_len(std::string_view data, size_t start, long nlines) {
|
||||||
|
if (nlines <= 0 || start > data.size()) return 0;
|
||||||
|
size_t pos = start;
|
||||||
|
long consumed = 0;
|
||||||
|
while (pos < data.size() && consumed < nlines) {
|
||||||
|
if (data[pos] == '\n') consumed++;
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
return pos - start;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string write_file(std::string_view path, long line_offset, long byte_offset,
|
||||||
|
long del_lines, long del_bytes,
|
||||||
std::string_view content, int *err) {
|
std::string_view content, int *err) {
|
||||||
*err = 0;
|
*err = 0;
|
||||||
std::string p(path);
|
std::string p(path);
|
||||||
@@ -179,8 +240,6 @@ std::string write_file(std::string_view path, int offset, int del_bytes,
|
|||||||
while (s < p.size() && (p[s] == ' ' || p[s] == '\t')) s++;
|
while (s < p.size() && (p[s] == ' ' || p[s] == '\t')) s++;
|
||||||
p = p.substr(s);
|
p = p.substr(s);
|
||||||
if (p.empty()) { *err = 1; return "[tool error: path is required]"; }
|
if (p.empty()) { *err = 1; return "[tool error: path is required]"; }
|
||||||
if (offset < 0) offset = 0;
|
|
||||||
if (del_bytes < 0) del_bytes = 0;
|
|
||||||
|
|
||||||
std::string data;
|
std::string data;
|
||||||
struct stat st;
|
struct stat st;
|
||||||
@@ -198,11 +257,32 @@ std::string write_file(std::string_view path, int offset, int del_bytes,
|
|||||||
if (slash != std::string::npos) mkdir_p(std::string_view(p).substr(0, slash));
|
if (slash != std::string::npos) mkdir_p(std::string_view(p).substr(0, slash));
|
||||||
else mkdir_p(".");
|
else mkdir_p(".");
|
||||||
}
|
}
|
||||||
if ((size_t)offset > data.size()) {
|
|
||||||
data.resize((size_t)offset, '\0');
|
/* Resolve insertion byte offset. Line-based takes precedence over byte-based
|
||||||
|
* ("first and foremost line-based writing"); if neither is given the whole
|
||||||
|
* file is overwritten by the caller. */
|
||||||
|
size_t ins_off = 0;
|
||||||
|
if (line_offset >= 0) {
|
||||||
|
ins_off = line_to_byte_offset(data, line_offset);
|
||||||
|
} else if (byte_offset >= 0) {
|
||||||
|
ins_off = (size_t)byte_offset;
|
||||||
|
if (ins_off > data.size()) data.resize(ins_off, '\0');
|
||||||
|
} else {
|
||||||
|
ins_off = 0;
|
||||||
}
|
}
|
||||||
size_t end_del = (size_t)offset + (size_t)del_bytes;
|
|
||||||
size_t prefix_len = (size_t)offset;
|
/* Resolve deletion span. Line-based takes precedence over byte-based. */
|
||||||
|
size_t del_len = 0;
|
||||||
|
if (del_lines >= 0) {
|
||||||
|
del_len = lines_to_byte_len(data, ins_off, del_lines);
|
||||||
|
} else if (del_bytes >= 0) {
|
||||||
|
del_len = (size_t)del_bytes;
|
||||||
|
if (ins_off + del_len > data.size())
|
||||||
|
del_len = (ins_off < data.size()) ? (data.size() - ins_off) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t end_del = ins_off + del_len;
|
||||||
|
size_t prefix_len = ins_off;
|
||||||
size_t suffix_len = (end_del < data.size()) ? (data.size() - end_del) : 0;
|
size_t suffix_len = (end_del < data.size()) ? (data.size() - end_del) : 0;
|
||||||
size_t clen = content.size();
|
size_t clen = content.size();
|
||||||
size_t newlen = prefix_len + clen + suffix_len;
|
size_t newlen = prefix_len + clen + suffix_len;
|
||||||
@@ -220,7 +300,7 @@ std::string write_file(std::string_view path, int offset, int del_bytes,
|
|||||||
|
|
||||||
/* ----------------------------- offload -------------------------------- */
|
/* ----------------------------- offload -------------------------------- */
|
||||||
std::string offload_tool_result(std::string_view res, int max_res,
|
std::string offload_tool_result(std::string_view res, int max_res,
|
||||||
std::vector<std::string> &tmps) {
|
std::vector<std::string> &tmps) {
|
||||||
if (max_res <= 0) max_res = 65536;
|
if (max_res <= 0) max_res = 65536;
|
||||||
if (res.size() <= (size_t)max_res) return std::string(res);
|
if (res.size() <= (size_t)max_res) return std::string(res);
|
||||||
std::string dir = tool_res_dir();
|
std::string dir = tool_res_dir();
|
||||||
|
|||||||
Reference in New Issue
Block a user