> ## Documentation Index
> Fetch the complete documentation index at: https://docs.gravixlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream PTY Output

> Attach to a PTY session and follow its output, including retained scrollback

`GET /v1/agents/runtime/{runtime_id}/pty/{session_id}/stream`

Streams the session's terminal output as Server-Sent Events. The stream begins with the
session's retained scrollback and then follows live output, so attaching to a session that
has been running unattended shows you what you missed before it shows you what happens next.

Multiple clients can stream the same session concurrently, and you can detach and re-attach
freely: the session is owned by the execution plane, not by the connection.

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import sys

  from gravixlayer import GravixLayer

  client = GravixLayer()  # defaults to cloud="aws", region="us-east-1"
  sandbox = client.runtime.create()  # defaults to template="base-small"
  session = sandbox.pty.create()
  sid = session.session_id

  for event in sandbox.pty.stream(sid):
      if event["type"] == "data":
          print(event["data"].decode("utf-8", "replace"), end="")
      elif event["type"] == "exit":
          print(f"\nsession exited: {event['exit_code']}")
      elif event["type"] == "error":
          print(f"\nstream error: {event['message']}")

  # Or with callbacks
  sandbox.pty.stream(
      sid,
      on_data=lambda b: sys.stdout.buffer.write(b),
      on_exit=lambda code, status: print("exited", code, status),
  )

  sandbox.kill()
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import { GravixLayer } from 'gravixlayer';

  const client = new GravixLayer(); // defaults to cloud="aws", region="us-east-1"
  const sandbox = await client.runtime.create(); // defaults to template="base-small"
  const session = await sandbox.pty.create();
  const sid = session.sessionId;

  for await (const event of sandbox.pty.stream(sid)) {
    if (event.type === 'data') {
      process.stdout.write(new TextDecoder().decode(event.data));
    } else if (event.type === 'exit') {
      console.log(`\nsession exited: ${event.exitCode}`);
    } else if (event.type === 'error') {
      console.log(`\nstream error: ${event.message}`);
    }
  }

  // Or with a handle callback
  const terminal = sandbox.pty.handle(sid).connect({
    onData: (chunk) => process.stdout.write(chunk),
    onExit: (code, status) => console.log('exited', code, status),
  });

  await sandbox.kill();
  ```

  ```bash CLI theme={"theme":{"light":"github-light","dark":"github-dark"}}
  # Writes raw terminal bytes to stdout, so escape sequences render normally.
  gravixlayer runtime pty attach "$RT" "$SID"
  ```

  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -N "https://api.gravixlayer.ai/v1/agents/runtime/$RT/pty/$SID/stream" \
    -H "Authorization: Bearer $GRAVIXLAYER_API_KEY"
  ```
</CodeGroup>

## Events

Each SSE frame carries one JSON object with a `type` field.

| `type`  | Payload               | Meaning                                           |
| ------- | --------------------- | ------------------------------------------------- |
| `data`  | `data`                | Base64 encoded terminal bytes                     |
| `exit`  | `exit_code`, `status` | The session's process has exited; the stream ends |
| `error` | `message`             | The stream could not continue                     |

```
data: {"type":"data","data":"dG90YWwgOApkcnd4ci14ci14IDIgcm9vdCByb290IDQwOTYK"}

data: {"type":"exit","exit_code":0,"status":"exited"}
```

## Why the data is base64 encoded

Terminal output is a byte stream, not text. It contains escape sequences, and a chunk
boundary can fall in the middle of a multi-byte UTF-8 character. Base64 keeps those bytes
intact through JSON.

The Python SDK decodes `data` back to `bytes` for you. If you need text, decode with
`errors="replace"` and accumulate across chunks rather than decoding each chunk in
isolation:

<CodeGroup>
  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import codecs

  decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")
  for event in sandbox.pty.stream(sid):
      if event["type"] == "data":
          print(decoder.decode(event["data"]), end="")
  ```

  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const decoder = new TextDecoder();
  for await (const event of sandbox.pty.stream(sid)) {
    if (event.type === 'data') process.stdout.write(decoder.decode(event.data));
  }
  ```
</CodeGroup>

## Scrollback

Each session retains up to 256 KB of recent output. Attaching replays that buffer first,
which is what makes detach and re-attach useful. If a session has produced more than 256 KB
since you last attached, the oldest output has been discarded and the replay starts partway
through.

If you need a complete transcript, keep a reader attached, or redirect the program's output
to a file in the runtime and read the file afterwards.
