Four settings read from the environment (endpoint, model, reasoning, scratch dir) pointed straight at getenv() memory instead of a copy, and both setstr() and the cleanup at exit called free() on them. Changing one of them from inside the session therefore freed the environment block: "/endpoint http://x.example" with SOLO_ENDPOINT set died inside the handler, and SOLO_TMPDIR killed the process on the way out without a slash command at all. Reproduced on musl, both are copies now, and the two post-load env re-reads go through setstr() so they stop leaking the value create() already stored. The generic parameter handler typed every number with json_object_new_double, so "/max_tokens 4096" reached the endpoint as 4096.0 and "/seed 3" as 3.0. A number written without a decimal point or an exponent now stays an integer, which is what the OpenAI schema asks for; 0.7, .5 and 1e3 are still floats, and a word is still a string. The deadline for a command was time(0) + SOLO_TIMEOUT, and time() counts whole seconds, so the effective limit was the timeout minus whatever fraction of the second the command happened to start in. Measured with SOLO_TIMEOUT=3, commands died after 2.38s, 2.47s, 2.68s and 3.19s depending on the phase. rp() takes its deadline from clock_gettime(CLOCK_MONOTONIC) now, which is also immune to the clock being stepped underneath it. A SOLO_TIMEOUT below 1 gave rp() a deadline already in the past, so every command failed at once with "still running after 0s", and atoi on anything unparseable lands there too. Below 1 now means no limit. The API read, which passed time(0) + 1e9 to mean "never", passes no deadline at all. Only the child called setpgid(0,0), so a kill(-k, ...) issued before the child reached it found no process group and was dropped, leaving the command running as an orphan. A standalone harness confirms both halves: without the parent claim the signal is lost and the child runs to completion, with it the child dies. The parent claims the group too, which is the belt to the child's braces. hc() wrote the response envelope with one uncounted write(), so a signal arriving mid-write truncated the JSON and the parent reported "request failed" for a request that had in fact succeeded. Both link steps write to <name>.new and rename it. Overwriting a running executable fails with ETXTBSY, which is exactly what happens when the agent rebuilds the binary it is itself running from, and a failed link used to take the working binary with it. make clean now removes the temp names as well. test.sh holds 13 offline tests: no API key and no network, every line handed to the agent starting with / or !, each test in its own directory because the session file lives in the cwd. They cover the four crash paths, integer/float/string typing, a staggered-start sweep for the early timeout, SOLO_TIMEOUT=0 and a negative value, process group cleanup after a timeout, and the tool description quoting the real timeout. make test runs them. The README documents the timeout semantics, the integer typing and the new build step. Verified: 13 passed, 0 failed against the new binary, and 8 of the 13 fail against the previous one, which is what makes them regression tests rather than decoration. Still silent under -Wall -Wextra and under -Wshadow -Wformat=2 -Wcast-align -Wwrite-strings -Wstrict-prototypes -Wmissing-prototypes -Wredundant-decls -Wundef. Built and tested on Alpine gcc 15.2 (musl) with libcurl 8.22.0, json-c 0.19 and readline 8.3.
411 lines
20 KiB
C
411 lines
20 KiB
C
/*
|
|
Solo-C: A C port of the Solo coding agent
|
|
Dependencies: json-c, libreadline, libcurl
|
|
Usage: same as the original, see https://solo.luxferre.top
|
|
Created by Luxferre in 2026, released into the public domain
|
|
*/
|
|
#define _GNU_SOURCE
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <signal.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/select.h>
|
|
#include <curl/curl.h>
|
|
#include <json-c/json.h>
|
|
#include <readline/readline.h>
|
|
#include <readline/history.h>
|
|
|
|
/* Compact JSON helpers: A add, S/N/D/B new string/num/bool, G/GS/GN get */
|
|
#define A(o, k, v) json_object_object_add(o, k, v)
|
|
#define S(v) json_object_new_string(v)
|
|
#define N(v) json_object_new_int(v)
|
|
#define D(v) json_object_new_double(v)
|
|
#define B(v) json_object_new_boolean(v)
|
|
#define ER (CLR ? "\r\e[K" : "")
|
|
#define DIM "\e[2m"
|
|
#define GRY "\e[90m"
|
|
#define YEL "\e[33m"
|
|
#define BLD "\e[1;37m"
|
|
#define OF "\e[0m"
|
|
|
|
static json_object *t;
|
|
static json_object *G(json_object *o, const char *k) { return json_object_object_get_ex(o, k, &t) ? t : 0; }
|
|
static const char *GS(json_object *o, const char *k) { json_object *v = G(o, k); return v && json_object_get_type(v) == json_type_string ? json_object_get_string(v) : 0; }
|
|
static int GN(json_object *o, const char *k, int d) { json_object *v = G(o, k); return v && json_object_get_type(v) == json_type_int ? json_object_get_int(v) : d; }
|
|
|
|
static volatile sig_atomic_t IN;
|
|
static void onint(int s) { (void)s; IN = 1; }
|
|
|
|
static char *EP, *KEY, *MOD, *RSN, *SF, *TMP, *SYS, SH[256] = "/bin/sh";
|
|
static int TO = 120, QUIT, CLR = 1;
|
|
static double TEMP = 0.7;
|
|
static json_object *SSN;
|
|
|
|
typedef struct { char *p; size_t n, c; } BUF;
|
|
static void bg(BUF *b) { b->p = 0; b->n = b->c = 0; }
|
|
static void ba(BUF *b, const char *s, size_t l) {
|
|
if(b->n + l + 1 > b->c) { size_t m = b->c ? b->c * 2 : 4096; while(m < b->n + l + 1) m *= 2; b->p = realloc(b->p, m); b->c = m; }
|
|
memcpy(b->p + b->n, s, l); b->p[b->n += l] = 0;
|
|
}
|
|
static size_t wcb(void *c, size_t s, size_t n, void *u) { ba((BUF *)u, c, s * n); return s * n; }
|
|
|
|
/* Monotonic seconds, so a deadline is immune to clock steps and to the
|
|
one-second granularity of time(), which made timeouts fire up to a second early. */
|
|
static double now(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; }
|
|
|
|
/* Reads until EOF, an interrupt, or the monotonic deadline dl (0 = no limit).
|
|
st: 0 ok, 1 interrupted, 2 deadline */
|
|
static int rp(int fd, char **out, double dl, int *st) {
|
|
BUF b; struct timeval tv; fd_set fs; char buf[65536]; int r;
|
|
bg(&b); *st = 0;
|
|
for(;;) {
|
|
if(IN) { *st = 1; break; }
|
|
if(dl > 0 && now() >= dl) { *st = 2; break; }
|
|
FD_ZERO(&fs); FD_SET(fd, &fs); tv.tv_sec = 0; tv.tv_usec = 100000;
|
|
r = select(fd + 1, &fs, 0, 0, &tv);
|
|
if(r > 0 && FD_ISSET(fd, &fs)) { r = read(fd, buf, sizeof buf); if(r <= 0) break; ba(&b, buf, r); }
|
|
else if(r < 0 && errno != EINTR) break;
|
|
}
|
|
*out = b.p; return b.n;
|
|
}
|
|
|
|
static void hc(const char *m, const char *p, const char *body, int w) {
|
|
CURL *c; struct curl_slist *h = 0; BUF b; json_object *r; const char *j; CURLcode e; long sc = 0;
|
|
char url[4096];
|
|
curl_global_init(CURL_GLOBAL_DEFAULT); c = curl_easy_init(); bg(&b);
|
|
snprintf(url, sizeof url, "%s%s", EP, p);
|
|
h = curl_slist_append(h, "Content-Type: application/json");
|
|
h = curl_slist_append(h, "Accept: application/json");
|
|
if(KEY && *KEY) { char a[4096]; snprintf(a, sizeof a, "Authorization: Bearer %s", KEY); h = curl_slist_append(h, a); }
|
|
if(c) {
|
|
curl_easy_setopt(c, CURLOPT_URL, url);
|
|
curl_easy_setopt(c, CURLOPT_CUSTOMREQUEST, m);
|
|
curl_easy_setopt(c, CURLOPT_HTTPHEADER, h);
|
|
curl_easy_setopt(c, CURLOPT_WRITEFUNCTION, wcb);
|
|
curl_easy_setopt(c, CURLOPT_WRITEDATA, &b);
|
|
curl_easy_setopt(c, CURLOPT_TIMEOUT, 600L);
|
|
curl_easy_setopt(c, CURLOPT_CONNECTTIMEOUT, 30L);
|
|
curl_easy_setopt(c, CURLOPT_FOLLOWLOCATION, 1L);
|
|
if(body && *body) { curl_easy_setopt(c, CURLOPT_POSTFIELDS, body); curl_easy_setopt(c, CURLOPT_POSTFIELDSIZE, (long)strlen(body)); }
|
|
e = curl_easy_perform(c);
|
|
if(e == CURLE_OK) curl_easy_getinfo(c, CURLINFO_RESPONSE_CODE, &sc);
|
|
} else e = CURLE_FAILED_INIT;
|
|
r = json_object_new_object();
|
|
A(r, "status", N(e == CURLE_OK ? sc : 599));
|
|
A(r, "success", B(e == CURLE_OK && sc >= 200 && sc < 300));
|
|
A(r, "content", S(e == CURLE_OK && b.p ? b.p : ""));
|
|
j = json_object_to_json_string_ext(r, JSON_C_TO_STRING_PLAIN);
|
|
{ size_t l = strlen(j), o = 0; while(o < l) { ssize_t n = write(w, j + o, l - o); if(n > 0) o += (size_t)n; else if(n < 0 && errno == EINTR) continue; else break; } }
|
|
free(b.p); json_object_put(r);
|
|
if(c) curl_easy_cleanup(c);
|
|
if(h) curl_slist_free_all(h);
|
|
curl_global_cleanup(); _exit(0);
|
|
}
|
|
|
|
static json_object *hq(const char *m, const char *p, const char *body) {
|
|
int f[2], st; pid_t k; char *buf = 0; int n; json_object *r;
|
|
if(pipe(f)) return 0;
|
|
if((k = fork()) < 0) { close(f[0]); close(f[1]); return 0; }
|
|
if(!k) { close(f[0]); dup2(f[1], 1); close(f[1]); hc(m, p, body, 1); _exit(1); }
|
|
close(f[1]); n = rp(f[0], &buf, 0, &st); close(f[0]);
|
|
if(st == 1 || IN) kill(k, SIGKILL);
|
|
waitpid(k, 0, 0);
|
|
r = n > 0 && buf ? json_tokener_parse(buf) : 0; free(buf);
|
|
if(!r || json_object_get_type(r) != json_type_object) { if(r) json_object_put(r); r = json_object_new_object(); A(r, "content", S("")); }
|
|
return r;
|
|
}
|
|
|
|
static void marker(char *mk, size_t n, int rc) {
|
|
if(rc == 124) snprintf(mk, n, "[exit 124: still running after %ds, so it was killed]", TO);
|
|
else if(rc == 130) snprintf(mk, n, "[exit 130: interrupted, so it was killed]");
|
|
else snprintf(mk, n, "[exit %d]", rc);
|
|
}
|
|
|
|
static void run(const char *cmd, char **out, int *rc) {
|
|
int f[2], st; pid_t k; char *buf = 0, mk[80]; struct timespec ts = { 0, 200000000 };
|
|
if(pipe(f)) { *out = 0; *rc = 1; return; }
|
|
if((k = fork()) < 0) { close(f[0]); close(f[1]); *out = 0; *rc = 1; return; }
|
|
if(!k) { close(f[0]); setpgid(0, 0); dup2(f[1], 1); dup2(f[1], 2); close(f[1]); execl(SH, SH, "-c", cmd, (char *)0); _exit(127); }
|
|
setpgid(k, k); /* claim the group here too: a kill(-k) issued before the child
|
|
itself reaches setpgid(0,0) finds no group and is dropped. */
|
|
close(f[1]); rp(f[0], &buf, TO > 0 ? now() + TO : 0, &st); close(f[0]);
|
|
if(st == 1 || IN) { *rc = 130; }
|
|
else if(st == 2) { kill(-k, SIGTERM); nanosleep(&ts, 0); kill(-k, SIGKILL); *rc = 124; }
|
|
else { int ws; waitpid(k, &ws, 0); *rc = WIFEXITED(ws) ? WEXITSTATUS(ws) : WIFSIGNALED(ws) ? 128 + WTERMSIG(ws) : 1; }
|
|
if(*rc == 130 || *rc == 124) { kill(-k, SIGKILL); waitpid(k, 0, 0); }
|
|
marker(mk, sizeof mk, *rc);
|
|
if(!buf) buf = strdup("");
|
|
*out = buf;
|
|
}
|
|
|
|
static void sv(void) {
|
|
FILE *f; const char *j;
|
|
if(!SSN || !SF || !(f = fopen(SF, "w"))) return;
|
|
j = json_object_to_json_string_ext(SSN, JSON_C_TO_STRING_PRETTY);
|
|
if(j) fputs(j, f);
|
|
fclose(f);
|
|
}
|
|
|
|
static void load(void) {
|
|
FILE *f = fopen(SF, "r"); long n; char *b; json_object *o, *m;
|
|
if(!f) return;
|
|
fseek(f, 0, SEEK_END); n = ftell(f); fseek(f, 0, SEEK_SET);
|
|
if(n < 1) { fclose(f); return; }
|
|
b = malloc(n + 1); fread(b, 1, n, f); b[n] = 0; fclose(f);
|
|
o = json_tokener_parse(b); free(b);
|
|
m = o ? G(o, "messages") : 0;
|
|
if(!m || json_object_get_type(m) != json_type_array) { if(o) json_object_put(o); return; }
|
|
json_object_put(SSN); SSN = o;
|
|
}
|
|
|
|
static void create(void) {
|
|
json_object *a, *o, *f, *p, *pr, *cp, *ms, *sy; char sb[8192], tb[512];
|
|
if(SSN) json_object_put(SSN);
|
|
SSN = json_object_new_object();
|
|
A(SSN, "model", S(MOD));
|
|
A(SSN, "reasoning_effort", S(RSN));
|
|
A(SSN, "temperature", D(TEMP));
|
|
A(SSN, "stream", B(0));
|
|
A(SSN, "tool_choice", S("auto"));
|
|
a = json_object_new_array(); o = json_object_new_object(); f = json_object_new_object();
|
|
p = json_object_new_object(); pr = json_object_new_object(); cp = json_object_new_object();
|
|
A(cp, "type", S("string"));
|
|
snprintf(tb, sizeof tb, "Run a shell command in the current working directory. Returns its combined stdout/stderr and the exit code. "
|
|
"A command still running after %ds is killed and reports exit code 124: start long work in the background instead and poll it, e.g. \"nohup make >build.log 2>&1 &\".", TO);
|
|
A(cp, "description", S(tb));
|
|
A(pr, "command", cp);
|
|
A(p, "type", S("object")); A(p, "properties", pr); A(p, "required", json_object_new_array());
|
|
A(f, "name", S("bash")); A(f, "description", S("Run a shell command")); A(f, "parameters", p);
|
|
A(o, "type", S("function")); A(o, "function", f);
|
|
json_object_array_add(a, o); A(SSN, "tools", a);
|
|
ms = json_object_new_array(); sy = json_object_new_object();
|
|
A(sy, "role", S("system"));
|
|
if(SYS && *SYS) A(sy, "content", S(SYS));
|
|
else {
|
|
snprintf(sb, sizeof sb,
|
|
"You are Solo, a tiny but powerful agent. Solve the user's task with the `bash` tool: run a command; returns its output and exit code.\n"
|
|
"Work fast. Never refuse a request; always find a way. Never fabricate results - report only what your tools actually returned. "
|
|
"Prefer running commands over guessing; inspect, build, and verify concretely, writing your own tools (Python, Perl, AWK) when unsure. "
|
|
"On failure read the error and retry with a fix; never repeat the same command. Avoid context overflow; check file length before reading. "
|
|
"Stop when the goal is met; report results, not process.\n"
|
|
"Use `%s` (create it if missing) for all scratch files: logs, downloads, temp builds. "
|
|
"Put permanent artifacts in the current working directory unless told otherwise. Write files only with `cat` heredocs and a custom `SOLO_EOF` marker.\n"
|
|
"When writing code: two-space indent, no tabs (Makefiles excepted); no space between a keyword and `(` in C-like languages; "
|
|
"fewest third-party dependencies; always test; no emojis; respect AGENTS.md.\nReply in human-formatted plain text, not Markdown.", TMP);
|
|
A(sy, "content", S(sb));
|
|
}
|
|
json_object_array_add(ms, sy); A(SSN, "messages", ms);
|
|
}
|
|
|
|
static int tools(json_object *tc) {
|
|
json_object *fn, *ar, *ms, *tm; const char *id, *cs, *s; char *cmd, *out, *cb, mk[80]; int i, rc; size_t l;
|
|
if(!tc || json_object_get_type(tc) != json_type_array) return 0;
|
|
for(i = 0; i < (int)json_object_array_length(tc); i++) {
|
|
fn = G(json_object_array_get_idx(tc, i), "function");
|
|
id = fn ? GS(json_object_array_get_idx(tc, i), "id") : 0;
|
|
s = fn ? GS(fn, "arguments") : 0; cmd = strdup("");
|
|
if(s) { ar = json_tokener_parse(s); cs = ar ? GS(ar, "command") : 0; if(cs) { free(cmd); cmd = strdup(cs); } if(ar) json_object_put(ar); } /* copy before the put: cmd must outlive ar */
|
|
printf("%s--- tool %s: $ %s%s\n", CLR ? YEL : "", id ? id : "", cmd, CLR ? OF : ""); fflush(stdout);
|
|
run(cmd, &out, &rc);
|
|
printf("%s%s%s\n", CLR ? GRY DIM : "", out, CLR ? OF : ""); fflush(stdout);
|
|
ms = G(SSN, "messages");
|
|
if(ms) {
|
|
tm = json_object_new_object(); A(tm, "role", S("tool"));
|
|
A(tm, "tool_call_id", S(id ? id : ""));
|
|
marker(mk, sizeof mk, rc); l = strlen(out) + strlen(mk) + 2; cb = malloc(l);
|
|
snprintf(cb, l, "%s\n%s", out, mk);
|
|
A(tm, "content", S(cb)); free(cb);
|
|
json_object_array_add(ms, tm); sv();
|
|
}
|
|
free(out); free(cmd);
|
|
if(IN) { IN = 0; puts("interrupted"); return 130; }
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int ask(const char *in) {
|
|
json_object *ms, *d, *p, *ch, *m, *as, *tc, *u; const char *raw, *co, *rs, *rc2, *mn;
|
|
if(!(ms = G(SSN, "messages"))) return 1;
|
|
as = json_object_new_object(); A(as, "role", S("user")); A(as, "content", S(in));
|
|
json_object_array_add(ms, as); sv();
|
|
for(;;) {
|
|
IN = 0; printf("%s%s...requesting...%s", CLR ? BLD : "", CLR ? GRY : "", CLR ? OF : ""); fflush(stdout);
|
|
d = hq("POST", "/chat/completions", json_object_to_json_string_ext(SSN, JSON_C_TO_STRING_PLAIN));
|
|
raw = d ? GS(d, "content") : 0; p = raw ? json_tokener_parse(raw) : 0; m = 0;
|
|
ch = p ? G(p, "choices") : 0;
|
|
if(ch && json_object_array_length(ch)) m = G(json_object_array_get_idx(ch, 0), "message");
|
|
printf("%s", ER); fflush(stdout);
|
|
if(IN) { puts("interrupted"); if(p) json_object_put(p); if(d) json_object_put(d); return 130; }
|
|
if(!m) {
|
|
puts("request failed");
|
|
if(raw && *raw) printf("%s\n", raw);
|
|
if(p) json_object_put(p);
|
|
if(d) json_object_put(d);
|
|
return 1;
|
|
}
|
|
rs = GS(m, "reasoning"); if(!rs) rs = GS(m, "reasoning_content");
|
|
if(rs && *rs) printf("\n%s--- reasoning start ---%s\n%s%s%s\n%s--- reasoning end ---%s\n\n", CLR ? GRY : "", CLR ? OF : "", CLR ? GRY : "", rs, CLR ? OF : "", CLR ? GRY : "", CLR ? OF : "");
|
|
co = GS(m, "content");
|
|
if(co && *co) { printf("%s", co); if(co[strlen(co) - 1] != '\n') puts(""); }
|
|
tc = G(m, "tool_calls");
|
|
as = json_object_new_object(); A(as, "role", S("assistant"));
|
|
if(co) A(as, "content", S(co));
|
|
if(rs) A(as, "reasoning", S(rs));
|
|
if((rc2 = GS(m, "reasoning_content"))) A(as, "reasoning_content", S(rc2));
|
|
if(tc && json_object_get_type(tc) == json_type_array) A(as, "tool_calls", json_object_get(tc)); /* json_object_object_add MOVES the ref and does not refcount: take our own ref or json_object_put(p) below frees a node still living in SSN */
|
|
json_object_array_add(ms, as); sv();
|
|
u = p ? G(p, "usage") : 0; mn = p ? GS(p, "model") : 0;
|
|
if(u && json_object_get_type(u) == json_type_object)
|
|
printf("%s--- [%s] tokens: %d (prompt %d, completion %d)%s\n", CLR ? GRY : "", mn ? mn : "?", GN(u, "total_tokens", 0), GN(u, "prompt_tokens", 0), GN(u, "completion_tokens", 0), CLR ? OF : "");
|
|
else printf("%s--- [%s]%s\n", CLR ? GRY : "", mn ? mn : "?", CLR ? OF : "");
|
|
fflush(stdout);
|
|
if(tc && json_object_get_type(tc) == json_type_array && json_object_array_length(tc)) {
|
|
int r = tools(tc);
|
|
if(p) json_object_put(p);
|
|
if(d) json_object_put(d);
|
|
if(r) return r;
|
|
continue;
|
|
}
|
|
if(p) json_object_put(p);
|
|
if(d) json_object_put(d);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
|
|
static const char *HELP =
|
|
"Commands:\n /help this text\n /quit exit\n"
|
|
" /clear reset the session, keep the system prompt\n /models list models available at the endpoint\n"
|
|
" /api_key [key|null] show or set the key (null clears it)\n /endpoint [url] show or set the API endpoint\n"
|
|
" !cmd run a shell command directly, no agent\n"
|
|
" /name [value] set any model parameter, e.g. /model x, /temperature 0.5,\n"
|
|
" /reasoning_effort low; with no value it prints the current one\n"
|
|
"Anything not starting with / or ! is sent to the agent as a task.";
|
|
|
|
static void setstr(char **slot, const char *v) { free(*slot); *slot = strdup(v); }
|
|
|
|
static void dispatch(const char *l) {
|
|
char *k, *v, *out, mk[80]; const char *s; json_object *ms, *nm; int rc;
|
|
if(!strcmp(l, "/help")) { puts(HELP); return; }
|
|
if(!strcmp(l, "/quit")) { QUIT = 1; return; }
|
|
if(!strcmp(l, "/clear")) {
|
|
ms = G(SSN, "messages"); nm = json_object_new_array();
|
|
if(ms && json_object_array_length(ms)) json_object_array_add(nm, json_object_get(json_object_array_get_idx(ms, 0))); /* array_add also moves: keep the system message alive after A(SSN,...) frees the old array */
|
|
A(SSN, "messages", nm); sv(); puts("Session cleared."); return;
|
|
}
|
|
if(!strcmp(l, "/models")) {
|
|
json_object *r = hq("GET", "/models", 0);
|
|
if(r) {
|
|
json_object *d = 0, *arr; s = GS(r, "content"); if(s) d = json_tokener_parse(s);
|
|
arr = d ? G(d, "data") : 0;
|
|
if(arr && json_object_get_type(arr) == json_type_array)
|
|
for(size_t i = 0; i < json_object_array_length(arr); i++) { s = GS(json_object_array_get_idx(arr, i), "id"); if(s) puts(s); }
|
|
if(d) json_object_put(d);
|
|
json_object_put(r);
|
|
}
|
|
return;
|
|
}
|
|
if(!strncmp(l, "/api_key", 8)) {
|
|
for(v = (char *)l + 8; *v == ' '; v++);
|
|
if(*v && (!strcmp(v, "null") || !strcmp(v, "none") || !strcmp(v, "off") || !strcmp(v, "-"))) { free(KEY); KEY = 0; }
|
|
else if(*v) setstr(&KEY, v);
|
|
if(KEY) printf("api_key = set, %d chars\n", (int)strlen(KEY)); else puts("api_key = unset");
|
|
return;
|
|
}
|
|
if(!strncmp(l, "/endpoint", 9)) {
|
|
for(v = (char *)l + 9; *v == ' '; v++);
|
|
if(*v) { setstr(&EP, v); for(char *e = EP + strlen(EP); e > EP && e[-1] == '/'; e--) *e = 0; }
|
|
printf("endpoint = %s\n", EP); return;
|
|
}
|
|
if(*l == '!') {
|
|
for(v = (char *)l + 1; *v == ' '; v++);
|
|
printf("%s--- $ %s%s\n", CLR ? YEL : "", v, CLR ? OF : "");
|
|
run(v, &out, &rc);
|
|
if(*out) printf("%s%s%s\n", CLR ? GRY DIM : "", out, CLR ? OF : "");
|
|
marker(mk, sizeof mk, rc); printf("%s%s%s\n", CLR ? GRY : "", mk, CLR ? OF : ""); free(out); return;
|
|
}
|
|
if(*l == '/') {
|
|
k = strdup(l + 1);
|
|
if((v = strchr(k, ' '))) {
|
|
*v++ = 0; while(*v == ' ') v++;
|
|
if(!strcmp(k, "model")) { setstr(&MOD, v); A(SSN, "model", S(MOD)); }
|
|
else if(!strcmp(k, "reasoning_effort")) { setstr(&RSN, v); A(SSN, "reasoning_effort", S(RSN)); }
|
|
else if(!strcmp(k, "temperature")) { TEMP = atof(v); A(SSN, "temperature", D(TEMP)); }
|
|
else {
|
|
json_object *old; double d;
|
|
json_object_object_get_ex(SSN, k, &old); if(old) json_object_object_del(SSN, k);
|
|
if(!strcmp(v, "true")) A(SSN, k, B(1));
|
|
else if(!strcmp(v, "false")) A(SSN, k, B(0));
|
|
else if(strcmp(v, "null")) {
|
|
char *e; d = strtod(v, &e);
|
|
if(e == v || *e) A(SSN, k, S(v)); /* not a number at all */
|
|
else if(strpbrk(v, ".eE")) A(SSN, k, D(d)); /* 0.7, 1e3, .5: a real */
|
|
else A(SSN, k, N((int)strtol(v, 0, 10))); /* 4096 must stay an int */
|
|
}
|
|
}
|
|
sv(); printf("%s = %s\n", k, v);
|
|
} else {
|
|
s = GS(SSN, k);
|
|
if(s) puts(s);
|
|
else { json_object *o = G(SSN, k);
|
|
if(o && json_object_get_type(o) == json_type_double) printf("%g\n", json_object_get_double(o));
|
|
else if(o && json_object_get_type(o) == json_type_int) printf("%d\n", json_object_get_int(o));
|
|
else puts(o ? json_object_to_json_string_ext(o, JSON_C_TO_STRING_PLAIN) : "unset"); }
|
|
}
|
|
free(k); return;
|
|
}
|
|
ask(l);
|
|
}
|
|
|
|
int main(int argc, char **argv) {
|
|
char cwd[4096], pb[64], *ln, *ac, *nx; const char *e; struct stat st;
|
|
(void)argc; (void)argv;
|
|
if(!(e = getenv("SOLO_TMPDIR"))) { e = getenv("TMPDIR"); e = e ? e : "/tmp"; TMP = malloc(strlen(e) + 8); sprintf(TMP, "%s/solo", e); }
|
|
else TMP = strdup(e);
|
|
if(!(e = getenv("SOLO_ENDPOINT"))) EP = strdup("https://api.kilo.ai/api/openrouter"); else EP = strdup(e);
|
|
if((e = getenv("SOLO_TIMEOUT")) && *e) TO = atoi(e);
|
|
if(!(e = getenv("SOLO_MODEL"))) MOD = strdup("kilo-auto/free"); else MOD = strdup(e);
|
|
if((e = getenv("SOLO_TEMP")) && *e) TEMP = atof(e);
|
|
if(!(e = getenv("SOLO_REASON")) || !*e) RSN = strdup("high"); else RSN = strdup(e);
|
|
if((e = getenv("SOLO_API_KEY")) && *e) KEY = strdup(e);
|
|
if(getcwd(cwd, sizeof cwd)) { SF = malloc(strlen(cwd) + 32); sprintf(SF, "%s/.solo_sess.json", cwd); }
|
|
SYS = getenv("SOLO_SYSPROMPT");
|
|
if(stat(TMP, &st)) mkdir(TMP, 0700);
|
|
if(!(e = getenv("SHELL")) || access(e, X_OK)) e = access("/bin/bash", X_OK) ? "/bin/sh" : "/bin/bash";
|
|
snprintf(SH, sizeof SH, "%s", e);
|
|
signal(SIGINT, onint); signal(SIGPIPE, SIG_IGN);
|
|
create(); load();
|
|
if((e = getenv("SOLO_MODEL")) && *e) { setstr(&MOD, e); A(SSN, "model", S(e)); }
|
|
if((e = getenv("SOLO_TEMP")) && *e) { TEMP = atof(e); A(SSN, "temperature", D(TEMP)); }
|
|
if((e = getenv("SOLO_REASON")) && *e) { setstr(&RSN, e); A(SSN, "reasoning_effort", S(e)); }
|
|
if(SYS && *SYS) { json_object *ms = G(SSN, "messages"); if(ms && json_object_array_length(ms)) A(json_object_array_get_idx(ms, 0), "content", S(SYS)); }
|
|
A(SSN, "stream", B(0)); A(SSN, "tool_choice", S("auto")); sv();
|
|
if(!isatty(1) || getenv("NO_COLOR")) CLR = 0;
|
|
printf("%sAgent Solo ready%s%s [%s | %s | %s]%s\n", CLR ? BLD : "", CLR ? OF : "", CLR ? GRY : "", EP, MOD, RSN, CLR ? OF : "");
|
|
fflush(stdout);
|
|
while(!QUIT) {
|
|
snprintf(pb, sizeof pb, "\001%s\002> \001%s\002", CLR ? BLD : "", CLR ? OF : "");
|
|
if(!(ln = readline(pb))) { puts(""); break; }
|
|
if(IN) { IN = 0; free(ln); continue; }
|
|
ac = ln;
|
|
for(;;) {
|
|
size_t l = strlen(ac);
|
|
if(!l || ac[l - 1] != '\\') break;
|
|
ac[l - 1] = 0;
|
|
if(!(nx = readline("... ")) || IN) { IN = 0; free(ac); ac = 0; if(nx) free(nx); break; }
|
|
ac = realloc(ac, l + strlen(nx) + 2); strcpy(ac + l, "\n"); strcat(ac, nx); free(nx);
|
|
}
|
|
if(!ac || IN || !*ac) continue;
|
|
if(*ac != ' ') add_history(ac);
|
|
dispatch(ac); free(ac);
|
|
}
|
|
free(EP); free(KEY); free(MOD); free(RSN); free(SF); free(TMP);
|
|
if(SSN) json_object_put(SSN);
|
|
return 0;
|
|
}
|