hydrateMessages, chat.history, and custom actions.
Data model
The standard approach (used by ChatGPT, Open WebUI, LibreChat, and others) stores messages as a tree with parent pointers:parentId pointers to the root, then reversing:
Backend setup
Store: tree operations
Define helpers that read and write the node tree. Adapt to your database:Agent: hydration + actions
Frontend
Sending actions
Wire up edit, undo, and branch switching to the transport:Branch navigation
To show the< 2/3 > sibling switcher, query the tree for siblings at each fork point. This is a frontend concern — the backend exposes the data, the UI navigates it.
The sibling data (which messages share the same parent) needs to come from your database — query it when loading the chat or include it as client data. The agent only returns the active branch via
hydrateMessages.How it works
| Operation | What happens |
|---|---|
| Send message | hydrateMessages appends the new message as a child of the current leaf, returns the active path |
| Edit message | onAction creates a sibling node with the same parent. The new node becomes the latest leaf, so hydrateMessages resolves through it. LLM responds to the edited history |
| Regenerate | Same as edit — create a new assistant sibling. The AI SDK’s regenerate() handles this via trigger: "regenerate-message" |
| Undo | onAction removes the last two nodes. chat.history.set() updates the accumulator. LLM responds to the earlier state |
| Switch branch | onAction updates which leaf is “active”. hydrateMessages loads the new path. LLM responds to the switched context |
Design notes
- Messages are immutable — edits create siblings, not mutations. This preserves full history for analytics and auditing.
- The tree lives in your database — the agent loads a linear path from it via
hydrateMessages. The agent itself doesn’t know about the tree structure. hydrateMessages+onAction+chat.historyare the three primitives. Hydration loads the active path, actions modify the tree, andchat.history.set()syncs the accumulator after tree modifications.- Frontend owns navigation — the
< 2/3 >UI, sibling queries, and branch switching triggers are client-side concerns. The backend just processes actions and returns responses.
See also
hydrateMessages— backend-controlled message history- Actions — custom actions with
actionSchemaandonAction chat.history— imperative history mutations- Database persistence — basic persistence pattern (linear)

