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

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 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 (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 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:

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 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:


# 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:

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:

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 Storage backends and protocol StateStore protocol, FileStateStore, InMemoryStateStore, StateConflictError
aisuite/agents/runner.py Execution engine Runner.run_sync, Runner.continue_sync
aisuite/agents/types.py Data structures RunState, StoredRunState, RunResult
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.

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, 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 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) 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →