Compare commits

...
32 Commits
Author SHA1 Message Date
luxferre 676fc1013c Add deprecation notice
Signed-off-by: Luxferre <1+luxferre@noreply.code.luxferre.top>
2026-10-14 13:25:18 +00:00
Luxferre 4eee5d6ef9 enabled multi-line paste 2026-09-24 14:38:57 +03:00
Luxferre 3b5a8a5bfd some opts 2026-09-19 00:33:48 +03:00
Luxferre e1efe70d8a oc compatibility overhaul 2026-09-19 00:19:25 +03:00
Luxferre ebc62b93bf changed session id algo 2026-09-18 21:08:15 +03:00
Luxferre d5e69c4a0a changed session id algo 2026-09-18 19:44:59 +03:00
Luxferre 0cc2987dcb some more oc fixes 2026-09-18 14:25:13 +03:00
Luxferre f23c9132ce more oc fixes 2026-09-18 12:08:01 +03:00
Luxferre 0b2d9f2a90 context overflow prevention 2026-09-18 10:25:51 +03:00
Luxferre 1d8a19dd67 more opencode fixes 2026-09-17 10:35:16 +03:00
Luxferre a02b388b1a more OC headers 2026-09-17 10:02:08 +03:00
Luxferre 9792cec64f opencode session fixes 2026-09-11 18:52:14 +03:00
Luxferre b776644261 session autoload mechanism 2026-09-11 17:43:45 +03:00
Luxferre 9ee04ddf32 tool call vis improvement 2026-09-11 15:30:32 +03:00
Luxferre 69571704a1 tool call vis improvement 2026-09-11 15:16:40 +03:00
Luxferre ea1fb205bc added tmpdir directive 2026-09-11 11:07:10 +03:00
Luxferre f21dc0efde added correct reasoning token stats 2026-09-11 10:55:19 +03:00
Luxferre d9c7bb4407 more env fixes 2026-09-10 12:28:00 +03:00
Luxferre b0a16e99e1 env tweaks 2026-09-10 12:20:41 +03:00
Luxferre 502a3f8c03 env var updates 2026-09-10 11:49:54 +03:00
Luxferre b3f2864f6a skills support 2026-09-10 10:38:36 +03:00
Luxferre 5b5853a2dd readme upd 2026-09-10 10:20:25 +03:00
Luxferre c0350f5d9b ux improvements 2026-09-10 10:16:35 +03:00
Luxferre ed5eb88ba9 C-j fix; tool doc upd 2026-09-09 20:33:07 +03:00
Luxferre 4ed12ed213 fixed write semantics and visual bug with newline insertion 2026-09-09 20:23:37 +03:00
Luxferre 208d874cce write_file improvements 2026-09-09 09:51:08 +03:00
Luxferre e6472a77b7 updated DS compat 2026-09-08 21:25:29 +03:00
Luxferre 5291415d4e added effective model id for routed responses 2026-09-06 10:57:09 +03:00
Luxferre c64e79b22f added default reasoning_effort 2026-09-02 08:58:28 +03:00
Luxferre f17f15aea8 switched to openrouter/free as the default model 2026-09-01 16:29:00 +03:00
Luxferre ad1b6efcd5 switched to kilo-auto/free as the default model 2026-09-01 11:42:29 +03:00
Luxferre 8efd5a5fa6 switched to kilo-auto/free as the default model 2026-09-01 11:42:15 +03:00
5 changed files with 2956 additions and 485 deletions
+82 -40
View File
@@ -1,5 +1,7 @@
# Bantam: tiny, powerful, DIY AI agent # Bantam: tiny, powerful, DIY AI agent
**N.B. These Bantam versions have been officially deprecated.** Main development is being done on the [bantam-cpp](https://code.luxferre.top/luxferre/bantam-cpp) port, and it's recommended to switch to it whenever possible. This repo has been archived to preserve the codebase for historical purposes, and no further updates are expected here.
## About ## About
Bantam is a minimalist, dependency-free AI agent specification with reference implementations in **Go** (`main.go` + `term_*.go`, module `code.luxferre.top/luxferre/bantam`) and **Perl 5** as **MicroBantam** (`mb`, under 100 SLOC). It provides an agentic loop capable of autonomous tool execution, direct shell interaction, file writing/editing, real-time response streaming, markdown terminal rendering with box-drawing tables, Fibonacci backoff network resilience, context window auto-discovery, token usage tracking with prompt cache breakdowns, and conversation compaction using any OpenAI-compatible completions API. Bantam is a minimalist, dependency-free AI agent specification with reference implementations in **Go** (`main.go` + `term_*.go`, module `code.luxferre.top/luxferre/bantam`) and **Perl 5** as **MicroBantam** (`mb`, under 100 SLOC). It provides an agentic loop capable of autonomous tool execution, direct shell interaction, file writing/editing, real-time response streaming, markdown terminal rendering with box-drawing tables, Fibonacci backoff network resilience, context window auto-discovery, token usage tracking with prompt cache breakdowns, and conversation compaction using any OpenAI-compatible completions API.
@@ -7,9 +9,9 @@ Bantam is a minimalist, dependency-free AI agent specification with reference im
The entire philosophy of Bantam is built upon two principles: The entire philosophy of Bantam is built upon two principles:
1. The structure must be as simple as possible for anyone to be able to reimplement the agent from a plain algorithm description. 1. The structure must be as simple as possible for anyone to be able to reimplement the agent from a plain algorithm description.
2. The agent only needs to provide two tools: a tool to call shell commands (`shell_exec`) and a tool to write/edit files (`write_file`). In theory, this should be sufficient to give LLMs the ability to handle tasks of any complexity. 2. The agent only needs to provide a minimal set of tools: a tool to run shell commands (`bash`), a tool to inspect files and directories (`read`), and a tool to write/edit files (`write`). In theory, this should be sufficient to give LLMs the ability to handle tasks of any complexity.
Because of the second principle, Bantam itself was named after Victorinox Bantam Alox, a small and lightweight Swiss army knife with only two tools. Because of this minimal philosophy, Bantam itself was named after Victorinox Bantam Alox, a small and lightweight Swiss army knife with minimal tools.
## Usage ## Usage
@@ -39,12 +41,14 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
1. Configure `model.cfg` (or `.bantam.cfg`, which takes priority) with your API settings: 1. Configure `model.cfg` (or `.bantam.cfg`, which takes priority) with your API settings:
```ini ```ini
endpoint=https://opencode.ai/zen/v1 endpoint=https://api.kilo.ai/api/openrouter
model=deepseek-v4-flash-free model=openrouter/free
temperature=0.7 temperature=0.7
api_key=your_api_key_here api_key=your_api_key_here
stream=true stream=true
context_window=200000 context_window=200000
max_tool_res=65536
reasoning_effort=high
``` ```
2. Interactive mode: 2. Interactive mode:
@@ -53,11 +57,11 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
./mb # MicroBantam (Perl 5) ./mb # MicroBantam (Perl 5)
``` ```
In interactive mode, prompts can span multiple lines: press **Ctrl+J** to insert a real line break (the cursor moves to the next line), then **Enter** to submit the whole multi-line prompt. The Go port ships its own raw-mode line editor (arrow keys move the cursor, Up/Down browse history, Backspace edits, Ctrl+C clears line / interrupts in-flight run, Ctrl+D exits), working everywhere without third-party dependencies. In interactive mode, prompts can span multiple lines: press **Ctrl+J** to insert a real line break (the cursor moves to the next line), then **Enter** to submit the whole multi-line prompt. Bracketed paste is enabled (`ESC[?2004h`) while editing, so a multi-line block pasted from the clipboard is inserted as literal line breaks and only the final **Enter** submits it. The Go port ships its own raw-mode line editor, working everywhere without third-party dependencies. It supports arrow keys and Up/Down for history, Backspace and Ctrl+D (non-empty line) to delete, and Emacs-style editing combos: **Ctrl+A** (start of line), **Ctrl+E** (end of line), **Ctrl+B** / **Ctrl+F** (move by character), **Ctrl+W** (delete previous word), **Ctrl+K** (kill to end of line), **Ctrl+U** (kill to start of line), **Ctrl+Left** / **Ctrl+Right** (move by word), and **Home** / **End** keys. **Ctrl+C** clears the line / interrupts an in-flight run, and **Ctrl+D** on an empty line exits.
After every interaction, Bantam displays token usage (prompt tokens, cached/uncached breakdown when supported by the provider, completion tokens, and context window utilization): After every interaction, Bantam displays token usage (prompt tokens, cached/uncached breakdown when supported by the provider, completion tokens, and context window utilization):
```text ```text
[tokens: 1420 prompt (1000 cached, 420 uncached) + 85 completion | context: 1420/200000 (0.7%)] [openrouter/free: 1420 prompt (1000 cached, 420 uncached) + 85 completion | context: 1420/200000 (0.7%)]
``` ```
Sessions are saved under `~/.bantam/sessions/` and can be managed with these commands: Sessions are saved under `~/.bantam/sessions/` and can be managed with these commands:
@@ -67,8 +71,11 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
- `/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` — compact context down to the system message and a concise summary using the LLM; the compaction prompt is appended to the conversation to derive the summary, then the conversation is reset to `[system, summary-user-message]` (a fresh prefix, so downstream prompt-cache hits depend on the provider and are not guaranteed) - `/compact` — compact context down to the system message and a concise summary using the LLM; the compaction prompt is appended to the conversation to derive the summary, then the conversation is reset to `[system, summary-user-message]` (a fresh prefix, so downstream prompt-cache hits depend on the provider and are not guaranteed)
- `/cfg <param> [val]` — inspect or update a configuration parameter live (writes to `.bantam.cfg`) - `/cfg <param> [val]` — inspect or update a configuration parameter live (writes to `.bantam.cfg`)
- `/model [val]` — alias for `/cfg model` (inspect or set the model)
- `/endpoint [val]` — alias for `/cfg endpoint` (inspect or set the API endpoint)
- `/skill` — with no argument, list every skill under the configured skills directory (or report that none are configured); with `<name> [prompt]`, load `<skills_dir>/<name>/SKILL.md` (or an absolute path when no skills dir is set) and send its contents prefixed to the optional `prompt` as the next user turn
- `/models` — query the `/models` path on the current inference endpoint and print a plain list of supported model IDs, marking the currently configured model with a leading `* ` (Go port) - `/models` — query the `/models` path on the current inference endpoint and print a plain list of supported model IDs, marking the currently configured model with a leading `* ` (Go port)
- `!<cmd>` — execute a shell command directly through `shell_exec` without adding the result to the conversation context (Go port) - `!<cmd>` — execute a shell command directly through `bash` without adding the result to the conversation context (Go port)
- `/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
@@ -95,13 +102,13 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
### High-Level Overview ### High-Level Overview
1. **Initialization**: Read the config file (`.bantam.cfg` if present, else `model.cfg`). Discover context window size from the `/models` endpoint (or fallback to `context_window` from `model.cfg` or 200,000 tokens). Prepare an array of messages starting with the built-in system prompt `{"role": "system", "content": system_prompt}`. 1. **Initialization**: Read the config file (`.bantam.cfg` if present, else `model.cfg`). Discover context window size from the `/models` endpoint (or fallback to `context_window` from `model.cfg` or 200,000 tokens). Prepare an array of messages starting with the built-in system prompt `{"role": "system", "content": system_prompt}`.
2. **Input Processing**: Take user prompt (via command-line file parameter or interactive stdin). If prefixed with `!`, execute the command directly via `shell_exec` without appending to conversation context. Otherwise, append `{"role": "user", "content": prompt}`, and invoke `AL(cfg, messages)`. 2. **Input Processing**: Take user prompt (via command-line file parameter or interactive stdin). If prefixed with `!`, execute the command directly via `bash` without appending to conversation context. Otherwise, append `{"role": "user", "content": prompt}`, and invoke `AL(cfg, messages)`.
3. **Agentic Loop (`AL`)**: 3. **Agentic Loop (`AL`)**:
- Send `messages` and tool definitions (`shell_exec`, `write_file`) to the OpenAI-compatible `/chat/completions` API endpoint with custom `User-Agent` headers and `stream_options: {"include_usage": true}`. - Send `messages` and tool definitions (`bash`, `read`, `write`) to the OpenAI-compatible `/chat/completions` API endpoint with custom request headers (see below) and `stream_options: {"include_usage": true}`.
- Support context cancellation (e.g. on `SIGINT` / Ctrl+C) to cleanly abort in-flight requests without appending incomplete messages. - Support context cancellation (e.g. on `SIGINT` / Ctrl+C) to cleanly abort in-flight requests without appending incomplete messages.
- On network or HTTP failure, retry using Fibonacci backoff delays (`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`). - On network or HTTP failure, retry using Fibonacci backoff delays (`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`).
- If `stream=true`, parse SSE data chunks (`data: {...}`) in real-time to stream reasoning content (`reasoning_content`) and response text directly to stdout, bracketing the reasoning block with `--- reasoning start ---` / `--- reasoning end ---` markers, rendering Markdown and tables constrained to terminal width. - If `stream=true`, parse SSE data chunks (`data: {...}`) in real-time to stream reasoning content (`reasoning_content`) and response text directly to stdout, bracketing the reasoning block with `--- reasoning start ---` / `--- reasoning end ---` markers, rendering Markdown and tables constrained to terminal width.
- Reconstruct the assistant message and track usage tokens (`prompt_tokens`, `completion_tokens`, cached tokens). If `tool_calls` exist, trace the call (`[tool call: name(args)]`), validate JSON arguments, execute the requested tool (`shell_exec` or `write_file`), trace the result (`[tool result: name]`), append the tool response `{"role": "tool", "tool_call_id": id, "content": result}`, and repeat the loop. - Reconstruct the assistant message and track usage tokens (`prompt_tokens`, `completion_tokens`, cached tokens). If `tool_calls` exist, trace the call (`[tool call: name(args)]`), validate JSON arguments, execute the requested tool (`bash`, `read`, or `write`; also accepting legacy `shell_exec` and `write_file`), trace the result (`[tool result: name]`), append the tool response `{"role": "tool", "tool_call_id": id, "content": result}`, and repeat the loop.
- If no tool calls remain or `max_al_iterations` is reached, return the updated messages list and turn usage stats. - If no tool calls remain or `max_al_iterations` is reached, return the updated messages list and turn usage stats.
4. **Post-Turn Reporting & Compaction**: 4. **Post-Turn Reporting & Compaction**:
- Display token usage and context window percentage. - Display token usage and context window percentage.
@@ -113,22 +120,23 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
1. Initialize system prompt from built-in default. 1. Initialize system prompt from built-in default.
2. Read model parameters from the config file (`.bantam.cfg` if present, else `model.cfg`) in `key=value` format and discover context window size. 2. Read model parameters from the config file (`.bantam.cfg` if present, else `model.cfg`) in `key=value` format and discover context window size.
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. If prefixed with `!`, execute the shell command directly via `shell_exec` and exit. Otherwise, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, and exit. 4. Read the first command-line parameter. If non-empty, read user prompt from the specified file. If prefixed with `!`, execute the shell command directly via `bash` and exit. Otherwise, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, 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 starting with `/save`, write the whole `messages` array to `~/.bantam/sessions/<name>.json` (or `<project-md5>.json` if no name is given) and return to step 5. If equal to `/continue` or `/cont`, load the session corresponding to the current project's MD5 hash and return to step 5. If equal to `/list`, print saved sessions and their summaries (marking current project session) 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 by appending the compaction prompt to derive the summary, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg <param>`) or update the config live by writing to `.bantam.cfg` (`/cfg <param> <val>`) and return to step 5. If equal to `/models`, query the `/models` path on the current inference endpoint and print a plain list of supported model IDs (the currently configured model marked with a leading `* `), then return to step 5. If starting with `!`, execute the command directly via `shell_exec` without adding the result to `messages` 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/<project-md5>.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 starting with `/save`, write the whole `messages` array to `~/.bantam/sessions/<name>.json` (or `<project-md5>.json` if no name is given) and return to step 5. If equal to `/continue` or `/cont`, load the session corresponding to the current project's MD5 hash and return to step 5. If equal to `/list`, print saved sessions and their summaries (marking current project session) 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 by appending the compaction prompt to derive the summary, replace `messages` with `[system, summary-user-message]`, and return to step 5. If starting with `/cfg`, display the current value (`/cfg <param>`) or update the config live by writing to `.bantam.cfg` (`/cfg <param> <val>`) and return to step 5. `/model` and `/endpoint` are aliases for `/cfg model` and `/cfg endpoint` respectively and behave the same way. If equal to `/skill` (no argument), list every skill under the configured skills directory (or report that none is configured) and return to step 5. If starting with `/skill`, resolve the skill (a `<skills_dir>/<name>/SKILL.md` file, or an absolute path to a skill directory or `SKILL.md` file when no skills directory is configured), prefix its contents to the optional remaining text, and send the result as the next user turn (returning to step 5 after the response). If equal to `/models`, query the `/models` path on the current inference endpoint and print a plain list of supported model IDs (the currently configured model marked with a leading `* `), then return to step 5. If starting with `!`, execute the command directly via `bash` without adding the result to `messages` 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/<project-md5>.json`.
6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, check 60% context threshold for auto-compaction, and go to step 5. 6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, display token usage, check 60% context threshold for auto-compaction, 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`) 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`, `context_window`) and 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`, `context_window`) 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 request headers mimicking OpenCode for all endpoints: `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`, `x-opencode-session: <ses_...>`, and a fresh `x-opencode-request: <msg_...>` per call (where the 12-hex timestamp prefix in `x-opencode-request` is the exact bitwise inverse of `x-opencode-session`). The `Authorization: Bearer <api_key>` header is added whenever `api_key` is set and not `-`.
- 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.
2. Append the assistant's response message object to `messages`. If non-streaming and response has reasoning tokens (`reasoning_content` or `reasoning`), output them wrapped in `--- reasoning start ---` / `--- reasoning end ---` markers. 2. Append the assistant's response message object to `messages`. If non-streaming and response has reasoning tokens (`reasoning_content` or `reasoning`), output them wrapped in `--- reasoning start ---` / `--- reasoning end ---` markers.
3. If there are pending `tool_calls` in the assistant response: 3. If there are pending `tool_calls` in the assistant response:
- For each tool call, output a trace log (`[tool call: name(args)]`). - For each tool call, output a trace log (`[tool call: name(args)]`).
- Sanitize tool arguments to filter out non-printable and space-like Unicode characters (protecting against indirect prompt injection), and validate JSON arguments. If invalid, format a tool-error response so the LLM can self-correct. - Sanitize tool arguments to filter out non-printable and space-like Unicode characters (protecting against indirect prompt injection), and validate JSON arguments. If invalid, format a tool-error response so the LLM can self-correct.
- Execute tool action (`shell_exec` or `write_file`). - Execute tool action (`bash`, `read`, or `write`; also accepting legacy `shell_exec` and `write_file`).
- Sanitize the tool result output to strip any non-printable and space-like Unicode characters (leaving only ASCII space, tab, newline, and printable Unicode characters). - Sanitize the tool result output to strip any non-printable and space-like Unicode characters (leaving only ASCII space, tab, newline, and printable Unicode characters).
- If the sanitized result is larger than `max_tool_res` bytes, spill it to a unique file under `$TMPDIR/bantam/toolres` and replace the content appended to `messages` with a short instruction naming the file, its line count, and how to read it partially using the `read` tool (specifying `offset` and `limit`); every spilled file is deleted when the round ends (i.e. when the model emits a final response with no tool calls).
- Output a trace log of the result (`[tool result: name]`). - Output a trace log of the result (`[tool result: name]`).
- Append tool result message (`role: "tool"`, `tool_call_id`, `content`: result string) to `messages`. - Append tool result message (`role: "tool"`, `tool_call_id`, `content`: result string) to `messages`.
- Loop back to step 1. - Loop back to step 1.
@@ -138,20 +146,23 @@ If the API rejects the request with an `Invalid assistant message: content or to
### Model configuration parameters ### Model configuration parameters
(shared by all implementations; the config file — `.bantam.cfg` takes priority over `model.cfg` when both exist — is plain `key=value` with `#` comments) (shared by all implementations; `.bantam.cfg` takes priority over `model.cfg` when both exist — both are plain `key=value` with `#` comments. Values defined in `.bantam.cfg` override `BANTAM_*` environment variables; when `.bantam.cfg` is absent, `BANTAM_*` environment variables override `model.cfg` and built-in defaults.)
- `endpoint` (base OpenAI-compatible API URL, default `https://opencode.ai/zen/v1`) - `endpoint` (base OpenAI-compatible API URL, default `https://api.kilo.ai/api/openrouter`; falls back to `BANTAM_ENDPOINT` env var)
- `model` (model name, e.g. `gpt-4o`) - `model` (model name, default `openrouter/free`; falls back to `BANTAM_MODEL` env var)
- `temperature` (model temperature, default 0.7) - `temperature` (model temperature, default 0.7; falls back to `BANTAM_TEMP` or `BANTAM_TEMPERATURE` env var)
- `api_key` (API key / Bearer token, optional; fall back to `OPENAI_API_KEY` env var) - `api_key` (API key / Bearer token, optional; falls back to `BANTAM_API_KEY` env var; set to `-` or left blank in config file to explicitly defer to `BANTAM_API_KEY`)
- `stream` (stream response tokens in real-time, default `true`) - `stream` (stream response tokens in real-time, default `true`; falls back to `BANTAM_STREAM` env var)
- `color` (ANSI coloring: `auto` (TTY-detected, default), `always`, or `never`; also disabled by `NO_COLOR`/`BANTAM_NO_COLOR` env vars) - `color` (ANSI coloring: `auto` (TTY-detected, default), `always`, or `never`; disabled by `NO_COLOR`/`BANTAM_NO_COLOR` env vars, falls back to `BANTAM_COLOR` env var)
- `timeout` (HTTP timeout in seconds for LLM API calls, default 300; in the Go port it bounds connection setup and time-to-first-byte, so long streaming responses are not cut off mid-stream) - `timeout` (HTTP timeout in seconds for LLM API calls, default 300; in the Go port it bounds connection setup and time-to-first-byte, so long streaming responses are not cut off mid-stream; falls back to `BANTAM_TIMEOUT` env var)
- `shell_timeout` (timeout in seconds for `shell_exec` commands, default 120) - `shell_timeout` (timeout in seconds for `bash` shell commands, default 120; falls back to `BANTAM_SHELL_TIMEOUT` env var)
- `max_al_iterations` (max tool-call loop iterations per `AL()` invocation, default 1000) - `max_al_iterations` (max tool-call loop iterations per `AL()` invocation, default 1000; falls back to `BANTAM_MAX_AL_ITERATIONS` env var)
- `context_window` (context window size in tokens, auto-discovered from `/models` API if available, fallback to this setting, default 200000) - `context_window` (context window size in tokens, auto-discovered from `/models` API if available, fallback to this setting, default 200000; falls back to `BANTAM_CONTEXT_WINDOW` env var)
- `max_tool_res` (maximum number of bytes of a tool result that may be sent into the model context directly, default 65536; larger results are written to a unique file under `$TMPDIR/bantam/toolres` and replaced by a short instruction naming the file and its line count so it can be read in parts using the `read` tool, and the file is deleted once the round ends; falls back to `BANTAM_MAX_TOOL_RES` env var)
- `reasoning_effort` (reasoning effort level, forwarded to chat completions API, default `high`; falls back to `BANTAM_REASONING_EFFORT` env var)
- `bantam_tools_dir` (optional path to a directory of extra shell tools; the Go port appends `"Extra shell tools can be found at <dir>"` to the system prompt at startup when set. The `BANTAM_TOOLS_DIR` environment variable overrides this and is checked first; if neither is set, nothing is appended. Note: this is an agent-internal hint, not forwarded to the API.) - `bantam_tools_dir` (optional path to a directory of extra shell tools; the Go port appends `"Extra shell tools can be found at <dir>"` to the system prompt at startup when set. When unset in the config file, it falls back to the `BANTAM_TOOLS_DIR` environment variable; if neither is set, nothing is appended. Note: this is an agent-internal hint, not forwarded to the API.)
- `bantam_skills_dir` (optional path to a directory of reusable *skills*; each skill lives in its own subdirectory as `<skill_name>/SKILL.md`. When set, the Go port appends `"Skills may be discovered and invoked from <dir>"` to the system prompt at startup, and the `/skill <skill_name> [prompt]` command loads a skill's `SKILL.md` and runs it as a prompt. When unset in the config file, it falls back to the `BANTAM_SKILLS_DIR` environment variable; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead. Note: this is an agent-internal hint, not forwarded to the API.)
The Go port also supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables. The Go port also supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
@@ -161,21 +172,32 @@ When enabled, the interactive console uses a subtle ANSI palette: the pending-re
### Tool call definitions ### Tool call definitions
#### `write_file` tool #### `read` tool
- Parameters: - Parameters:
- `path` (string, required): JSON-escaped file path to write to (must be created unless existing). - `filePath` (string, required): path to the file to read or directory to inspect (also accepts `path`).
- `offset` (integer, optional, default 0): byte offset to start writing from. - `offset` (integer, optional): line number to start reading from (1-indexed). Defaults to 1.
- `del_bytes` (integer, optional, default 0): bytes to delete starting from the `offset` prior to writing. - `limit` (integer, optional): maximum number of lines to read. Defaults to 2000.
- Return value: string
- Action: if the target is a regular file, returns numbered lines in `<line>: <content>` format. If the target is a directory, returns a list of directory entries (subdirectories include a trailing `/`).
#### `write` tool
- Parameters:
- `filePath` (string, required): JSON-escaped file path to write to (also accepts `path`).
- `offset` (integer, optional): byte offset to start writing from. If omitted together with `del_bytes`, the whole file is overwritten instead. Defaults to 0 (start of file).
- `del_bytes` (integer, optional): bytes to delete starting at `offset`. If omitted together with `offset`, the whole file is overwritten instead.
- `content` (string, required, may be empty): JSON-escaped content to write to the file. - `content` (string, required, may be empty): JSON-escaped content to write to the file.
- Return value: string - Return value: string
- Action: write `content` into the file at `path` starting at `offset` after deleting `del_bytes` bytes (creating the file and any necessary parent directories). - Action: if `offset` and `del_bytes` are both omitted, the entire file is overwritten with `content` (the file, and any necessary parent directories, are created if missing); otherwise `content` is written at `offset`, optionally deleting `del_bytes` bytes first (splicing prefix + content + suffix and never appending to the end of an existing file), creating the file and parent directories as needed. (Also accepted as legacy `write_file`.)
#### `shell_exec` tool #### `bash` tool
- Parameters: `command` (string) - Parameters:
- `command` (string, required): shell command to run.
- `workdir` (string, optional): working directory to execute the command in.
- Return value: string - Return value: string
- Action: run shell command specified in `command` subject to `shell_timeout` (default 120s) and return `output + '\n\nexit: ' + exit_code` string. - Action: run shell command specified in `command` via `bash -c` subject to `shell_timeout` (default 120s) and return `output + '\nexit: ' + exit_code` string. (Also accepted as legacy `shell_exec`.)
## MicroBantam ## MicroBantam
@@ -183,12 +205,12 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
### Features ### Features
- Full agentic loop: LLM calls, `shell_exec` / `write_file` tool execution with JSON argument validation (invalid args are fed back so the model can self-correct) - Full agentic loop: LLM calls, `bash` / `read` / `write` tool execution with JSON argument validation (invalid args are fed back so the model can self-correct)
- Indirect prompt injection defense: sanitizes tool parameters and tool outputs by filtering non-printable and space-like Unicode characters, preserving standard space, tab, newline, and printable Unicode characters - Indirect prompt injection defense: sanitizes tool parameters and tool outputs by filtering non-printable and space-like Unicode characters, preserving standard space, tab, newline, and printable Unicode characters
- A `...requesting...` in-flight indicator: in-place on a TTY (`\r` overwrite, erased on completion), a plain line when output is piped - A `...requesting...` in-flight indicator: in-place on a TTY (`\r` overwrite, erased on completion), a plain line when output is piped
- Session management: `/save`, `/list`, `/load <id>` (exact id only, no prefix matching), `/cfg <param> [val]`, and auto-save to `~/.bantam/sessions/autosave.json` after every turn and on exit; session ids get `-1`, `-2`, ... suffixes on same-second collisions - Session management: `/save`, `/list`, `/load <id>` (exact id only, no prefix matching), `/cfg <param> [val]`, 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>`, `/cfg`, `/help`) and file input mode - Interactive mode (`/quit`, `/clear`, `/save`, `/list`, `/load <id>`, `/cfg`, `/help`) and file input mode
- Same built-in default system prompt and `OPENAI_API_KEY` fallback as the Go implementation - Same built-in default system prompt and `BANTAM_API_KEY` fallback as the Go implementation
- Automatic recovery from `Invalid assistant message: content or tool_calls must be set` API errors: strips the last assistant message and retries (matches the Go port; note that a 4xx response other than this specific error aborts the run with an `API error` message, unlike the Go port which retries only on 5xx/408/429) - Automatic recovery from `Invalid assistant message: content or tool_calls must be set` API errors: strips the last assistant message and retries (matches the Go port; note that a 4xx response other than this specific error aborts the run with an `API error` message, unlike the Go port which retries only on 5xx/408/429)
### What it drops ### What it drops
@@ -198,6 +220,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
- Line editing, Ctrl+J multi-line prompts and history (plain single-line prompts) - Line editing, Ctrl+J multi-line prompts and history (plain single-line prompts)
- Fibonacci backoff network retries (a failed request aborts with an `API error` message) - Fibonacci backoff network retries (a failed request aborts with an `API error` message)
- `/compact` context summarization - `/compact` context summarization
- `max_tool_res` tool-result spilling (oversized results are sent to the model verbatim)
### Running MicroBantam ### Running MicroBantam
@@ -215,9 +238,20 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
## Extra tools ## Extra tools
The `extras/` directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's built-in tools are `shell_exec` and `write_file`, these helpers can be invoked directly by the agent through `shell_exec` to give it real-world capabilities (web search, live weather) that the base model alone does not have. They are plain `/bin/sh` scripts depending only on `curl` (and `jq` where noted), so the agent can discover and run them just like any other command. The `extras/` directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's built-in tools include `bash`, these helpers can be invoked directly by the agent through `bash` to give it real-world capabilities (web search, live weather) that the base model alone does not have. They are plain `/bin/sh` scripts depending only on `curl` (and `jq` where noted), so the agent can discover and run them just like any other command.
If you keep your own collection of helper scripts, point Bantam at them with the `BANTAM_TOOLS_DIR` environment variable or the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`). When either is defined (environment variable taking precedence over the config key), the Go port appends the line `Extra shell tools can be found at <dir>` to the system prompt at startup, so the agent is aware of where to look for them. The `extras/` scripts shipped here are just examples of what such a directory can contain. If you keep your own collection of helper scripts, point Bantam at them with the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_TOOLS_DIR` environment variable. When either is defined (the config file setting taking precedence over the environment variable), the Go port appends the line `Extra shell tools can be found at <dir>` to the system prompt at startup, so the agent is aware of where to look for them. The `extras/` scripts shipped here are just examples of what such a directory can contain.
## Skills
A *skill* is a reusable, self-contained instruction bundle the agent can load on demand. Each skill is a directory containing a `SKILL.md` file with the skill's prompt/instructions. Point Bantam at a skills directory with the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_SKILLS_DIR` environment variable; the config file setting takes precedence. When either is set, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and the agent can run a skill with:
```text
/skill # list every skill under the configured skills directory
/skill <name> [prompt] # loads <skills_dir>/<name>/SKILL.md and runs it as a prompt
```
A bare `/skill` (no name) lists all skills found under the configured skills directory — a subdirectory is treated as a skill only if it contains a `SKILL.md` file — or reports that no skills directory is configured. The contents of `SKILL.md` are prefixed to the optional `prompt` and sent as the next user turn. If neither `BANTAM_SKILLS_DIR` nor `bantam_skills_dir` is set, `/skill <name>` accepts an absolute path instead: either a skill directory (e.g. `/abs/path/to/skill`, which resolves to `/abs/path/to/skill/SKILL.md`) or a direct path to a `SKILL.md` file.
### `extras/websearch` ### `extras/websearch`
@@ -303,11 +337,19 @@ The default system prompt instructs the agent to respect `AGENTS.md` contents in
### How do I tell Bantam about extra shell tools? ### How do I tell Bantam about extra shell tools?
Set the `BANTAM_TOOLS_DIR` environment variable (or the `bantam_tools_dir` key in the config file — `.bantam.cfg` if present, else `model.cfg`) to a directory containing your helper scripts. When defined, the Go port appends `Extra shell tools can be found at <dir>` to the system prompt at startup, making the agent aware of them. The environment variable takes precedence over the config key; if neither is set, nothing is appended. Set the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_TOOLS_DIR` environment variable to a directory containing your helper scripts. When defined, the Go port appends `Extra shell tools can be found at <dir>` to the system prompt at startup, making the agent aware of them. The config file key takes precedence over the environment variable; if neither is set, nothing is appended.
### How do I give Bantam reusable skills?
Set the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_SKILLS_DIR` environment variable to a directory where each subdirectory is a skill containing a `SKILL.md` file. When defined, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and you (or the agent) can run a skill with `/skill <name> [prompt]`. The config file key takes precedence over the environment variable; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead.
### What happens when a tool result is too large for the context?
Bantam never stuffs an arbitrarily large tool result into the context window. The `max_tool_res` configuration parameter (default `65536`, config-file key takes precedence over the `BANTAM_MAX_TOOL_RES` environment variable) sets the maximum number of bytes of a tool result that may be sent to the model directly. When a result exceeds that limit, the Go port writes the full output to a unique file under `$TMPDIR/bantam/toolres` and replaces the result in the conversation with a short note naming that file and stating its total line count. The agent is then expected to read the file with the `read` tool, paging through it with line offsets and limits (`offset` and `limit`) rather than loading everything at once. Every file spilled during a round is deleted as soon as the round ends, i.e. when the model emits its final response with no pending tool calls. The built-in system prompt also advertises this behaviour to the model at startup.
### Is there any common config place for Bantam? ### Is there any common config place for Bantam?
No, loading the config file (`.bantam.cfg` if present, else `model.cfg`) is deliberately only supported from the current working directory. This allows natural separation of configs per project. In case there's no config file inside the project, Bantam will use the free Big Pickle model from OpenCode Zen with the temperature 0.7. Big Pickle has been chosen as the default because it has no set expiration date, unlike other OpenCode's keyless tiers. No, loading the config file (`.bantam.cfg` if present, else `model.cfg`) is deliberately only supported from the current working directory. This allows natural separation of configs per project. In case there's no config file inside the project, Bantam will use the `openrouter/free` model from Kilo Code with the temperature 0.7.
### Why no MCP support? ### Why no MCP support?
+1440 -384
View File
File diff suppressed because it is too large Load Diff
+1346 -9
View File
File diff suppressed because it is too large Load Diff
+46 -11
View File
@@ -4,12 +4,22 @@
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); 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+/ }; $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 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 with optional offset and byte deletion; 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.\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'; my $SDIR = ($ENV{HOME} || $ENV{USERPROFILE} || '.') . '/.bantam/sessions';
sub cfg { my %d = (endpoint=>'https://opencode.ai/zen/v1', model=>'big-pickle', temperature=>0.7, api_key=>'-', timeout=>300, shell_timeout=>120, max_al_iterations=>1000); sub cfg {
my $cf = -f '.bantam.cfg' ? '.bantam.cfg' : 'model.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');
if (open my $f, '<:encoding(UTF-8)', $cf) { while (<$f>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } } if (-f 'model.cfg' && open my $mf, '<:encoding(UTF-8)', 'model.cfg') { while (<$mf>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
$d{api_key} = $ENV{OPENAI_API_KEY} if $d{api_key} eq '-' && $ENV{OPENAI_API_KEY}; \%d } $d{endpoint} = $ENV{BANTAM_ENDPOINT} if $ENV{BANTAM_ENDPOINT};
$d{model} = $ENV{BANTAM_MODEL} if $ENV{BANTAM_MODEL};
my $t = $ENV{BANTAM_TEMP} // $ENV{BANTAM_TEMPERATURE};
$d{temperature} = $t if defined $t && $t =~ /^\d+(\.\d+)?$/;
$d{timeout} = $ENV{BANTAM_TIMEOUT} if defined $ENV{BANTAM_TIMEOUT} && $ENV{BANTAM_TIMEOUT} =~ /^\d+$/;
$d{shell_timeout} = $ENV{BANTAM_SHELL_TIMEOUT} if defined $ENV{BANTAM_SHELL_TIMEOUT} && $ENV{BANTAM_SHELL_TIMEOUT} =~ /^\d+$/;
$d{max_al_iterations} = $ENV{BANTAM_MAX_AL_ITERATIONS} if defined $ENV{BANTAM_MAX_AL_ITERATIONS} && $ENV{BANTAM_MAX_AL_ITERATIONS} =~ /^\d+$/;
$d{reasoning_effort} = $ENV{BANTAM_REASONING_EFFORT} if $ENV{BANTAM_REASONING_EFFORT};
if (-f '.bantam.cfg' && open my $bf, '<:encoding(UTF-8)', '.bantam.cfg') { while (<$bf>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
$d{api_key} = $ENV{BANTAM_API_KEY} if ($d{api_key} eq '-' || !$d{api_key}) && $ENV{BANTAM_API_KEY};
\%d }
sub filter_text { my $s = shift // ''; $s =~ s/[^\x20\t\n\p{L}\p{N}\p{P}\p{S}\p{M}\p{Zs}]//g; $s } sub filter_text { my $s = shift // ''; $s =~ s/[^\x20\t\n\p{L}\p{N}\p{P}\p{S}\p{M}\p{Zs}]//g; $s }
sub T { my ($n, $d, $p, $r) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>$r || [keys %$p]}}} } sub T { my ($n, $d, $p, $r) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>$r || [keys %$p]}}} }
sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') { sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') {
@@ -20,9 +30,9 @@ sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') {
} } } } } }
sub llm { my ($c, $msgs) = @_; sanitize_msgs($msgs); sub llm { my ($c, $msgs) = @_; 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'=>'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 '-'; $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 with optional offset and byte deletion.', {path=>{type=>'string'}, offset=>{type=>'integer'}, del_bytes=>{type=>'integer'}, 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}; } 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 $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)}); my $r = HTTP::Tiny->new(timeout=>0+$c->{timeout})->post("$ep/chat/completions", {headers=>$h, content=>encode_json(\%p)});
@@ -32,9 +42,16 @@ sub llm { my ($c, $msgs) = @_; sanitize_msgs($msgs);
my $rb = substr($r->{content} // '', 0, 500); my $rb = substr($r->{content} // '', 0, 500);
die "API error: " . (length($rb) ? "$rb (HTTP $r->{status})" : ($r->{reason} || "HTTP $r->{status}")) . "\n"; } 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], ''); 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 =~ s/\s+$//; utf8::decode($out); $out = filter_text($out);
$@ ? "$out\n[timeout after ${t}s]\nexit: -1" : "$out\nexit: " . ($? >> 8); } $@ ? "$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] // ''); 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; return "[write_file error: path required]" unless defined $p && length $p;
$off = 0 if $off < 0; $del = 0 if $del < 0; $off = 0 if $off < 0; $del = 0 if $del < 0;
@@ -63,8 +80,22 @@ sub AL { my ($c, $msgs) = ($_[0], $_[1]);
$tc->{function}{arguments} = filter_text($tc->{function}{arguments}); $tc->{function}{arguments} = filter_text($tc->{function}{arguments});
my $a = eval { decode_json($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}"; } 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 'bash' || $fn eq 'shell_exec') { $res = shell_exec($a->{command} // '', $c->{shell_timeout}); }
elsif ($fn eq 'write_file') { $res = write_file($a->{path}, $a->{offset}, $a->{del_bytes}, $a->{content}); } 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 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 error: cannot write $p: $!]";
if (!$res) { print $wfh $cnt; close $wfh; $res = "Successfully wrote " . length($cnt) . " bytes to $p"; }
}
} else {
$res = write_file($p, $a->{offset}, $a->{del_bytes}, $cnt);
}
}
else { $res = "unknown tool: $fn"; } else { $res = "unknown tool: $fn"; }
$res = filter_text($res); $res = filter_text($res);
print "[tool] $fn: $res\n"; print "[tool] $fn: $res\n";
@@ -79,7 +110,11 @@ sub load { my $f = "$SDIR/$_[0].json"; if (-f $f) { open my $fh, '<', $f or die
sub autosave { save($_[0], project_id()) } sub list_sessions { map { [$_->{id}, scalar @{$_->{messages} // []}] } sessions() } sub autosave { save($_[0], project_id()) } sub list_sessions { map { [$_->{id}, scalar @{$_->{messages} // []}] } sessions() }
sub set_cfg { my ($k, $v, @ls, $f) = @_; if (open my $fh, '<:encoding(UTF-8)', '.bantam.cfg') { while (<$fh>) { push @ls, (!/^#/ && /^([^\s=]+)\s*=/ && $1 eq $k) ? ($f = 1, "$k=$v\n") : $_; } } push @ls, "$k=$v\n" unless $f; if (open my $fh, '>:encoding(UTF-8)', '.bantam.cfg') { print $fh @ls; close $fh; } } sub set_cfg { my ($k, $v, @ls, $f) = @_; if (open my $fh, '<:encoding(UTF-8)', '.bantam.cfg') { while (<$fh>) { push @ls, (!/^#/ && /^([^\s=]+)\s*=/ && $1 eq $k) ? ($f = 1, "$k=$v\n") : $_; } } push @ls, "$k=$v\n" unless $f; if (open my $fh, '>:encoding(UTF-8)', '.bantam.cfg') { print $fh @ls; close $fh; } }
sub main { sub main {
my ($c, $sp) = (cfg(), $DEF_SP); my $msgs = [{role=>'system', content=>$sp}]; my ($c, $sp) = (cfg(), $DEF_SP);
my $tmp = (defined $ENV{TMPDIR} && $ENV{TMPDIR} ne '') ? $ENV{TMPDIR} : '/tmp';
$tmp =~ s{/+$}{};
$sp =~ s{\$TMPDIR/bantam}{$tmp/bantam}g;
my $msgs = [{role=>'system', content=>$sp}];
if (@ARGV) { open my $f, '<:encoding(UTF-8)', $ARGV[0] or die "cannot open $ARGV[0]: $!"; local $/; push @$msgs, {role=>'user', content=><$f>}; AL($c, $msgs); autosave($msgs); return; } if (@ARGV) { open my $f, '<:encoding(UTF-8)', $ARGV[0] or die "cannot open $ARGV[0]: $!"; local $/; push @$msgs, {role=>'user', content=><$f>}; AL($c, $msgs); autosave($msgs); return; }
print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save [name] /continue /list /load <id> /cfg <k> [v] /help\n"; print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save [name] /continue /list /load <id> /cfg <k> [v] /help\n";
while (1) { while (1) {
+3 -2
View File
@@ -1,8 +1,9 @@
endpoint=https://opencode.ai/zen/v1 endpoint=https://api.kilo.ai/api/openrouter
model=hy3-free model=openrouter/free
temperature=0.7 temperature=0.7
api_key=- api_key=-
stream=true stream=true
reasoning_effort=high
# Optional tuning (defaults shown): # Optional tuning (defaults shown):
# timeout=300 # timeout=300