# AutoGen chat history per user: save_state and load_state for multi-user apps (and why pickling agents fails)

*Disclosure: I work on MemorySync (I'm Mohammed, the founder). I drafted this article with AI assistance and checked it against the GitHub issue, the AutoGen docs and README, the published PyPI packages and the MemorySync docs listed under Sources on Oct 31, 2026. Code status: I ran Steps 1 to 4 offline with outbound connections blocked and no API keys, on Python 3.13.5 with autogen-agentchat, autogen-core and autogen-ext 0.7.5 (the latest release), using AutoGen's own `ReplayChatCompletionClient` in place of a real model. I ran the JSON-file store, a single `AssistantAgent` for two users, a two-agent `RoundRobinGroupChat` resumed from saved state, the renamed-agent check and a `pickle` attempt on an agent. I didn't run a real model, a SQL or MongoDB table, Azure Blob Storage, AutoGen 0.2, or the MemorySync snippet against the API (I only constructed the MemorySync memory object offline).*

You built an AutoGen workflow that works on your laptop: a few agents in global variables, history kept in the process. Then you put it behind a web API with logins, and now every user shares the same agents, or you try to save the agents to a database and they won't serialize.

That's the question in AutoGen issue [#3302](https://github.com/microsoft/autogen/issues/3302), "How to Store Multiple Autogen Agents and Histories in Azure for Multi-User Environment". The author had a Flask app on Azure, got chat history into `.pkl` files in Blob Storage, and hit "unserializable registered functions" when storing the agents in MongoDB. A reply in the thread points at the answer: create the agents per request and store only the message history and state.

Short version: **don't store agents. Store state.** In current AutoGen (AgentChat 0.4 and later), `await agent.save_state()` and `await team.save_state()` return a plain dict you can `json.dumps`. Rebuild the agents from code on each request, `load_state()` the dict for that user and session, run, then save it back. Details and what I checked are below.

MemorySync doesn't do this part for you. Saving and resuming a conversation is AutoGen's `save_state()`/`load_state()`. MemorySync comes up once, near the end, as a separate layer for facts about a user that should outlast any one conversation.

> **Skip the long-term memory plumbing.** The [MemorySync AutoGen guide](https://docs.memorysync.io/guides/autogen?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history) shows a `MemorySyncMemory` object you pass to `AssistantAgent(memory=[...])` for per-user memory across sessions. It doesn't replace `save_state()` for resuming a chat. When you're ready, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).

## Status (Nov 15, 2026)

- [#3302](https://github.com/microsoft/autogen/issues/3302) was opened Aug 6, 2024 (19:10 IST) with the `0.2` label and closed as completed on Jun 14, 2025 (09:55 IST). It has 6 comments. It was asked about AutoGen 0.2, where agents carry registered functions. I re-checked the issue on Nov 15: still closed as completed, still 6 comments.
- The current line is AgentChat 0.4 and later. The latest release on PyPI is still autogen-agentchat **0.7.5** (Sep 30, 2025; re-checked Nov 15). The [AutoGen "Managing State" tutorial](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/tutorial/state.html) covers `save_state()` and `load_state()` for agents and teams and says state "can be serialized to a file or written to a database".
- The [AutoGen README](https://github.com/microsoft/autogen) still says AutoGen "is now archived and no longer maintained by Microsoft" and recommends Microsoft Agent Framework for new projects, with a [migration guide](https://learn.microsoft.com/en-us/agent-framework/migration-guide/from-autogen/) (re-checked Nov 15). If you're starting fresh, read that first. If you have an AutoGen app in production, the steps below still apply.

## Step 1: Stop trying to store the agent object

An agent is code plus live objects: a model client with an HTTP connection, tools that point at your functions, locks. None of that belongs in a database. In my run on 0.7.5, `pickle.dumps()` on an `AssistantAgent` failed with:

```text
TypeError: cannot pickle '_thread.lock' object
```

The state is the part that changes per user, and it's plain data. Here's what `save_state()` returned for a single `AssistantAgent` after one exchange (0.7.5):

```json
{"type": "AssistantAgentState", "version": "1.0.0",
 "llm_context": {"messages": [
   {"content": "My name is Alice.", "source": "user", "type": "UserMessage"},
   {"content": "Nice to meet you, Alice.", "thought": null, "source": "assistant", "type": "AssistantMessage"}]}}
```

One thing to watch if you read state directly: the AutoGen tutorial's printed example shows a top-level `llm_messages` key, but on 0.7.5 the messages are under `llm_context.messages`. Treat the dict as opaque and hand it back to `load_state()`; don't build your UI on its layout.

## Step 2: Key state by user and session

Store one state document per `(user_id, session_id)`. The `user_id` comes from your auth, never from the request body. The `session_id` is your conversation or ticket ID, so one user can have several chats.

This is the store I ran. It writes JSON files so it runs anywhere:

```python
import json
from pathlib import Path

class StateStore:
    """One JSON document per (user_id, session_id). Stores AutoGen state, never the agent object."""
    def __init__(self, root="state"):
        self.root = Path(root)

    def _path(self, user_id: str, session_id: str) -> Path:
        return self.root / user_id / f"{session_id}.json"

    def load(self, user_id: str, session_id: str) -> dict | None:
        p = self._path(user_id, session_id)
        return json.loads(p.read_text()) if p.exists() else None

    def save(self, user_id: str, session_id: str, state: dict) -> None:
        p = self._path(user_id, session_id)
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(json.dumps(state))
```

In production, swap the body of `load` and `save` for your database. The shape is one row or document per user and session with a JSON column, a primary key on `(user_id, session_id)`, and an upsert on save. Postgres `jsonb`, a MongoDB document or a Blob per key all fit. I only ran the file version. If your user IDs can contain `/` or `..`, hash them before using them as file names.

## Step 3: Rebuild the agent per request, then load and save

```python
from autogen_agentchat.agents import AssistantAgent

store = StateStore()

def build_agent(model_client):
    # Tools, model client and system message come from code, not from storage.
    return AssistantAgent("assistant", model_client=model_client,
                          system_message="You are a helpful assistant.")

async def handle(user_id: str, session_id: str, text: str, model_client) -> str:
    agent = build_agent(model_client)
    state = store.load(user_id, session_id)
    if state is not None:
        await agent.load_state(state)
    result = await agent.run(task=text)
    store.save(user_id, session_id, await agent.save_state())
    return result.messages[-1].content
```

I ran this with `ReplayChatCompletionClient` for two users, Alice then Bob then Alice again, each in session `s1`. Alice's saved state held only her four messages and Bob's held only his two. Nothing crossed over, because each request built a new agent and loaded only that key.

Two rules make this safe under load:

1. **Never share an agent instance between users.** A global `AssistantAgent` keeps one model context. With two users it's one conversation.
2. **Serialize requests per session.** If the same user sends two messages at once, both requests load the same state, and the second save overwrites the first. A per-session lock, or a version column checked on save, avoids that.

## Step 4: Teams (group chats) work the same way

`team.save_state()` saves every participant plus the group chat manager. After a simulated restart (a brand-new team object), I loaded the saved state and ran a new task:

```python
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.conditions import MaxMessageTermination

def build_team(model_client):
    writer = AssistantAgent("writer", model_client=model_client)
    critic = AssistantAgent("critic", model_client=model_client)
    return RoundRobinGroupChat([writer, critic],
                               termination_condition=MaxMessageTermination(3))

team = build_team(model_client)
saved = store.load(user_id, session_id)
if saved is not None:
    await team.load_state(saved)
result = await team.run(task=text)
store.save(user_id, session_id, await team.save_state())
```

On 0.7.5 the saved team state's `agent_states` were keyed `writer`, `critic` and `RoundRobinGroupChatManager`. The messages the writer's model received after the resume included the earlier task and both earlier replies, then the new task.

The catch is that **agent names are part of the saved state**. When I loaded the same state into a team whose first agent was renamed from `writer` to `author`, `load_state()` raised:

```text
ValueError: Agent state for author not found in the saved state.
```

So treat agent names like a schema. If you rename one, migrate stored states or start fresh sessions.

(In my run the writer also had a plain Python function as a tool. The tool lives in code, so it came back with `build_team()` and didn't need storing. The state stayed JSON-serializable.)

## Check that it works

1. Send two messages as user A, then one as user B, then ask user A something from their first message. A's answer should use A's history, and B's state should hold only B's messages.
2. Restart the server between messages. The conversation should continue.
3. `json.dumps(await team.save_state())` should succeed. If it fails, something non-serializable got into a message, often a custom message type.
4. Rename an agent in a test and confirm you see the `ValueError` above, so you know your migration path before production does.

## Skip the plumbing with MemorySync

Disclosure: I build MemorySync.

Saved state brings back a conversation. It doesn't give the agent what it learned about a user in other conversations. A new `session_id` starts from an empty state by design. If you want facts like "Alex prefers the window seat" to carry into every new chat, that's a memory layer, and AutoGen has a `Memory` protocol for it.

The [MemorySync AutoGen guide](https://docs.memorysync.io/guides/autogen?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history) implements that protocol with `MemorySyncMemory`. You keep `save_state()` for the conversation and add memory per user.

From the guide:

**Unverified (not run):**

```bash
pip install autogen-memorysync
```

```python
from autogen_memorysync import MemorySyncMemory
from autogen_agentchat.agents import AssistantAgent

memory = MemorySyncMemory(
    api_key="ms_...",        # or set MEMORYSYNC_API_KEY in the environment
    user_id="customer-42",   # required: the end user these memories belong to
    session_id="support",    # optional: tags this conversation's facts; bounds clear()
)

agent = AssistantAgent("assistant", model_client=..., memory=[memory])
result = await agent.run(task="Which seat should I book for Alex?")

# After each exchange: sends the user's text for fact extraction
await memory.add_turn_pair(user_text, result.messages[-1].content)
```

Per the guide, recalled memories are added as a `SystemMessage` before each model call, `add_turn_pair` sends only the user's text and the reply isn't stored, and `clear()` clears only the current session unless you pass `clear_scope="user"`. Use the same `user_id` you key your state with. The guide lists autogen-agentchat / autogen-core 0.4+ and Python 3.10+. *Snippet adapted from the [MemorySync AutoGen guide](https://docs.memorysync.io/guides/autogen?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history). I only installed autogen-memorysync 1.1.0 and constructed `MemorySyncMemory` offline; I haven't run it against the API for this page.*

If you're moving to Microsoft Agent Framework instead, see [Add long-term memory to a Microsoft Agent Framework agent](https://memorysync.hashnode.dev/microsoft-agent-framework-long-term-memory).

**Try it:** MemorySync is optional here: the steps above work without it. If you want the memory layer from the snippet, [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history), create an API key in the dashboard and drop the snippet into your agent.

## Troubleshooting

- **`TypeError: cannot pickle '_thread.lock' object` (or "unserializable" errors in MongoDB).** You're storing the agent. Store `save_state()` instead (Step 1).
- **`ValueError: Agent state for <name> not found in the saved state.`** An agent was renamed or added since the state was saved (Step 4).
- **Users see each other's history.** An agent or team instance is shared across requests, or the store key doesn't include the user. Build per request (Step 3).
- **History resets after every request.** `load_state()` isn't called before `run()`, or the save happens before the run finishes.
- **`KeyError: 'llm_messages'` in code that reads state.** On 0.7.5 the messages are under `llm_context.messages` (Step 1).
- **You're on AutoGen 0.2.** `save_state()`/`load_state()` as shown here is the 0.4+ AgentChat API. The 0.2 line is what the original issue used; the AutoGen docs have a [migration guide from 0.2 to 0.4](https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/migration-guide.html).

## Common questions

**Should I save the agent's state or the team's state?**
Whatever you call `run()` on. For a team, `team.save_state()` already includes every agent's state.

**Can I store state in Azure Blob Storage like the issue author did?**
Yes. It's a JSON dict, so one blob per `(user_id, session_id)` works. Use JSON rather than `.pkl` so you can read and migrate it. I didn't run Blob Storage.

**Will the state grow forever?**
`AssistantAgent` keeps its model context in the state, so long chats grow. AutoGen has bounded model contexts, such as `BufferedChatCompletionContext`, if you only want the last N messages sent to the model. I didn't test those for this page.

**Is AutoGen still a good choice for a new app?**
The AutoGen README recommends Microsoft Agent Framework for new projects. This page is for apps already on AutoGen.

## Related pages

- [Add long-term memory to a Microsoft Agent Framework agent](https://memorysync.hashnode.dev/microsoft-agent-framework-long-term-memory)
- [LangGraph memory explained: checkpointer vs store, and per-user memory across threads](https://memorysync.hashnode.dev/langgraph-checkpointer-vs-store-cross-thread-memory)
- [CrewAI persistent memory](https://memorysync.hashnode.dev/crewai-persistent-memory-memorysync)

## Sources

- github.com/microsoft/autogen/issues/3302 (opened 2024-08-06 19:10 IST, label `0.2`, closed as completed 2025-06-14 09:55 IST, 6 comments; the question, the Flask/Azure setup, the MongoDB serialization problem and the replies; read via the GitHub API 2026-10-31, re-checked 2026-11-15)
- AutoGen docs: AgentChat "Managing State" tutorial (fetched 2026-10-31, re-fetched 2026-11-15; still says state "can be serialized to a file or written to a database"; printed example still uses a top-level `llm_messages` key)
- AutoGen README on GitHub (archived / no longer maintained notice, Microsoft Agent Framework recommendation; read 2026-10-31, re-read 2026-11-15) and the Microsoft Learn migration guide from AutoGen
- PyPI: autogen-agentchat, autogen-core, autogen-ext 0.7.5 (still latest as of 2026-11-15, released 2025-09-30) and autogen-memorysync 1.1.0 (installed 2026-10-31)
- Offline runs (2026-10-31): Steps 1 to 4 with `ReplayChatCompletionClient`, two users, a two-agent team resumed after rebuilding, a renamed-agent load, `pickle` on an agent, `json.dumps` on team state; no network, no model, no keys
- MemorySync AutoGen guide: docs.memorysync.io/guides/autogen (fetched 2026-10-31)

## Try it

For an overview of MemorySync, see [the MemorySync homepage](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history).

**Next step:** read the [MemorySync AutoGen guide](https://docs.memorysync.io/guides/autogen?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history), then [see MemorySync](https://memorysync.io/?utm_source=hashnode&utm_medium=organic_page&utm_campaign=v5_oct25&utm_content=pseo_autogen-save-state-load-state-per-user-chat-history). Founding offer: use code FOUNDING30 for 30% off Starter for your first 3 months (Settings → Billing).
