agent-tty
Persistent Python runtime for your AI agent.
Each session is a long-lived Python namespace in a child process. Agent cells
enter that namespace through eval/exec, so imports, variables, open
connections, servers, and in-memory decisions survive across agent turns.
bash_tool is curl: every call forks a process, runs, and dies. k is a
socket: one process stays alive, and every call is a function invocation inside
it. The agent sends source code through a structured pipe and receives captured
output. Humans attach through a real terminal with readline, tab completion,
colors, and Ctrl-C. Both channels share the same namespace.
Requires Python 3.10+.
Install the CLI:
python -m pip install agent-tty
k --version
After install, k is the complete CLI. Do not edit k.
From a source checkout, install editable once so k resolves to this tree:
python -m pip install -e .
k --version
The code help is the single source of truth:
k --help
Quick Start
Start the daemon in one terminal:
k daemon
Stop the daemon with k stop from a client terminal, or Ctrl-C in the daemon
terminal. Daemon shutdown terminates owned sessions, closes the control socket,
and removes TCP daemon.json metadata.
Use the client from another terminal:
k new work
k run work "x = 41"
k run work "x + 1"
# 42
Async cells:
k fire work "import time; time.sleep(2); y = x + 1"
# {"cell_id": "a1b2c3d4e5f6", "status": "fired"}
k poll work a1b2c3d4e5f6
# {"cell_id": "a1b2c3d4e5f6", "status": "done", "output": ""}
k run work "y"
# 42
Human attach:
k attach work
On PTY and WinPTY sessions, Ctrl-] detaches and leaves the session alive.
exit() exits the session process. On socket-console sessions, ending stdin
detaches.
Runtime Model
k daemon runs a foreground daemon. The daemon owns named session processes
and a local control socket.
Each session is a Python process with:
- one persistent namespace,
- one execution lock,
- one cell table for async
fireresults, - one human attach surface.
AI commands use a separate control path and share the same namespace as the human console.
Stateful First
Most command tools are intentionally stateless: fork, run, die. That is simple for humans, but wasteful for agents. Agents repeat imports, reopen connections, re-parse configs, and rebuild intermediate data because the process disappears after every call.
agent-tty flips that default. The process is the workspace. State is not a cleanup problem first; it is addressable memory.
Things that stay alive:
- Python variables, imports, compiled regexes, parsed configs, DataFrames, and models,
- database handles, HTTP sessions, WebSockets, TCP sockets, SSH tunnels, and browser/CDP sessions,
- Flask apps, local servers, file watchers, monitors, and other daemon threads,
- live control-plane decisions such as feature flags, rate limits, blocked IP sets, routing weights, and circuit breaker state.
Static config can become a Python variable. Patch one cell; the next request sees it. No restart is needed for logic that already lives inside the session.
Two Channels, One Namespace
The agent channel is structured: source code in, captured text/JSON out. It does not need to parse ANSI escape sequences, cursor movement, prompts, or screen redraws to know when a cell finished.
The human channel is interactive: k attach connects to the same process
through PTY, WinPTY, or socket-console mode. A human can inspect variables,
interrupt with Ctrl-C, or detach with Ctrl-] without discarding the namespace.
That split is the core design: pure data for agents, real terminal ergonomics for humans, one shared runtime underneath.
Session Modes
agent-tty uses the best local console surface available:
| mode | platform | human attach |
|---|---|---|
| POSIX PTY | Linux, macOS, WSL | raw terminal, readline, tab, arrows, Ctrl-C |
| WinPTY | Windows with pywinpty |
raw terminal through WinPTY |
| socket console | fallback | line-based InteractiveConsole over local TCP |
PTY and WinPTY sessions support Ctrl-] to detach while the Python session
keeps running. exit() exits the session process. Socket-console sessions
detach when stdin ends.
Default Work Surface
Default to the live session for project work. Once a daemon/session exists,
commands that affect the task should go through k run or k fire, even when
the command is as simple as ls, pwd, or git status.
That keeps the work in one inspectable runtime: cwd changes, environment mutations, imports, open sockets, cached data, and command history all stay with the session the human can attach to.
Use the host shell as plumbing:
- start or stop the daemon,
- write larger Python cells to files before loading them,
- inspect or repair the repository when no session is available.
Inside the session, call host commands through Python:
k run work "import subprocess; subprocess.run(['git', 'status'])"
Command Reference
k daemon [--show-token] start daemon in foreground
k stop stop daemon gracefully
k new <name> create a Python session
k int <name> interrupt running async cells
k kill <name> terminate session process and forget it
k run <name> "code" sync Python eval/exec, print raw output
k fire <name> "code" async queued eval/exec, print JSON cell_id
k poll <name> [cell_id] print JSON cell result
k status <name> print JSON session state
k vars <name> print JSON list of public namespace names
k complete <name> "text" print JSON Python completion candidates
k ls list sessions
k attach <name> attach human REPL to the session
k --version|-V|version print version
k new <name> creates a Python session. Put host commands inside Python cells
with subprocess:
k run work "import subprocess; subprocess.run(['git', 'status'])"
For larger code, write a Python file and load it into the live session:
cat > /tmp/agent_tty_task.py << 'PY'
import subprocess
result = subprocess.run(["git", "status"], text=True, capture_output=True)
print(result.stdout)
PY
k run work "exec(open('/tmp/agent_tty_task.py').read())"
REPL Patterns
Because the session is a Python REPL, ordinary Python patterns become agent operations:
- Prefix tax: import what you use once, then call shorter names in later cells.
- Print tax: expression results display automatically; the last expression does
not need
print(). - Hot reload: use
exec(open("module.py").read())orimportlib.reload(m)to update code without losing process state. - Incremental execution: split a long script into cells. If step 3 fails, fix step 3; steps 1 and 2 still exist in memory.
- Catch, fix, retry: read the traceback, patch a function, and run again in the same namespace.
- Host commands: use
subprocess.run(..., capture_output=True, text=True)from inside the session when you need the OS.
The REPL is Turing complete. File watchers, completion callbacks, local monitors, proxy servers, and control loops do not need to be built into agent-tty; the session can build them as Python code.
Output Formats
Each command has a fixed output style:
| command | format | shape |
|---|---|---|
k daemon |
process | foreground daemon; startup line on stderr |
k stop |
text | OK stopping daemon or ERR ... |
k new |
text | OK <name> pid=<pid> ... or ERR ... |
k int |
text | OK interrupted <name> (N cells) or ERR ... |
k kill |
text | OK killed <name> or ERR ... |
k ls |
text | one line per session, or (no sessions) |
k run |
raw text | captured stdout/stderr from the cell |
k fire |
JSON | {"cell_id":"...","status":"fired"} |
k poll |
JSON | `{"cell_id":"...","status":"running |
k status |
JSON | `{"state":"idle |
k vars |
JSON | {"vars":["name", ...]} |
k complete |
JSON | {"matches":["os.path", ...]} |
k attach |
stream | interactive console |
k --version |
text | agent-tty 0.2.1; aliases: k -V, k version |
k run prints expression results like a Python REPL: strings print as raw text;
other values use repr.
Assignments usually produce no output:
k run work "x = 1"
# empty output
Expressions print:
k run work "x + 1"
# 2
Async Cells
fire starts a background cell and returns immediately. Cells inside one
session execute serially under the session lock. Create multiple sessions for
parallel work.
k new a
k new b
k fire a "import time; time.sleep(5); result = 'A'"
k fire b "import time; time.sleep(5); result = 'B'"
poll with a cell id returns that cell. poll without a cell id returns the
most recent cell in the session, or {"status":"idle"} if no cells exist.
Transport
AF_UNIX mode uses K_SOCK, defaulting to /tmp/k.sock.
TCP mode uses 127.0.0.1:K_PORT (default 7399) and token authentication.
Native Windows uses TCP mode. The daemon writes private daemon metadata for
local clients:
k daemon pid=12345 127.0.0.1:7399 mode=winpty meta=...\daemon.json
The metadata file lets a new terminal run k ls without setting K_TOKEN.
Shutdown removes the metadata file.
| platform | metadata path |
|---|---|
| Windows | %LOCALAPPDATA%\agent-tty\daemon.json |
| POSIX TCP | $XDG_RUNTIME_DIR/agent-tty/daemon.json |
| POSIX TCP fallback | /tmp/agent-tty-$UID/daemon.json |
Only one auto-discoverable TCP daemon can own daemon.json at a time. Starting
a second TCP daemon while the metadata file points to a live daemon fails loud
instead of replacing the first daemon's token.
K_TOKEN and K_PORT remain explicit overrides for debugging or unusual
shells. Use k daemon --show-token only when you deliberately want shell setup
text printed to stderr:
k daemon --show-token
set K_TOKEN=abc123...
export K_TOKEN=abc123...
Attach uses the same token lookup in TCP mode, so k attach works from another
terminal after the daemon metadata file exists.
In a source checkout, k.py.template is an optional debug wrapper for storing a
local daemon token:
cp k.py.template k.py
# paste K_TOKEN and K_PORT into k.py if you want a fixed-token wrapper
python k.py ls
python k.py new work
k.py is listed in .gitignore because it contains a live local daemon token.
On Windows, WinPTY mode requires pywinpty. With pywinpty available the daemon
prints mode=winpty; otherwise it falls back to mode=socket.
Tests
Static syntax check:
python -B -m py_compile agent_tty.py k.py.template tests/test_pty_posix.py tests/test_tcp_windows.py
POSIX PTY regression suite:
python3 -B tests/test_pty_posix.py
The POSIX suite starts a daemon with a Unix socket, creates a PTY session, and
checks new, ls, run, fire, poll, status, complete, and kill.
Windows TCP regression suite:
python -B tests/test_tcp_windows.py
The Windows suite starts a daemon on loopback TCP, verifies daemon.json token
discovery without startup token leakage, then exercises the real CLI client
path. It works with WinPTY when pywinpty is installed and with socket-console
fallback otherwise.
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.2.1.tar.gz.
File metadata
- Download URL: agent_tty-0.2.1.tar.gz
- Upload date:
- Size: 31.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9107fb68d623595dcb71d8623b6fb02aabd83ad221f9ec273a96e4b778cf3cf7
|
|
| MD5 |
7589593c27db55904ed53f23641dd8ab
|
|
| BLAKE2b-256 |
01284ad9863a6b817240f93a7225269920347c707788e11128baf9a9bd1e4f94
|
Provenance
The following attestation bundles were made for agent_tty-0.2.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.2.1.tar.gz -
Subject digest:
9107fb68d623595dcb71d8623b6fb02aabd83ad221f9ec273a96e4b778cf3cf7 - Sigstore transparency entry: 1914404733
- Sigstore integration time:
-
Permalink:
rangersui/agent-tty@a36d1a0fb6d820737e09393af384aa3ded0c1ef9 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a36d1a0fb6d820737e09393af384aa3ded0c1ef9 -
Trigger Event:
release
-
Statement type:
File details
Details for the file agent_tty-0.2.1-py3-none-any.whl.
File metadata
- Download URL: agent_tty-0.2.1-py3-none-any.whl
- Upload date:
- Size: 18.2 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 |
d9b8ceedc585c7f23233a91a33a4d7d977716924c23c943d022578c50c741911
|
|
| MD5 |
40dc1b7a5e078ca4e12fd46c159dd299
|
|
| BLAKE2b-256 |
012df813b784a13889b94941268194feb3e3891725c5d95e648d9d0a87ac4929
|
Provenance
The following attestation bundles were made for agent_tty-0.2.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.2.1-py3-none-any.whl -
Subject digest:
d9b8ceedc585c7f23233a91a33a4d7d977716924c23c943d022578c50c741911 - Sigstore transparency entry: 1914404907
- Sigstore integration time:
-
Permalink:
rangersui/agent-tty@a36d1a0fb6d820737e09393af384aa3ded0c1ef9 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/rangersui
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@a36d1a0fb6d820737e09393af384aa3ded0c1ef9 -
Trigger Event:
release
-
Statement type: