/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include /* 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; } /* 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 && (double)time(0) >= 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); write(w, j, strlen(j)); 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, time(0) + 1e9, &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); } close(f[1]); rp(f[0], &buf, time(0) + TO, &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]; 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")); A(cp, "description", S("Run a shell command in the current working directory. Returns its combined stdout/stderr and the exit code. " "A command still running after 120s 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 &\".")); 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") && (d = strtod(v, 0), 1)) { char *e; d = strtod(v, &e); if(!*e && e != v) A(SSN, k, D(d)); else A(SSN, k, S(v)); } } 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 = (char *)e; if(!(e = getenv("SOLO_ENDPOINT"))) EP = strdup("https://api.kilo.ai/api/openrouter"); else EP = (char *)e; if((e = getenv("SOLO_TIMEOUT")) && *e) TO = atoi(e); if(!(e = getenv("SOLO_MODEL"))) MOD = strdup("kilo-auto/free"); else MOD = (char *)e; if((e = getenv("SOLO_TEMP")) && *e) TEMP = atof(e); if(!(e = getenv("SOLO_REASON")) || !*e) RSN = strdup("high"); else RSN = (char *)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) { MOD = strdup(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) { RSN = strdup(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, "%s> %s", 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; }