# How to Persist and Resume Agent Runs in aisuite Using State Stores

> Learn to persist and resume agent runs in aisuite using state stores. Save and load conversation state with Runner.run_sync and Runner.continue_sync for uninterrupted execution.

- Repository: [Andrew Ng/aisuite](https://github.com/andrewyng/aisuite)
- Tags: how-to-guide
- Published: 2026-07-27

---

**aisuite provides a plug-in StateStore protocol that lets you save full conversation state to disk or memory and resume execution later without losing context, using `Runner.run_sync` for new threads and `Runner.continue_sync` for existing ones.**

The `aisuite` library (from Andrew Ng) simplifies AI agent development with a unified interface for multiple LLM providers. When building production applications, you often need to maintain conversation state across server restarts or share threads between processes. The state store API in [`aisuite/agents/state_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/state_store.py) provides a standardized way to persist `RunState` objects and resume them safely using optimistic concurrency control.

## Core Architecture

The persistence system revolves around a protocol-based design that separates storage concerns from agent execution logic.

### The StateStore Protocol

The abstract contract is defined in [`aisuite/agents/state_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/state_store.py) (lines 53-66). Any valid state store must implement three methods:

- **`save_state(thread_id, state, revision=None)`** – Persists a `RunState` with an optional revision number for concurrency control.
- **`load_state(thread_id)`** – Returns a `StoredRunState` containing the serialized state, revision, and timestamps.
- **`delete_state(thread_id)`** – Removes the persisted thread permanently.

### Built-in Implementations

**`InMemoryStateStore`** (lines 68-89) stores data in a Python dictionary. It is useful for unit tests and ephemeral sessions but disappears when the process exits. It performs revision checks to simulate concurrent access patterns.

**`FileStateStore`** (lines 101-149) provides durable JSON-file storage. It writes each thread to `<root>/<quoted_thread_id>.json` atomically, creating directories as needed. This implementation handles file-system level persistence with automatic revision tracking.

### Runner Integration

The execution engine in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) orchestrates persistence automatically:

- **`Runner.run_sync`** (lines 38-45) accepts a `state_store` and `thread_id`. It validates that the thread does not already exist, executes the first turn, then calls `save_state` with `revision=None`.
- **`Runner.continue_sync`** (lines 94-104) loads existing state via `load_state`, appends the new user message to the recovered dialogue history, runs the agent for the next turn, and saves the updated state using the previous revision number.

If two processes attempt to write the same thread simultaneously, **`StateConflictError`** (lines 15-22) is raised, protecting against lost updates.

## The Persistence Flow

When you initiate a conversation with persistence enabled, aisuite follows this sequence:

1. **Creation**: `run_sync` checks for existing state. If found, it raises `ThreadAlreadyExistsError`. Otherwise, it executes the agent and calls `save_state(thread_id, run_state, revision=None)`.
2. **Continuation**: `continue_sync` calls `load_state(thread_id)` to retrieve the `StoredRunState`, extracts the revision number, runs the next turn, and calls `save_state` with that revision.
3. **Conflict Detection**: If the stored revision changed between load and save (indicating another write occurred), the save operation raises `StateConflictError`.

Both stores use the `StoredRunState` schema, which includes `thread_id`, serializable `state` (messages and metadata), a monotonically increasing `revision`, and `created_at`/`updated_at` timestamps.

## Code Examples

The following snippets demonstrate durable persistence to the file system. Replace `FileStateStore` with `InMemoryStateStore` for testing scenarios.

### Persisting a New Run

Create a `FileStateStore` pointing to a directory, then pass it to `run_sync` along with a unique `thread_id`:

```python
from aisuite.agents.runner import Runner
from aisuite.agents.state_store import FileStateStore
from aisuite.agents.utils import simple_agent  # Your agent definition

# Initialize durable storage (creates .my_state/ if needed)

store = FileStateStore(root=".my_state")

# Execute first turn and persist automatically

result = Runner.run_sync(
    agent=simple_agent,
    input="Explain quantum computing in simple terms.",
    state_store=store,
    thread_id="user_123_session",
)

print("Agent:", result.last_message.content)

```

After execution, `aisuite` writes the full dialogue state to [`.my_state/user_123_session.json`](https://github.com/andrewyng/aisuite/blob/main/.my_state/user_123_session.json) via `FileStateStore.save_state`.

### Resuming an Existing Thread

To continue a conversation in a new process or later session, use `continue_sync` with the same `thread_id` and store:

```python

# Resume the conversation where it left off

result2 = Runner.continue_sync(
    target=simple_agent,
    input="Can you give me an example?",
    state_store=store,
    thread_id="user_123_session",
)

print("Agent:", result2.last_message.content)

```

The `Runner.continue_sync` method loads the previous state, appends your new input to the message history, and persists the updated state with the correct revision number.

### Using the In-Memory Store for Testing

For unit tests or ephemeral workflows, use `InMemoryStateStore` to avoid file I/O:

```python
from aisuite.agents.state_store import InMemoryStateStore

mem_store = InMemoryStateStore()

# First turn

first = Runner.run_sync(
    simple_agent, 
    "What is Python?", 
    state_store=mem_store, 
    thread_id="test_thread"
)

# Continue immediately

second = Runner.continue_sync(
    simple_agent, 
    "How is it different from Java?", 
    state_store=mem_store, 
    thread_id="test_thread"
)

```

The in-memory store follows the same revision semantics as the file store, allowing you to test concurrency logic without creating temporary files.

### Handling Revision Conflicts

In production systems with concurrent access, you should wrap continuation calls to handle `StateConflictError`:

```python
from aisuite.agents.state_store import StateConflictError

try:
    result = Runner.continue_sync(
        simple_agent,
        "Process this urgent update.",
        state_store=store,
        thread_id="shared_thread",
    )
except StateConflictError:
    # Another process modified the thread since we loaded it

    # Strategy: reload and retry, or prompt user to refresh

    print("Conflict detected: Another instance updated this conversation.")

```

The error indicates that the revision number in storage no longer matches the one loaded into memory, preventing accidental overwrites of intermediate messages.

## Key Implementation Files

| File | Purpose | Key Components |
|------|---------|----------------|
| [`aisuite/agents/state_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/state_store.py) | Storage backends and protocol | `StateStore` protocol, `FileStateStore`, `InMemoryStateStore`, `StateConflictError` |
| [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) | Execution engine | `Runner.run_sync`, `Runner.continue_sync` |
| [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py) | Data structures | `RunState`, `StoredRunState`, `RunResult` |
| [`tests/agents/test_state_store.py`](https://github.com/andrewyng/aisuite/blob/main/tests/agents/test_state_store.py) | Verification suite | Round-trip persistence tests, revision conflict scenarios |

## Summary

- **aisuite** persists agent runs via the `StateStore` protocol, allowing you to resume conversations across process restarts.
- Use **`FileStateStore`** for durable JSON persistence or **`InMemoryStateStore`** for temporary testing.
- Call **`Runner.run_sync`** with `thread_id` and `state_store` to start a persisted thread; use **`Runner.continue_sync`** to resume it.
- The system uses **revision numbers** for optimistic concurrency control, raising `StateConflictError` if concurrent writes collide.
- You can implement custom backends (Redis, SQL, cloud storage) by adhering to the `StateStore` protocol defined in [`aisuite/agents/state_store.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/state_store.py).

## Frequently Asked Questions

### What is the difference between `run_sync` and `continue_sync`?

`run_sync` starts a fresh conversation thread and requires that the `thread_id` does not already exist in the store, raising `ThreadAlreadyExistsError` if it finds prior state. `continue_sync` loads existing state from the store, appends the new user message to the existing dialogue history, executes the next turn, and saves the updated state. According to the source in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py), `continue_sync` handles the revision logic automatically while `run_sync` initializes the first revision.

### How does aisuite handle concurrent writes to the same thread?

The framework implements **optimistic concurrency control** using monotonically increasing revision numbers. When `continue_sync` loads state, it captures the current revision. Upon saving, it passes that revision number to `save_state`. If another process has written to the same thread in the interim (incrementing the stored revision), the save operation detects the mismatch and raises `StateConflictError`, preventing silent data loss.

### Can I use a custom database instead of `FileStateStore`?

Yes. The `StateStore` class is a Python protocol (interface), not a concrete class. You can implement `save_state`, `load_state`, and `delete_state` for any backend—PostgreSQL, Redis, DynamoDB, or S3. Pass your custom instance to `Runner.run_sync` or `Runner.continue_sync` via the `state_store` parameter. The runner code in [`aisuite/agents/runner.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/runner.py) treats all stores uniformly, requiring only that they match the protocol.

### What data is actually stored in the `RunState`?

The `RunState` (defined in [`aisuite/agents/types.py`](https://github.com/andrewyng/aisuite/blob/main/aisuite/agents/types.py)) contains the complete execution context: the message history (user and assistant messages), any tool call steps taken during the run, agent metadata, and timestamps. When serialized via `FileStateStore`, this becomes a `StoredRunState` JSON object including the `thread_id`, serialized `state`, `revision` integer, and audit timestamps (`created_at`, `updated_at`). This allows full reconstruction of the conversation context when resuming.