Compare commits

..
3 Commits
Author SHA1 Message Date
Luxferre 0cce103b37 added some guards 2026-08-11 11:36:40 +03:00
Luxferre f039985c6f added some guards 2026-08-11 11:36:04 +03:00
Luxferre d60798f1a3 added Jim Tcl port of mb 2026-08-10 23:31:41 +03:00
7 changed files with 188 additions and 8 deletions
+13
View File
@@ -113,6 +113,8 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
- Loop back to step 1.
4. If no pending tool calls (or if `max_al_iterations` is reached), stop and return `messages`.
If the API rejects the request with an `Invalid assistant message: content or tool_calls must be set` error (usually caused by a previously cut-off stream that left an empty assistant message in the session), all implementations strip the last `assistant`-role message from the session and retry the call.
### Model configuration parameters
(shared by all implementations; `model.cfg` is plain `key=value` with `#` comments)
@@ -156,6 +158,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
- Session management: `/save`, `/list`, `/load <id>` (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 <id>`, `/help`) and file input mode
- Same built-in default system prompt and `OPENAI_API_KEY` fallback as the full implementation
- Automatic recovery from `Invalid assistant message: content or tool_calls must be set` API errors: strips the last assistant message and retries
### What it drops
@@ -165,6 +168,15 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
- Fibonacci backoff network retries (a failed request aborts with an `API error` message)
- `/compact` context summarization
### Jim Tcl port
`mb.tcl` is a **highly experimental** Jim Tcl port of the same agent (under 100 SLOC) with the same feature set as the Perl `mb`. It differs in three ways: it ships its own minimal HTTP client (raw sockets with chunked-transfer decoding) instead of `HTTP::Tiny`; it retries failed requests up to 3 times with a 2-second backoff instead of aborting on the first failure; and it relies on the external `timeout` command for `shell_exec` timeouts instead of `SIGALRM`. Run it the same way:
```bash
./mb.tcl # interactive (or: jimsh mb.tcl)
./mb.tcl prompt.txt # file input mode
```
### Running
```bash
@@ -178,6 +190,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
- `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, under 100 SLOC)
- `mb.tcl` — MicroBantam, Jim Tcl port (requires `jimsh` with the `json` and `ssl` extensions; under 100 SLOC)
- `model.cfg`, `system.txt` — shared configuration and system prompt
- `README.md` — this document
+21 -3
View File
@@ -134,7 +134,9 @@ sub llm {
return $d->{choices}[0]{message} if $d && $d->{choices} && @{$d->{choices}};
}
my $status = $res->{status} // 0;
my $err = $res->{reason} || $res->{content} || "HTTP status $status";
my $body = $res->{content} // '';
$body = substr($body, 0, 500) if length($body) > 500;
my $err = length($body) ? "$body (HTTP $status)" : ($res->{reason} || "HTTP status $status");
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; }
@@ -192,7 +194,9 @@ sub llm {
return \%msg;
}
my $status = $res->{status} // 0;
my $err = $res->{reason} || $res->{content} || "HTTP status $status";
my $body = $res->{content} // '';
$body = substr($body, 0, 500) if length($body) > 500;
my $err = length($body) ? "$body (HTTP $status)" : ($res->{reason} || "HTTP status $status");
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; }
@@ -210,7 +214,21 @@ sub AL {
for my $i (1 .. $mx) {
my $m = eval { llm($cfg, $msgs, \@tools) };
if ($@) { print c($@, 31); return $msgs; }
if ($@) {
if ($@ =~ /Invalid assistant message|content or tool_calls must be set/) {
my $stripped = 0;
for (my $j = @$msgs - 1; $j >= 0; $j--) {
if (($msgs->[$j]{role} // '') eq 'assistant') {
print c("[stripped malformed assistant message]", 33), "\n";
splice @$msgs, $j, 1;
$stripped = 1;
last;
}
}
redo if $stripped;
}
print c($@, 31); return $msgs;
}
push @$msgs, $m;
if (!$st) {
my $reas = $m->{reasoning_content} // $m->{reasoning};
+18 -2
View File
@@ -2,7 +2,7 @@
# Bantam agent: tiny, powerful, DIY
# Created by Luxferre in 2026, released into the public domain
import sys, os, json, time, subprocess, urllib.request
import sys, os, json, re, time, subprocess, urllib.request
try: import readline
except ImportError: readline = None
@@ -86,6 +86,9 @@ def sanitize_msgs(msgs):
except Exception:
fn["arguments"] = json.dumps({"invalid_raw": astr} if astr else {})
def is_invalid_assistant_err(e):
return bool(re.search(r"Invalid assistant message|content or tool_calls must be set", str(e)))
fib = [1, 1, 2, 3, 5, 8, 13, 21, 34]
def llm(cfg, msgs, tools):
@@ -155,7 +158,20 @@ MAX_DEPTH = 5
def AL(cfg, msgs, sp, depth=0):
stime, mx, st = num(cfg, "shell_timeout", 120), num(cfg, "max_al_iterations", 1000), cfg.get("stream", "true").lower() in ("true", "1", "yes")
for _ in range(mx):
m = llm(cfg, msgs, TOOLS); msgs.append(m)
try:
m = llm(cfg, msgs, TOOLS)
except RuntimeError as e:
if is_invalid_assistant_err(e):
for i in range(len(msgs) - 1, -1, -1):
if msgs[i].get("role") == "assistant":
print(c("[stripped malformed assistant message]", 33))
del msgs[i]
break
else:
raise
continue
raise
msgs.append(m)
if not st:
reas = m.get("reasoning_content") or m.get("reasoning")
if reas: print(c("--- reasoning start ---", 36) + "\n" + c(reas, 2) + "\n" + c("--- reasoning end ---", 36) + "\n")
+20 -1
View File
@@ -173,6 +173,11 @@ func sanitizeMessages(msgs []Message) {
}
}
func isInvalidAssistantErr(err error) bool {
s := err.Error()
return strings.Contains(s, "Invalid assistant message") || strings.Contains(s, "content or tool_calls must be set")
}
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}
@@ -324,7 +329,21 @@ func AL(cfg *Cfg, msgs []Message, sp string, depth int) ([]Message, error) {
done := false
for i := 0; i < cfg.MaxALIterations && !done; i++ {
m, err := llm(cfg, msgs, TOOLS)
if err != nil { return msgs, err }
if err != nil {
if isInvalidAssistantErr(err) {
stripped := false
for j := len(msgs) - 1; j >= 0; j-- {
if msgs[j].Role == "assistant" {
fmt.Println(c("[stripped malformed assistant message]", 33))
msgs = append(msgs[:j], msgs[j+1:]...)
stripped = true
break
}
}
if stripped { continue }
}
return msgs, err
}
for j := range m.ToolCalls {
tc := &m.ToolCalls[j]
astr := tc.Function.Arguments
+7 -1
View File
@@ -41,7 +41,9 @@ sub llm { my ($c, $msgs) = @_; # one non-streaming chat completion
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"; }
my $rb = $r->{content} // '';
$rb = substr($rb, 0, 500) if length($rb) > 500;
die "API error: " . (length($rb) ? "$rb (HTTP $r->{status})" : ($r->{reason} || "HTTP $r->{status}")) . "\n"; }
sub shell_exec { my ($cmd, $t) = @_; # run a command under a hard timeout
my $out = '';
@@ -56,6 +58,10 @@ sub AL { my ($c, $msgs, $sp, $depth) = @_; # the agentic loop: LLM <-> tools unt
$depth ||= 0;
for (1 .. $c->{max_al_iterations}) {
my $m = eval { llm($c, $msgs) };
if ($@ && $@ =~ /Invalid assistant message|content or tool_calls must be set/ && grep({ $_->{role} eq 'assistant' } @$msgs)) {
for (my $j = @$msgs - 1; $j >= 0; $j--) { if ($msgs->[$j]{role} eq 'assistant') { splice @$msgs, $j, 1; last; } }
print "[stripped malformed assistant message]\n"; redo;
}
if ($@) { print $@; return $msgs; }
push @$msgs, $m;
print $m->{content}, "\n" if defined $m->{content} && length $m->{content};
Executable
+108
View File
@@ -0,0 +1,108 @@
#!/usr/bin/env jimsh
# MicroBantam (mb): the Bantam agent in Jim Tcl (under 100 SLOC)
# Created by Luxferre in 2026, released into the public domain
set 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."
set SDIR "[expr {[info exists env(HOME)] ? $env(HOME) : ([info exists env(USERPROFILE)] ? $env(USERPROFILE) : ".")} ]/.bantam/sessions"
proc is_dict {d} { return [expr {![catch {dict keys $d}] && [llength $d] % 2 == 0}] }
proc safe_get {d args} { foreach k $args { if {![is_dict $d] || ![dict exists $d $k]} { return "" }; set d [dict get $d $k] }; return $d }
proc is_tool_call {tc} { return [expr {[is_dict $tc] && [dict exists $tc function] && [is_dict [dict get $tc function]] && [dict exists [dict get $tc function] name]}] }
proc jesc {s} { set map [list \\ \\\\ \" \\" \n \\n \r \\r \t \\t \f \\f \b \\b]; set s [string map $map $s]; set res ""; for {set i 0} {$i < [string length $s]} {incr i} { set c [string index $s $i]; scan $c %c k; append res [expr {$k < 32 ? [format "\\u%04x" $k] : $c}] }; return "\"$res\"" }
proc cfg {} { global env; set d [dict create endpoint https://opencode.ai/zen/v1 model big-pickle temperature 0.7 api_key - timeout 300 shell_timeout 120 max_al_iterations 1000]; if {[file exists model.cfg] && ![catch {open model.cfg r} f]} { while {[gets $f l] >= 0} { if {[regexp {^(\w+)\s*=\s*(.+)$} $l -> k v]} { dict set d $k $v } }; close $f }; if {[dict get $d api_key] eq "-" && [info exists env(OPENAI_API_KEY)]} { dict set d api_key $env(OPENAI_API_KEY) }; return $d }
proc sp {} { global DEF_SP; set p ""; if {[file exists system.txt] && ![catch {open system.txt r} f]} { set p [string trim [read $f]]; close $f }; return [expr {[string length $p] ? $p : $DEF_SP}] }
proc decode_chunked {b} { set res ""; set pos 0; while {$pos < [string length $b]} { set idx [string first "\r\n" $b $pos]; if {$idx == -1} break; scan [lindex [split [string range $b $pos [expr {$idx - 1}]] ";"] 0] "%x" clen; if {$clen == 0} break; set st [expr {$idx + 2}]; append res [string range $b $st [expr {$st + $clen - 1}]]; set pos [expr {$st + $clen + 2}] }; return $res }
proc http_request {m url hdrs body {t 300}} { set proto http; set host ""; set port 80; set path "/"; if {[regexp {^(https?)://([^/]+)(/.*)?$} $url -> proto hp reqp]} { set port [expr {$proto eq "https" ? 443 : 80}]; if {$reqp ne ""} { set path $reqp } }; if {![regexp {^([^:]+):(\d+)$} $hp -> host port]} { set host $hp }; set s [socket stream $host:$port]; $s timeout [expr {$t * 1000}]; if {$proto eq "https"} { $s ssl -sni $host }; set req "$m $path HTTP/1.1\r\nHost: $host\r\n"; dict for {k v} $hdrs { append req "$k: $v\r\n" }; append req "Content-Length: [string length $body]\r\nConnection: close\r\n\r\n$body"; $s puts -nonewline $req; $s flush; set resp [$s read]; $s close; set sep [string first "\r\n\r\n" $resp]; set hlen 4; if {$sep == -1} { set sep [string first "\n\n" $resp]; set hlen 2 }; if {$sep == -1} { error "invalid HTTP response" }; set htxt [string range $resp 0 [expr {$sep - 1}]]; set rbody [string range $resp [expr {$sep + $hlen}] end]; set status 0; set reason ""; set chunked 0; regexp {^HTTP/\d\.\d\s+(\d+)(?:\s+(.*))?$} [string trim [lindex [split $htxt "\n"] 0]] -> status reason; foreach l [lrange [split $htxt "\n"] 1 end] { if {[regexp -nocase {^transfer-encoding:\s*chunked$} [string trim $l]]} { set chunked 1 } }; return [dict create status $status reason $reason body [expr {$chunked ? [decode_chunked $rbody] : $rbody}]] }
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 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 llm {c msgs_var} {
upvar 1 $msgs_var msgs; sanitize_msgs msgs
set ep [string trimright [dict get $c endpoint] "/"]
set payload [encode_payload $c $msgs]
if {[catch {json::decode $payload}]} { error "API error: Local JSON validation failed" }
set is_tty [expr {[catch {exec sh -c "test -t 1" >@stdout}] == 0}]
set hdrs [dict create "Content-Type" "application/json" "User-Agent" "Mozilla/5.0 (compatible; MicroBantam/1.0)"]; if {[dict get $c api_key] ne "-"} { dict set $hdrs "Authorization" "Bearer [dict get $c api_key]" }
set last ""
for {set a 1} {$a <= 3} {incr a} {
if {$a > 1} { after [expr {($a - 1) * 2000}] }; puts -nonewline [expr {$is_tty ? "\r...requesting ($a/3)..." : "...requesting ($a/3)...\n"}]; flush stdout
set code [catch {http_request POST "$ep/chat/completions" $hdrs $payload [dict get $c timeout]} res]; if {$is_tty} { puts -nonewline "\r\u001b\[K"; flush stdout }
if {$code == 0 && [dict get $res status] == 200} {
set body [dict get $res body]
if {![catch {json::decode $body} data] && [is_dict $data] && [dict exists $data choices] && [llength [set choices [dict get $data choices]]] > 0} { return [dict get [lindex $choices 0] message] }
set last "invalid response body"
} else {
set st [expr {$code != 0 ? 0 : [dict get $res status]}]; set detail [safe_get $res body]
if {[string length $detail] > 400} { set detail [string range $detail 0 399] }
set last [expr {$code != 0 ? $res : ([string length $detail] ? "$detail (HTTP $st)" : ([safe_get $res reason] ne "" ? [safe_get $res reason] : "HTTP $st"))}]
if {$st != 0 && $st != 403 && $st != 429 && $st < 500} { error "API error: $last" }
}
}
error "API error: $last (after 3 attempts)"
}
proc shell_exec_cmd {cmd t} { catch {exec timeout $t sh -c "$cmd 2>&1"} out opts; set exit_code 0; if {[dict get $opts -code] != 0} { set errcode [dict get $opts -errorcode]; set exit_code [expr {[lindex $errcode 0] eq "CHILDSTATUS" ? [lindex $errcode 2] : -1}] }; set out [string trimright [string map [list "\u0000" "\n"] $out] " \t\n\r\v\f"]; return [expr {$exit_code == 124 ? "$out\n\[timeout after ${t}s\]\nexit: -1" : "$out\nexit: $exit_code"}] }
proc last_assistant {msgs} { for {set i [expr {[llength $msgs] - 1}]} {$i >= 0} {incr i -1} { set m [lindex $msgs $i]; if {[is_dict $m] && [safe_get $m role] eq "assistant"} { set cnt [safe_get $m content]; if {$cnt ne "" && $cnt ne "null"} { return $cnt } } }; return "" }
proc AL {c msgs_var sp depth} {
upvar 1 $msgs_var msgs
for {set iter 0} {$iter < [dict get $c max_al_iterations]} {incr iter} {
if {[catch {llm $c msgs} m]} {
if {[string match "*Invalid assistant message*" $m] || [string match "*content or tool_calls must be set*" $m]} {
set stripped 0
for {set j [expr {[llength $msgs] - 1}]} {$j >= 0} {incr j -1} { set mm [lindex $msgs $j]; if {[is_dict $mm] && [safe_get $mm role] eq "assistant"} { set msgs [lreplace $msgs $j $j]; puts "\[stripped malformed assistant message\]"; set stripped 1; break } }
if {$stripped} { continue }
}
puts $m; return $msgs
}
lappend msgs $m; set cnt [safe_get $m content]; if {$cnt ne "" && $cnt ne "null"} { puts $cnt }
if {[set tcs [safe_get $m tool_calls]] eq "" || $tcs eq "null"} break
if {[is_tool_call $tcs]} { set tcs [list $tcs] }
foreach tc $tcs {
if {![is_tool_call $tc]} continue
set fn [dict get [dict get $tc function] name]; set args_raw "\{\}"
if {[dict exists $tc function arguments]} { set args_raw [dict get [dict get $tc function] arguments] }
if {[catch {json::decode $args_raw} a] || ![is_dict $a]} { set bad [encode_json_msg [dict create invalid_raw $args_raw]]; dict set tc function arguments $bad; set res "bad JSON args for $fn: $bad"
} elseif {$fn eq "shell_exec"} { set cmd [safe_get $a command]; set res [shell_exec_cmd $cmd [dict get $c shell_timeout]]
} elseif {$fn eq "run_subagent"} { set prompt [safe_get $a prompt]; set child [list [dict create role system content "$sp\n\nImportant: this is a child agent"] [dict create role user content $prompt]]; set res [expr {$depth >= 5 ? "\[subagent depth limit (5) reached, child not spawned\]" : [last_assistant [AL $c child $sp [expr {$depth + 1}]]]}]
} else { set res "unknown tool: $fn" }
puts "\[tool\] $fn: $res"; lappend msgs [dict create role tool tool_call_id [safe_get $tc id] content $res]
}
}
return $msgs
}
proc sdir {} { global SDIR; if {![file isdirectory $SDIR]} { file mkdir $SDIR }; return $SDIR }
proc sessions {} { global SDIR; set s [list]; foreach f [glob -nocomplain "$SDIR/*.json"] { if {![catch {open $f r} fh]} { set c [read $fh]; close $fh; if {![catch {json::decode $c} d] && [is_dict $d]} { lappend s $d } } }; return [lsort -command {apply {{a b} { string compare [safe_get $b id] [safe_get $a id] }}} $s] }
proc save {msgs} { set sd [sdir]; set base [clock format [clock seconds] -format "%Y%m%d-%H%M%S"]; set id $base; set i 0; while {[file exists "$sd/$id.json"]} { incr i; set id "$base-$i" }; set mjs [list]; foreach m $msgs { lappend mjs [encode_json_msg $m] }; set f [open "$sd/$id.json" w]; puts $f "\{\n \"id\": [jesc $id],\n \"messages\": \[\n [join $mjs ",\n "]\n \]\n\}"; close $f; return $id }
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 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 main {argv} {
set c [cfg]; set sp_text [sp]; set msgs [list [dict create role system content $sp_text]]
if {[llength $argv] > 0} {
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
}
puts "MicroBantam ready ([dict get $c model]). Commands: /quit /clear /save /list /load <id> /help"
while {1} {
puts -nonewline "> "; flush stdout; if {[gets stdin u] < 0} break
if {[set u [string trim $u]] eq ""} continue
if {$u eq "/quit"} { break } \
elseif {$u eq "/clear"} { set msgs [list [dict create role system content $sp_text]]; autosave $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 {[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" } \
else { lappend msgs [dict create role user content $u]; AL $c msgs $sp_text 0; autosave $msgs }
}
autosave $msgs
}
if {[info exists argv0] && [file tail $argv0] eq "mb.tcl"} { main $argv }
+1 -1
View File
@@ -1,5 +1,5 @@
endpoint=https://opencode.ai/zen/v1
model=deepseek-v4-flash-free
model=big-pickle
temperature=0.7
api_key=-
stream=true