Example: Controller Attach Lifecycle
Example: Controller Attach Lifecycle
Single client attaches to a fresh PTY session as the controller, sends a keystroke, receives output, resizes the terminal, and disconnects cleanly. All frames carry the `pty-ws/v1` envelope; only the relevant fields are shown.
Pre-conditions
- The client has obtained a bearer token authorized for `instance_id = "inst-42"` and `session_id = "01HW8Q3M9C7K5P0XZ8N4F2RV1Q"`.
- The session has just been created server-side; no other clients are attached.
1. WebSocket upgrade
GET /agents/inst-42/sessions/01HW8Q3M9C7K5P0XZ8N4F2RV1Q/attach HTTP/1.1
Host: sandbox.example
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Protocol: pty-ws.v1
Authorization: Bearer eyJhbGciOi...
Server responds:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: pty-ws.v1
2. Server → client: `binding_hello`
{
"op": "binding_hello",
"id": "01hw8q3p2k0a8m7r9z3b4tc5xs",
"ts": "2026-05-09T14:30:00.000Z",
"sequence": 0,
"payload": {
"binding_uri": "https://agentic-sandbox.aiwg.io/bindings/pty-ws/v1",
"binding_version": "1.0.0",
"supported_operations": [
"SendMessage", "SendStreamingMessage", "GetTask",
"ListTasks", "CancelTask", "SubscribeToTask"
],
"activated_extensions": [],
"session": {
"session_id": "01HW8Q3M9C7K5P0XZ8N4F2RV1Q",
"current_sequence": 0
}
}
}
Note: `activated_extensions` is empty because the client hasn't yet sent its activation list.
3. Client → server: `pty.join_session`
The client activates `pty-extensions/v1` on its first frame and joins as controller:
{
"op": "pty.join_session",
"id": "01hw8q3q5y0a8m7r9z3b4tc5xt",
"ts": "2026-05-09T14:30:00.150Z",
"extensions": [
"https://agentic-sandbox.aiwg.io/extensions/pty-extensions/v1"
],
"service_parameters": {
"trace_id": "00-1234567890abcdef1234567890abcdef-aabbccddeeff0011-01"
},
"payload": {
"role": "controller",
"cols": 120,
"rows": 30,
"client_label": "alice@laptop"
}
}
4. Server → client: `RoleAssigned`
{
"op": "pty.session_frame",
"id": "01hw8q3r1q0a8m7r9z3b4tc5xu",
"ts": "2026-05-09T14:30:00.180Z",
"sequence": 1,
"payload": {
"kind": "RoleAssigned",
"role": "controller",
"client_id": "c-alice-7f3a"
}
}
5. Server → client: `MembershipChanged`
{
"op": "pty.session_frame",
"id": "01hw8q3r1r0a8m7r9z3b4tc5xv",
"ts": "2026-05-09T14:30:00.181Z",
"sequence": 2,
"payload": {
"kind": "MembershipChanged",
"controllers": [
{ "client_id": "c-alice-7f3a", "label": "alice@laptop" }
],
"observers": []
}
}
6. Server → client: initial `Keyframe`
The PTY has just been spawned with a shell prompt. The server emits a Keyframe so the client has a coherent starting screen state:
{
"op": "pty.session_frame",
"id": "01hw8q3r1s0a8m7r9z3b4tc5xw",
"ts": "2026-05-09T14:30:00.182Z",
"sequence": 3,
"payload": {
"kind": "Keyframe",
"snapshot": "AAECAwQFBgcICQ...<base64 VT100 screen state>...",
"snapshot_format": "vt100-screen-state-v1",
"snapshot_cols": 120,
"snapshot_rows": 30,
"anchor_sequence": 3
}
}
The client renders the snapshot. The terminal now shows `agent@inst-42:~$ ` with the cursor at column 17, row 1.
7. Client → server: keystroke
Alice types `ls -la\ `. The client base64-encodes the raw bytes (`bHMgLWxhCg==`):
{
"op": "pty.session_input",
"id": "01hw8q3v3a0a8m7r9z3b4tc5y0",
"ts": "2026-05-09T14:30:02.500Z",
"payload": {
"data": "bHMgLWxhCg=="
}
}
8. Server → client: streamed output
The shell echoes the command, runs `ls -la`, and prints the result. The server emits one or more `Output` frames as the bytes become available:
{
"op": "pty.session_frame",
"id": "01hw8q3v9b0a8m7r9z3b4tc5y1",
"ts": "2026-05-09T14:30:02.520Z",
"sequence": 4,
"payload": {
"kind": "Output",
"stream": "stdout",
"data": "bHMgLWxhDQp0b3RhbCAyNAo..."
}
}
{
"op": "pty.session_frame",
"id": "01hw8q3vac0a8m7r9z3b4tc5y2",
"ts": "2026-05-09T14:30:02.530Z",
"sequence": 5,
"payload": {
"kind": "Output",
"stream": "stdout",
"data": "ZHJ3eHIteHIteCAyIGFnZW50..."
}
}
After ~`keyframe_interval_seconds` of activity (or `keyframe_interval_frames` Output frames, whichever first), the server emits a fresh Keyframe. With default cadence (`5s` / `100 frames`), idle short sessions may not see one until the next reconnect.
9. Client → server: resize
Alice resizes the local terminal from 120×30 to 140×40:
{
"op": "pty.session_resize",
"id": "01hw8q3y4d0a8m7r9z3b4tc5y3",
"ts": "2026-05-09T14:30:05.000Z",
"payload": { "cols": 140, "rows": 40 }
}
10. Server → client: resize broadcast
{
"op": "pty.session_frame",
"id": "01hw8q3y4e0a8m7r9z3b4tc5y4",
"ts": "2026-05-09T14:30:05.020Z",
"sequence": 6,
"payload": { "kind": "Resize", "cols": 140, "rows": 40 }
}
The PTY's `winsize` is updated and the running shell receives `SIGWINCH`.
11. Client → server: `pty.leave_session`
Alice closes her terminal:
{
"op": "pty.leave_session",
"id": "01hw8q42z00a8m7r9z3b4tc5y5",
"ts": "2026-05-09T14:31:30.000Z",
"payload": {}
}
12. Server → client: final `MembershipChanged` + close
{
"op": "pty.session_frame",
"id": "01hw8q4310a8m7r9z3b4tc5y6",
"ts": "2026-05-09T14:31:30.010Z",
"sequence": 7,
"payload": {
"kind": "MembershipChanged",
"controllers": [],
"observers": []
}
}
{
"op": "binding_goodbye",
"id": "01hw8q4320a8m7r9z3b4tc5y7",
"ts": "2026-05-09T14:31:30.011Z",
"payload": { "reason": "session_closed" }
}
Server closes the WS with code `1000`. The session itself terminates if the agent's policy is `terminate_on_last_controller`; otherwise it remains running and a future client may rejoin.
Sequence summary
Client Server
│ HTTP upgrade ───────────▶│
│◀───────────── 101 Switch │
│◀── binding_hello (seq=0) │
│ pty.join_session ──────▶ │
│◀── RoleAssigned (seq=1) │
│◀── MembershipChanged (2) │
│◀── Keyframe (seq=3) │
│ pty.session_input ────▶ │
│◀── Output (seq=4) │
│◀── Output (seq=5) │
│ pty.session_resize ───▶ │
│◀── Resize (seq=6) │
│ pty.leave_session ────▶ │
│◀── MembershipChanged (7) │
│◀── binding_goodbye │
│◀── WS close 1000 │