oc compatibility overhaul

This commit is contained in:
Luxferre
2026-09-19 00:19:25 +03:00
parent ebc62b93bf
commit e1efe70d8a
4 changed files with 567 additions and 169 deletions
+36 -25
View File
@@ -7,9 +7,9 @@ Bantam is a minimalist, dependency-free AI agent specification with reference im
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.
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
@@ -45,7 +45,7 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
api_key=your_api_key_here
stream=true
context_window=200000
max_tool_res=15000
max_tool_res=65536
reasoning_effort=high
```
@@ -73,7 +73,7 @@ All implementations read `model.cfg` (or `.bantam.cfg`, which takes priority if
- `/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)
- `!<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
- `/clear` — reset the conversation to just the system prompt
- `/quit` — exit
@@ -100,13 +100,13 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
### 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}`.
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`)**:
- Send `messages` and tool definitions (`shell_exec`, `write_file`) to the OpenAI-compatible `/chat/completions` API endpoint with custom request headers (see below) 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.
- 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.
- 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.
4. **Post-Turn Reporting & Compaction**:
- Display token usage and context window percentage.
@@ -118,23 +118,23 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
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.
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.
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 `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`.
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. `/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.
### 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.
- Set custom request headers. If the endpoint host is `opencode.ai` (or a subdomain), send the OpenCode Zen header set: `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_<26 base32 chars>` per call. For every other endpoint, keep the legacy `User-Agent: opencode/1.18.31` plus `X-Session-Id` and `x-session-affinity`. 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`).
- If `stream=true`, parse SSE stream (`data: {...}`) for real-time reasoning and text output, bracketing reasoning with `--- reasoning start ---` / `--- reasoning end ---` markers.
- 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`).
- 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.
3. If there are pending `tool_calls` in the assistant response:
- 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.
- 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).
- 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 byte length, and how to read it partially (e.g. `tail -c +OFFSET <file> | head -c LENGTH`); every spilled file is deleted when the round ends (i.e. when the model emits a final response with no tool calls).
- 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]`).
- Append tool result message (`role: "tool"`, `tool_call_id`, `content`: result string) to `messages`.
- Loop back to step 1.
@@ -153,10 +153,10 @@ If the API rejects the request with an `Invalid assistant message: content or to
- `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`; 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; falls back to `BANTAM_TIMEOUT` env var)
- `shell_timeout` (timeout in seconds for `shell_exec` commands, default 120; falls back to `BANTAM_SHELL_TIMEOUT` env var)
- `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; 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; 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 15000; larger results are written to a unique file under `$TMPDIR/bantam/toolres` and replaced by a short instruction naming the file and its byte length so it can be read in parts, and the file is deleted once the round ends; falls back to `BANTAM_MAX_TOOL_RES` 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. 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.)
@@ -170,21 +170,32 @@ When enabled, the interactive console uses a subtle ANSI palette: the pending-re
### Tool call definitions
#### `write_file` tool
#### `read` tool
- Parameters:
- `path` (string, required): JSON-escaped file path to write to.
- `filePath` (string, required): path to the file to read or directory to inspect (also accepts `path`).
- `offset` (integer, optional): line number to start reading from (1-indexed). Defaults to 1.
- `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.
- Return value: string
- 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.
- 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
- 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
@@ -192,7 +203,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
### 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
- 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
@@ -225,7 +236,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
## 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` 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.
@@ -332,7 +343,7 @@ Set the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, el
### 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 `15000`, 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 byte length. The agent is then expected to read the file with `shell_exec`, paging through it with byte offsets (e.g. `tail -c +OFFSET <file> | head -c LENGTH`) 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.
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?