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

# WebSocket Chat

> Real-time bidirectional chat via WebSocket

Connect via WebSocket for real-time, bidirectional chat communication. WebSocket is ideal for applications that need to send multiple messages without reconnecting.

<Note>
  For event types and general chat concepts, see the [Chat Overview](/api-reference/chats/overview).
</Note>

## Connection

Connect to the WebSocket endpoint at:

```
wss://api.trellis.sh/v1/chats/ws
```

The JWT must be supplied via the `Authorization: Bearer` header on the upgrade request, or via a first-message authenticate frame for browser clients.

<Warning>
  **Breaking change.** Passing the JWT as a `?token=` query parameter is no longer accepted — the server rejects the upgrade with `HTTP 403` so the credential is treated as compromised on arrival. Query strings appear in load-balancer access logs, which is why this form was deprecated.
</Warning>

### Server-side keepalive (opt-in)

Long-running tool calls — for example a multi-query analytics tool that takes a minute to complete — can leave the WebSocket silent for tens of seconds while the server works. Some load balancers and mobile clients close idle connections in that window.

To prevent that, append `?heartbeat=1` to the connection URL. The server will then emit a small `heartbeat` event every 15 s while a turn is streaming, which keeps the connection lit on every hop:

```
wss://api.trellis.sh/v1/chats/ws?heartbeat=1
```

Clients should treat the event as a no-op (ignore it explicitly, or fall through any unknown-event handler):

```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
case "heartbeat":
  // server keepalive — no action required
  break;
```

<Note>
  Heartbeats are opt-in today and may become the default in a future release. Clients with strict event-type validation should add explicit handling before enabling the flag.
</Note>

## Authentication

<Tabs>
  <Tab title="Authorization header (recommended)">
    Set the `Authorization: Bearer <jwt>` header on the WebSocket upgrade request. This is the preferred form for non-browser clients (Python, Go, mobile SDKs):

    ```python theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import websockets

    async with websockets.connect(
        "wss://api.trellis.sh/v1/chats/ws",
        additional_headers=[("Authorization", "Bearer YOUR_TOKEN")],
    ) as ws:
        ...
    ```

    Upon successful connection:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {"event": "authenticated", "data": {"status": "ok"}}
    ```

    If the header is present but the JWT is invalid or expired, the connection is closed with code `4001`.
  </Tab>

  <Tab title="First message (browser)">
    The browser `WebSocket` constructor cannot set custom headers on the upgrade. Connect without auth, then send an authenticate frame as the first message:

    ```javascript theme={"theme":{"light":"github-light","dark":"github-dark"}}
    const ws = new WebSocket("wss://api.trellis.sh/v1/chats/ws");
    ws.onopen = () => {
      ws.send(JSON.stringify({ action: "authenticate", token: "YOUR_TOKEN" }));
    };
    ```

    Upon success:

    ```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
    {"event": "authenticated", "data": {"status": "ok"}}
    ```

    If authentication fails, the connection is closed with code `4001`.
  </Tab>
</Tabs>

### Concurrent connections

Each JWT may hold at most **3** concurrent WebSocket connections. A 4th connection receives a `WS_CONNECTION_CAP` error frame and is closed with code `4429`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "error", "data": {"code": "WS_CONNECTION_CAP", "retryable": false}}
```

Slots are released as connections close. To raise the ceiling for a workload, request a separate JWT (each call to [Authenticate](/api-reference/authentication/authenticate) returns a token with its own `jti` claim).

### Frame rate limits

Each socket may send at most **10 `send_message` frames per minute** with a **3-frame burst over 10 seconds**. Excess frames receive an `error` event:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "error", "data": {"code": "RATE_LIMIT", "retryable": true, "retry_after_seconds": 7}}
```

The throttle is keyed per-socket; combined with the 3-connection cap, the per-JWT ceiling on `send_message` is 30 per minute. Other actions (`ping`, `interrupt`, `delete_message`, `edit_message`) are not throttled.

## Sending Messages

After authentication, send chat messages using the `send_message` action:

<ParamField body="action" type="string" required>
  Must be `"send_message"`.
</ParamField>

<ParamField body="message" type="string" required>
  The natural language message to send.
</ParamField>

<ParamField body="integration_id" type="string">
  ID of the database integration to query. Required for database queries.
</ParamField>

<ParamField body="chat_id" type="string" required>
  ID of an existing chat. Obtain one from [Create Chat](/api-reference/chats/create-chat). Omitting this field returns an `error` event with `code: "MISSING_CHAT_ID"`; passing an ID that doesn't belong to the caller returns `code: "CHAT_NOT_FOUND"`. The connection stays open in both cases.
</ParamField>

<ParamField body="title" type="string">
  Optional title hint for the chat. If omitted, the chat keeps the title set at creation time.
</ParamField>

<ParamField body="model" type="string">
  AI model to use for this message. If omitted, the default model is used. See [List Models](/api-reference/models) for available values.
</ParamField>

<ParamField body="upload_ids" type="string[]">
  List of upload IDs to attach to this message. Upload files first via [Upload Files](/api-reference/chats/upload-files), then reference their IDs here. Requires `chat_id` to be set.
</ParamField>

<ParamField body="include_tool_events" type="boolean">
  If `true`, emit `tool_call` and `tool_result` events for real-time tool execution visibility. Default: `false`.
</ParamField>

### Example message

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "action": "send_message",
  "message": "How many orders were placed last month?",
  "chat_id": "chat_abc123",
  "integration_id": "550e8400-e29b-41d4-a716-446655440000",
  "model": "claude-sonnet-4-6"
}
```

### Example with file attachment

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "action": "send_message",
  "message": "Analyze the attached CSV data",
  "chat_id": "chat_abc123",
  "integration_id": "550e8400-e29b-41d4-a716-446655440000",
  "upload_ids": ["550e8400-e29b-41d4-a716-446655440000"]
}
```

## Response Events

Events are delivered as JSON objects with `event` and `data` fields:

| Event           | Data                                                                                            | Description                                                                                                                                                                                                                                                                           |
| --------------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `authenticated` | `{"status": "ok"}`                                                                              | Connection authenticated successfully                                                                                                                                                                                                                                                 |
| `chat_metadata` | `{"id": "chat_...", "user_message_id": "msg_..."}`                                              | Chat ID and persisted user message ID. Emitted at the start of every response.                                                                                                                                                                                                        |
| `processing`    | `{"status": "starting" \| "thinking" \| "analyzing" \| "processing"}`                           | AI processing status. `starting` is emitted within \~100ms of request receipt so the UI can render a spinner before agent startup completes.                                                                                                                                          |
| `tool_call`     | `{"tool": "...", "args": {...}}`                                                                | Tool invocation started (only if `include_tool_events` is `true`)                                                                                                                                                                                                                     |
| `tool_result`   | `{...}`                                                                                         | Tool execution completed (only if `include_tool_events` is `true`)                                                                                                                                                                                                                    |
| `visualization` | Full chart or table payload                                                                     | A chart or table was generated. See [Visualizations](/api-reference/chats/visualizations).                                                                                                                                                                                            |
| `text_delta`    | `{"content": "..."}`                                                                            | An incremental chunk of the final assistant text, streamed as the model generates it. Multiple frames arrive per turn. Concatenate `content` values in order to build the response. See [Chat Overview → Streaming text deltas](/api-reference/chats/overview#streaming-text-deltas). |
| `message`       | `{"content": "...", "id": "msg_..."}`                                                           | The final response. Always carries the **authoritative full content** plus the persisted message `id`. Safe to overwrite any streaming buffer with `content` on receipt.                                                                                                              |
| `interrupted`   | `{"status": "interrupted"}`                                                                     | The in-progress response was cancelled by an `interrupt` action                                                                                                                                                                                                                       |
| `error`         | `{"error": "...", "code": "...", "retryable": boolean, "debug_id": "...", "error_type": "..."}` | An error occurred. See [Error codes](#error-codes) for the stable `code` enum and retry semantics.                                                                                                                                                                                    |
| `pong`          | `{}`                                                                                            | Response to ping action                                                                                                                                                                                                                                                               |
| `heartbeat`     | `{"ts": <unix-seconds float>}`                                                                  | Server-initiated keepalive emitted every 15 s while a turn is streaming. Treat as a no-op. Only emitted when the WebSocket is opened with `?heartbeat=1` — see [Server-side keepalive](#server-side-keepalive-opt-in).                                                                |
| `deleted`       | `{"message_id": "msg_..."}`                                                                     | A message was successfully deleted via `delete_message`                                                                                                                                                                                                                               |

### Example response stream

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_001"}}
{"event": "processing", "data": {"status": "thinking"}}
{"event": "processing", "data": {"status": "analyzing"}}
{"event": "visualization", "data": {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", "title": "Orders by Region", "data": {"values": [{"label": "North", "value": 412}, {"label": "South", "value": 289}, {"label": "East", "value": 531}], "x_axis_label": "Region", "y_axis_label": "Orders"}}}
{"event": "text_delta", "data": {"content": "There were 1,232 orders "}}
{"event": "text_delta", "data": {"content": "placed last month, with the East "}}
{"event": "text_delta", "data": {"content": "region leading at 531."}}
{"event": "message", "data": {"content": "There were 1,232 orders placed last month, with the East region leading at 531.", "id": "msg_xyz"}}
```

## Interrupting a Response

Send an interrupt action while the AI is streaming a response to cancel it:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"action": "interrupt"}
```

The server cancels the active stream and responds with:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "interrupted", "data": {"status": "interrupted"}}
```

The connection remains open for subsequent messages after an interrupt.

<Note>
  Any assistant text already streamed via `text_delta` frames **before** the interrupt is persisted as a regular assistant message and appears in subsequent `GET /v1/chats/{id}` responses. Your client's streaming buffer at the moment of interrupt is a faithful preview of what got saved.
</Note>

## Editing Messages

Edit a previously sent user message. This truncates all messages after the edited one and re-streams a new response.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "action": "edit_message",
  "message_id": "msg_abc123",
  "content": "Show me the top 10 instead",
  "integration_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

The server responds with `chat_metadata` followed by a new response stream, just like `send_message`.

## Deleting Messages

Delete a message and all subsequent messages in the conversation:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "action": "delete_message",
  "message_id": "msg_abc123"
}
```

Response:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "deleted", "data": {"message_id": "msg_abc123"}}
```

## Keep-Alive

Send periodic pings to keep the connection alive:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"action": "ping"}
```

Response:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "pong", "data": {}}
```

## Interrupting a Response

Send an `interrupt` action while the AI is streaming to cancel the in-progress response:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"action": "interrupt"}
```

The server cancels the stream and responds with:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "interrupted", "data": {"status": "interrupted"}}
```

If no response is currently streaming when `interrupt` is sent, an error event is returned instead.

<Note>
  For SSE connections, interruption is handled automatically when the HTTP connection is closed — no special action required.
</Note>

## Managing Messages

### Delete a message

Send a `delete_message` action to remove a message and all subsequent messages from the chat. This is irreversible.

<ParamField body="action" type="string" required>
  Must be `"delete_message"`.
</ParamField>

<ParamField body="message_id" type="string" required>
  ID of the message to delete. The target message and every message after it are removed.
</ParamField>

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"action": "delete_message", "message_id": "msg_xyz789"}
```

On success:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "deleted", "data": {"message_id": "msg_xyz789"}}
```

### Edit a message

Send an `edit_message` action to replace a user message's content. All messages after the edited message are removed and a fresh agent response is streamed back.

<Note>
  Only `user` messages can be edited. Attempting to edit an assistant message returns an error event.
</Note>

<ParamField body="action" type="string" required>
  Must be `"edit_message"`.
</ParamField>

<ParamField body="message_id" type="string" required>
  ID of the user message to edit.
</ParamField>

<ParamField body="content" type="string" required>
  The new message content to replace the original.
</ParamField>

<ParamField body="integration_id" type="string">
  ID of the database integration to query for the re-streamed response.
</ParamField>

<ParamField body="model" type="string">
  AI model to use for the re-streamed response.
</ParamField>

<ParamField body="include_tool_events" type="boolean">
  If `true`, internal tool events are included in the stream. Defaults to `false`.
</ParamField>

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "action": "edit_message",
  "message_id": "msg_xyz789",
  "content": "Show me the top 10 projects by budget instead",
  "integration_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

The server streams back a fresh response starting with `chat_metadata`:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_new001"}}
{"event": "processing", "data": {"status": "thinking"}}
{"event": "message", "data": {"content": "Here are the top 10 projects by budget.", "id": "msg_new002"}}
```

## Error Handling

Errors are delivered as events, not connection closures (unless authentication fails):

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event": "error",
  "data": {
    "error": "Datasource not found",
    "code": "INTEGRATION_NOT_FOUND",
    "retryable": false,
    "debug_id": "4a8c0ae6",
    "error_type": "ValueError"
  }
}
```

The `error` string is human-readable and may change. Branch on `code` for retry logic. The `debug_id` is an 8-hex-char correlation ID — include it when reporting issues to support.

### Connection close codes

| Code   | Reason                                                                                                                                                   |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `4001` | Authentication required or invalid token                                                                                                                 |
| `4002` | Invalid JSON message                                                                                                                                     |
| `4400` | JWT was passed in the URL (`?token=`). In practice the upgrade is rejected with `HTTP 403` before the WebSocket exists, so this code is rarely observed. |
| `4429` | Concurrent-connection cap (3 per JWT) reached                                                                                                            |
| `1011` | Internal server error                                                                                                                                    |

### Error codes

The `code` field on `error` events is a stable enum. Branch on it for retry logic rather than string-matching the `error` text. New codes may be added — treat unknown codes as non-retryable.

| Code                         | Retryable | Typical cause                                                                                                                                                                |
| ---------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INTEGRATION_NOT_FOUND`      | no        | Datasource ID does not exist for this organization                                                                                                                           |
| `INTEGRATION_NOT_CONFIGURED` | no        | Datasource is missing required configuration                                                                                                                                 |
| `DATABASE_CONNECTION_FAILED` | **yes**   | Network or connection-pool failure reaching the datasource                                                                                                                   |
| `DATABASE_QUERY_FAILED`      | no        | SQL syntax error, missing column, or constraint violation                                                                                                                    |
| `SERIALIZATION_ERROR`        | no        | A value in the payload is not JSON-serializable                                                                                                                              |
| `VALIDATION_ERROR`           | no        | Invalid input (message content, parameters, IDs)                                                                                                                             |
| `ACCESS_DENIED`              | no        | Caller lacks permission for the requested resource                                                                                                                           |
| `MISSING_CHAT_ID`            | no        | `send_message` or `edit_message` was sent without `chat_id`. Create one via [Create Chat](/api-reference/chats/create-chat).                                                 |
| `CHAT_NOT_FOUND`             | no        | Provided `chat_id` does not belong to the caller. The connection stays open.                                                                                                 |
| `WS_CONNECTION_CAP`          | no        | The 3-concurrent-connection cap for this JWT was reached; the connection is closed with code `4429`.                                                                         |
| `RATE_LIMIT`                 | **yes**   | Either the per-socket `send_message` frame throttle (10/min + 3/10s burst) or an upstream LLM rate limit. The payload includes `retry_after_seconds` for the frame throttle. |
| `MODEL_ERROR`                | **yes**   | Upstream LLM (Anthropic / OpenAI / Google) returned an error                                                                                                                 |
| `INTERNAL_ERROR`             | **yes**   | Unclassified failure — check `debug_id` and retry with backoff                                                                                                               |
| `AGENT_RETRY_EXHAUSTED`      | **yes**   | The model produced invalid tool arguments repeatedly and the agent exhausted its retry budget. Often resolved by rephrasing the request or retrying with a different model.  |

## Multi-Message Conversations

WebSocket connections persist, allowing you to send multiple messages in the same session:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
// First message — uses chat_id obtained from POST /v1/chats/create
{"action": "send_message", "message": "Show me sales data", "chat_id": "chat_abc123", "integration_id": "..."}

// Response confirms chat_id and persisted user message
{"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_001"}}
{"event": "visualization", "data": {"id": "kl78mn9012", "type": "table", "headers": ["Region", "Sales"], "rows": [["North", "1,234,000"], ["South", "987,500"], ["East", "2,100,000"]]}}
{"event": "message", "data": {"content": "Here's the sales data broken down by region.", "id": "msg_002"}}

// Follow-up message — reuse the same chat_id
{"action": "send_message", "message": "Show that as a bar chart", "integration_id": "...", "chat_id": "chat_abc123"}

// Response
{"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_003"}}
{"event": "processing", "data": {"status": "thinking"}}
{"event": "visualization", "data": {"id": "op90qr1234", "type": "chart", "chart_type": "bar", "title": "Sales by Region", "data": {"values": [{"label": "North", "value": 1234000}, {"label": "South", "value": 987500}, {"label": "East", "value": 2100000}], "x_axis_label": "Region", "y_axis_label": "Sales (AED)"}}}
{"event": "message", "data": {"content": "Here's the regional breakdown as a bar chart. The East region leads with AED 2.1M.", "id": "msg_002"}}
```

<RequestExample>
  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const ws = new WebSocket("wss://api.trellis.sh/v1/chats/ws");

  let streamingResponse = false;
  let streamingBuffer = "";
  let authenticated = false;

  ws.onopen = () => {
    // Browser clients cannot set headers on the WS upgrade —
    // authenticate via the first frame instead.
    ws.send(JSON.stringify({ action: "authenticate", token: "YOUR_TOKEN" }));
  };

  ws.onmessage = (msg) => {
    const event = JSON.parse(msg.data);

    switch (event.event) {
      case "authenticated":
        authenticated = true;
        // Now that we're authenticated, send a message.
        ws.send(JSON.stringify({
          action: "send_message",
          message: "Show me orders by region as a bar chart",
          chat_id: "chat_abc123",
          integration_id: "550e8400-e29b-41d4-a716-446655440000",
        }));
        break;
      case "chat_metadata":
        console.log("Chat:", event.data.id, "| User message:", event.data.user_message_id);
        streamingResponse = true;
        streamingBuffer = "";
        break;
      case "visualization":
        if (event.data.type === "chart") {
          renderChart(event.data.chart_type, event.data.title, event.data.data);
        } else if (event.data.type === "table") {
          renderTable(event.data.headers, event.data.rows);
        }
        break;
      case "text_delta":
        // Append the incremental chunk and re-render
        streamingBuffer += event.data.content;
        renderStreamingMessage(streamingBuffer);
        break;
      case "message":
        // Replace buffer with authoritative full content + id
        displayMessage(event.data.content, event.data.id);
        streamingBuffer = "";
        streamingResponse = false;
        break;
      case "interrupted":
        // Any text already in streamingBuffer has been persisted server-side
        console.log("Response was interrupted");
        streamingResponse = false;
        break;
      case "deleted":
        console.log("Message deleted:", event.data.message_id);
        break;
      case "error":
        showError(event.data.error);
        streamingResponse = false;
        break;
      case "heartbeat":
        // Server keepalive — no action required
        break;
    }
  };

  // Interrupt a streaming response
  function interruptResponse() {
    if (streamingResponse) {
      ws.send(JSON.stringify({ action: "interrupt" }));
    }
  }

  // Delete a message
  function deleteMessage(messageId) {
    ws.send(JSON.stringify({ action: "delete_message", message_id: messageId }));
  }

  // Edit a message
  function editMessage(messageId, newContent, integrationId) {
    ws.send(JSON.stringify({
      action: "edit_message",
      message_id: messageId,
      content: newContent,
      integration_id: integrationId,
    }));
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import asyncio
  import json
  import websockets

  async def main():
      uri = "wss://api.trellis.sh/v1/chats/ws"

      async with websockets.connect(
          uri,
          additional_headers=[("Authorization", "Bearer YOUR_TOKEN")],
      ) as ws:
          # Wait for auth confirmation frame
          auth = await ws.recv()
          print(json.loads(auth))

          # Send message — chat_id is required
          await ws.send(json.dumps({
              "action": "send_message",
              "message": "Show me orders by region as a bar chart",
              "chat_id": "chat_abc123",
              "integration_id": "550e8400-e29b-41d4-a716-446655440000"
          }))
          
          # Receive events
          streaming_buffer = ""
          while True:
              response = await ws.recv()
              event = json.loads(response)

              if event["event"] == "chat_metadata":
                  print("Chat:", event["data"]["id"], "| User message:", event["data"]["user_message_id"])
              elif event["event"] == "visualization":
                  data = event["data"]
                  if data["type"] == "chart":
                      print(f"Chart ({data['chart_type']}): {data.get('title')}")
                      print(f"  Values: {data['data']['values']}")
                  elif data["type"] == "table":
                      print(f"Table headers: {data['headers']}")
                      print(f"  Rows: {len(data['rows'])}")
              elif event["event"] == "text_delta":
                  # Append the incremental chunk
                  streaming_buffer += event["data"]["content"]
              elif event["event"] == "message":
                  # Authoritative full content
                  print("Response:", event["data"]["content"])
                  break
              elif event["event"] == "interrupted":
                  print("Response interrupted; partial so far:", streaming_buffer)
                  break
              elif event["event"] == "error":
                  print("Error:", event["data"]["error"])
                  break
              elif event["event"] == "heartbeat":
                  # Server keepalive — no action required
                  continue

  asyncio.run(main())
  ```
</RequestExample>

<ResponseExample>
  ```json Authenticated theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"event": "authenticated", "data": {"status": "ok"}}
  ```

  ```json Chat response with chart theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_001"}}
  {"event": "processing", "data": {"status": "thinking"}}
  {"event": "processing", "data": {"status": "analyzing"}}
  {"event": "visualization", "data": {"id": "ab12xy3456", "type": "chart", "chart_type": "bar", "title": "Orders by Region", "data": {"values": [{"label": "North", "value": 412}, {"label": "South", "value": 289}, {"label": "East", "value": 531}], "x_axis_label": "Region", "y_axis_label": "Orders"}}}
  {"event": "text_delta", "data": {"content": "There were 1,232 orders "}}
  {"event": "text_delta", "data": {"content": "placed last month, with the East "}}
  {"event": "text_delta", "data": {"content": "region leading at 531."}}
  {"event": "message", "data": {"content": "There were 1,232 orders placed last month, with the East region leading at 531.", "id": "msg_xyz789"}}
  ```

  ```json Interrupted response theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"event": "chat_metadata", "data": {"id": "chat_abc123", "user_message_id": "msg_001"}}
  {"event": "processing", "data": {"status": "thinking"}}
  {"event": "text_delta", "data": {"content": "Looking at the regional breakdown,"}}
  {"event": "text_delta", "data": {"content": " the East region shows"}}
  {"event": "interrupted", "data": {"status": "interrupted"}}
  ```

  ```json Message deleted theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"event": "deleted", "data": {"message_id": "msg_xyz789"}}
  ```

  ```json Error theme={"theme":{"light":"github-light","dark":"github-dark"}}
  {"event": "error", "data": {"error": "Datasource not found", "code": "INTEGRATION_NOT_FOUND", "retryable": false, "debug_id": "4a8c0ae6", "error_type": "ValueError"}}
  ```
</ResponseExample>
