Managing Conversation State and Handling ThreadAlreadyExistsError in aisuite

aisuite prevents accidental conversation overwrites by raising ThreadAlreadyExistsError when you attempt to start a run with an existing thread ID, requiring you to use continuation APIs like Runner.continue_sync to resume persisted state.

The aisuite library separates agent execution from state persistence through a thread-based architecture. When building multi-turn conversational agents, you must understand how the Runner class manages conversation history and why it enforces strict thread ID uniqueness. This guide examines the source code implementation in aisuite/agents/runner.py and aisuite/agents/state_store.py to show you how to properly handle conversation state and resolve ThreadAlreadyExistsError.

How aisuite Manages Conversation State

Thread-ID to State Mapping

At the core of aisuite's state management is a strict one-to-one mapping between a thread ID and a persisted RunState object. When you invoke Runner.run() with a thread_id parameter, the framework checks the provided StateStore for existing entries before execution begins.

According to the source code in aisuite/agents/runner.py (lines 38-45), the _run_impl method performs this validation:


# From aisuite/agents/runner.py

if thread_id is not None and state_store is not None:
    existing_state = state_store.load_state(thread_id)
    if existing_state is not None:
        raise ThreadAlreadyExistsError(
            f"Thread with ID '{thread_id}' already exists. "
            f"Use continue_run() to continue an existing thread."
        )

This check ensures that you cannot accidentally overwrite an active conversation. If state exists, you must explicitly choose to continue the thread rather than replace it.

The StateStore Protocol

The persistence layer is defined by the StateStore protocol in aisuite/agents/state_store.py (lines 53-64), which specifies three required operations:

  • save_state(thread_id, run_state): Persists the conversation state
  • load_state(thread_id): Retrieves existing state or returns None
  • delete_state(thread_id): Removes persisted state

Both InMemoryStateStore and FileStateStore implement this protocol with optimistic concurrency checks using revision numbers to prevent race conditions when multiple workers access the same thread.

Understanding ThreadAlreadyExistsError

ThreadAlreadyExistsError is defined in aisuite/agents/runner.py (lines 30-33) as a subclass of RuntimeError:

class ThreadAlreadyExistsError(RuntimeError):
    """Raised when a thread ID already exists in the state store."""
    pass

The framework raises this error exclusively when you call Runner.run() with a thread_id that already exists in the configured StateStore. This serves as a safety mechanism to prevent data loss in long-running conversational applications. The error is re-exported in aisuite/__init__.py as part of the public API, allowing you to catch it with except ai.ThreadAlreadyExistsError.

Starting New Threads vs. Continuing Conversations

Starting a Fresh Thread with Runner.run

To initiate a new conversation, provide a unique thread_id and a StateStore instance to Runner.run():

import aisuite as ai

agent = ai.Agent(
    name="weather-assistant",
    model="openai:gpt-4o-mini",
    instructions="Answer weather queries concisely."
)

store = ai.FileStateStore(root=".aisuite/state")

result = ai.Runner.run(
    agent,
    "What is the weather in Tokyo?",
    thread_id="weather-session-tokyo-001",
    state_store=store,
)
print(result.final_output)

If weather-session-tokyo-001 already exists in the file store, the call raises ThreadAlreadyExistsError immediately before executing the agent.

Continuing an Existing Thread with Runner.continue_sync

To resume a conversation, use Runner.continue_sync() (or the async continue_run()), which loads the stored RunState, appends your new message, and executes the next turn. The implementation in aisuite/agents/runner.py (lines 84-92) validates that both state_store and thread_id are provided together:


# Continue the previous conversation

result = ai.Runner.continue_sync(
    agent,
    "What about tomorrow's forecast?",
    thread_id="weather-session-tokyo-001",
    state_store=store,
)
print(result.final_output)

If you attempt to continue a thread that does not exist, the framework raises StateNotFoundError, the logical counterpart to ThreadAlreadyExistsError.

State Persistence Implementations

InMemoryStateStore

The InMemoryStateStore class (aisuite/agents/state_store.py, lines 68-99) maintains state in a Python dictionary, making it ideal for testing or transient single-process applications. It implements optimistic concurrency by tracking revision numbers and raises StateConflictError if the stored revision does not match the expected value (lines 71-81).

FileStateStore

For production durability, FileStateStore (lines 101-148) persists state as JSON files under .<repo>/state/. It uses atomic write operations via temporary files to prevent corruption during system crashes. Like the in-memory variant, it supports revision-based conflict detection for concurrent access scenarios.

Optimistic Concurrency and Conflict Handling

Both store implementations validate revision numbers before saving. If two processes attempt to update the same thread simultaneously, the second write fails with StateConflictError, forcing the application to reload the current state and retry. This mechanism ensures conversation integrity without requiring database locks.

Practical Code Examples

Example 1: Handling Duplicate Thread IDs

This pattern shows how to catch ThreadAlreadyExistsError and automatically switch to continuation mode:

import aisuite as ai

agent = ai.Agent(name="assistant", model="openai:gpt-4o")
store = ai.FileStateStore(root=".aisuite/state")
thread_id = "user-session-123"

try:
    result = ai.Runner.run(
        agent,
        "Analyze this code repository",
        thread_id=thread_id,
        state_store=store,
    )
except ai.ThreadAlreadyExistsError:
    # Thread exists, so continue instead

    result = ai.Runner.continue_sync(
        agent,
        "Provide additional analysis on the test files",
        thread_id=thread_id,
        state_store=store,
    )

print(result.final_output)

Example 2: Multi-Turn Conversation with In-Memory State

For stateless applications or testing, use InMemoryStateStore to maintain conversation context across multiple turns:

import aisuite as ai

store = ai.InMemoryStateStore()
agent = ai.Agent(name="coder", model="anthropic:claude-3.5-sonnet")

# First turn - creates the thread

first = ai.Runner.run(
    agent,
    "Write a Python function to calculate fibonacci numbers",
    thread_id="coding-session",
    state_store=store,
)

# Second turn - continues automatically

second = ai.Runner.continue_sync(
    first,  # Can pass the previous result or the agent

    "Now optimize it for memoization",
    thread_id="coding-session",
    state_store=store,
)

print(second.final_output)

Example 3: Checking for State Existence Before Running

To proactively check if a thread exists before attempting creation:

import aisuite as ai

store = ai.FileStateStore(root=".aisuite/state")
thread_id = "persistent-thread-001"

# Check existence manually

existing = store.load_state(thread_id)

if existing is None:
    # Safe to create new thread

    result = ai.Runner.run(agent, "Initial message", thread_id=thread_id, state_store=store)
else:
    # Continue existing

    result = ai.Runner.continue_sync(agent, "Follow-up", thread_id=thread_id, state_store=store)

Summary

  • aisuite uses thread IDs to uniquely identify persisted conversation state across runs.
  • ThreadAlreadyExistsError is raised in aisuite/agents/runner.py when Runner.run() detects an existing state entry, preventing accidental overwrites.
  • Use Runner.continue_sync() with the same thread_id and StateStore to resume conversations safely.
  • The StateStore protocol supports custom implementations, with built-in InMemoryStateStore for testing and FileStateStore for persistence.
  • Both stores implement optimistic concurrency using revision numbers to handle race conditions.

Frequently Asked Questions

What causes ThreadAlreadyExistsError in aisuite?

ThreadAlreadyExistsError occurs when you call Runner.run() with a thread_id that already exists in the provided StateStore. According to the implementation in aisuite/agents/runner.py (lines 38-45), the framework checks for existing state before execution and raises this error to prevent you from overwriting an active conversation thread.

How do I resume a conversation after ThreadAlreadyExistsError?

Catch the error and call Runner.continue_sync() instead, passing the same thread_id and StateStore. The continuation API loads the existing RunState from the store, appends your new user message, and executes the next agent turn without losing conversation history.

Can I use a custom database for conversation state?

Yes. Implement the StateStore protocol defined in aisuite/agents/state_store.py (lines 53-64) with save_state(), load_state(), and delete_state() methods. You can then pass your custom store to Runner.run() or Runner.continue_sync(), allowing you to back conversation state with PostgreSQL, Redis, or any other database.

What's the difference between InMemoryStateStore and FileStateStore?

InMemoryStateStore keeps conversation state in a Python dictionary and loses data when the process exits, making it suitable for testing. FileStateStore persists state as JSON files under .<repo>/state/ using atomic write operations, enabling conversation durability across process restarts and deployments.

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 →