diff --git a/README.md b/README.md index a8d19d6..a6773b6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ The whole thing is pure stdlib Python with zero `ctypes`, zero hand-packed `stru ./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. +Trailing arguments form a one-shot prompt: the agent answers once, then a verifier subagent checks the original goal against what actually happened and the process exits 0 on verified success, 1 otherwise (strict JSON: a boolean plus one `{item, reason, proposal}` dict per failure, the proposal always moving forward, never repeating an already-tried action). `--continue` resumes an open turn in place instead of starting over. Without arguments, tai starts an interactive session. An unrecognized answer to an approval prompt is re-asked (up to 3 times), never treated as a no; the one exception is the harmless shell case, where an accidental "everything was fine" counts as approval rather than killing the turn. ## REPL commands @@ -210,6 +210,8 @@ A chunk hit from `search` returns **the actual chunk text**, not just FTS's high **Sealed search stays ranked without ever touching plaintext to disk.** Encrypted-at-rest events can't be FTS-indexed in place — SQLite can only full-text-search what it can read as text. Every boot decrypts events into a `:memory:`-only FTS5 mirror (`open_mem_events`), capped at the newest `MEM_EVENTS_CAP` (10,000) rows and synced incrementally as new ones arrive, so sealed vaults get exactly the same BM25 ranking and `[marked]` snippets as plaintext ones — and nothing decrypted ever becomes a temp file, a spill table, or a persistent index on disk. Alternatives were researched and rejected on purpose, not just not-gotten-to: page-level encryption (SQLCipher) is a non-standard C extension, incompatible with the stdlib-only rule; blind indexes (deterministic HMACs of tokens, à la CipherStash) would persist in the vault file and leak term frequency and access patterns to anyone who steals it; academic searchable-encryption schemes leak access patterns too and are far heavier than this threat model needs. Decrypting into RAM, once, where the key and plaintext already live during normal operation anyway, is the only option that keeps the at-rest file fully opaque *and* the search fully ranked. +Retrieval quality is measured, not assumed: `./tai.py --eval-retrieval` scores Hit@K, MRR, and nDCG against goldens mined straight from the vault, so a ranking change proves itself with numbers before it ships. + /search deploy ranked hits across records, events, audit, and chunks /records deploy search records for "deploy" /record mem:9f2c41aa77c3e5d1 @@ -258,9 +260,19 @@ Forking buys parallelism and context isolation, *not* security isolation — wor --- +## CAP: driven by, and driving, other agents + +tai speaks the [CLI Agent Protocol](https://cap-protocol.org/) (draft v1 plus the coding profile) in both directions: it can act as an agent under someone else's driver, and as the orchestrator driving other CLI agents: claude, codex, opencode, aider-class tools, anything that ships a manifest. The full wire contract, every deviation from the spec, and the fleet file schema live in [cap.md](cap.md); the protocol research archive is `research/cap-protocol/`. + +**As an agent.** `./tai.py --cap-manifest` prints tai's own manifest (transports, error codes, permissions, session config) on stdout and exits. `./tai.py --cap-speak` speaks stream-json on stdio: user frames in on stdin, assistant/tool/result frames out on stdout as pure NDJSON (everything human-readable goes to stderr, so the wire is never polluted). Permission mode maps onto tai's existing axes: `none` runs yolo, `confirm` and `interactive` run auto with research done up front, so a driver that cannot answer questions never stalls a turn. Turns cap at 50, the resulting diff caps at 64KB, and each turn writes diff plus transcript artifacts the driver can fetch. + +**As an orchestrator.** `./tai.py --cap resolve` matches a directory against registered CAP manifests, `./tai.py --cap chat` opens an interactive session against one agent, `./tai.py --cap run` executes a multi-agent plan from a TOML fleet file. Four drivers, one `CapDriver` interface: an interactive PTY driver (universal: `write_bytes` plus settle detection on a reimplemented VT screen, submits CR like a real terminal), stream-json over pipes, ACP as JSON-RPC over stdio (serving permission, elicitation, filesystem and terminal requests back to the agent, with a 600s terminal cap), and A2A over HTTP (draft-then-current endpoint probing, both `A2A`/`Agent2Agent` spellings, flavor sticky per session). Sessions get per-agent worktrees, `@cap://` routing between them, a turn budget that pages a human instead of spinning, and a JSONL audit trail of every turn. `--auto-answer` automates the last mile: whenever a driven agent asks a question, the answerer checks policy first (`--allow-tool` / `--deny-tool` / `--max-risk`), then runs an isolated `tai --auto` one-shot in its own vault that deep-researches the question and returns the `ANSWER:` marker, so every subagent question gets a researched answer with no human in the loop. + +**Honest gaps, stated up front.** gRPC is parsed in manifests but not driven (the standard library has no HTTP/2 and no protobuf, so there is no stdlib-only client to write); A2A is driven but not yet served; ACP server argv travels out-of-band in the fleet file because manifests carry none; the reverse-RPC JSON shapes on the speak path are tai-defined, since stream-json specifies no serving direction. TOML manifest and fleet parsing needs Python 3.11+ (`tomllib`); older interpreters get a clean error, not a crash. + ## Execution environment / 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 builds automatically on first use from an embedded Containerfile; every pip requirement the *agent's own features* need (`faster-whisper`, `edge-tts`) lives inside that image, while `tai.py` itself stays dependency-free — the sandbox is where "needs a real package" problems go, never the host process. Sandbox commands need no approval because the container is disposable; this is deliberately a *different axis* from the `--yes`/`--yolo`/`--auto` approval settings, not a replacement for them. +`/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 box is resource-capped at 2GB RAM, 2 CPUs, 256 processes (`BOX_MEMORY`/`BOX_CPUS`/`BOX_PIDS_LIMIT`), because an uncapped sandbox is just a slower way to fork-bomb the host; pre-existing boxes get memory and CPU applied live (`podman update`), the process cap lands on recreation. The image builds automatically on first use from an embedded Containerfile; every pip requirement the *agent's own features* need (`faster-whisper`, `edge-tts`) lives inside that image, while `tai.py` itself stays dependency-free — the sandbox is where "needs a real package" problems go, never the host process. Sandbox commands need no approval because the container is disposable; this is deliberately a *different axis* from the `--yes`/`--yolo`/`--auto` approval settings, not a replacement for them. ## Profiles and bots @@ -302,7 +314,7 @@ Passwords, tokens, and secrets live in a sealed `secrets` table inside the same ## Scheduler -Schedules persist in the same vault with sealed prompts: one-shot appointments (`at` an ISO datetime, naive means local time) or repeating work (`every` 60 seconds or more). A background thread ticks every `SCHEDULER_INTERVAL` (30) seconds in the REPL, the Telegram service, and `--scheduler` mode alike, and **claims due rows atomically** so multiple processes running at once (your REPL plus the systemd service, say) can never double-fire the same prompt. Repeats advance past any missed windows instead of backfilling a queue of stale ones; outcomes land right in the row and stay visible through `/schedules`, `/agents`, and `/agent `. +Schedules persist in the same vault with sealed prompts: one-shot appointments (`at` an ISO datetime, naive means local time) or repeating work (`every` 60 seconds or more). A background thread ticks every `SCHEDULER_INTERVAL` (30) seconds in the REPL, the Telegram service, and `--scheduler` mode alike, and **claims due rows atomically** so multiple processes running at once (your REPL plus the systemd service, say) can never double-fire the same prompt. Each tick fires at most `SCHEDULER_MAX_FIRES` (4) prompts, a schedule never runs twice concurrently (a status lease in the row), late one-shots still fire once, and nothing ever auto-retries: a failed run is an outcome to look at, not a loop to wake up to. Repeats advance past any missed windows instead of backfilling a queue of stale ones; outcomes land right in the row and stay visible through `/schedules`, `/agents`, and `/agent `. /schedule at 2026-10-08T09:00 water the plants /schedule every 1h check the inbox @@ -352,6 +364,7 @@ The interactive UI toolkit lives in `tai.py` itself (stdlib-only, no dependencie | `TAI_VOICE` | Edge voice name | `en-US-EmmaMultilingualNeural` | | `TAI_PASSPHRASE` | Personal seal key | Unset: built-in key; empty: plaintext | | `TAI_SESSION` | Shell session id (context scope) | Auto-derived per shell; fixed word shares | +| `CAP_SESSION_CONFIG` | Bulk session-config JSON for `--cap-speak` | Empty | | `TELEGRAM_BOT_TOKEN` | Bot token, or set during install | Empty | ## Telegram @@ -387,5 +400,8 @@ Unit tests live at the top level (`test_tai.py`, `test_seal.py`); integration te tests/ integration tests, one file per topic (see tests/README.md) tui.md TUI framework reference (see "TUI framework" above) grrtui.md the grr terminal client (`--tui`): screens, keys, caps, gaps + mplex.md the native multiplexer (`--mplex`): options, commands, keys + uiflow.md interactive UI flows (`plexer_show`): contract and caps + cap.md the CAP protocol binding (`--cap-*`, `--cap`): wire contract research/ archived design research, registry at research/INDEX.md AGENTS.md contributor conventions (tests, research rule)