Storage overview
AgentSquad separates memory from routing. Each agent receives its own scoped history; the Orchestrator also reads a merged cross-agent view so the Classifier can see the full conversation when selecting the next agent.
The ChatStorage protocol
Section titled “The ChatStorage protocol”public protocol ChatStorage: Sendable { func fetch( userId: String, sessionId: String, agentId: String, maxMessages: Int? ) async throws -> [ConversationMessage]
func save( _ message: ConversationMessage, userId: String, sessionId: String, agentId: String, maxMessages: Int? ) async throws
func saveMessages( _ messages: [ConversationMessage], userId: String, sessionId: String, agentId: String, maxMessages: Int? ) async throws
/// Merged, timestamp-ordered history across all agents; assistant messages `[agentId]`-prefixed. func fetchAllChats(userId: String, sessionId: String) async throws -> [ConversationMessage]}Conversation scope
Section titled “Conversation scope”fetch / save / saveMessages are keyed by a (userId, sessionId, agentId) triple and feed the selected agent. fetchAllChats returns a merged, timestamp-ordered view across all agents for the session; assistant messages are prefixed with [agentId] so the Classifier can attribute them.
See Messages & events for the ConversationMessage type.
maxMessages window
Section titled “maxMessages window”public enum ChatStorageDefaults { public static let maxMessages = 100 // counts messages, not pairs}maxMessages counts individual messages, not user/assistant pairs. The framework rounds any budget down to an even number so a user/assistant pair is never split. Pass nil to keep an unbounded history.
Default implementations on ChatStorage
Section titled “Default implementations on ChatStorage”Two helpers are provided as protocol extensions — call them in your own store, do not reimplement them:
trimToEvenPairs(_:maxMessages:)— trims to the most recentmaxMessages, rounding down to even.isConsecutiveSameRole(_:_:)— returnstruewhen a new message repeats the last stored role; stores drop such saves.
store: nil
Section titled “store: nil”Passing store: nil to the Orchestrator or voice assistant disables persistence entirely — every session starts fresh. Useful during development or for ephemeral flows.
Choosing a store
Section titled “Choosing a store”| Store | Platform | Persistence | Multi-agent [agentId] attribution |
|---|---|---|---|
InMemoryChatStorage | iOS 16+ | Session only | No |
FileChatStorage | iOS 16+ | Disk (JSON) | Yes |
DeviceChatStorage | iOS 17+ / macOS 14+ | Disk (SwiftData) | Yes |
store: nil | Any | None | — |
For voice sessions the same stores apply — pass the chosen instance to the voice assistant’s store: parameter exactly as you would for a text Orchestrator.
Any of them can be wrapped in TransformingChatStorage to scrub PII or reshape messages before they are persisted, or in SummarizingChatStorage to automatically compress long histories so agent context stays small.
Built-in stores
Section titled “Built-in stores”- InMemoryChatStorage — non-persistent, seedable, iOS 16+
- FileChatStorage — JSON files, iOS 16+
- DeviceChatStorage — SwiftData, iOS 17+ / macOS 14+
- TransformingChatStorage — wraps any store; PII scrub / message transform before saving, iOS 16+
- SummarizingChatStorage — wraps any store; automatically compresses long histories via an LLM summarizer, iOS 16+
Custom store
Section titled “Custom store”Need a remote database, an encrypted keychain bucket, or a shared app-group container? Write a custom store.