---
description: "The chat session as a pure state machine: the messages, whether a run is streaming, and the last error."
---

# `sessionReducer`

The chat session as a pure state machine: the messages, whether a run is streaming, and the last error. Use it when you manage a conversation yourself or test one in Node; [`useAgentSession`](/docs/reference/chat/use-agent-session) feeds it adapter stream parts and core agent events. `resumable` decides what Retry sends.

## Import

```ts
import { resumable, sessionReducer } from "@webmcp-agents/chat";
import type {
  SessionAction,
  SessionError,
  SessionState,
} from "@webmcp-agents/chat";
```

## Example

Play what an adapter streams into a session, then ask what Retry would send after the run failed.

```ts title="session.ts"
import { resumable, sessionReducer } from "@webmcp-agents/chat";
import type {
  AgentStreamPart,
  SessionState,
} from "@webmcp-agents/chat";

let state: SessionState = { messages: [], status: "idle" };

// A user message, then what an adapter streams back for it.
state = sessionReducer(state, {
  type: "send",
  id: "u-1",
  text: "Add two mugs to the cart",
});
const parts: AgentStreamPart[] = [
  { type: "message-start", messageId: "a-1" },
  {
    type: "tool-call",
    messageId: "a-1",
    id: "call-1",
    name: "add_to_cart",
    input: { slug: "mug", qty: 2 },
  },
  {
    type: "tool-result",
    id: "call-1",
    output: { content: [{ type: "text", text: "Added." }] },
  },
  {
    type: "error",
    error: "Couldn't reach the assistant.",
    code: "network",
  },
];
for (const part of parts) {
  state = sessionReducer(state, { type: "part", part });
}

console.log(state.status); // "error"
// What Retry sends: through the finished add_to_cart call.
console.log(resumable(state.messages).length); // 2
```

## Options

`sessionReducer(state, action)` is a reducer, so `useReducer(sessionReducer, { messages: [], status: "idle" })` works as is. `resumable(messages)` takes only the messages.

| Name | Type | Default | Description |
|---|---|---|---|
| `state` | `SessionState` | required | The session so far: `messages: AgentMessage[]`, `status: "idle" \| "streaming" \| "error"` and `error?: SessionError`. A new conversation is `{ messages: [], status: "idle" }`. |
| `action` | `SessionAction` | required | What happened: one of the actions below. |

A `SessionError` is `{ message: string; code?: "chat_disabled" | "rate_limited" | "bad_request" | "network" }`: the failure's text and a code the widget can explain.

### Actions

| Action | Effect |
|---|---|
| `{ type: "send", id, text }` | Appends a user message with one text part; `status` becomes `streaming`; clears `error`. |
| `{ type: "part", part, within? }` | Applies one [`AgentStreamPart`](/docs/reference/chat/adapter-types#agentstreampart) (below). With `within`, the part belongs to a sub-agent's run behind the `delegate_to_*` call with that id: it is applied to that call's nested messages (`AgentPart.agent.messages`) by the same state machine. A nested `finish` or `error` ends only the nested run; the parent stays streaming. |
| `{ type: "toolPhase", name, state, prompt? }` | A core agent event about a running tool: sets the newest open call of that tool name to `state` (`"running"` or `"awaiting-confirmation"`) and `prompt`. Calls run one at a time, so the name is enough. Reaches nested calls too. |
| `{ type: "stop" }` | `status` becomes `idle`; every `running` or `awaiting-confirmation` call, nested ones included, becomes `cancelled`. |
| `{ type: "retry" }` | No-op without a user message. Otherwise `status` becomes `streaming` and the messages become `resumable(messages)`. |
| `{ type: "reset" }` | Back to the empty session. |

### Stream parts

| Part | Effect |
|---|---|
| `message-start` | Appends an empty assistant message. |
| `text-delta` | Appends to the message's last text part, or adds one. |
| `reasoning` | Adds a reasoning part (kept for the next request, not rendered). |
| `tool-call` | Adds a tool part in state `running`. |
| `tool-result` | That call becomes `done` with its `output`. |
| `tool-error` | That call becomes `cancelled` when [`isCancelled(error)`](/docs/reference/chat/call-model-context-tool#cancelled-and-iscancelled), else `error`. |
| `step-finish` | No change. |
| `finish` | `status` becomes `idle`; with `reason: "aborted"`, open calls become `cancelled`. |
| `error` | `status` becomes `error`, `error` is set, open calls become `cancelled`. |

## Returns

`sessionReducer` returns the next `SessionState` (the same object when a `toolPhase` changed nothing).

### `resumable`

`resumable(messages: AgentMessage[]): AgentMessage[]` returns a new array: what Retry sends. It is the conversation through the last tool call that finished (`done` or `error`) after the last user message. What came after it goes: unfinished calls (dropped, not reported to the model as "Cancelled by the user") and a reply cut short. With no finished call, it returns the messages through the last user message, so the turn starts over. Without any user message it returns `messages` unchanged.

## Notes

- Pure and DOM-free: safe to test in Node.
- `useAgentSession` applies `stop` to a conversation loaded from sessionStorage, so a reload never shows a call as still running.

## See also

- [Add the chat](/docs/build/add-the-chat), the page that teaches the chat
- [`useAgentSession`](/docs/reference/chat/use-agent-session)
- [Adapter types](/docs/reference/chat/adapter-types)
- [Tool error codes](/docs/reference/core/tool-error)
