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.
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+
Custom store
Section titled “Custom store”Need a remote database, an encrypted keychain bucket, or a shared app-group container? Write a custom store.