Skip to main content

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

agent_tty-0.1.1.tar.gz (54.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

agent_tty-0.1.1-py3-none-any.whl (26.6 kB view details)

Uploaded Python 3

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

Hashes for agent_tty-0.1.1.tar.gz
Algorithm Hash digest
SHA256 10e68bb3ec0ba9203205d87b4eaf1640f4ec537bc4c129aee6bee6961631079d
MD5 45776c371684f19ca008650ba552d235
BLAKE2b-256 eb21a03064f914b8e654a8a6161a877120a27f182c44a9091ff8a3f6f8701585

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_tty-0.1.1.tar.gz:

Publisher: release.yml on rangersui/agent-tty

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for agent_tty-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 0efaf0711041a88632445656876cdb2718472c535d45c5194f0971f4f2255d18
MD5 2ab1c4233a7f73bba034117797f0bafc
BLAKE2b-256 24e543524bf4a6b903245ce768161f250063e0c7b0b527beda44975ac66bb3f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_tty-0.1.1-py3-none-any.whl:

Publisher: release.yml on rangersui/agent-tty

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page