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 con400. 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 congetPublicSessionShareMessages(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 unBlobsin 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. Pasalimit(como máximo 500) para obtener la ventana más reciente, y elnext_cursorde una ventana comobeforepara leer la anterior. Una ventana también se limita a 1,000,000 de caracteres de JSON: conserva sus mensajes más recientes, ynext_cursoridentifica entonces el más antiguo que conservó. Pasachild(el idses_…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.sourceesmirror;completees 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 conavailable: true,complete: true,total: 0y 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 workersask_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:
- La llamada a
send(). - El valor
setModel()osetAgent()del handle. - 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:
- Llama primero a
ensureReady(). El entorno de ejecución no existe hasta que se inicia el entorno aislado. - Abre el flujo antes de enviar un mensaje para no perderte los primeros eventos.
- Envía el mensaje.
- 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 soloPUTaupload.urlconupload.headersy sin encabezado Authorization. ConXMLHttpRequest(navegadores, React Native), el progreso indica los bytes enviados. Confetch, 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 mismoattachment_id. Un409de Storage significa que un intento anterior almacenó el archivo.kind: 'chunked'envía solicitudesPUTautenticadas y secuenciales deupload.chunk_sizebytes. El SDK acepta cualquierchunk_sizepositivo. 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,endpointytimeoutmuestran 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.