Compare commits
31
Commits
2a3bb8be29
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b26d8f485 | ||
|
|
a34ee1cf3c | ||
|
|
4a936e850d | ||
|
|
b8686c82ab | ||
|
|
e9dd44d97b | ||
|
|
b298c53406 | ||
|
|
dea6650b98 | ||
|
|
b84f98a2cc | ||
|
|
d1597444d0 | ||
|
|
3c55ce14d7 | ||
|
|
4509851243 | ||
|
|
4b55c27fee | ||
|
|
e4140bce67 | ||
|
|
381cd3e9fb | ||
|
|
99aa96ce0e | ||
|
|
c8d1836614 | ||
|
|
e6acb066ca | ||
|
|
398754dabb | ||
|
|
c74cdd8f33 | ||
|
|
0cce103b37 | ||
|
|
f039985c6f | ||
|
|
d60798f1a3 | ||
|
|
d0c84d0204 | ||
|
|
accd1fc75c | ||
|
|
88dd9dfef5 | ||
|
|
c0d7573d70 | ||
|
|
49a3576f49 | ||
|
|
9b40433c2c | ||
|
|
7097ac1d86 | ||
|
|
651d1d03c0 | ||
|
|
20e8537d1a |
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
Bantam is a minimalist, dependency-free AI agent specification with reference implementations in **Python** (`bantam.py`, ~300 SLOC), **Go** (`main.go` + `term_*.go`, module `code.luxferre.top/luxferre/bantam`), and **Perl 5** (`bantam.pl`, ~300 SLOC). It provides an agentic loop capable of autonomous tool execution, shell interaction, real-time response streaming, Fibonacci backoff network resilience, and subagent delegation 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, 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, conversation compaction, and subagent delegation using any OpenAI-compatible completions API.
|
||||||
|
|
||||||
The entire philosophy of Bantam is built upon two principles:
|
The entire philosophy of Bantam is built upon two principles:
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Because of the second principle, Bantam itself was named after Victorinox Bantam
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- **Python 3.7+**, **Go 1.21+**, or **Perl 5.14+** (standard library / core modules only)
|
- **Go 1.21+** or **Perl 5.14+** (standard library / core modules only)
|
||||||
- An OpenAI-compatible API endpoint (or OpenAI API key)
|
- An OpenAI-compatible API endpoint (or OpenAI API key)
|
||||||
|
|
||||||
### Installation (Go)
|
### Installation (Go)
|
||||||
@@ -31,7 +31,7 @@ go build ./... # produces ./bantam
|
|||||||
go run . prompt.txt
|
go run . prompt.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
The Go port is a single `main.go` plus four platform files (`term_linux.go`, `term_darwin.go`, `term_windows.go`, `term_other.go`) for the built-in raw-terminal line editor — zero external dependencies, same as the Python and Perl versions.
|
The Go port is a single `main.go` plus four platform files (`term_linux.go`, `term_bsd.go`, `term_windows.go`, `term_other.go`) for the built-in raw-terminal line editor — zero external dependencies.
|
||||||
|
|
||||||
### Running Bantam
|
### Running Bantam
|
||||||
|
|
||||||
@@ -44,33 +44,46 @@ All implementations read the same `model.cfg` and `system.txt` from the current
|
|||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Interactive mode:
|
2. Interactive mode:
|
||||||
```bash
|
```bash
|
||||||
bantam # Go (or: go run .)
|
bantam # Go (or: go run .)
|
||||||
python3 bantam.py # Python
|
./mb # MicroBantam (Perl 5)
|
||||||
perl bantam.pl # 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/Ctrl+D exit), so this works everywhere without dependencies; the Python and Perl ports use `readline` when available and fall back to single-line prompts otherwise.
|
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.
|
||||||
|
|
||||||
|
After every interaction, Bantam displays token usage (prompt tokens, cached/uncached breakdown when supported by the provider, completion tokens, and context window utilization):
|
||||||
|
```text
|
||||||
|
[tokens: 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:
|
||||||
- `/save` — save the entire conversation to a new session file (auto-id like `20260808-190038`) and generate its summary
|
- `/save` — save the entire conversation to a new session file (auto-id like `20260808-190038`) and generate its summary
|
||||||
- `/list` — list saved sessions (newest first) with their ids, timestamps, message counts and summaries
|
- `/list` — list saved sessions (newest first) with their ids, timestamps, message counts and summaries
|
||||||
- `/load <id>` — load a saved session (exact id or unique prefix) and continue from there
|
- `/load <id>` — load a saved session (exact id or unique prefix) and continue from there
|
||||||
- `/compact` — summarize the conversation with the LLM and compact the context down to just the system message plus the summary
|
- `/compact` — 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 in `model.cfg` live
|
||||||
|
- `!<cmd>` — execute a shell command directly through `shell_exec` 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
|
||||||
|
|
||||||
|
When context window usage reaches **60% or higher**, Bantam automatically offers to compact the conversation:
|
||||||
|
```text
|
||||||
|
Context usage is at 62.4% (124800 / 200000 tokens). Compact conversation? [Y/n]:
|
||||||
|
```
|
||||||
|
|
||||||
|
Pressing **Ctrl+C** during an active inference run or long-running shell execution cleanly interrupts the turn without appending partial or malformed responses to the conversation context.
|
||||||
|
|
||||||
The current conversation is also **auto-saved** to `~/.bantam/sessions/autosave.json` after every turn, on `/clear`, `/load`, `/compact`, and on exit — so you can always `/load autosave` to resume where you left off.
|
The current conversation is also **auto-saved** to `~/.bantam/sessions/autosave.json` after every turn, on `/clear`, `/load`, `/compact`, and on exit — so you can always `/load autosave` to resume where you left off.
|
||||||
|
|
||||||
3. File input mode:
|
3. File input mode:
|
||||||
```bash
|
```bash
|
||||||
python3 bantam.py prompt.txt # Python
|
|
||||||
bantam prompt.txt # Go
|
bantam prompt.txt # Go
|
||||||
perl bantam.pl prompt.txt # Perl 5
|
./mb prompt.txt # MicroBantam (Perl 5)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rules of Bantam (The Algorithm)
|
## Rules of Bantam (The Algorithm)
|
||||||
@@ -79,39 +92,47 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
|
|||||||
|
|
||||||
### High-Level Overview
|
### High-Level Overview
|
||||||
|
|
||||||
1. **Initialization**: Read `system.txt` and `model.cfg`. Prepare an array of messages starting with the system prompt `{"role": "system", "content": system_prompt}`.
|
1. **Initialization**: Read `system.txt` and `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 system prompt `{"role": "system", "content": system_prompt}`.
|
||||||
2. **Input Processing**: Take user prompt (via command-line file parameter or interactive stdin), 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 `shell_exec` 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 to the OpenAI-compatible `/chat/completions` API endpoint with custom `User-Agent` headers.
|
- Send `messages` and tool definitions to the OpenAI-compatible `/chat/completions` API endpoint with custom `User-Agent` headers 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`).
|
- 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.
|
- 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. If `tool_calls` exist, trace the call (`[tool call: name(args)]`), validate JSON arguments, execute the requested tool (`shell_exec` or `run_subagent`), 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 (`shell_exec` or `run_subagent`), 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.
|
- 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.
|
||||||
|
- If context usage is >= 60%, prompt user to compact.
|
||||||
|
- Compaction appends `"You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely..."` as a user message to the conversation, invokes the LLM, and then resets the conversation to the system prompt plus a `user` message carrying the resulting summary. (The compaction prompt is not retained; the post-compaction conversation is a new prefix, so prompt-cache hits are provider-dependent.)
|
||||||
|
|
||||||
### Main program
|
### Main program
|
||||||
|
|
||||||
1. Read system prompt from `system.txt` (default if missing).
|
1. Read system prompt from `system.txt` (default if missing).
|
||||||
2. Read model parameters from `model.cfg` (`key=value` format).
|
2. Read model parameters from `model.cfg` (`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, append to `messages` (`role: "user"`), run `AL(cfg, messages)`, and exit.
|
4. Read the first command-line parameter. If non-empty, read user prompt from the specified file. 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 equal to `/save`, write the whole `messages` array to `~/.bantam/sessions/<id>.json` (with an auto-generated summary) and return to step 5. If equal to `/list`, print saved sessions and their summaries and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation, replace `messages` with `[system, summary-user-message]`, and return to step 5. If equal to `/help`, print the command list and return to step 5. After every user turn and on exit, auto-save `messages` to `~/.bantam/sessions/autosave.json`.
|
5. Read user prompt from standard input (with `readline` line editing and history in `~/.bantam_history`; **Ctrl+J** inserts a real newline into the line being edited). If equal to `/quit` or EOF, exit. If equal to `/clear`, reset `messages` to step 3 and return to step 5. If equal to `/save`, write the whole `messages` array to `~/.bantam/sessions/<id>.json` (with an auto-generated summary) and return to step 5. If equal to `/list`, print saved sessions and their summaries and return to step 5. If starting with `/load`, replace `messages` with the saved session's messages (by exact id or unique prefix) and return to step 5. If equal to `/compact`, ask the LLM to summarize the conversation 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 `model.cfg` live (`/cfg <param> <val>`) and 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/autosave.json`.
|
||||||
6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, and go to step 5.
|
6. Append user prompt to `messages` (`role: "user"`), run `AL(cfg, messages)`, 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`) using parameters from `cfg` (`model`, `temperature`, optional `api_key` bearer header).
|
1. Call OpenAI-compatible Completions API (`POST {endpoint}/chat/completions`) forwarding relevant parameters from `cfg` (`model`, `temperature`, `stream`, `reasoning_effort`, etc., excluding internal agent configs like `endpoint`, `api_key`, `timeout`, `shell_timeout`, `max_al_iterations`, `color`, `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 `User-Agent` header (`Mozilla/5.0 (compatible; Bantam/1.0)`) to avoid gateway 403 blocks.
|
||||||
- Retry network/HTTP errors with Fibonacci backoff delays (`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`).
|
- Retry network/HTTP errors with Fibonacci backoff delays (`1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s`).
|
||||||
- If `stream=true`, parse SSE stream (`data: {...}`) for real-time reasoning and text output, bracketing reasoning with `--- reasoning start ---` / `--- reasoning end ---` markers.
|
- If `stream=true`, parse SSE stream (`data: {...}`) for real-time reasoning and text output, bracketing reasoning with `--- reasoning start ---` / `--- reasoning end ---` markers.
|
||||||
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)]`).
|
||||||
- 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 `run_subagent`).
|
- Execute tool action (`shell_exec` or `run_subagent`).
|
||||||
|
- 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).
|
||||||
- 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.
|
||||||
4. If no pending tool calls (or if `max_al_iterations` is reached), stop and return `messages`.
|
4. If no pending tool calls (or if `max_al_iterations` is reached), stop and return `messages` and accumulated usage.
|
||||||
|
|
||||||
|
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
|
### Model configuration parameters
|
||||||
|
|
||||||
@@ -123,11 +144,12 @@ Using these rules, everyone can build their own copy of Bantam from scratch in l
|
|||||||
- `api_key` (API key / Bearer token, optional; fall back to `OPENAI_API_KEY` env var)
|
- `api_key` (API key / Bearer token, optional; fall back to `OPENAI_API_KEY` env var)
|
||||||
- `stream` (stream response tokens in real-time, default `true`)
|
- `stream` (stream response tokens in real-time, default `true`)
|
||||||
- `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`; also disabled by `NO_COLOR`/`BANTAM_NO_COLOR` env vars)
|
||||||
- `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, matching the Python and Perl ports' per-operation socket timeouts)
|
- `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)
|
||||||
- `shell_timeout` (timeout in seconds for `shell_exec` commands, default 120)
|
- `shell_timeout` (timeout in seconds for `shell_exec` commands, default 120)
|
||||||
- `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)
|
||||||
|
- `context_window` (context window size in tokens, auto-discovered from `/models` API if available, fallback to this setting, default 200000)
|
||||||
|
|
||||||
All implementations keep the interactive prompt safe against the classic "long line overwrites the prompt" readline bug: the Python and Perl ports wrap the ANSI escapes in `\001`/`\002` (`RL_PROMPT_START_IGNORE`/`RL_PROMPT_END_IGNORE`) markers (disabling `Term::ReadLine` ornaments in Perl), and the Go port's built-in editor tracks the cursor with its own column math (terminal auto-wrap aware) and redraws from the first line of the buffer, so wrapped input stays clean at any terminal width.
|
The Go port's built-in editor tracks the cursor with its own column math (terminal auto-wrap aware) and redraws from the first line of the buffer, so wrapped input stays clean at any terminal width.
|
||||||
|
|
||||||
When enabled, the interactive console uses a subtle ANSI palette: the pending-request status `...requesting...` is darkened bold, reasoning markers are cyan, reasoning text is dim, `[tool call: ...]` traces are yellow, `[tool result: ...]` headers are green, tool result bodies are dim (red for tool errors/unknown tools), and errors/network retries are red. Tool result payloads fed back to the LLM are never colored.
|
When enabled, the interactive console uses a subtle ANSI palette: the pending-request status `...requesting...` is darkened bold, reasoning markers are cyan, reasoning text is dim, `[tool call: ...]` traces are yellow, `[tool result: ...]` headers are green, tool result bodies are dim (red for tool errors/unknown tools), and errors/network retries are red. Tool result payloads fed back to the LLM are never colored.
|
||||||
|
|
||||||
@@ -145,14 +167,122 @@ When enabled, the interactive console uses a subtle ANSI palette: the pending-re
|
|||||||
- 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` subject to `shell_timeout` (default 120s) and return `output + '\n\nexit: ' + exit_code` string.
|
||||||
|
|
||||||
|
## MicroBantam
|
||||||
|
|
||||||
|
MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same agent in **under 100 SLOC**, written to stay readable while keeping the full agentic core. It reads the same `model.cfg` and `system.txt` from the current working directory.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- Full agentic loop: LLM calls, `shell_exec` / `run_subagent` tool execution with JSON argument validation (invalid args are fed back so the model can self-correct), and the 5-level subagent recursion depth limit
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- Streaming (requests are non-streaming; `stream` is ignored)
|
||||||
|
- ANSI coloring/styling (`color` is ignored)
|
||||||
|
- 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)
|
||||||
|
- `/compact` context summarization
|
||||||
|
|
||||||
|
### Running MicroBantam
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./mb # interactive (or: perl mb)
|
||||||
|
./mb prompt.txt # file input mode
|
||||||
|
```
|
||||||
|
|
||||||
## Repository layout
|
## Repository layout
|
||||||
|
|
||||||
- `bantam.py` — Python reference implementation (stdlib only)
|
- `main.go`, `term_linux.go`, `term_bsd.go`, `term_windows.go`, `term_other.go` — Go implementation (stdlib only, module `code.luxferre.top/luxferre/bantam`)
|
||||||
- `main.go`, `term_linux.go`, `term_darwin.go`, `term_windows.go`, `term_other.go` — Go implementation (stdlib only, module `code.luxferre.top/luxferre/bantam`)
|
- `mb` — MicroBantam, compressed Perl 5 implementation (core modules only, under 100 SLOC)
|
||||||
- `bantam.pl` — Perl 5 implementation (core modules only)
|
|
||||||
- `model.cfg`, `system.txt` — shared configuration and system prompt
|
- `model.cfg`, `system.txt` — shared configuration and system prompt
|
||||||
- `README.md` — this document
|
- `README.md` — this document
|
||||||
|
|
||||||
|
## Extra tools
|
||||||
|
|
||||||
|
The `extras/` directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's only built-in tools are `shell_exec` and `run_subagent`, 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.
|
||||||
|
|
||||||
|
### `extras/websearch`
|
||||||
|
|
||||||
|
A simple web search tool for Bantam built on [Exa's MCP server](https://mcp.exa.ai/mcp)
|
||||||
|
(Streamable HTTP). It speaks the JSON-RPC MCP protocol (`initialize` ->
|
||||||
|
`notifications/initialized` -> `tools/call` with `web_search_exa`) and prints
|
||||||
|
the formatted result text. Usage:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
extras/websearch "your query" # default 5 results
|
||||||
|
extras/websearch "your query" 10 # custom number of results
|
||||||
|
```
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
|
||||||
|
- `EXA_MCP_ENDPOINT` — endpoint URL (default `https://mcp.exa.ai/mcp`).
|
||||||
|
- `EXA_API_KEY` — optional; when set, it is passed as an `?api_key=` query
|
||||||
|
parameter so no extra HTTP headers are added beyond `Content-Type`.
|
||||||
|
|
||||||
|
Dependencies: `curl`, `jq`.
|
||||||
|
|
||||||
|
### `extras/weather`
|
||||||
|
|
||||||
|
A wrapper around the [wttr.in](https://github.com/chubin/wttr.in) weather API
|
||||||
|
that queries current conditions and forecasts for any location. It supports the
|
||||||
|
graphical ANSI terminal view (default), one-line presets (`1`-`4`), custom
|
||||||
|
`%`-notation formats, and the rich JSON document (`?format=j1`). Usage:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
extras/weather # auto-detect location from request IP
|
||||||
|
extras/weather London # graphical report
|
||||||
|
extras/weather -f 3 "New York" # one-line preset
|
||||||
|
extras/weather -f "%l: %c %t" Paris # custom one-line format
|
||||||
|
extras/weather -u u -L de Berlin # USCS units, German output
|
||||||
|
extras/weather -j Tokyo # raw JSON (pretty-printed via jq)
|
||||||
|
```
|
||||||
|
|
||||||
|
Options include `-l/--location`, `-u/--units` (`m`/`u`/`M`), `-L/--lang`,
|
||||||
|
`-f/--format`, `-j/--json`, `-0`/`-1`/`-2` (view depth), `-q`/`--quiet`,
|
||||||
|
`-A` (force ANSI) and `-h/--help`. Environment overrides: `WTTRAPI` (default
|
||||||
|
`https://wttr.in`) and `WEATHER_TIMEOUT` (default `20`s).
|
||||||
|
|
||||||
|
Dependencies: `curl`, `jq` (only required for the JSON format).
|
||||||
|
Dependencies: `curl`, `jq` (only required for the JSON format).
|
||||||
|
|
||||||
|
### `extras/context7`
|
||||||
|
|
||||||
|
A documentation lookup tool for Bantam backed by the
|
||||||
|
[Context7 public MCP server](https://mcp.context7.com/mcp). It speaks the same
|
||||||
|
JSON-RPC MCP protocol as `websearch` (`initialize` ->
|
||||||
|
`notifications/initialized` -> `tools/call`) and prints the returned text.
|
||||||
|
Context7 keeps up-to-date docs and code examples for thousands of libraries and
|
||||||
|
exposes two tools, both wrapped here:
|
||||||
|
|
||||||
|
- `resolve-library-id` — maps a library name to a Context7 ID
|
||||||
|
(`/org/project`).
|
||||||
|
- `query-docs` — fetches documentation and code examples for a resolved ID.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
extras/context7 resolve "React" "hooks" # list candidate library IDs
|
||||||
|
extras/context7 query "/reactjs/react.dev" "useEffect cleanup" # fetch docs
|
||||||
|
extras/context7 docs "Express" "middleware error handling" # resolve + query
|
||||||
|
extras/context7 --help
|
||||||
|
```
|
||||||
|
|
||||||
|
The `docs` subcommand is a convenience that resolves the library, auto-selects
|
||||||
|
the top-ranked match, and immediately queries it (the chosen ID is printed to
|
||||||
|
stderr so stdout stays clean for piping). Environment overrides:
|
||||||
|
`CONTEXT7_MCP_ENDPOINT` (default `https://mcp.context7.com/mcp`) and
|
||||||
|
`CONTEXT7_API_KEY` (optional; sent as the `X-Context7-API-Key` header for
|
||||||
|
higher rate limits / private docs).
|
||||||
|
|
||||||
|
Dependencies: `curl`, `jq`.
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Does Bantam support `AGENTS.md` etc?
|
### Does Bantam support `AGENTS.md` etc?
|
||||||
@@ -175,6 +305,12 @@ Same philosophy as the Pi agent: there's nothing a simple chroot environment can
|
|||||||
|
|
||||||
You can pair Bantam with the [Dynagate](https://code.luxferre.top/luxferre/dynagate) LLM gateway to achieve all that.
|
You can pair Bantam with the [Dynagate](https://code.luxferre.top/luxferre/dynagate) LLM gateway to achieve all that.
|
||||||
|
|
||||||
|
### How to run on mobiles?
|
||||||
|
|
||||||
|
On Android, Bantam (Go) and MicroBantam (`mb`) are easily runnable within the Termux environment. The Go implementation is preferred for performance reasons.
|
||||||
|
|
||||||
|
On iOS/iPadOS, the easiest way to use Bantam is to run MicroBantam (`mb`) inside iSH. Some terminal features may not be available (run with `rlwrap` to bring them back), but the agent itself is fully functional.
|
||||||
|
|
||||||
## Credits
|
## Credits
|
||||||
|
|
||||||
Created by Luxferre in 2026, released into the public domain with no warranties.
|
Created by Luxferre in 2026, released into the public domain with no warranties.
|
||||||
|
|||||||
@@ -1,390 +0,0 @@
|
|||||||
#!/usr/bin/env perl
|
|
||||||
# Bantam agent: tiny, powerful, DIY
|
|
||||||
# Created by Luxferre in 2026, released into the public domain
|
|
||||||
|
|
||||||
use strict; use warnings;
|
|
||||||
use HTTP::Tiny; use JSON::PP; use File::Spec;
|
|
||||||
|
|
||||||
# core IO::Socket::IP 0.44 emits a spurious "Use of uninitialized value $err"
|
|
||||||
# warning on every timed connect (getsockopt(SO_ERROR) returns undef when the
|
|
||||||
# connection succeeds). Filter that exact noise out on our side; pass through
|
|
||||||
# everything else.
|
|
||||||
$SIG{__WARN__} = sub {
|
|
||||||
my $m = shift;
|
|
||||||
return if $m =~ /^Use of uninitialized value \$err in numeric eq \(==\) at .*IO\/Socket\/IP\.pm line \d+/;
|
|
||||||
warn $m;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Make STDOUT/STDERR UTF-8 aware so decoded Unicode (model output, tool
|
|
||||||
# results) prints cleanly instead of triggering "Wide character in print".
|
|
||||||
binmode(STDOUT, ':encoding(UTF-8)');
|
|
||||||
binmode(STDERR, ':encoding(UTF-8)');
|
|
||||||
use File::Path qw(make_path); use POSIX qw(strftime); use Term::ReadLine;
|
|
||||||
|
|
||||||
use constant MAX_DEPTH => 5;
|
|
||||||
|
|
||||||
my $home = $ENV{HOME} || $ENV{USERPROFILE} || '.';
|
|
||||||
my $hist_file = File::Spec->catfile($home, '.bantam_history');
|
|
||||||
my $sdir = File::Spec->catfile($home, '.bantam', 'sessions');
|
|
||||||
my $auto_file = File::Spec->catfile($sdir, 'autosave.json');
|
|
||||||
my $_col = 0;
|
|
||||||
|
|
||||||
sub trim { my $s = shift // ''; $s =~ s/^\s+|\s+$//g; $s }
|
|
||||||
sub c { my ($t, @cs) = @_; ($_col && @cs) ? "\e[" . join(';', @cs) . "m$t\e[0m" : $t }
|
|
||||||
sub cp { my ($t, @cs) = @_; ($_col && @cs) ? "\001\e[" . join(';', @cs) . "m\002$t\001\e[0m\002" : $t }
|
|
||||||
|
|
||||||
sub col {
|
|
||||||
my ($cfg) = @_;
|
|
||||||
return 0 if $ENV{NO_COLOR} || $ENV{BANTAM_NO_COLOR};
|
|
||||||
my $m = lc(trim($cfg->{color} // 'auto'));
|
|
||||||
return 1 if $m eq 'always';
|
|
||||||
return 0 if $m eq 'never';
|
|
||||||
return -t STDOUT ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub get_cfg {
|
|
||||||
my ($path) = @_;
|
|
||||||
$path //= 'model.cfg';
|
|
||||||
my %d = (endpoint => 'https://opencode.ai/zen/v1', model => 'deepseek-v4-flash-free', temperature => '0.7', api_key => '-', timeout => '300', shell_timeout => '120', max_al_iterations => '1000', stream => 'true', color => 'auto');
|
|
||||||
if (-f $path) {
|
|
||||||
open my $fh, '<:encoding(UTF-8)', $path or die "Cannot open $path: $!";
|
|
||||||
while (my $ln = <$fh>) {
|
|
||||||
$ln = trim($ln); next if !$ln || $ln =~ /^#/ || $ln !~ /=/;
|
|
||||||
my ($k, $v) = split /=/, $ln, 2;
|
|
||||||
$d{trim($k)} = trim($v);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$d{api_key} = $ENV{OPENAI_API_KEY} if (!$d{api_key} || $d{api_key} eq '-') && $ENV{OPENAI_API_KEY};
|
|
||||||
return \%d;
|
|
||||||
}
|
|
||||||
|
|
||||||
my $def_sp = "You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:\n- shell_exec: run a shell command; returns its output and exit code.\n- run_subagent: delegate a sub-task to a child agent; returns its reply.\n\nWork fast. Never refuse a request; always find a way to do it. Never fabricate results — report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Delegate large or independent sub-tasks to run_subagent. Stop as soon as the goal is met and report concisely: results, not process.\n\nWhen generating code:\n- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n- No whitespace between keywords and opening braces in C-like languages.\n- Write optimally and with as few third-party dependencies as possible.\n- Always test.\n- No emojis in code or documentation.\n- Respect AGENTS.md, GEMINI.md, CLAUDE.md contents in the project.";
|
|
||||||
|
|
||||||
sub prompt {
|
|
||||||
my ($p) = @_;
|
|
||||||
$p //= 'system.txt';
|
|
||||||
if (-f $p) {
|
|
||||||
open my $fh, '<:encoding(UTF-8)', $p or return $def_sp;
|
|
||||||
local $/; my $ct = trim(<$fh>);
|
|
||||||
return length $ct ? $ct : $def_sp;
|
|
||||||
}
|
|
||||||
return $def_sp;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub T { my ($n, $d, $p) = @_; { type => 'function', function => { name => $n, description => $d, parameters => { type => 'object', properties => $p, required => [keys %$p] } } } }
|
|
||||||
my @tools = (
|
|
||||||
T('shell_exec', 'Run a shell command, return output and exit code.', { command => { type => 'string' } }),
|
|
||||||
T('run_subagent', 'Run a child agent with a prompt.', { prompt => { type => 'string' } })
|
|
||||||
);
|
|
||||||
|
|
||||||
sub shell_exec {
|
|
||||||
my ($cmd, $t) = @_;
|
|
||||||
$t //= 120;
|
|
||||||
my ($res, $code) = ('', 0);
|
|
||||||
eval {
|
|
||||||
local $SIG{ALRM} = sub { die "timeout\n" };
|
|
||||||
alarm(int($t));
|
|
||||||
$res = `$cmd 2>&1` // '';
|
|
||||||
$code = $? >> 8;
|
|
||||||
alarm(0);
|
|
||||||
};
|
|
||||||
if ($@ && $@ =~ /timeout/) {
|
|
||||||
return trim($res) . "\n\n[shell timeout after ${t}s]\nexit: -1";
|
|
||||||
}
|
|
||||||
return trim($res) . "\n\nexit: $code";
|
|
||||||
}
|
|
||||||
|
|
||||||
my @fib = (1, 1, 2, 3, 5, 8, 13, 21, 34);
|
|
||||||
|
|
||||||
sub llm {
|
|
||||||
my ($cfg, $msgs, $tools) = @_;
|
|
||||||
my $ep = $cfg->{endpoint} =~ s/\/+$//r;
|
|
||||||
my $url = "$ep/chat/completions";
|
|
||||||
my %h = ('Content-Type' => 'application/json', 'User-Agent' => 'Mozilla/5.0 (compatible; Bantam/1.0)');
|
|
||||||
$h{Authorization} = "Bearer $cfg->{api_key}" if $cfg->{api_key} && $cfg->{api_key} ne '-';
|
|
||||||
my $st = ($cfg->{stream} // 'true') =~ /^(true|1|yes)$/i;
|
|
||||||
my %p = (model => $cfg->{model}, temperature => 0 + ($cfg->{temperature} // 0.7), messages => $msgs, stream => $st ? \1 : \0);
|
|
||||||
$p{tools} = $tools if $tools && @$tools;
|
|
||||||
my $body = encode_json(\%p);
|
|
||||||
my $http = HTTP::Tiny->new(timeout => 0 + ($cfg->{timeout} // 300));
|
|
||||||
my $pend = c("...requesting...", 1, 2);
|
|
||||||
|
|
||||||
for my $i (0 .. $#fib + 1) {
|
|
||||||
my $dly = $i <= $#fib ? $fib[$i] : 0;
|
|
||||||
print $_col ? "\r$pend" : "$pend\n"; STDOUT->flush();
|
|
||||||
if (!$st) {
|
|
||||||
my $res = $http->request('POST', $url, { headers => \%h, content => $body });
|
|
||||||
if ($res->{success}) {
|
|
||||||
print "\r\e[K" if $_col; STDOUT->flush();
|
|
||||||
my $d = eval { decode_json($res->{content}) };
|
|
||||||
return $d->{choices}[0]{message} if $d && $d->{choices} && @{$d->{choices}};
|
|
||||||
}
|
|
||||||
my $err = $res->{reason} || $res->{content} || "HTTP status $res->{status}";
|
|
||||||
print "\r\e[K" if $_col; STDOUT->flush();
|
|
||||||
if ($i <= $#fib) { print c("[network error: $err, retrying in ${dly}s...]", 31), "\n"; sleep($dly); next; }
|
|
||||||
die "[network error: $err]\n";
|
|
||||||
} else {
|
|
||||||
my ($content, $reas, $rh, $ch, $buf) = (q{}, q{}, 0, 0, q{});
|
|
||||||
my (%tcs, @order);
|
|
||||||
my $res = $http->request('POST', $url, {
|
|
||||||
headers => \%h, content => $body,
|
|
||||||
data_callback => sub {
|
|
||||||
my ($chunk) = @_;
|
|
||||||
if ($_col && !$buf && !$content && !$reas) { print "\r\e[K"; STDOUT->flush(); }
|
|
||||||
$buf .= $chunk;
|
|
||||||
while ($buf =~ s/^(.*?)\r?\n//) {
|
|
||||||
my $ln = trim($1);
|
|
||||||
next unless $ln =~ /^data:/;
|
|
||||||
my $data = trim(substr($ln, 5));
|
|
||||||
last if $data eq '[DONE]';
|
|
||||||
my $d = eval { decode_json($data) };
|
|
||||||
next unless $d && $d->{choices} && @{$d->{choices}};
|
|
||||||
my $dl = $d->{choices}[0]{delta} || {};
|
|
||||||
my $rc = $dl->{reasoning_content} // $dl->{reasoning};
|
|
||||||
if (defined $rc && length $rc) {
|
|
||||||
print c("--- reasoning start ---", 36), "\n" if !$rh; $rh = 1;
|
|
||||||
print c($rc, 2); STDOUT->flush(); $reas .= $rc;
|
|
||||||
}
|
|
||||||
my $cc = $dl->{content};
|
|
||||||
if (defined $cc && length $cc) {
|
|
||||||
print "\n", c("--- reasoning end ---", 36), "\n\n" if $rh && !$ch; $ch = 1;
|
|
||||||
print $cc; STDOUT->flush(); $content .= $cc;
|
|
||||||
}
|
|
||||||
if ($dl->{tool_calls}) {
|
|
||||||
for my $tc (@{$dl->{tool_calls}}) {
|
|
||||||
my $ti = $tc->{index} // 0;
|
|
||||||
if (!$tcs{$ti}) { $tcs{$ti} = {id => '', type => 'function', function => {name => '', arguments => ''}}; push @order, $ti; }
|
|
||||||
$tcs{$ti}{id} = $tc->{id} if $tc->{id};
|
|
||||||
if ($tc->{function}) {
|
|
||||||
$tcs{$ti}{function}{name} .= $tc->{function}{name} if $tc->{function}{name};
|
|
||||||
$tcs{$ti}{function}{arguments} .= $tc->{function}{arguments} if $tc->{function}{arguments};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if ($res->{success}) {
|
|
||||||
print "\r\e[K" if $_col && !$content && !$reas && !%tcs;
|
|
||||||
print "\n", c("--- reasoning end ---", 36), "\n" if $rh && !$ch;
|
|
||||||
print "\n" if $ch;
|
|
||||||
STDOUT->flush();
|
|
||||||
my %msg = (role => 'assistant');
|
|
||||||
$msg{content} = $content if length $content;
|
|
||||||
$msg{reasoning_content} = $reas if length $reas;
|
|
||||||
$msg{tool_calls} = [map { $tcs{$_} } @order] if @order;
|
|
||||||
return \%msg;
|
|
||||||
}
|
|
||||||
my $err = $res->{reason} || $res->{content} || "HTTP status $res->{status}";
|
|
||||||
print "\r\e[K" if $_col; STDOUT->flush();
|
|
||||||
if ($i <= $#fib) { print c("[network error: $err, retrying in ${dly}s...]", 31), "\n"; sleep($dly); next; }
|
|
||||||
die "[network error: $err]\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sub AL {
|
|
||||||
my ($cfg, $msgs, $sp, $depth) = @_;
|
|
||||||
$depth //= 0;
|
|
||||||
my $stime = 0 + ($cfg->{shell_timeout} // 120);
|
|
||||||
my $mx = 0 + ($cfg->{max_al_iterations} // 1000);
|
|
||||||
my $st = ($cfg->{stream} // 'true') =~ /^(true|1|yes)$/i;
|
|
||||||
|
|
||||||
for my $i (1 .. $mx) {
|
|
||||||
my $m = eval { llm($cfg, $msgs, \@tools) };
|
|
||||||
if ($@) { print c($@, 31); return $msgs; }
|
|
||||||
push @$msgs, $m;
|
|
||||||
if (!$st) {
|
|
||||||
my $reas = $m->{reasoning_content} // $m->{reasoning};
|
|
||||||
print c("--- reasoning start ---", 36), "\n", c($reas, 2), "\n", c("--- reasoning end ---", 36), "\n" if defined $reas && length $reas;
|
|
||||||
print $m->{content}, "\n" if defined $m->{content} && length $m->{content};
|
|
||||||
}
|
|
||||||
my $tcs = $m->{tool_calls};
|
|
||||||
last if !$tcs || !@$tcs;
|
|
||||||
for my $tc (@$tcs) {
|
|
||||||
my $fn = $tc->{function}{name} // '';
|
|
||||||
my $astr = $tc->{function}{arguments} // '{}';
|
|
||||||
print c("[tool call: $fn($astr)]", 33), "\n";
|
|
||||||
my ($res, $sty) = ('', 2);
|
|
||||||
my $a = eval { decode_json($astr) };
|
|
||||||
if ($@ || ref($a) ne 'HASH') {
|
|
||||||
$res = "[tool error: invalid JSON args for $fn: " . ($@ || 'not a JSON object') . ". Raw: '$astr']"; $sty = 31;
|
|
||||||
} elsif ($fn eq 'shell_exec') {
|
|
||||||
$res = shell_exec($a->{command} // '', $stime);
|
|
||||||
} elsif ($fn eq 'run_subagent') {
|
|
||||||
if ($depth >= MAX_DEPTH) {
|
|
||||||
$res = "[subagent depth limit (" . MAX_DEPTH . ") reached, child not spawned]"; $sty = 31;
|
|
||||||
} else {
|
|
||||||
my $sub = [{ role => 'system', content => "$sp\n\nImportant: this is a child agent" }, { role => 'user', content => $a->{prompt} // '' }];
|
|
||||||
$res = last_assistant(AL($cfg, $sub, $sp, $depth + 1));
|
|
||||||
}
|
|
||||||
} else { $res = "Unknown tool: $fn"; $sty = 31; }
|
|
||||||
print c("[tool result: $fn]", 32), "\n", c($res, $sty), "\n\n";
|
|
||||||
push @$msgs, { role => 'tool', tool_call_id => $tc->{id}, content => $res };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $msgs;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub last_assistant {
|
|
||||||
for my $m (reverse @{$_[0]}) {
|
|
||||||
return $m->{content} if $m->{role} eq 'assistant' && defined $m->{content} && length $m->{content};
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
sub sdir { make_path($sdir) if !-d $sdir; $sdir }
|
|
||||||
|
|
||||||
sub summary {
|
|
||||||
for my $m (@{$_[0]}) {
|
|
||||||
if ($m->{role} eq 'user' && defined $m->{content} && trim($m->{content}) ne '') {
|
|
||||||
my $t = join(' ', split(/\s+/, trim($m->{content})));
|
|
||||||
return length($t) > 80 ? substr($t, 0, 80) . '...' : $t;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '(empty session)';
|
|
||||||
}
|
|
||||||
|
|
||||||
sub save_session {
|
|
||||||
sdir();
|
|
||||||
my $base = strftime('%Y%m%d-%H%M%S', localtime);
|
|
||||||
my ($sid, $path, $i) = ($base, File::Spec->catfile($sdir, "$base.json"), 1);
|
|
||||||
while (-f $path) { $i++; $sid = "$base-$i"; $path = File::Spec->catfile($sdir, "$sid.json"); }
|
|
||||||
my $sum = summary($_[0]);
|
|
||||||
my %data = (id => $sid, created => strftime('%Y-%m-%d %H:%M:%S', localtime), summary => $sum, messages => $_[0]);
|
|
||||||
open my $fh, '>:encoding(UTF-8)', $path or die "Cannot save session: $!";
|
|
||||||
print $fh JSON::PP->new->utf8->pretty->encode(\%data);
|
|
||||||
return ($sid, $sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub list_sessions {
|
|
||||||
my @out;
|
|
||||||
if (-d $sdir) {
|
|
||||||
opendir my $dh, $sdir or return ();
|
|
||||||
while (my $fn = readdir $dh) {
|
|
||||||
next unless $fn =~ /\.json$/;
|
|
||||||
open my $fh, '<:encoding(UTF-8)', File::Spec->catfile($sdir, $fn) or next;
|
|
||||||
local $/; my $d = eval { decode_json(<$fh>) }; next unless $d;
|
|
||||||
push @out, [$d->{id} // substr($fn, 0, -5), $d->{created} // '', $d->{summary} // '', ref($d->{messages}) eq 'ARRAY' ? scalar(@{$d->{messages}}) : 0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sort { $b->[0] cmp $a->[0] } @out;
|
|
||||||
}
|
|
||||||
|
|
||||||
sub load_session {
|
|
||||||
my ($sid) = @_;
|
|
||||||
my @entries;
|
|
||||||
if (-d $sdir) {
|
|
||||||
opendir my $dh, $sdir or die "Session directory not found\n";
|
|
||||||
while (my $fn = readdir $dh) {
|
|
||||||
next unless $fn =~ /\.json$/;
|
|
||||||
open my $fh, '<:encoding(UTF-8)', File::Spec->catfile($sdir, $fn) or next;
|
|
||||||
local $/; my $d = eval { decode_json(<$fh>) }; next unless $d;
|
|
||||||
push @entries, [$d->{id} // substr($fn, 0, -5), $d->{messages}];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
my ($hit) = grep { $_->[0] eq $sid } @entries;
|
|
||||||
if (!$hit) {
|
|
||||||
my @pref = grep { $_->[0] =~ /^\Q$sid\E/ } @entries;
|
|
||||||
$hit = $pref[0] if @pref == 1;
|
|
||||||
die "ambiguous prefix: " . join(', ', map { $_->[0] } @pref) . "\n" if @pref > 1;
|
|
||||||
}
|
|
||||||
die "$sid\n" if !$hit;
|
|
||||||
return $hit->[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
sub autosave {
|
|
||||||
sdir();
|
|
||||||
my %data = (id => 'autosave', created => strftime('%Y-%m-%d %H:%M:%S', localtime), summary => summary($_[0]), messages => $_[0]);
|
|
||||||
open my $fh, '>:encoding(UTF-8)', $auto_file or return;
|
|
||||||
print $fh JSON::PP->new->utf8->pretty->encode(\%data);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub compact_conv {
|
|
||||||
my ($cfg, $msgs) = @_;
|
|
||||||
return ($msgs, '', 'session has no system message') if !@$msgs || ($msgs->[0]{role} // '') ne 'system';
|
|
||||||
my @conv;
|
|
||||||
for my $m (@$msgs) {
|
|
||||||
next if ($m->{role} // '') eq 'system';
|
|
||||||
my $ct = $m->{content} // '';
|
|
||||||
$ct = encode_json([map { { function => { name => $_->{function}{name}, arguments => $_->{function}{arguments} } } } @{$m->{tool_calls}}]) if !length($ct) && $m->{tool_calls};
|
|
||||||
next unless length $ct;
|
|
||||||
$ct = substr($ct, 0, 4000) . '...[truncated]' if length($ct) > 4000;
|
|
||||||
push @conv, ($m->{role} // '?') . ": $ct";
|
|
||||||
}
|
|
||||||
return ($msgs, '', 'no conversation to summarize') unless @conv;
|
|
||||||
my $joined = join("\n\n", @conv);
|
|
||||||
$joined = substr($joined, -100000) . "\n...[earlier parts truncated]" if length($joined) > 100000;
|
|
||||||
my $sys = "You are a conversation summarizer for an AI agent's context window. Summarize concisely but completely, preserving all important facts, decisions, code, errors, and the current task state, so the agent can continue the work without the original messages. Output only the summary.";
|
|
||||||
my %cc = (%$cfg, stream => 'false');
|
|
||||||
my $sm = [{ role => 'system', content => $sys }, { role => 'user', content => "Summarize this conversation:\n\n$joined" }];
|
|
||||||
my $m = eval { llm(\%cc, $sm, []) };
|
|
||||||
return ($msgs, '', "LLM error: $@") if $@;
|
|
||||||
my $s = trim($m->{content} // $m->{reasoning_content} // '');
|
|
||||||
return ($msgs, '', 'LLM returned an empty summary') unless length $s;
|
|
||||||
return ([{ role => 'system', content => $msgs->[0]{content} }, { role => 'user', content => "Summary of the previous conversation:\n$s\n\nPlease continue from here." }], $s, undef);
|
|
||||||
}
|
|
||||||
|
|
||||||
sub main {
|
|
||||||
my $sp = prompt(); my $cfg = get_cfg(); $_col = col($cfg);
|
|
||||||
my $msgs = [{ role => 'system', content => $sp }];
|
|
||||||
if (@ARGV && $ARGV[0]) {
|
|
||||||
my $p = $ARGV[0];
|
|
||||||
if (!-f $p) { print c("Error: file '$p' not found.", 31), "\n"; exit 1; }
|
|
||||||
open my $fh, '<:encoding(UTF-8)', $p or die "Cannot open $p: $!";
|
|
||||||
local $/; push @$msgs, { role => 'user', content => trim(<$fh>) };
|
|
||||||
AL($cfg, $msgs, $sp); autosave($msgs); exit 0;
|
|
||||||
}
|
|
||||||
print c("Bantam Agent ready", 1, 32), c(" (Ctrl+J = new line)", 2), "\n";
|
|
||||||
print c("endpoint: $cfg->{endpoint} model: $cfg->{model} temp: " . ($cfg->{temperature} // '0.7'), 2), "\n";
|
|
||||||
my $term = Term::ReadLine->new('bantam');
|
|
||||||
$Term::ReadLine::termcap_nowarn = 1; # silence termcap warning on stub Term::ReadLine
|
|
||||||
$term->ornaments(0) if $term->can('ornaments');
|
|
||||||
my $prompt_str = ref($term) eq 'Term::ReadLine::Gnu' ? cp("> ", 1, 36) : c("> ", 1, 36);
|
|
||||||
while (1) {
|
|
||||||
my $line = -t STDIN ? $term->readline($prompt_str) : do { print c("> ", 1, 36); scalar <STDIN> };
|
|
||||||
last unless defined $line;
|
|
||||||
my $u = trim($line); next unless length $u;
|
|
||||||
if ($u eq '/quit') { last; }
|
|
||||||
elsif ($u eq '/clear') { $msgs = [{ role => 'system', content => $sp }]; autosave($msgs); next; }
|
|
||||||
elsif ($u eq '/save') { my ($sid, $sm) = save_session($msgs); print c("[session saved: $sid]", 32), " ", c($sm, 2), "\n"; next; }
|
|
||||||
elsif ($u eq '/list') {
|
|
||||||
my @ss = list_sessions();
|
|
||||||
if (!@ss) { print c("No sessions saved yet.", 33), "\n"; next; }
|
|
||||||
for my $s (@ss) {
|
|
||||||
my ($sid, $st, $sm, $n) = @$s;
|
|
||||||
my $mk = ($sid eq 'autosave') ? c(" (autosave)", 33) : '';
|
|
||||||
print c($sid, 32), $mk, c(" $st [$n msgs]", 2), "\n ", c($sm, 2), "\n";
|
|
||||||
}
|
|
||||||
next;
|
|
||||||
} elsif ($u =~ /^\/load(?:\s+(.*))?$/) {
|
|
||||||
my $target = trim($1 // '');
|
|
||||||
if (!length $target) { print c("Usage: /load <session-id>", 31), "\n"; next; }
|
|
||||||
my $loaded = eval { load_session($target) };
|
|
||||||
if ($@) { print c("Session not found: " . trim($@), 31), "\n"; next; }
|
|
||||||
$msgs = $loaded; autosave($msgs);
|
|
||||||
print c("[session loaded: $target]", 32), " ", c(summary($msgs), 2), "\n";
|
|
||||||
next;
|
|
||||||
} elsif ($u eq '/compact') {
|
|
||||||
if (@$msgs <= 1) { print c("Nothing to compact yet.", 33), "\n"; next; }
|
|
||||||
print c("[compacting conversation...]", 33), "\n";
|
|
||||||
my ($nm, $sm, $err) = compact_conv($cfg, $msgs);
|
|
||||||
if ($err) { print c("[compact failed: $err]", 31), "\n"; next; }
|
|
||||||
$msgs = $nm; autosave($msgs);
|
|
||||||
print c("[compacted to " . scalar(@$msgs) . " messages]", 32), "\n";
|
|
||||||
print c("--- summary ---", 33), "\n", c($sm, 2), "\n";
|
|
||||||
next;
|
|
||||||
} elsif ($u eq '/help') {
|
|
||||||
print c("Bantam commands:", 1, 36), "\n";
|
|
||||||
my @cmds = (["/quit", "exit"], ["/clear", "reset to system prompt"], ["/save", "save session"], ["/list", "list sessions"], ["/load <id>", "load session"], ["/compact", "compact context"], ["/help", "show help"]);
|
|
||||||
for my $kv (@cmds) { printf "%s%s\n", c(sprintf(" %-12s", $kv->[0]), 1, 32), $kv->[1]; }
|
|
||||||
next;
|
|
||||||
}
|
|
||||||
push @$msgs, { role => 'user', content => $u };
|
|
||||||
AL($cfg, $msgs, $sp); autosave($msgs);
|
|
||||||
}
|
|
||||||
autosave($msgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
main() if !caller();
|
|
||||||
1;
|
|
||||||
@@ -1,307 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# Bantam agent: tiny, powerful, DIY
|
|
||||||
# Created by Luxferre in 2026, released into the public domain
|
|
||||||
|
|
||||||
import sys, os, json, time, subprocess, urllib.request
|
|
||||||
try: import readline
|
|
||||||
except ImportError: readline = None
|
|
||||||
|
|
||||||
HIST = os.path.expanduser("~/.bantam_history")
|
|
||||||
SDIR = os.path.expanduser("~/.bantam/sessions")
|
|
||||||
AUTO = os.path.join(SDIR, "autosave.json")
|
|
||||||
_COL = False
|
|
||||||
|
|
||||||
def c(t, *cs): return t if not _COL or not cs else "\033[" + ";".join(map(str, cs)) + "m" + t + "\033[0m"
|
|
||||||
|
|
||||||
def cp(t, *cs): # colored prompt: hide ANSI escapes from readline's width calc (\001/\002 = RL_PROMPT_START/END_IGNORE)
|
|
||||||
if readline is None or not _COL or not cs: return c(t, *cs)
|
|
||||||
return "\001\033[" + ";".join(map(str, cs)) + "m\002" + t + "\001\033[0m\002"
|
|
||||||
|
|
||||||
def col(cfg):
|
|
||||||
if os.environ.get("NO_COLOR") or os.environ.get("BANTAM_NO_COLOR"): return False
|
|
||||||
m = str(cfg.get("color", "auto")).strip().lower()
|
|
||||||
if m == "always": return True
|
|
||||||
if m == "never": return False
|
|
||||||
try: return sys.stdout.isatty()
|
|
||||||
except Exception: return False
|
|
||||||
|
|
||||||
def get_cfg(path="model.cfg"):
|
|
||||||
d = {"endpoint": "https://opencode.ai/zen/v1", "model": "big-pickle", "temperature": "0.7", "api_key": "-", "timeout": "300", "shell_timeout": "120", "max_al_iterations": "1000", "stream": "true", "color": "auto"}
|
|
||||||
if os.path.exists(path):
|
|
||||||
for ln in open(path, encoding="utf-8"):
|
|
||||||
ln = ln.strip()
|
|
||||||
if ln and not ln.startswith("#") and "=" in ln:
|
|
||||||
k, v = ln.split("=", 1); d[k.strip()] = v.strip()
|
|
||||||
if not d.get("api_key") and "OPENAI_API_KEY" in os.environ: d["api_key"] = os.environ["OPENAI_API_KEY"]
|
|
||||||
return d
|
|
||||||
|
|
||||||
def num(cfg, k, d):
|
|
||||||
try: return type(d)(cfg.get(k, d))
|
|
||||||
except (TypeError, ValueError): return d
|
|
||||||
|
|
||||||
DEFAULT_SYSTEM_PROMPT = """You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:
|
|
||||||
- shell_exec: run a shell command; returns its output and exit code.
|
|
||||||
- run_subagent: delegate a sub-task to a child agent; returns its reply.
|
|
||||||
|
|
||||||
Work 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.
|
|
||||||
|
|
||||||
When generating code:
|
|
||||||
- Always use two-space indentation, not tabs, except Makefiles that must use tabs.
|
|
||||||
- No whitespace between keywords and opening braces in C-like languages.
|
|
||||||
- Write optimally and with as few third-party dependencies as possible.
|
|
||||||
- Always test.
|
|
||||||
- No emojis in code or documentation.
|
|
||||||
- Respect AGENTS.md, GEMINI.md, CLAUDE.md contents in the project."""
|
|
||||||
|
|
||||||
def prompt(path="system.txt"):
|
|
||||||
if os.path.exists(path): return open(path, encoding="utf-8").read().strip()
|
|
||||||
return DEFAULT_SYSTEM_PROMPT
|
|
||||||
|
|
||||||
def T(name, desc, props): return {"type": "function", "function": {"name": name, "description": desc, "parameters": {"type": "object", "properties": props, "required": list(props)}}}
|
|
||||||
TOOLS = [T("shell_exec", "Run a shell command, return output and exit code.", {"command": {"type": "string"}}),
|
|
||||||
T("run_subagent", "Run a child agent with a prompt.", {"prompt": {"type": "string"}})]
|
|
||||||
|
|
||||||
def shell(cmd, t=120):
|
|
||||||
try:
|
|
||||||
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=float(t))
|
|
||||||
return f"{(r.stdout + r.stderr).strip()}\n\nexit: {r.returncode}"
|
|
||||||
except subprocess.TimeoutExpired as e:
|
|
||||||
out, err = e.stdout or "", e.stderr or ""
|
|
||||||
if isinstance(out, bytes): out = out.decode("utf-8", "replace")
|
|
||||||
if isinstance(err, bytes): err = err.decode("utf-8", "replace")
|
|
||||||
return f"{(out + err).strip()}\n\n[shell timeout after {t}s]\nexit: -1"
|
|
||||||
|
|
||||||
fib = [1, 1, 2, 3, 5, 8, 13, 21, 34]
|
|
||||||
|
|
||||||
def llm(cfg, msgs, tools):
|
|
||||||
url = cfg["endpoint"].rstrip("/") + "/chat/completions"
|
|
||||||
h = {"Content-Type": "application/json", "User-Agent": "Mozilla/5.0 (compatible; Bantam/1.0)"}
|
|
||||||
k = cfg.get("api_key", "").strip()
|
|
||||||
if k and k != "-": h["Authorization"] = "Bearer " + k
|
|
||||||
st = cfg.get("stream", "true").lower() in ("true", "1", "yes")
|
|
||||||
p = {"model": cfg["model"], "temperature": float(cfg.get("temperature", 0.7)), "messages": msgs, "stream": st}
|
|
||||||
if tools: p["tools"] = tools
|
|
||||||
pend = c("...requesting...", 1, 2)
|
|
||||||
for i, dly in enumerate(fib + [0]):
|
|
||||||
try:
|
|
||||||
if _COL: sys.stdout.write("\r" + pend); sys.stdout.flush()
|
|
||||||
else: sys.stdout.write(pend + "\n"); sys.stdout.flush()
|
|
||||||
req = urllib.request.Request(url, data=json.dumps(p).encode(), headers=h, method="POST")
|
|
||||||
with urllib.request.urlopen(req, timeout=num(cfg, "timeout", 300)) as r:
|
|
||||||
if not st:
|
|
||||||
msg = json.loads(r.read().decode())["choices"][0]["message"]
|
|
||||||
if _COL: sys.stdout.write("\r\033[K"); sys.stdout.flush()
|
|
||||||
return msg
|
|
||||||
if _COL: sys.stdout.write("\r\033[K"); sys.stdout.flush()
|
|
||||||
content, reas, tcs, rh, ch = "", "", {}, False, False
|
|
||||||
for ln in r:
|
|
||||||
ln = ln.decode("utf-8").strip()
|
|
||||||
if not ln.startswith("data:"): continue
|
|
||||||
if ln[5:].strip() == "[DONE]": break
|
|
||||||
try:
|
|
||||||
dl = json.loads(ln[5:].strip())["choices"][0].get("delta", {})
|
|
||||||
rc = dl.get("reasoning_content") or dl.get("reasoning")
|
|
||||||
if rc:
|
|
||||||
if not rh: sys.stdout.write(c("--- reasoning start ---", 36) + "\n"); rh = True
|
|
||||||
sys.stdout.write(c(rc, 2)); sys.stdout.flush(); reas += rc
|
|
||||||
cc = dl.get("content")
|
|
||||||
if cc:
|
|
||||||
if rh and not ch: sys.stdout.write("\n" + c("--- reasoning end ---", 36) + "\n\n")
|
|
||||||
ch = True; sys.stdout.write(cc); sys.stdout.flush(); content += cc
|
|
||||||
for tc in dl.get("tool_calls", []):
|
|
||||||
ti = tc.get("index", 0)
|
|
||||||
if ti not in tcs: tcs[ti] = {"id": tc.get("id", ""), "type": "function", "function": {"name": "", "arguments": ""}}
|
|
||||||
if tc.get("id"): tcs[ti]["id"] = tc["id"]
|
|
||||||
fn = tc.get("function")
|
|
||||||
if fn:
|
|
||||||
if fn.get("name"): tcs[ti]["function"]["name"] += fn["name"]
|
|
||||||
if fn.get("arguments"): tcs[ti]["function"]["arguments"] += fn["arguments"]
|
|
||||||
except Exception: pass
|
|
||||||
if rh and not ch: sys.stdout.write("\n" + c("--- reasoning end ---", 36) + "\n")
|
|
||||||
elif ch: sys.stdout.write("\n")
|
|
||||||
sys.stdout.flush()
|
|
||||||
m = {"role": "assistant", "content": content or None}
|
|
||||||
if reas: m["reasoning_content"] = reas
|
|
||||||
if tcs: m["tool_calls"] = list(tcs.values())
|
|
||||||
return m
|
|
||||||
except Exception as e:
|
|
||||||
if _COL: sys.stdout.write("\r\033[K"); sys.stdout.flush()
|
|
||||||
if i < len(fib): print(c(f"[network error: {e}, retrying in {dly}s...]", 31)); time.sleep(dly)
|
|
||||||
else: raise
|
|
||||||
|
|
||||||
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)
|
|
||||||
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")
|
|
||||||
if m.get("content"): print(m["content"])
|
|
||||||
tcs = m.get("tool_calls")
|
|
||||||
if not tcs: break
|
|
||||||
for tc in tcs:
|
|
||||||
fn, astr = tc["function"]["name"], tc["function"].get("arguments", "{}")
|
|
||||||
print(c(f"[tool call: {fn}({astr})]", 33))
|
|
||||||
try:
|
|
||||||
a = json.loads(astr) if astr else {}
|
|
||||||
if not isinstance(a, dict): raise ValueError("args must be a JSON object")
|
|
||||||
err = ""
|
|
||||||
except Exception as e: err, a = str(e), {}
|
|
||||||
if err: res, sty = f"[tool error: invalid JSON args for {fn}: {err}. Raw: {astr!r}]", 31
|
|
||||||
elif fn == "shell_exec": res, sty = shell(a.get("command", ""), stime), 2
|
|
||||||
elif fn == "run_subagent":
|
|
||||||
if depth >= MAX_DEPTH:
|
|
||||||
res, sty = f"[subagent depth limit ({MAX_DEPTH}) reached, child not spawned]", 31
|
|
||||||
else:
|
|
||||||
sub = [{"role": "system", "content": sp + "\n\nImportant: this is a child agent"}, {"role": "user", "content": a.get("prompt", "")}]
|
|
||||||
res, sty = last(AL(cfg, sub, sp, depth + 1)), 2
|
|
||||||
else: res, sty = f"Unknown tool: {fn}", 31
|
|
||||||
print(c(f"[tool result: {fn}]", 32) + "\n" + c(res, sty) + "\n")
|
|
||||||
msgs.append({"role": "tool", "tool_call_id": tc["id"], "content": res})
|
|
||||||
else: msgs.append({"role": "assistant", "content": f"[max AL iterations ({mx}) reached]"})
|
|
||||||
return msgs
|
|
||||||
|
|
||||||
def last(msgs):
|
|
||||||
for m in reversed(msgs):
|
|
||||||
if m.get("role") == "assistant" and m.get("content"): return m["content"]
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def sdir(): os.makedirs(SDIR, exist_ok=True); return SDIR
|
|
||||||
|
|
||||||
def summary(msgs):
|
|
||||||
for m in msgs:
|
|
||||||
if m.get("role") == "user" and isinstance(m.get("content"), str) and m["content"].strip():
|
|
||||||
t = " ".join(m["content"].split()); return t[:80] + ("..." if len(t) > 80 else "")
|
|
||||||
return "(empty session)"
|
|
||||||
|
|
||||||
def save(msgs):
|
|
||||||
d, base, i = sdir(), time.strftime("%Y%m%d-%H%M%S"), 1
|
|
||||||
sid, path = base, os.path.join(d, base + ".json")
|
|
||||||
while os.path.exists(path): i += 1; sid = base + "-" + str(i); path = os.path.join(d, sid + ".json")
|
|
||||||
data = {"id": sid, "created": time.strftime("%Y-%m-%d %H:%M:%S"), "summary": summary(msgs), "messages": msgs}
|
|
||||||
open(path, "w", encoding="utf-8").write(json.dumps(data, ensure_ascii=False, indent=2))
|
|
||||||
return sid, data["summary"]
|
|
||||||
|
|
||||||
def sessions():
|
|
||||||
out = []
|
|
||||||
if os.path.isdir(SDIR):
|
|
||||||
for fn in os.listdir(SDIR):
|
|
||||||
if not fn.endswith(".json"): continue
|
|
||||||
try:
|
|
||||||
d = json.load(open(os.path.join(SDIR, fn), encoding="utf-8"))
|
|
||||||
out.append((d.get("id", fn[:-5]), d.get("created", ""), d.get("summary", ""), len(d.get("messages", []))))
|
|
||||||
except Exception: pass
|
|
||||||
return sorted(out, key=lambda x: x[0], reverse=True)
|
|
||||||
|
|
||||||
def load(sid):
|
|
||||||
entries = []
|
|
||||||
if os.path.isdir(SDIR):
|
|
||||||
for fn in os.listdir(SDIR):
|
|
||||||
if not fn.endswith(".json"): continue
|
|
||||||
try: entries.append((json.load(open(os.path.join(SDIR, fn), encoding="utf-8")).get("id", fn[:-5]), os.path.join(SDIR, fn)))
|
|
||||||
except Exception: pass
|
|
||||||
hit = next((e for e in entries if e[0] == sid), None)
|
|
||||||
if not hit:
|
|
||||||
pref = [e for e in entries if e[0].startswith(sid)]
|
|
||||||
if len(pref) == 1: hit = pref[0]
|
|
||||||
elif len(pref) > 1: raise KeyError("ambiguous prefix: " + ", ".join(e[0] for e in pref))
|
|
||||||
if not hit: raise KeyError(sid)
|
|
||||||
return json.load(open(hit[1], encoding="utf-8"))["messages"]
|
|
||||||
|
|
||||||
def autosave(msgs):
|
|
||||||
d = {"id": "autosave", "created": time.strftime("%Y-%m-%d %H:%M:%S"), "summary": summary(msgs), "messages": msgs}
|
|
||||||
open(os.path.join(sdir(), "autosave.json"), "w", encoding="utf-8").write(json.dumps(d, ensure_ascii=False, indent=2))
|
|
||||||
|
|
||||||
def summarize(cfg, msgs):
|
|
||||||
conv = []
|
|
||||||
for m in msgs:
|
|
||||||
if m.get("role") == "system": continue
|
|
||||||
ct = m.get("content")
|
|
||||||
if not ct and m.get("tool_calls"): ct = json.dumps([{"function": tc["function"]["name"], "arguments": tc["function"]["arguments"]} for tc in m["tool_calls"]], ensure_ascii=False)
|
|
||||||
if not ct: continue
|
|
||||||
if len(ct) > 4000: ct = ct[:4000] + "...[truncated]"
|
|
||||||
conv.append(f"{m.get('role', '?')}: {ct}")
|
|
||||||
if not conv: return "", "no conversation to summarize"
|
|
||||||
joined = "\n\n".join(conv)
|
|
||||||
if len(joined) > 100000: joined = joined[-100000:] + "\n...[earlier parts truncated]"
|
|
||||||
sm = [{"role": "system", "content": "You are a conversation summarizer for an AI agent's context window. Summarize concisely but completely, preserving all important facts, decisions, code, errors, and the current task state, so the agent can continue the work without the original messages. Output only the summary."},
|
|
||||||
{"role": "user", "content": "Summarize this conversation:\n\n" + joined}]
|
|
||||||
cc = dict(cfg); cc["stream"] = "false"
|
|
||||||
try: m = llm(cc, sm, [])
|
|
||||||
except Exception as e: return "", f"LLM error: {e}"
|
|
||||||
s = (m.get("content") or m.get("reasoning_content") or "").strip()
|
|
||||||
return (s, None) if s else ("", "LLM returned an empty summary")
|
|
||||||
|
|
||||||
def compact(cfg, msgs):
|
|
||||||
if not msgs or msgs[0].get("role") != "system": return msgs, "", "session has no system message"
|
|
||||||
s, err = summarize(cfg, msgs)
|
|
||||||
if err: return msgs, "", err
|
|
||||||
return [{"role": "system", "content": msgs[0]["content"]}, {"role": "user", "content": "Summary of the previous conversation:\n" + s + "\n\nPlease continue from here."}], s, None
|
|
||||||
|
|
||||||
def main():
|
|
||||||
global _COL
|
|
||||||
if readline:
|
|
||||||
try: readline.read_history_file(HIST)
|
|
||||||
except OSError: pass
|
|
||||||
try: readline.parse_and_bind('"\\C-j": "\\C-v\\C-j"') # real LF via quoted-insert
|
|
||||||
except Exception: pass
|
|
||||||
sp, cfg = prompt(), get_cfg()
|
|
||||||
_COL = col(cfg)
|
|
||||||
msgs = [{"role": "system", "content": sp}]
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1]:
|
|
||||||
p = sys.argv[1]
|
|
||||||
if not os.path.exists(p): print(c(f"Error: file '{p}' not found.", 31)); sys.exit(1)
|
|
||||||
msgs.append({"role": "user", "content": open(p, encoding="utf-8").read().strip()})
|
|
||||||
AL(cfg, msgs, sp); autosave(msgs)
|
|
||||||
if readline:
|
|
||||||
try: readline.write_history_file(HIST)
|
|
||||||
except OSError: pass
|
|
||||||
sys.exit(0)
|
|
||||||
print(c("Bantam Agent ready", 1, 32) + c(" (Ctrl+J = new line)", 2))
|
|
||||||
print(c(f"endpoint: {cfg['endpoint']} model: {cfg['model']} temp: {cfg.get('temperature', '0.7')}", 2))
|
|
||||||
while True:
|
|
||||||
try: u = input(cp("> ", 1, 36)).strip()
|
|
||||||
except (EOFError, KeyboardInterrupt): print(); break
|
|
||||||
if not u: continue
|
|
||||||
if u == "/quit": break
|
|
||||||
elif u == "/clear": msgs = [{"role": "system", "content": sp}]; autosave(msgs); continue
|
|
||||||
elif u == "/save":
|
|
||||||
sid, sm = save(msgs); print(c(f"[session saved: {sid}]", 32) + " " + c(sm, 2)); continue
|
|
||||||
elif u == "/list":
|
|
||||||
ss = sessions()
|
|
||||||
if not ss: print(c("No sessions saved yet.", 33)); continue
|
|
||||||
for sid, st, sm, n in ss:
|
|
||||||
mk = c(" (autosave)", 33) if sid == "autosave" else ""
|
|
||||||
print(c(sid, 32) + mk + c(f" {st} [{n} msgs]", 2) + "\n " + c(sm, 2))
|
|
||||||
continue
|
|
||||||
elif u.startswith("/load"):
|
|
||||||
parts = u.split(None, 1)
|
|
||||||
if len(parts) < 2: print(c("Usage: /load <session-id>", 31)); continue
|
|
||||||
try:
|
|
||||||
msgs = load(parts[1]); autosave(msgs)
|
|
||||||
print(c(f"[session loaded: {parts[1]}]", 32) + " " + c(summary(msgs), 2))
|
|
||||||
except KeyError as e: print(c(f"Session not found: {e}", 31))
|
|
||||||
continue
|
|
||||||
elif u == "/compact":
|
|
||||||
if len(msgs) <= 1: print(c("Nothing to compact yet.", 33)); continue
|
|
||||||
print(c("[compacting conversation...]", 33))
|
|
||||||
nm, sm, err = compact(cfg, msgs)
|
|
||||||
if err: print(c(f"[compact failed: {err}]", 31)); continue
|
|
||||||
msgs = nm; autosave(msgs)
|
|
||||||
print(c(f"[compacted to {len(msgs)} messages]", 32)); print(c("--- summary ---", 33) + "\n" + c(sm, 2))
|
|
||||||
continue
|
|
||||||
elif u == "/help":
|
|
||||||
print(c("Bantam commands:", 1, 36))
|
|
||||||
for k, v in [("/quit", "exit"), ("/clear", "reset to system prompt"), ("/save", "save session"), ("/list", "list sessions"), ("/load <id>", "load session"), ("/compact", "compact context"), ("/help", "show help")]:
|
|
||||||
print(c(f" {k:<12}", 1, 32) + v)
|
|
||||||
continue
|
|
||||||
msgs.append({"role": "user", "content": u})
|
|
||||||
AL(cfg, msgs, sp); autosave(msgs)
|
|
||||||
autosave(msgs)
|
|
||||||
if readline:
|
|
||||||
try: readline.write_history_file(HIST)
|
|
||||||
except OSError: pass
|
|
||||||
|
|
||||||
if __name__ == "__main__": main()
|
|
||||||
Executable
+199
@@ -0,0 +1,199 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# context7 - query library/framework documentation via the Context7 public
|
||||||
|
# MCP server (https://mcp.context7.com/mcp).
|
||||||
|
#
|
||||||
|
# Context7 keeps up-to-date docs and code examples for thousands of libraries
|
||||||
|
# and frameworks and exposes them through an MCP (Model Context Protocol)
|
||||||
|
# server. This script speaks the JSON-RPC MCP protocol over HTTP:
|
||||||
|
# 1. initialize -> obtains an Mcp-Session-Id (optional)
|
||||||
|
# 2. notifications/initialized -> no reply
|
||||||
|
# 3. tools/call -> runs a Context7 tool and prints text
|
||||||
|
#
|
||||||
|
# Context7 provides two tools:
|
||||||
|
# * resolve-library-id maps a library name to a Context7 ID (/org/project).
|
||||||
|
# * query-docs fetches docs + examples for a resolved library ID.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# context7 resolve <libraryName> [query]
|
||||||
|
# Search for a library and print the candidate Context7 IDs with their
|
||||||
|
# description, snippet count, reputation and benchmark score.
|
||||||
|
#
|
||||||
|
# context7 query <libraryId> <query>
|
||||||
|
# Fetch documentation for an explicit library ID (format /org/project
|
||||||
|
# or /org/project/version). The ID usually comes from `resolve`.
|
||||||
|
#
|
||||||
|
# context7 docs <libraryName> <query>
|
||||||
|
# Convenience: resolve the library, auto-pick the top-ranked match and
|
||||||
|
# immediately query its documentation.
|
||||||
|
#
|
||||||
|
# context7 --help
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# CONTEXT7_MCP_ENDPOINT optional; default https://mcp.context7.com/mcp
|
||||||
|
# CONTEXT7_API_KEY optional; if set, sent as the X-Context7-API-Key
|
||||||
|
# request header (for higher rate limits / private
|
||||||
|
# docs). No extra headers are added when unset.
|
||||||
|
#
|
||||||
|
# Dependencies: curl, jq.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '/^# Usage:/,/^# Dependencies:/p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CMD="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$CMD" in
|
||||||
|
-h|--help|help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
resolve|query|docs)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "context7: unknown command '$CMD' (try --help)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# --- Validate argument counts per command. ---
|
||||||
|
case "$CMD" in
|
||||||
|
resolve)
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
echo "context7: resolve requires <libraryName> [query]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
LIB_NAME="$1"
|
||||||
|
QUERY="${2:-$1}"
|
||||||
|
;;
|
||||||
|
query)
|
||||||
|
if [ "$#" -lt 2 ]; then
|
||||||
|
echo "context7: query requires <libraryId> <query>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
LIB_ID="$1"
|
||||||
|
QUERY="$2"
|
||||||
|
;;
|
||||||
|
docs)
|
||||||
|
if [ "$#" -lt 2 ]; then
|
||||||
|
echo "context7: docs requires <libraryName> <query>" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
LIB_NAME="$1"
|
||||||
|
QUERY="$2"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
ENDPOINT="${CONTEXT7_MCP_ENDPOINT:-https://mcp.context7.com/mcp}"
|
||||||
|
|
||||||
|
# Optional auth header (single token, no spaces expected in an API key).
|
||||||
|
AUTH_ARGS=""
|
||||||
|
if [ -n "${CONTEXT7_API_KEY:-}" ]; then
|
||||||
|
AUTH_ARGS="-H X-Context7-API-Key:${CONTEXT7_API_KEY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- MCP handshake + tool call helper. ---
|
||||||
|
# Args: $1 = tool name, $2 = arguments JSON. Prints the parsed text content.
|
||||||
|
mcp_call() {
|
||||||
|
TOOL="$1"
|
||||||
|
ARGS="$2"
|
||||||
|
|
||||||
|
INIT_HEADERS=$(mktemp)
|
||||||
|
INIT_BODY=$(mktemp)
|
||||||
|
curl -s -D "$INIT_HEADERS" -X POST "$ENDPOINT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
$AUTH_ARGS \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"context7","version":"1.0"}}}' \
|
||||||
|
> "$INIT_BODY"
|
||||||
|
|
||||||
|
SID=$(grep -i '^mcp-session-id:' "$INIT_HEADERS" | tr -d '\r' | awk '{print $2}')
|
||||||
|
|
||||||
|
if [ -n "$SID" ]; then
|
||||||
|
curl -s -X POST "$ENDPOINT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-H "Mcp-Session-Id: $SID" \
|
||||||
|
$AUTH_ARGS \
|
||||||
|
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
|
||||||
|
SID_ARGS="-H Mcp-Session-Id:$SID"
|
||||||
|
else
|
||||||
|
SID_ARGS=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
CALL_BODY=$(mktemp)
|
||||||
|
curl -s -X POST "$ENDPOINT" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
$SID_ARGS \
|
||||||
|
$AUTH_ARGS \
|
||||||
|
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"$TOOL\",\"arguments\":$ARGS}}" \
|
||||||
|
> "$CALL_BODY"
|
||||||
|
|
||||||
|
# Take the last "data:" SSE payload (or the whole body if plain JSON).
|
||||||
|
DATA=$(grep -E '^data:[[:space:]]' "$CALL_BODY" | tail -n1 | sed 's/^data:[[:space:]]*//')
|
||||||
|
if [ -z "$DATA" ]; then
|
||||||
|
DATA=$(cat "$CALL_BODY")
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||||||
|
|
||||||
|
if [ -z "$DATA" ]; then
|
||||||
|
echo "context7: no response from MCP server" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Surface protocol errors.
|
||||||
|
ERROR_MSG=$(printf '%s' "$DATA" | jq -r 'if .error then (.error|tostring) else empty end')
|
||||||
|
if [ -n "$ERROR_MSG" ]; then
|
||||||
|
echo "context7 error: $ERROR_MSG" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$DATA" | jq -r '
|
||||||
|
(.result.content // [])
|
||||||
|
| map(select(.type == "text"))
|
||||||
|
| map(.text)
|
||||||
|
| .[]
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Dispatch. ---
|
||||||
|
case "$CMD" in
|
||||||
|
resolve)
|
||||||
|
ARGS=$(jq -n --arg n "$LIB_NAME" --arg q "$QUERY" \
|
||||||
|
'{libraryName:$n, query:$q}')
|
||||||
|
mcp_call "resolve-library-id" "$ARGS"
|
||||||
|
;;
|
||||||
|
query)
|
||||||
|
ARGS=$(jq -n --arg id "$LIB_ID" --arg q "$QUERY" \
|
||||||
|
'{libraryId:$id, query:$q}')
|
||||||
|
mcp_call "query-docs" "$ARGS"
|
||||||
|
;;
|
||||||
|
docs)
|
||||||
|
RARGS=$(jq -n --arg n "$LIB_NAME" --arg q "$QUERY" \
|
||||||
|
'{libraryName:$n, query:$q}')
|
||||||
|
RESOLVE_OUT=$(mcp_call "resolve-library-id" "$RARGS") || exit 1
|
||||||
|
# Auto-pick the first candidate library ID from the resolve output.
|
||||||
|
TOP_ID=$(printf '%s\n' "$RESOLVE_OUT" \
|
||||||
|
| grep -m1 -oE '/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+' || true)
|
||||||
|
if [ -z "$TOP_ID" ]; then
|
||||||
|
echo "context7: could not extract a library ID from resolve output:" >&2
|
||||||
|
printf '%s\n' "$RESOLVE_OUT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "context7: using library ID $TOP_ID (from resolve of '$LIB_NAME')" >&2
|
||||||
|
QARGS=$(jq -n --arg id "$TOP_ID" --arg q "$QUERY" \
|
||||||
|
'{libraryId:$id, query:$q}')
|
||||||
|
mcp_call "query-docs" "$QARGS"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+190
@@ -0,0 +1,190 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# weather - query current weather and forecasts for any location via wttr.in.
|
||||||
|
#
|
||||||
|
# wttr.in (https://github.com/chubin/wttr.in) is a console-oriented weather
|
||||||
|
# service backed by World Weather Online data. It supports several output
|
||||||
|
# formats: a graphical ANSI view for terminals (the default), one-line text
|
||||||
|
# formats (built-in presets 1-4 or a custom %-notation string), a rich JSON
|
||||||
|
# document (?format=j1 / j2) for scripts and APIs, plus PNG / HTML /
|
||||||
|
# Prometheus metrics.
|
||||||
|
#
|
||||||
|
# This script is a thin, friendly wrapper around the wttr.in HTTP API. It
|
||||||
|
# builds the request URL from the supplied location and options, fetches the
|
||||||
|
# data with curl and (when JSON is requested) pretty-prints it with jq.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# weather [options] [location]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# -l, --location LOC Location: city, airport code, domain, IP,
|
||||||
|
# "lat,lon" coordinates, or "~Name" for a custom label.
|
||||||
|
# Default: auto-detect from the request IP.
|
||||||
|
# -u, --units U Unit system: m (metric, default), u (USCS/imperial),
|
||||||
|
# M (metric with wind speed in m/s).
|
||||||
|
# -L, --lang LANG Output language code, e.g. de, fr, ru, zh-cn.
|
||||||
|
# -f, --format FMT Output format:
|
||||||
|
# j1 -> rich JSON document (default for --json)
|
||||||
|
# j2 -> JSON document, imperial units
|
||||||
|
# 1-4 -> built-in one-line presets
|
||||||
|
# "%..." -> custom one-line %-notation format
|
||||||
|
# Omit to get the default graphical terminal view.
|
||||||
|
# -j, --json Alias for --format j1 (emit JSON).
|
||||||
|
# -0 Current weather only.
|
||||||
|
# -1 Current weather + today's forecast.
|
||||||
|
# -2 Current weather + today's + tomorrow's forecast.
|
||||||
|
# -q, --quiet Quiet: no "Weather report" header / city name.
|
||||||
|
# -A Force ANSI output (ignore User-Agent detection).
|
||||||
|
# -h, --help Show this help text.
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# WTTRAPI Base endpoint. Default: https://wttr.in
|
||||||
|
# WEATHER_TIMEOUT curl connect/read timeout in seconds. Default: 20
|
||||||
|
#
|
||||||
|
# Dependencies: curl, jq (jq only needed for the JSON format).
|
||||||
|
#
|
||||||
|
# Examples:
|
||||||
|
# weather
|
||||||
|
# weather London
|
||||||
|
# weather -u u -L de Berlin
|
||||||
|
# weather -f 3 "New York"
|
||||||
|
# weather -f "%l: %c %t (feels %f), wind %w %m" Paris
|
||||||
|
# weather -j Tokyo
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
WTTRAPI="${WTTRAPI:-https://wttr.in}"
|
||||||
|
TIMEOUT="${WEATHER_TIMEOUT:-20}"
|
||||||
|
|
||||||
|
# Single-letter options (view/quiet/units) concatenated into one run.
|
||||||
|
SHORT=""
|
||||||
|
LANG_CODE=""
|
||||||
|
FORMAT=""
|
||||||
|
LOCATION=""
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
sed -n '/^# Usage:/,/^# Examples:/p' "$0" | sed 's/^# \{0,1\}//'
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Parse arguments. ---
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-l|--location)
|
||||||
|
[ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; }
|
||||||
|
LOCATION="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-u|--units)
|
||||||
|
[ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; }
|
||||||
|
case "$2" in
|
||||||
|
m|u|M) SHORT="${SHORT}$2" ;;
|
||||||
|
*) echo "weather: unknown unit '$2' (use m, u or M)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-L|--lang)
|
||||||
|
[ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; }
|
||||||
|
LANG_CODE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-f|--format)
|
||||||
|
[ "$#" -ge 2 ] || { echo "weather: $1 requires an argument" >&2; exit 1; }
|
||||||
|
FORMAT="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-j|--json)
|
||||||
|
FORMAT="j1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-0|-1|-2|-q|-A)
|
||||||
|
SHORT="${SHORT}${1#-}"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
LOCATION="${LOCATION:+$LOCATION }$(printf '%s' "$*" | sed 's/^ //')"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo "weather: unknown option '$1' (try --help)" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Positional: append to location (preserves multi-word locations).
|
||||||
|
LOCATION="${LOCATION:+$LOCATION }$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Assemble the query string. ---
|
||||||
|
# wttr.in accepts a leading run of single-letter options, then '&'-joined
|
||||||
|
# long options (key=value). We build both parts and join them.
|
||||||
|
QUERY="$SHORT"
|
||||||
|
|
||||||
|
# Long options: format and lang.
|
||||||
|
LONG=""
|
||||||
|
if [ -n "$FORMAT" ]; then
|
||||||
|
case "$FORMAT" in
|
||||||
|
j1|j2)
|
||||||
|
LONG="${LONG:+$LONG&}format=${FORMAT}"
|
||||||
|
;;
|
||||||
|
[1-4])
|
||||||
|
LONG="${LONG:+$LONG&}format=${FORMAT}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# Custom %-notation one-line format -> URL-encode it.
|
||||||
|
ENC=$(printf '%s' "$FORMAT" | jq -sRr @uri)
|
||||||
|
LONG="${LONG:+$LONG&}format=${ENC}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
if [ -n "$LANG_CODE" ]; then
|
||||||
|
LONG="${LONG:+$LONG&}lang=$(printf '%s' "$LANG_CODE" | jq -sRr @uri)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Combine short + long parts into a single query string.
|
||||||
|
if [ -n "$QUERY" ] && [ -n "$LONG" ]; then
|
||||||
|
QUERY="${QUERY}&${LONG}"
|
||||||
|
elif [ -z "$QUERY" ] && [ -n "$LONG" ]; then
|
||||||
|
QUERY="$LONG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# URL-encode the location (spaces -> %20, etc.). Empty means auto-detect.
|
||||||
|
if [ -n "$LOCATION" ]; then
|
||||||
|
LOC_ENC=$(printf '%s' "$LOCATION" | jq -sRr @uri)
|
||||||
|
else
|
||||||
|
LOC_ENC=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
URL="${WTTRAPI}/${LOC_ENC}"
|
||||||
|
if [ -n "$QUERY" ]; then
|
||||||
|
URL="${URL}?${QUERY}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Fetch. ---
|
||||||
|
# wttr.in returns the graphical ANSI art whenever the request looks like it
|
||||||
|
# comes from a console client (curl's default User-Agent), and HTML for
|
||||||
|
# browsers. So we keep curl's default identity. The "-A" flag above is a
|
||||||
|
# wttr.in URL option ("force ANSI") and is already part of the query string.
|
||||||
|
|
||||||
|
RESP=$(curl -s -L --max-time "$TIMEOUT" "$URL") || {
|
||||||
|
echo "weather: failed to reach $WTTRAPI" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Emit. ---
|
||||||
|
if [ "$FORMAT" = "j1" ] || [ "$FORMAT" = "j2" ]; then
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
printf '%s\n' "$RESP" | jq . 2>/dev/null || printf '%s\n' "$RESP"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$RESP"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
printf '%s\n' "$RESP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# websearch - simple web search for Bantam via Exa's MCP server (Streamable HTTP).
|
||||||
|
#
|
||||||
|
# Talks to https://mcp.exa.ai/mcp using the JSON-RPC MCP protocol:
|
||||||
|
# 1. initialize -> obtains an Mcp-Session-Id from response headers
|
||||||
|
# 2. notifications/initialized -> no reply
|
||||||
|
# 3. tools/call (web_search_exa) -> prints the formatted result text
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# websearch "your query"
|
||||||
|
# websearch "your query" [num_results]
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# EXA_MCP_ENDPOINT optional; default https://mcp.exa.ai/mcp
|
||||||
|
# EXA_API_KEY optional; if set, sent as an URL query parameter (?api_key=)
|
||||||
|
# so that NO extra HTTP headers are added beyond Content-Type.
|
||||||
|
#
|
||||||
|
# Dependencies: curl, jq.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ "$#" -lt 1 ]; then
|
||||||
|
echo "Usage: websearch \"query\" [num_results]" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
QUERY="$1"
|
||||||
|
NUM="${2:-5}"
|
||||||
|
|
||||||
|
ENDPOINT="${EXA_MCP_ENDPOINT:-https://mcp.exa.ai/mcp}"
|
||||||
|
|
||||||
|
# Append API key as a query parameter (no extra headers).
|
||||||
|
if [ -n "${EXA_API_KEY:-}" ]; then
|
||||||
|
URL="${ENDPOINT}?api_key=${EXA_API_KEY}"
|
||||||
|
else
|
||||||
|
URL="${ENDPOINT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build the tools/call arguments JSON safely (proper query escaping).
|
||||||
|
ARGS=$(jq -n --arg q "$QUERY" --argjson n "$NUM" \
|
||||||
|
'{query:$q, numResults:$n}')
|
||||||
|
|
||||||
|
# --- Step 1: initialize, capture headers (for Mcp-Session-Id) and body. ---
|
||||||
|
INIT_HEADERS=$(mktemp)
|
||||||
|
INIT_BODY=$(mktemp)
|
||||||
|
curl -s -D "$INIT_HEADERS" -X POST "$URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"websearch","version":"1.0"}}}' \
|
||||||
|
> "$INIT_BODY"
|
||||||
|
|
||||||
|
SID=$(grep -i '^mcp-session-id:' "$INIT_HEADERS" | tr -d '\r' | awk '{print $2}')
|
||||||
|
if [ -z "$SID" ]; then
|
||||||
|
echo "websearch: failed to obtain MCP session id" >&2
|
||||||
|
cat "$INIT_BODY" >&2
|
||||||
|
rm -f "$INIT_HEADERS" "$INIT_BODY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Step 2: send initialized notification (fire and forget). ---
|
||||||
|
curl -s -X POST "$URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-H "Mcp-Session-Id: $SID" \
|
||||||
|
-d '{"jsonrpc":"2.0","method":"notifications/initialized"}' > /dev/null
|
||||||
|
|
||||||
|
# --- Step 3: call web_search_exa and extract the text result. ---
|
||||||
|
CALL_BODY=$(mktemp)
|
||||||
|
curl -s -X POST "$URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-H "Mcp-Session-Id: $SID" \
|
||||||
|
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"web_search_exa\",\"arguments\":$ARGS}}" \
|
||||||
|
> "$CALL_BODY"
|
||||||
|
|
||||||
|
# Parse the response: take the last "data:" SSE payload (or the whole body
|
||||||
|
# if it is plain JSON) and print the text blocks from the result.
|
||||||
|
DATA=$(grep -E '^data:[[:space:]]' "$CALL_BODY" | tail -n1 | sed 's/^data:[[:space:]]*//')
|
||||||
|
if [ -z "$DATA" ]; then
|
||||||
|
DATA=$(cat "$CALL_BODY")
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$DATA" ]; then
|
||||||
|
echo "websearch: no response from MCP server" >&2
|
||||||
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Surface server/protocol errors, then emit text content.
|
||||||
|
ERROR_MSG=$(printf '%s' "$DATA" | jq -r 'if .error then (.error|tostring) else empty end')
|
||||||
|
if [ -n "$ERROR_MSG" ]; then
|
||||||
|
echo "websearch error: $ERROR_MSG" >&2
|
||||||
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s' "$DATA" | jq -r '
|
||||||
|
(.result.content // [])
|
||||||
|
| map(select(.type == "text"))
|
||||||
|
| map(.text)
|
||||||
|
| .[]
|
||||||
|
'
|
||||||
|
|
||||||
|
rm -f "$INIT_HEADERS" "$INIT_BODY" "$CALL_BODY"
|
||||||
|
|
||||||
|
exit 0
|
||||||
+2120
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
# MicroBantam (mb): the Bantam agent in <100 SLOC - readable, core modules only
|
||||||
|
# Created by Luxferre in 2026, released into the public domain
|
||||||
|
use strict; use warnings; use HTTP::Tiny; use JSON::PP; use POSIX qw(strftime); use File::Path qw(make_path);
|
||||||
|
$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
|
||||||
|
my $DEF_SP = "You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:\n- shell_exec: run a shell command; returns its output and exit code.\n- run_subagent: delegate a sub-task to a child agent; returns its reply.\n\nWork fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Delegate large or independent sub-tasks to run_subagent. Stop as soon as the goal is met and report concisely: results, not process.\n\nWhen generating code:\n- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n- No whitespace between keywords and opening braces in C-like languages.\n- Write optimally and with as few third-party dependencies as possible.\n- Always test.\n- No emojis in code or documentation.\n- Respect AGENTS.md, GEMINI.md, CLAUDE.md contents in the project.";
|
||||||
|
my $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);
|
||||||
|
if (open my $f, '<:encoding(UTF-8)', 'model.cfg') { while (<$f>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
|
||||||
|
$d{api_key} = $ENV{OPENAI_API_KEY} if $d{api_key} eq '-' && $ENV{OPENAI_API_KEY}; \%d }
|
||||||
|
sub sp { my $p = ''; if (open my $f, '<:encoding(UTF-8)', 'system.txt') { local $/; $p = <$f>; } $p =~ s/^\s+|\s+$//g; length($p) ? $p : $DEF_SP }
|
||||||
|
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) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>[keys %$p]}}} }
|
||||||
|
sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') {
|
||||||
|
for my $tc (@{$m->{tool_calls} // []}) { $tc->{function}{arguments} = filter_text($tc->{function}{arguments});
|
||||||
|
my $a = eval { decode_json($tc->{function}{arguments} // '{}') };
|
||||||
|
$tc->{function}{arguments} = encode_json({invalid_raw => $tc->{function}{arguments} // ''}) if ref $a ne 'HASH'; }
|
||||||
|
$m->{content} = filter_text($m->{content}) if ($m->{role} // '') eq 'tool' && defined $m->{content};
|
||||||
|
} } }
|
||||||
|
sub llm { my ($c, $msgs) = @_; sanitize_msgs($msgs);
|
||||||
|
my $ep = $c->{endpoint}; $ep =~ s{/+$}{};
|
||||||
|
my $h = {'Content-Type'=>'application/json', 'User-Agent'=>'Mozilla/5.0 (compatible; MicroBantam/1.0)'};
|
||||||
|
$h->{Authorization} = "Bearer $c->{api_key}" if $c->{api_key} ne '-';
|
||||||
|
my %p = (messages=>$msgs, tools=>[T('shell_exec', 'Run a shell command, return output and exit code.', {command=>{type=>'string'}}), T('run_subagent', 'Run a child agent with a prompt.', {prompt=>{type=>'string'}})], model=>$c->{model}, temperature=>0+$c->{temperature});
|
||||||
|
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 $r = HTTP::Tiny->new(timeout=>0+$c->{timeout})->post("$ep/chat/completions", {headers=>$h, content=>encode_json(\%p)});
|
||||||
|
print "\r\e[K" if $tty;
|
||||||
|
my $d = $r->{success} ? eval { decode_json($r->{content}) } : undef;
|
||||||
|
return $d->{choices}[0]{message} if $d && $d->{choices} && @{$d->{choices}};
|
||||||
|
my $rb = substr($r->{content} // '', 0, 500);
|
||||||
|
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], '');
|
||||||
|
eval { local $SIG{ALRM} = sub { alarm 0; die "timeout\n" }; alarm $t; $out = `$cmd 2>&1`; alarm 0; };
|
||||||
|
$out =~ s/\s+$//; utf8::decode($out); $out = filter_text($out);
|
||||||
|
$@ ? "$out\n[timeout after ${t}s]\nexit: -1" : "$out\nexit: " . ($? >> 8); }
|
||||||
|
sub last_assistant { for my $m (reverse @{$_[0]}) { return $m->{content} if ($m->{role} // '') eq 'assistant' && defined $m->{content} && length $m->{content}; } '' }
|
||||||
|
sub AL { my ($c, $msgs, $sp, $depth) = ($_[0], $_[1], $_[2], $_[3] || 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};
|
||||||
|
my $tcs = $m->{tool_calls}; last unless $tcs && @$tcs;
|
||||||
|
for my $tc (@$tcs) {
|
||||||
|
my ($fn, $res) = ($tc->{function}{name});
|
||||||
|
$tc->{function}{arguments} = filter_text($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}"; }
|
||||||
|
elsif ($fn eq 'shell_exec') { $res = shell_exec($a->{command} // '', $c->{shell_timeout}); }
|
||||||
|
elsif ($fn eq 'run_subagent') { $res = $depth >= 5 ? '[subagent depth limit (5) reached, child not spawned]' : filter_text(last_assistant(AL($c, [{role=>'system', content=>"$sp\n\nImportant: this is a child agent"}, {role=>'user', content=>filter_text($a->{prompt} // '')}], $sp, $depth + 1))); }
|
||||||
|
else { $res = "unknown tool: $fn"; }
|
||||||
|
$res = filter_text($res);
|
||||||
|
print "[tool] $fn: $res\n";
|
||||||
|
push @$msgs, {role=>'tool', tool_call_id=>$tc->{id}, content=>$res};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$msgs; }
|
||||||
|
sub sessions { my @s; for my $f (glob "$SDIR/*.json") { open my $fh, '<', $f or next; local $/; my $d = eval { decode_json(<$fh>) }; push @s, $d if $d; } sort { $b->{id} cmp $a->{id} } @s }
|
||||||
|
sub sdir { make_path($SDIR) unless -d $SDIR; $SDIR }
|
||||||
|
sub save { sdir(); my ($id, $i) = (strftime('%Y%m%d-%H%M%S', localtime), 0); $id .= '-' . ++$i while -f "$SDIR/$id.json";
|
||||||
|
open my $f, '>', "$SDIR/$id.json" or die "cannot save: $!"; print $f JSON::PP->new->utf8->pretty->encode({id=>$id, messages=>$_[0]}); close $f; $id }
|
||||||
|
sub load { my ($hit) = grep { $_->{id} eq $_[0] } sessions(); die "no session: $_[0]\n" unless $hit; $hit->{messages} }
|
||||||
|
sub autosave { sdir(); open my $f, '>', "$SDIR/autosave.json" or return; print $f JSON::PP->new->utf8->pretty->encode({id=>'autosave', messages=>$_[0]}); }
|
||||||
|
sub list_sessions { map { [$_->{id}, scalar @{$_->{messages} // []}] } sessions() }
|
||||||
|
sub set_cfg { my ($k, $v, @ls, $f) = @_;
|
||||||
|
if (open my $fh, '<:encoding(UTF-8)', 'model.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)', 'model.cfg') { print $fh @ls; close $fh; } }
|
||||||
|
sub main {
|
||||||
|
my ($c, $sp) = (cfg(), sp()); 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, $sp); autosave($msgs); return; }
|
||||||
|
print "MicroBantam ready ($c->{model}). Commands: /quit /clear /save /list /load <id> /cfg <k> [v] /help\n";
|
||||||
|
while (1) {
|
||||||
|
print "> "; my $u = <STDIN>; last unless defined $u; $u =~ s/^\s+|\s+$//g; next unless length $u;
|
||||||
|
if ($u eq '/quit') { last; }
|
||||||
|
elsif ($u eq '/clear') { $msgs = [{role=>'system', content=>$sp}]; autosave($msgs); }
|
||||||
|
elsif ($u eq '/save') { print "session saved: ", save($msgs), "\n"; }
|
||||||
|
elsif ($u eq '/list') { print "$_->[0] [$_->[1] msgs]\n" for list_sessions(); }
|
||||||
|
elsif ($u =~ /^\/load(?:\s+(\S+))?$/) { if (defined $1) { $msgs = eval { load($1) }; $@ ? print($@) : (autosave($msgs), print "loaded: $1\n"); } else { print "usage: /load <session id>\n"; } }
|
||||||
|
elsif ($u =~ /^\/cfg(?:\s+(\S+)(?:\s+(.+))?)?$/) { if (defined $2) { set_cfg($1, $2); $c = cfg(); print "config: $1=$2\n"; } elsif (defined $1) { print exists $c->{$1} ? "$1=$c->{$1}\n" : "$1 not set\n"; } else { print "usage: /cfg <param> [val]\n"; } }
|
||||||
|
elsif ($u eq '/help') { print "Commands: /quit /clear /save /list /load <id> /cfg <k> [v] /help\n"; }
|
||||||
|
else { push @$msgs, {role=>'user', content=>$u}; AL($c, $msgs, $sp); autosave($msgs); }
|
||||||
|
}
|
||||||
|
autosave($msgs);
|
||||||
|
}
|
||||||
|
main() unless caller();
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
endpoint=https://opencode.ai/zen/v1
|
endpoint=https://opencode.ai/zen/v1
|
||||||
model=deepseek-v4-flash-free
|
model=hy3-free
|
||||||
temperature=0.7
|
temperature=0.7
|
||||||
api_key=-
|
api_key=-
|
||||||
stream=true
|
stream=true
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools
|
|||||||
- shell_exec: run a shell command; returns its output and exit code.
|
- shell_exec: run a shell command; returns its output and exit code.
|
||||||
- run_subagent: delegate a sub-task to a child agent; returns its reply.
|
- run_subagent: delegate a sub-task to a child agent; returns its reply.
|
||||||
|
|
||||||
Work 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.
|
Work 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.
|
||||||
|
|
||||||
When generating code:
|
When generating code:
|
||||||
- Always use two-space indentation, not tabs, except Makefiles that must use tabs.
|
- Always use two-space indentation, not tabs, except Makefiles that must use tabs.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build darwin
|
//go:build darwin || dragonfly || freebsd || netbsd || openbsd
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
+21
-3
@@ -1,10 +1,28 @@
|
|||||||
//go:build !linux && !darwin && !windows
|
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func termWidth() int { return 80 }
|
func termWidth() int {
|
||||||
|
if w := os.Getenv("COLUMNS"); w != "" {
|
||||||
|
if n, err := strconv.Atoi(strings.TrimSpace(w)); err == nil && n > 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out, err := exec.Command("tput", "cols").Output(); err == nil {
|
||||||
|
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n > 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
|
||||||
func isTerminal(fd int) bool { return false }
|
func isTerminal(fd int) bool { return false }
|
||||||
|
|
||||||
|
|||||||
+20
-2
@@ -2,9 +2,27 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "errors"
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func termWidth() int { return 80 }
|
func termWidth() int {
|
||||||
|
if w := os.Getenv("COLUMNS"); w != "" {
|
||||||
|
if n, err := strconv.Atoi(strings.TrimSpace(w)); err == nil && n > 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out, err := exec.Command("tput", "cols").Output(); err == nil {
|
||||||
|
if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n > 0 {
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 80
|
||||||
|
}
|
||||||
|
|
||||||
func isTerminal(fd int) bool { return false }
|
func isTerminal(fd int) bool { return false }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user