Skip to main content

Message Types

STELLA uses structured messages for communication between agents and the frontend. Messages are sent over LiveKit data channels.

Overview

All messages follow this structure:

interface Message {
type: string; // Message type identifier
timestamp: number; // Unix timestamp in milliseconds
data: any; // Type-specific payload
}

Transcript Messages

Used for speech transcription (both interim and final).

Schema

interface TranscriptMessage {
type: "transcript";
timestamp: number;
data: {
speaker: "user" | "assistant";
text: string;
isFinal: boolean;
confidence?: number; // 0-1, STT confidence
language?: string; // ISO language code
};
}

Usage (Agent)

# Send interim transcript (while user is speaking)
await self.send_transcript(
text="Hello, I would like to",
speaker="user",
is_final=False
)

# Send final transcript
await self.send_transcript(
text="Hello, I would like to order a pizza",
speaker="user",
is_final=True
)

# Send assistant response
await self.send_transcript(
text="I'd be happy to help you order a pizza!",
speaker="assistant",
is_final=True
)

Usage (Frontend)

room.on('dataReceived', (payload) => {
const message = JSON.parse(new TextDecoder().decode(payload));

if (message.type === 'transcript') {
const { speaker, text, isFinal } = message.data;

if (speaker === 'user') {
if (isFinal) {
addMessage({ speaker: 'user', text });
} else {
updateInterimTranscript(text);
}
} else {
addMessage({ speaker: 'assistant', text });
}
}
});

Status Messages

Indicate the agent's current state.

Schema

interface StatusMessage {
type: "status";
timestamp: number;
data: {
status: "listening" | "thinking" | "speaking" | "idle";
message?: string; // Optional descriptive message
};
}

Status Values

StatusDescriptionUI Indication
listeningWaiting for user inputMicrophone active
thinkingProcessing user inputLoading indicator
speakingPlaying TTS audioSpeaker animation
idleNot actively engagedReady state

Usage (Agent)

# Indicate processing
await self.send_status("thinking", "Analyzing your request...")

# Indicate speaking
await self.send_status("speaking")

# Indicate listening
await self.send_status("listening")

Usage (Frontend)

function AgentStatus({ status, message }) {
const indicators = {
listening: { icon: <MicIcon />, label: 'Listening' },
thinking: { icon: <Spinner />, label: message || 'Thinking' },
speaking: { icon: <SpeakerIcon />, label: 'Speaking' },
idle: { icon: <CheckIcon />, label: 'Ready' },
};

const current = indicators[status];

return (
<div className="agent-status">
{current.icon}
<span>{current.label}</span>
</div>
);
}

Progress Messages

Show task progress with a todo list.

Schema

interface ProgressMessage {
type: "progress";
timestamp: number;
data: {
todos: TodoItem[];
};
}

interface TodoItem {
id: string;
description: string;
status: "pending" | "in_progress" | "completed";
}

Usage (Agent)

from stella_sdk import TodoItem

# Initialize todo list
await self.update_todo([
TodoItem(id="1", description="Understand request", status="in_progress"),
TodoItem(id="2", description="Search database", status="pending"),
TodoItem(id="3", description="Generate response", status="pending"),
])

# Update progress
await self.complete_task("1")
await self.start_task("2")

Usage (Frontend)

function ProgressList({ todos }) {
return (
<ul className="progress-list">
{todos.map(todo => (
<li key={todo.id} className={`todo-${todo.status}`}>
<StatusIcon status={todo.status} />
<span>{todo.description}</span>
</li>
))}
</ul>
);
}

Control Messages

Commands from frontend to agent.

Schema

interface ControlMessage {
type: "control";
timestamp: number;
data: {
action: "interrupt" | "pause" | "resume" | "stop";
payload?: any;
};
}

Actions

ActionDescription
interruptStop current TTS playback
pausePause the conversation
resumeResume the conversation
stopEnd the session

Usage (Frontend)

// Interrupt agent speech
room.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify({
type: 'control',
data: { action: 'interrupt' }
}))
);

Usage (Agent)

async def on_data_message(self, message: dict):
if message.get("type") == "control":
action = message["data"]["action"]

if action == "interrupt":
await self.pipeline.cancel_tts()
await self.send_status("listening")

elif action == "pause":
self.is_paused = True

elif action == "resume":
self.is_paused = False
await self.send_status("listening")

User Text Messages

Text input from the user (alternative to voice).

Schema

interface UserTextMessage {
type: "user_text";
timestamp: number;
data: string; // The text message
}

Usage (Frontend)

function sendTextMessage(text: string) {
room.localParticipant.publishData(
new TextEncoder().encode(JSON.stringify({
type: 'user_text',
timestamp: Date.now(),
data: text
}))
);
}

Usage (Agent)

async def on_data_message(self, message: dict):
if message.get("type") == "user_text":
text = message["data"]
# Handle same as voice transcript
await self.on_transcript(text, is_final=True)

Custom Messages

You can define custom message types for application-specific needs.

Example: Form Data

// Frontend sends form data
interface FormDataMessage {
type: "form_data";
timestamp: number;
data: {
formId: string;
fields: Record<string, any>;
};
}
# Agent handles form data
async def on_data_message(self, message: dict):
if message.get("type") == "form_data":
form_id = message["data"]["formId"]
fields = message["data"]["fields"]
await self.process_form(form_id, fields)

Example: UI Updates

// Agent sends UI update
interface UIUpdateMessage {
type: "ui_update";
timestamp: number;
data: {
component: string;
props: Record<string, any>;
};
}

Message Encoding

Messages are serialized as JSON and encoded as UTF-8:

// Sending
const message = { type: 'transcript', data: { ... } };
const encoded = new TextEncoder().encode(JSON.stringify(message));
room.localParticipant.publishData(encoded);

// Receiving
room.on('dataReceived', (payload) => {
const decoded = new TextDecoder().decode(payload);
const message = JSON.parse(decoded);
});

Next Steps