# How ML Intern Manages Session State and Auto-Saves Conversations to Hugging Face

> Discover how ML Intern manages session state and auto-saves conversations to Hugging Face. Learn about its SessionManager, state tracking, and efficient upload process.

- Repository: [Hugging Face/ml-intern](https://github.com/huggingface/ml-intern)
- Tags: internals
- Published: 2026-04-24

---

**ML Intern uses a `SessionManager` to orchestrate conversation state, while a `Session` object tracks turn counts; after every N user turns (default 3), it saves the trajectory locally as JSON and spawns a detached subprocess to upload the file to a Hugging Face dataset repository.**

ML Intern, the open-source agent framework from Hugging Face, implements a robust session management system that persists interactive conversations without blocking the main agent loop. The architecture separates **session state management** from the **auto-save functionality**, ensuring that user interactions remain responsive while conversation trajectories are asynchronously backed up to a Hugging Face dataset. This article examines the source code to explain exactly how `SessionManager` coordinates lifecycles, how `Session` tracks turns, and how the [`session_uploader.py`](https://github.com/huggingface/ml-intern/blob/main/session_uploader.py) subprocess handles uploads.

## Session Architecture: Manager, AgentSession, and Core Session

The system relies on three primary components to maintain state across conversational turns.

### The SessionManager Orchestrator

Located in [`backend/session_manager.py`](https://github.com/huggingface/ml-intern/blob/main/backend/session_manager.py), the `SessionManager` acts as the central registry for all active conversations. It generates unique session identifiers, wires together event queues, and enforces capacity limits. When a new conversation starts, the manager creates a `Session` instance inside a thread pool to avoid blocking the async event loop.

```python

# backend/session_manager.py – create_session()

session_id = str(uuid.uuid4())
submission_queue = asyncio.Queue()
event_queue = asyncio.Queue()

# Blocking constructors run in a thread pool

tool_router, session = await asyncio.to_thread(_create_session_sync)

agent_session = AgentSession(
    session_id=session_id,
    session=session,
    tool_router=tool_router,
    submission_queue=submission_queue,
    user_id=user_id,
    hf_token=hf_token,
)
self.sessions[session_id] = agent_session

```

### AgentSession Wrapper

The `AgentSession` class (also in [`backend/session_manager.py`](https://github.com/huggingface/ml-intern/blob/main/backend/session_manager.py)) serves as a container that holds:
- The live `Session` object
- A `ToolRouter` for tool execution
- The `submission_queue` for user inputs
- Bookkeeping flags: `is_active`, `is_processing`, and `last_auto_save_turn`

This wrapper connects the core session logic to the frontend via an `EventBroadcaster`, ensuring real-time event streaming while the session state evolves.

### Core Session Data Structure

The `Session` class in [`agent/core/session.py`](https://github.com/huggingface/ml-intern/blob/main/agent/core/session.py) contains the actual conversation state:
- `turn_count`: Tracks user turns (not internal tool calls)
- `event_queue`: Stores events for frontend broadcasting
- `context_manager`: Maintains message history
- Methods: `increment_turn()`, `auto_save_if_needed()`, and `save_trajectory_local()`

## Turn Tracking and Auto-Save Triggers

The auto-save mechanism triggers at the end of each processed turn inside [`agent/core/agent_loop.py`](https://github.com/huggingface/ml-intern/blob/main/agent/core/agent_loop.py).

### Incrementing the Turn Counter

After completing a user interaction, the `Handlers.run_agent` coroutine increments the counter:

```python

# agent/core/agent_loop.py – after each turn

session.increment_turn()                # Line ~99

await session.auto_save_if_needed()   # Line ~100

```

The `increment_turn` method simply advances the counter:

```python
def increment_turn(self) -> None:
    """Increment turn counter (called after each user interaction)"""
    self.turn_count += 1

```

### The Auto-Save Conditional

The `auto_save_if_needed` method in [`agent/core/session.py`](https://github.com/huggingface/ml-intern/blob/main/agent/core/session.py) checks whether the configured interval has elapsed:

```python
async def auto_save_if_needed(self) -> None:
    """Check if auto-save should trigger and save if so (completely non-blocking)"""
    if not self.config.save_sessions:
        return

    interval = self.config.auto_save_interval
    if interval <= 0:
        return

    turns_since_last_save = self.turn_count - self.last_auto_save_turn
    if turns_since_last_save >= interval:
        logger.info(f"Auto-saving session (turn {self.turn_count})...")
        # Fire-and-forget save

        self.save_and_upload_detached(self.config.session_dataset_repo)
        self.last_auto_save_turn = self.turn_count

```

When `turn_count` exceeds `auto_save_interval` (default **3**), the method calls `save_and_upload_detached()` and updates `last_auto_save_turn` to prevent duplicate saves.

## Local Persistence and Detached Upload Pipeline

The save-and-upload process splits into three distinct stages to ensure the agent loop never waits for network IO.

### Saving Trajectories Locally

The `save_trajectory_local` method serializes the full conversation state to JSON:

```python
def save_trajectory_local(self, directory="session_logs", upload_status="pending", dataset_url=None):
    log_dir = Path(directory)
    log_dir.mkdir(parents=True, exist_ok=True)

    trajectory = self.get_trajectory()
    trajectory["upload_status"] = upload_status
    trajectory["upload_url"] = dataset_url
    trajectory["last_save_time"] = datetime.now().isoformat()

    filename = f"session_{self.session_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
    filepath = log_dir / filename
    with open(filepath, "w") as f:
        json.dump(trajectory, f, indent=2)
    return str(filepath)

```

This creates a file containing messages, events, configuration, and upload metadata under the `session_logs/` directory.

### Spawning the Upload Subprocess

The `save_and_upload_detached` method handles the non-blocking upload trigger:

```python
def save_and_upload_detached(self, repo_id: str) -> Optional[str]:
    # Save locally first

    local_path = self.save_trajectory_local(upload_status="pending")
    if not local_path:
        return None

    # Spawn detached subprocess

    try:
        uploader_script = Path(__file__).parent / "session_uploader.py"
        subprocess.Popen(
            [sys.executable, str(uploader_script), "upload", local_path, repo_id],
            stdin=subprocess.DEVNULL,
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            start_new_session=True,
        )
    except Exception as e:
        logger.warning(f"Failed to spawn upload subprocess: {e}")

    return local_path

```

The `start_new_session=True` flag detaches the child process completely, allowing the main application to continue processing user inputs while the upload proceeds in the background.

### The session_uploader.py Implementation

The standalone script [`agent/core/session_uploader.py`](https://github.com/huggingface/ml-intern/blob/main/agent/core/session_uploader.py) handles the Hugging Face Hub interaction:

```python

# agent/core/session_uploader.py – upload_session_as_file()

api = HfApi()
api.upload_file(
    path_or_fileobj=tmp_path,
    path_in_repo=repo_path,
    repo_id=repo_id,
    repo_type="dataset",
    token=hf_token,
    commit_message=f"Add session {session_id}",
)

```

Each session uploads to `sessions/YYYY-MM-DD/<session_id>.jsonl` within the target dataset. On success, the script updates the local JSON file with `"upload_status": "success"` and the dataset URL. If upload fails, it marks `"upload_status": "failed"`, and the retry logic (invoked during the next `submission_loop` startup) will attempt re-upload.

## Configuration and Environment Setup

Configuration lives in [`agent/config.py`](https://github.com/huggingface/ml-intern/blob/main/agent/config.py) and supports environment variable expansion:

| Setting | Description | Default |
|---------|-------------|---------|
| `save_sessions` | Master toggle for all save/upload behavior | `True` |
| `auto_save_interval` | User turns between saves (0 disables) | `3` |
| `session_dataset_repo` | Target Hugging Face dataset repository | `"akseljoonas/hf-agent-sessions"` |
| `HF_SESSION_UPLOAD_TOKEN` | Environment variable for write-access token | — |

The system uses a write-only token stored in the environment, never hard-coded in source files.

## Code Examples

### Creating a Session Programmatically

```python
from backend.session_manager import session_manager

# Create a session for a user

session_id = await session_manager.create_session(
    user_id="alice",
    hf_token="hf_…",                      # Optional OAuth token

    model="anthropic/claude-sonnet-3.5", # Optional model override

)

print(f"New session: {session_id}")

```

### Sending User Input

```python
await session_manager.submit_user_input(
    session_id, 
    "Explain the difference between BERT and GPT."
)

```

The input enters the `submission_queue`, processes through `Handlers.run_agent`, and automatically triggers the auto-save check after the turn completes.

### Observing Auto-Save Behavior

When the third user turn completes (default interval), the logs show:

```text
INFO:agent.core.session:Auto-saving session (turn 3)...
INFO:agent.core.session:Saved session locally to session_logs/session_…_20240424_153210.json
INFO:agent.core.session:Spawned upload subprocess (detached)

```

### Manual Shutdown Trigger

Shutting down a session forces a final save before cleanup:

```python
await session_manager.shutdown_session(session_id)

# Internally calls Session.save_and_upload_detached()

```

## Summary

- **SessionManager** ([`backend/session_manager.py`](https://github.com/huggingface/ml-intern/blob/main/backend/session_manager.py)) creates and tracks `AgentSession` wrappers, managing lifecycle and capacity limits.
- **Session** ([`agent/core/session.py`](https://github.com/huggingface/ml-intern/blob/main/agent/core/session.py)) maintains the `turn_count` and implements `auto_save_if_needed()`, which triggers every `auto_save_interval` user turns.
- **Local saving** occurs via `save_trajectory_local()`, writing JSON trajectories with upload metadata.
- **Detached uploads** run through [`session_uploader.py`](https://github.com/huggingface/ml-intern/blob/main/session_uploader.py) as a separate process, ensuring non-blocking IO to Hugging Face datasets.
- **Configuration** controls behavior via `save_sessions`, `auto_save_interval`, and environment tokens.

## Frequently Asked Questions

### How often does ML Intern auto-save conversations?

By default, ML Intern auto-saves after every **3 user turns**. This interval is configurable via the `auto_save_interval` setting in [`agent/config.py`](https://github.com/huggingface/ml-intern/blob/main/agent/config.py). Setting the value to `0` disables auto-save entirely, while manual saves still occur during session shutdown.

### Where are conversation trajectories stored before upload?

Trajectories are stored locally as JSON files in the `session_logs/` directory (configurable). Each file includes the full message history, event queue, configuration, and upload status. The local storage acts as a durable buffer; if the Hugging Face upload fails, the pending file remains available for retry on the next application startup.

### Why does ML Intern use a detached subprocess for uploads?

The system uses `subprocess.Popen` with `start_new_session=True` to spawn [`session_uploader.py`](https://github.com/huggingface/ml-intern/blob/main/session_uploader.py) as a detached process. This architecture prevents network IO from blocking the main async agent loop, ensuring that conversation latency remains consistent regardless of upload speed or Hugging Face API availability.

### What permissions does the Hugging Face token require?

The token stored in the `HF_SESSION_UPLOAD_TOKEN` environment variable requires **write access** to the dataset repository specified in `session_dataset_repo`. The token is passed securely to [`session_uploader.py`](https://github.com/huggingface/ml-intern/blob/main/session_uploader.py) and never hard-coded in the source repository, following the principle of least privilege for API credentials.