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

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


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

Incrementing the Turn Counter

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


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

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 checks whether the configured interval has elapsed:

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:

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:

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 handles the Hugging Face Hub interaction:


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

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

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:

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:

await session_manager.shutdown_session(session_id)

# Internally calls Session.save_and_upload_detached()

Summary

  • SessionManager (backend/session_manager.py) creates and tracks AgentSession wrappers, managing lifecycle and capacity limits.
  • Session (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 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. 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 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 and never hard-coded in the source repository, following the principle of least privilege for API credentials.

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 →