# Responders

A responder answers turns. It owns one reply's execution: given the conversation so far and the new input, it produces one reply. The [session](https://june.dev/docs/chat/sessions.md) owns everything around it: the conversation's history, the journal and each turn's lifecycle.

## Choosing a responder

| You want                                       | Use                                                                   |
| ---------------------------------------------- | --------------------------------------------------------------------- |
| a normal model chat                            | [`createChat({ model, ... })`](#model-chat), a model-backed responder |
| a conversational front door to a June workflow | [`agentResponder()`](#chat-with-a-june-agent)                         |
| an existing service, or bespoke execution      | [your own `Responder`](#custom-responders)                            |

What any of them sees of earlier turns is decided by the chat's [context](https://june.dev/docs/chat/context.md), not by the responder.

## Model chat

`modelResponder({ model, instructions, ...settings })` answers turns with an AI SDK model. `createChat({ model, instructions })` is shorthand for `createChat({ responder: modelResponder({ model, instructions }) })`. June doesn't wrap providers: the AI SDK and the provider package own the model, the protocol, auth, retries and errors, and June passes your `LanguageModel` straight to `streamText`.

```ts
import { openai } from "june/sdk/openai";
import { createChat, fileStore, modelResponder } from "june/chat";

const chat = createChat({
  responder: modelResponder({
    model: openai("gpt-5-mini"),
    instructions: "You explain software briefly and plainly.",
    temperature: 0.2,
    maxOutputTokens: 800,
  }),
  store: fileStore(".june/chat"),
});
```

- **What's sent.** `instructions` as the system prompt, then the conversation as the chat's [context](https://june.dev/docs/chat/context.md) built it, then the new input. Instructions are configuration: they're sent with every turn but never stored in the journal, so changing them changes later turns without rewriting history.
- **Settings.** `maxOutputTokens`, `temperature`, `topP`, `topK`, `presencePenalty`, `frequencyPenalty`, `stopSequences`, `seed`, `maxRetries`, `headers` and `providerOptions` pass through to `streamText` unchanged.
- **Streaming.** Text arrives as `text-delta` events, and `turn.result.text` is the full reply. The completed turn records `metadata`: the `model` that answered (`provider` and `modelId`), token `usage`, and the `finishReason`. Raw provider requests and responses are never stored.
- **Errors.** A provider error, after the AI SDK's own retries, fails the turn with that error. Canceling a turn aborts the request.
- **No tools.** A model chat only talks. For tools, approvals and multi-step work, answer turns with a June workflow through [`agentResponder()`](#chat-with-a-june-agent), where tool calls and approvals go through the runtime you already use.

## Chat with a June agent

`agentResponder()` answers each turn by running a June workflow, the same `<Agent>`s, `<Step>`s and tools you'd run with `june run`:

```tsx
import { openai } from "june/sdk/openai";
import { tool, jsonSchema, type ModelMessage } from "ai";
import { Agent, Step, Tool } from "june";
import { agentResponder, createChat, fileStore } from "june/chat";

const deploy = tool({
  description: "Deploy to an environment",
  inputSchema: jsonSchema<{ env: string }>({
    type: "object",
    properties: { env: { type: "string" } },
    required: ["env"],
  }),
  needsApproval: true,
  execute: async ({ env }) => `deployed to ${env}`,
});

function Engineer({
  messages,
  input,
}: {
  messages: ModelMessage[];
  input: string;
}) {
  return (
    <Agent model={openai("gpt-5-mini")} instructions="You ship code.">
      <Tool name="deploy" value={deploy} />
      <Step
        id="reply"
        prompt={[...messages, { role: "user", content: input }]}
      />
    </Agent>
  );
}

export default createChat({
  responder: agentResponder({
    id: "engineer",
    version: "1",
    render: ({ messages, input }) => (
      <Engineer messages={messages} input={input} />
    ),
    reply: (result) => String(result.steps.reply),
  }),
  store: fileStore(".june/chat"),
});
```

- **`render`** builds the workflow for one turn. It gets the turn's `input`, the completed conversation as `messages`, and the session and turn ids. Nothing is injected into your agents: the conversation reaches the model only where you pass it, here as the step's prompt.
- **`reply`** picks the assistant's reply from the run's `RunResult`, usually one step's result. June never guesses which step is the answer, so a multi-step workflow stays deterministic. It must return a string.
- **`run`** passes `concurrency` and `timeout` to `start()`. `id` and `version` are recorded on each turn's provenance, with `kind: "agent"`.
- **`startRun`** launches each turn's workflow, `start()` by default. It gets the rendered element, the run options (with the turn's `signal` and the responder's `onUpdate` already set) and the turn's `sessionId` and `turnId`, and returns the `RunHandle`. A host passes its own to register and observe the exact run a turn launches, as June Studio does; compose your `onUpdate` with the responder's rather than replacing it, or the turn loses its `run-update` events.

[`src/examples/chat/agent.tsx`](https://github.com/johno/june/blob/main/packages/june/src/examples/chat/agent.tsx) is a two-step version, a draft and then a review whose result is the reply, that runs under `june chat`.

### Sessions and runs

A session is the long-lived conversation; a run is one execution of the workflow. One session creates many runs, one per turn. Every turn starts its own run with `start()`, so everything the runtime does, it does here unchanged: steps, `<Parallel>`, `<Catch>`, concurrency, timeouts. A failure no `<Catch>` handles fails the turn. `turn.result.run` holds the run's `RunResult`, with its steps, trace and usage, for as long as you keep the result.

Chat isn't a second workflow runtime. It doesn't run steps, schedule tools or keep its own trace: it starts a run per turn, passes the conversation in through `render`, and takes one string back through `reply`.

### What June preserves

A workflow turn is still a June run, so everything the runtime gives you is there in chat too.

#### Approvals

Tool calls that need approval go through the runtime's own approvals. The turn emits an `approval-request` event and lists the call in `turn.requests`; `turn.approve(id)` and `turn.deny(id, reason)` call the run's `approve` and `deny`. There's no second permission system:

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

declare const turn: Turn;

for await (const event of turn) {
  if (event.type === "approval-request") {
    const { id, toolName, input } = event.request;
    // Events are buffered, so skip a request the run has since withdrawn.
    if (!turn.requests.some((request) => request.id === id)) continue;
    if (toolName === "deploy" && (input as { env: string }).env !== "prod") {
      turn.approve(id);
    } else {
      turn.deny(id, "Only non-prod deploys from chat");
    }
  }
}
```

`june chat` and `june ask` ask on stderr, like `june run`; the [CLI reference](https://june.dev/docs/cli.md#june-chat) has the details.

#### Traces

Each commit of the run arrives as a `run-update` event with a snapshot of the trace tree, the same tree `onUpdate` gives `start()`: steps, their status, tool calls, partial text, model and usage. Use it to show progress. Reply text arrives in `turn.result` unless you name the step that writes it; see [Streaming the reply](#streaming-the-reply).

To show the trace in a UI, project each snapshot with `projectChatTrace`. It turns the tree into plain rows: steps with their tool calls, model calls and pending approvals, nested as the workflow nests them, each with a title that says its state ("Running research", "fetch failed") and a `status` of `running`, `error` or `pending`. Tool inputs and outputs become bounded summaries with credential-looking keys redacted, and a row's `id` stays the same from one snapshot to the next.

```ts
import { projectChatTrace, type ChatTraceRow, type Turn } from "june/chat";

declare const turn: Turn;

let rows: ChatTraceRow[] = [];
for await (const event of turn) {
  if (event.type === "run-update") {
    rows = projectChatTrace(event.tree, { requests: turn.requests });
  }
}
```

An `approval` row carries the `approvalId` to pass to `turn.approve` or `turn.deny`. The rows are React-free, so any UI can draw them; `june-design-system`'s Trace components fit them line for line. The runtime still owns status and lifecycle: the projection only reads the tree, and replacing your rows with each new snapshot's is all a UI has to do. June Studio's chat pane does exactly this, with a composition per row kind; see [Studio's chat traces](https://github.com/johno/june/blob/main/packages/studio/README.md#chat-traces).

#### Streaming the reply

June never guesses which step is the reply, since a workflow can stream several. Name it with `stream`, and set `stream` on that `<Step>`:

```tsx
import { Agent, Step } from "june";
import { agentResponder } from "june/chat";
import type { LanguageModel } from "ai";

declare const model: LanguageModel;

agentResponder({
  render: ({ messages, input }) => (
    <Agent model={model}>
      <Step id="reply" stream prompt={[...messages, { role: "user", content: input }]} />
    </Agent>
  ),
  reply: (result) => String(result.steps.reply),
  stream: { step: "reply" },
});
```

As the step writes, each new piece of its text arrives as a `text-delta` event, after the `run-update` that carried it, just like a model chat's, so `june chat`, `june ask` and June Studio show it as it's written. `reply` still picks the final text, and it must continue what streamed: anything left over arrives as one last `text-delta`, so the deltas always add up to `turn.result.text`, and a reply that contradicts the streamed text fails the turn rather than showing one answer and keeping another. Canceling stops the deltas with the run.

Stream a step that doesn't call tools. A step's `partialText` holds the text of every model call it makes, but its result is the last call's text, so a reply step that says "Let me check" before a tool call would stream text its result doesn't keep, and the turn fails. Put tool work in earlier steps and stream the step that writes the answer.

#### Canceling

`turn.cancel()` aborts the run, which aborts every step in flight, and records the turn as canceled.

#### What's stored

The journal keeps the reply and small provenance only: `kind: "agent"`, your `id` and `version`, the model when every step used the same one, and the run's total usage. The trace, tool inputs and tool outputs are never written, since they can be large or sensitive. They're available live, in `run-update` events and `turn.result.run`, and gone when your process lets go of them. If you need them later, record them yourself from `turn.result.run`.

## The contract

A responder has a `respond()` method. For each turn it gets:

- `input`: the user's input, a string;
- `messages`: the conversation so far as AI SDK `ModelMessage`s, as the chat's [context](https://june.dev/docs/chat/context.md) built it;
- `signal`: an `AbortSignal` that aborts when the turn is canceled;
- `sessionId` and `turnId`.

It returns an execution right away:

- `result`, a promise of the reply's `text` and optional `metadata` (`model`, `usage`, `finishReason`). June keeps only those fields.
- `events`, optional live events: `text-delta`, `run-update` and `approval-request`.
- `requests`, `approve(id)` and `deny(id, reason)`, when it asks for tool approvals.
- `steer(input)`, when it can take more input while it runs. See [Steer](https://june.dev/docs/chat/turns.md#steer).

The responder can also set `kind`, `id` and `version`, which every turn it answers records as [provenance](#provenance).

Both built-in responders implement this contract, and so does [yours](#custom-responders).

## Custom responders

A responder can be anything that returns an execution:

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

declare const store: ChatStore;

const shout: Responder = {
  respond({ input, messages, signal }) {
    const text = `${input.toUpperCase()} (${messages.length / 2} earlier turns)`;

    async function* events() {
      for (const word of text.split(" ")) {
        signal.throwIfAborted();
        yield { type: "text-delta" as const, text: `${word} ` };
      }
    }

    return {
      events: events(),
      result: Promise.resolve({ text }),
    };
  },
};

const session = await createChat({ responder: shout, store }).createSession();
console.log((await session.send("hi").result).text);
```

- `result` resolves with the reply's `text` and optional `metadata` (`model`, `usage`, `finishReason`). June keeps only those fields.
- Set `kind`, `id` and `version` on the responder to record which one answered each turn.
- `events` is optional. June reads it once, in order, as the turn runs. The turn ends once `result` resolves and `events` ends, so end the stream when the reply is done.
- Honor `signal` to stop work when the turn is canceled.
- A responder that asks for tool approvals exposes `requests`, `approve(id)` and `deny(id, reason)`; the turn's methods of the same names pass straight through.
- A rejected `result`, or an `events` stream that throws, fails the turn.

## Provenance

Every completed turn records small, JSON-safe metadata about how it was answered:

- `responder`: the responder's `kind` (`"model"` for model chats, `"agent"` for workflow chats, `"custom"` by default), and its `id` and `version` when it sets them;
- `model`: the `provider` and `modelId`, when a model answered;
- `usage` and `finishReason`, when the responder reports them.

That's all. API keys, auth headers, instructions, raw provider requests and responses, and full runtime traces are never written to the journal.

Provenance is what lets feedback be traced back to what produced the reply. Set `version` whenever what answers changes, a new prompt, model or workflow, so turns answered before and after the change stay distinguishable. The [Copy Agent](https://github.com/johno/june/blob/main/apps/copy-agent/README.md) records the active agent's id as the version, so every reply says which agent wrote it. Datasets and eval cases built from sessions carry it in their [metadata](https://june.dev/docs/chat/learning.md#provenance).
