2026-10-14 00:01:03 +03:00
/* main.cpp — entry point, agentic loop, REPL, command dispatch. */
#include "bantam.hpp"
#include <array>
#include <cctype>
2026-10-14 08:36:39 +03:00
#include <cerrno>
2026-10-14 00:01:03 +03:00
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <curl/curl.h>
#include <dirent.h>
#include <span>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
/* ----------------------------- usage / pct ---------------------------- */
static int ctx_tokens ( Usage & u ) {
if ( u . context_tokens > u . prompt_tokens ) return u . context_tokens ;
return u . prompt_tokens ;
}
static std :: string format_usage ( Usage & u , int cw ) {
int base = ctx_tokens ( u );
if ( cw <= 0 ) cw = 262144 ;
double pct = ( double ) base * 100.0 / ( double ) cw ;
std :: string s ;
if ( ! u . model . empty ()) str_appendf ( s , "[%s: " , u . model . c_str ());
else s += "[" ;
if ( u . cached_tokens > 0 ) {
int uncached = u . prompt_tokens - u . cached_tokens ;
if ( uncached < 0 ) uncached = 0 ;
str_appendf ( s , "%d prompt (%d cached, %d uncached) + %d completion | context: %d/%d (%.1f%%)]" ,
u . prompt_tokens , u . cached_tokens , uncached , u . completion_tokens , base , cw , pct );
} else {
str_appendf ( s , "%d prompt + %d completion | context: %d/%d (%.1f%%)]" ,
u . prompt_tokens , u . completion_tokens , base , cw , pct );
}
return s ;
}
static double context_pct ( Usage & u , int cw ) {
if ( cw <= 0 ) cw = 262144 ;
return ( double ) ctx_tokens ( u ) * 100.0 / ( double ) cw ;
}
/* ----------------------------- invalid-assistant detection ------------- */
static bool is_invalid_assistant_err ( const char * err ) {
if ( ! err ) return false ;
std :: string low = err ;
for ( char & c : low ) c = ( char ) std :: tolower (( unsigned char ) c );
return low . find ( "invalid assistant message" ) != std :: string :: npos
|| low . find ( "content or tool_calls must be set" ) != std :: string :: npos
|| low . find ( "tool_calls must be set" ) != std :: string :: npos
|| low . find ( "content must be set" ) != std :: string :: npos ;
}
/* ----------------------------- sanitize args -------------------------- */
static std :: string sanitize_arguments ( std :: string_view args ) {
std :: string f = filter_text ( args );
2026-10-14 08:36:39 +03:00
Json o ;
if ( Json :: parse ( f , o ) && o . is_object ()) return f ;
Json w = Json :: object ();
2026-10-14 00:01:03 +03:00
w [ "invalid_raw" ] = f ;
2026-10-14 08:36:39 +03:00
return w . dump ();
2026-10-14 00:01:03 +03:00
}
/* ----------------------------- system prompt -------------------------- */
static const char * default_system_prompt =
"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 "
"- read: read a file or directory from the local filesystem; returns numbered lines or directory entries. \n "
2026-10-14 10:58:08 +03:00
"- 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 "
2026-10-14 00:01:03 +03:00
"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 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 "
"- Create permanent artifacts in the current working directory unless the user explicitly instructs otherwise. \n "
"- Tool results larger than $MAX_TOOL_RES bytes are not inserted into the context in full: they are saved under $TMPDIR/bantam/toolres and replaced by a short note giving the file path and its total byte length. Read such a file with the read tool, paging through it with line offsets (using offset and limit) instead of loading it all at once. \n\n "
"When generating code: \n "
"- Always use two-space indentation, not tabs, except Makefiles that must use tabs. \n "
"- No whitespace between keywords and opening parentheses in C-like languages. \n "
"- Write optimally and with as few third-party dependencies as possible. \n "
"- Always test. \n "
"- No emojis in code or documentation. \n "
"- Respect AGENTS.md contents in the project." ;
static std :: string build_system_prompt ( Cfg & cfg ) {
std :: string tmp = bantam_tmp_dir ();
std :: string sp = default_system_prompt ;
size_t found = 0 ;
while (( found = sp . find ( "$TMPDIR/bantam" , found )) != std :: string :: npos ) {
sp . replace ( found , std :: strlen ( "$TMPDIR/bantam" ), tmp );
found += tmp . size ();
}
char mtr [ 32 ]; std :: snprintf ( mtr , sizeof mtr , "%d" , cfg . max_tool_res );
auto fm = sp . find ( "$MAX_TOOL_RES" );
if ( fm != std :: string :: npos ) sp . replace ( fm , std :: strlen ( "$MAX_TOOL_RES" ), mtr );
std :: string out = sp ;
std :: string td = tools_dir ( cfg );
if ( ! td . empty ()) { out += " \n\n Extra shell tools can be found at " ; out += td ; }
std :: string sd = skills_dir ( cfg );
if ( ! sd . empty ()) { out += " \n\n Skills may be discovered and invoked from " ; out += sd ; }
return out ;
}
/* ----------------------------- json arg helpers ----------------------- */
2026-10-14 08:36:39 +03:00
static std :: string get_str ( const Json & j , const char * k ) {
return j [ k ]. str ();
2026-10-14 00:01:03 +03:00
}
2026-10-14 08:36:39 +03:00
static int get_int ( const Json & j , const char * k , int def = 0 ) {
return j [ k ]. as_int ( def );
2026-10-14 00:01:03 +03:00
}
2026-10-14 08:36:39 +03:00
static std :: string tool_path_arg ( const Json & a ) {
2026-10-14 00:01:03 +03:00
std :: string v = get_str ( a , "filePath" );
if ( v . empty ()) v = get_str ( a , "path" );
return v ;
}
static bool trim_is_empty ( std :: string_view s ) {
size_t i = 0 ;
while ( i < s . size () && ( s [ i ] == ' ' || s [ i ] == '\n' || s [ i ] == '\r' || s [ i ] == '\t' )) i ++ ;
return i == s . size ();
}
static std :: string last_role ( Messages & msgs ) {
if ( msgs . items . empty ()) return "" ;
return msgs . items . back (). role ;
}
/* ----------------------------- AL loop -------------------------------- */
static int AL ( Cfg & cfg , Messages & msgs , Usage & out , volatile int * interrupted , std :: string * err ) {
int done = 0 ;
std :: vector < std :: string > tmps ;
int rc = 0 ;
for ( int i = 0 ; i < cfg . max_al_iterations && ! done ; i ++ ) {
if ( * interrupted ) { rc = - 1 ; break ; }
Message m ; Usage u ;
std :: string lerr ;
int lrc = llm ( cfg , msgs , true , m , u , lerr , interrupted );
if ( lrc != 0 ) {
if ( * interrupted ) { rc = - 1 ; break ; }
if ( is_invalid_assistant_err ( lerr . c_str ())) {
int stripped = 0 ;
for ( int j = ( int ) msgs . items . size () - 1 ; j >= 0 ; j -- ) {
if ( msgs . items [ j ]. role == "assistant" ) {
cprint ( "[stripped malformed assistant message] \n " , std :: array { 33 });
msgs . items . erase ( msgs . items . begin () + j );
stripped = 1 ;
break ;
}
}
if ( stripped ) continue ;
}
if ( err ) * err = std :: move ( lerr );
rc = - 1 ; break ;
}
out . prompt_tokens += u . prompt_tokens ;
out . completion_tokens += u . completion_tokens ;
out . total_tokens += u . total_tokens ;
out . context_tokens = u . context_tokens ;
if ( u . cached_tokens > 0 ) out . cached_tokens = u . cached_tokens ;
if ( ! u . model . empty ()) out . model = u . model ;
for ( auto & tc : m . calls ) {
tc . arguments = sanitize_arguments ( tc . arguments );
}
msgs . items . push_back ( m );
if ( ! cfg . stream ) {
if ( ! m . reasoning . empty ()) {
cprint ( "--- reasoning start --- \n " , std :: array { 36 });
cprint ( m . reasoning , std :: array { 2 });
cprint ( " \n --- reasoning end --- \n " , std :: array { 36 });
}
if ( ! m . content . empty ()) { render_md_print ( m . content ); printf ( " \n " ); }
}
if ( m . calls . empty ()) {
if ( ! m . reasoning . empty () && trim_is_empty ( m . content )) {
cprint ( "[auto continue: response was reasoning-only] \n " , std :: array { 33 });
Message cont ; cont . role = "user" ; cont . content = "continue" ;
msgs . items . push_back ( cont );
continue ;
}
done = 1 ; break ;
}
for ( size_t tci = 0 ; tci < m . calls . size (); tci ++ ) {
if ( * interrupted ) { rc = - 1 ; break ; }
ToolCall & tc = m . calls [ tci ];
std :: string astr = filter_text ( tc . arguments );
if ( ! m . calls_shown ) {
cprint ( sfmt ( "[tool call: %s(%s)]" , tc . name . c_str (), astr . c_str ()), std :: array { 33 });
printf ( " \n " );
}
std :: string res ; int sty = 2 ;
2026-10-14 08:36:39 +03:00
Json a_mut ;
if ( ! Json :: parse ( astr , a_mut ) || ! a_mut . is_object ()) {
2026-10-14 00:01:03 +03:00
res = sfmt ( "[tool error: invalid JSON args for %s: parse error. Raw: %s]" , tc . name . c_str (), astr . c_str ());
sty = 31 ;
} else {
2026-10-14 08:36:39 +03:00
const Json & a = a_mut ;
2026-10-14 00:01:03 +03:00
std :: string fn = tc . name ;
if ( fn == "bash" || fn == "shell_exec" ) {
std :: string cmd = get_str ( a , "command" );
std :: string workdir = get_str ( a , "workdir" );
std :: string fcmd = filter_text ( cmd );
std :: string fwd = filter_text ( workdir );
int to = cfg . shell_timeout ;
if ( a . contains ( "timeout" )) { int n = get_int ( a , "timeout" ); if ( n > 0 ) to = ( n > 1000 ) ? n / 1000 : n ; }
res = shell_with_workdir ( fcmd , fwd , to , interrupted );
} else if ( fn == "read" ) {
std :: string p = tool_path_arg ( a );
std :: string fp = filter_text ( p );
int off = get_int ( a , "offset" , 1 ); if ( off < 1 ) off = 1 ;
int lim = get_int ( a , "limit" , 2000 ); if ( lim <= 0 ) lim = 2000 ;
res = read_file_or_dir ( fp , off , lim , & sty );
} else if ( fn == "write" || fn == "write_file" ) {
std :: string p = tool_path_arg ( a );
std :: string fp = filter_text ( p );
std :: string cv = get_str ( a , "content" );
bool has_content = a . contains ( "content" ) && a [ "content" ]. is_string ();
2026-10-14 10:58:08 +03:00
bool has_line = a . contains ( "lineOffset" ) && ! a [ "lineOffset" ]. is_null ();
bool has_byte = a . contains ( "byteOffset" ) && ! a [ "byteOffset" ]. is_null ();
bool has_del_lines = a . contains ( "delLines" ) && ! a [ "delLines" ]. is_null ();
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 ;
2026-10-14 00:01:03 +03:00
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 ; }
2026-10-14 10:58:08 +03:00
else if ( ! has_line && ! has_byte && ! has_del_lines && ! has_del_bytes ) {
/* No offset/deletion parameters: overwrite the whole file. */
2026-10-14 00:01:03 +03:00
std :: string c = filter_text ( cv );
auto sl = fp . find_last_of ( '/' );
if ( sl != std :: string :: npos ) mkdir_p ( std :: string_view ( fp ). substr ( 0 , sl ));
else mkdir_p ( "." );
2026-10-14 08:36:39 +03:00
if ( ! write_file_str ( fp , c )) { res = sfmt ( "[tool error: write %s: %s]" , fp . c_str (), strerror ( errno )); sty = 31 ; }
else { res = sfmt ( "Successfully wrote %zu bytes to %s" , c . size (), fp . c_str ()); }
2026-10-14 00:01:03 +03:00
} else {
int werr = 0 ; std :: string c = filter_text ( cv );
2026-10-14 10:58:08 +03:00
res = write_file ( fp , line_off , byte_off , del_lines , del_bytes , c , & werr );
2026-10-14 00:01:03 +03:00
if ( werr ) sty = 31 ;
}
} else {
res = sfmt ( "Unknown tool: %s" , fn . c_str ());
sty = 31 ;
}
}
std :: string fres = filter_text ( res );
res = offload_tool_result ( fres , cfg . max_tool_res , tmps );
{
std :: string hdr = cstr ( "[tool result: " , std :: array { 32 });
std :: string fn2 = cstr ( tc . name , std :: array { 32 });
printf ( "%s%s] \n " , hdr . c_str (), fn2 . c_str ());
std :: string body = cstr ( res , std :: array { sty });
printf ( "%s \n\n " , body . c_str ());
}
Message tm ; tm . role = "tool" ; tm . tool_call_id = tc . id ; tm . content = res ;
msgs . items . push_back ( tm );
if ( * interrupted ) { rc = - 1 ; break ; }
}
if ( rc != 0 ) break ;
}
if ( ! done && rc == 0 ) {
Message maxm ; maxm . role = "assistant" ;
maxm . content = sfmt ( "[max AL iterations (%d) reached]" , cfg . max_al_iterations );
msgs . items . push_back ( maxm );
}
cleanup_temps ( tmps );
return rc ;
}
/* ----------------------------- run_turn ------------------------------- */
static int run_turn ( Cfg & cfg , Messages & msgs , Usage & usage , std :: string * err , volatile int * interrupted ) {
* interrupted = 0 ;
usage = Usage {};
int rc = AL ( cfg , msgs , usage , interrupted , err );
if ( last_role ( msgs ) == "tool" ) {
Message cont ; cont . role = "user" ; cont . content = "continue" ;
msgs . items . push_back ( cont );
cprint ( "[auto continue: last message was a tool result] \n " , std :: array { 33 });
rc = AL ( cfg , msgs , usage , interrupted , err );
}
if ( rc != 0 ) {
if ( * interrupted ) { cprint ( " \n [interrupted] \n " , std :: array { 33 }); }
else { cprint ( " \n [error: " , std :: array { 31 }); cprint (( ! err || err -> empty ()) ? "error" : * err , std :: array { 31 }); cprint ( "] \n " , std :: array { 31 }); }
}
return rc ;
}
/* ----------------------------- direct shell --------------------------- */
static void run_direct_shell ( std :: string_view cmd , int timeout ) {
std :: string fcmd = filter_text ( cmd );
cprint ( sfmt ( "[tool call: bash({ \" command \" : \" %s \" })]" , fcmd . c_str ()), std :: array { 33 });
printf ( " \n " );
std :: string res = shell_with_workdir ( fcmd , "" , timeout , & g_interrupted );
printf ( "%s \n " , cstr ( "[tool result: bash]" , std :: array { 32 }). c_str ());
printf ( "%s \n " , cstr ( res , std :: array { 2 }). c_str ());
}
/* ----------------------------- skills --------------------------------- */
static std :: string skill_prompt ( std :: string_view u , Cfg & cfg ) {
std :: string_view rest = u . substr ( 6 );
while ( ! rest . empty () && rest . front () == ' ' ) rest = rest . substr ( 1 );
if ( rest . empty ()) return "" ;
std :: string name ( rest );
std :: string prompt ;
auto sp = name . find ( ' ' );
if ( sp != std :: string :: npos ) { prompt = name . substr ( sp + 1 ); name = name . substr ( 0 , sp ); }
std :: string sd = skills_dir ( cfg );
std :: string path ;
if ( ! sd . empty ()) {
path = sfmt ( "%s/%s/SKILL.md" , sd . c_str (), name . c_str ());
} else if ( name . find ( "SKILL.md" ) != std :: string :: npos ) {
path = name ;
} else {
path = name + "/SKILL.md" ;
}
struct stat st ;
if ( stat ( path . c_str (), & st ) != 0 )
return sfmt ( "[cannot read skill \" %s \" : %s]" , name . c_str (), strerror ( errno ));
if ( S_ISDIR ( st . st_mode ))
return sfmt ( "[cannot read skill \" %s \" : Is a directory]" , name . c_str ());
2026-10-14 08:36:39 +03:00
std :: string data ;
if ( ! read_file_str ( path , data ))
return sfmt ( "[cannot read skill \" %s \" : %s]" , name . c_str (), strerror ( errno ));
2026-10-14 00:01:03 +03:00
while ( ! data . empty () && ( data . back () == '\n' || data . back () == '\r' )) data . pop_back ();
std :: string out = data ;
if ( ! prompt . empty ()) { out += " \n\n " ; out += prompt ; }
return out ;
}
static void list_skills ( Cfg & cfg ) {
std :: string sd = skills_dir ( cfg );
if ( sd . empty ()) { cprint ( "No skills directory configured (set BANTAM_SKILLS_DIR or bantam_skills_dir). \n " , std :: array { 33 }); return ; }
DIR * d = opendir ( sd . c_str ());
if ( ! d ) { cprint ( sfmt ( "[skill error: cannot read skills dir: %s]" , strerror ( errno )), std :: array { 31 }); return ; }
std :: string names ; int cnt = 0 ; struct dirent * de ;
while (( de = readdir ( d ))) {
if ( std :: strcmp ( de -> d_name , "." ) == 0 || std :: strcmp ( de -> d_name , ".." ) == 0 ) continue ;
std :: string full = sfmt ( "%s/%s" , sd . c_str (), de -> d_name );
struct stat st ; if ( stat ( full . c_str (), & st ) != 0 || ! S_ISDIR ( st . st_mode )) continue ;
std :: string sk = sfmt ( "%s/%s/SKILL.md" , sd . c_str (), de -> d_name );
if ( access ( sk . c_str (), F_OK ) == 0 ) { str_appendf ( names , " %s \n " , de -> d_name ); cnt ++ ; }
}
closedir ( d );
if ( cnt == 0 ) { cprint ( "No skills found in " , std :: array { 33 }); cprint ( sd , std :: array { 33 }); cprint ( " \n " , std :: array { 33 }); return ; }
cprint ( "Available skills in " , std :: array { 1 , 36 }); cprint ( sd , std :: array { 1 , 36 }); cprint ( ": \n " , std :: array { 1 , 36 });
fputs ( names . c_str (), stdout );
}
/* ----------------------------- compact -------------------------------- */
static void do_compact ( Cfg & cfg , Messages & msgs ) {
if ( msgs . items . size () <= 1 ) { cprint ( "Nothing to compact yet. \n " , std :: array { 33 }); return ; }
cprint ( "[compacting conversation...] \n " , std :: array { 33 });
std :: string sum , e ;
auto nm = compact ( cfg , msgs , & sum , & e , & g_interrupted );
int interrupted = g_interrupted ;
if ( ! nm ) {
if ( interrupted ) cprint ( " \n [interrupted] \n " , std :: array { 33 });
else { cprint ( sfmt ( " \n [compact failed: %s]" , e . empty () ? "compact failed" : e . c_str ()), std :: array { 31 }); printf ( " \n " ); }
return ;
}
msgs = std :: move ( * nm );
autosave ( msgs );
printf ( "%s%d messages] \n " , cstr ( "[compacted to " , std :: array { 32 }). c_str (), ( int ) msgs . items . size ());
printf ( "%s" , cstr ( "--- summary --- \n " , std :: array { 33 }). c_str ());
printf ( "%s \n " , cstr ( sum , std :: array { 2 }). c_str ());
}
/* ----------------------------- main ----------------------------------- */
struct CurlGlobalGuard {
CurlGlobalGuard () { curl_global_init ( CURL_GLOBAL_DEFAULT ); }
~ CurlGlobalGuard () { curl_global_cleanup (); }
};
int main ( int argc , char ** argv ) {
CurlGlobalGuard curl_guard ;
setup_signal_handler ();
Cfg cfg = get_cfg ( config_path ());
cfg . context_window = fetch_context_window ( cfg );
std :: string sp = build_system_prompt ( cfg );
g_col = color_enabled ( cfg );
Messages msgs ;
{
Message sys ; sys . role = "system" ; sys . content = sp ;
msgs . items . push_back ( sys );
}
if ( argc > 1 && argv [ 1 ] && argv [ 1 ][ 0 ] != '\0' ) {
std :: string p = argv [ 1 ];
struct stat st ;
if ( stat ( p . c_str (), & st ) != 0 || S_ISDIR ( st . st_mode )) {
cprint ( "Error: file '" , std :: array { 31 }); cprint ( p , std :: array { 31 }); cprint ( "' not found. \n " , std :: array { 31 });
return 1 ;
}
2026-10-14 08:36:39 +03:00
std :: string u ;
if ( ! read_file_str ( p , u )) {
2026-10-14 00:01:03 +03:00
cprint ( "Error: file '" , std :: array { 31 }); cprint ( p , std :: array { 31 }); cprint ( "' not found. \n " , std :: array { 31 });
return 1 ;
}
size_t a = 0 ; while ( a < u . size () && ( u [ a ] == ' ' || u [ a ] == '\n' || u [ a ] == '\r' || u [ a ] == '\t' )) a ++ ;
u = u . substr ( a );
if ( ! u . empty () && u [ 0 ] == '!' ) { run_direct_shell ( u . substr ( 1 ), cfg . shell_timeout ); return 0 ; }
Message um ; um . role = "user" ; um . content = u ;
msgs . items . push_back ( um );
Usage usg ; std :: string err ;
run_turn ( cfg , msgs , usg , & err , & g_interrupted );
if ( ! err . empty ()) return 1 ;
cprint ( format_usage ( usg , cfg . context_window ), std :: array { 2 }); printf ( " \n " );
autosave ( msgs );
return 0 ;
}
load_history ();
auto sess = find_project_session ();
if ( sess && sess -> messages . items . size () > 1 ) {
cprint ( "[session found for this directory] \n " , std :: array { 33 });
cprint ( " " , std :: array { 33 }); cprint ( sess -> id , std :: array { 32 }); printf ( " \n " );
cprint ( " " , std :: array { 33 }); cprint ( sess -> summary , std :: array { 2 }); printf ( " \n " );
std :: string created = sfmt ( " created %s [%d messages] \n " , sess -> created . c_str (), ( int ) sess -> messages . items . size ());
cprint ( created , std :: array { 2 });
printf ( " Resume this session? [Y/n]: " ); fflush ( stdout );
bool ok ; std :: string ans = read_plain ( "" , ok );
if ( ok ) {
std :: string a2 = ans ;
size_t ai = 0 ; while ( ai < a2 . size () && ( a2 [ ai ] == ' ' || a2 [ ai ] == '\n' || a2 [ ai ] == '\r' || a2 [ ai ] == '\t' )) ai ++ ;
a2 = a2 . substr ( ai );
while ( ! a2 . empty () && ( a2 . back () == '\n' || a2 . back () == '\r' || a2 . back () == ' ' )) a2 . pop_back ();
if ( a2 . empty () || a2 [ 0 ] == 'y' || a2 [ 0 ] == 'Y' ) {
msgs = std :: move ( sess -> messages );
autosave ( msgs );
printf ( "%s%s] \n " , cstr ( "[session resumed: " , std :: array { 32 }). c_str (), sess -> id . c_str ());
}
}
}
const std :: string * re_ptr = cfg_raw_get ( cfg , "reasoning_effort" );
std :: string re = ( re_ptr && ! re_ptr -> empty ()) ? * re_ptr : "high" ;
{
cprint ( "Bantam Agent ready" , std :: array { 1 , 32 }); printf ( " " );
cprint ( " (Enter submits; Ctrl+J or paste = new line) \n " , std :: array { 2 });
std :: string info = sfmt ( "endpoint: %s model: %s temp: %g reasoning: %s context: %d \n " ,
cfg . endpoint . c_str (), cfg . model . c_str (), cfg . temperature , re . c_str (), cfg . context_window );
cprint ( info , std :: array { 2 });
}
for (;;) {
g_interrupted = 0 ;
bool ok ;
std :: string u = read_line ( "> " , ok );
if ( ! ok ) { printf ( " \n " ); break ; }
std :: string ut = u ;
size_t a = 0 ; while ( a < ut . size () && ( ut [ a ] == ' ' || ut [ a ] == '\n' || ut [ a ] == '\r' || ut [ a ] == '\t' )) a ++ ;
ut = ut . substr ( a );
if ( ut . empty ()) continue ;
add_history ( u );
/* command aliases */
for ( int ci = 0 ; ci < 2 ; ci ++ ) {
const char * cmd = ( ci == 0 ) ? "model" : "endpoint" ;
std :: string slash = sfmt ( "/%s" , cmd );
if ( u == slash ) {
u = sfmt ( "/cfg %s" , cmd );
} else if ( u . size () > slash . size () && u . rfind ( slash , 0 ) == 0 && u [ slash . size ()] == ' ' ) {
u = sfmt ( "/cfg %s %s" , cmd , u . c_str () + slash . size () + 1 );
}
}
if ( u == "/quit" ) break ;
if ( u == "/clear" ) {
msgs = Messages {};
Message s2 ; s2 . role = "system" ; s2 . content = build_system_prompt ( cfg );
msgs . items . push_back ( s2 );
autosave ( msgs ); continue ;
}
if ( u == "/save" || u . starts_with ( "/save " )) {
std :: string name = u . substr ( 5 );
size_t ni = 0 ; while ( ni < name . size () && name [ ni ] == ' ' ) ni ++ ;
name = name . substr ( ni );
std :: string sid , sm ;
save_session ( msgs , name , & sid , & sm );
printf ( "%s%s] \n " , cstr ( "[session saved: " , std :: array { 32 }). c_str (), sid . c_str ());
cprint ( " " , std :: array { 32 }); cprint ( sm , std :: array { 2 }); printf ( " \n " );
continue ;
}
if ( u == "/continue" || u == "/cont" ) {
std :: string pid = project_id ();
std :: string lerr ;
auto lm = load_session ( pid , & lerr );
if ( ! lm ) { cprint ( "No session found for current project: " , std :: array { 31 }); cprint ( lerr , std :: array { 31 }); printf ( " \n " ); continue ; }
msgs = std :: move ( * lm );
autosave ( msgs );
printf ( "%s%s] \n " , cstr ( "[session continued: " , std :: array { 32 }). c_str (), pid . c_str ());
std :: string sm = summary_of ( msgs );
cprint ( " " , std :: array { 32 }); cprint ( sm , std :: array { 32 }); printf ( " \n " );
continue ;
}
if ( u == "/list" ) {
auto ss = sessions_load ();
if ( ss . empty ()) { cprint ( "No sessions saved yet. \n " , std :: array { 33 }); continue ; }
std :: string pid = project_id ();
for ( auto & s : ss ) {
printf ( "%s" , cstr ( s . id , std :: array { 32 }). c_str ());
if ( s . id == pid ) cprint ( " (current project)" , std :: array { 33 });
std :: string meta = sfmt ( " %s [%d msgs] \n " , s . created . c_str (), ( int ) s . messages . items . size ());
cprint ( meta , std :: array { 2 });
cprint ( " " , std :: array { 32 }); cprint ( s . summary , std :: array { 2 }); printf ( " \n " );
}
continue ;
}
if ( u == "/load" || u . starts_with ( "/load " )) {
std :: vector < std :: string > parts ;
{
std :: string_view sv ( u );
size_t i = 0 ;
while ( i < sv . size ()) {
while ( i < sv . size () && sv [ i ] == ' ' ) i ++ ;
if ( i >= sv . size ()) break ;
size_t j = i ;
while ( j < sv . size () && sv [ j ] != ' ' ) j ++ ;
parts . push_back ( std :: string ( sv . substr ( i , j - i )));
i = j ;
}
}
if ( parts . size () < 2 ) { cprint ( "Usage: /load <session-id> \n " , std :: array { 31 }); continue ; }
std :: string lerr ;
auto lm = load_session ( parts [ 1 ], & lerr );
if ( ! lm ) { cprint ( "Session not found: " , std :: array { 31 }); cprint ( lerr , std :: array { 31 }); printf ( " \n " ); continue ; }
msgs = std :: move ( * lm );
autosave ( msgs );
printf ( "%s%s] \n " , cstr ( "[session loaded: " , std :: array { 32 }). c_str (), parts [ 1 ]. c_str ());
std :: string sm = summary_of ( msgs );
cprint ( " " , std :: array { 32 }); cprint ( sm , std :: array { 32 }); printf ( " \n " );
continue ;
}
if ( u == "/compact" ) { do_compact ( cfg , msgs ); continue ; }
if ( u == "/cfg" || u . starts_with ( "/cfg " )) {
/* tokens after "/cfg " */
std :: string rest = u . substr ( 4 );
while ( ! rest . empty () && rest . front () == ' ' ) rest = rest . substr ( 1 );
std :: string sub , key , val ;
{
size_t i = 0 ;
while ( i < rest . size () && rest [ i ] != ' ' ) i ++ ;
sub = rest . substr ( 0 , i );
size_t j = i ;
while ( j < rest . size () && rest [ j ] == ' ' ) j ++ ;
size_t ks = j ;
while ( j < rest . size () && rest [ j ] != ' ' ) j ++ ;
key = rest . substr ( ks , j - ks );
size_t vs = j ;
while ( vs < rest . size () && rest [ vs ] == ' ' ) vs ++ ;
val = rest . substr ( vs );
}
if ( sub . empty ()) { cprint ( "Usage: /cfg <key> [value] \n " , std :: array { 31 }); continue ; }
if ( sub == "get" ) {
if ( key . empty ()) { cprint ( "Usage: /cfg get <key> \n " , std :: array { 31 }); continue ; }
const std :: string * v = cfg_raw_get ( cfg , key );
if ( v && ! v -> empty ()) printf ( "%s=%s \n " , cstr ( key , std :: array { 32 }). c_str (), v -> c_str ());
else printf ( "%s not set \n " , cstr ( key , std :: array { 31 }). c_str ());
} else if ( sub == "set" ) {
if ( key . empty ()) { cprint ( "Usage: /cfg set <key> <value> \n " , std :: array { 31 }); continue ; }
if ( set_cfg ( config_path (), key , val ) != 0 ) { cprint ( "[cfg error] \n " , std :: array { 31 }); continue ; }
cfg = get_cfg ( config_path ());
if ( key == "model" || key == "endpoint" || key == "api_key" )
cfg . context_window = fetch_context_window ( cfg );
g_col = color_enabled ( cfg );
printf ( "%s%s=%s] \n " , cstr ( "[config updated: " , std :: array { 32 }). c_str (), key . c_str (), val . c_str ());
} else if ( key . empty ()) {
/* single token: treat as get */
const std :: string * v = cfg_raw_get ( cfg , sub );
if ( v && ! v -> empty ()) printf ( "%s=%s \n " , cstr ( sub , std :: array { 32 }). c_str (), v -> c_str ());
else printf ( "%s not set \n " , cstr ( sub , std :: array { 31 }). c_str ());
} else {
/* legacy "/cfg key value": treat as set */
if ( set_cfg ( config_path (), sub , key ) != 0 ) { cprint ( "[cfg error] \n " , std :: array { 31 }); continue ; }
cfg = get_cfg ( config_path ());
if ( sub == "model" || sub == "endpoint" || sub == "api_key" )
cfg . context_window = fetch_context_window ( cfg );
g_col = color_enabled ( cfg );
printf ( "%s%s=%s] \n " , cstr ( "[config updated: " , std :: array { 32 }). c_str (), sub . c_str (), key . c_str ());
}
continue ;
}
if ( u == "/skill" || u . starts_with ( "/skill " )) {
std :: string rest = u . substr ( 6 );
while ( ! rest . empty () && rest . front () == ' ' ) rest = rest . substr ( 1 );
if ( rest . empty ()) { list_skills ( cfg ); continue ; }
std :: string composed = skill_prompt ( u , cfg );
if ( composed . empty ()) { cprint ( "/skill usage: /skill <name> [prompt] \n " , std :: array { 31 }); continue ; }
if ( composed . starts_with ( "[cannot read skill" )) {
cprint ( composed , std :: array { 31 }); printf ( " \n " ); continue ;
}
u = composed ;
/* fall through to a normal LLM turn */
} else if ( u == "/models" ) {
int e2 ; std :: string ml = list_models ( cfg , & e2 );
if ( e2 != 0 ) { cprint ( "[error querying /models: " , std :: array { 31 }); cprint ( ml , std :: array { 31 }); printf ( "] \n " ); continue ; }
cprint ( "Supported models at " , std :: array { 1 , 36 }); cprint ( cfg . endpoint , std :: array { 1 , 36 }); cprint ( ": \n " , std :: array { 1 , 36 });
render_md_print ( ml );
continue ;
} else if ( ! u . empty () && u [ 0 ] == '!' ) {
run_direct_shell ( u . substr ( 1 ), cfg . shell_timeout ); continue ;
} else if ( u == "/help" ) {
cprint ( "Bantam commands: \n " , std :: array { 1 , 36 });
const char * cmds [][ 2 ] = {
{ "/quit" , "exit" }, { "/clear" , "reset to system prompt" }, { "/save [name]" , "save session (default: project MD5)" },
{ "/continue" , "continue project session (alias: /cont)" }, { "/list" , "list sessions" }, { "/load <id>" , "load session" },
{ "/compact" , "compact context" }, { "/cfg <k> [v]" , "get/set config" }, { "/model [m]" , "alias for /cfg model" },
{ "/endpoint [e]" , "alias for /cfg endpoint" }, { "!<cmd>" , "run shell command directly" }, { "/models" , "list models at endpoint" },
{ "/skill <n> [p]" , "load SKILL.md and run as prompt" }, { "/help" , "show help" }};
for ( int i = 0 ; i < 14 ; i ++ ) {
std :: string label = sfmt ( " %-18s" , cmds [ i ][ 0 ]);
cprint ( label , std :: array { 1 , 32 });
cprint ( cmds [ i ][ 1 ], std :: array { 1 , 32 });
printf ( " \n " );
}
continue ;
}
/* normal LLM turn */
{
Message um ; um . role = "user" ; um . content = u ;
msgs . items . push_back ( um );
Usage usg ; std :: string err ;
run_turn ( cfg , msgs , usg , & err , & g_interrupted );
if ( ! err . empty ()) continue ;
cprint ( format_usage ( usg , cfg . context_window ), std :: array { 2 }); printf ( " \n " );
double pct = context_pct ( usg , cfg . context_window );
if ( pct >= 60.0 && msgs . items . size () > 1 ) {
std :: string q = sfmt ( "Context usage is at %.1f%% (%d / %d tokens). Compact conversation? [Y/n]: " ,
pct , ctx_tokens ( usg ), cfg . context_window );
cprint ( q , std :: array { 33 });
bool ok2 ; std :: string ans = read_plain ( "" , ok2 );
if ( ok2 ) {
std :: string a2 = ans ;
size_t ai = 0 ; while ( ai < a2 . size () && ( a2 [ ai ] == ' ' || a2 [ ai ] == '\n' || a2 [ ai ] == '\r' || a2 [ ai ] == '\t' )) ai ++ ;
a2 = a2 . substr ( ai );
while ( ! a2 . empty () && ( a2 . back () == '\n' || a2 . back () == '\r' || a2 . back () == ' ' )) a2 . pop_back ();
if ( a2 . empty () || a2 [ 0 ] == 'y' || a2 [ 0 ] == 'Y' ) do_compact ( cfg , msgs );
}
}
autosave ( msgs );
}
}
autosave ( msgs );
save_history ();
return 0 ;
}