> Markdown version of [Conversation History & Session Persistence](https://vaadin.com/docs/latest/flow/ai-support/conversation-history). Section index: [llms.txt](https://vaadin.com/docs/latest/flow/llms.txt)

# Conversation History & Session Persistence

The orchestrator tracks conversation history internally. Use `getHistory()` to get a snapshot and `withHistory()` to restore it.

## <a id="persisting-history"></a>Persisting History

Use `ResponseListener` to persist conversation state after each exchange:

```java
var orchestrator = AIOrchestrator
        .builder(provider, systemPrompt)
        .withMessageList(messageList)
        .withInput(messageInput)
        .withResponseListener(event -> {
            if (event.getError().isPresent()) {
                return;
            }
            var history = orchestrator.getHistory();
            saveToDatabase(sessionId, history);
        })
        .build();
```

`saveToDatabase` is a placeholder for application code. Write the `ChatMessage` list to whichever storage the application uses, so the history can be loaded again later.

The listener fires once per turn for both successful and failed exchanges — check `event.getError()` to tell them apart. It is not called when history is restored via `withHistory()`, and empty responses (turns where the model emitted only tool calls or no visible content) are not appended to history. The event also carries the finish reason and token usage (new in undefined) of the turn — see [Response Metadata](https://vaadin.com/docs/latest/flow/ai-support/listeners.md#response-metadata).

> **Important: UI Updates from ResponseListener**
>
> With a streaming provider, or when [background execution](https://vaadin.com/docs/latest/flow/ai-support/llm-providers.md#background-execution) is enabled, the listener runs on a background thread, where blocking I/O (such as database writes) is safe. With a synchronous provider in the default execution mode, the whole turn — this listener included — runs in the request that triggered the prompt, so blocking work extends that request. To update Vaadin UI components from this callback, wrap the update in `ui.access()`.

## <a id="restoring-history"></a>Restoring History

Restore a previously saved conversation when creating a new orchestrator. The following example pre-populates the message list with a saved exchange:

**Flow** — `AIOrchestratorHistory.java`

```java
MessageList messageList = new MessageList();
MessageInput messageInput = new MessageInput();

LLMProvider provider = getLLMProvider();

List<ChatMessage> savedHistory = getHistory();
Map<String, List<AIAttachment>> savedAttachments = getSavedAttachments();

AIOrchestrator.builder(provider, "You are a helpful assistant.")
        .withMessageList(messageList).withInput(messageInput)
        .withHistory(savedHistory, savedAttachments).build();

add(messageList, messageInput);
```

This replays messages into the LLM provider’s memory, populates the Message List UI, and rebuilds internal mappings for attachment click handling. A provider whose memory lives outside the orchestrator, such as a `SpringAILLMProvider` created from a `ChatClient`, ignores the provider part of the restore; see [LLM Providers](https://vaadin.com/docs/latest/flow/ai-support/llm-providers.md).

> **Note:** `getHistory()` returns a point-in-time snapshot. If called while a streaming response is in progress, the snapshot may include the user message without its corresponding assistant response. Use `ResponseListener` to capture history at the right time.

## <a id="data-model"></a>Data Model

Conversation history is represented as a list of `ChatMessage` records with four fields:

- `role()` — `USER` or `ASSISTANT`

- `content()` — the text content

- `messageId()` — UUID assigned to user messages; `null` for assistant messages. Used as the correlation key for attachments.

- `time()` — timestamp; may be `null`

File attachments are represented as `AIAttachment` records with `name()`, `mimeType()`, and `data()` (raw bytes). Attachments are not stored in `ChatMessage` — they are correlated separately via the message ID.

## <a id="session-persistence"></a>Session Persistence

`AIOrchestrator` is serializable. Conversation history, UI component bindings, listener registrations, and display names are preserved across serialization. However, the LLM provider, tool objects, and the controller are transient — they are not serialized and must be restored after deserialization.

### <a id="reconnecting-after-deserialization"></a>Reconnecting after Deserialization

After the session is restored, call `reconnect(LLMProvider)` to supply a new provider and optionally restore tools and file attachments. The `apply()` call replays the existing conversation history onto the new provider so that it has full context for subsequent prompts. The UI is not modified — the message list, input, and file receiver components retain their state automatically.

```java
orchestrator.reconnect(provider)
        .withTools(tools)                   // optional
        .withController(controller)         // optional
        .withAttachments(savedAttachments)  // optional
        .apply();
```

Provider settings such as the streaming mode and [background execution](https://vaadin.com/docs/latest/flow/ai-support/llm-providers.md#background-execution) live on the provider and are not serialized; apply them to the new provider instance before passing it to `reconnect()`.

> **Note: Controller Reattachment**
>
> `AIController` instances, including `GridAIController`, `ChartAIController`, and `FormAIController`, are not serialized with the orchestrator. Create a new controller after session restore and pass it to `withController()` on the reconnector. The grid and chart controllers each have their own state capture and restoration API. The form controller has no separate state object, because the form fields themselves are persisted with the `VaadinSession`. See [AI-Powered Grid](https://vaadin.com/docs/latest/flow/ai-support/ai-powered-grid.md), [AI-Powered Chart](https://vaadin.com/docs/latest/flow/ai-support/ai-powered-chart.md), and [AI Form Filler](https://vaadin.com/docs/latest/flow/ai-support/ai-powered-form.md) for the specifics.

If `prompt()` is called before reconnecting, it throws an `IllegalStateException`.

> **Note: LLMProvider Implementations**
>
> `LLMProvider` implementations are not serializable. Always create a new provider instance after session restore and pass it to `reconnect()`.
