# Turns and streaming

A turn is one user input, answered once. It streams live events while it runs, settles with a reply or an error, and leaves a few entries in the session's journal.

```text
input ─▶ accepted ─▶ responder starts ─▶ live events stream ─▶ reply or error settles ─▶ journal records the outcome
```

The responder that answers is covered in [Responders](https://june.dev/docs/chat/responders.md), and the journal in [Sessions and stores](https://june.dev/docs/chat/sessions.md).

## A turn

```ts
import { createChat, type ChatStore, type Responder } from "june/chat";

declare const responder: Responder;
declare const store: ChatStore;

const chat = createChat({ responder, store });
const session = await chat.createSession();

const { text } = await session.send("What is June?").result;
```

`send()` starts the turn right away, exactly once. Awaiting `turn.result` gives you the reply without reading any events; the turn runs either way.

Turns in a session run one at a time. A turn sent while another is running waits for it, so it always sees the previous reply:

```ts
import type { Session } from "june/chat";

declare const session: Session;

await session.send("Design an auth API.").result;
await session.send("Make it smaller.").result;
```

## Streaming

A turn is also an async iterable of live events, for showing progress while it runs:

```ts
import type { Session } from "june/chat";

declare const session: Session;

const turn = session.send("What is June?");

for await (const event of turn) {
  if (event.type === "text-delta") process.stdout.write(event.text);
}

const { text } = await turn.result;
```

Events are how a caller watches a turn; the journal is what survives it. Events are never stored, so a reply that streamed and was then canceled is visible in the events you received but isn't recorded. Iterate the turn, await `turn.result`, or both. A turn's events can be read by one iterator, which gets every event from the start even if it begins late.

A turn yields `ChatEvent`s, in order:

- **`turn-start`**, with the `sessionId` and `turnId`, once the input is accepted.
- **`text-delta`**, with a piece of the reply's `text`, as a responder streams it. [Model chats](https://june.dev/docs/chat/responders.md#model-chat) stream every reply this way.
- **`run-update`**, with a snapshot of the trace `tree`, each time a [workflow turn's](https://june.dev/docs/chat/responders.md#chat-with-a-june-agent) run commits.
- **`approval-request`**, with the tool call's `request`, when a workflow step asks to run a tool that needs approval.
- **`turn-end`**, with the turn as it now stands, once it ends: completed, failed, canceled or interrupted.

New event types may be added, so ignore the ones you don't handle.

`turn.result` resolves with the recorded reply: `sessionId`, `turnId`, `text`, the turn's `metadata`, and, for workflow turns, the `run` behind it.

## Cancel

Canceling stops the current turn; it has no effect on later ones.

`turn.cancel(reason)` aborts the responder: its `signal` aborts, a model request is aborted, and a workflow's run is aborted with every step in flight. The turn ends as `canceled` right away, even if the responder ignores the signal, and `turn.result` rejects with `reason`, or an `AbortError` without one. Any text that streamed before the cancel stays visible in the live events you already received, but it's never recorded as a reply: a canceled turn has no `output`, isn't in `messages()`, and isn't part of later context.

A turn canceled before it's accepted writes nothing. Once the responder has returned its reply and June has started recording it, canceling has no effect and the turn completes.

## Steer

Steering adds input to the current turn while it's being answered, say a correction mid-way through a long task. It's recorded on that turn, so if the turn completes, later turns see it too as part of the conversation.

```ts
import type { Session } from "june/chat";

declare const session: Session;

const turn = session.send("Draft the migration plan.");
await turn.accepted;
await session.steer("Assume we can't take downtime.");
await turn.result;
```

Steering is a capability, not a promise. It only works when the running turn's responder implements `steer()` on its execution, and `session.steer()` rejects when no turn is running or its responder can't steer. The built-in model and agent responders don't: neither the AI SDK nor June's runtime has a safe point to add input to a call that's already been sent, and June won't pretend by rewriting a prompt after the fact.

A custom responder accepts steering by implementing `steer(input)` on its execution, resolving once it has taken the input into account or throwing to refuse it. Only input the responder accepts before its reply is in is recorded: a `turn.steered` entry on the same turn, written before the turn's outcome, so the journal shows why the reply changed. Steering that arrives after the reply, or after a cancel, is refused. `session.steer()` waits for the turn's responder to start, so it works right after `turn.accepted`. It belongs to that turn rather than becoming a turn of its own: the turn's `steering` in `turns()` lists it, and in `messages()` and later context it appears as an extra user message between the turn's input and its reply.

## When a turn doesn't complete

| Status        | What happened                                                 | What the caller sees                                                                                              | What the journal records                | Retry?          |
| ------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------- |
| `failed`      | the responder, or a workflow step no `<Catch>` handled, threw | `turn.result` rejects with the error                                                                              | `turn.failed`, with the error           | yes, a new turn |
| `canceled`    | someone called `turn.cancel()`                                | `turn.result` rejects with the reason                                                                             | `turn.canceled`, with the reason        | yes, a new turn |
| `interrupted` | the process stopped, or the outcome couldn't be written       | the turn, in this process or when a session is reopened; `turn.result` rejects if the outcome couldn't be written | nothing: `turn.accepted` has no outcome | yes, a new turn |

None of them is a reply, so none reaches later context. Each stays in `turns()` and the journal, so you can see what was asked and why it wasn't answered. Side effects, such as a workflow's tool calls, come from the [responder](https://june.dev/docs/chat/responders.md), and may have happened before any of these.

## Retry

> **A retry runs the work again.** Model calls, workflow steps and tools all run again from the start, side effects included. June never retries on its own.

`session.retry(turnId)` answers a failed, canceled or interrupted turn's input again, as a new turn:

```ts
import type { Session } from "june/chat";

declare const session: Session;

for (const turn of session.turns()) {
  if (turn.status === "interrupted") {
    const retry = session.retry(turn.id);
    console.log((await retry.result).text);
  }
}
```

- It starts from the beginning: a new turn, a new model call or a new workflow run. Nothing resumes where the original stopped, because June doesn't checkpoint turns in flight.
- The original turn stays as it was, with its own entries. The new turn's `turn.accepted` entry records `retryOf`, and `turns()` shows it as `retryOf` on the new turn.
- Completed turns can't be retried; send the input again if you want another answer.
- Each call is a new attempt: retrying the same turn twice makes two retries, both linked to it. `retry()` can only tell a turn is still running if this `Session` object is running it: a turn another process is still answering reads as `interrupted` here, so don't retry it.

### Side effects happen again

June Chat doesn't guarantee exactly-once anything. When you retry a turn:

- its model calls run again, and are billed again;
- a workflow's steps and tools run again from the start, including ones that already ran before the process stopped: a deploy, an email, a payment;
- an interrupted turn may have done any part of its work before it stopped, and June can't tell how much.

So retry deliberately. Make tools that change the world idempotent, for example by passing an idempotency key derived from the turn id, or ask before retrying a turn whose tools have side effects. Interrupted agent turns are never retried for you.
