vessel
vessel is a PTY-based runtime for spawning, controlling, and observing interactive terminal agents over a Unix socket.
It is designed for AI orchestrators, test harnesses, and automation systems that need real terminal semantics (not just stdout pipes).

What vessel is (and is not)
- Is: local control plane for interactive worker processes (
spawn,send,wait,snapshot,events,attach,view). - Is: good for multi-agent workflows, TUI testing, and reproducible terminal automation.
- Is not: container runtime, distributed scheduler, or durable job queue.
Requirements
- Linux with Unix sockets + PTY support
- Rust 1.85+ (for building from source)
tmux(optional, only forvessel view)
Install
Quick start (2 minutes)
# 1) Spawn a worker shell
# 2) Send a command (+ Enter)
# 3) Wait for expected output, then inspect the virtual screen
# 4) Clean up (SIGTERM by default; use --force for hard kill)
# 5) Stop server when done
Mental model
Agent = PTY process + transcript ring + virtual screen
Server = owns all agent state, listens on Unix socket
Client = stateless CLI sending JSON requests
View = tmux dashboard; panes run read-only attach streams
Key implications:
snapshotreflects current terminal state (best for assertions).tail/dumpreflect transcript bytes (useful for logs/streaming).- State is in-memory in the server process (no persistence across server restart).
Core command map
Lifecycle
Input/output
--newline/--enter write the submit key in a separate PTY write, 50ms after
the text. Full-screen TUIs classify input by arrival timing: a CR/LF that lands
in the same burst as the text is treated as pasted content and gets inserted
into the composer instead of submitting it, so the prompt sits there looking
delivered. The pause puts the key outside that burst. Use
--submit-delay-ms 0 to write it immediately (fine for shells and other
line-oriented programs), or raise it for a TUI that still swallows the key.
Multi-line prompts
Use --paste (-p) for any prompt that spans more than one line. It wraps the
text in bracketed-paste markers (ESC[200~ … ESC[201~), which a TUI reads as
a single paste: the newlines become lines in the composer. Without it the first
newline submits a truncated prompt and every remaining line lands as its own
turn — three separate agent turns for one prompt, each billed, none of them
what was asked.
--paste --enter is the usual orchestrator pairing: paste the whole prompt,
then submit it as one turn. Pass - as the text to read the payload from
stdin, which avoids quoting a long prompt onto the command line:
Any ESC[201~ inside the text is dropped rather than forwarded — it would
otherwise close the bracket early and deliver the remainder as live keystrokes.
Sending to a group
send, send-bytes, and send-keys all take the same selectors kill and
signal use: --label (repeatable, agents must carry all of them), --proc
(command substring), and --all. They match running agents only.
With a selector, omit the agent ID — every positional is payload:
Passing both an ID and a selector is rejected rather than guessed at. Note that
--proc has no -p short form on these three commands, because -p is
--paste on send; kill and signal keep -p for --proc.
A fan-out reports one line per agent and exits non-zero if any of them missed the input — a group send can partially fail, and staying silent would report that as success:
$ vessel send --label batch "hi" --enter
tidy-otter ok
brave-heron error: write failed: Input/output error
Deliveries run concurrently, so the per-agent submit delays overlap instead of stacking. A request naming a single agent ID is unchanged: silent on success, non-zero on failure.
Synchronization and assertions
Streaming and observability
One-off command execution
Recording and replay scaffolding
Orchestration patterns
Spawn dependencies:
# Wait for setup to exit before starting app
# Wait for output from another agent before spawning
Recommended cleanup for automation:
Output formats for automation
Many commands support --format text|json|pretty.
text: compact, pipe-friendlyjson: structured envelope ({"<key>": ..., "advice": [...]})pretty: human-oriented terminal output
Example:
|
Server behavior
- Server auto-starts for most regular commands.
eventsandsubscribedo not auto-start (they expect an existing server/session).- Default socket path:
/run/user/$UID/vessel.sock(fallback/tmp/vessel-$UID.sock). - Override with
VESSEL_SOCKETor--socket.
Logging
Logs go to stderr, at vessel=warn by default and vessel=debug with
--verbose. RUST_LOG overrides the filter in every mode.
| Variable | Effect |
|---|---|
VESSEL_LOG=<path> |
Also append logs to a file, at vessel=info or better so server lifecycle events are captured without --verbose. |
VESSEL_LOG_FORMAT=json |
Emit JSON spans and events to stderr instead of the human formatter, for callers that parse vessel's logs. |
Troubleshooting
If you hit stale socket/session issues:
||
Notes:
killsends SIGTERM by default; some interactive shells ignore it. Use--forcefor deterministic teardown.- For TUI inspection, prefer
snapshotorattach --readonlyover plaintail.
Development
Relevant docs:
AGENTS.md- contributor + agent workflowdocs/testing.md- testing approach and scenarios