hashbrown

Message History

Persist, restore, and prune the messages your chat sends to the model.

Hashbrown keeps chat state in a messages array. Each turn appends user, assistant, tool, and error messages to that array, and each request sends the current history so the model has conversational context.

Use message history when you want to:

  1. Restore a chat after a page refresh
  2. Persist conversations to local storage or your backend
  3. Remove or summarize older turns before the next request
  4. Let users delete or retry part of a conversation

Public History and AG-UI Checkpoints

chat.value() remains Hashbrown's opinionated public view of the conversation. When an AG-UI agent sends a message checkpoint, Hashbrown retains the complete canonical AG-UI history internally and uses it for subsequent requests. Stable message IDs, metadata, and roles that do not have a Hashbrown public representation remain in that internal checkpoint.

The public view continues to use Hashbrown's existing message shapes: user and assistant messages remain familiar, reasoning is correlated with assistant messages, and tool results settle their corresponding tool calls. Canonical system, developer, and activity messages are not added to chat.value(). The internal canonical history is not exposed as a separate public API.

Shared state is synchronized independently from message history. See Shared Agent State when your application and agent need to exchange structured state snapshots and deltas.


Initialize with Stored Messages

Pass messages when you create the resource. Hashbrown uses this array as the initial chat history.

support-chat.component.ts
import { Component, effect } from '@angular/core';
import { type Chat } from '@hashbrownai/core';
import { chatResource } from '@hashbrownai/angular';

const STORAGE_KEY = 'support-chat-messages';

function loadMessages(): Chat.Message<string, Chat.AnyTool>[] {
  if (typeof window === 'undefined') return [];

  const stored = window.localStorage.getItem(STORAGE_KEY);
  if (!stored) return [];

  try {
    return JSON.parse(stored);
  } catch {
    return [];
  }
}

@Component({
  selector: 'app-support-chat',
  standalone: true,
  template: `
    @for (message of chat.value(); track $index) {
      <p>{{ message.content }}</p>
    }
  `,
})
export class SupportChatComponent {
  chat = chatResource({
    system: 'You are a helpful support assistant.',
    messages: loadMessages(),
  });

  constructor() {
    effect(() => {
      if (typeof window === 'undefined') return;

      window.localStorage.setItem(
        STORAGE_KEY,
        JSON.stringify(this.chat.value()),
      );
    });
  }
}

Keep the stored value as plain JSON. Do not store functions, component references, Promise values, or provider-specific request objects in message history.


Replace History with setMessages

Use setMessages when your app needs to replace the current history. Always pass a new array instead of mutating chat.value() in place.

support-chat.component.ts
keepRecentMessages() {
  this.chat.setMessages(this.chat.value().slice(-8));
}

clearHistory() {
  this.chat.setMessages([]);
}

removeLastAssistantMessage() {
  const messages = this.chat.value();
  const index = messages.findLastIndex(
    (message) => message.role === 'assistant',
  );

  if (index === -1) return;

  this.chat.setMessages(messages.filter((_, i) => i !== index));
}

This is useful for "clear chat", "delete message", "retry from here", and "trim old turns" controls.


Order of Operations

Hashbrown creates one chat instance for the resource and keeps its message history until you intentionally replace it. The messages option seeds the initial history only; changing the value you passed to messages later does not reset an active chat.

Reactive options such as system, apiUrl, threadId, tools, and transport are applied to future requests. Updating those options does not append a message, clear history, or resend the conversation by itself.

Use sendMessage to add a new turn, setMessages to replace history, and reload to remove the last assistant message before retrying. For completion resources, input is different from chat history: changing input synchronizes the backing single user message for the next completion.

Put durable behavior and constraints in system. Put conversational facts, prior turns, summaries, and user-visible conversation state in message history.


Summarize Older Turns

For long chats, keep recent messages verbatim and replace older turns with a summary. This preserves context while reducing payload size.

support-chat.component.ts
async compactHistory() {
  const messages = this.chat.value();
  const recentMessages = messages.slice(-8);
  const olderMessages = messages.slice(0, -8);

  if (olderMessages.length === 0) return;

  const summary = await summarizeMessages(olderMessages);

  this.chat.setMessages([
    {
      role: 'user',
      content: `Previous conversation summary:\n${summary}`,
    },
    ...recentMessages,
  ]);
}

Put durable application rules in system. Use summarized messages for conversational facts, user preferences, and prior decisions.


Client History vs. Threads

Client-managed history sends the message array with each request. It is simple and works well for local storage, short conversations, and user-controlled editing.

Use Threads when you want the server to own persistence, rehydrate by threadId, and send only new message deltas after a conversation is established.


Next Steps

Message HistoryPublic History and AG-UI CheckpointsInitialize with Stored MessagesReplace History with setMessagesOrder of OperationsSummarize Older TurnsClient History vs. ThreadsNext Steps