2026-08-09 13:12:15 +03:00
#!/usr/bin/env perl
# MicroBantam (mb): the Bantam agent in <100 SLOC - readable, core modules only
# Created by Luxferre in 2026, released into the public domain
use strict ; use warnings ; use HTTP::Tiny ; use JSON::PP ; use POSIX qw(strftime) ; use File::Path qw(make_path) ;
2026-08-09 13:22:33 +03:00
$SIG { __WARN__ } = sub {
my $m = shift ;
return if $m =~ /^Use of uninitialized value \$err in numeric eq \(==\) at .*IO\/Socket\/IP\.pm line \d+/ ;
warn $m ;
};
# Make STDOUT/STDERR UTF-8 aware so decoded Unicode (model output, tool
# results) prints cleanly instead of triggering "Wide character in print"
binmode ( STDOUT , ':encoding(UTF-8)' );
binmode ( STDERR , ':encoding(UTF-8)' );
2026-08-09 13:12:15 +03:00
$| = 1 ; # unbuffered output: live spinner, responsive prompt
my $DEF_SP = "You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:\n- shell_exec: run a shell command; returns its output and exit code.\n- run_subagent: delegate a sub-task to a child agent; returns its reply.\n\nWork 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. Delegate large or independent sub-tasks to run_subagent. Stop as soon as the goal is met and report concisely: results, not process.\n\nWhen generating code:\n- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n- No whitespace between keywords and opening braces 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, GEMINI.md, CLAUDE.md contents in the project." ;
my $SDIR = ( $ENV { HOME } || $ENV { USERPROFILE } || '.' ) . '/.bantam/sessions' ;
sub cfg { my %d = ( endpoint => 'https://opencode.ai/zen/v1' , model => 'big-pickle' , temperature => 0.7 , api_key => '-' , timeout => 300 , shell_timeout => 120 , max_al_iterations => 1000 );
if ( open my $f , '<' , 'model.cfg' ) { while ( <$f> ) { /^(\w+)\s*=\s*(.+)$/ and $d { $1 } = $2 ; } }
$d { api_key } = $ENV { OPENAI_API_KEY } if $d { api_key } eq q{-} && $ENV { OPENAI_API_KEY };
\ %d ; }
sub sp { my $p = '' ;
if ( open my $f , '<' , 'system.txt' ) { local $/ ; $p = <$f> ; }
$p =~ s/^\s+|\s+$//g ;
length ( $p ) ? $p : $DEF_SP ; }
sub T { my ( $n , $d , $p ) = @_ ; { type => 'function' , function => { name => $n , description => $d , parameters => { type => 'object' , properties => $p , required => [ keys %$p ]}}} }
sub llm { my ( $c , $msgs ) = @_ ; # one non-streaming chat completion
my $ep = $c -> { endpoint }; $ep =~ s{/+$}{} ;
my $h = { 'Content-Type' => 'application/json' , 'User-Agent' => 'Mozilla/5.0 (compatible; MicroBantam/1.0)' };
$h -> { Authorization } = "Bearer $c->{api_key}" if $c -> { api_key } ne '-' ;
my $body = encode_json ({ model => $c -> { model }, temperature => 0 + $c -> { temperature }, messages => $msgs ,
tools => [ T ( 'shell_exec' , 'Run a shell command, return output and exit code.' , { command => { type => 'string' }}),
T ( 'run_subagent' , 'Run a child agent with a prompt.' , { prompt => { type => 'string' }})]});
my $tty = - t STDOUT ;
print $tty ? "\r...requesting..." : "...requesting...\n" ;
my $r = HTTP::Tiny -> new ( timeout => 0 + $c -> { timeout }) -> post ( "$ep/chat/completions" , { headers => $h , content => $body });
print "\r\e[K" if $tty ; # clear the spinner line
my $d = $r -> { success } ? eval { decode_json ( $r -> { content }) } : undef ;
return $d -> { choices }[ 0 ]{ message } if $d && $d -> { choices } && @ { $d -> { choices }};
die "API error: " . ( $r -> { reason } || "HTTP $r->{status}" ) . "\n" ; }
sub shell_exec { my ( $cmd , $t ) = @_ ; # run a command under a hard timeout
my $out = '' ;
eval { local $SIG { ALRM } = sub { alarm 0 ; die "timeout\n" }; alarm $t ; $out = `$cmd 2>&1` ; alarm 0 ; };
$out =~ s/\s+$// ;
$@ ? "$out\n[timeout after ${t}s]\nexit: -1" : "$out\nexit: " . ( $? >> 8 ); }
sub last_assistant { for my $m ( reverse @ { $_ [ 0 ]}) { return $m -> { content } if $m -> { role } eq 'assistant' && defined $m -> { content } && length $m -> { content }; } '' }
sub AL { my ( $c , $msgs , $sp , $depth ) = @_ ; # the agentic loop: LLM <-> tools until done
$depth ||= 0 ;
for ( 1 .. $c -> { max_al_iterations }) {
my $m = eval { llm ( $c , $msgs ) };
if ( $@ ) { print $@ ; return $msgs ; }
push @$msgs , $m ;
print $m -> { content }, "\n" if defined $m -> { content } && length $m -> { content };
my $tcs = $m -> { tool_calls };
last unless $tcs && @$tcs ;
for my $tc ( @$tcs ) {
my $fn = $tc -> { function }{ name };
my $a = eval { decode_json ( $tc -> { function }{ arguments } // '{}' ) };
my $res ;
if ( ref $a ne 'HASH' ) { $res = "bad JSON args for $fn: $tc->{function}{arguments}" ; }
elsif ( $fn eq 'shell_exec' ) { $res = shell_exec ( $a -> { command } // '' , $c -> { shell_timeout }); }
elsif ( $fn eq 'run_subagent' ) { $res = $depth >= 5 ? '[subagent depth limit (5) reached, child not spawned]'
: last_assistant ( AL ( $c , [{ role => 'system' , content => "$sp\n\nImportant: this is a child agent" },
{ role => 'user' , content => $a -> { prompt } // '' }], $sp , $depth + 1 )); }
else { $res = "unknown tool: $fn" ; }
print "[tool] $fn: $res\n" ;
push @$msgs , { role => 'tool' , tool_call_id => $tc -> { id }, content => $res };
}
}
$msgs ; }
sub sessions { my @s ; # all saved sessions, newest first
for my $f ( glob "$SDIR/*.json" ) { open my $fh , '<' , $f or next ; local $/ ; my $d = eval { decode_json ( <$fh> ) }; push @s , $d if $d ; }
sort { $b -> { id } cmp $a -> { id } } @s ; }
sub sdir { make_path ( $SDIR ) unless - d $SDIR ; $SDIR }
sub save { sdir (); my $id = strftime ( '%Y%m%d-%H%M%S' , localtime ); my $i = 0 ;
$id .= '-' . ++ $i while - f "$SDIR/$id.json" ;
open my $f , '>' , "$SDIR/$id.json" or die "cannot save: $!" ;
print $f JSON::PP -> new -> utf8 -> pretty -> encode ({ id => $id , messages => $_ [ 0 ]}); close $f ; $id ; }
sub load { my ( $want ) = @_ ; my ( $hit ) = grep { $_ -> { id } eq $want } sessions ();
die "no session: $want\n" unless $hit ; $hit -> { messages }; }
sub autosave { sdir (); open my $f , '>' , "$SDIR/autosave.json" or return ; print $f JSON::PP -> new -> utf8 -> pretty -> encode ({ id => 'autosave' , messages => $_ [ 0 ]}); }
sub list_sessions { map { [ $_ -> { id }, scalar @ { $_ -> { messages } // [] }] } sessions () }
sub main {
my ( $c , $sp ) = ( cfg (), sp ());
my $msgs = [{ role => 'system' , content => $sp }];
if ( @ARGV ) { open my $f , '<' , $ARGV [ 0 ] or die "cannot open $ARGV[0]: $!" ; # file mode
local $/ ; push @$msgs , { role => 'user' , content => <$f> };
AL ( $c , $msgs , $sp ); autosave ( $msgs ); return ; }
print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save /list /load <id> /help\n" ;
while ( 1 ) {
print "> " ; my $u = <STDIN> ;
last unless defined $u ;
$u =~ s/^\s+|\s+$//g ;
next unless length $u ;
if ( $u eq '/quit' ) { last ; }
elsif ( $u eq '/clear' ) { $msgs = [{ role => 'system' , content => $sp }]; autosave ( $msgs ); }
elsif ( $u eq '/save' ) { print "session saved: " , save ( $msgs ), "\n" ; }
elsif ( $u eq '/list' ) { print "$_->[0] [$_->[1] msgs]\n" for list_sessions (); }
elsif ( $u =~ /^\/load(?:\s+(\S+))?$/ ) { if ( defined $1 ) { $msgs = eval { load ( $1 ) }; $@ ? print ( $@ ) : ( autosave ( $msgs ), print "loaded: $1\n" ); } else { print "usage: /load <session id>\n" ; } }
elsif ( $u eq '/help' ) { print "Commands: /quit /clear /save /list /load <id> /help\n" ; }
else { push @$msgs , { role => 'user' , content => $u }; AL ( $c , $msgs , $sp ); autosave ( $msgs ); }
}
autosave ( $msgs );
}
main () unless caller ();