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

# Streaming Events

A streamed turn is a `text/event-stream`. The SDK turns each frame into a typed `TurnEvent`. The stream ends after a terminal event and `[DONE]`.

For a shopping reply you usually need three events: save the conversation id, append text as it arrives, then take the finished message (including product cards).

| Event            | When                        | What to do                                             |
| ---------------- | --------------------------- | ------------------------------------------------------ |
| `turn.started`   | First, once                 | Save `conversation_id`, `turn_id`, `message_id`.       |
| `part.started`   | A text or tool block begins | Add a block at `part_index`.                           |
| `part.delta`     | Text is generating          | Append `delta` to the text at `part_index`.            |
| `part.completed` | A block is done             | Replace the block at `part_index` with the final part. |
| `turn.completed` | Success, once               | Full `message` plus `usage`.                           |
| `error`          | The turn failed             | Read `code`, `message`, `retryable`.                   |

`part_index` is the position in the final message. Order inside one reply is stable. See [messages and parts](/conversations/messages-and-parts) for what to render.

<CodeGroup>
  ```typescript TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  for await (const event of stream) {
    switch (event.type) {
      case "turn.started":
        conversationId = event.conversation_id;
        break;
      case "part.started":
      case "part.completed":
        parts[event.part_index] = event.part;
        break;
      case "part.delta":
        text += event.delta;
        break;
      case "turn.completed":
        done(event.message);
        break;
      case "error":
        if (event.retryable) retry();
        else showError(event.message);
        break;
    }
  }
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  for event in stream:
      if event.type == "turn.started":
          conversation_id = event.conversation_id
      elif event.type in ("part.started", "part.completed"):
          parts[event.part_index] = event.part
      elif event.type == "part.delta":
          text += event.delta
      elif event.type == "turn.completed":
          done(event.message)
      elif event.type == "error":
          if event.retryable:
              retry()
          else:
              show_error(event.message)
  ```

  ```text SSE theme={"theme":{"light":"github-light","dark":"github-dark"}}
  event: turn.started
  data: {"type":"turn.started","conversation_id":"conv_123","turn_id":"turn_…","message_id":"msg_…"}

  event: part.delta
  data: {"type":"part.delta","part_index":0,"delta":"Let me pull up some options."}

  event: part.completed
  data: {"type":"part.completed","part_index":1,"part":{"type":"tool","tool_name":"search_products",…}}

  event: turn.completed
  data: {"type":"turn.completed","message":{"parts":[ … ]},"usage":{"credits_charged":2,"searches_run":1}}
  ```
</CodeGroup>

The stream sends a heartbeat about every 15 seconds — silence between tokens is not a dead connection. A turn that fails **before** it starts returns a normal HTTP error, not an `error` event. See [errors](/conversations/errors).
