Install the Layla SDK from npm:
npm install @layla-network/sdkImport the SDK from the package root:
import LaylaSDK, {
Layla,
LaylaAbortError,
LaylaBridgeUnavailableError,
LaylaError,
installLaylaMock,
makeMockCharacter,
type LaylaChatMessage,
type LaylaChatHistoryEntry,
type LaylaScheduledChatMessage,
type LaylaMemory,
type LaylaPersona,
type LaylaTTSVoice,
type GenerateVoiceToFileResult,
type BackgroundAudioMetadata,
type BackgroundAudioStatusListener,
type BackgroundAudioTrackChangedListener,
type BackgroundAudioFinishedListener,
type LaylaExecutionContext,
type ChatContextFinishedSpeaking,
type ChatContextFinishedSpeakingListener,
type ChatContextNewMessage,
type ChatContextNewMessageListener,
type ChatContextSentimentUpdate,
type ChatContextSentimentUpdateListener,
type ChatContextStartedSpeaking,
type ChatContextStartedSpeakingListener,
type ChatContextStartedThinking,
type ChatContextStartedThinkingListener,
type LaylaApiSaveChatMessage,
type LaylaApiScheduledChatMessage,
type LaylaApiGetScheduledChatMessages,
type LaylaApiCancelScheduledChatMessage,
type LaylaApiSaveFile,
type LaylaApiReadFile,
type LaylaApiGetMemories,
type LaylaApiGetTopMemories,
type LaylaApiCreateOrUpdateMemories,
type LaylaApiGetPersona,
type LaylaApiGetTTSVoices,
type LaylaApiGenerateVoice,
type LaylaApiGenerateVoiceToFile,
type LaylaApiStopSpeaking,
type LaylaApiStartBackgroundAudioPlayer,
type LaylaApiStopBackgroundAudioPlayer,
type LaylaApiPauseBackgroundAudioPlayer,
type LaylaApiResumeBackgroundAudioPlayer,
type LaylaApiSkipBackgroundAudioTrack,
type LaylaApiGetInferenceEngines,
type LaylaApiSetInferenceEngine,
type LaylaApiGetExecutionContext,
type LaylaApiEvent_onGetChatSessionsResponse,
type LaylaApiEvent_onSaveChatMessageResponse,
type LaylaApiEvent_onScheduledChatMessage,
type LaylaApiEvent_onGetScheduledChatMessagesResponse,
type LaylaApiEvent_onCancelScheduledChatMessage,
type LaylaApiEvent_onGetMemoriesResponse,
type LaylaApiEvent_onGetTopMemoriesResponse,
type LaylaApiEvent_onCreateOrUpdateMemoriesResponse,
type LaylaApiEvent_onGetPersonaResponse,
type LaylaApiEvent_onGetTTSVoicesResponse,
type LaylaApiEvent_onGetInferenceEnginesResponse,
type LaylaApiEvent_onSetInferenceEngineResponse,
type LaylaApiEvent_onGetExecutionContextResponse,
type LaylaApiEvent_onChatContextFinishedSpeaking,
type LaylaApiEvent_onChatContextNewMessage,
type LaylaApiEvent_onChatContextSentimentUpdate,
type LaylaApiEvent_onChatContextStartedSpeaking,
type LaylaApiEvent_onChatContextStartedThinking,
type LaylaApiEvent_onFinishedSpeaking,
type LaylaApiEvent_onGenerateVoiceToFileResponse,
type LaylaApiEvent_onBackgroundAudioTrackChanged,
type LaylaApiEvent_onBackgroundAudioStatus,
type LaylaApiEvent_onBackgroundAudioFinished,
type LaylaApiEvent_onSaveFileResponse,
type LaylaApiEvent_onReadFileResponse,
type LaylaCharacter,
type MemoryListOptions,
type ReadFileResult,
type SaveFileResult,
type SentimentValues,
type TavernCardV2,
} from '@layla-network/sdk';LaylaSDK, Layla, and the default export are aliases for the same client class. Most apps should create one client and reuse it.
const layla = new LaylaSDK();The SDK must run inside the Layla WebView for real host calls. It does not use an API key, base URL, or HTTP endpoint. Instead, it wraps the WebView bridge between the mini-app and the Layla React Native host.
Creates the SDK client.
import { LaylaSDK } from '@layla-network/sdk';
const layla = new LaylaSDK();The constructor accepts an optional LaylaSDKOptions object.
const layla = new LaylaSDK({
model: 'layla',
});model is reserved for compatibility and future use. The Layla host chooses the actual model.
Returns the context in which the host launched the mini-app. A contextual
mini-app receives the current character and chat session. The context always
includes app_version, the version of the Layla app hosting the mini-app. For
a standalone top-level mini-app, character and session_id are both null.
const context = await layla.contextual.getExecutionContext();
console.log(context.app_version);
console.log(context.character?.data.data.name);
console.log(context.session_id);The character and session fields may each be null when that part of the
context is not active. Detect standalone mode by checking both fields. Pass an
abort signal as the first argument:
const context = await layla.contextual.getExecutionContext({
signal: controller.signal,
});Listens for messages that the host adds to the surrounding character chat. This pushed event is available only when the mini-app is running in a character chat context. Its payload includes the message, character ID, session ID, and timestamp.
import type { ChatContextNewMessageListener } from '@layla-network/sdk';
const onNewMessage: ChatContextNewMessageListener = ({
message,
character_id,
session_id,
timestamp,
}) => {
console.log(message.role, message.content);
console.log(character_id, session_id, timestamp);
};
layla.contextual.on('chatContextNewMessage', onNewMessage);
// Remove the exact listener when the UI is disposed.
layla.contextual.off('chatContextNewMessage', onNewMessage);Contextual mini-apps can also react to the surrounding character's sentiment, speech, and thinking state:
import type {
ChatContextFinishedSpeakingListener,
ChatContextSentimentUpdateListener,
ChatContextStartedSpeakingListener,
ChatContextStartedThinkingListener,
} from '@layla-network/sdk';
const onSentimentUpdate: ChatContextSentimentUpdateListener = ({ sentiment }) => {
setExpression(sentiment);
};
const onStartedSpeaking: ChatContextStartedSpeakingListener = () => {
setSpeaking(true);
};
const onFinishedSpeaking: ChatContextFinishedSpeakingListener = () => {
setSpeaking(false);
};
const onStartedThinking: ChatContextStartedThinkingListener = () => {
setThinking(true);
};
layla.contextual.on('chatContextSentimentUpdate', onSentimentUpdate);
layla.contextual.on('chatContextStartedSpeaking', onStartedSpeaking);
layla.contextual.on('chatContextFinishedSpeaking', onFinishedSpeaking);
layla.contextual.on('chatContextStartedThinking', onStartedThinking);
layla.contextual.off('chatContextSentimentUpdate', onSentimentUpdate);
layla.contextual.off('chatContextStartedSpeaking', onStartedSpeaking);
layla.contextual.off('chatContextFinishedSpeaking', onFinishedSpeaking);
layla.contextual.off('chatContextStartedThinking', onStartedThinking);chatContextSentimentUpdate receives { sentiment }, where sentiment is a
key of SentimentValues. The speaking and thinking lifecycle listeners receive
null; listeners that do not need it can declare no parameters, as above.
The host uses the shared wire event on_finished_speaking for both contextual
speech completion and TTS playback completion. Consequently,
chatContextFinishedSpeaking can also fire when a TTS request finishes; use the
event as a speech-finished signal rather than as a uniquely identifiable source.
Creates a chat completion. Use this when you only need the final assistant message.
import { LaylaSDK, type LaylaChatMessage } from '@layla-network/sdk';
const layla = new LaylaSDK();
const messages: LaylaChatMessage[] = [
{ role: 'system', content: 'You are concise and helpful.' },
{ role: 'user', content: 'Give me one chess tip.' },
];
const completion = await layla.chat.completions.create({
messages,
});
const text = completion.choices[0]?.message.content ?? '';You may pass model for OpenAI-shaped compatibility.
await layla.chat.completions.create({
model: 'layla',
messages,
});Starts a streaming chat completion and returns a ChatCompletionStream immediately. Use this for UI that updates while Layla is generating.
const stream = layla.chat.completions.stream({
messages: [
{ role: 'user', content: 'Tell me a short story.' },
],
});
stream.on('content', (_delta, snapshot) => {
setAssistantText(snapshot);
});
stream.on('end', () => {
setBusy(false);
});
stream.on('error', (error) => {
showError(error.message);
setBusy(false);
});
const finalText = await stream.finalContent();You can also stream with async iteration:
const stream = layla.chat.completions.stream({ messages });
for await (const chunk of stream) {
const choice = chunk.choices[0];
const content = choice?.delta.content ?? '';
const reasoning = choice?.delta.reasoning ?? '';
appendToAssistantMessage(content);
appendToReasoningPanel(reasoning);
}Breaking out of the for await loop aborts the stream.
When the Layla host wraps generated text in <think> and </think> tags, the
SDK removes those tags from the visible assistant content. Text inside the tags
is streamed as choices[0].delta.reasoning and returned on the final
choices[0].message.reasoning; text outside the tags remains in
choices[0].delta.content and choices[0].message.content.
Returns a streaming completion using the OpenAI-style stream: true option.
const stream = await layla.chat.completions.create({
messages,
stream: true,
});
stream.on('content', (_delta, snapshot) => {
setAssistantText(snapshot);
});Fetches the inference engines available for subsequent chat completions.
const engines = await layla.chat.getInferenceEngines();
for (const engineName of engines) {
console.log(engineName);
}Pass an abort signal as the first argument:
const engines = await layla.chat.getInferenceEngines({
signal: controller.signal,
});Selects the inference engine used for subsequent chat completions. Use a name
returned by getInferenceEngines(), or pass null to reset to the host's
default engine.
const engines = await layla.chat.getInferenceEngines();
const result = await layla.chat.setInferenceEngine(engines[0] ?? null);
if (!result.success) {
throw new Error('The requested inference engine is unavailable.');
}
console.log(result.engineName); // selected name, or null for the defaultThe host returns success: false and resets to its default engine when the
requested name is not found.
Pass an abort signal as the second argument:
await layla.chat.setInferenceEngine('local-engine', {
signal: controller.signal,
});The stream object supports event listeners, async iteration, final result helpers, and abort.
const stream = layla.chat.completions.stream({ messages });
const logContent = (delta: string, snapshot: string) => {
console.log(delta, snapshot);
};
stream.on('chunk', (chunk) => {
console.log(chunk.choices[0]?.delta.content ?? '');
console.log(chunk.choices[0]?.delta.reasoning ?? '');
});
stream.on('content', logContent);
stream.on('reasoning', (delta, snapshot) => {
console.log(delta, snapshot);
});
stream.off('content', logContent);
const fullText = await stream.finalContent();
const completion = await stream.finalChatCompletion();Abort an active stream from a stop button:
const stream = layla.chat.completions.stream({ messages });
stopButton.onclick = () => {
stream.abort();
};Lists available Layla characters. offset defaults to 0, and range defaults to 10.
const characters = await layla.characters.list();
for (const character of characters) {
console.log(character.id, character.data.data.name);
}Request a specific page:
const pageSize = 10;
const page = 2;
const characters = await layla.characters.list(page * pageSize, pageSize);Pass an abort signal as the third argument:
const controller = new AbortController();
const characters = await layla.characters.list(0, 10, {
signal: controller.signal,
});Gets a character portrait. The returned value is a ready-to-use image source string, or null if no image is available.
const characters = await layla.characters.list(0, 1);
const character = characters[0];
const imageSrc = await layla.characters.getImage(character.id);
if (imageSrc) {
imageElement.src = imageSrc;
}Fetches a character's chat sessions. Results come back as an object containing the character_id and a sessions array in reverse chronological order.
const { sessions } = await layla.chat.getChatSessions(character.id);
for (const session of sessions) {
console.log(
session.session_id,
session.last_message_timestamp,
session.last_message_content,
);
}Use offset and range when you need to page through a longer session list. Pass an abort signal as the fourth argument.
const sessionsPage = await layla.chat.getChatSessions(character.id, 10, 10, {
signal: controller.signal,
});Fetches the newest chat messages for a specific chat session. Results come back as a paged array of LaylaChatHistoryEntry items in reverse chronological order.
const { sessions } = await layla.chat.getChatSessions(character.id, 0, 1);
const sessionId = sessions[0]?.session_id;
const history = sessionId
? await layla.chat.getChatHistory(sessionId)
: [];
for (const entry of history) {
console.log(entry.role, entry.content);
}Use offset and range when you need to page through a longer transcript. Pass an abort signal as the fourth argument.
const historyPage = await layla.chat.getChatHistory(sessionId, 20, 10, {
signal: controller.signal,
});Creates or updates a message in chat history and returns the saved
LaylaChatHistoryEntry. Pass id: 0 (or another non-positive value) to create
a message. Pass an existing positive id to update it.
const saved = await layla.chat.saveChatMessage({
id: 0,
role: 'user',
name: 'alex',
content: 'Remember this message.',
character_id: character.id,
session_id: sessionId,
timestamp: Date.now(),
});
console.log(saved.id);Pass an abort signal as the second argument:
const saved = await layla.chat.saveChatMessage(message, {
signal: controller.signal,
});Creates a scheduled chat message and returns the saved
LaylaScheduledChatMessage. Pass id: 0 (or another non-positive value) to
create a scheduled message; the host returns the assigned id.
const scheduled = await layla.chat.scheduleChatMessage({
id: 0,
character_id: character.id,
session_id: sessionId ?? null,
timestamp: Date.now() + 60 * 60 * 1000,
message: 'Check in about the plan in one hour.',
});
console.log(scheduled.id);Pass an abort signal as the second argument:
const scheduled = await layla.chat.scheduleChatMessage(message, {
signal: controller.signal,
});Fetches all scheduled chat messages known to the host. The host protocol does not paginate or filter this response, so filter locally when you only need messages for one character or session.
const scheduledMessages = await layla.chat.getScheduledChatMessages();
const forCharacter = scheduledMessages.filter(
(entry) => entry.character_id === character.id,
);Pass an abort signal as the first argument:
const scheduledMessages = await layla.chat.getScheduledChatMessages({
signal: controller.signal,
});Cancels a scheduled chat message by id. The response contains the requested
id, a success boolean, and an optional message from the host.
const result = await layla.chat.cancelScheduledChatMessage(scheduled.id);
if (!result.success) {
throw new Error(result.message ?? 'Unable to cancel scheduled message');
}Pass an abort signal as the second argument:
const result = await layla.chat.cancelScheduledChatMessage(scheduled.id, {
signal: controller.signal,
});Fetches the newest memories for a specific character. Results come back as a paged array of LaylaMemory items in reverse chronological order. Each memory includes the session_id of the chat session it belongs to.
const memories = await layla.memories.list(character.id);
for (const memory of memories) {
console.log(memory.rawText, memory.summary);
}Use offset and range when you need to page through a longer memory list. Pass minTimestamp, maxTimestamp, or an abort signal in the fourth argument.
const recentMemories = await layla.memories.list(character.id, 0, 20, {
minTimestamp: Date.now() - 7 * 24 * 60 * 60 * 1000,
signal: controller.signal,
});Fetches the top memories for a specific character. The host determines the
ranking heuristic, and results come back as LaylaMemory items in reverse
chronological order.
const topMemories = await layla.memories.getTopMemories(character.id, 5);
for (const memory of topMemories) {
console.log(memory.summary ?? memory.rawText);
}Pass an abort signal as the third argument:
const topMemories = await layla.memories.getTopMemories(character.id, 5, {
signal: controller.signal,
});Creates or updates memories and returns the saved LaylaMemory entries. Pass id: 0 (or another non-positive value) to create a memory. Pass an existing positive id to update it.
const savedMemories = await layla.memories.createOrUpdate([
{
id: 0,
character_id: character.id,
session_id: sessionId,
rawText: 'Alex prefers concise answers.',
timestamp: Date.now(),
summary: 'Prefers concise answers.',
knowledgeGraphJSON: null,
},
]);
console.log(savedMemories[0]?.id);Pass an abort signal as the second argument:
const savedMemories = await layla.memories.createOrUpdate(memories, {
signal: controller.signal,
});Fetches the default persona when characterId is omitted or null. Pass a
character id to ask the host for that character-specific persona.
const defaultPersona = await layla.personas.get();
console.log(defaultPersona.name, defaultPersona.description);const characterPersona = await layla.personas.get(character.id, {
signal: controller.signal,
});The returned LaylaPersona contains:
namedescription
Fetches the TTS voices installed in Layla.
const voices = await layla.tts.getVoices();
for (const voice of voices) {
console.log(voice.id, voice.name, voice.tags);
}Pass an abort signal as the first argument:
const voices = await layla.tts.getVoices({
signal: controller.signal,
});Each LaylaTTSVoice contains:
idtypetagsname
Generates and plays voice audio on the host device using the selected TTS
voice. The promise resolves after the host emits on_finished_speaking, which
means playback has completed. Pass null as ttsVoiceId to use Layla's global
default TTS voice:
await layla.tts.generateVoice(
null,
'I will use your default Layla voice.',
);Pass a voice ID to use a specific installed voice:
const [voice] = await layla.tts.getVoices();
if (voice) {
await layla.tts.generateVoice(
voice.id,
'I will say this out loud through Layla.',
);
}Pass an abort signal as the third argument:
await layla.tts.generateVoice(voice.id, 'Stop me if the UI changes.', {
signal: controller.signal,
});Aborting an in-progress generateVoice(...) call sends stop_speaking to the
host.
Generates voice audio without playing it. Pass a voice ID or null to use
Layla's global default TTS voice. With the default save: false, the result's
audio_data_base64 contains a ready-to-use audio data URI:
const result: GenerateVoiceToFileResult =
await layla.tts.generateVoiceToFile(
null,
'Generate this line without playing it.',
);
if (result.success && result.audio_data_base64) {
audioElement.src = result.audio_data_base64;
}Pass true as the third argument to ask the host to save the generated audio
inside the mini-app's private files. The result then contains filename
instead of audio data:
const result = await layla.tts.generateVoiceToFile(
voice.id,
'Save this generated line.',
true,
{ signal: controller.signal },
);
if (!result.success || !result.filename) {
throw new Error(result.message ?? 'Voice generation failed');
}GenerateVoiceToFileResult contains success, audio_data_base64,
filename, and an optional message. The audio data includes its data URI
prefix. A saved filename includes its extension.
Stops any in-progress TTS playback on the host device. The promise resolves
after the host emits on_finished_speaking, which is also the event used when
normal playback completes.
stopButton.onclick = () => {
void layla.tts.stopSpeaking();
};Pass an abort signal as the first argument if the UI no longer needs to wait for the stop acknowledgement:
await layla.tts.stopSpeaking({
signal: controller.signal,
});Background music and long-form audio use the separate
layla.backgroundAudio surface. Its control methods are fire-and-forget in the
host protocol: each returned promise resolves when the command has been posted
to the WebView bridge, not when playback reaches a particular state.
Start a queue with start(queueAudioFiles, metadata?). Starting again replaces
the current queue. Local paths are resolved from the mini-app root; remote
audio URLs may also be used.
await layla.backgroundAudio.start(
['audio/intro.mp3', 'https://example.com/audio/episode.mp3'],
{
title: 'A quiet journey',
artist: 'Layla Mini-App',
albumTitle: 'Stories',
artworkUrl: 'https://example.com/artwork.jpg',
},
);artworkUrl, when provided, must be a remote HTTPS URL. The other metadata
fields are optional and may be shown on the lock screen or in the media
notification.
Control playback with:
await layla.backgroundAudio.pause();
await layla.backgroundAudio.resume();
await layla.backgroundAudio.skip(); // next track
await layla.backgroundAudio.skip(0); // zero-based queue index
await layla.backgroundAudio.stop(); // clears and releases the playerpause() retains the queue and playback position. stop() clears the queue,
so playback must be restarted with start(...).
Listen for track changes, periodic status, and queue completion:
const onTrackChanged: BackgroundAudioTrackChangedListener = ({
currentIndex,
previousIndex,
}) => console.log(previousIndex, currentIndex);
const onStatus: BackgroundAudioStatusListener = (status) => {
console.log(status.playing, status.currentTime, status.duration);
};
const onFinished: BackgroundAudioFinishedListener = () => {
console.log('The queue finished and the player was released.');
};
layla.backgroundAudio.on('trackChanged', onTrackChanged);
layla.backgroundAudio.on('status', onStatus);
layla.backgroundAudio.on('finished', onFinished);
layla.backgroundAudio.off('trackChanged', onTrackChanged);
layla.backgroundAudio.off('status', onStatus);
layla.backgroundAudio.off('finished', onFinished);Status contains playing, currentIndex, currentTime, duration, and
isLoaded. The host normally emits status about once per second while active,
but may throttle or suspend it while the app is backgrounded. Do not use status
events to drive queue logic.
Scores a piece of text with Layla's sentiment classifier and returns SentimentValues, keyed by emotion category.
const sentiment = await layla.classifier.getSentiment(
'I am thrilled to start this new project.',
);
console.log(sentiment);Updates a Layla character and resolves with the updated character id. If the host creates a new character, the returned id may differ from the id you passed in.
const updatedId = await layla.characters.update({
id: character.id,
data: {
...character.data,
data: {
...character.data.data,
description: 'A careful strategist with a dry sense of humor.',
},
},
});To include an image when updating a character, store a base64-encoded image URI in character.data.data.extensions.image. Include the data URI prefix, such as data:image/png;base64,.
const updatedId = await layla.characters.update({
id: 'new-character',
data: {
spec: 'chara_card_v2',
spec_version: '2.0',
data: {
name: 'Mira',
description: 'A warm sci-fi guide.',
personality: 'curious, kind',
scenario: '',
first_mes: "Hi, I'm Mira.",
mes_example: '',
creator_notes: '',
system_prompt: '',
post_history_instructions: '',
alternate_greetings: [],
tags: ['sci-fi'],
creator: 'My Mini-App',
character_version: '1.0',
extensions: {
image: imageSrc,
},
},
},
});Returns the image generation models that are immediately available on the host. Models that are not downloaded are omitted. Each entry has an id, name, and description. Pass a model's id as the modelId argument to generateImage(...) to generate with that specific model.
const models = await layla.images.getImageGenerationModels();
for (const model of models) {
console.log(model.id, model.name, model.description);
}Generates an image from a prompt. Progress updates are reported through the callback. The returned value is a ready-to-use image source string, or null if the host does not return an image.
const imageSrc = await layla.images.generateImage(
'A cozy pixel-art study with warm lamplight',
(status, step, totalSteps) => {
setProgress({
status,
step,
totalSteps,
});
},
);
if (imageSrc) {
previewImage.src = imageSrc;
}Pass img2img_base64 when the host should use an existing image as the image-to-image base. Include the data URI prefix, such as data:image/png;base64,.
const imageSrc = await layla.images.generateImage(
'Restyle this portrait as soft watercolor',
onProgress,
sourceImageBase64,
);Pass modelId to generate with a specific image model — one of the ids returned by getImageGenerationModels(). When omitted, the host uses its default image model.
const [model] = await layla.images.getImageGenerationModels();
const imageSrc = await layla.images.generateImage(
'A cozy pixel-art study with warm lamplight',
onProgress,
undefined,
model?.id,
);Use an abort signal when the UI can cancel image generation. img2img_base64 and modelId come before the options argument, so pass undefined for the ones you are not using:
const controller = new AbortController();
const imagePromise = layla.images.generateImage(
prompt,
onProgress,
undefined,
undefined,
{ signal: controller.signal },
);
controller.abort();
try {
await imagePromise;
} catch (error) {
if (error instanceof LaylaAbortError) {
return;
}
throw error;
}Saves raw base64-encoded content as a file. Do not include a data URI prefix.
Set share to true to ask the native host to open its share sheet after
saving. It defaults to false.
const contentBase64 = btoa('Hello from Layla.');
const result = await layla.utils.saveFile(
'hello.txt',
contentBase64,
true,
);
if (!result.success) {
throw new Error(result.message ?? 'Unable to save file');
}The browser mock stores files in browser localStorage when share is false.
Browsers cannot reproduce the native share sheet, so share: true downloads
the file instead without storing it.
Pass an abort signal as the fourth argument:
await layla.utils.saveFile('hello.txt', contentBase64, false, {
signal: controller.signal,
});Reads a file from the mini-app's private directory. The returned
content_base64 includes a data URI prefix when the host finds the file, or
null when the file cannot be read.
const result = await layla.utils.readFile('hello.txt');
if (result.content_base64) {
downloadLink.href = result.content_base64;
} else {
throw new Error(result.message ?? 'Unable to read file');
}Pass an abort signal as the second argument:
await layla.utils.readFile('hello.txt', {
signal: controller.signal,
});Chat, character requests, classifier requests, and image generation can be cancelled from the mini-app.
const controller = new AbortController();
try {
const completion = await layla.chat.completions.create({
messages,
signal: controller.signal,
});
} catch (error) {
if (error instanceof LaylaAbortError) {
return;
}
throw error;
}For streaming chat, either pass a signal or call stream.abort().
const controller = new AbortController();
const stream = layla.chat.completions.stream({
messages,
signal: controller.signal,
});
controller.abort();SDK-specific errors extend LaylaError.
import {
LaylaAbortError,
LaylaBridgeUnavailableError,
LaylaError,
} from '@layla-network/sdk';
try {
const completion = await layla.chat.completions.create({ messages });
} catch (error) {
if (error instanceof LaylaAbortError) {
return;
}
if (error instanceof LaylaBridgeUnavailableError) {
showError('Open this mini-app inside Layla.');
return;
}
if (error instanceof LaylaError) {
showError(error.message);
return;
}
throw error;
}Exported error classes:
LaylaErrorLaylaAbortErrorLaylaBridgeUnavailableError
Installs a mock Layla host for local development outside the Layla WebView. Install it before the first SDK call.
import { installLaylaMock } from '@layla-network/sdk';
if (import.meta.env.DEV) {
installLaylaMock({
debug: true,
});
}Customize mock chat responses:
installLaylaMock({
respond: (messages) => {
const last = messages.at(-1)?.content ?? '';
return `Mock response to: ${last}`;
},
});Customize the inference engines exposed by the mock:
installLaylaMock({
inferenceEngines: ['local-fast', 'local-quality'],
});
const engines = await layla.chat.getInferenceEngines();
await layla.chat.setInferenceEngine(engines[0] ?? null);The mock accepts names in inferenceEngines and null. An unknown name returns
success: false, resets the mock to its default engine, and reports
engineName: null.
Customize and exercise the contextual surface:
const character = makeMockCharacter('Aria');
const mock = installLaylaMock({
characters: [character],
executionContext: {
app_version: '1.8.0',
character,
session_id: 'mock-aria-session-1',
},
});
const context = await layla.contextual.getExecutionContext();
layla.contextual.on('chatContextNewMessage', ({ message }) => {
console.log(message.content);
});
layla.contextual.on('chatContextSentimentUpdate', ({ sentiment }) => {
console.log(sentiment);
});
layla.contextual.on('chatContextStartedSpeaking', () => {
console.log('The contextual character started speaking.');
});
layla.contextual.on('chatContextFinishedSpeaking', () => {
console.log('The contextual character finished speaking.');
});
layla.contextual.on('chatContextStartedThinking', () => {
console.log('The contextual character started thinking.');
});
mock.emitChatContextNewMessage({
message: { role: 'user', content: 'Hello from the surrounding chat.' },
character_id: character.id,
session_id: context.session_id ?? 'mock-aria-session-1',
timestamp: Date.now(),
});
mock.emitChatContextSentimentUpdate({ sentiment: 'joy' });
mock.emitChatContextStartedSpeaking();
mock.emitChatContextFinishedSpeaking();
mock.emitChatContextStartedThinking();When executionContext is omitted, the mock returns { app_version: 'mock', character: null, session_id: null }, representing a standalone top-level
mini-app. The mock handle's emitChatContext... methods let local tests drive
the same pushed events that the Layla host emits.
Customize mock session history with static transcript data:
installLaylaMock({
chatHistory: [
{
role: 'assistant',
name: 'Aria',
character_id: 'mock-aria',
session_id: 'mock-aria-session-1',
content: 'I saved the last idea we talked about.',
timestamp: Date.now(),
},
],
});
const { sessions } = await layla.chat.getChatSessions('mock-aria');
const history = sessions[0]
? await layla.chat.getChatHistory(sessions[0].session_id)
: [];When chatHistory is omitted, the mock supplies multiple sessions per default character so local apps can exercise the same session-first flow.
Customize mock scheduled messages with static schedule data:
installLaylaMock({
scheduledChatMessages: [
{
id: 1,
character_id: 'mock-aria',
session_id: 'mock-aria-session-1',
timestamp: Date.now() + 60 * 60 * 1000,
message: 'Follow up on the saved idea.',
},
],
});
const scheduled = await layla.chat.getScheduledChatMessages();
const saved = await layla.chat.scheduleChatMessage({
id: 0,
character_id: 'mock-aria',
session_id: null,
timestamp: Date.now() + 2 * 60 * 60 * 1000,
message: 'Start a fresh check-in later.',
});
await layla.chat.cancelScheduledChatMessage(saved.id);Scheduled messages created through the mock are available to later
layla.chat.getScheduledChatMessages() calls in the same mock session.
Customize mock memories with static memory data:
installLaylaMock({
memories: [
{
id: 1,
character_id: 'mock-aria',
session_id: 'mock-aria-session-1',
rawText: 'Aria remembers that Alex likes quiet mornings.',
timestamp: Date.now(),
summary: 'Alex likes quiet mornings.',
knowledgeGraphJSON: null,
},
],
});
const memories = await layla.memories.list('mock-aria');
const topMemories = await layla.memories.getTopMemories('mock-aria', 3);When memories is omitted, the mock supplies a small memory set per default
character. The top-memories mock response filters those same memories by
character and returns the newest entries up to the requested limit.
Customize mock personas:
installLaylaMock({
persona: {
name: 'Alex',
description: 'A thoughtful local-development user persona.',
},
personas: {
'mock-aria': {
name: 'Aria',
description: 'A focused character-specific mock persona.',
},
},
});
const defaultPersona = await layla.personas.get();
const ariaPersona = await layla.personas.get('mock-aria');When persona is omitted, the mock supplies a default persona. When
personas is omitted, the mock derives character-specific personas from the
mock character cards.
Customize mock TTS voices:
installLaylaMock({
ttsVoices: [
{
id: 'local-narrator',
type: 'mock',
tags: ['narrator', 'demo'],
name: 'Local Narrator',
},
],
});
const voices = await layla.tts.getVoices();
await layla.tts.generateVoice(voices[0].id, 'Preview this voice.');
await layla.tts.generateVoice(null, 'Preview the global default voice.');
const audio = await layla.tts.generateVoiceToFile(
null,
'Generate a mock audio data URI.',
);
const savedAudio = await layla.tts.generateVoiceToFile(
null,
'Save a mock audio file.',
true,
);
await layla.tts.stopSpeaking();The browser mock does not synthesize audio; generateVoice(...) waits for the
mock latency and then emits on_finished_speaking, whether passed a configured
voice ID or null for the global default. stopSpeaking() immediately emits
the same completion event and cancels any pending mock TTS completion.
generateVoiceToFile(...) returns a small mock WAV data URI, or saves
mock-voice.wav to mock private-file storage when save is true.
The background-audio mock emits status updates as start, pause, resume, and skip commands change its simulated state. The mock handle can also drive any background-audio event directly:
const mock = installLaylaMock();
layla.backgroundAudio.on('status', console.log);
await layla.backgroundAudio.start(['intro.mp3', 'chapter-1.mp3']);
await layla.backgroundAudio.skip();
mock.emitBackgroundAudioTrackChanged({
previousIndex: 0,
currentIndex: 1,
});
mock.emitBackgroundAudioStatus({
playing: true,
currentIndex: 1,
currentTime: 12,
duration: 90,
isLoaded: true,
});
mock.emitBackgroundAudioFinished();Customize mock private files with static base64 data or data URIs:
installLaylaMock({
files: {
'hello.txt': 'data:text/plain;base64,SGVsbG8gZnJvbSBMYXlsYS4=',
},
});
const result = await layla.utils.readFile('hello.txt');Files saved through layla.utils.saveFile(...) with share: false are stored
in browser localStorage, so later layla.utils.readFile(...) calls can read
them on the same origin. Files downloaded with share: true are not stored.
The returned handle can uninstall the mock.
const mock = installLaylaMock();
mock.uninstall();Creates a valid mock character card for use with installLaylaMock.
import { installLaylaMock, makeMockCharacter } from '@layla-network/sdk';
installLaylaMock({
characters: [
makeMockCharacter('Aria'),
makeMockCharacter('Kai', {
tags: ['demo'],
personality: 'playful, direct',
}),
],
});Useful exported types include:
LaylaSDKOptionsRequestOptionsLaylaChatRoleLaylaChatMessageLaylaChatHistoryEntryLaylaScheduledChatMessageLaylaMemoryLaylaPersonaLaylaTTSVoiceGenerateVoiceToFileResultBackgroundAudioMetadataBackgroundAudioTrackChangedBackgroundAudioTrackChangedListenerBackgroundAudioStatusBackgroundAudioStatusListenerBackgroundAudioFinishedBackgroundAudioFinishedListenerLaylaExecutionContextChatContextFinishedSpeakingChatContextFinishedSpeakingListenerChatContextNewMessageChatContextNewMessageListenerChatContextSentimentUpdateChatContextSentimentUpdateListenerChatContextStartedSpeakingChatContextStartedSpeakingListenerChatContextStartedThinkingChatContextStartedThinkingListenerMemoryListOptionsLaylaApiEvent_onGetChatSessionsResponseLaylaApiSaveChatMessageLaylaApiEvent_onSaveChatMessageResponseLaylaApiScheduledChatMessageLaylaApiGetScheduledChatMessagesLaylaApiCancelScheduledChatMessageLaylaApiEvent_onScheduledChatMessageLaylaApiEvent_onGetScheduledChatMessagesResponseLaylaApiEvent_onCancelScheduledChatMessageLaylaApiGetMemoriesLaylaApiGetTopMemoriesLaylaApiCreateOrUpdateMemoriesLaylaApiGetPersonaLaylaApiGetTTSVoicesLaylaApiGenerateVoiceLaylaApiGenerateVoiceToFileLaylaApiStopSpeakingLaylaApiStartBackgroundAudioPlayerLaylaApiStopBackgroundAudioPlayerLaylaApiPauseBackgroundAudioPlayerLaylaApiResumeBackgroundAudioPlayerLaylaApiSkipBackgroundAudioTrackLaylaApiGetInferenceEnginesLaylaApiSetInferenceEngineLaylaApiGetExecutionContextLaylaApiEvent_onGetMemoriesResponseLaylaApiEvent_onGetTopMemoriesResponseLaylaApiEvent_onCreateOrUpdateMemoriesResponseLaylaApiEvent_onGetPersonaResponseLaylaApiEvent_onGetTTSVoicesResponseLaylaApiEvent_onGetInferenceEnginesResponseLaylaApiEvent_onSetInferenceEngineResponseLaylaApiEvent_onGetExecutionContextResponseLaylaApiEvent_onChatContextFinishedSpeakingLaylaApiEvent_onChatContextNewMessageLaylaApiEvent_onChatContextSentimentUpdateLaylaApiEvent_onChatContextStartedSpeakingLaylaApiEvent_onChatContextStartedThinkingLaylaApiEvent_onFinishedSpeakingLaylaApiEvent_onGenerateVoiceToFileResponseLaylaApiEvent_onBackgroundAudioTrackChangedLaylaApiEvent_onBackgroundAudioStatusLaylaApiEvent_onBackgroundAudioFinishedLaylaApiSaveFileLaylaApiEvent_onSaveFileResponseLaylaApiReadFileLaylaApiEvent_onReadFileResponseReadFileResultSaveFileResultLaylaCharacterTavernCardV2SentimentValuesTavernCharacterBookChatCompletionChatCompletionChunkChatCompletionCreateParamsBaseChatCompletionCreateParamsNonStreamingChatCompletionCreateParamsStreaming
Protocol types are also exported for host integration and advanced typing, but ordinary mini-apps should prefer the high-level SDK methods above.
The TypeScript source is the source of truth for current signatures:
src/index.tssrc/client.tssrc/resources/chat/index.tssrc/resources/chat/stream.tssrc/resources/characters.tssrc/resources/classifier.tssrc/resources/images.tssrc/resources/memories.tssrc/resources/personas.tssrc/resources/tts.tssrc/resources/background-audio.tssrc/resources/contextual.tssrc/resources/utils.tssrc/protocol.tssrc/errors.tssrc/mock.ts