Custom Conversation Processor
Replace the managed LLM with browser-owned conversation logic.
When to use this pattern
Use a custom conversation processor when you want Liforma to handle microphone capture, turn orchestration, avatar speech, and history — but your app owns the "brain". Examples: rule-based receptionists, keyword routing, client-side rubrics, or streaming text you generate in the browser without exposing API keys.
Registered server processors (opaque processorId with encrypted credentials) are a
separate upcoming capability. This guide covers the browser function only.
Session setup
Pass conversationProcessor on Experience.startSession(). The SDK mints a
session with responseMode: 'custom' and never serializes your function to the API.
Typical pairing: mode: 'conversation' and speechInputMode: 'auto'.
const experience = await Experience.startSession({
experienceId: 'exp_01DEMO1SPANISHCAFE',
mode: 'conversation',
speechInputMode: 'auto',
conversationProcessor: async ({ text, conversation, signal }) => {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
if (text.toLowerCase().includes('checkout')) {
return 'Checkout is at 11am. Need a late checkout?';
}
if (text.toLowerCase().includes('breakfast')) {
return 'Breakfast is served until 10 in the lounge.';
}
return 'I can help with checkout, breakfast, or directions.';
}
});
experience.on('conversationProcessorError', ({ utteranceId, message }) => {
console.error('Processor failed', utteranceId, message);
});
await experience.attach({ container: '#avatar' }); Processor contract
Your function receives:
text— finalized user utteranceutteranceId— stable id for this captureconversation— immutable snapshot ofConversationMessage[]signal—AbortSignalcancelled on interrupt or destroy
Return one of:
- A string — spoken via
speak() { text, characterId?, behavior? }— optional character override and enqueue vs interruptAsyncIterable<string>— streamed chunks; the player speaks sentence segments as they arrive
conversationProcessor: async function* ({ text, signal }) {
const chunks = buildStoryChunks(text);
for (const chunk of chunks) {
if (signal.aborted) throw new DOMException('Aborted', 'AbortError');
yield chunk;
}
} Errors and cancellation
Processor failures emit conversationProcessorError. Liforma does not fall back to the
managed LLM. Handle retries or speak a fallback line in your handler.
experience.on('conversationProcessorError', ({ utteranceId, message }) => {
showErrorBanner(message);
});
// speak({ behavior: 'interrupt' }) aborts an in-flight processor via signal. Transcript observation
Use onUserTranscript or userTranscript events for live captions. Partial
updates (isFinal: false) are ephemeral UI data; only finals commit to history and
trigger the processor.
experience.on('userTranscript', (update) => {
if (!update.isFinal) {
liveCaptionEl.textContent = update.text;
return;
}
liveCaptionEl.textContent = update.text;
commitUtterance(update.utteranceId, update.text);
});
experience.on('userSpeechStarted', () => micIndicator.classList.add('active'));
experience.on('userSpeechEnded', () => micIndicator.classList.remove('active')); Related guides
- Listen Once Capture — host-owned turns with
listenOnce()instead of automatic processor orchestration - Guided Scripted Practice — presenter mode without a processor
- Experience API — full method reference