v
Skip to content

Referencia API ​

Todo lo que puede usar el código de interfaz de una carta: globales del sandbox, componentes, cada campo y método de useYumina(), definiciones de tipos y los sustitutos de las APIs del navegador bloqueadas.

El código de interfaz está en Paneles → Código de frontend. Si buscas una introducción, lee primero la Guía de UI personalizada; esta página sirve para consultar firmas.


Globales del sandbox ​

Estos nombres están disponibles en todo tu código de interfaz sin ningún import:

NombreTipoQué es
ReactmóduloReact completo (useState, useEffect, useRef, useMemo, useCallback, useLayoutEffect, Fragment, ...)
useYuminahookSDK de la plataforma; consulta SDK useYumina()
useAssetFonthookCarga una fuente propia desde la biblioteca de recursos; consulta useAssetFont()
IconsobjetoTodos los iconos de Lucide como componentes: <Icons.Heart />, <Icons.Sword />. Catálogo completo: https://lucide.dev/icons
ChatcomponenteEl chat completo como bloque; consulta <Chat>
MessageListcomponenteMensajes sin barra de entrada; consulta <MessageList>
MessageInputcomponenteSolo la barra de entrada; consulta <MessageInput>
ChatCanvascomponenteAlias antiguo de <Chat />; consulta <ChatCanvas>
ModelTrigger, ModelPickerModalcomponenteEl botón de modelo y el selector de modelos del chat; consulta Componentes del selector de modelo
SessionMemoryModalcomponenteEl diálogo de memoria de la sesión del chat; consulta Componentes del selector de modelo
LoreSlot, LoreButton, LorePanel, LoreGroup, LoreSwitchcomponenteControles que envían una entrada de lore a la IA solo mientras están encendidos; consulta Controles de lore
exports, moduleobjetoRespaldo de exportación al estilo CJS; normalmente puedes ignorarlos

NO importes React ni ninguno de los nombres de arriba: los inyecta el sandbox. Las sentencias import se eliminan al compilar, así que import { useState } from "react" deja useState sin definir. Usa React.useState.

Tus propios archivos SÍ se pueden importar: el código de interfaz con varios archivos usa sintaxis de módulos ES: import StatBar from "./stat-bar". Puedes omitir las extensiones .tsx, .ts, .jsx y .js.


SDK useYumina() ​

Llámalo dentro de tu función de componente:

tsx
function MyWorld() {
  const api = useYumina()
  // api.variables, api.sendMessage(...), ...
}

Todo lo que ofrece, agrupado por uso:

Lecturas de estado (síncronas) ​

Leen el estado más reciente de la partida. El componente se vuelve a renderizar cada vez que cambia cualquiera de ellos.

CampoTipoSignificado
variablesRecord<string, unknown>Variables de la partida con alcance de sesión, con el ID de cada variable como clave. Su nombre visible también sirve para leer (api.variables.hunger encuentra la variable llamada hunger aunque su ID sea un UUID). Ejemplo: { health: 80, gold: 150 }. Consulta ID y nombre visible
globalVariablesRecord<string, unknown>Durante la partida tiene los mismos valores que variables. Admite la misma búsqueda por ID o nombre
worldNamestringNombre de la carta actual
worldCoverstring | nullLa portada de la carta como URL absoluta, null si no tiene. Sigue los cambios de portada automáticamente: úsala para avatares de personajes, cabeceras o pantallas de presentación en lugar de volver a subir la portada como recurso
background{ id, url, blur, dim, opacity, position } | nullLa imagen que el host pinta detrás del chat por defecto, ya resuelta (url es la definitiva, blur en px, dim y opacity de 0 a 1, position "center" | "top" | "bottom"). null si la carta no tiene. Una interfaz a pantalla completa puede pintar el mismo fondo o ignorarlo
worldIdstringUUID de la carta actual
sessionIdstringUUID de la sesión de juego actual
currentUser{ id, name?, image? } | nullLa cuenta tal cual: id, nombre visible y avatar de la cuenta. null si no hay sesión iniciada. Úsalo para interfaz de cuenta, como «ver perfil». Para mostrar al jugador dentro de la historia, usa user
user{ name: string; avatar: string | null }El jugador que se interpreta, con la misma lógica persona/cuenta que la macro . Si el jugador tiene una persona activa, user.name es el nombre de la persona y user.avatar su avatar; si no, se usa la cuenta. Úsalo para burbujas de chat dentro de la historia, fichas de personaje y paneles de perfil
mode"session" | "guest-preview""session" es una sesión de juego real. "guest-preview" es la vista previa del hub sin sesión iniciada: las acciones que cambian el estado no hacen nada y le piden al host que muestre el aviso de inicio de sesión
capabilities{ canSendMessage, canPersistSession, canUseSessionApis, requiresAuth }Lo que permite el mode actual. Léelo para desactivar botones que no harían nada (por ejemplo, Enviar en la vista previa de invitado) o para mostrar un botón «Inicia sesión para continuar»
languagestringCódigo del idioma activo en el host ("en", "zh", ...). Úsalo para elegir traducciones dentro de la carta sin depender de la instancia de i18next del host
messagesArray<Record<string, unknown>>Historial de mensajes (la parte cargada; consulta hasEarlierMessages). Consulta SandboxMessage
hasEarlierMessagesbooleanHay mensajes más antiguos en el servidor que aún no se han cargado. Llama a loadEarlierMessages() para traerlos
isLoadingEarlierbooleantrue mientras se trae una página anterior
isStreamingbooleantrue mientras la IA está generando una respuesta
streamingContentstringTexto de la IA en directo mientras se genera (se actualiza a menudo)
streamingReasoningstringTexto de «pensamiento» o razonamiento de la IA en directo (solo en modelos de razonamiento)
pendingChoicesstring[]Etiquetas de los botones de elección que han emitido las reglas
errorstring | nullMensaje de error actual (fallo de la API, error de generación) o null
errorCodestring | nullCódigo de máquina de error (por ejemplo, "CONTENT_FILTER"); null si no se conoce
sendFailureNoncenumberSube en uno cada vez que un envío falla definitivamente, incluidos los fallos que solo muestran un aviso y no rellenan error. Vigílalo para devolver el texto del jugador a tu propia caja de entrada
composerSendKey"enter" | "mod-enter"El ajuste Pulsa Enter para enviar del jugador. "enter": Enter envía y Mayús+Enter hace salto de línea. "mod-enter": Ctrl/⌘+Enter envía y Enter hace salto de línea. Una caja de entrada hecha a mano debería respetarlo
readOnlybooleantrue cuando miras la sesión de otra persona; <Chat /> oculta la entrada automáticamente. Vale true hasta que el host envía el primer estado
checkpointsArray<Checkpoint>Puntos de control guardados; consulta Checkpoint
greetingContentstring | nullTexto de apertura calculado a partir de las entradas de la carta (<Chat /> lo usa como contenido cuando aún no hay mensajes)
canvasMode"chat" | "custom" | "fullscreen"Modo de lienzo actual
speakerBubblesboolean | undefinedLa carta tiene activado Una burbuja por personaje en Procesar la respuesta
selectedModelstringID del modelo de IA seleccionado
userPlanstringPlan de suscripción del usuario ("free", "go", "plus", "pro", "ultra", "internal")
preferredProvider"official" | "private"API oficial o clave propia del usuario
balancenumber | nullEl saldo de Mushies del jugador, null si no se conoce
entriesReadonlyArray<SandboxEntry>Las entradas de lore de la carta: solo las activadas, ordenadas por position. Consulta Búsquedas del Lorebook y SandboxEntry
worldbooksReadonlyArray<Worldbook>Los escenarios de la carta (id, name, description?, enabled?, activation, ...). SandboxEntry.worldbookId apunta a estos IDs
loreUiBindingsReadonlyArray<LoreUiBinding>Qué entrada de lore envía cada id de <LoreSlot>. Consulta Controles de lore

ID y nombre visible ​

Cada variable tiene dos etiquetas:

  • ID: la clave con la que se guarda de verdad en el estado de la partida. Las variables nuevas reciben un UUID aleatorio como a8c5a685-1317-40b2-8f11-baccd39f9042 hasta que lo cambias. Usa el ID real de la carta; las cartas importadas o antiguas pueden conservar IDs con mayúsculas y minúsculas mezcladas.
  • Nombre visible: cómo la llamas tú (hunger, Love meter, zombieKills).

api.variables usa el ID como clave y además resuelve el nombre visible:

jsx
api.variables.hunger        // works whether "hunger" is the ID or the display name
api.variables["Love meter"] // display names with spaces work too
api.setVariable("hunger", 50)  // writes to that variable's ID

Si existen los dos, gana el ID. Si una variable tiene el ID hp y otra tiene también hp como nombre visible, api.variables.hp lee la que tiene ID hp. Si varias variables comparten nombre visible, gana la última definida, igual que en la búsqueda por nombre del motor. Mejor usa nombres únicos para evitar ambigüedades.

Object.keys(api.variables) sigue devolviendo las claves guardadas, sin alias de nombre visible. Recorrer el objeto no añade filas duplicadas por los alias.

Acciones de juego ​

MétodoQué hace
sendMessage(text, attachments?)Envía un mensaje como jugador y provoca una respuesta de la IA. attachments es un array opcional de imágenes obtenidas con pickChatImage(). No espera respuesta
pickChatImage()Abre el selector de imágenes del host para adjuntar al chat. Se resuelve con { type: "image", mimeType, name, data } (data en base64) o null si se cancela o no está disponible. Pasa el resultado a sendMessage(text, [image])
setVariable(id, value, options?)Cambia una variable. id es el ID de la variable o su nombre visible. No espera respuesta: el valor aparece en api.variables al momento y se guarda en segundo plano. options ({ scope?, targetUserId? }) se acepta por compatibilidad con cartas antiguas; el host lo ignora
patchVariables(values)Guarda varias variables en una sola escritura: values es { variableId: value, ... }, con el ID como clave (aquí no se buscan nombres visibles). Devuelve Promise<void>, que se resuelve cuando el servidor confirma la escritura y se rechaza si falla (vistas de solo lectura, sesión cambiada, error del servidor). Úsalo cuando el siguiente paso depende de que el valor ya esté guardado
executeAction(actionId)Dispara los comportamientos cuyo disparador es El jugador pulsa un botón con este nombre en Botón (por ejemplo, "attack"). No espera respuesta ni puede pasar valores; para eso usa executeActionAndWait
await executeActionAndWait(actionId, params?)Lo mismo, pero espera el resultado y puede pasar valores. params es un objeto simple como { price: 5, item: "Mushroom ale" }; el comportamiento los lee como {param.price} en el valor de una condición, el valor de un efecto o un aviso (Comportamientos · Pasar un valor desde un botón). Los valores deben ser texto, números o verdadero/falso; hasta 30, con nombres de hasta 60 caracteres y textos de hasta 500. Se resuelve con { applied: true, variables, firedIds } cuando ya se han aplicado el estado del host, los registros, las notificaciones y los efectos de audio. Se rechaza ante fallos, tiempo agotado, vistas no disponibles (vista previa de invitado, solo lectura, repeticiones), IDs vacíos o que no son texto y sesiones caducadas. variables es la instantánea confirmada por el servidor; las escrituras optimistas posteriores siguen en api.variables
callAi(ai, input?)Ejecuta una de las IA de la carta por nombre o ID, normalmente una IA Por interfaz con Formato de respuesta. input es lo que quieras pasarle (un texto o un objeto pequeño). Se resuelve con { text, fields, fallback }: text son sus palabras, fields los valores que rellenó y fallback vale true si falló y se usó una frase de respaldo. Se resuelve con null si no pudo ejecutarse (no está en juego aquí, está en enfriamiento, ocupada o esta vista no puede llamar a IA). A dónde van sus palabras y sus campos se configura en su Formato de respuesta; con A la historia, su frase también se añade al chat
onAiOutput(channel?, cb)Escucha las palabras enviadas a Un canal de la interfaz: las de una IA cuyo Dónde van sus palabras es Un canal de la interfaz, o las de una regla de Procesar la respuesta configurada como Un canal de la interfaz. cb({ channel, id, name, text, fields }). Pasa un nombre de canal para oír solo ese canal, o solo cb para oírlos todos. En una regla de Procesar la respuesta, id es "reply", name es la regla y fields es {}. Devuelve una función para cancelar la suscripción
setScene(scene, options?)Les dice a las IA qué muestra la pantalla ahora mismo. scene es un texto corto o un objeto pequeño ({ room: "kitchen", onCamera: false }); null lo borra. Todas las llamadas a IA posteriores (respuestas, regeneraciones, IA entre bastidores) lo reciben como un bloque propio; no se guarda. options.events es [{ name, when? }]: eventos que la IA puede disparar escribiendo [event: name]
onStoryEvent(cb)Escucha eventos de la historia: una IA que escribe [event: name], o una regla de Procesar la respuesta configurada como Un evento de la historia. cb({ id, name }). Solo se dispara en una sesión real. Devuelve una función para cancelar la suscripción
switchGreeting(index)Cambia el primer mensaje a la apertura número index (0 = la primera apertura). Devuelve Promise<void>, que se resuelve cuando el cambio ya se ha aplicado. El cambio devuelve las variables a los valores iniciales de esa apertura, así que cambia tus variables después de esperar la promesa
clearPendingChoices()Descarta los botones de elección pendientes sin elegir ninguno
setComposerDraft(text)Pone text en la caja de mensaje del chat y le da el foco. No lo envía. Úsalo cuando quieres que el jugador revise o edite el mensaje antes de pulsar Enviar (por ejemplo, un botón de interacción con un PNJ que deja preparada una frase para empezar la conversación). Es local al sandbox, sin ida y vuelta al host, así que solo funciona junto a los componentes incluidos <MessageInput> / <Chat>
tsx
// A shop button that runs the "buy" behavior with this item's price
async function buy() {
  try {
    await api.executeActionAndWait("buy", { item: "Mushroom ale", price: 5 })
  } catch (e) {
    api.showToast("Couldn't buy that right now", "error")
  }
}

// A fortune-teller button that calls a UI-based AI
async function askFortune() {
  const answer = await api.callAi("Fortune Mushroom", { question: "Will I find the key?" })
  if (answer) setFortune(answer.fields.Fortune)
}

Control de chat ​

Todo lo que puede hacer la barra de chat por defecto, disponible para que tu interfaz también pueda hacerlo.

MétodoQué hace
editMessage(messageId, content, options?)Edita un mensaje existente. Devuelve Promise<boolean>; true si sale bien. options.swipeIndex indica en qué swipe se abrió la edición; si para entonces hay otro swipe activo, no se guarda (false)
deleteMessage(messageId)Elimina un mensaje. Devuelve Promise<boolean>
regenerateMessage(messageId)Pide a la IA que regenere esa respuesta (no espera respuesta)
continueLastMessage()Sigue generando a partir del último mensaje de la IA (no espera respuesta)
stopGeneration()Corta la generación en curso (no espera respuesta)
restartChat()Borra todos los mensajes, reinicia el estado y empieza de cero
swipeMessage(messageId, "left" | "right")Cambia entre las alternativas de la IA (swipes) de un mensaje. Devuelve Promise<Record<string, unknown>>
loadEarlierMessages()Trae la página anterior del historial; las filas se añaden al principio de messages. Devuelve Promise<boolean>

Sesiones y ramificación ​

MétodoQué hace
revertToMessage(messageId)Rebobina la conversación hasta justo antes de messageId. Devuelve Promise<void>
branchFromMessage(messageId)Crea una sesión nueva a partir de ese mensaje (copia los mensajes hasta él, incluido, y la instantánea del estado). Devuelve Promise<string | null>: el ID de la nueva sesión, o null si falla (durante la generación, en salas multijugador o si faltan mensajes)
getBranchContext()Trae la parte del árbol de ramas en la que estás (la propia, la madre, las hermanas y las hijas). Devuelve Promise<BranchContext>. Se pide de nuevo en cada llamada; no hay caché en el cliente. Consulta BranchContext
createSession(worldId)Empieza una sesión nueva de una carta. Devuelve Promise<string> con el ID de la sesión nueva
deleteSession(sessionId)Elimina una sesión. Devuelve Promise<void>
listSessions(worldId)Lista todas las sesiones de una carta. Devuelve Promise<Array<Record<string, unknown>>>

Puntos de control ​

Un punto de control es una instantánea con nombre dentro de la sesión actual a la que puedes volver.

MétodoQué hace
saveCheckpoint()Guarda el estado actual de la sesión como un punto de control nuevo. Devuelve Promise<void> (el campo checkpoints se actualiza después)
loadCheckpoints()Pide al host que actualice el array checkpoints. Devuelve Promise<void>
restoreCheckpoint(checkpointId)Devuelve la sesión a un punto de control guardado. Devuelve Promise<void>
deleteCheckpoint(checkpointId)Elimina un punto de control. Devuelve Promise<void>

Audio ​

trackId es el ID de pista: haz clic en la pista en Paneles → Audio y usa Copiar ID. Cambiar el nombre de la pista no lo cambia.

MétodoQué hace
playAudio(trackId, opts?)Reproduce una de las pistas de audio de la carta. opts: { volume?, fadeDuration?, chainTo?, maxDuration?, duckBgm?, loop? }; las duraciones van en segundos; chainTo indica el trackId que sonará después; duckBgm baja la BGM mientras suena; loop sustituye el ajuste de bucle de la pista solo en esta reproducción
stopAudio(trackId?, fadeDuration?)Detiene una pista (sin trackId, lo detiene todo). fadeDuration en segundos. Destruye el elemento; usa pauseAudio si quieres retomar desde el mismo punto
pauseAudio(trackId)Pausa una pista donde está, conservando la posición
resumeAudio(trackId)Retoma una pista pausada con pauseAudio
onAudioEnded(cb)Te avisa cuando una pista sin bucle termina: cb(trackId). Devuelve una función para cancelar la suscripción. Úsalo para avanzar solo una lista de reproducción propia
setAudioVolume(type, volume)type es "bgm" o "sfx", volume va de 0 a 1
getAudioVolume(type)Devuelve al momento el volumen actual (0–1)

Lectura por voz (TTS) ​

Lee texto en voz alta con el sistema de voz de la plataforma (Fish Audio a través de OpenRouter). Se cobra al jugador por carácter; volver a reproducir el mismo texto con la misma voz sale gratis porque queda en caché. Solo suena una voz a la vez. Las etiquetas en línea del estilo [whisper] / [excited] dentro del texto controlan la entonación.

La lectura por voz es opcional: los jugadores la activan en Ajustes › Pantalla. Hasta que lo hacen, ttsState.enabled vale false, tts.speak / tts.preview se resuelven con { ok: false, reason: "disabled" } sin sintetizar ni cobrar, y tts.setPrefs no cambia nada. Oculta tu interfaz de voz mientras enabled sea false.

Método / campoQué hace
tts.speak(opts)opts: { messageId?, text?, key?, voice?, waitForEnd? }. Pasa messageId para leer un mensaje del chat (respeta el modo de lectura que eligió el jugador), o text para cualquier frase de la carta (key le da una identidad de reproducción estable). voice sustituye opcionalmente la voz del jugador por un id del marketplace de fish.audio (32 caracteres hexadecimales), para que las cartas pongan voz a sus propios personajes. Con waitForEnd: true, el éxito significa que todos los fragmentos de audio terminaron de verdad; la cancelación, la reproducción bloqueada, un error de medios o el tiempo agotado devuelven ok: false. Sin esa opción, el éxito significa que la reproducción quedó en cola. Devuelve Promise<{ok, reason?}>; reason es "disabled" (el jugador no ha activado la lectura), "unavailable" (vista previa o repetición), "insufficient" (no hay suficientes Mushies), "rate-limited", "empty", "timeout" o un error
tts.stop()Detiene la lectura actual (también cancela una síntesis pendiente y la cola de lectura automática)
tts.onPlaybackFrame(cb)Te pasa los fotogramas de salida medidos (unos 15 por segundo) mientras suena una frase. Compara la key del fotograma con la de la frase que lanzaste. Devuelve una función para cancelar la suscripción; cancélala al desmontar
tts.setPrefs(prefs)Cambia las preferencias de lectura del jugador: { enabled?, autoPlay?, mode?, voice?, voicePool?, volume? } (mode es "full" o "dialogue"; volume de 0 a 100; voicePool es la lista de ids de voz que el reparto automático puede dar a los personajes, [] = todas; voice fija un grupo de una sola, "" = todas). Se guarda en la cuenta; los valores nuevos vuelven por ttsState. Solo funciona si el jugador ya activó la lectura; enabled: true se ignora (solo Ajustes la enciende) y enabled: false la apaga
tts.preview(voice)Reproduce una muestra corta de una voz ("" = la voz automática). La primera escucha se cobra como cualquier síntesis y luego queda en caché para todos. Devuelve Promise<{ok, reason?}>
ttsStateSolo lectura: { available, enabled, voice, voicePool, mode, autoPlay, volume, playback }. available es false fuera de sesiones reales (vista previa de invitado, repetición); en ese caso oculta TODA la interfaz de voz. enabled indica si el jugador la activó (false hasta que enciende la lectura en Ajustes). playback es { key, status: "loading" | "playing" | "blocked", progress?, level? } o null (progress de 0 a 1, unas 2 veces por segundo); playing sigue la reproducción real. Las conversaciones con waitForEnd pueden incluir un level de salida real (0–1, unos 15 Hz); no aparece si no está disponible. Una interfaz propia puede mostrar el estado de cada hablante por frase y anillos de progreso

Entrada por voz (mantener para hablar) ​

Deja que el jugador hable en lugar de escribir. Graba la plataforma (la carta nunca toca el micrófono) y te devuelve las palabras. Es gratis para el jugador. La caja de mensaje del chat por defecto ya tiene micrófono; usa estos métodos para construir tu propio botón para hablar, por ejemplo el estrado de una partida de hombres lobo.

Método / campoQué hace
voice.prepare(opts?)Comprueba el permiso de micrófono del navegador y luego suelta el micrófono sin grabar ni transcribir. Pasa { requireLevels: true } para la detección automática de silencio; devuelve { ok: false, reason: "levels-unavailable" } si el analizador no puede funcionar. Llámalo antes de una conversación de pago para que un micrófono bloqueado no deje la conversación colgada. Cancela con voice.cancel().
voice.record(opts?)Empieza a grabar; llámalo cuando se pulsa tu botón para hablar. Para la detección automática de silencio, pasa también opts.requireLevels: true; para el mantener-para-hablar normal puedes omitirlo. opts.onLevel(level) recibe el volumen de entrada en directo (0–1, unas 15 veces por segundo) para dibujar una onda. Se resuelve tras voice.stop() con { ok: true, text }, o con { ok: false, reason }; reason es "cancelled", "too-short" (un toque, no una pulsación mantenida), "denied" (micrófono bloqueado), "empty", "rate-limited", "unavailable" (vista previa o repetición), "timeout" o "error". Pausa cualquier lectura por voz para que no se grabe a la IA
voice.stop()Termina el fragmento y lo transcribe (se suelta el botón para hablar)
voice.cancel()Descarta el fragmento
voice.setPrefs(prefs)Cambia las preferencias de entrada por voz del jugador: { enabled?, mode?, key? }. mode es "" (seguir a la carta), "confirm" o "auto"; key es el KeyboardEvent.code que hay que mantener pulsado
voiceInputStateSolo lectura: { available, enabled, mode, cardMode, playerMode, key }. mode es lo que el jugador quiere al soltar: "auto" (enviarlo tal cual) o "confirm" (revisarlo antes). Es el settings.voiceInputMode de tu carta salvo que el jugador lo haya cambiado. key es el KeyboardEvent.code que mantiene pulsado para hablar

El valor por defecto de tu carta se elige en Ajustes de la carta → Voz → Entrada por voz del jugador: Revisar antes es "confirm" y Enviar ya es "auto". En el JSON de la carta es settings.voiceInputMode: "auto" | "confirm".

Voz en directo continua (OpenAI Realtime) ​

api.realtimeVoice ofrece una conversación continua por micrófono dentro de una sesión guardada y editable. Llama a getConfig() para saber si está disponible de verdad y quién paga; que el objeto exista no garantiza el acceso. El host pide un consentimiento explícito para el micrófono antes de capturar. Puede pagar una clave de OpenAI guardada, un patrocinio de pruebas activado o el saldo de Yumina, si se ha activado expresamente. Con saldo, el consentimiento indica la reserva temporal del servidor y el cobro según el uso medido. El audio del micrófono y el contexto de la carta se envían a OpenAI; las transcripciones pueden guardarse en la historia. No se pide la cámara. Los recibos duraderos y la recuperación tras recargar están en el contrato de voz con saldo (en inglés).

MétodoContrato
realtimeVoice.getConfig()Sin argumentos; no gasta ni captura. Una llamada con saldo devuelve transport: "server-ws-v1", funding: "balance", los reservationCredits del servidor y finishAcknowledged: true; el transporte antiguo devuelve false. Las vistas desactivadas devuelven no disponible.
realtimeVoice.finish()Sin argumentos. Con saldo, primero silencia en local y recoge los registros finales duraderos de este intento antes de devolver finished; pending, incomplete, unavailable y el not-started atómico son resultados distintos. pending no trae cursor final. El transporte antiguo devuelve unsupported y stop no cambia.
realtimeVoice.prepare({ avatar? }?)Llámalo desde el clic del jugador, antes de cualquier trabajo lento (montar el contexto, guardar). Se resuelve con { intent }; pasa ese intent a start. Se rechaza si la voz no está disponible en esta vista
realtimeVoice.start({ instructions, intent?, context?, voice?, avatar?, tools?, interruptionMode? })Se resuelve con { status: "connected" }; las instrucciones tienen de 1 a 12.000 caracteres. context admite las mismas formas que updateContext. voice es "marin" (por defecto) o "cedar". Usa gpt-realtime-2.1, VAD semántico medio y respuestas gestionadas por el host. interruptionMode es "automatic" (por defecto) o "manual"; un modo no válido se rechaza antes de capturar. tools es [] (sin herramienta de inspección) o ["request_inspection_focus"]; si lo omites, se mantiene el valor por defecto. La opción avatar: true requiere el servicio LiveAvatar de pruebas configurado en el host y espera a su conexión de medios; si no está disponible, se rechaza de forma visible. Muestra al jugador las promesas rechazadas.
realtimeVoice.interrupt()Con saldo, descarta al momento la salida en curso y envía la posición real que llegó a sonar, sin cortar la captura continua. El transporte antiguo mantiene su vaciado de entrada y toma de turno. La llamada sigue conectada.
realtimeVoice.stop()Cierra al momento la captura y la reproducción del host y pide al servidor que cuelgue en las llamadas con pago. Silenciar en local no es lo mismo que un final confirmado.
realtimeVoice.setMuted({ input, output })Silencia la captura y la reproducción por separado. Que hable el asistente no silencia el micrófono.
realtimeVoice.updateContext(context)Actualiza el contexto de la escena presenciada: un texto o { state: Record<string, string>, events: Array<{ id: string, text: string }> }. El texto aplanado no puede pasar de 4.000 caracteres.
realtimeVoice.updateInstructions(text)En el transporte antiguo, sustituye la dirección inicial (de 1 a 12.000 caracteres) en una pausa entre turnos. Con saldo, las instrucciones son fijas y la llamada solo emite un aviso no fatal de «no admitido». Con saldo, updateContext agrupa las actualizaciones presenciadas y emite confirmaciones pending/configuring/applied con número de revisión.
realtimeVoice.reactToScene(notice)Envía como observación un evento público de la escena que ya ha ocurrido, sin ejecutar su acción. Consulta los avisos acotados más abajo. No incluyas nunca texto privado de cuadernos o páginas.
realtimeVoice.cancelSceneReaction(id?)Quita por ID un aviso en cola o sonando. Sin ID, cancela solo las repreguntas pendientes y conserva los anuncios de eventos físicos. No detiene la llamada ni borra la entrada del micrófono.
realtimeVoice.resolveTool(callId, { accepted, focus?, reason? })La carta valida la petición de inspección acotada antes de aceptarla; el proveedor no ejecuta código ni aplica efectos de estado.
realtimeVoice.setSpatial({ x, z, yaw, sourceX, sourceZ })Actualiza la posición para la reproducción espacial.
realtimeVoice.onEvent(callback)Suscripción; devuelve una función para cancelarla.

El modo avatar pasa el audio de la IA por el LiveAvatar que elige el servidor y solo reproduce el audio sincronizado que este devuelve. No pide la cámara ni envía el audio del micrófono directamente a LiveAvatar. El host añade eventos video-status (connecting, live, stopped) y eventos video-frame con un ImageBitmap. Copia cada bitmap de forma síncrona a tu propio canvas dentro del listener del evento; el sandbox lo cierra justo después y confirma la entrega. No guardes el bitmap, no metas fotogramas en el estado de React ni los persistas. Vuelve a poner una imagen fija con video-status: stopped. El host se encarga de los tokens de sesión, la interrupción, la reproducción espacial, el control de flujo de fotogramas y la limpieza; las cartas no pueden elegir credenciales ni endpoints de avatar arbitrarios. La actividad de reproducción del avatar sigue el flujo devuelto, así que el que la IA termine de generar no corta antes de tiempo un turno hablado en pantalla.

Usa la interrupción manual cuando un personaje deba terminar cada frase salvo que el jugador pulse un control explícito:

tsx
// In the click handler that starts the call:
const { intent } = await api.realtimeVoice.prepare();
await api.realtimeVoice.start({ intent, instructions: "Your character direction.", interruptionMode: "manual" });
// In the player's Interrupt button/key handler:
api.realtimeVoice.interrupt();

El modo manual mantiene el micrófono abierto entre frases. La entrada que empieza o continúa durante una generación pendiente, un audio de avatar pendiente, una salida audible o su cola de eco queda fuera de los eventos de transcripción del usuario y del historial nativo de la conversación, aunque su ASR final llegue más tarde o no coincida con las palabras del personaje. La entrada protegida esperada no emite el aviso de palabras solapadas. Las confirmaciones nativas de borrado siguen bloqueando las respuestas siguientes. Una interrupción explícita admite una captura nueva solo después de la confirmación de vaciado del búfer de entrada; vuelve a hablar después de ese límite. La entrada que terminó antes de un nuevo turno de salida conserva su comportamiento de cierre habitual. Silenciar, pausar y desconectar siguen controlando la captura y la reproducción como siempre. Esta opción cambia qué entrada se admite, no el hardware del micrófono, el VAD, la voz ni el modelo acústico.

Los eventos incluyen status (connecting, connected, stopped, error), transcript (role, id estable, text acumulado, final) y peticiones tool de inspección acotadas. input usa los IDs de transcripción con speaking, transcribing o discarded; limpia el estado de la frase pendiente con discarded para que una transcripción fallida no deje un envío esperando para siempre. activity es listening, thinking o speaking; speaking sigue la reproducción real del búfer de salida, no el final de la generación. input-level es el volumen medido del micrófono (0–1) y level el volumen medido de la salida (0–1). Usa el medidor que corresponda a cada participante. input-hint trae un mensaje corto para el jugador y capture-settings indica qué cancelación de eco aplicó el navegador.

Las llamadas se detienen cuando el jugador se va, oculta la pestaña, pierde el permiso, para explícitamente o llega a cinco minutos. En las pruebas patrocinadas, además, el servidor cuelga por su cuenta, solo se permite una llamada a la vez por cuenta y hay un presupuesto de inicios en 24 horas móviles de 12 por jugador y 60 en total. Esta API es independiente de api.voice (mantener para hablar) y de api.tts (lectura por voz).

La salida de audio se desbloquea en el clic de consentimiento y se comprueba antes de capturar el micrófono. Si falla la reanudación, el audio sigue suspendido o el desbloqueo tarda más de dos segundos, start se rechaza y se liberan los recursos. Esto confirma que el navegador está listo para reproducir; el volumen del dispositivo lo sigue controlando el jugador.

Los avisos de escena tienen id (de 1 a 160 letras, dígitos, guiones bajos, dos puntos o guiones) y kind, con exactamente estos campos adicionales:

KindCampos adicionales
return, seen, overdue, writing, caught, knock, power-restored, clearanceNinguno
bulletintext: texto del anuncio no vacío, de 600 caracteres como máximo
power-cutseconds: número finito de 3 a 12; quien observa no puede ver durante ese tiempo
begin-inspectionfocus: desk, door o bed; line: anuncio público no vacío, de 240 caracteres como máximo
searchfocus: desk, door o bed
follow-upevidenceId: el ID de una declaración pública anterior de un residente; line: una sola pregunta no vacía, de 240 caracteres como máximo

Los campos extra se rechazan. Los avisos se deduplican por conexión y se limitan a ocho eventos aceptados cada diez segundos y a 128 IDs. Silenciar la salida suprime los avisos nuevos; la voz activa y las respuestas pendientes mantienen su orden normal. El texto de un anuncio se interpreta como un comunicado público; los demás eventos reciben un breve reconocimiento de ese evento concreto. start sigue admitiendo como máximo 12.000 caracteres de instrucciones y updateContext como máximo 4.000.

start({ instructions, context }) admite la misma forma de contexto. Para una escena física que cambia, usa una instantánea estructurada: las claves estables de state describen los hechos públicos actuales y los IDs inmutables de los eventos identifican observaciones registradas. Un hecho que cambia sustituye a su valor anterior. Que falte un evento significa que quedó fuera del presupuesto de historial, no que no ocurriera. No reutilices un ID para un testimonio corregido o nuevo. Incluye solo lo que este actor puede saber; esta API no filtra por ti páginas privadas ni acciones que no ha visto.

El host envía la instantánea completa al conectar y al reconectar. En el camino GPT-Live activado por el host, las instantáneas siguientes solo envían los hechos que cambiaron y los IDs de eventos que aún no se habían entregado, en una pausa de la reproducción. Los eventos pendientes sobreviven a la agrupación de estado, y cambiar un extracto no vuelve a reproducir un evento antiguo. Los textos simples de antes y el camino Realtime siguen sustituyendo el contexto completo. Límites: 16 campos de estado, 64 eventos por instantánea, 4.000 caracteres aplanados y 12.000 caracteres para las instrucciones iniciales combinadas más el separador de contexto y el texto. Las claves y los IDs son metadatos de transporte, no contenido hablado. Si quien produce los eventos supera la capacidad de eventos pendientes o de historial de la conexión, la llamada termina de forma visible en lugar de perder eventos en silencio.

El SDK solo reenvía el ID y el tipo del aviso. Las cartas existentes sin una variable unperson-room pueden seguir enviando los avisos fijos y sin texto return, seen, overdue, writing y caught. Son observaciones que informa la carta; el host no establece si son ciertas dentro de la ficción. Con el esquema de sala de Unperson, el host rellena cada evento admitido a partir del estado guardado confirmado; pasar texto al SDK no da permiso para inventar pruebas. Una repregunta tiene que citar algo que un residente dijo antes en el mismo intento. Caduca tras 45 segundos de tiempo de sala, un cambio de fase o de intento, o una declaración recibida más reciente. Una nueva intervención por micrófono también invalida al momento las repreguntas pendientes. Otros esquemas de sala necesitan un adaptador en el host para referencias de aviso más ricas, como anuncios, inspecciones y repreguntas ligadas a pruebas.

UI / navegación ​

MétodoQué hace
toggleImmersive()Activa o desactiva el modo inmersivo (pantalla completa)
openPersonaManager()Abre el gestor de personas del jugador para cambiar, crear o editar personas sin salir de la carta (el mismo panel que abre el menú «+» de la caja de mensaje). Durante la partida, cambiar de persona cambia la persona guardada de esta sesión y se aplica en el siguiente mensaje
getPersonaProfile()Lee la persona seleccionada de la sesión cuando el jugador decide importarla. Devuelve Promise<{ name, appearance, personality, backstory, entries? } | null> (entries es [{ title, content }]); null si no hay persona seleccionada; los fallos se rechazan. Las notas privadas nunca se incluyen. Guarda una copia en tus propios datos si la identidad tiene que quedarse fija durante la partida
openModelPicker(options?)Abre el selector de modelos del chat. Pasa { purpose: "side-completion" } cuando la elección es para tus propias llamadas a ai.complete; entonces se dejan fuera los modelos de prueba que solo sirven para el chat
openSessionManager()Abre el gestor de sesiones y ramas (cambiar, renombrar, eliminar ramas)
sharePlaythrough()Abre el diálogo para compartir esta sesión como partida compartida
openSupport()Abre el diálogo «apoyar al creador» (propina en dólares o regalo de Mushies), atribuido a la carta que se está jugando. Pensado para cartas con UI personalizada a pantalla completa, cuyo lienzo tapa el botón de corazón de la cabecera. Devuelve Promise<{opened, reason}>; reason es "self" (eres el creador), "signed-out" o "unavailable" (vista previa del editor). Espera el resultado y muestra el motivo: la primera persona que pulsa este botón es el propio creador probando su carta, y si no pasa nada parece que está roto
openCreditTopUp()Abre la ventana para recargar Mushies
fetchAsset(ref)Lee uno de tus recursos como bytes en crudo. Recibe un id de recurso ("@asset:<uuid>" o el uuid solo), nunca una URL. Devuelve Promise<{ok, bytes?, contentType?, error?}>; error es "bad-ref", "http-404", "too-large" (máximo 32MB), "unavailable" o un mensaje de red. El sandbox no puede pedir nada por su cuenta, y el navegador solo da una vía de carga privilegiada a imágenes y medios (no existe una etiqueta <model>), así que esta es la única forma de que entre un .glb, una tabla JSON grande o un atlas de sprites. Comprueba ok antes de tocar bytes. También funciona en la vista previa del editor
copyToClipboard(text)Copia al portapapeles (sustituye a navigator.clipboard.writeText)
navigate(path)Pide al host que navegue a una ruta como "/app/hub" (sustituye a window.location = ...)
showToast(message, type?)Muestra un aviso en la interfaz del host. type: "success", "error" o "info" (por defecto)

Almacenamiento local del navegador (por carta) ​

Sustituye a localStorage. Va por worldId; una carta no puede leer las claves de otra. Es almacenamiento de preferencias y caché de este dispositivo: no se sincroniza entre dispositivos ni entra en puntos de control, ramas o instantáneas compartidas. El alias sessionStorage del navegador también usa esta API local; no es lo mismo que api.sessionStorage, que se explica abajo.

MétodoQué hace
storage.get(key)Lee. Devuelve Promise<string | null>
storage.set(key, value)Escribe (solo texto). Devuelve Promise<void>
storage.remove(key)Borra. Devuelve Promise<void>

¿Necesitas datos complejos? Usa JSON.stringify / JSON.parse al guardar y al leer.

JSON en la nube (por sesión) ​

Usa api.sessionStorage para registros JSON pequeños que pertenecen a una sesión de juego y deben acompañarla en otros dispositivos. Se guardan aparte de las variables de la partida que usa la IA. Todos los métodos devuelven una promesa con { value, version, exists }. Una clave que nunca se escribió devuelve { value: null, version: 0, exists: false }; una clave borrada conserva su versión, así que comprueba exists en vez de suponer que null significa que no existe.

MétodoQué hace
sessionStorage.get(key)Lee el registro actual, o el registro congelado en una repetición compartida
sessionStorage.set(key, value, { expectedVersion })Guarda un valor JSON solo si su versión actual coincide; las opciones son obligatorias
sessionStorage.remove(key, { expectedVersion })Borra el valor y deja una marca de borrado con versión; las opciones son obligatorias
tsx
const previous = await api.sessionStorage.get("journal");
const saved = await api.sessionStorage.set(
  "journal", { notes: ["Llegamos al pueblo"] },
  { expectedVersion: previous.version }
);
// Usa saved.version para el siguiente cambio, incluido remove().

Ante SESSION_STORAGE_CONFLICT, conserva el borrador del jugador, vuelve a leer el registro y concilia los cambios antes de enviar otra vez. No reintentes a ciegas con valores antiguos. Espera una respuesta correcta antes de mostrar que se ha guardado. Respeta api.readOnly: en las repeticiones compartidas no se puede escribir. Leer y escribir requiere una sesión real o una repetición compartida autorizada; la vista previa del editor o la de invitado no sirven.

Las claves tienen de 1 a 128 letras o dígitos ASCII, _, ., : o -, y empiezan por letra o dígito. Límites: 32 KiB de JSON serializado en UTF-8 por valor, 256 KiB de valores actuales por sesión, 128 claves contando las marcas de borrado, 16 MiB de historial de valores por sesión y 1.000 cambios por hora entre todas las sesiones del propietario. Mantén los registros acotados; no es un almacén de archivos.

Las restauraciones de puntos de control que cambian JSON comparten estos límites de capacidad y frecuencia. Pasar del límite de historial de 16 MiB o de otra cuota devuelve HTTP 413; pasar del límite de cambios por hora devuelve 429. Se deshace la restauración entera, incluidos los medios y el estado de la historia, sin restauraciones a medias. El historial existente se conserva. Si los valores no cambian, la restauración no duplica el historial y puede seguir adelante aunque se hayan alcanzado estos límites de JSON.

El JSON y las referencias a medios se capturan juntos en los puntos de control, las ramas de la misma cuenta y las instantáneas compartidas. Restaurar un punto de control restaura ambos. Los datos de una sesión privada en curso solo son accesibles para su propietario; ser el creador de la carta no da acceso a ellos. Una instantánea compartida sigue las reglas existentes de visibilidad, estado de publicación de la carta, ocultación/moderación y nivel de contenido. Según esas reglas, quien comparte y el creador de la carta pueden leer la instantánea con privilegios, incluidas las instantáneas ocultas, sin publicar o sensibles. Los demás tienen que pasar los controles que correspondan; lo compartido como no listado es accesible con el enlace. No guardes secretos en registros que entren en algo compartido. Las claves arbitrarias del almacenamiento del navegador no se migran automáticamente.

Imágenes del jugador (por sesión) ​

Usa api.media para imágenes privadas del jugador. Los recursos del creador siguen usando @asset:. Si el servidor tiene configurado almacenamiento S3, las subidas están activadas en todas las cartas salvo que el operador pause las subidas nuevas.

MétodoQué hace
media.list(offset?)Devuelve { items, hasMore, uploadsEnabled }; carga más páginas mientras hasMore sea true
media.pick({ entryId?, metadata? }?)Abre el selector y sube la imagen; se resuelve con { mediaId, entryId }, o null si se cancela
media.upload(file, { entryId?, filename?, metadata?, uploadId? }?)Sube un Blob/File; devuelve { mediaId, entryId }. Reutiliza uploadId al reintentar la misma petición
media.remove(entryId, version)Quita la asociación con la sesión actual usando la versión del elemento; devuelve { removed }

Los elementos de la lista incluyen { id, entryId, filename, metadata, version, sizeBytes, url, thumbnailUrl, deleted }; id es el ID del medio. Guarda entryId/mediaId en el JSON, nunca URLs firmadas de visualización ni bytes en base64. Renueva las URLs con list() al volver a abrir y antes de que caduquen a los cinco minutos. Los elementos borrados pueden tener URLs nulas. Quitar una asociación con la sesión no borra el archivo para siempre ni libera capacidad mientras queden otras referencias.

Se admiten JPEG/PNG/WebP estáticos de hasta 16 MiB y 40 millones de píxeles decodificados. El servidor genera imágenes WebP de hasta 2048 píxeles más miniaturas; no es una copia de seguridad del archivo original. Las imágenes usan la capacidad de recursos compartida de la cuenta. Consulta la receta de imágenes del jugador para guardarlas y migrarlas.

Búsquedas del Lorebook ​

Acceso de solo lectura a las entradas de lore y los escenarios de la carta desde tu interfaz. Sirve para inspeccionar o escoger entradas a mano al montar el prompt de una llamada lateral, para construir un visor de diario dentro del juego o para un panel de depuración.

Campo / métodoQué hace
entriesReadonlyArray<SandboxEntry>: todas las entradas activadas, ya ordenadas por position. Consulta SandboxEntry
getEntry(name)Busca una entrada por nombre exacto (distingue mayúsculas y minúsculas). Devuelve SandboxEntry | null. En localhost, si no la encuentra, registra una sola advertencia con los nombres disponibles: útil cuando renombras una entrada y se te olvida actualizar la carta
worldbooksLos escenarios de la carta. Compara entry.worldbookId con worldbook.id para agrupar las entradas por escenario

En la mayoría de los casos no necesitas tocarlos directamente: pasa includeLorebook: "matched" a ai.complete() y el servidor monta el lore por ti (más abajo). Recurre a entries / getEntry cuando necesites control exacto; por ejemplo, «este PNJ solo conoce las entradas con la etiqueta tavern».

Llamar a las IA de la carta o hacer completaciones crudas ​

Hay dos formas de sacar texto de un modelo desde tu interfaz:

  • api.callAi(name, input) ejecuta una de las IA de la carta, configurada en el lienzo: su modelo, lo que ve, los campos de su Formato de respuesta, los reintentos, el enfriamiento y las frases de respaldo están en sus ajustes, y la respuesta se comprueba antes de llegarte. Úsala cuando la llamada forma parte del juego (una adivina, el regateo de un tendero, un PNJ que responde a un botón). Consulta Acciones de juego.
  • api.ai.complete(...), más abajo, es una llamada cruda al modelo: escribes todo el prompt en código y nada se comprueba por ti.

Completaciones crudas de IA ​

Llama al LLM fuera del flujo principal del chat. Úsalo para «monólogo interior de un PNJ en un panel lateral», «descripciones de objetos generadas por IA», «chats de móvil dentro de la carta», etc. No escribe en el historial de mensajes, no provoca actualizaciones de estado y no consume aperturas.

tsx
const api = useYumina()
const text = await api.ai.complete({
  messages: [
    { role: "system", content: "You are a surly merchant." },
    { role: "user", content: "Price me an iron sword." },
  ],
  onDelta: (chunk) => setStreaming((s) => s + chunk),  // optional, per-token
  model: "anthropic/claude-sonnet-4.6",                 // optional, defaults to selectedModel
  maxTokens: 500,                                       // optional, default 2048, max 8192
  temperature: 0.7,                                     // optional
  includeLorebook: "matched",                           // optional — see below
})

Devuelve Promise<string> con la respuesta completa. Si la llamada falla (límite de frecuencia, petición no válida, error del proveedor, sin sesión), la promesa igualmente se resuelve, con un texto de la forma "[Error: <message>]"; comprueba ese prefijo antes de usar el texto. Solo se rechaza al agotarse el tiempo de 120 segundos del cliente. En la vista previa de invitado se resuelve con "".

El content de un mensaje también puede ser un array de partes { type: "text", text } y { type: "image_url", image_url: { url } }, o el mensaje puede llevar attachments (imágenes de pickChatImage()), para modelos que leen imágenes.

Para respuestas que deba leer una máquina, pasa responseFormat: { type: "json_object" } y pide JSON explícitamente en tus mensajes. El SDK reenvía esta opción al proveedor. Que funcione depende del proveedor y el modelo elegidos; no impone el esquema de tu juego. Analiza el resultado y valida cada acción antes de aplicarla. Sin la opción, la completación sigue siendo de texto normal. Cualquier otra forma de formato se rechaza con HTTP 400 antes de la inferencia.

context: "session": contexto narrativo compartido ​

Activa la persona actual de la sesión autenticada, los prompts narrativos activados y los ajustes de generación actuales del jugador:

tsx
const result = await api.ai.complete({
  context: "session",
  includeLorebook: "matched",
  responseFormat: { type: "json_object" },
  messages: [
    { role: "system", content: 'Return JSON with only {"actions":[],"line":""}.' },
    { role: "user", content: sceneObservation },
  ],
})

La persona sigue a la cuenta salvo que la sesión esté bloqueada, incluido un bloqueo explícito sin persona. Los campos públicos de descripción de la persona se resuelven en el servidor; las notas privadas de la persona nunca se incluyen. Los prompts narrativos siguen las mismas reglas de carpetas activadas, modelo seleccionado y elegibilidad de la cuenta que el chat normal.

En el contexto de sesión, si omites includeLorebook equivale a "matched"; false excluye las entradas de la carta pero mantiene la persona y los prompts del usuario. true/"all" se salta la selección por palabras clave pero sigue respetando las condiciones de estado, los escenarios activos y el lore ligado a la interfaz. La coincidencia usa todos los mensajes de usuario que pasas, así que una instrucción final de corrección no borra la escena original. Las macros nativas usan el estado guardado de la sesión y la persona actual; las macros del último mensaje se refieren al historial que pasas. Las secciones de sistema, los diálogos de ejemplo, las entradas con profundidad y los roles posteriores al historial conservan su colocación nativa alrededor de ese historial. Los mensajes de sistema de quien llama van después de esas secciones narrativas, y el modo JSON añade una instrucción final que protege el protocolo de objeto pedido. Sigue validando en tu carta el esquema que vuelve.

worldbookIds (solo en contexto de sesión) limita el lore nativo a los escenarios indicados más las entradas que no están en ningún escenario. Si lo omites, la selección es la normal; [] significa solo las entradas fuera de escenarios. Los IDs tienen que existir; la activación, el interruptor Activado y las condiciones de las entradas siguen aplicándose. Pasar worldbookIds sin context: "session" devuelve HTTP 400.

Los ajustes actuales del jugador los pone la app anfitriona; las cartas no pueden pasar un objeto de preferencias arbitrario. maxTokens/temperature explícitos en la llamada tienen prioridad, después los ajustes del jugador, luego los de la carta y por último los valores crudos por defecto de abajo. Siguen aplicándose el tope de 8192 tokens de salida y el rango de temperatura de 0 a 2. Las preferencias admitidas de muestreo, razonamiento y streaming se reenvían igual que en el chat normal (incluida la penalización por repetición propia de cada modelo). Los ajustes de sesión no válidos devuelven HTTP 400.

Solo se usa el historial que pasas a la llamada lateral: no se importan el chat guardado, los resúmenes ni los efectos pendientes del chat. maxContext y los presupuestos de lore de la carta limitan las entradas opcionales que coinciden; no recortan el lore obligatorio, los presets ni el protocolo de quien llama, ni garantizan que el prompt completo quepa en la ventana de cada proveedor. El texto devuelto no aplica efectos ni guarda mensajes automáticamente. Si dejas context sin poner, se mantiene el comportamiento crudo que se describe abajo.

Salida estructurada opcional ​

Pasa responseFormat para pedir salida en JSON. Omítelo para el comportamiento de texto de siempre, o usa { type: "json_object" } para el modo JSON propio de cada proveedor. La salida con esquema estricto requiere por ahora una conexión de OpenRouter (oficial o BYOK):

tsx
const text = await api.ai.complete({
  messages: [{ role: "user", content: "Choose an action: wait." }],
  responseFormat: {
    type: "json_schema",
    json_schema: {
      name: "decision_v1",
      strict: true,
      schema: {
        type: "object",
        properties: { action: { type: "string", enum: ["wait"] } },
        required: ["action"],
        additionalProperties: false,
      },
    },
  },
})

El envoltorio acepta exactamente los campos que se ven. Los nombres de esquema deben cumplir /^[A-Za-z0-9_-]{1,64}$/, strict tiene que ser true y la raíz del esquema tiene que ser un objeto con type: "object". El formato serializado completo está limitado a 16.384 bytes UTF-8, profundidad 16 (la raíz del formato está en profundidad cero) y 2.048 valores visitados. Usa valores JSON simples; se rechazan ciclos, accesores y hooks de serialización. Los caracteres del esquema cuentan para el límite de 50.000 caracteres de contenido y para la estimación de coste del prompt.

Los demás adaptadores de proveedor rechazan json_schema con HTTP 400 y el código UNSUPPORTED_RESPONSE_FORMAT: “This provider does not support JSON Schema side completions.” OpenRouter envía el esquema completo y exige un endpoint que admita los parámetros pedidos, manteniendo las preferencias de enrutamiento oficiales y el enrutamiento de la cuenta BYOK. Los errores de esquema propios de un endpoint vuelven como resultado "[Error: …]", nunca como un éxito vacío ni como un formato más débil. Compruébalo y ofrece una alternativa adecuada.

Un esquema limita la estructura de la respuesta; no da permiso para actuar. Analiza y valida el contenido devuelto, el estado actual, las acciones permitidas y las pruebas antes de aplicar cualquier cambio de estado. Las palabras clave de esquema propias de cada proveedor se comprueban en el proveedor.

Límites y costes ​

LímiteValorOrigen
Máx. mensajes por llamada50El servidor rechaza con HTTP 400
Máx. contenido total50.000 caracteres sumando todos los mensajesEl servidor rechaza con HTTP 400
maxTokens por defecto2048Valor crudo por defecto; el contexto de sesión hereda antes los ajustes
Tope de maxTokens8192Los valores mayores se recortan sin aviso
Rango de temperature0–2, por defecto 1.0Los valores fuera de rango se recortan
Modelo por defectoEl selectedModel del jugador; si no hay ni model ni selectedModel, anthropic/claude-sonnet-4.6
Límite de frecuenciaGrupo propio para llamadas laterales: 100 llamadas laterales por minuto, aparte del presupuesto por minuto del chat principalAl pasarse devuelve HTTP 429 con el código RATE_LIMITED y la cabecera Retry-After
CréditosMismo cobro por token que el chat principal. A los usuarios BYOK no se les descuentan créditos en el servidor, pero pagan a su propio proveedorSe registra con el endpoint "side-completion"
AutenticaciónLa sesión tiene que ser del jugador actual; si no, la llamada falla con HTTP 404

ai.decide: preguntas de elección ​

await api.ai.decide({ state, questions }) pide a un modelo que elija una opción por pregunta y devuelve probabilidades en lugar de prosa. state es un objeto JSON pequeño que describe la situación. questions asocia el nombre de cada pregunta con { type: "choice", instructions, criteria }, donde criteria asocia cada etiqueta de opción con una descripción de cuándo aplica:

tsx
const { answers } = await api.ai.decide({
  state: { suspect: "Vex", lastLine: playerText },
  questions: {
    lying: {
      type: "choice",
      instructions: "Is the player lying to the guard?",
      criteria: { yes: "The statement contradicts what they did", no: "The statement fits what they did" },
    },
  },
})
// answers.lying → { choice: "yes", probabilities: { yes: 0.8, no: 0.2 }, confidence: 0.8 }

Hasta 8 preguntas por llamada, hasta 64 opciones por pregunta, etiquetas de opción de hasta 128 caracteres y unos 32.000 caracteres en total. Usa el modelo de decisión del servidor; el uso oficial lo paga la plataforma, y con una clave BYOK de OpenRouter puede cobrarse al proveedor del jugador. No cambia nada en la historia ni en el estado. Si falla, se rechaza (hay un límite de 15 segundos); no rellenes tú una respuesta que falte. Necesita una sesión real.

ai.context: montar la dirección de un actor secundario ​

await api.ai.context({ actor: "voice" | "director", model?, recentMessages? }) devuelve { instructions, receipt } para la sesión guardada y editable actual. Monta la dirección activa de la carta y los presets de usuario que apliquen sin generar texto, sin cobrar una llamada al modelo y sin cambiar el estado. Pasa explícitamente el modelo de destino real: un modelo de voz en directo y uno de texto pueden cumplir condiciones de entrada distintas. Para este método, el SDK no pone en su lugar el modelo de chat seleccionado.

recentMessages contiene solo registros públicos presenciados por el actor, { role: "user" | "assistant", content }: como máximo 24 mensajes, 4.000 caracteres cada uno y 12.000 en total. No se importa el historial normal del chat ni el perfil de persona de la cuenta. usa el nombre ficticio del jugador en la carta; las variables privadas no pueden expandirse dentro de las instrucciones. Las etiquetas de actor delimitan las entradas; los presets de estilo/sistema activos compartidos se aplican a ambos actores. La activación normal de entradas, la coincidencia de lore y las macros siguen funcionando. Los valores por defecto de ai.complete no cambian.

El recibo indica los IDs de las entradas incluidas y omitidas, los motivos de omisión y los IDs de los prompts de usuario que aplican, además del modelo, la política de persona/historial y el número de caracteres de las instrucciones. No contiene el texto de las entradas. Las instrucciones tienen un tope de 9.000 caracteres; si la dirección se pasa, se rechaza en lugar de recortarse en silencio. Captura los fallos y mantén visible la última dirección confirmada, marcada como desactualizada, hasta que una actualización salga bien. En una llamada en directo con el transporte antiguo, pasa el reemplazo correcto a realtimeVoice.updateInstructions; con saldo, las instrucciones iniciales son fijas. updateContext por sí solo no sustituye la dirección inicial.

includeLorebook: inyectar el lore de la carta automáticamente ​

Sin context: "session", las llamadas laterales se saltan el montaje del prompt del chat principal. Pasa includeLorebook y el servidor antepone un mensaje de sistema construido con las entradas de la carta:

ValorComportamiento
omitido / falseSin inyección (por defecto). Úsalo para traducciones, resúmenes, clasificación: todo lo que no necesite el contexto de la carta
true / "all"Inyecta todas las entradas activadas que no son de apertura, ordenadas por position. Predecible, más tokens
"matched"Ejecuta el mismo buscador de palabras clave que usa el chat principal sobre el último mensaje del usuario en messages. Las entradas alwaysSend se incluyen siempre; las que se activan por palabra clave solo se añaden cuando vienen al caso. Recomendado para llamadas laterales en personaje

Sin esto, una llamada lateral en personaje solo sabe lo que le cuentan tus mensajes, y la voz del personaje se va alejando de la del chat principal. Con "matched", un chat de móvil con un PNJ ve el mismo lore de la carta y el mismo perfil de personaje que ve el chat principal.

tsx
// A phone chat that stays in canon
api.ai.complete({
  messages: [
    { role: "system", content: "Stay strictly in character as Balder. Reply in one or two short lines." },
    ...history,
    { role: "user", content: userText },
  ],
  includeLorebook: "matched",  // server pulls Balder's profile + relevant world lore
})

Si necesitas más control (inyectar una entrada concreta por nombre, o solo las entradas con cierta etiqueta), recorre api.entries y monta tú el mensaje de sistema en lugar de usar includeLorebook:

tsx
const tavernLore = api.entries
  .filter((e) => e.tags?.includes("tavern"))
  .map((e) => `【${e.name}】\n${e.content}`)
  .join("\n\n")

api.ai.complete({
  messages: [
    { role: "system", content: `You are the tavern keeper.\n\n${tavernLore}` },
    { role: "user", content: userText },
  ],
})

Limitaciones del modo "matched": solo busca palabras clave en el último mensaje del usuario (no en todo el historial), y las entradas con condiciones que dependen de variables de la partida no se activan en llamadas laterales (el buscador ve un estado vacío). Usa true para incluirlo todo si te importa más la precisión que los tokens.

Inyección de contexto ​

Inyecta un mensaje de contexto de un solo uso en el siguiente turno de la IA del chat principal. Se gasta tras usarse una vez y no crea ningún mensaje visible en el chat. Úsalo para cosas que la IA principal debe saber pero que el jugador no debe ver como burbuja de chat: un mensaje en el móvil, un diálogo de un PNJ fuera de escena, un cambio en el entorno.

tsx
api.injectContext("You just received a cryptic text: 'Tonight, 9pm, usual place.'", { role: "system" })
// En el siguiente mensaje del jugador, la IA principal verá esto como un mensaje de sistema.

options: { role?: "system" \| "user" } (por defecto "system").

Para algo que la IA deba ver en todas las llamadas mientras siga siendo cierto (en qué habitación está el jugador, qué hay en pantalla), usa setScene.

Selector de modelo ​

Campo / métodoQué hace
selectedModelID del modelo actual
userPlanNivel del plan del usuario
preferredProvider"official" o "private"
setPreferredProvider(provider)Cambia entre "official" y "private". Devuelve Promise<{ ok, provider?, error? }>
setModel(modelId)Cambia de modelo (no espera respuesta)
getModels(provider?)Devuelve Promise<{ models, pinnedModels, recentlyUsed }>, donde models es Array<{ id, name, provider, contextLength, supportsImages? }>. Pasa "private" para los modelos de la clave propia del jugador
pinModel(modelId) / unpinModel(modelId)Fija o quita de fijados un modelo. Devuelve Promise<{ pinnedModels, accepted }>; accepted vale false si no se pudo fijar porque la lista está llena
modelFallbackCuando el modelo elegido no se puede usar y el host ofrece otro: el aviso que hay que mostrar; si no, null. <Chat> y <MessageInput> ya lo gestionan

La API también lleva los campos y métodos que usa el chat integrado para sus propios paneles: mezcla de modelos (mixMode, modelPool, setMixMode, addToPool, removeFromPool, setPoolWeight, togglePoolLock), un modelo local en el ordenador del jugador (localBridge, reconnectLocalBridge, resolveModelFallback, cancelModelFallback), los prompts del jugador (playerPrompts, togglePlayerPrompt), la memoria y los resúmenes de la sesión (memorySummaryEnabled, getSessionMemory, getSessionSummary y sus setters), Lore Shift (getLiveCanon y relacionados) y la protección de estado (getStateGuardSettings, setStateGuardSettings). Su forma sigue a la interfaz integrada y no se explica aquí.

Recursos ​

MétodoQué hace
resolveAssetUrl(ref)Convierte una referencia @asset:<id> en su URL /cdn/<id>. Es una transformación de texto, sin red. Las URLs HTTP/HTTPS pasan sin cambios. <img src="@asset:…"> y los @asset: dentro de estilos en línea y etiquetas <style> se resuelven solos; llámalo cuando necesites la URL como texto

Markdown ​

MétodoQué hace
renderMarkdown(text)Convierte markdown en HTML seguro (entidades HTML escapadas, etiquetas peligrosas eliminadas, formato conservado). Pasa el resultado a dangerouslySetInnerHTML dentro de una burbuja propia; mira el ejemplo
tsx
<div dangerouslySetInnerHTML={{ __html: api.renderMarkdown(msg.rawContent) }} />

Componentes ​

<Chat> ​

El chat completo de la plataforma. Sin props, es el chat por defecto.

Incluye: lista de mensajes, desplazamiento automático, cursor de generación, controles de swipe, acciones de mensaje (editar/eliminar/regenerar), barra de entrada, botones de elección, selector de modelo, modo de solo lectura y texto de apertura mientras no hay mensajes.

tsx
<Chat renderBubble={(msg) => <MyBubble {...msg} />} />

Props ​

PropTipoDescripción
renderBubble?(props: BubbleProps) => ReactNodePersonaliza el aspecto de cada burbuja de mensaje. Si lo omites, se usa el renderizado de markdown por defecto
className?stringClase CSS extra en el contenedor exterior
children?ReactNodeContenido que se muestra encima de la lista de mensajes (por ejemplo, una cabecera HUD fija)
design?, fontMap?objetoEl aspecto de los mensajes del editor de la Interfaz del jugador (Aspecto de los mensajes y sus ajustes finos). Solo se usan si no hay renderBubble. Los rellena el editor visual; normalmente tú no

BubbleProps ​

El objeto msg que recibe tu callback renderBubble:

CampoTipoSignificado
contentHtmlstringHTML seguro ya renderizado (el markdown ya está convertido). Normalmente se pasa a dangerouslySetInnerHTML
contentstringEl texto markdown sin las directivas [var: op value]. Úsalo para tus propias búsquedas con regex o pasa un trozo a renderMarkdown
rawContentstringTexto markdown original antes de renderizar (con el texto de las directivas)
role"user" | "assistant" | "system"Origen del mensaje
messageIndexnumberPosición en la lista (0 = el primero, normalmente la apertura)
isStreamingbooleantrue mientras este mensaje se está generando
stateSnapshotRecord<string, unknown> | nullEstado de la partida en el momento en que se generó este mensaje (útil para «qué HP o qué ubicación había entonces»)
variablesRecord<string, unknown>Variables actuales (las más recientes) de la partida
renderMarkdown(text) => stringAyuda: convierte cualquier texto markdown en HTML seguro

<MessageList> ​

Solo la lista de mensajes (con desplazamiento, cursor de generación y controles de swipe). Sin barra de entrada.

tsx
<MessageList />

No acepta renderBubble: para personalizar burbujas usa <Chat renderBubble={...} />, o prescinde de <MessageList> y lee api.messages directamente (el patrón de novela visual).

<MessageInput> ​

Solo la barra de entrada (con selector de modelo, botones de elección, menú de continuar/reiniciar y estado de generación).

tsx
<MessageInput />

Se oculta sola cuando api.readOnly es true.

<ChatCanvas> ​

Un alias de <Chat /> que se mantiene para que las cartas antiguas sigan funcionando. En código nuevo, usa <Chat />.

Componentes del selector de modelo ​

ComponenteQué hace
<ModelTrigger onClick={...} />El botón de modelo de la caja de mensaje del chat: nombre del modelo actual y saldo del jugador. Props: onClick (obligatorio), model?, provider?, showBalance? (por defecto true), compactBalance?, className?
<ModelPickerModal open={...} onClose={...} />El selector de modelos. Con solo open y onClose en una sesión real, abre el mismo selector que el chat (también puedes llamar a api.openModelPicker()). Pasa onSelectModel(modelId), title o subtitle para usarlo en una elección tuya en lugar del modelo del chat
<SessionMemoryModal open={...} onClose={...} />El diálogo de memoria de la sesión del chat. Necesita la extensión de memoria de sesión; comprueba api.memorySummaryEnabled antes de mostrar un botón para abrirlo

Una interfaz a pantalla completa sin <MessageInput> no tiene botón de modelo salvo que lo añadas:

tsx
var [pickerOpen, setPickerOpen] = React.useState(false)
// ...
<ModelTrigger onClick={function () { setPickerOpen(true) }} />
<ModelPickerModal open={pickerOpen} onClose={function () { setPickerOpen(false) }} />

Controles de lore ​

Envían una entrada de lore a la IA solo mientras un control de tu interfaz está encendido. El jugador nunca ve el texto de la entrada.

ComponenteQué hace
<LoreSlot id="slot-id" />No pinta nada. Mientras está montado, la entrada ligada a slot-id va a la IA; al desmontarlo, deja de ir. Móntalo de forma condicional ({open && <LoreSlot id="x" />})
<LoreButton slotId label icon? description? variant? className? />Un botón que enciende y apaga un slot. variant: "chip" (pastilla pequeña, por defecto) o "card" (fila a todo el ancho con interruptor y description)
<LoreSwitch slotId onLabel? offLabel? onIcon? offIcon? className? />Dos botones para un mismo slot: uno lo enciende y el otro lo apaga. Las etiquetas vienen en chino por defecto (开启 / 关闭), así que pasa las tuyas
<LoreGroup slots={[{ id, label, icon?, description? }]} variant? className? />Una fila de opciones en la que como mucho un slot está encendido a la vez. Elegir otra vez el activo lo apaga
<LorePanel title? slots? defaultOpen? className?>…</LorePanel>Un panel plegable para agrupar botones de lore. slots lista los ids de slot que cuenta su indicador de «cuántos están encendidos». title viene en chino por defecto (知识库), así que pasa el tuyo

LoreButton, LoreSwitch y LoreGroup guardan su estado en una variable llamada __lore_<slotId>, así que la elección se mantiene tras recargar, en los puntos de control y en las ramas.

Pon un control en tu código y se crea una entrada para su id de slot; aparece en Lore de interfaz dentro del Lorebook (Paneles → Lorebook). Escribe ahí lo que debe leer la IA. También puedes añadir condiciones de variables a la vinculación: entonces el control tiene que estar encendido y las condiciones tienen que cumplirse. api.loreUiBindings lista las vinculaciones.


Código de comportamiento (ctx) ​

Un comportamiento puede incluir un fragmento de Código entre sus efectos. Se ejecuta dentro del sandbox de la carta, con la API en vivo, cuando se dispara el comportamiento, y solo mientras el jugador tiene la partida abierta. El cuerpo del código es una función async, así que await funciona. Recibe un objeto, ctx:

Campo / métodoQué hace
ctx.varsUna copia de los valores actuales, con el ID de la variable como clave
ctx.get(name)Un valor, por nombre visible o ID
ctx.set(name, value)Escribe un valor
ctx.add(name, n)Suma n a un número
ctx.push(name, item)Añade un elemento a una lista
ctx.toast(text)Muestra un aviso al jugador
ctx.say(text)Envía una frase como jugador
ctx.callAi(ai, input?)Llama a una de las IA de la carta; se resuelve igual que api.callAi
ctx.random(a, b)Un número entero de a a b, ambos incluidos
ctx.event{ reactionId }: el comportamiento que se disparó

Las escrituras se acumulan mientras se ejecuta el código y se guardan juntas al terminar (con patchVariables). Si el código lanza un error, se registra en la consola del navegador y los valores escritos antes del error se guardan igualmente.

js
// Draw two cards from the deck
const deck = [...(ctx.get("deck") || [])]
const hand = []
for (let i = 0; i < 2 && deck.length; i++) {
  hand.push(deck.splice(ctx.random(0, deck.length - 1), 1)[0])
}
ctx.set("deck", deck)
ctx.set("hand", hand)
ctx.toast("You drew " + hand.join(" and "))

useAssetFont() ​

Carga un recurso de fuente subido como @font-face y te devuelve un texto listo para usar como valor de font-family en CSS.

tsx
const fontFamily = useAssetFont("@asset:my-font-id", {
  family: "Cinzel",
  fallback: "serif",
})
return <div style={{ fontFamily }}>Ancient runes</div>

Firma ​

ts
useAssetFont(
  assetRef: string | null | undefined,
  options?: AssetFontOptions
): string

La fuente se carga de forma asíncrona. Mientras carga, el hook devuelve options.fallback (por defecto "serif"); cuando está lista, se vuelve a renderizar con el nombre de familia completo (con un sufijo para evitar choques de nombres).

AssetFontOptions ​

CampoTipoDescripción
family?stringNombre de la familia tipográfica. Si se omite, se deduce del nombre del archivo o de assetRef
fallback?stringFuente de respaldo mientras carga. Por defecto "serif"
filename?string | nullNombre original del archivo, para adivinar el formato
mimeType?string | nullTipo MIME, para adivinar el formato
format?"opentype" | "truetype" | "woff" | "woff2" | nullFormato explícito
weight?string | numberfont-weight
style?stringfont-style (por ejemplo, "italic")
stretch?stringfont-stretch
display?FontDisplayfont-display (por defecto "swap")

Tipos ​

SandboxMessage ​

Forma de cada elemento de api.messages:

ts
interface SandboxMessage {
  id: string
  sessionId: string
  role: "user" | "assistant" | "system"
  content: string
  status?: "complete" | "streaming" | "failed"
  errorMessage?: string | null
  stateChanges?: Record<string, unknown> | null   // diff de variables actualizadas por este mensaje
  stateSnapshot?: Record<string, unknown> | null  // estado completo al generarse el mensaje
  swipes?: Array<{ content?, rawContent?, createdAt, model?, tokenCount?, refusal? }>
                                                  // respuestas alternativas de la IA; solo la activa conserva su texto
  activeSwipeIndex?: number
  model?: string | null
  tokenCount?: number | null
  generationTimeMs?: number | null
  creditCost?: number | null
  compacted?: boolean                   // oculto en la sección "older messages"
  attachments?: Array<{ type, mimeType, name, url }> | null
  createdAt: string                     // ISO-8601
}

En el sandbox, los swipes no llevan su propio stateSnapshot, y los que no son el activo no tienen content ni rawContent.

Checkpoint ​

ts
interface Checkpoint {
  id: string
  name: string
  messageCount: number
  createdAt: string   // ISO-8601
}

SandboxEntry ​

Una entrada de lore de solo lectura, disponible en api.entries y api.getEntry():

ts
interface SandboxEntry {
  id: string
  name: string
  content: string
  keywords: string[]
  position: number
  section: "system-presets" | "examples" | "chat-history" | "post-history"
  enabled: boolean
  role: string                            // "system" | "character" | "lore" | etc.
  tags?: string[]
  worldbookId?: string                    // el escenario al que pertenece; coincide con los IDs de api.worldbooks
  folderId?: string                       // la carpeta en la que la guardaste, si hay
  audience?: "ai" | "player" | "both"
  conditions?: Condition[]                // condiciones de variables, si es lore condicional
  conditionLogic?: "all" | "any"
  portrait?: string | null                // retrato del personaje como URL absoluta
  portraitVideo?: { idle: string | null; speaking: string | null } | null
}

Es una versión reducida del WorldEntry interno del motor, con solo los campos que una carta necesita para montar prompts y mostrar cosas. El entorno de ejecución ya quita las entradas desactivadas y ordena por position, así que las cartas nunca tienen que hacerlo.

BranchContext ​

ts
interface BranchNode {
  id: string
  name: string | null
  parentSessionId: string | null
  branchedFromMessageId: string | null
  messageCount: number
  updatedAt: string   // ISO-8601
  createdAt: string   // ISO-8601
}

interface BranchContext {
  current: BranchNode          // la sesión en la que estás
  parent: BranchNode | null    // la rama de la que te separaste, o null en la raíz
  siblings: BranchNode[]       // otras ramas separadas del mismo padre, las más antiguas primero
  children: BranchNode[]       // ramas separadas de `current`, las más antiguas primero
}

APIs de navegador bloqueadas ​

Tu código se ejecuta dentro de un iframe sandbox="allow-scripts" de otro origen, sin allow-same-origin, y su política de seguridad de contenido tiene connect-src 'none'. Eso significa:

  • Sin acceso a las cookies ni al localStorage de la app anfitriona
  • Sin peticiones de red propias (fetch, XMLHttpRequest, WebSocket)
  • Sin manipular window.parent directamente

El sandbox sustituye algunas APIs del navegador para que el código antiguo no se rompa:

Sustituciones ​

Lo que escribisteLo que pasa de verdad
fetch('/api/...')El host no lo atiende: la promesa se resuelve con una respuesta vacía, sin datos. Usa los métodos del SDK de abajo
fetch('/cdn/...')Pasa al navegador, que lo bloquea (connect-src 'none'). Usa api.fetchAsset() para leer los bytes de un recurso
fetch('any other URL')Rechazado (lanza un error)
localStorage.getItem/setItem/removeItem/clearLas escrituras se guardan por carta a través del host, pero getItem solo ve los valores escritos desde que se cargó la página. Usa api.storage para leer los valores guardados
sessionStorage.*Igual
navigator.clipboard.writeText()Equivale a api.copyToClipboard()
navigator.clipboard.readText() / read() / write()Rechazado (lanza un error)
window.location.pathname / href / assign / replaceObjeto sintético; pathname siempre es /app/chat/{sessionId}; asignar o llamar a assign / replace provoca la navegación
window.location.reload()Recarga la sesión
window.__yuminaToggleImmersive()Equivale a api.toggleImmersive()

Usa el SDK ​

No escribasEscribe
fetch('/api/sessions', { method: 'POST' })api.createSession(worldId)
fetch('/api/sessions/' + sid, { method: 'DELETE' })api.deleteSession(sid)
localStorage.getItem("k")await api.storage.get("k")
window.location = "/app/hub"api.navigate("/app/hub")
navigator.clipboard.writeText(t)api.copyToClipboard(t)

APIs de navegador que SÍ están disponibles ​

Todo lo que no toca la red ni el origen de la app anfitriona funciona como en cualquier navegador, sin envoltorio del SDK:

APIUso típico en cartas
<input type="file"> + FileReaderLeer un archivo que elige el jugador. Para conservar una imagen del jugador, súbela con api.media; consulta Receta: Imágenes subidas por el jugador
URL.createObjectURL / revokeObjectURLCrear una URL temporal en memoria para un Blob (por ejemplo, una vista previa antes de guardar)
<canvas> + getContext("2d") + toDataURL / toBlobRedimensionar, recortar o componer imágenes antes de subirlas
<img>, <audio>, <video>Mostrar URLs https:, referencias @asset:... y URLs data:/blob:
<iframe>Incrustar páginas alojadas en *.pages.dev, *.vercel.app, *.netlify.app o *.github.io
IntersectionObserver, ResizeObserver, matchMedia, requestAnimationFramePrimitivas estándar de maquetación y animación
crypto.randomUUID, crypto.subtleHashes y generación de IDs para estado en el cliente
WebAudio (AudioContext)Síntesis o análisis de audio ligeros
Pointer lockControles en primera persona (requestPointerLock)

De un vistazo: toda la API ​

Una tabla para repasarla de una vez.

useYumina()
├── State reads
│   ├── variables, globalVariables
│   ├── worldName, worldId, worldCover, background, sessionId
│   ├── currentUser (account), user (persona-aware)
│   ├── mode, capabilities, language
│   ├── messages, hasEarlierMessages, isLoadingEarlier
│   ├── isStreaming, streamingContent, streamingReasoning
│   ├── pendingChoices, error, errorCode, sendFailureNonce, composerSendKey
│   ├── readOnly, greetingContent, canvasMode, speakerBubbles
│   ├── checkpoints
│   ├── entries, worldbooks, loreUiBindings
│   └── selectedModel, userPlan, preferredProvider, balance
├── Game actions
│   ├── sendMessage(text, attachments?)
│   ├── pickChatImage() → Promise<image | null>
│   ├── setVariable(id, value)
│   ├── patchVariables(values) → Promise<void>
│   ├── executeAction(actionId)
│   ├── executeActionAndWait(actionId, params?) → Promise<{applied, variables, firedIds}>
│   ├── callAi(ai, input?) → Promise<{text, fields, fallback} | null>
│   ├── onAiOutput(channel?, cb) → unsubscribe
│   ├── setScene(scene, { events? })
│   ├── onStoryEvent(cb) → unsubscribe
│   ├── switchGreeting(index) → Promise<void>
│   ├── clearPendingChoices()
│   └── setComposerDraft(text)              // prefill, no send
├── Chat control
│   ├── editMessage(id, content, options?) → Promise<boolean>
│   ├── deleteMessage(id) → Promise<boolean>
│   ├── regenerateMessage(id)
│   ├── continueLastMessage()
│   ├── stopGeneration()
│   ├── restartChat()
│   ├── swipeMessage(id, direction) → Promise
│   └── loadEarlierMessages() → Promise<boolean>
├── Sessions / branching
│   ├── revertToMessage(id) → Promise<void>
│   ├── branchFromMessage(id) → Promise<string | null>
│   ├── getBranchContext() → Promise<BranchContext>
│   ├── createSession(worldId) → Promise<string>
│   ├── deleteSession(id) → Promise<void>
│   └── listSessions(worldId) → Promise<Array>
├── Checkpoints
│   ├── saveCheckpoint() → Promise<void>
│   ├── loadCheckpoints() → Promise<void>
│   ├── restoreCheckpoint(id) → Promise<void>
│   └── deleteCheckpoint(id) → Promise<void>
├── Audio
│   ├── playAudio(trackId, opts?)
│   ├── stopAudio(trackId?, fadeDuration?)
│   ├── pauseAudio(trackId)
│   ├── resumeAudio(trackId)
│   ├── onAudioEnded(cb) → unsubscribe
│   ├── setAudioVolume(type, volume)
│   └── getAudioVolume(type) → number
├── Voice
│   ├── tts.speak / stop / onPlaybackFrame / setPrefs / preview, ttsState
│   ├── voice.prepare / record / stop / cancel / setPrefs, voiceInputState
│   └── realtimeVoice.getConfig / finish / prepare / start / interrupt / stop / setMuted / updateContext /
│       updateInstructions / reactToScene / cancelSceneReaction / resolveTool /
│       setSpatial / onEvent
├── UI / navigation
│   ├── toggleImmersive()
│   ├── openPersonaManager(), getPersonaProfile() → Promise
│   ├── openModelPicker(options?), openSessionManager(), sharePlaythrough()
│   ├── openSupport() → Promise<{opened, reason}>, openCreditTopUp()
│   ├── fetchAsset(ref) → Promise<{ok, bytes?, contentType?, error?}>
│   ├── copyToClipboard(text)
│   ├── navigate(path)
│   └── showToast(message, type?)
├── Storage
│   ├── storage.get(key) → Promise<string | null>
│   ├── storage.set(key, value) → Promise<void>
│   └── storage.remove(key) → Promise<void>
├── JSON de sesión en la nube
│   ├── sessionStorage.get(key) → Promise<{value, version, exists}>
│   ├── sessionStorage.set(key, value, {expectedVersion}) → Promise<{value, version, exists}>
│   └── sessionStorage.remove(key, {expectedVersion}) → Promise<{value, version, exists}>
├── Imágenes de la sesión
│   ├── media.list(offset?) → Promise<{items, hasMore, uploadsEnabled}>
│   ├── media.pick(options?) → Promise<{mediaId, entryId} | null>
│   ├── media.upload(file, options?) → Promise<{mediaId, entryId}>
│   └── media.remove(entryId, version) → Promise<{removed}>
├── Lorebook
│   ├── entries (ReadonlyArray<SandboxEntry>)  // sorted by position, enabled only
│   ├── worldbooks
│   └── getEntry(name) → SandboxEntry | null
├── AI
│   ├── ai.complete({ messages, onDelta?, model?, maxTokens?, temperature?, context?, worldbookIds?, includeLorebook?, responseFormat? }) → Promise<string>
│   │    // includeLorebook: true | "all" | "matched" — auto-inject world lore
│   ├── ai.decide({ state, questions }) → Promise<{ answers }>
│   └── ai.context({ actor, model?, recentMessages? }) → Promise<{ instructions, receipt }>
├── Context injection
│   └── injectContext(message, { role? })
├── Model picker
│   ├── setModel(modelId), setPreferredProvider(provider) → Promise
│   ├── getModels(provider?) → Promise<{ models, pinnedModels, recentlyUsed }>
│   ├── pinModel(id), unpinModel(id) → Promise<{ pinnedModels, accepted }>
│   └── modelFallback
├── Assets
│   └── resolveAssetUrl(ref) → string
└── Markdown
    └── renderMarkdown(text) → string   // safe HTML

Sandbox globals (no import)
├── React
├── useYumina, useAssetFont
├── Icons  (every Lucide icon)
├── Chat, MessageList, MessageInput, ChatCanvas (alias of Chat)
├── ModelTrigger, ModelPickerModal, SessionMemoryModal
├── LoreSlot, LoreButton, LoreSwitch, LoreGroup, LorePanel
└── Tailwind utility classes (CSS-level)

Behavior code (ctx)
└── vars, get, set, add, push, toast, say, callAi, random, event

Blocked / replaced
├── fetch(...) → blocked; use SDK methods / fetchAsset
├── localStorage / sessionStorage → write-through only; read with api.storage
├── window.location → synthetic + navigate
└── navigator.clipboard → copyToClipboard

Browser APIs that work as-is
├── <input type="file"> + FileReader      // then api.media.upload
├── <canvas>, URL.createObjectURL          // image processing
├── IntersectionObserver, ResizeObserver, matchMedia, rAF
├── crypto.randomUUID, crypto.subtle
└── WebAudio (AudioContext)

Siguiente: la Guía de UI personalizada tiene ejemplos completos, y las Recetas tienen montajes enteros de los que partir.