molroomolroo
Guides

WebSocket Real-time

Set up persistent WebSocket connections for live emotion updates.

WebSocket Real-time

For real-time applications like chatbots, games, and interactive experiences, WebSocket connections provide lower latency and server-pushed updates compared to REST polling. This guide walks through connecting, sending turns, handling proactive actions, and reconnection.

Connection Setup

Open a WebSocket connection by providing your session ID and API key:

JavaScript

const ws = new WebSocket(
  'wss://api.molroo.io/v1/ws/SESSION_ID?token=YOUR_API_KEY'
)
 
ws.onopen = () => console.log('Connected')
 
ws.onmessage = (event) => {
  const msg = JSON.parse(event.data)
 
  switch (msg.type) {
    case 'connected':
      console.log('Session:', msg.session_id)
      break
    case 'turn_result':
      handleEmotion(msg.data)
      break
    case 'proactive':
      handleProactive(msg.data)
      break
    case 'missed_proactive':
      handleMissed(msg.data)
      break
  }
}
 
ws.onerror = (err) => console.error('WebSocket error:', err)
ws.onclose = (event) => console.log('Disconnected:', event.code, event.reason)

Connection URL Format

wss://api.molroo.io/v1/ws/{sessionId}?token={apiKey}
ParameterDescription
sessionIdThe session ID to connect to
tokenYour API key (mk_live_ or mk_test_)

On successful connection, the server sends a connected message:

{
  "type": "connected",
  "session_id": "session_xyz"
}

Sending Turns

Send a turn through the WebSocket by sending a JSON message with type: "turn":

ws.send(JSON.stringify({
  type: 'turn',
  message: 'Hello! How are you today?'
}))

The server responds with a turn_result message containing the same data as the REST POST /v1/turn endpoint:

{
  "type": "turn_result",
  "data": {
    "appraisal": { ... },
    "new_emotion": { "V": 0.45, "A": 0.52, "D": 0.18 },
    "discrete_emotion": "joy",
    "emotion_intensity": 0.58,
    "prompt_data": {
      "formatted": "..."
    }
  }
}

Sending Appraisals

You can also send direct appraisal vectors over WebSocket:

ws.send(JSON.stringify({
  type: 'appraisal',
  appraisal: {
    goal_relevance: 0.8,
    goal_congruence: 0.7,
    expectedness: 0.4,
    controllability: 0.6,
    agency: 0.3,
    norm_compatibility: 0.5
  },
  context: 'Player completed a side quest'
}))

Proactive Actions

molroo's emotion engine can push proactive actions when the character's internal state triggers behavior without user input -- for example, when stress builds up or a need goes unmet.

Enabling Proactive Behavior

Proactive behavior is configured at the session level. When enabled, the server pushes proactive messages:

{
  "type": "proactive",
  "data": {
    "action": "initiate_conversation",
    "reason": "belonging_need_unmet",
    "emotion": { "V": -0.2, "A": 0.6, "D": -0.1 },
    "discrete_emotion": "loneliness",
    "prompt_data": {
      "formatted": "..."
    }
  }
}

Handling Proactive Actions

function handleProactive(data) {
  switch (data.action) {
    case 'initiate_conversation':
      // Character wants to reach out
      showNotification(`${characterName} wants to talk to you`)
      break
    case 'express_emotion':
      // Character needs to express built-up emotion
      displayExpression(data.emotion, data.discrete_emotion)
      break
    case 'seek_comfort':
      // Character is distressed and seeking support
      triggerComfortSequence(data)
      break
  }
}

Acknowledging Proactive Actions

After handling a proactive action, acknowledge it so the engine knows it was delivered:

ws.send(JSON.stringify({
  type: 'ack_proactive',
  action_id: data.action_id
}))

Reconnection

Connections can drop due to network issues, server restarts, or client inactivity. Implement automatic reconnection with exponential backoff:

function createConnection(sessionId, apiKey) {
  let ws
  let retries = 0
  const maxRetries = 10
 
  function connect() {
    ws = new WebSocket(
      `wss://api.molroo.io/v1/ws/${sessionId}?token=${apiKey}`
    )
 
    ws.onopen = () => {
      console.log('Connected')
      retries = 0 // reset on successful connection
    }
 
    ws.onmessage = (event) => {
      const msg = JSON.parse(event.data)
 
      if (msg.type === 'missed_proactive') {
        // Missed proactive actions are delivered on reconnect
        msg.data.forEach(handleProactive)
        return
      }
 
      // Handle other message types...
    }
 
    ws.onclose = (event) => {
      if (event.code === 1000) return // normal close
 
      if (retries < maxRetries) {
        const delay = Math.min(1000 * 2 ** retries, 30000)
        const jitter = delay * (0.5 + Math.random() * 0.5)
        retries++
        console.log(`Reconnecting in ${Math.round(jitter)}ms (attempt ${retries})`)
        setTimeout(connect, jitter)
      }
    }
  }
 
  connect()
  return {
    send: (data) => ws.send(JSON.stringify(data)),
    close: () => ws.close(1000, 'Client closing'),
  }
}

Missed Proactive Actions

When you reconnect after a disconnection, any proactive actions that fired while the client was offline are automatically delivered as a missed_proactive message:

{
  "type": "missed_proactive",
  "data": [
    {
      "action": "initiate_conversation",
      "reason": "belonging_need_unmet",
      "timestamp": 1700000000000,
      "emotion": { "V": -0.2, "A": 0.6, "D": -0.1 },
      "prompt_data": { "formatted": "..." }
    }
  ]
}

This ensures no character-initiated events are lost during connection gaps.

Heartbeat

The server sends periodic ping frames. The WebSocket protocol handles pong responses automatically in browsers. If you are using a server-side WebSocket client, make sure your library responds to pings:

// Node.js with the 'ws' library
const WebSocket = require('ws')
const ws = new WebSocket('wss://api.molroo.io/v1/ws/SESSION_ID?token=KEY')
 
ws.on('ping', () => {
  ws.pong()
})

If the server does not receive a pong within 30 seconds, the connection is terminated.

Complete Example

A full real-time chat integration with reconnection and proactive handling:

const MOLROO_KEY = 'mk_live_abc123'
const SESSION_ID = 'session_001'
 
const conn = createConnection(SESSION_ID, MOLROO_KEY)
 
// Send a user message
function onUserMessage(text) {
  conn.send({ type: 'turn', message: text })
}
 
// Handle incoming messages
function handleMessage(msg) {
  switch (msg.type) {
    case 'turn_result': {
      const { prompt_data, discrete_emotion } = msg.data
      // Feed prompt_data.formatted into your LLM
      generateReply(prompt_data.formatted, discrete_emotion)
      break
    }
    case 'proactive': {
      // Character initiated an action
      handleProactive(msg.data)
      conn.send({ type: 'ack_proactive', action_id: msg.data.action_id })
      break
    }
    case 'missed_proactive': {
      // Catch up on missed actions after reconnect
      msg.data.forEach(handleProactive)
      break
    }
  }
}

When to Use WebSocket vs REST

Use CaseRecommendedWhy
Real-time chatbotsWebSocketLow latency, server-pushed proactive actions
Games with NPCsWebSocketFrequent updates, persistent connection
Interactive experiencesWebSocketBidirectional communication, live emotion updates
Batch processingRESTStateless, no persistent connection needed
Serverless functionsRESTFunctions are ephemeral, cannot hold connections
Simple integrationsRESTEasier to implement, fewer moving parts
Infrequent interactionsRESTNo need to maintain a connection

Rule of thumb: If your user is actively interacting and expects real-time feedback, use WebSocket. If you are processing events in the background or on a schedule, use REST.