Haygen

Decisions/ADR-003-lite-mode-accumulation.md

ADR-003: Lite Mode Uses USER_TALKING_MESSAGE Accumulation Pattern

Decision

The Lite mode conversation loop (attachLiteLoop) accumulates text from USER_TALKING_MESSAGE events into pendingTranscript. On USER_END_MESSAGE, it drains pendingTranscript as the final user utterance. It does NOT read event.message on USER_END_MESSAGE.

Context

The implementation plan originally assumed USER_END_MESSAGE carried the final transcript in an event.message field. Reading the actual SDK type definitions revealed:

// UserTalkingEndEvent
interface UserTalkingEndEvent {
  type: string
  task_id: string
  // NO message field
}

USER_END_MESSAGE only signals "user stopped speaking." The transcript lives in USER_TALKING_MESSAGE events (which fire incrementally with partial/full transcript chunks).

Implementation

let pendingTranscript = ''

function handleUserTalking(event: { message: string }) {
  const text = event.message?.trim()
  if (text) pendingTranscript = text
}

async function handleUserEnd() {
  const userText = pendingTranscript.trim()
  pendingTranscript = ''
  if (!userText) return
  // → call LLM → speak
}

Consequence

The last USER_TALKING_MESSAGE before USER_END_MESSAGE becomes the final transcript. If HeyGen ever adds a message field to UserTalkingEndEvent, this pattern still works correctly (we just ignore the new field).