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 aRunStatewith an optional revision number for concurrency control.load_state(thread_id)– Returns aStoredRunStatecontaining 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 astate_storeandthread_id. It validates that the thread does not already exist, executes the first turn, then callssave_statewithrevision=None.Runner.continue_sync(lines 94-104) loads existing state viaload_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:
- Creation:
run_syncchecks for existing state. If found, it raisesThreadAlreadyExistsError. Otherwise, it executes the agent and callssave_state(thread_id, run_state, revision=None). - Continuation:
continue_synccallsload_state(thread_id)to retrieve theStoredRunState, extracts the revision number, runs the next turn, and callssave_statewith that revision. - 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
StateStoreprotocol, allowing you to resume conversations across process restarts. - Use
FileStateStorefor durable JSON persistence orInMemoryStateStorefor temporary testing. - Call
Runner.run_syncwiththread_idandstate_storeto start a persisted thread; useRunner.continue_syncto resume it. - The system uses revision numbers for optimistic concurrency control, raising
StateConflictErrorif concurrent writes collide. - You can implement custom backends (Redis, SQL, cloud storage) by adhering to the
StateStoreprotocol defined inaisuite/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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →