# @aichatkit/storage-adapter

Base storage adapter abstract class for Hypermode ChatKit with backend synchronization support and
rich response items.

## Installation

```bash
npm install @aichatkit/storage-adapter
```

## Usage

This package provides the base abstract class for implementing storage adapters. You typically won't
use this package directly, but rather create or use an implementation like
`@aichatkit/localstorage-adapter`.

```typescript
import { StorageAdapter } from "@aichatkit/storage-adapter"
import { Conversation, ChatResponseItem, Message } from "@aichatkit/types"

// Example: Create a custom adapter implementation
class CustomStorageAdapter extends StorageAdapter {
  private conversations: Map<string, Conversation> = new Map()
  private agentMapping: Map<string, string> = new Map()

  async saveConversation(conversation: Conversation): Promise<void> {
    this.conversations.set(conversation.id, { ...conversation })
  }

  async getConversation(id: string): Promise<Conversation | null> {
    return this.conversations.get(id) || null
  }

  async getAllConversations(): Promise<Conversation[]> {
    return Array.from(this.conversations.values())
  }

  async deleteConversation(id: string): Promise<boolean> {
    const deleted = this.conversations.delete(id)
    this.agentMapping.delete(id)
    return deleted
  }

  async addItem(conversationId: string, item: ChatResponseItem): Promise<Conversation | null> {
    const conversation = this.conversations.get(conversationId)
    if (!conversation) return null

    conversation.items.push(item)
    await this.saveConversation(conversation)
    return conversation
  }

  async getConversationItems(conversationId: string): Promise<ChatResponseItem[]> {
    // Check if we have network callbacks for backend sync
    const agentId = await this.getConversationAgent(conversationId)

    if (agentId && this.callbacks?.getConversationItems) {
      try {
        const backendItems = await this.callbacks.getConversationItems(agentId)

        // Update local storage with backend data
        const conversation = await this.getConversation(conversationId)
        if (conversation) {
          conversation.items = backendItems
          await this.saveConversation(conversation)
        }

        return backendItems
      } catch (error) {
        console.error("Backend sync failed, using local data:", error)
      }
    }

    // Fall back to local data
    const conversation = await this.getConversation(conversationId)
    return conversation?.items || []
  }

  async clearConversationHistory(conversationId: string): Promise<void> {
    const agentId = await this.getConversationAgent(conversationId)

    // Clear on backend if possible
    if (agentId && this.callbacks?.clearConversationHistory) {
      try {
        await this.callbacks.clearConversationHistory(agentId)
      } catch (error) {
        console.error("Failed to clear backend history:", error)
      }
    }

    // Clear locally
    const conversation = await this.getConversation(conversationId)
    if (conversation) {
      conversation.items = []
      await this.saveConversation(conversation)
    }
  }

  async setConversationAgent(conversationId: string, agentId: string): Promise<void> {
    this.agentMapping.set(conversationId, agentId)
  }

  async getConversationAgent(conversationId: string): Promise<string | null> {
    return this.agentMapping.get(conversationId) || null
  }
}
```

## Abstract Methods

All storage adapters must implement these core methods:

### saveConversation(conversation: Conversation): Promise<void>

Saves a conversation with all its response items to storage.

```typescript
abstract saveConversation(conversation: Conversation): Promise<void>;
```

**Parameters**:

- `conversation`: The conversation object with items array containing messages, tool calls, cards,
  etc.

**Example**:

```typescript
await adapter.saveConversation({
  id: "conv-1",
  title: "My Chat",
  items: [
    {
      id: "msg_1",
      type: "message",
      content: "Hello",
      role: "user",
    },
    {
      id: "tool_1",
      type: "tool_call",
      toolCall: {
        id: "call_1",
        name: "get_weather",
        arguments: { location: "NYC" },
        status: "completed",
        result: { temperature: "22°C" },
      },
    },
  ],
})
```

### getConversation(id: string): Promise<Conversation | null>

Retrieves a conversation from storage by ID with all its response items.

```typescript
abstract getConversation(id: string): Promise<Conversation | null>;
```

**Parameters**:

- `id`: ID of the conversation to retrieve

**Returns**: Promise resolving to the conversation with all item types or null if not found

### getAllConversations(): Promise<Conversation[]>

Retrieves all conversations from storage with their complete response items.

```typescript
abstract getAllConversations(): Promise<Conversation[]>;
```

**Returns**: Promise resolving to an array of all conversations with their items

### deleteConversation(id: string): Promise<boolean>

Deletes a conversation from storage.

```typescript
abstract deleteConversation(id: string): Promise<boolean>;
```

**Parameters**:

- `id`: ID of the conversation to delete

**Returns**: Promise resolving to true if successful, false otherwise

### addItem(conversationId: string, item: ChatResponseItem): Promise<Conversation | null>

Adds any type of response item to a conversation.

```typescript
abstract addItem(conversationId: string, item: ChatResponseItem): Promise<Conversation | null>;
```

**Parameters**:

- `conversationId`: ID of the conversation
- `item`: Response item to add (message, tool call, card, etc.)

**Returns**: Promise resolving to the updated conversation or null if not found

**Example**:

```typescript
// Add a message
await adapter.addItem("conv-1", {
  id: "msg_2",
  type: "message",
  content: "How's the weather?",
  role: "user",
})

// Add a tool call
await adapter.addItem("conv-1", {
  id: "tool_2",
  type: "tool_call",
  toolCall: {
    id: "call_2",
    name: "get_weather",
    arguments: { location: "NYC" },
    status: "executing",
  },
})
```

### addMessage(conversationId: string, message: Message): Promise<Conversation | null>

Convenience method for adding message items.

```typescript
async addMessage(conversationId: string, message: Message): Promise<Conversation | null>
```

This method converts a Message to a MessageItem and calls `addItem()`.

## Backend Synchronization Methods

These methods enable synchronization with backend agents:

### getConversationItems(conversationId: string): Promise<ChatResponseItem[]>

Gets all conversation items with backend synchronization.

```typescript
abstract getConversationItems(conversationId: string): Promise<ChatResponseItem[]>;
```

This method should:

1. Check if a backend agent exists for the conversation
2. Sync with backend if available to get all item types
3. Fall back to local storage if backend sync fails

### getConversationHistory(conversationId: string): Promise<Message[]>

Gets conversation history (messages only) with backend synchronization.

```typescript
async getConversationHistory(conversationId: string): Promise<Message[]>
```

This convenience method filters message items from all response items.

### clearConversationHistory(conversationId: string): Promise<void>

Clears conversation history both locally and on backend.

```typescript
abstract clearConversationHistory(conversationId: string): Promise<void>;
```

### setConversationAgent(conversationId: string, agentId: string): Promise<void>

Maps a conversation to a backend agent ID.

```typescript
abstract setConversationAgent(conversationId: string, agentId: string): Promise<void>;
```

### getConversationAgent(conversationId: string): Promise<string | null>

Gets the backend agent ID for a conversation.

```typescript
abstract getConversationAgent(conversationId: string): Promise<string | null>;
```

## Optional Methods

### initialize(config?: Record<string, any>): Promise<void>

Optional initialization method.

```typescript
async initialize(config?: Record<string, any>): Promise<void> {
  return Promise.resolve();
}
```

### syncAllConversationsWithBackend(): Promise<void>

Syncs all conversations with backend (called on app load).

```typescript
async syncAllConversationsWithBackend?(): Promise<void> {
  return Promise.resolve();
}
```

## Network Callbacks

Storage adapters can receive network callbacks for backend synchronization:

### StorageAdapterCallbacks Interface

```typescript
interface StorageAdapterCallbacks {
  getConversationItems?: (agentId: string) => Promise<ChatResponseItem[]>
  clearConversationHistory?: (agentId: string) => Promise<void>
}
```

### setNetworkCallbacks(callbacks: StorageAdapterCallbacks): void

Sets network callbacks for backend communication.

```typescript
import { NetworkAdapter } from "@aichatkit/network-adapter"

const networkAdapter = new SomeNetworkAdapter()
const storageAdapter = new SomeStorageAdapter()

// Connect storage with network for syncing
storageAdapter.setNetworkCallbacks({
  getConversationItems: (agentId) => networkAdapter.getConversationItems(agentId),
  clearConversationHistory: (agentId) => networkAdapter.clearConversationHistory(agentId),
})
```

## Data Types

### Conversation

```typescript
interface Conversation {
  id: string
  title: string
  items: ChatResponseItem[] // Mixed array of messages, tool calls, cards, etc.
}
```

### ChatResponseItem

Union type supporting multiple item types:

```typescript
type ChatResponseItem = MessageItem | ToolCallItem | CardItem

interface MessageItem {
  id: string | number
  type: "message"
  content: string
  role: "user" | "assistant"
  timestamp?: string
}

interface ToolCallItem {
  id: string | number
  type: "tool_call"
  toolCall: {
    id: string
    name: string
    arguments: Record<string, any>
    status: "pending" | "executing" | "completed" | "error"
    result?: any
    error?: string
  }
}

interface CardItem {
  id: string | number
  type: "card"
  card: {
    id: string
    type: string
    title?: string
    content: Record<string, any>
    actions?: CardAction[]
  }
}
```

### Message

```typescript
interface Message {
  id: string | number
  content: string
  role: "user" | "assistant"
  timestamp?: string
}
```

## Agent-Conversation Mapping

Storage adapters maintain a mapping between conversations and backend agents:

```typescript
// When creating a new conversation with an agent
await storageAdapter.setConversationAgent("conv-1", "agent-123")

// When retrieving the agent for a conversation
const agentId = await storageAdapter.getConversationAgent("conv-1")

// When deleting a conversation, also remove the agent mapping
await storageAdapter.deleteConversation("conv-1") // Should also remove agent mapping
```

## Synchronization Flow

1. **App Initialization**: Call `syncAllConversationsWithBackend()` to get latest state
2. **Conversation Switch**: Sync specific conversation when user switches to it
3. **Item History**: Always try backend first, fall back to local storage
4. **Background Sync**: Periodically sync to keep data fresh

### Example Sync Implementation

```typescript
async getConversationItems(conversationId: string): Promise<ChatResponseItem[]> {
  const agentId = await this.getConversationAgent(conversationId);

  if (agentId && this.callbacks?.getConversationItems) {
    try {
      // Try to get latest from backend
      const backendItems = await this.callbacks.getConversationItems(agentId);

      // Update local storage with backend data
      const conversation = await this.getConversation(conversationId);
      if (conversation) {
        conversation.items = backendItems;
        await this.saveConversation(conversation);
      }

      return backendItems;
    } catch (error) {
      console.error('Backend sync failed:', error);
      // Continue to local fallback
    }
  }

  // Fall back to local storage
  const conversation = await this.getConversation(conversationId);
  return conversation?.items || [];
}
```

## Available Implementations

- **[@aichatkit/localstorage-adapter](../localstorage-adapter)**: Browser localStorage
  implementation
- **Custom implementations**: Create your own for databases, cloud storage, etc.

## Testing

Mock storage adapters for testing:

```typescript
class MockStorageAdapter extends StorageAdapter {
  private data: Map<string, Conversation> = new Map()

  async saveConversation(conversation: Conversation): Promise<void> {
    this.data.set(conversation.id, { ...conversation })
  }

  async getConversation(id: string): Promise<Conversation | null> {
    return this.data.get(id) || null
  }

  // ... implement other methods
}
```

## License

[MIT](../../LICENSE) © Hypermode
