many improvements

This commit is contained in:
Luxferre
2026-08-15 08:27:24 +03:00
parent 398754dabb
commit e6acb066ca
9 changed files with 283 additions and 22 deletions
+5 -4
View File
@@ -31,7 +31,7 @@ go build ./... # produces ./bantam
go run . prompt.txt go run . prompt.txt
``` ```
The Go port is a single `main.go` plus four platform files (`term_linux.go`, `term_darwin.go`, `term_windows.go`, `term_other.go`) for the built-in raw-terminal line editor — zero external dependencies, same as the Python and Perl versions. The Go port is a single `main.go` plus four platform files (`term_linux.go`, `term_bsd.go`, `term_windows.go`, `term_other.go`) for the built-in raw-terminal line editor — zero external dependencies, same as the Python and Perl versions.
### Running Bantam ### Running Bantam
@@ -60,6 +60,7 @@ All implementations read the same `model.cfg` and `system.txt` from the current
- `/list` — list saved sessions (newest first) with their ids, timestamps, message counts and summaries - `/list` — list saved sessions (newest first) with their ids, timestamps, message counts and summaries
- `/load <id>` — load a saved session (exact id or unique prefix) and continue from there - `/load <id>` — load a saved session (exact id or unique prefix) and continue from there
- `/compact` — summarize the conversation with the LLM and compact the context down to just the system message plus the summary - `/compact` — summarize the conversation with the LLM and compact the context down to just the system message plus the summary
- `/cfg <param> [val]` — inspect or update a configuration parameter in `model.cfg` live
- `/help` — show all supported commands - `/help` — show all supported commands
- `/clear` — reset the conversation to just the system prompt - `/clear` — reset the conversation to just the system prompt
- `/quit` — exit - `/quit` — exit
@@ -94,12 +95,12 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
2. Read model parameters from `model.cfg` (`key=value` format). 2. Read model parameters from `model.cfg` (`key=value` format).
3. Prepare a new message list with the system prompt (`role: "system"`). 3. Prepare a new message list with the system prompt (`role: "system"`).
4. Read the first command-line parameter. If non-empty, read user prompt from the specified file, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, and exit. 4. Read the first command-line parameter. If non-empty, read user prompt from the specified file, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, and exit.
5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If equal to `/save`, write the whole `messages` array to `~/.bantam/sessions/<id>.json` (with an auto-generated summary) and return to step 5. If equal to `/list`, print saved sessions and their summaries and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation, replace `messages` with `[system, summary-user-message]`, and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/autosave.json`. 5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If equal to `/save`, write the whole `messages` array to `~/.bantam/sessions/<id>.json` (with an auto-generated summary) and return to step 5. If equal to `/list`, print saved sessions and their summaries and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg <param>`) or update `model.cfg` live (`/cfg <param> <val>`) and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/autosave.json`.
6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, and go to step 5. 6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, and go to step 5.
### Agentic loop (`AL(cfg, messages)`) function ### Agentic loop (`AL(cfg, messages)`) function
1. Call OpenAI-compatible Completions API (`POST {endpoint}/chat/completions`) using parameters from `cfg` (`model`, `temperature`, optional `api_key` bearer header). 1. Call OpenAI-compatible Completions API (`POST {endpoint}/chat/completions`) forwarding relevant parameters from `cfg` (`model`, `temperature`, `stream`, `reasoning_effort`, etc., excluding internal agent configs like `endpoint`, `api_key`, `timeout`, `shell_timeout`, `max_al_iterations`, `color`) and optional `api_key` bearer header.
- Set custom `User-Agent` header (`Mozilla/5.0 (compatible; Bantam/1.0)`) to avoid gateway 403 blocks. - Set custom `User-Agent` header (`Mozilla/5.0 (compatible; Bantam/1.0)`) to avoid gateway 403 blocks.
- Retry network/HTTP errors with Fibonacci backoff delays (`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`). - Retry network/HTTP errors with Fibonacci backoff delays (`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`).
- If `stream=true`, parse SSE stream (`data: {...}`) for real-time reasoning and text output, bracketing reasoning with `--- reasoning start ---` / `--- reasoning end ---` markers. - If `stream=true`, parse SSE stream (`data: {...}`) for real-time reasoning and text output, bracketing reasoning with `--- reasoning start ---` / `--- reasoning end ---` markers.
@@ -187,7 +188,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
## Repository layout ## Repository layout
- `bantam.py` — Python reference implementation (stdlib only) - `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`) - `main.go`, `term_linux.go`, `term_bsd.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) - `bantam.pl` — Perl 5 implementation (core modules only)
- `mb` — MicroBantam, compressed Perl 5 implementation (core modules only, under 100 SLOC) - `mb` — MicroBantam, compressed Perl 5 implementation (core modules only, under 100 SLOC)
- `mb.tcl` — MicroBantam, Jim Tcl port (requires `jimsh` with the `json` and `ssl` extensions; under 100 SLOC) - `mb.tcl` — MicroBantam, Jim Tcl port (requires `jimsh` with the `json` and `ssl` extensions; under 100 SLOC)
+51 -2
View File
@@ -55,6 +55,33 @@ sub get_cfg {
return \%d; return \%d;
} }
sub set_cfg {
my ($path, $k, $v) = @_;
$path //= 'model.cfg';
my (@lines, $found);
if (-f $path) {
open my $fh, '<:encoding(UTF-8)', $path or return;
while (my $ln = <$fh>) {
my $s = trim($ln);
if ($s !~ /^#/ && $s =~ /=/) {
my ($pk) = split /=/, $s, 2;
if (trim($pk) eq $k) {
push @lines, "$k=$v\n";
$found = 1;
next;
}
}
push @lines, $ln;
}
}
if (!$found) {
push @lines, "\n" if @lines && $lines[-1] !~ /\n$/;
push @lines, "$k=$v\n";
}
open my $fh, '>:encoding(UTF-8)', $path or return;
print $fh @lines;
}
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 $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.";
sub prompt { sub prompt {
@@ -118,6 +145,13 @@ sub llm {
$h{Authorization} = "Bearer $cfg->{api_key}" if $cfg->{api_key} && $cfg->{api_key} ne '-'; $h{Authorization} = "Bearer $cfg->{api_key}" if $cfg->{api_key} && $cfg->{api_key} ne '-';
my $st = ($cfg->{stream} // 'true') =~ /^(true|1|yes)$/i; my $st = ($cfg->{stream} // 'true') =~ /^(true|1|yes)$/i;
my %p = (model => $cfg->{model}, temperature => 0 + ($cfg->{temperature} // 0.7), messages => $msgs, stream => $st ? \1 : \0); my %p = (model => $cfg->{model}, temperature => 0 + ($cfg->{temperature} // 0.7), messages => $msgs, stream => $st ? \1 : \0);
for my $k (keys %$cfg) {
next if $k =~ /^(endpoint|api_key|timeout|shell_timeout|max_al_iterations|color)$/;
my $val = eval { decode_json($cfg->{$k}) };
$p{$k} = defined $val ? $val : $cfg->{$k};
}
$p{messages} = $msgs;
$p{stream} = $st ? \1 : \0;
$p{tools} = $tools if $tools && @$tools; $p{tools} = $tools if $tools && @$tools;
my $body = encode_json(\%p); my $body = encode_json(\%p);
my $http = HTTP::Tiny->new(timeout => 0 + ($cfg->{timeout} // 300)); my $http = HTTP::Tiny->new(timeout => 0 + ($cfg->{timeout} // 300));
@@ -411,10 +445,25 @@ sub main {
print c("[compacted to " . scalar(@$msgs) . " messages]", 32), "\n"; print c("[compacted to " . scalar(@$msgs) . " messages]", 32), "\n";
print c("--- summary ---", 33), "\n", c($sm, 2), "\n"; print c("--- summary ---", 33), "\n", c($sm, 2), "\n";
next; next;
} elsif ($u =~ /^\/cfg(?:\s+(\S+)(?:\s+(.+))?)?$/) {
my ($k, $v) = ($1, $2);
if (defined $v) {
$v = trim($v);
set_cfg('model.cfg', $k, $v);
$cfg = get_cfg('model.cfg');
$_col = col($cfg);
print c("[config updated: $k=$v]", 32), "\n";
} elsif (defined $k) {
if (exists $cfg->{$k}) { print c("$k=$cfg->{$k}", 32), "\n"; }
else { print c("$k not set", 31), "\n"; }
} else {
print c("Usage: /cfg <param> [val]", 31), "\n";
}
next;
} elsif ($u eq '/help') { } elsif ($u eq '/help') {
print c("Bantam commands:", 1, 36), "\n"; print c("Bantam commands:", 1, 36), "\n";
my @cmds = (["/quit", "exit"], ["/clear", "reset to system prompt"], ["/save", "save session"], ["/list", "list sessions"], ["/load <id>", "load session"], ["/compact", "compact context"], ["/help", "show help"]); my @cmds = (["/quit", "exit"], ["/clear", "reset to system prompt"], ["/save", "save session"], ["/list", "list sessions"], ["/load <id>", "load session"], ["/compact", "compact context"], ["/cfg <k> [v]", "get/set config"], ["/help", "show help"]);
for my $kv (@cmds) { printf "%s%s\n", c(sprintf(" %-12s", $kv->[0]), 1, 32), $kv->[1]; } for my $kv (@cmds) { printf "%s%s\n", c(sprintf(" %-15s", $kv->[0]), 1, 32), $kv->[1]; }
next; next;
} }
push @$msgs, { role => 'user', content => $u }; push @$msgs, { role => 'user', content => $u };
+36 -2
View File
@@ -35,6 +35,21 @@ def get_cfg(path="model.cfg"):
if not d.get("api_key") and "OPENAI_API_KEY" in os.environ: d["api_key"] = os.environ["OPENAI_API_KEY"] if not d.get("api_key") and "OPENAI_API_KEY" in os.environ: d["api_key"] = os.environ["OPENAI_API_KEY"]
return d return d
def set_cfg(path, k, v):
lines, found = [], False
if os.path.exists(path):
for ln in open(path, encoding="utf-8"):
s = ln.strip()
if not s.startswith("#") and "=" in s and s.split("=", 1)[0].strip() == k:
lines.append(f"{k}={v}\n")
found = True
else:
lines.append(ln)
if not found:
if lines and not lines[-1].endswith("\n"): lines[-1] += "\n"
lines.append(f"{k}={v}\n")
with open(path, "w", encoding="utf-8") as f: f.writelines(lines)
def num(cfg, k, d): def num(cfg, k, d):
try: return type(d)(cfg.get(k, d)) try: return type(d)(cfg.get(k, d))
except (TypeError, ValueError): return d except (TypeError, ValueError): return d
@@ -99,6 +114,11 @@ def llm(cfg, msgs, tools):
if k and k != "-": h["Authorization"] = "Bearer " + k if k and k != "-": h["Authorization"] = "Bearer " + k
st = cfg.get("stream", "true").lower() in ("true", "1", "yes") st = cfg.get("stream", "true").lower() in ("true", "1", "yes")
p = {"model": cfg["model"], "temperature": float(cfg.get("temperature", 0.7)), "messages": msgs, "stream": st} p = {"model": cfg["model"], "temperature": float(cfg.get("temperature", 0.7)), "messages": msgs, "stream": st}
for k, v in cfg.items():
if k in ("endpoint", "api_key", "timeout", "shell_timeout", "max_al_iterations", "color"): continue
try: p[k] = json.loads(v)
except Exception: p[k] = v
p["messages"], p["stream"] = msgs, st
if tools: p["tools"] = tools if tools: p["tools"] = tools
pend = c("...requesting...", 1, 2) pend = c("...requesting...", 1, 2)
for i, dly in enumerate(fib + [0]): for i, dly in enumerate(fib + [0]):
@@ -331,10 +351,24 @@ def main():
msgs = nm; autosave(msgs) msgs = nm; autosave(msgs)
print(c(f"[compacted to {len(msgs)} messages]", 32)); print(c("--- summary ---", 33) + "\n" + c(sm, 2)) print(c(f"[compacted to {len(msgs)} messages]", 32)); print(c("--- summary ---", 33) + "\n" + c(sm, 2))
continue continue
elif u.startswith("/cfg"):
parts = u.split(None, 2)
if len(parts) == 2:
k = parts[1]
if k in cfg: print(c(f"{k}={cfg[k]}", 32))
else: print(c(f"{k} not set", 31))
elif len(parts) >= 3:
k, v = parts[1], parts[2]
set_cfg("model.cfg", k, v)
cfg = get_cfg("model.cfg")
_COL = col(cfg)
print(c(f"[config updated: {k}={v}]", 32))
else: print(c("Usage: /cfg <param> [val]", 31))
continue
elif u == "/help": elif u == "/help":
print(c("Bantam commands:", 1, 36)) print(c("Bantam commands:", 1, 36))
for k, v in [("/quit", "exit"), ("/clear", "reset to system prompt"), ("/save", "save session"), ("/list", "list sessions"), ("/load <id>", "load session"), ("/compact", "compact context"), ("/help", "show help")]: for k, v in [("/quit", "exit"), ("/clear", "reset to system prompt"), ("/save", "save session"), ("/list", "list sessions"), ("/load <id>", "load session"), ("/compact", "compact context"), ("/cfg <k> [v]", "get/set config"), ("/help", "show help")]:
print(c(f" {k:<12}", 1, 32) + v) print(c(f" {k:<15}", 1, 32) + v)
continue continue
msgs.append({"role": "user", "content": u}) msgs.append({"role": "user", "content": u})
AL(cfg, msgs, sp); autosave(msgs) AL(cfg, msgs, sp); autosave(msgs)
+91 -4
View File
@@ -42,9 +42,10 @@ type Cfg struct {
MaxALIterations int MaxALIterations int
Stream bool Stream bool
Color string Color string
Raw map[string]string
} }
var defCfg = Cfg{"https://opencode.ai/zen/v1", "big-pickle", "-", 0.7, 300, 120, 1000, true, "auto"} var defCfg = Cfg{"https://opencode.ai/zen/v1", "big-pickle", "-", 0.7, 300, 120, 1000, true, "auto", nil}
func atoiD(s string, d int) int { func atoiD(s string, d int) int {
if v, e := strconv.Atoi(strings.TrimSpace(s)); e == nil { if v, e := strconv.Atoi(strings.TrimSpace(s)); e == nil {
@@ -55,12 +56,19 @@ func atoiD(s string, d int) int {
func getCfg(path string) Cfg { func getCfg(path string) Cfg {
cfg := defCfg cfg := defCfg
cfg.Raw = map[string]string{
"endpoint": cfg.Endpoint, "model": cfg.Model, "temperature": fmt.Sprintf("%v", cfg.Temperature),
"api_key": cfg.APIKey, "stream": strconv.FormatBool(cfg.Stream), "color": cfg.Color,
"timeout": strconv.Itoa(cfg.Timeout), "shell_timeout": strconv.Itoa(cfg.ShellTimeout),
"max_al_iterations": strconv.Itoa(cfg.MaxALIterations),
}
if d, err := os.ReadFile(path); err == nil { if d, err := os.ReadFile(path); err == nil {
for _, ln := range strings.Split(string(d), "\n") { for _, ln := range strings.Split(string(d), "\n") {
ln = strings.TrimSpace(ln) ln = strings.TrimSpace(ln)
if ln == "" || ln[0] == '#' || !strings.Contains(ln, "=") { continue } if ln == "" || ln[0] == '#' || !strings.Contains(ln, "=") { continue }
k, v, _ := strings.Cut(ln, "=") k, v, _ := strings.Cut(ln, "=")
k, v = strings.TrimSpace(k), strings.TrimSpace(v) k, v = strings.TrimSpace(k), strings.TrimSpace(v)
cfg.Raw[k] = v
switch k { switch k {
case "endpoint": cfg.Endpoint = v case "endpoint": cfg.Endpoint = v
case "model": cfg.Model = v case "model": cfg.Model = v
@@ -74,10 +82,41 @@ func getCfg(path string) Cfg {
} }
} }
} }
if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("OPENAI_API_KEY") != "" { cfg.APIKey = os.Getenv("OPENAI_API_KEY") } if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("OPENAI_API_KEY") != "" {
cfg.APIKey = os.Getenv("OPENAI_API_KEY")
cfg.Raw["api_key"] = cfg.APIKey
}
return cfg return cfg
} }
func setCfg(path, key, val string) error {
var lines []string
found := false
if d, err := os.ReadFile(path); err == nil {
for _, ln := range strings.Split(string(d), "\n") {
trimmed := strings.TrimSpace(ln)
if !strings.HasPrefix(trimmed, "#") && strings.Contains(trimmed, "=") {
k, _, _ := strings.Cut(trimmed, "=")
if strings.TrimSpace(k) == key {
lines = append(lines, key+"="+val)
found = true
continue
}
}
lines = append(lines, ln)
}
}
if !found {
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines[len(lines)-1] = key + "=" + val
lines = append(lines, "")
} else {
lines = append(lines, key+"="+val)
}
}
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
}
const defaultSystemPrompt = `You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools: const defaultSystemPrompt = `You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:
- shell_exec: run a shell command; returns its output and exit code. - shell_exec: run a shell command; returns its output and exit code.
- run_subagent: delegate a sub-task to a child agent; returns its reply. - run_subagent: delegate a sub-task to a child agent; returns its reply.
@@ -182,6 +221,19 @@ func llm(cfg *Cfg, msgs []Message, tools []map[string]any) (Message, error) {
sanitizeMessages(msgs) 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 }
for k, v := range cfg.Raw {
switch k {
case "endpoint", "api_key", "timeout", "shell_timeout", "max_al_iterations", "color":
continue
default:
var jv any
if err := json.Unmarshal([]byte(v), &jv); err == nil {
p[k] = jv
} else {
p[k] = v
}
}
}
body, _ := json.Marshal(p) body, _ := json.Marshal(p)
client := &http.Client{Transport: &http.Transport{ client := &http.Client{Transport: &http.Transport{
DialContext: (&net.Dialer{Timeout: time.Duration(cfg.Timeout) * time.Second}).DialContext, DialContext: (&net.Dialer{Timeout: time.Duration(cfg.Timeout) * time.Second}).DialContext,
@@ -669,6 +721,19 @@ func readLine(prompt string) (string, bool) {
} }
switch rn { switch rn {
case '\r': case '\r':
if stdin.Buffered() > 0 {
if b, _ := stdin.Peek(1); len(b) > 0 && b[0] == '\n' {
stdin.ReadByte()
}
}
W := termWidth()
s := string(e.buf)
P := visibleLen(e.prompt)
er, _ := textPos(P, W, s, len([]rune(s)))
pr, _ := textPos(P, W, s, e.pos)
if down := er - pr; down > 0 {
fmt.Printf("\033[%dB", down)
}
fmt.Print("\r\n") fmt.Print("\r\n")
return string(e.buf), true return string(e.buf), true
case '\n': case '\n':
@@ -804,10 +869,32 @@ func main() {
fmt.Println(c(fmt.Sprintf("[compacted to %d messages]", len(msgs)), 32)) fmt.Println(c(fmt.Sprintf("[compacted to %d messages]", len(msgs)), 32))
fmt.Println(c("--- summary ---", 33) + "\n" + c(sm, 2)) fmt.Println(c("--- summary ---", 33) + "\n" + c(sm, 2))
continue continue
case strings.HasPrefix(u, "/cfg"):
parts := strings.SplitN(u, " ", 3)
if len(parts) == 2 {
k := strings.TrimSpace(parts[1])
if v, ok := cfg.Raw[k]; ok {
fmt.Println(c(k+"="+v, 32))
} else {
fmt.Println(c(k+" not set", 31))
}
} else if len(parts) >= 3 {
k, v := strings.TrimSpace(parts[1]), strings.TrimSpace(parts[2])
if err := setCfg("model.cfg", k, v); err != nil {
fmt.Println(c("[cfg error: "+err.Error()+"]", 31))
continue
}
cfg = getCfg("model.cfg")
COL = col(cfg)
fmt.Println(c(fmt.Sprintf("[config updated: %s=%s]", k, v), 32))
} else {
fmt.Println(c("Usage: /cfg <param> [val]", 31))
}
continue
case u == "/help": case u == "/help":
fmt.Println(c("Bantam commands:", 1, 36)) fmt.Println(c("Bantam commands:", 1, 36))
for _, kv := range [][2]string{{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save", "save session"}, {"/list", "list sessions"}, {"/load <id>", "load session"}, {"/compact", "compact context"}, {"/help", "show help"}} { for _, kv := range [][2]string{{"/quit", "exit"}, {"/clear", "reset to system prompt"}, {"/save", "save session"}, {"/list", "list sessions"}, {"/load <id>", "load session"}, {"/compact", "compact context"}, {"/cfg <k> [v]", "get/set config"}, {"/help", "show help"}} {
fmt.Println(c(fmt.Sprintf(" %-12s", kv[0]), 1, 32) + kv[1]) fmt.Println(c(fmt.Sprintf(" %-15s", kv[0]), 1, 32) + kv[1])
} }
continue continue
} }
+83
View File
@@ -1290,3 +1290,86 @@ func TestCompactHappyPath(t *testing.T) {
t.Errorf("continuation message = %+v", msgs[1]) t.Errorf("continuation message = %+v", msgs[1])
} }
} }
// ---------- setCfg and LLM parameter forwarding ----------
func TestSetCfgUpdatesAndAppends(t *testing.T) {
p := filepath.Join(t.TempDir(), "model.cfg")
if err := os.WriteFile(p, []byte("model=old-model\ntemperature=0.5\n"), 0644); err != nil {
t.Fatalf("WriteFile: %v", err)
}
if err := setCfg(p, "model", "new-model"); err != nil {
t.Fatalf("setCfg update: %v", err)
}
if err := setCfg(p, "reasoning_effort", "high"); err != nil {
t.Fatalf("setCfg append: %v", err)
}
cfg := getCfg(p)
if cfg.Model != "new-model" {
t.Errorf("Model = %q, want new-model", cfg.Model)
}
if cfg.Raw["reasoning_effort"] != "high" {
t.Errorf("Raw[reasoning_effort] = %q, want high", cfg.Raw["reasoning_effort"])
}
if cfg.Temperature != 0.5 {
t.Errorf("Temperature = %v, want 0.5", cfg.Temperature)
}
}
func TestLLMForwardsRelevantParameters(t *testing.T) {
var received map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewDecoder(r.Body).Decode(&received)
w.Write([]byte(`{"choices":[{"message":{"role":"assistant","content":"ok"}}]}`))
}))
defer srv.Close()
cfgFile := writeCfg(t, strings.Join([]string{
"endpoint=" + srv.URL,
"model=custom-llm",
"temperature=0.3",
"stream=false",
"reasoning_effort=medium",
"top_p=0.95",
"max_tokens=4096",
"color=always",
"timeout=100",
}, "\n"))
cfg := getCfg(cfgFile)
_, err := llm(&cfg, []Message{{Role: "user", Content: strp("hi")}}, nil)
if err != nil {
t.Fatalf("llm: %v", err)
}
if received["model"] != "custom-llm" {
t.Errorf("model = %v, want custom-llm", received["model"])
}
if received["temperature"] != 0.3 {
t.Errorf("temperature = %v, want 0.3", received["temperature"])
}
if received["stream"] != false {
t.Errorf("stream = %v, want false", received["stream"])
}
if received["reasoning_effort"] != "medium" {
t.Errorf("reasoning_effort = %v, want medium", received["reasoning_effort"])
}
if received["top_p"] != 0.95 {
t.Errorf("top_p = %v, want 0.95", received["top_p"])
}
if received["max_tokens"] != float64(4096) {
t.Errorf("max_tokens = %v, want 4096", received["max_tokens"])
}
if _, exists := received["color"]; exists {
t.Errorf("color should not be forwarded to OpenAI endpoint")
}
if _, exists := received["timeout"]; exists {
t.Errorf("timeout should not be forwarded to OpenAI endpoint")
}
if _, exists := received["endpoint"]; exists {
t.Errorf("endpoint should not be forwarded to OpenAI endpoint")
}
}
+10 -5
View File
@@ -32,9 +32,9 @@ sub llm { my ($c, $msgs) = @_; # one non-streaming chat completion
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 '-';
my $body = encode_json({model=>$c->{model}, temperature=>0+$c->{temperature}, messages=>$msgs, my %p = (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'}})], model=>$c->{model}, temperature=>0+$c->{temperature});
tools=>[T('shell_exec', 'Run a shell command, return output and exit code.', {command=>{type=>'string'}}), 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}; }
T('run_subagent', 'Run a child agent with a prompt.', {prompt=>{type=>'string'}})]}); my $body = encode_json(\%p);
my $tty = -t STDOUT; my $tty = -t STDOUT;
print $tty ? "\r...requesting..." : "...requesting...\n"; print $tty ? "\r...requesting..." : "...requesting...\n";
my $r = HTTP::Tiny->new(timeout=>0+$c->{timeout})->post("$ep/chat/completions", {headers=>$h, content=>$body}); my $r = HTTP::Tiny->new(timeout=>0+$c->{timeout})->post("$ep/chat/completions", {headers=>$h, content=>$body});
@@ -92,13 +92,17 @@ sub save { sdir(); my $id = strftime('%Y%m%d-%H%M%S', localtime); my $i = 0;
sub load { my ($want) = @_; my ($hit) = grep { $_->{id} eq $want } sessions(); die "no session: $want\n" unless $hit; $hit->{messages}; } 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 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() }
sub set_cfg { my ($k, $v) = @_; my (@ls, $f);
if (open my $fh, '<:encoding(UTF-8)', 'model.cfg') { while (<$fh>) { if (!/^#/ && /^(\w+)\s*=/ && $1 eq $k) { push @ls, "$k=$v\n"; $f = 1; } else { push @ls, $_; } } }
push @ls, "$k=$v\n" unless $f;
if (open my $fh, '>:encoding(UTF-8)', 'model.cfg') { print $fh @ls; close $fh; } }
sub main { 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>}; AL($c, $msgs, $sp); autosave($msgs); return; } 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"; print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save /list /load <id> /cfg <k> [v] /help\n";
while (1) { while (1) {
print "> "; my $u = <STDIN>; last unless defined $u; print "> "; my $u = <STDIN>; last unless defined $u;
$u =~ s/^\s+|\s+$//g; next unless length $u; $u =~ s/^\s+|\s+$//g; next unless length $u;
@@ -107,7 +111,8 @@ sub main {
elsif ($u eq '/save') { print "session saved: ", save($msgs), "\n"; } elsif ($u eq '/save') { print "session saved: ", save($msgs), "\n"; }
elsif ($u eq '/list') { print "$_->[0] [$_->[1] msgs]\n" for list_sessions(); } 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 =~ /^\/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"; } elsif ($u =~ /^\/cfg(?:\s+(\S+)(?:\s+(.+))?)?$/) { if (defined $2) { set_cfg($1, $2); $c = cfg(); print "config: $1=$2\n"; } elsif (defined $1) { print exists $c->{$1} ? "$1=$c->{$1}\n" : "$1 not set\n"; } else { print "usage: /cfg <param> [val]\n"; } }
elsif ($u eq '/help') { print "Commands: /quit /clear /save /list /load <id> /cfg <k> [v] /help\n"; }
else { push @$msgs, {role=>'user', content=>$u}; AL($c, $msgs, $sp); autosave($msgs); } else { push @$msgs, {role=>'user', content=>$u}; AL($c, $msgs, $sp); autosave($msgs); }
} }
autosave($msgs); autosave($msgs);
+5 -3
View File
@@ -19,7 +19,7 @@ proc http_request {m url hdrs body {t 300}} { set proto http; set host ""; set p
proc clean_msg_for_api {m} { if {![is_dict $m]} { return "" }; set r [safe_get $m role]; if {$r eq ""} { return "" }; set c [safe_get $m content]; set res [dict create role $r]; if {$r eq "system" || $r eq "user"} { dict set res content $c } elseif {$r eq "assistant"} { dict set res content $c; set vtcs [list]; set raw_tcs [safe_get $m tool_calls]; if {[is_tool_call $raw_tcs]} { set raw_tcs [list $raw_tcs] }; foreach tc $raw_tcs { if {[is_tool_call $tc]} { set fn [dict get [dict get $tc function] name]; set a "\{\}"; if {[dict exists $tc function arguments]} { set a [dict get [dict get $tc function] arguments] }; set tcid [safe_get $tc id]; if {$tcid eq ""} { set tcid "call_0" }; set tp "function"; if {[dict exists $tc type]} { set tp [dict get $tc type] }; lappend vtcs [dict create id $tcid type $tp function [dict create name $fn arguments $a]] } }; if {[llength $vtcs]} { dict set res tool_calls $vtcs } } elseif {$r eq "tool"} { set tcid [safe_get $m tool_call_id]; if {$tcid eq ""} { set tcid "call_0" }; dict set res tool_call_id $tcid; dict set res content $c }; return $res } proc clean_msg_for_api {m} { if {![is_dict $m]} { return "" }; set r [safe_get $m role]; if {$r eq ""} { return "" }; set c [safe_get $m content]; set res [dict create role $r]; if {$r eq "system" || $r eq "user"} { dict set res content $c } elseif {$r eq "assistant"} { dict set res content $c; set vtcs [list]; set raw_tcs [safe_get $m tool_calls]; if {[is_tool_call $raw_tcs]} { set raw_tcs [list $raw_tcs] }; foreach tc $raw_tcs { if {[is_tool_call $tc]} { set fn [dict get [dict get $tc function] name]; set a "\{\}"; if {[dict exists $tc function arguments]} { set a [dict get [dict get $tc function] arguments] }; set tcid [safe_get $tc id]; if {$tcid eq ""} { set tcid "call_0" }; set tp "function"; if {[dict exists $tc type]} { set tp [dict get $tc type] }; lappend vtcs [dict create id $tcid type $tp function [dict create name $fn arguments $a]] } }; if {[llength $vtcs]} { dict set res tool_calls $vtcs } } elseif {$r eq "tool"} { set tcid [safe_get $m tool_call_id]; if {$tcid eq ""} { set tcid "call_0" }; dict set res tool_call_id $tcid; dict set res content $c }; return $res }
proc encode_json_msg {m} { if {![is_dict $m]} { return "\{\}" }; set parts [list]; dict for {k v} $m { if {$k eq "tool_calls"} { set raw_tcs $v; if {[is_tool_call $raw_tcs]} { set raw_tcs [list $raw_tcs] }; set tcs [list]; foreach tc $raw_tcs { if {[is_tool_call $tc]} { set tcp [list]; dict for {tck tcv} $tc { if {$tck eq "function" && [is_dict $tcv]} { set fnp [list]; dict for {fk fv} $tcv { lappend fnp "[jesc $fk]:[jesc $fv]" }; lappend tcp "[jesc $tck]:\{ [join $fnp ", "] \}" } else { lappend tcp "[jesc $tck]:[jesc $tcv]" } }; lappend tcs "\{ [join $tcp ", "] \}" } }; lappend parts "[jesc $k]:\[ [join $tcs ", "] \]" } else { lappend parts [expr {$v eq "null" ? "[jesc $k]:null" : "[jesc $k]:[jesc $v]"}] } }; return "\{ [join $parts ", "] \}" } proc encode_json_msg {m} { if {![is_dict $m]} { return "\{\}" }; set parts [list]; dict for {k v} $m { if {$k eq "tool_calls"} { set raw_tcs $v; if {[is_tool_call $raw_tcs]} { set raw_tcs [list $raw_tcs] }; set tcs [list]; foreach tc $raw_tcs { if {[is_tool_call $tc]} { set tcp [list]; dict for {tck tcv} $tc { if {$tck eq "function" && [is_dict $tcv]} { set fnp [list]; dict for {fk fv} $tcv { lappend fnp "[jesc $fk]:[jesc $fv]" }; lappend tcp "[jesc $tck]:\{ [join $fnp ", "] \}" } else { lappend tcp "[jesc $tck]:[jesc $tcv]" } }; lappend tcs "\{ [join $tcp ", "] \}" } }; lappend parts "[jesc $k]:\[ [join $tcs ", "] \]" } else { lappend parts [expr {$v eq "null" ? "[jesc $k]:null" : "[jesc $k]:[jesc $v]"}] } }; return "\{ [join $parts ", "] \}" }
proc sanitize_msgs {msgs_var} { upvar 1 $msgs_var msgs; set new [list]; foreach m $msgs { if {[is_dict $m] && [dict exists $m tool_calls] && [set tcs [dict get $m tool_calls]] ne "null" && $tcs ne ""} { if {[is_tool_call $tcs]} { set tcs [list $tcs] }; set ntcs [list]; foreach tc $tcs { if {[is_tool_call $tc]} { set raw [dict get [dict get $tc function] arguments]; if {[catch {json::decode $raw} p] || ![is_dict $p]} { dict set tc function arguments [encode_json_msg [dict create invalid_raw $raw]] } }; lappend ntcs $tc }; dict set m tool_calls $ntcs }; lappend new $m }; set msgs $new } proc sanitize_msgs {msgs_var} { upvar 1 $msgs_var msgs; set new [list]; foreach m $msgs { if {[is_dict $m] && [dict exists $m tool_calls] && [set tcs [dict get $m tool_calls]] ne "null" && $tcs ne ""} { if {[is_tool_call $tcs]} { set tcs [list $tcs] }; set ntcs [list]; foreach tc $tcs { if {[is_tool_call $tc]} { set raw [dict get [dict get $tc function] arguments]; if {[catch {json::decode $raw} p] || ![is_dict $p]} { dict set tc function arguments [encode_json_msg [dict create invalid_raw $raw]] } }; lappend ntcs $tc }; dict set m tool_calls $ntcs }; lappend new $m }; set msgs $new }
proc encode_payload {c msgs} { set mjs [list]; foreach m $msgs { set cm [clean_msg_for_api $m]; if {$cm ne ""} { lappend mjs [encode_json_msg $cm] } }; set tools {[{"type":"function","function":{"name":"shell_exec","description":"Run a shell command, return output and exit code.","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},{"type":"function","function":{"name":"run_subagent","description":"Run a child agent with a prompt.","parameters":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}]}; return "\{ \"model\": [jesc [dict get $c model]], \"temperature\": [expr {[dict get $c temperature] + 0}], \"messages\": \[ [join $mjs ", "] \], \"tools\": $tools \}" } proc encode_payload {c msgs} { set mjs [list]; foreach m $msgs { set cm [clean_msg_for_api $m]; if {$cm ne ""} { lappend mjs [encode_json_msg $cm] } }; set tools {[{"type":"function","function":{"name":"shell_exec","description":"Run a shell command, return output and exit code.","parameters":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}},{"type":"function","function":{"name":"run_subagent","description":"Run a child agent with a prompt.","parameters":{"type":"object","properties":{"prompt":{"type":"string"}},"required":["prompt"]}}}]}; set extra [list]; dict for {k v} $c { if {$k in {endpoint api_key timeout shell_timeout max_al_iterations stream color}} continue; if {![catch {json::decode $v} jv]} { lappend extra "[jesc $k]:$v" } else { lappend extra "[jesc $k]:[jesc $v]" } }; set ex_str [expr {[llength $extra] ? ", [join $extra ", "]" : ""}]; return "\{ \"model\": [jesc [dict get $c model]], \"temperature\": [expr {[dict get $c temperature] + 0}], \"messages\": \[ [join $mjs ", "] \], \"tools\": $tools$ex_str \}" }
proc llm {c msgs_var} { proc llm {c msgs_var} {
upvar 1 $msgs_var msgs; sanitize_msgs msgs upvar 1 $msgs_var msgs; sanitize_msgs msgs
@@ -83,6 +83,7 @@ proc save {msgs} { set sd [sdir]; set base [clock format [clock seconds] -format
proc load_session {want} { foreach s [sessions] { if {[safe_get $s id] eq $want} { return [dict get $s messages] } }; error "no session: $want" } proc load_session {want} { foreach s [sessions] { if {[safe_get $s id] eq $want} { return [dict get $s messages] } }; error "no session: $want" }
proc autosave {msgs} { set sd [sdir]; set mjs [list]; foreach m $msgs { lappend mjs [encode_json_msg $m] }; if {![catch {open "$sd/autosave.json" w} f]} { puts $f "\{\n \"id\": \"autosave\",\n \"messages\": \[\n [join $mjs ",\n "]\n \]\n\}"; close $f } } proc autosave {msgs} { set sd [sdir]; set mjs [list]; foreach m $msgs { lappend mjs [encode_json_msg $m] }; if {![catch {open "$sd/autosave.json" w} f]} { puts $f "\{\n \"id\": \"autosave\",\n \"messages\": \[\n [join $mjs ",\n "]\n \]\n\}"; close $f } }
proc list_sessions {} { set res [list]; foreach s [sessions] { lappend res [list [safe_get $s id] [llength [expr {[dict exists $s messages] ? [dict get $s messages] : {}}]]] }; return $res } proc list_sessions {} { set res [list]; foreach s [sessions] { lappend res [list [safe_get $s id] [llength [expr {[dict exists $s messages] ? [dict get $s messages] : {}}]]] }; return $res }
proc set_cfg {k v} { set ls [list]; set f 0; if {[file exists model.cfg] && ![catch {open model.cfg r} fh]} { while {[gets $fh l] >= 0} { if {![regexp {^\s*#} $l] && [regexp {^(\w+)\s*=} $l -> pk] && $pk eq $k} { lappend ls "$k=$v"; set f 1 } else { lappend ls $l } }; close $fh }; if {!$f} { lappend ls "$k=$v" }; if {![catch {open model.cfg w} fh]} { puts $fh [join $ls "\n"]; close $fh } }
proc main {argv} { proc main {argv} {
set c [cfg]; set sp_text [sp]; set msgs [list [dict create role system content $sp_text]] set c [cfg]; set sp_text [sp]; set msgs [list [dict create role system content $sp_text]]
@@ -90,7 +91,7 @@ proc main {argv} {
if {[catch {open [lindex $argv 0] r} f]} { puts stderr "cannot open [lindex $argv 0]: $f"; exit 1 } if {[catch {open [lindex $argv 0] r} f]} { puts stderr "cannot open [lindex $argv 0]: $f"; exit 1 }
set content [read $f]; close $f; lappend msgs [dict create role user content $content]; AL $c msgs $sp_text 0; autosave $msgs; return set content [read $f]; close $f; lappend msgs [dict create role user content $content]; AL $c msgs $sp_text 0; autosave $msgs; return
} }
puts "MicroBantam ready ([dict get $c model]). Commands: /quit /clear /save /list /load <id> /help" puts "MicroBantam ready ([dict get $c model]). Commands: /quit /clear /save /list /load <id> /cfg <k> \[v\] /help"
while {1} { while {1} {
puts -nonewline "> "; flush stdout; if {[gets stdin u] < 0} break puts -nonewline "> "; flush stdout; if {[gets stdin u] < 0} break
if {[set u [string trim $u]] eq ""} continue if {[set u [string trim $u]] eq ""} continue
@@ -99,7 +100,8 @@ proc main {argv} {
elseif {$u eq "/save"} { puts "session saved: [save $msgs]" } \ elseif {$u eq "/save"} { puts "session saved: [save $msgs]" } \
elseif {$u eq "/list"} { foreach item [list_sessions] { puts "[lindex $item 0] \[[lindex $item 1] msgs\]" } } \ elseif {$u eq "/list"} { foreach item [list_sessions] { puts "[lindex $item 0] \[[lindex $item 1] msgs\]" } } \
elseif {[regexp {^\/load(?:\s+(\S+))?$} $u -> want_id]} { if {$want_id eq ""} { puts "usage: /load <session id>" } elseif {[catch {load_session $want_id} loaded]} { puts $loaded } else { set msgs $loaded; autosave $msgs; puts "loaded: $want_id" } } \ elseif {[regexp {^\/load(?:\s+(\S+))?$} $u -> want_id]} { if {$want_id eq ""} { puts "usage: /load <session id>" } elseif {[catch {load_session $want_id} loaded]} { puts $loaded } else { set msgs $loaded; autosave $msgs; puts "loaded: $want_id" } } \
elseif {$u eq "/help"} { puts "Commands: /quit /clear /save /list /load <id> /help" } \ elseif {[regexp {^\/cfg(?:\s+(\S+)(?:\s+(.+))?)?$} $u -> ck cv]} { if {$cv ne ""} { set_cfg $ck [string trim $cv]; set c [cfg]; puts "config: $ck=[string trim $cv]" } elseif {$ck ne ""} { puts [expr {[dict exists $c $ck] ? "$ck=[dict get $c $ck]" : "$ck not set"}] } else { puts "usage: /cfg <param> \[val\]" } } \
elseif {$u eq "/help"} { puts "Commands: /quit /clear /save /list /load <id> /cfg <k> \[v\] /help" } \
else { lappend msgs [dict create role user content $u]; AL $c msgs $sp_text 0; autosave $msgs } else { lappend msgs [dict create role user content $u]; AL $c msgs $sp_text 0; autosave $msgs }
} }
autosave $msgs autosave $msgs
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build darwin //go:build darwin || dragonfly || freebsd || netbsd || openbsd
package main package main
+1 -1
View File
@@ -1,4 +1,4 @@
//go:build !linux && !darwin && !windows //go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !windows
package main package main