2026-10-03 09:15:02 +03:00
/*
Solo-C: A C port of the Solo coding agent
Dependencies: json-c, libreadline, libcurl
2026-10-04 17:00:39 +03:00
Usage: solo [prompt] - no prompt opens the REPL, a prompt runs it
non-interactively: one task, then exit
2026-10-03 09:15:02 +03:00
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>
2026-10-04 17:00:39 +03:00
#include <ctype.h>
2026-10-03 09:15:02 +03:00
#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 ; }
2026-10-04 21:32:50 +03:00
/* readline calls this from its input loop, in a safe context, after it has
dealt with a signal caught while it was reading a line. A Ctrl-C only
redraws the prompt: the half-typed text stays in the buffer, so clear it
here, the way bash does. The flag is left set on purpose; the prompt loop
clears it, so a Ctrl-C during a command still aborts the turn, and one
during a multi-line task still abandons it. */
static int ci_hook ( void ) {
if ( IN ) { rl_replace_line ( "" , 1 ); rl_redisplay (); }
return 0 ;
}
2026-10-03 09:15:02 +03:00
2026-10-04 17:00:39 +03:00
static char * EP , * KEY , * MOD , * RSN , * SF , * TMP , * SYS , * SELF , SH [ 256 ] = "/bin/sh" ;
2026-10-03 09:15:02 +03:00
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 ; }
2026-10-04 06:06:06 +00:00
/* 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 */
2026-10-03 09:15:02 +03:00
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 ; }
2026-10-04 06:06:06 +00:00
if ( dl > 0 && now () >= dl ) { * st = 2 ; break ; }
2026-10-03 09:15:02 +03:00
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 );
2026-10-04 06:06:06 +00:00
{ 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 );
2026-10-03 09:15:02 +03:00
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 ); }
2026-10-04 06:06:06 +00:00
close ( f [ 1 ]); n = rp ( f [ 0 ], & buf , 0 , & st ); close ( f [ 0 ]);
2026-10-03 09:15:02 +03:00
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 ); }
2026-10-04 06:06:06 +00:00
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 ]);
2026-10-03 09:15:02 +03:00
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 ;
}
2026-10-05 19:05:33 +03:00
/* Builds the single tool the agent exposes and stores it in the session. Kept
apart from create() because the description quotes the timeout, so it has to
be rebuilt after load(): a session saved under one SOLO_TIMEOUT would
otherwise keep telling the model the old limit while run() enforced the new
one. */
static void mkschema ( void ) {
json_object * a , * o , * f , * p , * pr , * cp , * rq ; char tb [ 512 ];
2026-10-03 09:15:02 +03:00
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" ));
2026-10-04 06:06:06 +00:00
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 ));
2026-10-03 09:15:02 +03:00
A ( pr , "command" , cp );
2026-10-05 19:05:33 +03:00
rq = json_object_new_array (); json_object_array_add ( rq , S ( "command" )); /* command is required, so an endpoint cannot quietly send an empty call */
A ( p , "type" , S ( "object" )); A ( p , "properties" , pr ); A ( p , "required" , rq );
2026-10-03 09:15:02 +03:00
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 );
2026-10-05 19:05:33 +03:00
}
static void create ( void ) {
json_object * ms , * sy ; char * sb ; size_t nb ;
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" ));
mkschema ();
2026-10-03 09:15:02 +03:00
ms = json_object_new_array (); sy = json_object_new_object ();
A ( sy , "role" , S ( "system" ));
if ( SYS && * SYS ) A ( sy , "content" , S ( SYS ));
else {
2026-10-04 17:00:39 +03:00
nb = 4096 + strlen ( TMP ) + strlen ( SELF ) + 3 * strlen ( EP ); /* the prompt expands those paths in */
sb = malloc ( nb );
snprintf ( sb , nb ,
2026-10-03 09:15:02 +03:00
"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 "
2026-10-04 17:00:39 +03:00
"A multi-stage task can be split into subagents: copy the current session file `.solo_sess.json` into a fresh directory under `%s`, "
"work from there and run `SOLO_ENDPOINT=%s %s 'the subtask, stated in full'`, or use your own copy of this binary under `%s` when that path is not a file. "
"The copy of the session carries the model, tools and system prompt, so a subagent starts from this same setup and then appends its own turns to that file only: "
"its answer, progress and token stats arrive on stdout while the banner goes to stderr, so launch independent subtasks in parallel with `&` "
"and their tool output never lands in your context. "
"Put SOLO_MODEL, SOLO_REASON, SOLO_TEMP or SOLO_SYSPROMPT in front of SOLO_ENDPOINT to give a subagent a different setup. \n "
2026-10-03 09:15:02 +03:00
"When writing code: two-space indent, no tabs (Makefiles excepted); no space between a keyword and `(` in C-like languages; "
2026-10-04 17:00:39 +03:00
"fewest third-party dependencies; always test; no emojis; respect AGENTS.md. \n Reply in human-formatted plain text, not Markdown." ,
TMP , TMP , EP , SELF , TMP );
2026-10-03 09:15:02 +03:00
A ( sy , "content" , S ( sb ));
2026-10-04 17:00:39 +03:00
free ( sb );
2026-10-03 09:15:02 +03:00
}
json_object_array_add ( ms , sy ); A ( SSN , "messages" , ms );
}
2026-10-05 19:05:33 +03:00
/* ran is set to the number of commands actually executed, so the caller can see
a round that ran nothing at all and stop a peer that keeps sending empty
tool calls instead of looping on it forever. */
static int tools ( json_object * tc , int * ran ) {
2026-10-03 09:15:02 +03:00
json_object * fn , * ar , * ms , * tm ; const char * id , * cs , * s ; char * cmd , * out , * cb , mk [ 80 ]; int i , rc ; size_t l ;
2026-10-05 19:05:33 +03:00
* ran = 0 ;
2026-10-03 09:15:02 +03:00
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 ;
2026-10-05 19:05:33 +03:00
s = fn ? GS ( fn , "arguments" ) : 0 ; cmd = 0 ;
if ( s ) { ar = json_tokener_parse ( s ); cs = ar ? GS ( ar , "command" ) : 0 ; if ( cs ) cmd = strdup ( cs ); if ( ar ) json_object_put ( ar ); } /* copy before the put: cmd must outlive ar */
if ( cmd ) { char * q = cmd ; while ( * q && isspace (( unsigned char ) * q )) q ++ ; if ( !* q ) { free ( cmd ); cmd = 0 ; } } /* a blank command is no command */
if ( ! cmd ) {
printf ( "%s--- tool %s: no command in the arguments%s \n " , CLR ? YEL : "" , id ? id : "" , CLR ? OF : "" );
out = strdup ( "error: this tool call carried no usable \" command \" argument, so nothing ran. Call bash again with a command, e.g. { \" command \" : \" ls -l \" }." );
rc = 1 ;
} else {
printf ( "%s--- tool %s: $ %s%s \n " , CLR ? YEL : "" , id ? id : "" , cmd , CLR ? OF : "" ); fflush ( stdout );
run ( cmd , & out , & rc );
( * ran ) ++ ;
}
2026-10-03 09:15:02 +03:00
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 ;
2026-10-05 19:05:33 +03:00
int empty = 0 ;
2026-10-03 09:15:02 +03:00
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 ();
if ( tc && json_object_get_type ( tc ) == json_type_array && json_object_array_length ( tc )) {
2026-10-05 19:05:33 +03:00
int r , ran ; /* the usage line waits for the turn that ends the loop, so a tool run is not chopped up by stats */
r = tools ( tc , & ran );
2026-10-03 09:15:02 +03:00
if ( p ) json_object_put ( p );
if ( d ) json_object_put ( d );
if ( r ) return r ;
2026-10-05 19:05:33 +03:00
if ( ran ) empty = 0 ;
else if ( ++ empty >= 3 ) { /* three rounds in a row with nothing to run: feed the errors back was useless, so give up rather than spin */
puts ( "no command in three consecutive tool rounds, stopping the turn" );
return 1 ;
}
2026-10-03 09:15:02 +03:00
continue ;
}
2026-10-04 17:00:39 +03:00
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 );
2026-10-03 09:15:02 +03:00
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 "
2026-10-04 17:00:39 +03:00
"Anything not starting with / or ! is sent to the agent as a task. \n "
"A prompt as an argument skips the REPL: ask once, then exit, for scripts and pipes." ;
2026-10-03 09:15:02 +03:00
static void setstr ( char ** slot , const char * v ) { free ( * slot ); * slot = strdup ( v ); }
2026-10-05 19:05:33 +03:00
static void rtrim ( char * s ) { size_t n = strlen ( s ); while ( n && isspace (( unsigned char ) s [ n - 1 ])) s [ -- n ] = 0 ; }
/* Runs the command lines (a leading / or !) of one already right-trimmed line.
The trim happens in dispatch(), because "/help " and "/quit " must stay
commands rather than being read as a parameter named help or quit. */
static int command ( char * l ) {
2026-10-03 09:15:02 +03:00
char * k , * v , * out , mk [ 80 ]; const char * s ; json_object * ms , * nm ; int rc ;
2026-10-04 17:00:39 +03:00
if ( ! strcmp ( l , "/help" )) { puts ( HELP ); return 0 ; }
if ( ! strcmp ( l , "/quit" )) { QUIT = 1 ; return 0 ; }
2026-10-03 09:15:02 +03:00
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 */
2026-10-04 17:00:39 +03:00
A ( SSN , "messages" , nm ); sv (); puts ( "Session cleared." ); return 0 ;
2026-10-03 09:15:02 +03:00
}
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 );
}
2026-10-04 17:00:39 +03:00
return 0 ;
2026-10-03 09:15:02 +03:00
}
if ( ! strncmp ( l , "/api_key" , 8 )) {
2026-10-05 19:05:33 +03:00
for ( v = l + 8 ; * v == ' ' ; v ++ );
2026-10-03 09:15:02 +03:00
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" );
2026-10-04 17:00:39 +03:00
return 0 ;
2026-10-03 09:15:02 +03:00
}
if ( ! strncmp ( l , "/endpoint" , 9 )) {
2026-10-05 19:05:33 +03:00
for ( v = l + 9 ; * v == ' ' ; v ++ );
2026-10-03 09:15:02 +03:00
if ( * v ) { setstr ( & EP , v ); for ( char * e = EP + strlen ( EP ); e > EP && e [ - 1 ] == '/' ; e -- ) * e = 0 ; }
2026-10-04 17:00:39 +03:00
printf ( "endpoint = %s \n " , EP ); return 0 ;
2026-10-03 09:15:02 +03:00
}
if ( * l == '!' ) {
2026-10-05 19:05:33 +03:00
for ( v = l + 1 ; * v == ' ' ; v ++ );
2026-10-03 09:15:02 +03:00
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 : "" );
2026-10-04 17:00:39 +03:00
marker ( mk , sizeof mk , rc ); printf ( "%s%s%s \n " , CLR ? GRY : "" , mk , CLR ? OF : "" ); free ( out ); return rc ;
2026-10-03 09:15:02 +03:00
}
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 ));
2026-10-04 06:06:06 +00:00
else if ( strcmp ( v , "null" )) {
2026-10-03 09:15:02 +03:00
char * e ; d = strtod ( v , & e );
2026-10-04 06:06:06 +00:00
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 */
2026-10-03 09:15:02 +03:00
}
}
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" ); }
}
2026-10-04 17:00:39 +03:00
free ( k ); return 0 ;
2026-10-03 09:15:02 +03:00
}
2026-10-05 19:05:33 +03:00
return 0 ;
}
/* Runs one line and returns the exit status for it. A task keeps its exact
text, including a leading space that keeps it out of the history. */
static int dispatch ( const char * l ) {
char * cl ; int rc ;
if ( * l != '/' && * l != '!' ) return ask ( l );
cl = strdup ( l ); rtrim ( cl ); rc = command ( cl ); free ( cl );
return rc ;
2026-10-03 09:15:02 +03:00
}
int main ( int argc , char ** argv ) {
2026-10-04 17:00:39 +03:00
char cwd [ 4096 ], pb [ 64 ], sp [ 4096 ], * ln , * ac , * nx , * task ; const char * e ; struct stat st ;
FILE * BN ; size_t jl = 0 ; ssize_t sl ; int i , one = 0 , rc ;
setvbuf ( stdout , 0 , _IOLBF , 0 ); /* line buffered, so a piped answer arrives while it is printed */
for ( i = 1 ; i < argc ; i ++ ) jl += strlen ( argv [ i ]) + 1 ;
task = malloc ( jl + 1 ); task [ 0 ] = 0 ; /* every word becomes part of one task, so quoting is optional */
for ( i = 1 ; i < argc ; i ++ ) { if ( i > 1 ) strcat ( task , " " ); strcat ( task , argv [ i ]); }
for ( char * q = task ; * q ; q ++ ) if ( ! isspace (( unsigned char ) * q )) { one = 1 ; break ; }
sl = readlink ( "/proc/self/exe" , sp , sizeof sp - 1 ); /* our own path, so a subagent can be re-launched */
if ( sl > 0 ) { sp [ sl ] = 0 ; SELF = strdup ( sp ); }
else SELF = ( argc > 0 && argv [ 0 ]) ? realpath ( argv [ 0 ], 0 ) : 0 ;
if ( ! SELF ) SELF = strdup (( argc > 0 && argv [ 0 ]) ? argv [ 0 ] : "solo" );
2026-10-03 09:15:02 +03:00
if ( ! ( e = getenv ( "SOLO_TMPDIR" ))) { e = getenv ( "TMPDIR" ); e = e ? e : "/tmp" ; TMP = malloc ( strlen ( e ) + 8 ); sprintf ( TMP , "%s/solo" , e ); }
2026-10-04 06:06:06 +00:00
else TMP = strdup ( e );
if ( ! ( e = getenv ( "SOLO_ENDPOINT" ))) EP = strdup ( "https://api.kilo.ai/api/openrouter" ); else EP = strdup ( e );
2026-10-04 17:00:39 +03:00
for ( char * z = EP + strlen ( EP ); z > EP && z [ - 1 ] == '/' ; z -- ) * z = 0 ;
2026-10-03 09:15:02 +03:00
if (( e = getenv ( "SOLO_TIMEOUT" )) && * e ) TO = atoi ( e );
2026-10-04 06:06:06 +00:00
if ( ! ( e = getenv ( "SOLO_MODEL" ))) MOD = strdup ( "kilo-auto/free" ); else MOD = strdup ( e );
2026-10-03 09:15:02 +03:00
if (( e = getenv ( "SOLO_TEMP" )) && * e ) TEMP = atof ( e );
2026-10-04 06:06:06 +00:00
if ( ! ( e = getenv ( "SOLO_REASON" )) || !* e ) RSN = strdup ( "high" ); else RSN = strdup ( e );
2026-10-03 09:15:02 +03:00
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 );
2026-10-04 21:32:50 +03:00
rl_signal_event_hook = ci_hook ;
2026-10-05 19:05:33 +03:00
create (); load (); mkschema (); /* a restored session carries the schema of the run that saved it: rebuild it, so the timeout in the description matches the one run() enforces */
2026-10-04 06:06:06 +00:00
if (( e = getenv ( "SOLO_MODEL" )) && * e ) { setstr ( & MOD , e ); A ( SSN , "model" , S ( e )); }
2026-10-03 09:15:02 +03:00
if (( e = getenv ( "SOLO_TEMP" )) && * e ) { TEMP = atof ( e ); A ( SSN , "temperature" , D ( TEMP )); }
2026-10-04 06:06:06 +00:00
if (( e = getenv ( "SOLO_REASON" )) && * e ) { setstr ( & RSN , e ); A ( SSN , "reasoning_effort" , S ( e )); }
2026-10-03 09:15:02 +03:00
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 ;
2026-10-04 17:00:39 +03:00
BN = one ? stderr : stdout ; /* one-shot: the banner goes to stderr, so stdout holds the answer alone */
fprintf ( BN , "%sAgent Solo ready%s%s [%s | %s | %s]%s \n " , CLR ? BLD : "" , CLR ? OF : "" , CLR ? GRY : "" , EP , MOD , RSN , CLR ? OF : "" );
fflush ( BN );
if ( one ) { IN = 0 ; rc = dispatch ( task ); free ( task ); return rc ; } /* one task, then exit with its status */
free ( task );
2026-10-03 09:15:02 +03:00
while ( ! QUIT ) {
2026-10-03 19:18:12 +03:00
snprintf ( pb , sizeof pb , " \001 %s \002 > \001 %s \002 " , CLR ? BLD : "" , CLR ? OF : "" );
2026-10-04 21:32:50 +03:00
IN = 0 ; /* a Ctrl-C during a command must not reach the line about to be typed */
2026-10-03 09:15:02 +03:00
if ( ! ( ln = readline ( pb ))) { puts ( "" ); break ; }
2026-10-04 21:32:50 +03:00
IN = 0 ; /* a Ctrl-C during the line only cleared that line, not this turn */
2026-10-03 09:15:02 +03:00
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 );
2026-10-04 17:00:39 +03:00
( void ) dispatch ( ac ); free ( ac );
2026-10-03 09:15:02 +03:00
}
free ( EP ); free ( KEY ); free ( MOD ); free ( RSN ); free ( SF ); free ( TMP );
if ( SSN ) json_object_put ( SSN );
return 0 ;
}