TTY
The REPL as a network surface: an authenticated WebSocket that streams the agent's run live, for hosted terminals and embedded chat.
Without triggers, af run drops into an interactive REPL — but that REPL lives on the container's stdin, which a hosted platform can't reach. The tty trigger exposes the same interactive conversation over a WebSocket, so a control plane can attach a browser terminal to a deployed agent and the operator can chat with it live: every step, tool call and result streams as it happens.
triggers:
- type: tty
# path: /tty (default)
# port: 8090 (default)
token_env: TTY_TOKEN # required — bearer auth, deny by defaultBecause it's a trigger like any other, it composes: an agent with tty alongside discord or cron is still a long-lived service, and the terminal is a window into that same agent — same memory, same permissions, same audit trail.
The protocol
Connect with a WebSocket to ws://host:8090/tty. Auth is the usual bearer token, presented one of two ways:
- an
authorization: Bearer <token>header, for server-side clients; - the WebSocket subprotocol
bearer.<token>, for browsers (which cannot set headers on a WebSocket). The server selects the subprotocol back on the upgrade.
Every frame in both directions is JSON. On connect the server announces itself:
{"type": "hello", "handle": "task-bot", "name": "Juniper", "description": "Triages inbound support tickets", "conversation_id": "8b1f..."}handle is the operator's handle for the agent; name is the name the agent chose for itself on first boot (it falls back to the handle until that ritual runs), and description is its job. A client can label the agent by name instead of the raw handle, which is what Looped Meet shows for an agent in a call. description is omitted when the agent has none. Older clients that only read handle keep working, since the extra fields are additive.
Send input:
{"type": "input", "text": "what's on the calendar today?"}Input may carry images for the turn — a screenshot, a shared-screen frame — as base64 (no data: prefix), at most 4 per turn:
{"type": "input", "text": "what's on my screen?", "images": [{"mediaType": "image/jpeg", "data": "<base64>"}]}While the run executes, the server streams its progress — the same events the local TUI renders:
{"type": "step", "n": 1}
{"type": "assistant", "content": "Let me check."}
{"type": "tool_call", "name": "run_bash", "arguments": "{...}"}
{"type": "tool_result", "name": "run_bash", "content": "...", "durationMs": 412}
{"type": "result", "status": "ok", "reply": "Two meetings: ...", "steps": 2}result ends the turn; send the next input after it. One run at a time per socket — an input sent mid-run gets {"type": "error", ...} back rather than queueing.
Cancelling a run
While a run is in flight you can stop it. Send:
{"type": "cancel"}This fires the same abort the /stop command does. The run halts at its next step boundary and ends with {"type": "result", "status": "aborted", ...}, the normal terminal frame, so a client that streams until result needs no new handling beyond the added status. The socket takes the next input right after, no reconnect. A cancel with nothing running is a no-op.
This is what a barge-in looks like from the other side: a participant tells a meeting agent to stop, and the bridge sends cancel instead of dropping the connection. The abandoned run stops burning tokens and stops running tool calls the moment it reaches a step boundary, rather than executing to completion with its reply going nowhere.
Sessions
Each connection is a conversation. By default a new socket gets a fresh conversation_id; pass ?conversation_id=<id> in the URL to resume one across reconnects (with memory.scope: thread — Memory), so a dropped connection or a page reload picks up where it left off.
Agent-created schedules deliver into the terminal too: a reminder created in a tty conversation arrives as {"type": "message", "text": "..."} on any socket attached to that conversation. If no terminal is attached when it fires, delivery falls through to the agent's other triggers as usual.
Exposure
token_env is required — an unauthenticated terminal contradicts deny-by-default, and this surface can drive everything the agent is permitted to do. The token resolves at startup; a missing env var fails right there. As with the webhook trigger, put a TLS-terminating proxy in front of the port before exposing it (wss://), and treat the token like the credential it is.
Every turn lands in the agent's run history with its status, steps and tokens, exactly like a run from any other trigger.
Are you an AI? Visit llms.txt — these docs as plain markdown.