tool format hardening
This commit is contained in:
@@ -94,10 +94,27 @@ sub shell_exec {
|
|||||||
return trim($res) . "\n\nexit: $code";
|
return trim($res) . "\n\nexit: $code";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub sanitize_msgs {
|
||||||
|
my ($msgs) = @_;
|
||||||
|
return unless $msgs && ref($msgs) eq 'ARRAY';
|
||||||
|
for my $m (@$msgs) {
|
||||||
|
next unless ref($m) eq 'HASH' && ($m->{role} // '') eq 'assistant' && $m->{tool_calls};
|
||||||
|
for my $tc (@{$m->{tool_calls}}) {
|
||||||
|
next unless ref($tc) eq 'HASH' && $tc->{function};
|
||||||
|
my $astr = $tc->{function}{arguments} // '{}';
|
||||||
|
my $a = eval { decode_json($astr) };
|
||||||
|
if ($@ || ref($a) ne 'HASH') {
|
||||||
|
$tc->{function}{arguments} = encode_json({ invalid_raw => $astr // '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
my @fib = (1, 1, 2, 3, 5, 8, 13, 21, 34);
|
my @fib = (1, 1, 2, 3, 5, 8, 13, 21, 34);
|
||||||
|
|
||||||
sub llm {
|
sub llm {
|
||||||
my ($cfg, $msgs, $tools) = @_;
|
my ($cfg, $msgs, $tools) = @_;
|
||||||
|
sanitize_msgs($msgs);
|
||||||
my $ep = $cfg->{endpoint} =~ s/\/+$//r;
|
my $ep = $cfg->{endpoint} =~ s/\/+$//r;
|
||||||
my $url = "$ep/chat/completions";
|
my $url = "$ep/chat/completions";
|
||||||
my %h = ('Content-Type' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (compatible; Bantam/1.0)');
|
my %h = ('Content-Type' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (compatible; Bantam/1.0)');
|
||||||
@@ -119,8 +136,10 @@ sub llm {
|
|||||||
my $d = eval { decode_json($res->{content}) };
|
my $d = eval { decode_json($res->{content}) };
|
||||||
return $d->{choices}[0]{message} if $d && $d->{choices} && @{$d->{choices}};
|
return $d->{choices}[0]{message} if $d && $d->{choices} && @{$d->{choices}};
|
||||||
}
|
}
|
||||||
my $err = $res->{reason} || $res->{content} || "HTTP status $res->{status}";
|
my $status = $res->{status} // 0;
|
||||||
|
my $err = $res->{reason} || $res->{content} || "HTTP status $status";
|
||||||
print "\r\e[K" if $_col; STDOUT->flush();
|
print "\r\e[K" if $_col; STDOUT->flush();
|
||||||
|
die "[HTTP error: $err]\n" if $status >= 400 && $status < 500 && $status != 408 && $status != 429;
|
||||||
if ($i <= $#fib) { print c("[network error: $err, retrying in ${dly}s...]", 31), "\n"; sleep($dly); next; }
|
if ($i <= $#fib) { print c("[network error: $err, retrying in ${dly}s...]", 31), "\n"; sleep($dly); next; }
|
||||||
die "[network error: $err]\n";
|
die "[network error: $err]\n";
|
||||||
} else {
|
} else {
|
||||||
@@ -175,8 +194,10 @@ sub llm {
|
|||||||
$msg{tool_calls} = [map { $tcs{$_} } @order] if @order;
|
$msg{tool_calls} = [map { $tcs{$_} } @order] if @order;
|
||||||
return \%msg;
|
return \%msg;
|
||||||
}
|
}
|
||||||
my $err = $res->{reason} || $res->{content} || "HTTP status $res->{status}";
|
my $status = $res->{status} // 0;
|
||||||
|
my $err = $res->{reason} || $res->{content} || "HTTP status $status";
|
||||||
print "\r\e[K" if $_col; STDOUT->flush();
|
print "\r\e[K" if $_col; STDOUT->flush();
|
||||||
|
die "[HTTP error: $err]\n" if $status >= 400 && $status < 500 && $status != 408 && $status != 429;
|
||||||
if ($i <= $#fib) { print c("[network error: $err, retrying in ${dly}s...]", 31), "\n"; sleep($dly); next; }
|
if ($i <= $#fib) { print c("[network error: $err, retrying in ${dly}s...]", 31), "\n"; sleep($dly); next; }
|
||||||
die "[network error: $err]\n";
|
die "[network error: $err]\n";
|
||||||
}
|
}
|
||||||
@@ -208,6 +229,7 @@ sub AL {
|
|||||||
my ($res, $sty) = ('', 2);
|
my ($res, $sty) = ('', 2);
|
||||||
my $a = eval { decode_json($astr) };
|
my $a = eval { decode_json($astr) };
|
||||||
if ($@ || ref($a) ne 'HASH') {
|
if ($@ || ref($a) ne 'HASH') {
|
||||||
|
$tc->{function}{arguments} = encode_json({ invalid_raw => $astr });
|
||||||
$res = "[tool error: invalid JSON args for $fn: " . ($@ || 'not a JSON object') . ". Raw: '$astr']"; $sty = 31;
|
$res = "[tool error: invalid JSON args for $fn: " . ($@ || 'not a JSON object') . ". Raw: '$astr']"; $sty = 31;
|
||||||
} elsif ($fn eq 'shell_exec') {
|
} elsif ($fn eq 'shell_exec') {
|
||||||
$res = shell_exec($a->{command} // '', $stime);
|
$res = shell_exec($a->{command} // '', $stime);
|
||||||
|
|||||||
@@ -71,9 +71,25 @@ def shell(cmd, t=120):
|
|||||||
if isinstance(err, bytes): err = err.decode("utf-8", "replace")
|
if isinstance(err, bytes): err = err.decode("utf-8", "replace")
|
||||||
return f"{(out + err).strip()}\n\n[shell timeout after {t}s]\nexit: -1"
|
return f"{(out + err).strip()}\n\n[shell timeout after {t}s]\nexit: -1"
|
||||||
|
|
||||||
|
def sanitize_msgs(msgs):
|
||||||
|
for m in msgs:
|
||||||
|
if isinstance(m, dict) and m.get("role") == "assistant" and "tool_calls" in m:
|
||||||
|
tcs = m.get("tool_calls") or []
|
||||||
|
for tc in tcs:
|
||||||
|
if isinstance(tc, dict) and "function" in tc:
|
||||||
|
fn = tc.get("function") or {}
|
||||||
|
astr = fn.get("arguments", "{}")
|
||||||
|
try:
|
||||||
|
p = json.loads(astr) if astr else {}
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
fn["arguments"] = json.dumps({"invalid_raw": astr} if astr else {})
|
||||||
|
except Exception:
|
||||||
|
fn["arguments"] = json.dumps({"invalid_raw": astr} if astr else {})
|
||||||
|
|
||||||
fib = [1, 1, 2, 3, 5, 8, 13, 21, 34]
|
fib = [1, 1, 2, 3, 5, 8, 13, 21, 34]
|
||||||
|
|
||||||
def llm(cfg, msgs, tools):
|
def llm(cfg, msgs, tools):
|
||||||
|
sanitize_msgs(msgs)
|
||||||
url = cfg["endpoint"].rstrip("/") + "/chat/completions"
|
url = cfg["endpoint"].rstrip("/") + "/chat/completions"
|
||||||
h = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (compatible; Bantam/1.0)"}
|
h = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (compatible; Bantam/1.0)"}
|
||||||
k = cfg.get("api_key", "").strip()
|
k = cfg.get("api_key", "").strip()
|
||||||
@@ -126,6 +142,11 @@ def llm(cfg, msgs, tools):
|
|||||||
return m
|
return m
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if _COL: sys.stdout.write("\r\033[K"); sys.stdout.flush()
|
if _COL: sys.stdout.write("\r\033[K"); sys.stdout.flush()
|
||||||
|
if isinstance(e, urllib.error.HTTPError):
|
||||||
|
code = e.code
|
||||||
|
body = e.read().decode("utf-8", "replace").strip() if hasattr(e, "read") else str(e)
|
||||||
|
if 400 <= code < 500 and code not in (408, 429):
|
||||||
|
raise RuntimeError(f"HTTP {code}: {body}")
|
||||||
if i < len(fib): print(c(f"[network error: {e}, retrying in {dly}s...]", 31)); time.sleep(dly)
|
if i < len(fib): print(c(f"[network error: {e}, retrying in {dly}s...]", 31)); time.sleep(dly)
|
||||||
else: raise
|
else: raise
|
||||||
|
|
||||||
@@ -148,7 +169,9 @@ def AL(cfg, msgs, sp, depth=0):
|
|||||||
a = json.loads(astr) if astr else {}
|
a = json.loads(astr) if astr else {}
|
||||||
if not isinstance(a, dict): raise ValueError("args must be a JSON object")
|
if not isinstance(a, dict): raise ValueError("args must be a JSON object")
|
||||||
err = ""
|
err = ""
|
||||||
except Exception as e: err, a = str(e), {}
|
except Exception as e:
|
||||||
|
err, a = str(e), {}
|
||||||
|
tc["function"]["arguments"] = json.dumps({"invalid_raw": astr} if astr else {})
|
||||||
if err: res, sty = f"[tool error: invalid JSON args for {fn}: {err}. Raw: {astr!r}]", 31
|
if err: res, sty = f"[tool error: invalid JSON args for {fn}: {err}. Raw: {astr!r}]", 31
|
||||||
elif fn == "shell_exec": res, sty = shell(a.get("command", ""), stime), 2
|
elif fn == "shell_exec": res, sty = shell(a.get("command", ""), stime), 2
|
||||||
elif fn == "run_subagent":
|
elif fn == "run_subagent":
|
||||||
|
|||||||
@@ -157,7 +157,24 @@ type streamDelta struct {
|
|||||||
} `json:"choices"`
|
} `json:"choices"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sanitizeMessages(msgs []Message) {
|
||||||
|
for i := range msgs {
|
||||||
|
if msgs[i].Role == "assistant" && len(msgs[i].ToolCalls) > 0 {
|
||||||
|
for j := range msgs[i].ToolCalls {
|
||||||
|
tc := &msgs[i].ToolCalls[j]
|
||||||
|
astr := tc.Function.Arguments
|
||||||
|
var a map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {
|
||||||
|
fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr})
|
||||||
|
tc.Function.Arguments = string(fixed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
||||||
|
sanitizeMessages(msgs)
|
||||||
p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": msgs, "stream": cfg.Stream}
|
p := map[string]any{"model": cfg.Model, "temperature": cfg.Temperature, "messages": msgs, "stream": cfg.Stream}
|
||||||
if tools != nil { p["tools"] = tools }
|
if tools != nil { p["tools"] = tools }
|
||||||
body, _ := json.Marshal(p)
|
body, _ := json.Marshal(p)
|
||||||
@@ -176,14 +193,21 @@ func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
|
|||||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Bantam/1.0)")
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Bantam/1.0)")
|
||||||
if cfg.APIKey != "" && cfg.APIKey != "-" { req.Header.Set("Authorization", "Bearer "+cfg.APIKey) }
|
if cfg.APIKey != "" && cfg.APIKey != "-" { req.Header.Set("Authorization", "Bearer "+cfg.APIKey) }
|
||||||
resp, err = client.Do(req)
|
resp, err = client.Do(req)
|
||||||
|
var is4xxClientErr bool
|
||||||
if err == nil && resp.StatusCode >= 400 {
|
if err == nil && resp.StatusCode >= 400 {
|
||||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
b, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
err = fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
err = fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
||||||
|
if resp.StatusCode < 500 && resp.StatusCode != 408 && resp.StatusCode != 429 {
|
||||||
|
is4xxClientErr = true
|
||||||
|
}
|
||||||
resp = nil
|
resp = nil
|
||||||
}
|
}
|
||||||
if err == nil { break }
|
if err == nil { break }
|
||||||
if COL { fmt.Print("\r\033[K") }
|
if COL { fmt.Print("\r\033[K") }
|
||||||
|
if is4xxClientErr {
|
||||||
|
return Message{}, err
|
||||||
|
}
|
||||||
if i < len(fib) {
|
if i < len(fib) {
|
||||||
fmt.Println(c(fmt.Sprintf("[network error: %v, retrying in %ds...]", err, fib[i]), 31))
|
fmt.Println(c(fmt.Sprintf("[network error: %v, retrying in %ds...]", err, fib[i]), 31))
|
||||||
time.Sleep(time.Duration(fib[i]) * time.Second)
|
time.Sleep(time.Duration(fib[i]) * time.Second)
|
||||||
@@ -301,6 +325,15 @@ func AL(cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
|
|||||||
for i := 0; i < cfg.MaxALIterations && !done; i++ {
|
for i := 0; i < cfg.MaxALIterations && !done; i++ {
|
||||||
m, err := llm(cfg, msgs, TOOLS)
|
m, err := llm(cfg, msgs, TOOLS)
|
||||||
if err != nil { return msgs, err }
|
if err != nil { return msgs, err }
|
||||||
|
for j := range m.ToolCalls {
|
||||||
|
tc := &m.ToolCalls[j]
|
||||||
|
astr := tc.Function.Arguments
|
||||||
|
var a map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(astr), &a); err != nil || a == nil {
|
||||||
|
fixed, _ := json.Marshal(map[string]string{"invalid_raw": astr})
|
||||||
|
tc.Function.Arguments = string(fixed)
|
||||||
|
}
|
||||||
|
}
|
||||||
msgs = append(msgs, m)
|
msgs = append(msgs, m)
|
||||||
if !cfg.Stream {
|
if !cfg.Stream {
|
||||||
if m.ReasoningContent != "" {
|
if m.ReasoningContent != "" {
|
||||||
|
|||||||
@@ -4,13 +4,9 @@
|
|||||||
|
|
||||||
use strict; use warnings; use HTTP::Tiny; use JSON::PP; use POSIX qw(strftime); use File::Path qw(make_path);
|
use strict; use warnings; use HTTP::Tiny; use JSON::PP; use POSIX qw(strftime); use File::Path qw(make_path);
|
||||||
|
|
||||||
$SIG{__WARN__} = sub {
|
$SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /^Use of uninitialized value \$err in numeric eq \(==\) at .*IO\/Socket\/IP\.pm line \d+/ };
|
||||||
my $m = shift;
|
|
||||||
return if $m =~ /^Use of uninitialized value \$err in numeric eq \(==\) at .*IO\/Socket\/IP\.pm line \d+/;
|
|
||||||
warn $m;
|
|
||||||
};
|
|
||||||
|
|
||||||
binmode(STDOUT, ':encoding(UTF-8)');binmode(STDERR, ':encoding(UTF-8)');binmode(STDIN, ':encoding(UTF-8)');
|
binmode(STDOUT, ':encoding(UTF-8)'); binmode(STDERR, ':encoding(UTF-8)'); binmode(STDIN, ':encoding(UTF-8)');
|
||||||
|
|
||||||
$| = 1; # unbuffered output: live spinner, responsive prompt
|
$| = 1; # unbuffered output: live spinner, responsive prompt
|
||||||
|
|
||||||
@@ -29,7 +25,10 @@ sub sp { my $p = '';
|
|||||||
|
|
||||||
sub T { my ($n, $d, $p) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>[keys %$p]}}} }
|
sub T { my ($n, $d, $p) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>[keys %$p]}}} }
|
||||||
|
|
||||||
|
sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH' && $m->{tool_calls}) { for my $tc (@{$m->{tool_calls}}) { my $a = eval { decode_json($tc->{function}{arguments} // '{}') }; $tc->{function}{arguments} = encode_json({invalid_raw => $tc->{function}{arguments} // ''}) if ref $a ne 'HASH'; } } } }
|
||||||
|
|
||||||
sub llm { my ($c, $msgs) = @_; # one non-streaming chat completion
|
sub llm { my ($c, $msgs) = @_; # one non-streaming chat completion
|
||||||
|
sanitize_msgs($msgs);
|
||||||
my $ep = $c->{endpoint}; $ep =~ s{/+$}{};
|
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'=>'Mozilla/5.0 (compatible; MicroBantam/1.0)'};
|
||||||
$h->{Authorization} = "Bearer $c->{api_key}" if $c->{api_key} ne '-';
|
$h->{Authorization} = "Bearer $c->{api_key}" if $c->{api_key} ne '-';
|
||||||
@@ -66,11 +65,9 @@ sub AL { my ($c, $msgs, $sp, $depth) = @_; # the agentic loop: LLM <-> tools unt
|
|||||||
my $fn = $tc->{function}{name};
|
my $fn = $tc->{function}{name};
|
||||||
my $a = eval { decode_json($tc->{function}{arguments} // '{}') };
|
my $a = eval { decode_json($tc->{function}{arguments} // '{}') };
|
||||||
my $res;
|
my $res;
|
||||||
if (ref $a ne 'HASH') { $res = "bad JSON args for $fn: $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 '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]'
|
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)); }
|
||||||
: 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"; }
|
else { $res = "unknown tool: $fn"; }
|
||||||
print "[tool] $fn: $res\n";
|
print "[tool] $fn: $res\n";
|
||||||
push @$msgs, {role=>'tool', tool_call_id=>$tc->{id}, content=>$res};
|
push @$msgs, {role=>'tool', tool_call_id=>$tc->{id}, content=>$res};
|
||||||
@@ -86,8 +83,7 @@ sub save { sdir(); my $id = strftime('%Y%m%d-%H%M%S', localtime); my $i = 0;
|
|||||||
$id .= '-' . ++$i while -f "$SDIR/$id.json";
|
$id .= '-' . ++$i while -f "$SDIR/$id.json";
|
||||||
open my $f, '>', "$SDIR/$id.json" or die "cannot save: $!";
|
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; }
|
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();
|
sub load { my ($want) = @_; my ($hit) = grep { $_->{id} eq $want } sessions(); die "no session: $want\n" unless $hit; $hit->{messages}; }
|
||||||
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 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 list_sessions { map { [$_->{id}, scalar @{$_->{messages} // []}] } sessions() }
|
||||||
|
|
||||||
@@ -95,14 +91,11 @@ sub main {
|
|||||||
my ($c, $sp) = (cfg(), sp());
|
my ($c, $sp) = (cfg(), sp());
|
||||||
my $msgs = [{role=>'system', content=>$sp}];
|
my $msgs = [{role=>'system', content=>$sp}];
|
||||||
if (@ARGV) { open my $f, '<:encoding(UTF-8)', $ARGV[0] or die "cannot open $ARGV[0]: $!"; # file mode
|
if (@ARGV) { open my $f, '<:encoding(UTF-8)', $ARGV[0] or die "cannot open $ARGV[0]: $!"; # file mode
|
||||||
local $/; push @$msgs, {role=>'user', content=><$f>};
|
local $/; push @$msgs, {role=>'user', content=><$f>}; AL($c, $msgs, $sp); autosave($msgs); return; }
|
||||||
AL($c, $msgs, $sp); autosave($msgs); return; }
|
|
||||||
print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save /list /load <id> /help\n";
|
print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save /list /load <id> /help\n";
|
||||||
while (1) {
|
while (1) {
|
||||||
print "> "; my $u = <STDIN>;
|
print "> "; my $u = <STDIN>; last unless defined $u;
|
||||||
last unless defined $u;
|
$u =~ s/^\s+|\s+$//g; next unless length $u;
|
||||||
$u =~ s/^\s+|\s+$//g;
|
|
||||||
next unless length $u;
|
|
||||||
if ($u eq '/quit') { last; }
|
if ($u eq '/quit') { last; }
|
||||||
elsif ($u eq '/clear') { $msgs = [{role=>'system', content=>$sp}]; autosave($msgs); }
|
elsif ($u eq '/clear') { $msgs = [{role=>'system', content=>$sp}]; autosave($msgs); }
|
||||||
elsif ($u eq '/save') { print "session saved: ", save($msgs), "\n"; }
|
elsif ($u eq '/save') { print "session saved: ", save($msgs), "\n"; }
|
||||||
|
|||||||
Reference in New Issue
Block a user