ML Intern Event System: Complete Guide to Event Types and Consumption

The ML Intern event system emits 16 distinct Server-Sent Events (SSE) including ready, assistant_chunk, tool_call, and approval_required, which can be consumed either directly via EventSource or through the SSEChatTransport abstraction that integrates with the Vercel AI SDK.

The huggingface/ml-intern repository implements a robust event-driven architecture to communicate agent session state from backend to frontend. Understanding the ML Intern event system is essential for building custom UI integrations or debugging agent workflows, as every user interaction, tool execution, and state change is propagated via typed JSON payloads over SSE streams.

Event Types Emitted by the ML Intern Event System

The backend emits events through an asyncio.Queue managed by the EventBroadcaster class in backend/session_manager.py. Each event is a JSON object containing an event_type string and an optional data payload. The following 16 event types are defined in frontend/src/types/events.ts and emitted throughout the agent lifecycle:

  • ready – Signals that the session has been created and the agent is ready to receive input. Payload: { message: string }.
  • processing – Indicates the agent has started processing a turn (user message, approval, etc.). Payload: {}.
  • assistant_message – Delivers the complete assistant response when streaming is disabled. Payload: { content: string }.
  • assistant_chunk – Streams a fragment of the assistant’s reply token-by-token. Payload: { content: string }.
  • assistant_stream_end – Marks the end of a streaming response. Payload: {}.
  • tool_call – Notifies when the LLM requests a tool execution. Payload: { tool: string, arguments: Record<string, any>, tool_call_id?: string }.
  • tool_output – Returns the result of a tool execution. Payload: { tool: string, output: string, success: boolean, tool_call_id?: string }.
  • tool_log – Emits log lines from a running tool process. Payload: { tool: string, log: string, agent_id?: string, label?: string }.
  • tool_state_change – Reports status changes (running, rejected, cancelled, etc.). Payload: { tool: string, state: string, tool_call_id?: string, jobUrl?: string }.
  • approval_required – Requests user approval before executing one or more tool calls. Payload: { tools: Array<{tool:string, arguments:Record<string,any>, tool_call_id:string}>, count: number }.
  • turn_complete – Indicates the entire turn (including all tool calls) is finished. Payload: { history_size: number }.
  • compacted – Informs that context has been compacted (summarized) to save tokens. Payload: { old_tokens: number, new_tokens: number }.
  • error – Signals an unrecoverable error in the session. Payload: { error: string }.
  • shutdown – Sent when the session is being shut down. Payload: {}.
  • interrupted – Indicates the user manually stopped the session. Payload: {}.
  • undo_complete – Confirms an undo operation succeeded. Payload: {}.
  • plan_update – Updates the internal plan of upcoming tool calls. Payload: { plan: [{ id:string, content:string, status:"pending"|"in_progress"|"completed" }] }.

These events are pushed to the session’s event queue via session.send_event(Event(event_type=..., data=...)) as implemented in backend/session_manager.py (lines 322-324, 347-348).

How to Consume ML Intern Events

Low-Level SSE Subscription with EventSource

For direct access to the raw event stream, subscribe to the GET /api/events/{session_id} endpoint using the browser’s native EventSource API. This endpoint is implemented in backend/routes/agent.py (lines 605-610) and creates a subscriber via EventBroadcaster.subscribe().

import { useEffect } from 'react';

function useAgentEvents(sessionId: string) {
  useEffect(() => {
    const source = new EventSource(`/api/events/${sessionId}`);

    source.onmessage = (e) => {
      const evt = JSON.parse(e.data) as { event_type: string; data?: any };
      switch (evt.event_type) {
        case 'ready':
          console.log('Agent ready →', evt.data?.message);
          break;
        case 'assistant_chunk':
          console.log('Streaming →', evt.data?.content);
          break;
        case 'tool_log':
          console.log(`[${evt.data?.tool}]`, evt.data?.log);
          break;
        case 'error':
          console.error('Session error:', evt.data?.error);
          break;
      }
    };

    source.onerror = (err) => {
      console.error('SSE error', err);
      source.close();
    };

    return () => source.close();
  }, [sessionId]);
}

High-Level Integration with SSEChatTransport

For applications using the Vercel AI SDK, the repository provides SSEChatTransport in frontend/src/lib/sse-chat-transport.ts. This class parses the SSE stream, converts chat-related events into UIMessageChunks, and forwards side-channel events to a callbacks object.

import { SSEChatTransport } from '@/lib/sse-chat-transport';

const sideCallbacks = {
  onReady: () => console.log('Ready'),
  onProcessing: () => console.log('Processing…'),
  onToolLog: (tool: string, log: string) => console.log(`Tool ${tool}: ${log}`),
  onApprovalRequired: (tools: any[]) => {
    /* Render approval UI */
  },
  onError: (msg: string) => console.error('Agent error', msg),
};

const transport = new SSEChatTransport(sessionId, sideCallbacks);

// Send a message to trigger the event stream
const stream = await transport.sendMessages({
  trigger: 'submit-message',
  chatId: 'main',
  messages: [{ role: 'user', parts: [{ type: 'text', text: 'Hello' }] }],
});

The transport’s internal switch block (lines 91-266 in sse-chat-transport.ts) handles the mapping between raw SSE events and the SideChannelCallbacks interface.

Backend Event Emission

To emit custom events from the backend, access the session instance and call send_event() with an Event object:

from your_module.events import Event

# Inside an async context with a Session instance `session`

await session.send_event(
    Event(event_type="my_custom_event", data={"info": "extra"})
)

Subscribed clients receive the payload as: data: {"event_type":"my_custom_event","data":{"info":"extra"}}.

Key Source Files and Implementation Details

Understanding the following files is crucial for advanced customization of the ML Intern event system:

Summary

  • The ML Intern event system emits 16 distinct event types via Server-Sent Events to communicate agent state, tool execution, and errors.
  • Events are defined in frontend/src/types/events.ts and emitted through session.send_event() in the backend.
  • Consume events directly using the EventSource API against /api/events/{session_id} for low-level control.
  • Use SSEChatTransport for high-level integration with the Vercel AI SDK and automatic UI message chunking.
  • The EventBroadcaster in backend/session_manager.py manages the message queue and subscriber distribution.

Frequently Asked Questions

What are the main event types in the ML Intern event system?

The system emits lifecycle events (ready, processing, turn_complete), messaging events (assistant_chunk, assistant_message, assistant_stream_end), tool events (tool_call, tool_output, tool_log, tool_state_change), control events (approval_required, interrupted, undo_complete), and error events (error, shutdown). Each event carries a typed payload defined in frontend/src/types/events.ts.

How do I listen to ML Intern events from a React frontend?

Create an EventSource pointing to /api/events/${sessionId} and attach an onmessage handler to parse the JSON payload. Alternatively, instantiate SSEChatTransport from frontend/src/lib/sse-chat-transport.ts with a SideChannelCallbacks object to receive typed events and automatic Vercel AI SDK integration.

Can I emit custom events from the ML Intern backend?

Yes. Within any async context holding a Session reference, call await session.send_event(Event(event_type="custom_name", data={...})). The event is immediately pushed to the session’s event_queue and broadcast to all connected SSE clients via EventBroadcaster.

What is the difference between assistant_chunk and assistant_message?

assistant_chunk fires multiple times during streaming mode to deliver token-by-token updates (payload: { content: string }), while assistant_message fires once to deliver the complete response when streaming is disabled. The assistant_stream_end event marks the conclusion of a streaming sequence.

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 →