oc compatibility overhaul
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
use strict; use warnings; use HTTP::Tiny; use JSON::PP; use POSIX qw(strftime); use File::Path qw(make_path); use Digest::MD5 qw(md5_hex); use Cwd qw(abs_path);
|
||||
$SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /^Use of uninitialized value \$err in numeric eq \(==\) at .*IO\/Socket\/IP\.pm line \d+/ };
|
||||
binmode $_ => ':encoding(UTF-8)' for *STDIN, *STDOUT, *STDERR; $| = 1; # unbuffered output in UTF-8
|
||||
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- write_file: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.\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. Stop as soon as the goal is met and report concisely: results, not process.\nUse 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\nWhen 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.";
|
||||
my $DEF_SP = "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- write: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.\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. Stop as soon as the goal is met and report concisely: results, not process.\nUse 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\nWhen 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.";
|
||||
my $SDIR = ($ENV{HOME} || $ENV{USERPROFILE} || '.') . '/.bantam/sessions';
|
||||
sub cfg {
|
||||
my %d = (endpoint=>'https://api.kilo.ai/api/openrouter', model=>'openrouter/free', temperature=>0.7, api_key=>'-', timeout=>300, shell_timeout=>120, max_al_iterations=>1000, reasoning_effort=>'high');
|
||||
@@ -30,9 +30,9 @@ sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') {
|
||||
} } }
|
||||
sub llm { my ($c, $msgs) = @_; sanitize_msgs($msgs);
|
||||
my $ep = $c->{endpoint}; $ep =~ s{/+$}{};
|
||||
my $h = {'Content-Type'=>'application/json', 'User-Agent'=>'Mozilla/5.0 (compatible; MicroBantam/1.0)'};
|
||||
my $h = {'Content-Type'=>'application/json', 'User-Agent'=>'opencode/1.18.31 ai-sdk/provider-utils/4.0.23 runtime/bun/1.3.14', 'x-opencode-client'=>'cli', 'x-opencode-project'=>'global'};
|
||||
$h->{Authorization} = "Bearer $c->{api_key}" if $c->{api_key} ne '-';
|
||||
my %p = (messages=>$msgs, tools=>[T('shell_exec', 'Run a shell command, return output and exit code.', {command=>{type=>'string'}}), T('write_file', 'Write content to a file. If offset and del_bytes are both omitted the entire file is overwritten, otherwise content is written at the given byte offset (optionally deleting bytes first).', {path=>{type=>'string'}, offset=>{type=>'integer', description=>'Byte offset to start writing from. If omitted together with del_bytes the whole file is overwritten instead. Defaults to 0.'}, del_bytes=>{type=>'integer', description=>'Bytes to delete starting at offset. If omitted together with offset the whole file is overwritten instead.'}, content=>{type=>'string'}}, ['path', 'content'])], model=>$c->{model}, temperature=>0+$c->{temperature});
|
||||
my %p = (messages=>$msgs, tools=>[T('bash', 'Executes a given bash command, return output and exit code.', {command=>{type=>'string'}}), T('read', 'Read a file or directory from the local filesystem.', {filePath=>{type=>'string'}, offset=>{type=>'integer'}, limit=>{type=>'integer'}}, ['filePath']), T('write', 'Write content to a file. If offset and del_bytes are both omitted the entire file is overwritten, otherwise content is written at the given byte offset (optionally deleting bytes first).', {filePath=>{type=>'string'}, offset=>{type=>'integer'}, del_bytes=>{type=>'integer'}, content=>{type=>'string'}}, ['filePath', 'content'])], model=>$c->{model}, temperature=>0+$c->{temperature});
|
||||
for my $k (keys %$c) { next if $k =~ /^(endpoint|api_key|timeout|shell_timeout|max_al_iterations|stream|color)$/; my $val = eval { decode_json($c->{$k}) }; $p{$k} = defined $val ? $val : $c->{$k}; }
|
||||
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=>encode_json(\%p)});
|
||||
@@ -42,9 +42,16 @@ sub llm { my ($c, $msgs) = @_; sanitize_msgs($msgs);
|
||||
my $rb = substr($r->{content} // '', 0, 500);
|
||||
die "API error: " . (length($rb) ? "$rb (HTTP $r->{status})" : ($r->{reason} || "HTTP $r->{status}")) . "\n"; }
|
||||
sub shell_exec { my ($cmd, $t, $out) = (filter_text($_[0]), $_[1], '');
|
||||
eval { local $SIG{ALRM} = sub { alarm 0; die "timeout\n" }; alarm $t; $out = `$cmd 2>&1`; alarm 0; };
|
||||
eval { local $SIG{ALRM} = sub { alarm 0; die "timeout\n" }; alarm $t; $out = `bash -c \Q$cmd\E 2>&1`; alarm 0; };
|
||||
$out =~ s/\s+$//; utf8::decode($out); $out = filter_text($out);
|
||||
$@ ? "$out\n[timeout after ${t}s]\nexit: -1" : "$out\nexit: " . ($? >> 8); }
|
||||
sub read_file { my ($p, $off, $lim) = ($_[0] // '', $_[1] || 1, $_[2] || 2000);
|
||||
return "[read error: filePath required]" unless length $p;
|
||||
return "[read error: no such file or directory: $p]" unless -e $p;
|
||||
if (-d $p) { opendir my $dh, $p or return "[read error: cannot open dir $p: $!]"; my @e = sort grep { !/^\.\.?$/ } readdir $dh; closedir $dh; return join("\n", map { (-d "$p/$_" ? "$_/" : $_) } @e); }
|
||||
open my $fh, '<:encoding(UTF-8)', $p or return "[read error: cannot open $p: $!]"; my ($ln, $cnt, @r) = (0, 0);
|
||||
while (defined(my $line = <$fh>)) { $ln++; next if $ln < $off; chomp $line; $line = substr($line, 0, 2000) if length($line) > 2000; push @r, "$ln: $line"; last if ++$cnt >= $lim; }
|
||||
close $fh; join("\n", @r) }
|
||||
sub write_file { my ($p, $off, $del, $cnt) = ($_[0], $_[1] || 0, $_[2] || 0, $_[3] // '');
|
||||
return "[write_file error: path required]" unless defined $p && length $p;
|
||||
$off = 0 if $off < 0; $del = 0 if $del < 0;
|
||||
@@ -73,15 +80,16 @@ sub AL { my ($c, $msgs) = ($_[0], $_[1]);
|
||||
$tc->{function}{arguments} = filter_text($tc->{function}{arguments});
|
||||
my $a = eval { decode_json($tc->{function}{arguments} // '{}') };
|
||||
if (ref $a ne 'HASH') { $tc->{function}{arguments} = encode_json({invalid_raw => $tc->{function}{arguments} // ''}); $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 'write_file') {
|
||||
my ($p, $cnt) = ($a->{path}, defined $a->{content} ? $a->{content} : '');
|
||||
elsif ($fn eq 'bash' || $fn eq 'shell_exec') { $res = shell_exec($a->{command} // '', $c->{shell_timeout}); }
|
||||
elsif ($fn eq 'read') { $res = read_file($a->{filePath} // $a->{path}, $a->{offset}, $a->{limit}); }
|
||||
elsif ($fn eq 'write' || $fn eq 'write_file') {
|
||||
my ($p, $cnt) = ($a->{filePath} // $a->{path}, defined $a->{content} ? $a->{content} : '');
|
||||
if (!(exists $a->{offset} && defined $a->{offset}) && !(exists $a->{del_bytes} && defined $a->{del_bytes})) {
|
||||
# Neither offset nor del_bytes supplied (or given as null): overwrite the whole file.
|
||||
if (!defined $p || $p eq '') { $res = "[write_file error: path required]"; }
|
||||
if (!defined $p || $p eq '') { $res = "[write error: filePath required]"; }
|
||||
else {
|
||||
my $dir = $p =~ m{^(.*)/[^/]+$} ? $1 : ''; make_path($dir) if length($dir) && !-d $dir;
|
||||
open my $wfh, '>:raw', $p or $res = "[write_file error: cannot write $p: $!]";
|
||||
open my $wfh, '>:raw', $p or $res = "[write error: cannot write $p: $!]";
|
||||
if (!$res) { print $wfh $cnt; close $wfh; $res = "Successfully wrote " . length($cnt) . " bytes to $p"; }
|
||||
}
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user