From 7097ac1d86b1afe34afbcf2de6df531b16b184d5 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Sun, 9 Aug 2026 13:12:15 +0300 Subject: [PATCH] added microbantam --- README.md | 28 ++++++++++++++ mb | 107 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100755 mb diff --git a/README.md b/README.md index 1821a2c..7a73285 100644 --- a/README.md +++ b/README.md @@ -145,11 +145,39 @@ When enabled, the interactive console uses a subtle ANSI palette: the pending-re - Return value: string - Action: run shell command specified in `command` subject to `shell_timeout` (default 120s) and return `output + '\n\nexit: ' + exit_code` string. +## MicroBantam + +MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same agent in **under 100 SLOC** (92 non-blank lines), written to stay readable while keeping the full agentic core. It reads the same `model.cfg` and `system.txt` from the current working directory. + +### Features + +- Full agentic loop: LLM calls, `shell_exec` / `run_subagent` tool execution with JSON argument validation (invalid args are fed back so the model can self-correct), and the 5-level subagent recursion depth limit +- A `...requesting...` in-flight indicator: in-place on a TTY (`\r` overwrite, erased on completion), a plain line when output is piped +- Session management: `/save`, `/list`, `/load ` (exact id only, no prefix matching), and auto-save to `~/.bantam/sessions/autosave.json` after every turn and on exit; session ids get `-1`, `-2`, ... suffixes on same-second collisions +- Interactive mode (`/quit`, `/clear`, `/save`, `/list`, `/load `, `/help`) and file input mode +- Same built-in default system prompt and `OPENAI_API_KEY` fallback as the full implementation + +### What it drops + +- Streaming (requests are non-streaming; `stream` is ignored) +- ANSI coloring/styling (`color` is ignored) +- `Term::ReadLine` line editing, Ctrl+J multi-line prompts and readline history (plain single-line prompts) +- Fibonacci backoff network retries (a failed request aborts with an `API error` message) +- `/compact` context summarization + +### Running + +```bash +./mb # interactive (or: perl mb) +./mb prompt.txt # file input mode +``` + ## Repository layout - `bantam.py` — Python reference implementation (stdlib only) - `main.go`, `term_linux.go`, `term_darwin.go`, `term_windows.go`, `term_other.go` — Go implementation (stdlib only, module `code.luxferre.top/luxferre/bantam`) - `bantam.pl` — Perl 5 implementation (core modules only) +- `mb` — MicroBantam, compressed Perl 5 implementation (core modules only, 92 SLOC) - `model.cfg`, `system.txt` — shared configuration and system prompt - `README.md` — this document diff --git a/mb b/mb new file mode 100755 index 0000000..77ed5c1 --- /dev/null +++ b/mb @@ -0,0 +1,107 @@ +#!/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); +$| = 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 /help\n"; + while (1) { + print "> "; my $u = ; + 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 \n"; } } + elsif ($u eq '/help') { print "Commands: /quit /clear /save /list /load /help\n"; } + else { push @$msgs, {role=>'user', content=>$u}; AL($c, $msgs, $sp); autosave($msgs); } + } + autosave($msgs); +} + +main() unless caller();