# How Chat History Persistence Works in Derisk Agent Conversations

> Learn how DeriskAgent chat history persistence works. Explore the three-layer architecture and database storage for complete conversation retrieval.

- Repository: [derisk-ai/openderisk](https://github.com/derisk-ai/openderisk)
- Tags: internals
- Published: 2026-02-28

---

**Derisk-Serve persists every agent conversation turn to a relational database using a three-layer architecture—conversation objects, storage adapters, and SQLAlchemy-backed services—enabling full chat history retrieval via REST API.**

The derisk-ai/openderisk framework implements transactional chat history persistence to ensure LLM-driven agent conversations survive process restarts and remain queryable for auditing or UI replay. This implementation relies on explicit storage adapters that map in-memory conversation objects to database rows, separating concerns between business logic and data access.

## The Three-Layer Persistence Architecture

The persistence pipeline is structured into distinct layers to decouple agent logic from storage implementation:

- **Conversation objects**: In-memory representations (`StorageConversation`) that aggregate messages and metadata.
- **Storage adapters**: Translation layers (`DBStorageConversationItemAdapter`, `DBMessageStorageItemAdapter`) that convert objects to SQLAlchemy entities.
- **Serve component**: The `Serve` class that instantiates concrete `SQLAlchemyStorage` backends and exposes the history API.

This design allows agents to remain storage-agnostic while guaranteeing atomic persistence of conversation state.

## In-Memory Conversation Objects

### StorageConversation Interface

The core in-memory representation is the `StorageConversation` class defined in **[`derisk/core/interface/message.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk/core/interface/message.py)**. This object maintains a list of `MessageStorageItem` instances, a unique `conv_uid`, and references to the storage backends. Key methods include `add_user_message`, `add_view_message`, and `save_to_storage`.

When an agent initiates a turn, the `_build_conversation` function in **[`derisk-serve/src/derisk_serve/agent/agents/chat/agent_chat.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk-serve/src/derisk_serve/agent/agents/chat/agent_chat.py)** constructs a `StorageConversation` and injects the conversation-level and message-level storage adapters:

```python

# derisk-serve/src/derisk_serve/agent/agents/chat/agent_chat.py

async def _build_conversation(...):
    return await StorageConversation(
        conv_uid=conv_id,
        chat_mode="chat_agent",
        user_name=user_name,
        sys_code=sys_code,
        model_name=model_name,
        summary=summary,
        param_type="derisks",
        param_value=select_param,
        app_code=app_code,
        conv_storage=conv_serve.conv_storage,
        message_storage=conv_serve.message_storage,
        async_load=True,
    ).async_load()

```

The `async_load=True` parameter triggers immediate re-hydration of existing messages from the database when reconnecting to an ongoing conversation.

## Database Adapters and Entity Mapping

### Conversation-Level Persistence

The `DBStorageConversationItemAdapter` in **[`derisk/storage/chat_history/storage_adapter.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk/storage/chat_history/storage_adapter.py)** translates `StorageConversation` objects into `ChatHistoryEntity` rows for the `chat_history` table. The adapter's `to_storage_format` method serializes message references and generates a summary field:

```python

# derisk/storage/chat_history/storage_adapter.py

def to_storage_format(self, item: StorageConversation) -> ChatHistoryEntity:
    message_ids = ",".join(item.message_ids)
    messages = None
    if not item.save_message_independent and item.messages:
        message_dict_list = [_conversation_to_dict(item)]
        messages = json.dumps(message_dict_list, ensure_ascii=False)
    summary = item.summary or item.get_latest_user_message().last_text[:250]
    return ChatHistoryEntity(
        conv_uid=item.conv_uid,
        chat_mode=item.chat_mode,
        summary=summary,
        user_name=item.user_name,
        messages=messages,
        message_ids=message_ids,
        sys_code=item.sys_code,
        app_code=item.app_code,
    )

```

The adapter concatenates message IDs into a comma-separated string and stores legacy full-conversation JSON only when `save_message_independent` is disabled. The summary field auto-populates from the first 250 characters of the latest user message.

### Message-Level Persistence

Individual messages are persisted separately via `DBMessageStorageItemAdapter`, which maps `MessageStorageItem` objects to `ChatHistoryMessageEntity` rows in the `chat_history_message` table. This adapter extracts the `round_index` from message metadata and stores the full detail as JSON:

```python

# derisk/storage/chat_history/storage_adapter.py

def to_storage_format(self, item: MessageStorageItem) -> ChatHistoryMessageEntity:
    round_index = item.message_detail.get("round_index", 0)
    message_detail = json.dumps(item.message_detail, ensure_ascii=False)
    return ChatHistoryMessageEntity(
        conv_uid=item.conv_uid,
        index=item.index,
        round_index=round_index,
        message_detail=message_detail,
    )

```

Both adapters implement `from_storage_format` to support bidirectional translation when loading historical data.

## The Save and Load Lifecycle

### Persisting Conversation Rounds

Persistence is triggered explicitly at the end of each LLM turn. The `AgentChat` class calls `save_conversation`, which invokes `end_current_round` on the current message. This method delegates to `StorageConversation.save_to_storage`:

```python

# derisk/core/interface/message.py

def end_current_round(self) -> None:
    """End the current round of conversation."""
    self.save_to_storage()

def save_to_storage(self) -> None:
    # Save messages first

    message_list = self._get_message_items()
    self._message_ids = [...]
    # Persist the conversation record

    self.conv_storage.save(self, self.message_storage)

```

The `save_to_storage` method writes message rows first, then persists the conversation metadata with references to those messages. This two-phase commit ensures referential integrity between the `chat_history` and `chat_history_message` tables.

### Loading Existing Conversations

When resuming a conversation, the `Service.create_storage_conv` method instantiates `StorageConversation` with `load_message=True`. The constructor immediately calls `async_load` (or `load_from_storage` for synchronous contexts), which queries the database via the storage adapters and reconstructs the in-memory message list:

```python

# Conceptual flow based on derisk-serve/src/derisk_serve/conversation/service/service.py

conv = StorageConversation(
    conv_uid=existing_id,
    load_message=True,  # Triggers database query

    conv_storage=conv_storage,
    message_storage=message_storage,
)

```

## Serve Component and API Exposure

The `Serve` class in **[`derisk-serve/src/derisk_serve/conversation/serve.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk-serve/src/derisk_serve/conversation/serve.py)** initializes the storage layer during `before_start`. It creates `SQLAlchemyStorage` instances for both conversation and message entities, binding them to the database manager:

```python

# derisk-serve/src/derisk_serve/conversation/serve.py

self._conv_storage = SQLAlchemyStorage(
    self._db_manager,
    ChatHistoryEntity,
    DBStorageConversationItemAdapter(),
    JsonSerializer(),
)
self._message_storage = SQLAlchemyStorage(
    self._db_manager,
    ChatHistoryMessageEntity,
    DBMessageStorageItemAdapter(),
    JsonSerializer(),
)

```

The service registers the REST endpoint `GET /messages/history` (defined in **[`derisk-serve/src/derisk_serve/conversation/api/endpoints.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk-serve/src/derisk_serve/conversation/api/endpoints.py)**), which delegates to `Service.get_history_messages`. This method reconstructs the `StorageConversation` from the database and returns a list of `MessageVo` objects for UI consumption.

## Implementation Examples

### Initializing the Storage Service

Usually performed by the application framework during startup:

```python
from derisk_serve.conversation.serve import Serve
from derisk.component import SystemApp

system_app = SystemApp()
serve = Serve(system_app)
serve.init_app(system_app)  # Creates tables and mounts routers

```

### Creating and Saving Agent Conversations

Inside an agent implementation:

```python
from derisk_serve.agent.agents.chat.agent_chat import _build_conversation

conv = await _build_conversation(
    conv_id="c12345",
    select_param={},
    model_name="gpt-4",
    summary="",
    app_code="my_app",
    conv_serve=ConversationServe.get_instance(system_app),
    user_name="alice",
    sys_code="derisk",
)

# Append user input and persist

conv.add_user_message("What is my portfolio risk?")
conv.end_current_round()  # Triggers save_to_storage → Database rows

```

### Retrieving Chat History

Via the REST API:

```bash
curl -X GET "http://localhost:8000/api/v1/serve/derisk/messages/history?conv_uid=c12345"

```

Or programmatically through the service layer:

```python
from derisk_serve.conversation.service.service import Service

service = Service(system_app, config)
history = service.get_history_messages({"conv_uid": "c12345"})
for msg in history:
    print(msg.role, msg.context)

```

## Summary

- **StorageConversation** objects in [`derisk/core/interface/message.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk/core/interface/message.py) provide the in-memory interface for agents, abstracting database details.
- **Adapter pattern**: `DBStorageConversationItemAdapter` and `DBMessageStorageItemAdapter` in [`derisk/storage/chat_history/storage_adapter.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk/storage/chat_history/storage_adapter.py) handle bidirectional translation between objects and SQLAlchemy entities.
- **Two-phase persistence**: Messages are saved individually to `chat_history_message` before the parent conversation record updates in `chat_history`, maintaining referential integrity.
- **Explicit lifecycle**: Persistence occurs only when `end_current_round` is called, allowing agents to control exactly when state is committed.
- **Serve component** instantiates `SQLAlchemyStorage` backends and exposes the history via the `/messages/history` REST endpoint.

## Frequently Asked Questions

### How is conversation data structured in the database?

Conversation metadata resides in the `chat_history` table via `ChatHistoryEntity`, storing fields like `conv_uid`, `chat_mode`, and a comma-separated `message_ids` string. Individual messages are stored in the `chat_history_message` table via `ChatHistoryMessageEntity`, containing `round_index`, `index`, and JSON `message_detail`. This separation allows efficient querying of conversation lists without loading full message content.

### What triggers the persistence of a conversation round?

The `AgentChat.save_conversation` method calls `current_message.end_current_round()`, which internally invokes `StorageConversation.save_to_storage()`. This design ensures persistence happens only after the LLM completes generation and the agent finalizes the turn, preventing storage of incomplete or failed interactions.

### Can conversations be retrieved asynchronously?

Yes. The `StorageConversation` supports async operations via `async_load()`, which is automatically invoked when `async_load=True` is passed to the constructor. The `Serve` component uses `SQLAlchemyStorage` with async-capable database managers, allowing non-blocking history retrieval in high-throughput scenarios.

### Where is the chat history API endpoint defined?

The REST endpoint `GET /messages/history` is registered in **[`derisk-serve/src/derisk_serve/conversation/api/endpoints.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk-serve/src/derisk_serve/conversation/api/endpoints.py)**. It delegates to `Service.get_history_messages` in **[`derisk-serve/src/derisk_serve/conversation/service/service.py`](https://github.com/derisk-ai/openderisk/blob/main/derisk-serve/src/derisk_serve/conversation/service/service.py)**, which uses the storage adapters to reconstruct `StorageConversation` objects from the database and return `MessageVo` responses.