Skip to main content

agent-tty

A persistent TTY for your AI agent. Shared live terminal for humans.

bash_tool runs a command and forgets. agent-tty gives your agent a persistent TTY inside tmux — variables, cwd, imports, connections, SSH sessions, and debugger state survive across agent turns. You watch the same terminal live, 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.

Give your agent k when it needs memory between turns: Python imports, database connections, browser/CDP sockets, remote shells, debuggers, running servers. You see everything through k watch — cell markers, completion ticks, frame noise hidden — or tmux attach for native raw takeover.

km calls your agent back when a long cell finishes. k poll is a simple fallback for runtimes without monitor/interrupt support.

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)"

Recommended Workflow

The agent defaults to k — it is the shared working terminal, and you watch the same cwd/env/history/output the agent sees. For code with quotes, f-strings, SQL, shell variables, or any escaping complexity, the agent writes a file with its shell tool, then loads it through k.

cat > /tmp/task.py << 'EOF'
import os

conn = db.connect(os.environ["DATABASE_URL"])
rows = conn.execute("SELECT * FROM orders WHERE status = 'pending'").fetchall()
print(f"found {len(rows)} pending orders")
EOF
k run -j py "exec(open('/tmp/task.py').read())"

The heredoc preserves content literally. source/exec loads it into the live session, so imports, variables, cwd, sockets, and database handles still persist. This avoids shell-quoting fights and the multiline-send edge cases that can confuse frame detection: the command sent to k is always one simple line.

Simple commands still go straight through k: k run -j work "echo hello".

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, whitespace-stripped lines to stdin. Hook exit = frame done; the line that triggered the exit is treated as the frame boundary and removed from output. 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.

The source/exec workflow avoids shell-quoting problems and the multiline-send edge cases that can confuse frame detection: the command sent to k is always a single simple line, while the real code loads inside the live session. Repeat-mode frame collision from command output (5+ identical non-empty lines) is a separate limitation that still applies regardless of how code is sent.

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 — callback monitor

km wakes your agent when a long cell finishes. It tails the session log and emits one JSON event per line to stdout — no polling, no sleep loops.

Works with any agent host that has background-notification support: Claude Code's Monitor tool, Codex App Server via vendor/codex_bridge.py, or a plain subprocess reader.

km <session> [cell_id] [-1]

-1 exits after first completion — one-shot .then() for agent orchestration.

Why km after k

k is the agent's stateful terminal. km is the callback channel for long-running cells. Background task support alone is not enough when process state matters; km lets the persistent TTY keep running and wakes the agent when a cell finishes. k poll works 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 the monitor in the background, and gets woken exactly once on completion. Zero wasted calls.

Continuous mode

Without -1, km runs indefinitely — every event streams as a JSON line. Useful for multi-cell orchestration where your agent reacts to each completion in sequence.

Codex bridge (experimental)

vendor/codex_bridge.py is a local experiment for hosts that expose Codex App Server. It reads km stdout, polls completed cells with k poll, and starts a visible Codex turn with turn/start when the target thread is idle. If the thread is already active, events are queued and batched into one visible turn after the thread becomes idle. That makes the event visible as a normal Codex turn instead of hiding it in prompt history.

Important caveat from local testing on 2026-06-20: Codex App Server does not expose a single Monitor-like primitive. thread/inject_items persists data for later turns but does not wake an agent; turn/steer needs an active turn and expected turn id; turn/interrupt stops work but does not carry payload; and turn/start wakes an idle thread but can create a parallel side turn if another turn is still active. The bridge owns that state machine.

Codex Desktop may also fail to live-refresh turns started by another app-server client. Treat this bridge as better suited to headless/remote automation and external sinks such as tmux, a web UI, or email. It is not a guarantee that the Desktop UI will update in real time.

# find candidates first
python vendor/codex_bridge.py --list-threads --thread-cwd . --thread-search "agent tty"
python vendor/codex_bridge.py --list-loaded

# run the bridge daemon
python vendor/codex_bridge.py --session work --thread-id THREAD_ID

The bridge is deliberately type-sealed: km lines must parse into KmEvent, thread ids must become ThreadHandle, thread runtime must become ThreadRuntimeStatus, idle delivery must become IdleThread, and visible turn/start calls only accept an IdleThread plus an EventPrompt derived from a validated event and optional PollResult. The static check is python tests/test_bridge_contracts.py.

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_bridge_contracts.py # Codex bridge type-seal contracts, no app-server
python tests/test_docs.py           # docs/package drift, no tmux
python -m mypy --platform linux src vendor tests # POSIX type surface
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)
vendor/codex_bridge.py     experimental km → Codex App Server bridge
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.2.tar.gz (70.3 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.2-py3-none-any.whl (29.5 kB view details)

Uploaded Python 3

File details

Details for the file agent_tty-0.1.2.tar.gz.

File metadata

  • Download URL: agent_tty-0.1.2.tar.gz
  • Upload date:
  • Size: 70.3 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.2.tar.gz
Algorithm Hash digest
SHA256 66d923d14d7ec9216e22c4761e258abc2393eccd5ab98b11271e55e3d9a2e197
MD5 0f455aff2b9ea50403b062694192dfc2
BLAKE2b-256 1e89db6133543c9a3fcbea502b14346c86c11d50db1bb05cb0e38bbea40437b9

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_tty-0.1.2.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.2-py3-none-any.whl.

File metadata

  • Download URL: agent_tty-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 29.5 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.2-py3-none-any.whl
Algorithm Hash digest
SHA256 88296652c59f085ef6f857d5614902145ba857148983a6087a388c7e4fd10699
MD5 3747493ebe2f7053cc1e22af25e3e13d
BLAKE2b-256 35ba2a01e6729b4f6c990635c4d971cfbd47a74b4c9b3daeb97a61b86e89a687

See more details on using hashes here.

Provenance

The following attestation bundles were made for agent_tty-0.1.2-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