199 lines
9.3 KiB
Markdown
199 lines
9.3 KiB
Markdown
retoor <retoor@molodetz.nl>
|
|
|
|
# tai
|
|
|
|
tai is a single-file autonomous AI agent written in Python. The entire
|
|
implementation lives in `tai.py` (about 2000 lines) and uses only the Python
|
|
standard library: no dependencies, no install step, no build system.
|
|
|
|
The agent runs as an interactive REPL or as a one-shot command. It reasons
|
|
through an OpenAI-compatible backend, acts through fourteen tools, keeps
|
|
per-profile memory, seals its stored state at rest, and can isolate shell and
|
|
file operations inside a container sandbox.
|
|
|
|
## Requirements
|
|
|
|
- Python 3.10 or newer, no third-party packages.
|
|
- Optional: `podman` or `docker` for the sandbox and Telegram voice notes.
|
|
- Optional: `tmux` for terminal content capture.
|
|
|
|
## Quick start
|
|
|
|
./tai.py
|
|
./tai.py --profile work
|
|
./tai.py --yes
|
|
./tai.py --version
|
|
./tai.py what is 2+3, use the shell
|
|
|
|
Trailing arguments form a one-shot prompt: the agent answers once and exits
|
|
with code 0. Without arguments, tai starts an interactive session.
|
|
|
|
## REPL commands
|
|
|
|
| Command | Effect |
|
|
|-------------------|---------------------------------------------------|
|
|
| `/profile [name]` | Show the current profile or switch to it |
|
|
| `/profiles` | List all profiles, current marked with `*` |
|
|
| `/env [target]` | Show or switch execution environment |
|
|
| `/skills` | List loaded skill files |
|
|
| `/fork <task>` | Spawn a background subagent, REPL stays free |
|
|
| `/agents` | List background subagents |
|
|
| `/agent <id>` | Show one subagent result |
|
|
| `/agent clear` | Purge finished subagents |
|
|
| `/compact` | Compress history into a summary |
|
|
| `/clear` | Drop history, keep the system message |
|
|
| `/help` | Show the command overview |
|
|
| `/quit` | Exit |
|
|
|
|
Any other input is sent to the agent.
|
|
|
|
## Installation
|
|
|
|
./tai.py --install
|
|
|
|
This copies `tai.py` to `~/.local/bin/tai.py` as an executable and registers
|
|
a guarded `command_not_found_handle` block in `~/.bashrc` (backed up once to
|
|
`~/.bashrc.bak-tai`, idempotent), so unknown shell commands are answered by
|
|
the agent instead of failing.
|
|
|
|
## Backends
|
|
|
|
The primary backend is `model.cloud.pravda.education`, an OpenAI-compatible
|
|
gateway that needs no API key and selects a free model per request. If a
|
|
request fails, tai retries it on `devplace.net/openai/v1`, which requires
|
|
`DEVPLACE_API_KEY`. Both endpoints speak `/chat/completions`, including
|
|
native tool calls and streaming.
|
|
|
|
## Orchestration
|
|
|
|
`/fork <task>` spawns a background subagent with its own context while the
|
|
REPL stays free (the prompt shows a `+N` counter). `/agents` lists workers,
|
|
`/agent <id>` shows a result, `/agent clear` purges finished ones.
|
|
|
|
The model itself orchestrates through the `fork` tool (task, timeout up to
|
|
one hour, profile) and the `poll` tool (id, wait up to two minutes). Workers
|
|
get 12 steps, a cooperative deadline, no session writes, and no interactive
|
|
approval prompts. Nesting is capped at two levels. Timeouts and errors
|
|
surface as statuses, never silently.
|
|
|
|
## Tools
|
|
|
|
| Tool | Purpose |
|
|
|------------------------------|------------------------------------------------------|
|
|
| `shell` | Run a shell command, output truncated |
|
|
| `read_file` | Read a text file, large files truncated |
|
|
| `write_file` | Write content to a file, creating parent directories |
|
|
| `edit_file` | Replace one unique exact text match in a file |
|
|
| `web_search` | Search the web, optionally images or page content |
|
|
| `web_fetch` | Fetch a URL and return its text content |
|
|
| `speak` | Synthesize speech, save MP3, play when possible |
|
|
| `listen` | Record from the microphone and transcribe it |
|
|
| `remember` | Merge knowledge into the profile system message |
|
|
| `recall` | Search past session memory by keyword |
|
|
| `load_skill` | Load a skill file by name |
|
|
| `get_current_terminal_content` | Capture the current tmux pane with scrollback |
|
|
| `fork` | Spawn a background subagent |
|
|
| `poll` | Collect a background subagent result |
|
|
|
|
Web search runs on `rsearch.app.molodetz.nl`. Destructive shell commands ask
|
|
for confirmation unless `--yes` is given; read-only commands run directly.
|
|
|
|
## Skills
|
|
|
|
Standard agent skill files (`SKILL.md` with `name` plus `description`
|
|
frontmatter, optional `scripts/`, `references/`, `assets/`, per the Agent
|
|
Skills open format) are discovered in `~/.tai/skills/*/` and
|
|
`./.tai/skills/*/` (project wins on name collisions). Descriptions stay in
|
|
context; the agent loads full instructions through `load_skill` only when
|
|
needed. `/skills` lists what is available.
|
|
|
|
## Sandbox
|
|
|
|
`/env` shows the execution environment, `/env sandbox` switches shell and
|
|
file tools into an isolated `tai-box` container (podman or docker, no mounts,
|
|
no shared filesystem), `/env home` switches back. The image is built
|
|
automatically on first use from an embedded Containerfile; every pip
|
|
requirement (`faster-whisper`, `edge-tts`) lives inside the image while
|
|
`tai.py` itself stays dependency-free. Sandbox commands need no approval
|
|
because the container is disposable.
|
|
|
|
## Telegram
|
|
|
|
./tai.py --install-telegram
|
|
./tai.py --uninstall-telegram
|
|
|
|
Install asks for the bot token up front, verifies it against `getMe`, stores
|
|
it in `~/.tai/telegram.env` (0600), builds the sandbox container (used for
|
|
voice transcription), and registers a `tai-telegram.service` systemd user
|
|
unit with linger enabled (failures ignored). The bot long-polls, answers
|
|
text, transcribes voice notes, and understands `/new`. It runs without
|
|
`--yes`, so destructive shell commands are denied. Uninstall stops and
|
|
removes the service and purges the container and image; the token file and
|
|
data stay.
|
|
|
|
## Profiles and memory
|
|
|
|
Each profile owns a system message plus session history under
|
|
`~/.tai/profiles`, stored with mode 0600.
|
|
|
|
/profile show current profile
|
|
/profile [name] switch profile, creating it when missing
|
|
/profiles list all profiles
|
|
|
|
The `remember` tool merges an instruction into the current profile system
|
|
message through the model itself: it adds facts, updates behavior, or removes
|
|
forgotten items while preserving the rest. It fires by default on new
|
|
passwords and behavior changes. `recall` searches the per-profile episodic
|
|
log in `~/.tai/memory.db` (SQLite). Context is budgeted at roughly 32k
|
|
tokens with automatic compaction at 80 percent.
|
|
|
|
## Sealed storage
|
|
|
|
Storage is sealed by default with a built-in key, which stops casual reads
|
|
but not a determined attacker, since the key ships in the source. Set
|
|
`TAI_PASSPHRASE` for real protection: a home sealed with the default key is
|
|
re-sealed to your passphrase automatically on first boot, with a notice.
|
|
|
|
The key comes from PBKDF2-SHA256 (200k rounds) over a random salt in
|
|
`~/.tai/.seal`; values use a per-value nonce with HMAC-SHA256 encrypt-then-
|
|
MAC. SQLite access goes through custom `tai_enc`/`tai_dec` functions
|
|
registered with `create_function`, so inserts encrypt inline and recall
|
|
decrypts before matching. Existing plaintext data is sealed automatically on
|
|
first sealed start. A wrong passphrase refuses to start with exit code 2.
|
|
Set the variable empty for plaintext storage.
|
|
|
|
This construction uses only the standard library and is honest file-theft
|
|
protection, not audited cryptography; high-value secrets still belong in a
|
|
dedicated manager.
|
|
|
|
## Voice
|
|
|
|
`speak` synthesizes free neural speech via the Microsoft Edge Read Aloud
|
|
protocol, implemented with `socket` and `ssl` from the standard library. No
|
|
key, no package. MP3 files land in `~/.tai/audio` and play when an OS player
|
|
exists. `listen` records and transcribes when a recorder (`arecord`, `sox`,
|
|
`ffmpeg`) and a transcriber (`whisper-cpp`, `whisper`) are installed, and
|
|
reports exactly what is missing otherwise.
|
|
|
|
## Configuration
|
|
|
|
| Variable | Purpose | Default |
|
|
|---------------------|--------------------------------------|--------------------------------------------|
|
|
| `TAI_HOME` | State directory | `~/.tai` |
|
|
| `TAI_MODEL` | Model id, ignored by primary gateway | `openrouter/free` |
|
|
| `DEVPLACE_API_KEY` | Fallback backend credential | Empty, fallback disabled |
|
|
| `TAI_VOICE` | Edge voice name | `en-US-EmmaMultilingualNeural` |
|
|
| `TAI_PASSPHRASE` | Personal seal key | Unset: built-in key; empty: plaintext |
|
|
| `TELEGRAM_BOT_TOKEN`| Bot token, or set during install | Empty |
|
|
|
|
## Testing
|
|
|
|
python3 test_seal.py
|
|
python3 test_tai.py
|
|
|
|
## Layout
|
|
|
|
tai.py the entire agent
|
|
test_seal.py seal regression tests
|
|
test_tai.py skills, telegram, install, and parser tests
|