Saltar al contenido
BeeuniBeeuni
Español
Esc
↑↓navegar↵abrir⌘Jvista previa
En esta página

Sesiones

Ejecuta una sesión, transmite sus eventos en tiempo real y gestiona los errores que puede generar.

Una sesión es una ejecución de un agente, en su propio entorno aislado y en su propia rama de git. beeuni.session(projectId, sessionId) devuelve el identificador para todo lo que hace una sesión: iniciarla, enviar prompts, recibir eventos en streaming y consultar su estado. Esta página cubre el identificador, el protocolo de confirmación de disponibilidad, el streaming y los errores tipados que puede generar una llamada al SDK.

const s = beeuni.session(projectId, sessionId);

s es el identificador para todo lo que hace una sesión. El ID de la sesión, el ID del entorno aislado y el nombre de la rama tienen el mismo valor. Consulta Sesiones para conocer el concepto.

Ciclo de vida de la sesión

Método Encapsula Qué hace
s.get(opts?) GET /projects/:pid/sessions/:sid Consulta los detalles de la sesión
s.update(input) PATCH …/sessions/:sid Cambia el nombre de la sesión, reemplaza sus etiquetas o combina claves de metadatos
s.start(waitMs?) POST …/sessions/:sid/start Aprovisiona e inicia el entorno de ejecución
s.restart() POST …/sessions/:sid/restart Reinicia el entorno de ejecución; conserva el mismo entorno aislado
s.configState() GET …/sessions/:sid/config Consulta si la configuración está actualizada (stale) y el estado de la versión de configuración (release)
s.reloadConfig(input?) POST …/sessions/:sid/reload Recompila la configuración del agente y reemplaza el entorno de ejecución tras la validación
s.reloadConfigStream(input, onEvent) POST …/sessions/:sid/reload-stream Ejecuta la misma recarga y emite fases de progreso confirmadas por el servidor
s.stop() POST …/sessions/:sid/stop Detiene el entorno de ejecución; la sesión se conserva
s.delete() DELETE …/sessions/:sid Elimina la sesión
s.setSharing(intent) PUT …/sharing Establece las opciones de uso compartido y visibilidad
s.participants() GET …/sessions/:sid/participants Consulta quién puede abrir la sesión y quién envió cada prompt
s.watch() GET …/sessions/:sid/watch Consulta si recibes las notificaciones de esta sesión. Requiere notification_center; 403 feature_disabled mientras está desactivado
s.setWatch(watching) PUT …/sessions/:sid/watch Activa (true) o silencia (false) las notificaciones de la sesión para ti. Requiere notification_center; 403 feature_disabled mientras está desactivado
s.presence({ tab_id, active, alerts? }, { keepalive? }) PUT …/sessions/:sid/presence Informa de una pestaña abierta. alerts: true indica que la pestaña muestra sus propias notificaciones, por lo que se retiene la notificación push al teléfono. Con notification_center desactivado, cualquier pestaña presente la retiene. keepalive permite que la solicitud continúe tras cerrar una página; activado de forma predeterminada para un informe de ausencia
s.cost() GET /usage/session-costs/:sid Consulta el coste definitivo del LLM y del cómputo sin iniciar el entorno de ejecución
s.scope() GET …/sessions/:sid/scope Consulta la restricción de secretos almacenada y las vinculaciones de conexión materializadas
s.rescope(input) PUT …/sessions/:sid/scope Reemplaza los campos de alcance proporcionados para el siguiente prompt o llamada a una herramienta
s.commit(input?) — Confirma el trabajo del agente en un commit
s.providerSecretPool.get(providerId) GET …/provider-secret-pools/:providerId Consulta los ID de los secretos de cuenta seleccionados
s.providerSecretPool.list() GET …/provider-secret-pools Enumera los grupos configurados, incluidos los vacíos, e informa de can_edit
s.providerSecretPool.set(providerId, secretIds) PUT …/provider-secret-pools/:providerId Reemplaza la selección; null restaura el valor predeterminado del proyecto

Etiquetas y metadatos

Cada sesión incluye labels: string[] y un objeto metadata. Úsalos para clasificar las sesiones y decidir cómo las muestra tu aplicación.

  • Etiquetas de formato libre: se eliminan los espacios al inicio y al final de cada una, tienen entre 1 y 64 caracteres, con un máximo de 20 por sesión, y se descartan los duplicados. La coincidencia es exacta y distingue entre mayúsculas y minúsculas.
  • Metadatos: un objeto JSON de formato libre para tus propias claves, con un máximo de 16.384 caracteres de JSON por escritura. El servidor también almacena allí sus propias claves (custom_name, source, …); una solicitud que incluya una clave gestionada por el servidor se rechaza con 400. Añade un prefijo a tus claves (app_…) para evitar conflictos con ellas.
// Set both at create.
const project = beeuni.project(projectId)
const created = await project.sessions.create({
  initial_prompt: 'Triage ticket T-142',
  labels: ['support', 'tier:gold'],
  metadata: { app_ticket: 'T-142', app_priority: 2 },
})

// Update later. `labels` replaces the list; `metadata` merges keys and a
// `null` value removes that key.
const s = beeuni.session(projectId, created.session_id)
await s.update({ labels: ['support', 'resolved'], metadata: { app_priority: null } })

// List by label, server-side: a session must carry EVERY given label.
const { items } = await project.sessions.listPage({ labels: ['support', 'resolved'] })

// Render conditionally.
for (const session of items) {
  const badge = session.labels?.includes('tier:gold') ? 'Gold' : null
  const ticket = session.metadata.app_ticket as string | undefined
}

En React, pasa labels a useProjectSessions(projectId, { labels }); cada conjunto de etiquetas tiene su propia entrada de caché. Los agentes establecen los mismos campos desde la CLI (beeuni sessions new --label … --meta k=v, beeuni sessions update --label …) y desde el servidor MCP alojado (start_session labels / metadata, list_sessions labels). Una versión ejecutable es packages/sdk/examples/12-session-labels.ts.

Con pooled_provider_secrets y llm_gateway activados, una sesión puede seleccionar varios secretos de cuenta disponibles para un proveedor. Crea un recurso con alcance de proyecto mediante beeuni.accounts.secretResources.create(accountId, { project_id: projectId, ...input }). Todos los miembros del proyecto pueden usarlo de forma predeterminada. Restringe su uso con .setAccess(accountId, secretId, 'members', userIds). Las consultas devuelven metadatos y permisos, nunca el valor del secreto. Tras un 429 del proveedor antes de que comience la salida, la pasarela prueba otra clave seleccionada.

await s.providerSecretPool.set('anthropic', [primarySecretId, backupSecretId]);
const selected = await s.providerSecretPool.get('anthropic');
await s.providerSecretPool.set('anthropic', null); // inherit project default

Usa el método en streaming cuando quien realiza la llamada muestre el progreso de la recarga:

await s.reloadConfigStream({ refresh_repo: false }, (event) => {
  if (event.type === 'phase') console.log(event.phase);
});

Las fases son checking-session, refreshing-workspace, compiling-config, applying-config y confirming-config. El servidor omite refreshing-workspace cuando refresh_repo es false. La fase applying-config incluye el reemplazo del entorno de ejecución validado por el daemon.

s.configState() devuelve release cuando la API admite versiones de configuración. Su tipo es SessionConfigRelease. Los resultados de la recarga incluyen el mismo objeto.

const { stale, release } = await s.configState();
if (release?.fallback_reason) {
  // The desired config failed on the box. An earlier config runs the session.
  console.warn(release.fallback_reason, release.running_release_id);
} else if (stale === true) {
  // Moves the session onto the base branch's current config AND
  // fast-forwards its /workspace checkout. `workspace_checkout` says what
  // happened to the checkout; `detail` words both halves.
  const result = await s.reloadConfig();
  console.log(result.workspace_checkout, result.detail);
}

stale tiene tres estados. null significa que Beeuni no pudo determinarlo. Comprueba stale === true, nunca !stale. Una API anterior omite release. Consulta Convergencia de la configuración.

s.participants() devuelve { participants, total, multi_user }. participants enumera las personas que pueden abrir la sesión ahora, con el propietario primero, hasta un máximo de 20, cada una { user_id, name, email, avatar_url, is_viewer }; total es el recuento total. multi_user es true cuando dos o más personas pueden abrir la sesión. En React, useSessionParticipants(projectId, sessionId) consulta los mismos datos. Quién escribió cada mensaje se indica en s.messageAuthors().

Tres métodos de consulta más completan el identificador:

  • s.previews() — posibles puertos de vista previa que expone el entorno de ejecución.
  • s.publicShares.list() / .create(input) / .revoke(shareId) — enlaces públicos para compartir.
  • s.reminders.list() / .create(input) / .update(reminderId, { enabled }) / .remove(reminderId) — prompts programados para esta sesión. Consulta Recordatorios. .create({ transcript: true }) crea un enlace de solo lectura a la conversación (un enlace activo por sesión; una segunda llamada devuelve el mismo enlace). Léelo de forma anónima con getPublicSessionShareMessages(token); findActiveTranscriptShare(shares) selecciona el activo de .list().
  • s.audit(limit?) — el registro de auditoría de las acciones del agente en la sesión.
  • s.attachments.upload(file, options?) — guarda un archivo privado antes del inicio. Máximo de 50 MiB. Devuelve una URL estable para una parte de archivo de un prompt.
  • s.attachments.read(attachmentId, signal?) — lee los bytes guardados como un Blob sin iniciar el sandbox.
  • s.transcript(options?) — una transcripción compacta del lado del servidor (texto y llamadas a herramientas, sin entradas ni salidas de herramientas). Funciona con un token de sesión con ámbito de proyecto.
  • s.transcriptSync(options?) — la transcripción que el servidor guarda al final de cada turno, en forma de sobres de mensajes de runtime. Cada parte se conserva 1 —texto, razonamiento y cada llamada a herramienta con su entrada, salida, error y metadatos— excepto los bytes de los adjuntos. Responde mientras el sandbox está detenido o aún se está activando, sin iniciarlo. Pasa limit (como máximo 500) para obtener la ventana más reciente, y el next_cursor de una ventana como before para leer la anterior. Una ventana también se limita a 1,000,000 de caracteres de JSON: conserva sus mensajes más recientes, y next_cursor identifica entonces el más antiguo que conservó. Pasa child (el id ses_… de un subagente, la sesión que abre una fila de subagente) para leer la transcripción guardada de ese subagente; se guarda la de cada subagente que la conversación haya enviado. source es mirror; complete es true solo cuando la ventana contiene todos los mensajes guardados y la copia guardada llega al primer mensaje de la sesión. Una sesión cuyo entorno de ejecución se leyó por completo y no contenía mensajes responde con available: true, complete: true, total: 0 y ningún mensaje. Una llamada a herramienta guardada antes del 2026-09-28, cuando el historial guardado omitía las entradas y salidas de herramientas, se entrega con la entrada que su título o sus metadatos acreditan y, si es un comando, también con su salida.
  • s.voiceTranscript(options?) — la transcripción de la llamada de voz en directo de esta sesión (turnos hablados más llamadas a herramientas de los workers ask_beeuni/run_command). Devuelve una lista vacía cuando la sesión no tiene una llamada en directo, no un 404.

Archivos que almacenó una sesión

findSessionAttachments(messages) enumera todos los archivos almacenados a los que hace referencia una transcripción, en orden y una sola vez cada uno: un archivo subido por un usuario, una referencia <file> en un prompt o la copia que el historial guardado conserva de un archivo que mostró el agente. Lee tanto una transcripción en directo como una guardada, y nunca lanza una excepción por una transcripción mal formada.

import { findSessionAttachments } from '@beeuni/sdk'

const saved = await s.transcriptSync({ limit: 500 })
for (const file of findSessionAttachments(saved.messages)) {
  const blob = await s.attachments.read(file.attachment_id) // no sandbox needed
  console.log(file.filename, file.role, blob.size)
}

Cada resultado es { url, attachment_id, filename, mime, message_id, role }. role es user para un archivo subido y assistant para un archivo que mostró el agente; mime es null para este último, y los bytes llevan su propio tipo.

Quién escribió cada mensaje

Cada mensaje tiene un autor. El servidor lo obtiene de la credencial que envió el prompt: un miembro del proyecto o el agente de otra sesión. La transcripción del entorno de ejecución no lo incluye.

const { authors, initial_author } = await beeuni.session(projectId, sessionId).messageAuthors()
// getSessionMessageAuthors(projectId, sessionId) is the same call as a function.

const author = authors[runtimeMessageId] // undefined when the sender is unknown

authors usa como clave el id del mensaje del entorno de ejecución. initial_author es la sesión principal del primer mensaje de una sesión creada. Es null cuando la sesión no tuvo un primer prompt. Cada autor es un SessionMessageAuthor:

kind Campos
'member' user_id, name, email (string | null)
'session' session_id, name (el título de la sesión), agent (el agente que ejecuta, si tiene nombre)

Qué modelo respondió a un turno

Un mensaje de la transcripción indica el modelo que solicitó su turno. Cuando la cadena de respaldo del proyecto responde desde otro modelo, solo el registro de solicitudes de la puerta de enlace contiene el modelo que respondió y lo que Beeuni facturó.

const { latest, billed_cost, turns } = await beeuni.session(projectId, sessionId).modelUsage()
// getSessionModelUsage(projectId, sessionId) is the same call as a function.

latest?.served_model   // 'glm-5.3-flash': the model that answered the newest request
latest?.fallback_from  // 'codex/gpt-6.1-sol' when a fallback model answered, else null
turns[userMessageId]   // { served_models, fallback_from, billed_cost } for one turn

latest es null antes de la primera respuesta. billed_cost es el importe en USD que Beeuni descontó por las llamadas a modelos de la sesión. turns usa como clave el id del mensaje del entorno de ejecución del prompt que inició cada turno. served_models enumera los modelos que respondieron en ese turno, primero los que recibieron más solicitudes. En React, useSessionModelUsage(projectId, sessionId) lee el mismo registro; llama a su refetch() cuando termine un turno.

En React, useSessionMessageAuthors(projectId, sessionId, messageCount) lee los mismos datos. Pasa el número de mensajes de la transcripción. Un mensaje nuevo es el único evento que añade un autor, así que la consulta vuelve a obtener los datos cuando cambia el número.

La disponibilidad requiere un protocolo de enlace

Antes de enviar un prompt, llama a ensureReady(). Aprovisiona el sandbox si es necesario, espera a que arranque el entorno de ejecución y devuelve el entorno de ejecución resuelto.

const { runtimeSessionId, runtimeUrl, sandboxId } = await s.ensureReady();

En un arranque en frío, ensureReady() puede lanzar RUNTIME_UNAVAILABLE. Consulta Reintentar en un arranque en frío para saber qué significa y cómo reintentarlo.

s.send() y s.abort() llaman a ensureReady() por ti.

Inicializar una fijación de sesión del entorno de ejecución autorizada por el servidor

Un host de React renderizado en el servidor puede proporcionar la fijación de sesión del entorno de ejecución ya persistida para la misma sesión de Beeuni:

const session = useSession(projectId, sessionId, {
  initialRuntimeSessionId: persistedSession.runtime_session_id ?? persistedSession.opencode_session_id,
});

El valor inicial solo hidrata el contenido de la transcripción en caché mientras se ejecuta /start. No anula la identidad del entorno de ejecución. La fijación que devuelve /start es la fuente de autoridad y sustituye un valor inicial obsoleto.

opencode_session_id es el nombre obsoleto de runtime_session_id. Ambos contienen el mismo valor; lee el nombre antiguo solo como alternativa de respaldo indicada arriba.

No aceptes este valor de un selector de tenant no confiable. No crees una sesión del entorno de ejecución en el host. Beeuni crea y persiste la sesión raíz. Las cachés de consultas del entorno de ejecución y los controladores de transcripción tienen como ámbito el entorno de ejecución del sandbox, por lo que los ids de sesión del entorno de ejecución iguales de distintos sandboxes no comparten entradas de caché.

Los reintentos de mensajes pendientes conservan la URL de su sandbox de origen cuando cambias de sesión. Una respuesta de mensaje 404 o 410 detiene los reintentos automáticos sin borrar la transcripción. Una reconciliación explícita puede reintentar la lectura y restaurar la sincronización normal tras una respuesta correcta.

Enviar un prompt

s.setModel({ providerID, modelID }); // sticky for later send() calls
s.setAgent('build'); // sticky for later send() calls

await s.send('Refactor the auth module');
await s.send('One-off task', { model, agent }); // overrides for this call only
await s.abort(); // stop the current run

send() coloca el prompt en la bandeja de entrada duradera de prompts de la sesión (POST .../prompts), la vía que usan el editor web y la CLI. Se resuelve cuando se almacena el prompt, con la fila de la bandeja de entrada (prompt_id, state, message_id), no cuando termina el turno. Lee la respuesta de s.stream() o de la transcripción.

Para las sesiones REST de runtime, el primer send() en un handle lee el modelo y el agente persistidos en la sesión de Beeuni. Esto evita que una sesión de runtime heredada de una instantánea reutilice valores predeterminados obsoletos de la instantánea.

La precedencia de selección del prompt es:

  1. La llamada a send().
  2. El valor setModel() o setAgent() del handle.
  3. El valor predeterminado persistido de la sesión de Beeuni.

setModel solo elige lo que solicita el siguiente send local: nunca sale del handle. Para persistir un modelo nuevo para una sesión en ejecución del lado del servidor, usa changeModel:

const { applied_live } = await s.changeModel('anthropic/claude-opus-4-8');

El cambio se aplica al reiniciar el entorno de ejecución, por lo que un turno en curso termina. applied_live es true cuando una sesión en ejecución lo aplicó en ese momento, y false cuando se aplica en el siguiente inicio. Solo el propietario de la sesión o quien haga la llamada con permisos de administrador del proyecto puede cambiar el modelo; cualquier otra persona recibe 403.

send() resuelve el entorno de ejecución y después le envía el prompt. abort() detiene la ejecución actual sin eliminar la sesión.

Ámbito y coste de la sesión

Lee la restricción de secretos almacenada y los enlaces de conexión materializados. secrets_allowlist: null significa que se aplica la concesión de secretos del agente:

const scope = await s.scope();
scope.connector_bindings_configured; // false = inherits the project defaults

connector_bindings es el mapa RESUELTO, por lo que tiene el mismo aspecto para una sesión que haya sobrescrito sus conectores y para otra que herede los valores predeterminados del proyecto. Lee connector_bindings_configured para distinguirlas antes de mostrar el alcance o devolverlo.

Reemplaza uno o ambos campos de alcance:

await s.rescope({
  secrets: ['DATABASE_URL'],
  connector_bindings: {
    github: { connection_id: connectionId },
  },
});

Cada campo proporcionado reemplaza por completo su valor anterior. Omite un campo para dejarlo sin cambios. Los cambios de conexión se aplican a la siguiente llamada a una herramienta. Eliminar un secreto impide su entrega futura, pero no puede eliminar un valor ya revelado del contexto del modelo ni de un proceso existente.

Ambos ejes tienen una forma explícita de volver al valor predeterminado. No equivalen a un valor vacío:

await s.rescope({
  secrets: null, // inherit the agent's secret grant
  connector_bindings: null, // drop the override; inherit the project defaults
});

secrets: [] y connector_bindings: {} son la instrucción opuesta: un «sin secretos del proyecto» y «sin ningún conector» explícitos, incluidos los valores predeterminados del proyecto. Una sesión que envía {} cuando quería enviar null deniega por defecto todos los alias que no haya nombrado.

Lee el registro unificado de costes:

const cost = await s.cost();

El registro combina el coste finalizado del LLM, el coste facturado del cómputo del entorno aislado, el uso del modelo, los totales de tokens, la duración del cómputo y los asientos contables. s.cost() no llama a ensureReady().

Estado del entorno de ejecución y vistas previas

Método Devuelve Uso
s.health(init?) { status, ok, health, body } Comprobar si el entorno de ejecución está activo
s.previewUrl(port, path?) string Obtener una URL de proxy para un puerto que el agente haya expuesto
s.proxyUrl(url?) string | undefined Reescribir una URL de localhost que el agente haya mostrado
const { ok, health } = await s.health();
const url = s.previewUrl(3000, '/docs');

s.health() nunca lanza excepciones. Llámalo en cualquier momento, incluso antes de que la sesión tenga un entorno de ejecución. s.previewUrl() y s.proxyUrl() necesitan un entorno de ejecución resuelto: llama primero a s.ensureReady() o lanzarán SessionNotReadyError. Consulta Errores de preparación de la sesión.

Qué admite el entorno de ejecución

health.capabilities enumera las funciones de sesión que ofrece el entorno de ejecución. Consulta una con runtimeSupports(capabilities, capability) y oculta el control cuando devuelva false. Una sesión de runtime ofrece las diez; una sesión de pi ofrece solo session.subagents y responde a las demás con 501 feature_not_supported (404 para el documento de configuración). Harnesses contiene la matriz completa de runtime y pi.

Capacidad Función
session.rewind Revertir a un mensaje y restaurarlo
session.compact Resumir la conversación bajo demanda
session.commands Comandos de barra del proyecto
session.fork Bifurcar una sesión en un mensaje
session.subagents Sesiones hijas de subagentes
session.mcp Servidores MCP a los que se conecta el propio entorno de ejecución
session.todo La lista de tareas del entorno de ejecución
session.shell Un comando de shell ejecutado como un turno
session.attach Conectar el cliente de terminal propio del harness
session.config Un documento de configuración del entorno de ejecución (/global/config); los agentes y modelos proceden del proyecto en ambos casos
import { runtimeSupports } from '@beeuni/sdk';

const { health } = await s.health();
if (!runtimeSupports(health?.capabilities, 'session.compact')) hideCompact();

runtimeSupports devuelve true antes de que llegue una respuesta de estado y para un entorno de ejecución que no enumere ninguna capacidad session.*: un demonio creado antes de que existieran las capacidades ejecuta runtime, que ofrece todas las funciones. En React, usa useRuntimeSupports.

La respuesta de start también incluye la lista: SessionStartResult.capabilities se establece con stage: 'ready'. useSession la registra, de modo que la comprobación de capacidades es correcta antes de que responda la primera consulta de estado.

Términos de estado

Todos los hosts usan el mismo vocabulario, de modo que cada estado tiene el mismo término en todas las pantallas.

Exportación Uso
sessionListStatus(session, reviewCount?) Una sesión en una lista: needs-you, starting, running, done, stopped, failed, legacy
SESSION_LIST_STATUS[status] { label, tone, description } para ese estado
sessionConnectionLabel(connection) Una etiqueta breve para el ordenador de la sesión, o null para unknown y live
SESSION_NOTICE La frase del editor de mensajes mientras el ordenador no esté listo
turnRetryLabel(secondsLeft) Retrying in 5s, Retrying now o Waiting to retry cuando no se conoce la cuenta atrás

Los tonos son actionable, live, progress, muted y danger. El verde es solo para live y actionable; una sesión finalizada es muted. Un estado que envíe una API más reciente y que esta compilación no conozca se muestra como stopped, nunca como failed.

Streaming

Usa s.stream() para recibir eventos en directo en un script o servidor. En una aplicación React, usa useSession en su lugar: gestiona todo el ciclo de vida de la sesión por ti.

s.stream() es el flujo de eventos de compatibilidad REST de runtime. La API de Beeuni lo transmite mediante proxy desde el entorno aislado. No hay un endpoint WebSocket independiente. El transporte es fetch con un cuerpo de respuesta en streaming, que se lee mediante ReadableStream y TextDecoderStream. El SDK gestiona la reconexión, la espera progresiva y la comprobación de latidos.

Transmite una sesión en streaming:

  1. Llama primero a ensureReady(). El entorno de ejecución no existe hasta que se inicia el entorno aislado.
  2. Abre el flujo antes de enviar un mensaje para no perderte los primeros eventos.
  3. Envía el mensaje.
  4. Cierra el flujo cuando veas session.idle.
const session = beeuni.session(projectId, sessionId);
const { runtimeSessionId } = await session.ensureReady();

const stream = await session.stream({
  onEvent: (event) => {
    if (event.type === 'session.idle' && event.properties.sessionID === runtimeSessionId) {
      onTurnDone();
      stream.close();
    }
  },
});

await session.send('Refactor the auth module');

El flujo entrega eventos en lotes, separados por 16 ms. Los eventos message.part.delta consecutivos para un mismo campo de una parte dentro de un lote llegan como un único evento. Su properties.delta es el texto de esos eventos, unido en orden. Su id es el id del último evento. Su matriz coalesced enumera los eventos que reemplazó. Añade delta al campo de la parte, como harías con un único delta.

El streaming necesita fetch con un cuerpo ReadableStream real y TextDecoderStream. Los navegadores, Node 18 y versiones posteriores, Bun y Cloudflare Workers lo admiten.

Un host cuyo fetch no pueda transmitir en streaming proporciona su propio transporte. El SDK lo llama una vez por conexión y sigue gestionando la reconexión, la reanudación de Last-Event-ID, la espera progresiva y los latidos:

import { configureBeeuni, notifyHostSignal, type RuntimeEventTransport } from '@beeuni/sdk';

const transport: RuntimeEventTransport = async function* ({ url, headers, signal }) {
  // Yield { data, id?, retry? } for each server-sent message of ONE connection.
  // Return when the server ends it. Throw (with `status` for a non-2xx answer)
  // when it fails.
};

configureBeeuni({ backendUrl, getToken, eventStreamTransport: transport });

// A host without `visibilitychange` / `online` events reports them itself.
notifyHostSignal('visible'); // the app is back in the foreground
notifyHostSignal('online');  // the network is back
notifyHostSignal('retry');   // a person asked to reconnect now

React Native no tiene fetch en streaming de forma predeterminada. La aplicación móvil de Beeuni usa este punto de integración con react-native-sse. createHttpSessionSyncController sigue estando disponible para un host que solo necesite un historial acotado y sincronización de estado.

El controlador carga primero los 10 mensajes más recientes. loadOlder() sigue el cursor del servidor. loadHttpSessionHistory() sigue todos los cursores para las exportaciones explícitas.

Tipos de eventos

Cada evento tiene un type y un objeto properties que contiene sus datos, por ejemplo event.properties.sessionID.

type Cuándo se activa
message.updated / message.removed Un mensaje cambió o se eliminó.
message.part.updated / message.part.removed Una parte (texto, llamada a herramienta, archivo) creció o se eliminó.
session.status Cambió el estado de actividad de la sesión.
session.idle El turno terminó.
session.error El turno falló. El evento contiene el error.
question.asked El agente solicitó información.
question.replied / question.rejected Llegó la respuesta a una pregunta.

Convierte mensajes y partes sin procesar en contenido renderizable con classifyTurn. Consulta Referencia del SDK.

Reintentos en un arranque en frío

ensureReady() consulta el endpoint /start de la sesión —cada llamada usa sondeo largo de hasta 30 s— hasta que el entorno de ejecución alcanza una etapa terminal ready/failed/stopped o vence su plazo (readyTimeoutMs, ~180 s de forma predeterminada). En una sesión ya activa, el primer sondeo resuelve ready inmediatamente. En un arranque en frío, sigue consultando mientras el sandbox indica retriable: true, por lo que un inicio lento simplemente tarda más en lugar de lanzar una excepción. Solo lanza un ApiError con code: 'RUNTIME_UNAVAILABLE' if the runtime is still not ready cuando vence el plazo.

ensureReady() es idempotente, así que las llamadas simultáneas para la misma sesión comparten una solicitud /start en lugar de enviar varias. La función auxiliar retryUntilReady de abajo ahora es opcional —ensureReady() ya reintenta internamente—, pero sigue siendo útil si quieres un tiempo total mayor que el readyTimeoutMs predeterminado.

async function retryUntilReady<T>(ensure: () => Promise<T>): Promise<T> {
  const deadline = Date.now() + 300_000;
  for (;;) {
    try {
      return await ensure();
    } catch (error) {
      const provisioning = error instanceof ApiError && error.code === 'RUNTIME_UNAVAILABLE';
      if (!provisioning || Date.now() > deadline) throw error;
      await new Promise((r) => setTimeout(r, 3_000));
    }
  }
}

Consulta Clases de error para ver la estructura completa de ApiError. En React, useSession reintenta /start por ti, así que no necesitas este patrón.

Qué te indica /start

Cada respuesta de /start describe esa llamada, no el historial acumulado de la fila. Cuatro campos contienen esa información.

Campo Significado
observed_at Un único reloj para toda la respuesta.
action Lo que hizo el servidor: inspected, checked_provider, resumed, provisioned, restored, reconciled, awaited_wake, cooling_down.
observation Lo que comprobó el servidor. known: false significa no comprobado en esta llamada, nunca «comprobado sin encontrar nada».
boot phase (provisioning / resuming / booting / ready / parked / failed), since y actively_starting.

boot.actively_starting responde a «¿se está ejecutando una operación del proveedor para esta sesión ahora mismo?». Un payload starting con actively_starting: false significa que el servidor está esperando a que termine el periodo de espera para reintentar, no que una máquina esté arrancando.

{
  "stage": "starting",
  "retriable": true,
  "reason": "runtime_wake_cooldown",
  "observed_at": "2026-08-26T14:00:00.000Z",
  "action": "cooling_down",
  "boot": { "phase": "resuming", "since": "2026-08-26T13:58:00.000Z", "actively_starting": false },
  "observation": {
    "provider": { "known": false, "status": null, "checked_at": null },
    "runtime": { "known": false, "state": null, "boot_phase": null, "checked_at": null }
  },
  "failure": {
    "category": "sandbox-provider",
    "message": "The runtime did not start (attempt 2). Retrying automatically.",
    "retryable": true,
    "evidence": {
      "check": "provider_not_running",
      "observed_at": "2026-08-26T13:58:00.000Z",
      "error": null,
      "attempts": 2,
      "next_retry_at": "2026-08-26T14:03:00.000Z"
    }
  }
}

Un inicio fallido se reintenta por ti

Un inicio que falla establece un periodo de espera, no un resultado permanente. La siguiente llamada a /start tras ese periodo vuelve a intentar la activación automáticamente. El periodo de espera aumenta con los fallos consecutivos (2 min, 5 min, 10 min). Tras cinco fallos consecutivos, /start responde stage: "failed" con el número de intentos en failure.message; ese resultado caduca 30 minutos después del último fallo, y POST …/restart lo borra inmediatamente.

retriable se calcula en cada llamada. Un estado que el servidor aún puede reintentar nunca contiene retriable: false.

failure.evidence indica la comprobación que produjo el resultado negativo, cuándo se ejecutó y cuándo reintentará el servidor. Cada fallo de /start lo contiene.

Archivos adjuntos al prompt

Sube archivos locales cuando los usuarios los añadan al editor de mensajes. Las subidas privadas pertenecen al proyecto y no requieren una sesión en ejecución.

import { usePromptAttachments } from '@beeuni/sdk/react';

const uploads = usePromptAttachments(projectId);
// File picker, paste, and drop handlers call uploads.add(file) or uploads.addMany(files).
// Render uploads.attachments. Refuse Send only while an attachment is `error` or `aborted`.

async function submit() {
  const ids = uploads.attachments.map((item) => item.id);
  uploads.submit(ids); // Send never waits: the composer clears and uploads continue.
  paintMessage(text, ids);
  try {
    const parts = await uploads.whenReady(ids);
    await beeuni.session(projectId, sessionId).prompts.create({
      clientMessageId,
      messageId,
      parts: [{ type: 'text', text }, ...parts],
    });
    uploads.forget(ids);
  } catch {
    uploads.reclaim(ids);
  }
}

whenReady(ids, { signal }) se resuelve cuando todas las subidas están listas, en el orden de ids. Se rechaza cuando una subida falla, se cancela o se elimina, o cuando signal se cancela. submit(ids) entrega las entradas a ese envío: salen de attachments, dejan de contar para los límites y siguen subiendo después de que el editor de mensajes se desmonte. Llama a forget(ids) cuando el POST se complete correctamente, o a reclaim(ids) para devolverlas al editor de mensajes tras un envío fallido. La parte de archivo resuelta contiene attachment_id, filename y mime. No contiene ninguna URL ni bytes del archivo. El servidor vincula y materializa la subida. Cada archivo llega al ordenador de la sesión, incluidas las imágenes y los PDF: antes de que se ejecute el prompt, el servidor lo escribe en /workspace/uploads/.beeuni-inbox/ y el prompt lo referencia mediante su ruta. El propio prompt no transporta bytes del archivo. Con el historial guardado activado, el servidor también conserva una copia para la transcripción. Un fallo al copiar nunca impide que un archivo llegue al ordenador. Un archivo que el servidor no puede escribir bloquea el prompt, que se reintenta. Usa estas partes al crear sesiones en la plataforma, iniciar sesiones ya activas o en las rutas de la bandeja de entrada de prompts. No pases identificadores privados directamente a sendParts del entorno de ejecución de runtime. Las partes de archivo heredadas de la plataforma con url siguen funcionando.

Cada elemento tiene un id estable, la referencia file original, receivedBytes y un estado: pending, uploading, processing, ready, error o aborted. Los bytes enviados determinan el progreso. El controlador publica una instantánea del progreso solo cuando cambia el porcentaje entero, como máximo diez por segundo por subida. processing puede permanecer al 100 % hasta que el servidor complete la subida. Los límites son 50 MiB por archivo, 100 MiB por mensaje y 20 archivos. Los archivos no deben estar vacíos. De forma predeterminada, se suben dos archivos simultáneamente.

retry(id) reanuda el mismo identificador. Después de attachment_size_mismatch o attachment_failed, el servidor no conserva ningún identificador utilizable, por lo que retry(id) vuelve a subir el File como un nuevo adjunto. Una subida caducada no se puede reintentar: retry(id) lanza una excepción y el error del elemento contiene el código attachment_expired. abort(id) cancela el trabajo sin terminar. remove(id) elimina la entrada, cancela su subida y se resuelve de inmediato. El DELETE del servidor se realiza en la medida de lo posible y nunca se rechaza; una subida que no pueda eliminar caduca después de 24 horas. forget(ids) libera las entradas sin eliminar los objetos del servidor. El hook cancela el trabajo listado al desmontarse o cambiar de proyecto e intenta eliminar sus subidas en la medida de lo posible: ningún envío las retiene y los borradores no conservan ningún identificador. Las subidas entregadas continúan.

Las selecciones solo existen en memoria. Nunca guardes objetos File, URL de blob, URL firmadas ni identificadores de subida en un borrador. Las subidas abandonadas caducan después de 24 horas.

Los hosts sin framework usan beeuni.project(projectId).attachments.createController() con los mismos métodos. Suscríbete mediante subscribe(listener) y lee getSnapshot(). Llama a dispose() cuando se cierre el editor de mensajes. Quienes hagan llamadas directas pueden usar attachments.upload(file, { signal, onProgress, onUpload, resume }) and attachments.delete(attachmentId). Conserva los metadatos de onUpload para reanudar manualmente con el mismo ID.

El servidor selecciona el transporte en el campo upload del identificador:

  • kind: 'direct' es el valor predeterminado. El SDK envía el archivo completo en un solo PUT a upload.url con upload.headers y sin encabezado Authorization. Con XMLHttpRequest (navegadores, React Native), el progreso indica los bytes enviados. Con fetch, el progreso indica 0 y luego el tamaño completo. Una URL caducada, o una que Storage rechaza con 400, 401 o 403, se vuelve a firmar una vez para el mismo attachment_id. Un 409 de Storage significa que un intento anterior almacenó el archivo.
  • kind: 'chunked' envía solicitudes PUT autenticadas y secuenciales de upload.chunk_size bytes. El SDK acepta cualquier chunk_size positivo. Solo un despliegue cuya capa perimetral descarta cuerpos de solicitud grandes selecciona este modo.

La finalización verifica el tamaño almacenado y el SHA-256. Si la finalización responde 409 attachment_not_uploaded, onUpload informa del identificador directo con received_bytes: 0, por lo que retry(id) o una reanudación vuelve a enviar el archivo. El inicio, la carga y la finalización reintentan los tiempos de espera agotados, los errores de red, 429 y 5xx con espera exponencial y variación aleatoria. El margen es de 60 segundos desde el primer fallo, por lo que una carga larga que falla al final sigue reintentándose. La finalización también reintenta attachment_processing, con un margen de cinco minutos. El inicio nunca reintenta 402 (un BillingError: la cuenta no puede ejecutarse) ni 429 attachment_budget_exceeded (40 cargas sin terminar o 500 MiB de cargas sin enviar para el usuario; las cargas no utilizadas caducan en un plazo de 24 horas). El servidor responde o rechaza una finalización en un máximo de 105 segundos. Cada solicitud de finalización permite 120 segundos. Las cancelaciones del llamante nunca se reintentan.

La referencia de un archivo adjunto enviado se libera 1 hora después de que se entregue su prompt, y cuando se elimina su sesión o proyecto. El siguiente barrido de mantenimiento elimina el archivo, y su attachment_id ya no se puede enviar.

Archivos

s.files lee y escribe en el sandbox de la sesión: list, read, readBlob, status, findFiles, findText, upload, create, copy, remove, mkdir, rename. Cada llamada resuelve primero el entorno de ejecución y siempre se dirige al sandbox propio de esta sesión. Consulta la referencia del SDK para ver la lista completa de métodos.

Verbos de sesión

El identificador lee y responde al entorno de ejecución por ti. Cada verbo aprovisiona primero la sesión y se dirige al sandbox propio de esta sesión, sea cual sea el harness que la ejecute.

// A page of the transcript, oldest first: { info, parts } messages (beeuni.transcript.v1).
const { messages, hasMore } = await s.messages({ limit: 50 });
// What the agent waits on.
const { statuses, permissions, questions } = await s.pending();
await s.answerPermission(permissions[0].id, 'once'); // 'once' | 'always' | 'reject'
await s.answerQuestion(questions[0].id, [['Yes']]); // null dismisses the question
// Summarize the conversation. Needs the `session.compact` capability (see health()).
await s.compact();

s.runtime, el cliente de compatibilidad REST sin procesar, está obsoleto. Usa los verbos anteriores con send, abort, rewind y stream.

Prepara una sesión de proyecto

Llama a ensureWarm() cuando la página de inicio de un proyecto necesite un entorno de ejecución listo antes del primer prompt.

const project = beeuni.project(projectId);
const warm = await project.sessions.ensureWarm();

// An ORDINARY session. Prompt it like any other.
await beeuni.session(projectId, warm.session.session_id).send("Build me a widget");

ensureWarm() crea o devuelve una sesión no utilizada para el usuario actual. Es la misma creación que ejecuta sessions.create(), con los valores predeterminados del proyecto: el mismo control de facturación y los mismos requisitos de conectores. La única diferencia es metadata.warm, que oculta la sesión de sessions.list() hasta que llega su primer prompt.

Trátala como especulativa. Un 409 WARM_SESSION_UNAVAILABLE significa que el proyecto no se puede preparar ahora mismo; continúa con sessions.create(), que informa del motivo real.

La sesión preparada lleva el agente DEFAULT y el sandbox del proyecto. Si el usuario elige otro, abandónala y llama a sessions.create(): una sesión preparada sin utilizar permanece oculta y se elimina automáticamente.

Gestión de errores

Cada llamada a través de createBeeuni se rechaza con una subclase tipada de Error, nunca con un objeto simple. Captura el error, comprueba instanceof y decide cómo proceder según .status o .code.

import { ApiError, BillingError } from '@beeuni/sdk';

try {
  await beeuni.project(projectId).sessions.create();
} catch (err) {
  if (err instanceof BillingError) {
    // 402 — out of credits or over a plan limit
  } else if (err instanceof ApiError) {
    // any other failed request — err.status, err.code, err.detail
  } else {
    throw err;
  }
}

Clases de error

Clase Hereda de Cuándo se lanza Campos clave
ApiError Error Predeterminada para cualquier solicitud fallida: estado incorrecto, fallo de red, tiempo de espera agotado o cancelación status, code, detail, response, url, endpoint, timeout
AuthError ApiError getToken devolvió null. Beeuni nunca envió la solicitud code siempre es 'NO_SESSION'
BillingError ApiError HTTP 402. La única clase de error de facturación status (402), code, detail.message
RequestTooLargeError ApiError HTTP 431. Normalmente, demasiados archivos en una solicitud detail.suggestion
SessionNotReadyError Error Se ejecutó un accesor del entorno de ejecución antes de ensureReady() name es 'SessionNotReadyError'

ApiError.name es 'ApiError' de forma predeterminada. Dos casos lo sobrescriben:

  • name: 'AbortError', code: 'ABORTED': la solicitud se canceló, por ejemplo, al navegar. Esto no es un fallo. Ignóralo.
  • code: 'TIMEOUT': se agotó el tiempo de espera de la propia solicitud. url, endpoint y timeout muestran qué agotó el tiempo de espera.

Para cualquier otro fallo, status contiene el código de estado HTTP. code proviene del error_code del backend o, en su defecto, del estado como cadena. message es una propiedad propia enumerable de ApiError, por lo que se conserva con JSON.stringify y la expansión de objetos.

Beeuni vuelve a enviar una solicitud que recibe 401 con un token nuevo de getToken(), cuando ese token difiere del que envió. Una solicitud con un cuerpo de flujo de un solo uso no se vuelve a enviar.

Beeuni reintenta algunas solicitudes antes de que tu código vea un error. Si una solicitud GET o HEAD devuelve 502, 503 o 504, Beeuni la reintenta hasta 2 veces, con una espera de 250 ms y luego de 500 ms. Un fallo transitorio de transporte en una GET o HEAD —un error de red, no un código de estado— se reintenta de la misma forma. Un reintento que tiene éxito nunca llega a onError. Beeuni nunca reintenta solicitudes POST, PUT, PATCH o DELETE, ni una respuesta 500.

Beeuni lanza AuthError en el cliente, antes de enviar una solicitud, cuando getToken() devuelve null. AuthError hereda de ApiError, por lo que err instanceof ApiError sigue coincidiendo. Comprueba err instanceof AuthError o err.code === 'NO_SESSION' para tratar «no ha iniciado sesión» como un caso distinto de un fallo del backend.

Beeuni lanza BillingError para cada respuesta HTTP 402: sin créditos, por encima de un límite del plan u otro control de facturación. detail.message contiene el motivo del backend y code contiene el código de máquina del backend (por ejemplo, app_account_unfunded). BillingError hereda de ApiError, así que comprueba BillingError primero cuando gestiones ambos.

Beeuni lanza RequestTooLargeError para HTTP 431. Esto normalmente significa que la solicitud llevaba demasiados archivos. detail.suggestion contiene una indicación lista para mostrar al usuario.

Errores de disponibilidad de la sesión

Dos errores significan que el sandbox de la sesión aún no está listo. Gestiona cada uno de forma distinta.

SessionNotReadyError lanza una excepción de forma síncrona cuando llamas a un accessor de runtime — session.previewUrl() o session.proxyUrl() — antes de que este identificador de sesión haya resuelto su sandbox. Un identificador de sesión solo resuelve su propio sandbox; nunca recurre al sandbox de otra sesión.

import { SessionNotReadyError } from '@beeuni/sdk';

const s = beeuni.session(projectId, sessionId);
try {
  const url = s.previewUrl(3000); // throws: not resolved yet
} catch (err) {
  if (err instanceof SessionNotReadyError) {
    await s.ensureReady();
  }
}

Llama primero a await session.ensureReady() o llama a send(), que prepara la sesión internamente. session.health() es el único accessor que nunca lanza este error, por lo que puedes consultarlo antes de que se inicie la sesión.

RUNTIME_UNAVAILABLE es el segundo error: significa que ensureReady() agotó el tiempo de espera mientras esperaba un arranque en frío. Consulta Reintentar durante un arranque en frío para ver el patrón completo. En React, useSession lo reintenta por ti y lo expone mediante el valor phase en lugar de lanzar una excepción.

Helpers

Helper Firma Qué hace
parseBillingError(error) (error) => Error Envuelve una respuesta de 402 en un BillingError. Devuelve los demás errores sin cambios
isBillingError(error) (error) => boolean Devuelve error instanceof BillingError
formatBillingErrorForUI(error) (error) => BillingErrorUI | null Devuelve null para errores que no son de facturación. En caso contrario, devuelve { alertTitle, alertSubtitle } para un modal de actualización
isRuntimeNotReadyResponse(error) (error) => boolean Es true para el 503 del daemon del sandbox mientras el runtime de la sesión aún no puede aceptar una solicitud. La solicitud no se reenvió, por lo que es seguro reintentar. Misma respuesta en runtime y pi
isSandboxNotReadyError(error) (error) => boolean Es true para la respuesta anterior y para un sandbox detenido, iniciándose o aparcado. Represéntalo como «despertando» y sigue consultándolo
isRuntimeStartingError(error) (error) => boolean Es true para ambas respuestas anteriores y para una URL de runtime que aún no está fijada. Un límite de errores reintenta en este caso en lugar de mostrar un fallo
import { formatBillingErrorForUI } from '@beeuni/sdk';

try {
  await beeuni.session(projectId, sessionId).start();
} catch (err) {
  const ui = formatBillingErrorForUI(err);
  if (ui) showUpgradeModal(ui.alertTitle, ui.alertSubtitle);
}

En @beeuni/sdk/react

@beeuni/sdk/react vuelve a exportar BillingError, RequestTooLargeError, parseBillingError, isBillingError y formatBillingErrorForUI. No vuelve a exportar ApiError ni AuthError; impórtalos desde @beeuni/sdk.

useSession clasifica cada fallo de send, answerQuestion, answerPermission y rejectQuestion en un único objeto sendError, por lo que no necesitas escribir comprobaciones instanceof manualmente:

interface BeeuniSendError {
  kind: 'billing' | 'runtime-not-ready' | 'runtime-error';
  message: string;
  billing?: BillingError; // set when kind is 'billing'
  cause: unknown;
}
const s = useSession(projectId, sessionId);

if (s.sendError?.kind === 'billing') {
  const ui = formatBillingErrorForUI(s.sendError.billing);
}

Consulta Hooks de React para ver el resto de useSession.

¿Te ha resultado útil esta página?