agent-tty
Persistent REPL for AI agents. Shared live terminal for humans.
bash_tool runs a command and forgets. agent-tty keeps a persistent TTY
inside tmux — variables, cwd, imports, connections, SSH sessions, and
debugger state survive across agent turns. The human watches the same
terminal live, can interrupt with k int, or take over with tmux attach.
The package is agent-tty. The CLI command is k, intentionally short to minimise token overhead in agent tool calls. km is the companion event monitor.
Requires POSIX + tmux 3.0+ — k drives tmux, tail, and POSIX signals; it does not bundle or replace them.
Why agent-tty
bash_tool is curl. k is a socket.
Use k when the process must keep memory between commands: Python imports,
database connections, browser/CDP sockets, remote shells, debuggers, running
servers. k watch gives a human the same live filtered view — cell markers,
completion ticks, frame noise hidden. tmux attach is native raw takeover.
Use km when a long cell should call the agent back on completion.
Use k poll only as a simple fallback for scripts or agent runtimes without a
monitor/interrupt path.
Quick Start
k new work bash
k run -j work "echo hello"
# {"cell_id":"...","status":"done","output":"hello"}
k new py python3 -i # Python 3.12 and below
k new py "env PYTHON_BASIC_REPL=1 python3 -i" # Python 3.13+ (disables _pyrepl auto-indent)
k run -j py "print(42)"
Install
Requires: POSIX, Python 3.10+, tmux 3.0+
pip install agent-tty # → k, km, agent-tty in PATH
To refresh a stale k/km entry point, reinstall in the same shell environment
that will run it, then verify the resolved command:
python -m pip install --upgrade --force-reinstall agent-tty
k --version
km --version
agent-tty --version
python -m agent_tty --version
command -v k # use: where.exe k (PowerShell)
Or without pip:
git clone <repo> && cd agent-tty
./scripts/k --help # works immediately (dev shim)
Or symlink into PATH:
ln -sf "$(pwd)/scripts/k" /usr/local/bin/k
ln -sf "$(pwd)/scripts/km" /usr/local/bin/km
Commands
k new <session> [cmd...] [--prompt="x"] spawn (default: bash)
k new <session> <cmd> --prompt=./hook hook mode
k fire [-t N] [session] <code> async fire (default 300s)
k poll [session] [cell_id] poll (O(1))
k run [-j] [-t N] [session] <code> sync (default 30s)
k await ... alias for run
k notify [session] <message> notification (direct to log)
k int [session] ctrl-c (+ re-frame in repeat mode)
k kill <session> kill + cleanup
k ls list sessions
k status [session] health + next action
k watch [session] live filtered view
k history [-n N] [session] last N×5 lines (default 5)
k --version print agent-tty version
aliases: k -V, k version
Session resolves: explicit arg > K_SESSION env > auto-detect (single session).
k status work repairs the log pipe if needed and prints the next useful command:
OK work pipe=ok state=running cell=a1b2c3d4e5f6 next='k poll work a1b2c3d4e5f6 or k int work'
Frame Detection
Three modes via --prompt:
| --prompt= | mode | how |
|---|---|---|
| (not set) | repeat | 5 empty Enters → 5 identical lines → done |
"(gdb)" |
exact | match prompt string |
./hook.py |
hook | stdin lines → hook exit → done |
Hook protocol: k feeds ANSI-stripped lines to stdin. Hook exits = frame end. Hook paths must include a path separator (/). Path is canonicalised to absolute at k new time; hook must exist and be executable.
How It Works
k fire "echo hello"
|
+-- acquires lock (rejected fire = zero side effects)
+-- sends code via paste-buffer (atomic)
bash multiline: writes 0600 temp script, sends "source <script>"
+-- sends 5 frame Enters (repeat mode only)
+-- starts background stream processor
|
stream processor tails log:
ECHOING: skip echo_count lines
OUTPUT: collect lines
DONE: 5 identical lines / prompt match / hook exit
|
writes result file -> exits
|
k poll
+-- checks result file (O(1))
+-- returns JSON
Safety
| invariant | mechanism |
|---|---|
| one cell per session | O_EXCL lock, acquired before send |
| timeout keeps lock | lock marked timed_out; subsequent polls say use k int or k kill |
| completed-cell recovery | bg watcher marks completed; next fire/run can clear a done-lock without losing the result file |
| orphan recovery | bg process group in lock, poll checks os.killpg(pgid, 0) (POSIX) |
| no line-wrap skew | tmux width 10000 |
| atomic send | per-session named paste-buffer k_{session} |
| bash multiline state | private per-cell script + source, so cd/env/functions persist without interleaved prompt echoes |
| ctrl-c safe | kills watcher, writes {"status": "error", "output": "interrupted"}, re-sends frame enters (repeat only) |
| session name validation | [A-Za-z0-9_.-]+, no .., no path traversal |
| idempotent pipe restart | pipe-pane replaced on every fire/run |
| atomic result writes | tmp + fsync +os.replace — poll never reads partial JSON |
| no output classification | "done" = prompt appeared, not success |
JSON Schema (k)
fired: {"cell_id": "...", "status": "fired"}
running: {"cell_id": "...", "status": "running"}
done: {"cell_id": "...", "status": "done", "output": "..."}
timeout: {"cell_id": "...", "status": "timeout", "output": ""}
timeout(2+): {"cell_id": "...", "status": "timeout", "output": "use k int or k kill"}
error: {"status": "error", "output": "..."}
cell error: {"cell_id": "...", "status": "error", "output": "..."}
JSON errors without cell_id: no session 'x'; use k new x bash, active cell 'x', pipe failed: ..., send failed: ..., no active cell on 'x', invalid cell_id.
JSON errors with cell_id: interrupted, unknown cell, watcher died, result missing, lock update failed; use k int or k kill, lock release failed, interrupt failed; use k kill.
Text-only errors: no session found; use k ls or k new <session> bash, no log for 'x'; use k status x, watcher kill failed; use k kill.
Metadata on Disk
$XDG_RUNTIME_DIR/k_cells/<session>/ (or /tmp/k_cells_<uid>/<session>/)
_session.json {name} or {name, prompt}
_lock.json {cell_id, log_offset, echo_count, bg_pgid, completed?, timed_out?, timeout_polled?, terminal_status?}
_output.log pipe-pane stream (append-only)
<cell_id>_result.json stream processor output (deleted after poll)
Known Limitations
agent-tty is POSIX-only: it requires tmux, tail, and POSIX process signals. WSL is fine; native Windows fails fast.
Frame collision (repeat mode): if output contains 5+ consecutive identical non-empty lines, the stream processor falsely detects completion. Extremely rare — 5 identical lines = zero information entropy.
echo_count heuristic: generic REPL mode assumes 1 sent line = 1 echoed line. Bash multiline cells avoid this by sourcing a private per-cell script; other REPLs still rely on prompt filtering or hook/exact prompt mode.
Hook mode: no ... filtering (user takes full control). Hook paths must include a path separator to distinguish them from string prompts.
Python 3.13+ _pyrepl: The new Python REPL auto-indents pasted code, doubling indentation on multi-line blocks. Workaround: k new py "env PYTHON_BASIC_REPL=1 python3 -i". Single-line code is unaffected.
km — event monitor
Callback-style completion for persistent TTY cells. km tails the session log via pipe-pane and emits one JSON line per event to stdout. No polling, no sleep loops.
Each stdout line is a JSON event. Works with any agent host that has background-notification support: Claude Code's Monitor tool reads each line as an interrupt, Codex Desktop can bridge via its notify callback, and plain subprocess readers work the same way.
km <session> [cell_id] [-1]
-1 exits after first completion — one-shot .then() for agent orchestration.
Why km after k
k is the stateful terminal. km is the callback channel for long-running cells.
Background task support alone is not enough when the process state matters;
km lets the persistent TTY keep running and wakes the agent when the cell
finishes. k poll is O(1) and still useful for simple scripts, but poll loops
waste tokens and add latency:
# poll loop: agent burns a tool call every N seconds
# k poll → "running" → k poll → "running" → k poll → "done"
# km: one tool call, block until done
km work -1
# {"cell_id": "...", "session": "work", "status": "done", "ts": "..."}
With km -1, the agent fires a long task, starts km as a background monitor, and gets interrupted exactly once when the task completes. Zero wasted calls.
Continuous mode
Without -1, km runs indefinitely — every fired/done/notify event streams as a JSON line. Useful for multi-cell orchestration where the agent needs to react to each completion in sequence.
Events
fired: {"cell_id": "...", "session": "...", "status": "fired", "ts": "..."}
done: {"cell_id": "...", "session": "...", "status": "done", "ts": "..."}
timeout: {"cell_id": "...", "session": "...", "status": "timeout", "ts": "..."}
interrupted: {"cell_id": "...", "session": "...", "status": "interrupted", "ts": "..."}
notify: {"session": "...", "status": "notify", "from": "...", "message": "...", "ts": "..."}
closed: {"session": "...", "status": "closed", "ts": "..."}
error: {"session": "...", "status": "error", "message": "...", "ts": "..."}
Testing
python tests/test_contracts.py # static code contracts, no tmux
python tests/test_docs.py # docs/package drift, no tmux
bash tests/test.sh # 66 tests (64 without gdb), runtime smoke suite
python tests/test_regressions.py # targeted audit regressions
python tests/run_all.py # all suites
Files
src/agent_tty/cli.py k — main script
src/agent_tty/monitor.py km — event monitor
scripts/k, scripts/km dev shims (no pip install needed)
pyproject.toml pip install agent-tty → agent-tty, k, km in PATH
man/agent-tty.1 man page source
tests/test.sh runtime smoke suite
tests/*.py static, docs, and regression suites
SKILL.md agent reference
EXAMPLES.md patterns + philosophy
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_tty-0.1.1.tar.gz.
File metadata
- Download URL: agent_tty-0.1.1.tar.gz
- Upload date:
- Size: 54.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10e68bb3ec0ba9203205d87b4eaf1640f4ec537bc4c129aee6bee6961631079d
|
|
| MD5 |
45776c371684f19ca008650ba552d235
|
|
| BLAKE2b-256 |
eb21a03064f914b8e654a8a6161a877120a27f182c44a9091ff8a3f6f8701585
|
Provenance
The following attestation bundles were made for agent_tty-0.1.1.tar.gz:
Publisher:
release.yml on rangersui/agent-tty
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_tty-0.1.1.tar.gz -
Subject digest:
10e68bb3ec0ba9203205d87b4eaf1640f4ec537bc4c129aee6bee6961631079d - Sigstore transparency entry: 1879556874
- Sigstore integration time:
-
Permalink:
rangersui/agent-tty@5552014d43ac20dfab9d384179bc93ed0a50ad05 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5552014d43ac20dfab9d384179bc93ed0a50ad05 -
Trigger Event:
release
-
Statement type:
File details
Details for the file agent_tty-0.1.1-py3-none-any.whl.
File metadata
- Download URL: agent_tty-0.1.1-py3-none-any.whl
- Upload date:
- Size: 26.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0efaf0711041a88632445656876cdb2718472c535d45c5194f0971f4f2255d18
|
|
| MD5 |
2ab1c4233a7f73bba034117797f0bafc
|
|
| BLAKE2b-256 |
24e543524bf4a6b903245ce768161f250063e0c7b0b527beda44975ac66bb3f9
|
Provenance
The following attestation bundles were made for agent_tty-0.1.1-py3-none-any.whl:
Publisher:
release.yml on rangersui/agent-tty
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_tty-0.1.1-py3-none-any.whl -
Subject digest:
0efaf0711041a88632445656876cdb2718472c535d45c5194f0971f4f2255d18 - Sigstore transparency entry: 1879556948
- Sigstore integration time:
-
Permalink:
rangersui/agent-tty@5552014d43ac20dfab9d384179bc93ed0a50ad05 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@5552014d43ac20dfab9d384179bc93ed0a50ad05 -
Trigger Event:
release
-
Statement type: