Aller au contenu
BeeuniBeeuni
Français
Esc
↑↓naviguer↵ouvrir⌘Japerçu
Sur cette page

Sessions

Exécutez une session, recevez ses événements en continu et gérez les erreurs qu’elle peut générer.

Une session est une exécution d’agent, dans son propre bac à sable, sur sa propre branche git. beeuni.session(projectId, sessionId) renvoie l’objet de contrôle de toutes les opérations d’une session : la démarrer, envoyer des prompts, recevoir les événements en continu et consulter son état. Cette page présente cet objet, la négociation de disponibilité, le streaming et les erreurs typées qu’un appel au SDK peut générer.

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

s est l’objet de contrôle de toutes les opérations d’une session. L’ID de session, l’ID du bac à sable et le nom de branche ont la même valeur. Voir Sessions pour le concept.

Cycle de vie d’une session

Méthode Encapsule Fonction
s.get(opts?) GET /projects/:pid/sessions/:sid Lit les détails de la session
s.update(input) PATCH …/sessions/:sid Renomme la session, remplace ses étiquettes ou fusionne les clés de métadonnées
s.start(waitMs?) POST …/sessions/:sid/start Provisionne et démarre l’environnement d’exécution
s.restart() POST …/sessions/:sid/restart Redémarre l’environnement d’exécution ; conserve le même bac à sable
s.configState() GET …/sessions/:sid/config Lit l’état d’actualisation de la configuration (stale) et l’état de publication de la configuration (release)
s.reloadConfig(input?) POST …/sessions/:sid/reload Recompile la configuration de l’agent et remplace l’environnement d’exécution après validation
s.reloadConfigStream(input, onEvent) POST …/sessions/:sid/reload-stream Effectue le même rechargement et émet les phases de progression confirmées par le serveur
s.stop() POST …/sessions/:sid/stop Arrête l’environnement d’exécution ; la session est conservée
s.delete() DELETE …/sessions/:sid Supprime la session
s.setSharing(intent) PUT …/sharing Définit le partage et la visibilité
s.participants() GET …/sessions/:sid/participants Lit qui peut ouvrir la session et qui a envoyé chaque prompt
s.watch() GET …/sessions/:sid/watch Lit si vous recevez les notifications de cette session. Nécessite notification_center ; 403 feature_disabled lorsque cette option est désactivée
s.setWatch(watching) PUT …/sessions/:sid/watch Active (true) ou coupe (false) les notifications de la session pour vous. Nécessite notification_center ; 403 feature_disabled lorsque cette option est désactivée
s.presence({ tab_id, active, alerts? }, { keepalive? }) PUT …/sessions/:sid/presence Signale un onglet ouvert. alerts: true indique que l’onglet affiche ses propres notifications, ce qui bloque la notification push sur le téléphone. Lorsque notification_center est désactivé, tout onglet présent la bloque. keepalive permet à la requête de se poursuivre après la fermeture d’une page ; activé par défaut pour un signalement d’absence
s.cost() GET /usage/session-costs/:sid Lit le coût finalisé du LLM et du calcul sans démarrer l’environnement d’exécution
s.scope() GET …/sessions/:sid/scope Lit les restrictions de secrets enregistrées et les liaisons de connexion matérialisées
s.rescope(input) PUT …/sessions/:sid/scope Remplace les champs de portée fournis pour le prochain prompt ou appel d’outil
s.commit(input?) — Crée un commit du travail de l’agent
s.providerSecretPool.get(providerId) GET …/provider-secret-pools/:providerId Lit les ID des secrets de compte sélectionnés
s.providerSecretPool.list() GET …/provider-secret-pools Liste les pools configurés, y compris les pools vides, et indique can_edit
s.providerSecretPool.set(providerId, secretIds) PUT …/provider-secret-pools/:providerId Remplace la sélection ; null rétablit la valeur par défaut du projet

Étiquettes et métadonnées

Chaque session possède labels: string[] et un objet metadata. Utilisez-les pour classer les sessions et décider comment votre application les affiche.

  • Les étiquettes sont libres : les espaces en début et en fin sont supprimés, chacune compte de 1 à 64 caractères, avec un maximum de 20 par session et suppression des doublons. La correspondance est exacte et sensible à la casse.
  • Les métadonnées sont un objet JSON libre pour vos propres clés, limité à 16 384 caractères de JSON par écriture. Le serveur y stocke aussi ses propres clés (custom_name, source, …) ; une requête qui nomme une clé gérée par le serveur est refusée avec 400. Préfixez vos clés (app_…) pour éviter les conflits.
// 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
}

Dans React, passez labels à useProjectSessions(projectId, { labels }) ; chaque ensemble d’étiquettes dispose de sa propre entrée de cache. Les agents définissent les mêmes champs depuis la CLI (beeuni sessions new --label … --meta k=v, beeuni sessions update --label …) et depuis le serveur MCP hébergé (start_session labels / metadata, list_sessions labels). Une version exécutable est disponible dans packages/sdk/examples/12-session-labels.ts.

Lorsque pooled_provider_secrets et llm_gateway sont activés, une session peut sélectionner plusieurs secrets de compte disponibles pour un même fournisseur. Créez une ressource à portée de projet via beeuni.accounts.secretResources.create(accountId, { project_id: projectId, ...input }). Par défaut, chaque membre du projet peut l’utiliser. Restreignez-la avec .setAccess(accountId, secretId, 'members', userIds). Les lectures renvoient les métadonnées et les autorisations, jamais la valeur du secret. Après un 429 du fournisseur avant le début de la sortie, la passerelle essaie une autre clé sélectionnée.

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

Utilisez la méthode en streaming lorsque l’appelant affiche la progression du rechargement :

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

Les phases sont checking-session, refreshing-workspace, compiling-config, applying-config et confirming-config. Le serveur omet refreshing-workspace lorsque refresh_repo vaut false. La phase applying-config inclut le remplacement de l’environnement d’exécution validé par le démon.

s.configState() renvoie release lorsque l’API prend en charge les publications de configuration. Son type est SessionConfigRelease. Les résultats du rechargement contiennent le même objet.

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 possède trois états. null signifie que Beeuni n’a pas pu déterminer l’état. Testez stale === true, jamais !stale. Une API plus ancienne omet release. Voir Convergence de la configuration.

s.participants() renvoie { participants, total, multi_user }. participants liste les personnes qui peuvent actuellement ouvrir la session, le propriétaire en premier, avec un maximum de 20, chacune { user_id, name, email, avatar_url, is_viewer } ; total est le nombre total. multi_user vaut true lorsque deux personnes ou plus peuvent ouvrir la session. Dans React, useSessionParticipants(projectId, sessionId) lit les mêmes données. L’auteur de chaque message est indiqué par s.messageAuthors().

Trois autres méthodes de lecture complètent cet objet de contrôle :

  • s.previews() — ports d’aperçu potentiels exposés par le runtime.
  • s.publicShares.list() / .create(input) / .revoke(shareId) — liens de partage publics.
  • s.reminders.list() / .create(input) / .update(reminderId, { enabled }) / .remove(reminderId) — prompts planifiés pour cette session. Voir Rappels. .create({ transcript: true }) crée un lien en lecture seule vers la conversation (un seul lien actif par session ; un deuxième appel renvoie le même lien). Consultez-le anonymement avec getPublicSessionShareMessages(token) ; findActiveTranscriptShare(shares) sélectionne le lien actif dans .list().
  • s.audit(limit?) — journal d’audit des actions de l’agent pour la session.
  • s.attachments.upload(file, options?) — enregistrer un fichier privé avant le démarrage. Maximum 50 MiB. Renvoie une URL stable pour une partie de prompt de type fichier.
  • s.attachments.read(attachmentId, signal?) — lire les octets enregistrés sous forme de Blob sans démarrer le bac à sable.
  • s.transcript(options?) — une transcription compacte côté serveur (texte et appels d’outils, sans les entrées ni les sorties des outils). Fonctionne avec un jeton de session limité au projet.
  • s.transcriptSync(options?) — la transcription que le serveur enregistre à la fin de chaque tour, sous forme d’enveloppes de messages runtime. Chaque partie est conservée à l’identique — texte, raisonnement et chaque appel d’outil avec son entrée, sa sortie, son erreur et ses métadonnées — à l’exception des octets des pièces jointes. Répond lorsque le bac à sable est arrêté ou encore en cours de réveil, sans le démarrer. Passez limit (500 au maximum) pour obtenir la fenêtre la plus récente, et le next_cursor d’une fenêtre comme before pour lire la fenêtre précédente. Une fenêtre est également limitée à 1 000 000 de caractères JSON : elle conserve ses messages les plus récents, et next_cursor désigne alors le plus ancien message conservé. Passez child (l’identifiant ses_… d’un sous-agent, la session ouverte par une ligne de sous-agent) pour lire la transcription enregistrée de ce sous-agent ; chaque sous-agent lancé par la conversation est enregistré. source vaut mirror ; complete n’est vrai que lorsque la fenêtre contient tous les messages enregistrés et que la copie enregistrée remonte au premier message de la session. Une session dont le runtime a été lu intégralement et ne contenait aucun message renvoie available: true, complete: true, total: 0 et aucun message. Un appel d’outil enregistré avant le 2026-09-28, lorsque l’historique enregistré omettait les entrées et sorties des outils, est fourni avec l’entrée attestée par son titre ou ses métadonnées, et, pour une commande, avec sa sortie également.
  • s.voiceTranscript(options?) — transcription de l’appel vocal en cours de cette session (tours parlés et appels d’outils des workers ask_beeuni/run_command). Renvoie une liste vide lorsque la session n’a pas d’appel en cours, et non une erreur 404.

Fichiers stockés par une session

findSessionAttachments(messages) répertorie tous les fichiers stockés auxquels une transcription fait référence, dans l’ordre et une seule fois chacun : un fichier envoyé par un utilisateur, une référence <file> dans un prompt, ou la copie que l’historique enregistré conserve d’un fichier présenté par l’agent. Il lit indifféremment une transcription en direct ou enregistrée, et ne lève jamais d’exception si elle est mal formée.

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)
}

Chaque résultat est un { url, attachment_id, filename, mime, message_id, role }. role vaut user pour un fichier envoyé et assistant pour un fichier présenté par l’agent ; mime vaut null pour ce dernier, et les octets portent leur propre type.

Auteur de chaque message

Chaque message a un auteur. Le serveur le détermine à partir des identifiants qui ont envoyé le prompt : un membre du projet ou l’agent d’une autre session. La transcription du runtime ne le contient pas.

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 est indexé par identifiant de message du runtime. initial_author est la session parente pour le premier message d’une session créée. Il vaut null lorsque la session n’avait pas de premier prompt. Chaque auteur est un SessionMessageAuthor :

kind Champs
'member' user_id, name, email (string | null)
'session' session_id, name (le titre de la session), agent (l’agent qu’elle exécute, s’il est nommé)

Modèle ayant répondu à un tour

Un message de transcription indique le modèle demandé pour son tour. Lorsque la chaîne de repli du projet répond avec un autre modèle, seul l’enregistrement de la requête dans la passerelle contient le modèle qui a répondu et ce que Beeuni a 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 vaut null avant la première réponse. billed_cost est le montant en USD débité par Beeuni pour les appels aux modèles de la session. turns est indexé par l’identifiant de message du runtime du prompt qui a lancé chaque tour. served_models répertorie les modèles qui ont répondu pendant ce tour, par nombre de requêtes décroissant. Dans React, useSessionModelUsage(projectId, sessionId) lit le même enregistrement ; appelez son refetch() à la fin d’un tour.

Dans React, useSessionMessageAuthors(projectId, sessionId, messageCount) lit les mêmes données. Passez le nombre de messages de la transcription. Un nouveau message est le seul événement qui ajoute un auteur ; la requête récupère donc à nouveau les données lorsque ce nombre change.

La disponibilité passe par une négociation

Avant d’envoyer un prompt, appelez ensureReady(). Il provisionne le bac à sable si nécessaire, attend le démarrage du runtime et renvoie le runtime résolu.

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

Lors d’un démarrage à froid, ensureReady() peut lever RUNTIME_UNAVAILABLE. Voir Réessayer lors d’un démarrage à froid pour comprendre ce que cela signifie et comment réessayer.

s.send() et s.abort() appellent ensureReady() pour vous.

Initialiser un ancrage de session du runtime autorisé par le serveur

Un hôte React rendu côté serveur peut fournir l’ancrage de session du runtime déjà persisté pour la même session Beeuni :

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

La valeur initiale hydrate uniquement le contenu de transcription en cache pendant l’exécution de /start. Elle ne remplace pas l’identité du runtime. L’ancrage renvoyé par /start fait autorité et remplace une valeur initiale obsolète.

opencode_session_id est l’ancien nom, désormais obsolète, de runtime_session_id. Les deux portent la même valeur ; ne lisez l’ancien nom qu’en dernier recours, comme ci-dessus.

N’acceptez pas cette valeur depuis un sélecteur de locataire non fiable. Ne créez pas de session du runtime dans l’hôte. Beeuni crée et persiste la session racine. Les caches de requêtes du runtime et les contrôleurs de transcription sont limités au runtime du bac à sable ; des identifiants de session du runtime identiques dans des bacs à sable différents ne partagent donc pas d’entrées de cache.

Les nouvelles tentatives de récupération des messages en attente conservent l’URL de leur bac à sable d’origine lorsque vous changez de session. Une réponse de message 404 ou 410 arrête les nouvelles tentatives automatiques sans effacer la transcription. Une réconciliation explicite peut retenter la lecture et rétablir la synchronisation normale après une réponse réussie.

Envoyer 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() place le prompt dans la boîte de réception durable des prompts de la session (POST .../prompts), le chemin utilisé par l’éditeur web et la CLI. Il se résout lorsque le prompt est stocké, avec la ligne de la boîte de réception (prompt_id, state, message_id), et non à la fin du tour. Lisez la réponse dans s.stream() ou dans la transcription.

Pour les sessions REST runtime, le premier send() sur un handle lit le modèle et l’agent persistés dans la session Beeuni. Cela empêche une session runtime héritée d’un instantané de réutiliser les valeurs par défaut obsolètes de cet instantané.

L’ordre de priorité des choix du prompt est :

  1. L’appel send().
  2. La valeur setModel() ou setAgent() du handle.
  3. La valeur par défaut persistée de la session Beeuni.

setModel choisit uniquement ce que le prochain send local demande — cette valeur ne quitte jamais le handle. Pour persister un nouveau modèle côté serveur pour une session en cours, utilisez changeModel :

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

Le changement prend effet en redémarrant le runtime, ce qui met fin à tout tour en cours. applied_live vaut true lorsqu’une session en cours l’a appliqué immédiatement, et false lorsqu’il s’applique au prochain démarrage. Seuls le propriétaire de la session ou un appelant disposant des autorisations de gestionnaire de projet peuvent changer le modèle ; tous les autres reçoivent 403.

send() résout le runtime, puis lui envoie le prompt. abort() arrête l’exécution en cours sans supprimer la session.

Périmètre et coût de la session

Lisez la restriction des secrets stockée et les liaisons de connexion matérialisées. secrets_allowlist: null signifie que l’autorisation de secrets de l’agent s’applique :

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

connector_bindings est la table RÉSOLUE : elle a donc le même aspect pour une session qui a remplacé ses connecteurs et pour une session qui hérite des valeurs par défaut du projet. Lisez connector_bindings_configured pour les distinguer avant d’afficher la portée ou de la renvoyer.

Remplacez un ou les deux champs de portée :

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

Chaque champ fourni remplace intégralement sa valeur précédente. Omettez un champ pour le laisser inchangé. Les changements de connexion s’appliquent au prochain appel d’outil. La suppression d’un secret empêche sa transmission future, mais ne peut pas retirer une valeur déjà divulguée du contexte du modèle ou d’un processus existant.

Les deux axes disposent d’un moyen explicite de revenir à la valeur par défaut. Ce n’est pas la même chose qu’une valeur vide :

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

secrets: [] et connector_bindings: {} donnent l’instruction inverse : explicitement « aucun secret du projet » et « aucun connecteur », y compris ceux définis par défaut dans le projet. Une session qui envoie {} alors qu’elle voulait null refuse par défaut tout alias qu’elle n’a pas nommé.

Lisez le relevé unifié des coûts :

const cost = await s.cost();

Le relevé regroupe le coût LLM finalisé, le coût facturé du calcul dans le bac à sable, l’utilisation des modèles, les totaux de tokens, la durée de calcul et les écritures comptables. s.cost() n’appelle pas ensureReady().

État de l’environnement d’exécution et aperçus

Méthode Renvoie Utilisation
s.health(init?) { status, ok, health, body } Vérifier si l’environnement d’exécution est actif
s.previewUrl(port, path?) string Obtenir une URL de proxy pour un port exposé par l’agent
s.proxyUrl(url?) string | undefined Réécrire une URL localhost affichée par l’agent
const { ok, health } = await s.health();
const url = s.previewUrl(3000, '/docs');

s.health() ne lève jamais d’exception. Appelez-le à tout moment, même avant que la session dispose d’un environnement d’exécution. s.previewUrl() et s.proxyUrl() nécessitent un environnement d’exécution résolu : appelez s.ensureReady() d’abord, sinon ils lèvent SessionNotReadyError. Voir Erreurs de disponibilité de session.

Fonctionnalités prises en charge par l’environnement d’exécution

health.capabilities liste les fonctionnalités de session fournies par l’environnement d’exécution. Lisez-en une avec runtimeSupports(capabilities, capability) et masquez la commande lorsque cet appel renvoie false. Une session runtime fournit les dix ; une session pi fournit uniquement session.subagents et répond aux autres par 501 feature_not_supported (404 pour le document de configuration). Harnais présente la matrice complète pour runtime et pi.

Capacité Fonctionnalité
session.rewind Revenir à un message, puis le restaurer
session.compact Résumer la conversation à la demande
session.commands Commandes slash du projet
session.fork Créer une branche de session à partir d’un message
session.subagents Sessions enfants de sous-agents
session.mcp Serveurs MCP auxquels l’environnement d’exécution se connecte lui-même
session.todo Liste de tâches de l’environnement d’exécution
session.shell Commande shell exécutée comme un tour
session.attach Connecter le propre client de terminal du harnais
session.config Document de configuration de l’environnement d’exécution (/global/config) ; dans tous les cas, les agents et les modèles proviennent du projet
import { runtimeSupports } from '@beeuni/sdk';

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

runtimeSupports renvoie true avant l’arrivée d’une réponse de santé, ainsi que pour un environnement d’exécution qui ne liste aucune capacité session.* : un démon créé avant l’existence des capacités exécute runtime, qui fournit toutes les fonctionnalités. Dans React, utilisez useRuntimeSupports.

La réponse start contient aussi la liste : SessionStartResult.capabilities est défini avec stage: 'ready'. useSession l’enregistre ; le contrôle des capacités est donc correct avant même la réponse de la première sonde de santé.

Termes d’état

Chaque hôte utilise le même vocabulaire : un état porte donc le même nom sur tous les écrans.

Export Utilisation
sessionListStatus(session, reviewCount?) Une session dans une liste : needs-you, starting, running, done, stopped, failed, legacy
SESSION_LIST_STATUS[status] { label, tone, description } pour cet état
sessionConnectionLabel(connection) Un libellé court pour l’ordinateur de la session, ou null pour unknown et live
SESSION_NOTICE La phrase de la zone de saisie lorsque l’ordinateur n’est pas prêt
turnRetryLabel(secondsLeft) Retrying in 5s, Retrying now ou Waiting to retry lorsqu’aucun compte à rebours n’est connu

Les tonalités sont actionable, live, progress, muted et danger. Le vert est réservé à live et actionable ; une session terminée est muted. Un état envoyé par une API plus récente et inconnu de cette version s’affiche comme stopped, jamais comme failed.

Streaming

Utilisez s.stream() pour recevoir des événements en direct dans un script ou sur un serveur. Dans une application React, utilisez plutôt useSession : il gère tout le cycle de vie de la session pour vous.

s.stream() est le flux d’événements de compatibilité REST d’runtime. L’API Beeuni le relaie depuis le bac à sable. Il n’existe aucun point de terminaison WebSocket distinct. Le transport est fetch avec un corps de réponse en streaming, lu via ReadableStream et TextDecoderStream. Le SDK gère la reconnexion, les délais progressifs entre tentatives et la vérification des signaux de vie.

Diffusez les événements d’une session :

  1. Appelez d’abord ensureReady(). L’environnement d’exécution n’existe pas tant que le bac à sable n’a pas démarré.
  2. Ouvrez le flux avant d’envoyer un message, afin de ne pas manquer les premiers événements.
  3. Envoyez le message.
  4. Fermez le flux lorsque vous voyez 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');

Le flux transmet les événements par lots, espacés de 16 ms. Les événements message.part.delta consécutifs d’un lot, pour un même champ de partie, arrivent sous forme d’un seul événement. Son properties.delta contient leurs textes, concaténés dans l’ordre. Son id est l’identifiant du dernier événement. Son tableau coalesced liste les événements qu’il a remplacés. Ajoutez delta au champ de la partie, comme pour un delta unique.

Le streaming nécessite fetch avec un véritable corps ReadableStream et TextDecoderStream. Les navigateurs, Node 18 et versions ultérieures, Bun et Cloudflare Workers le prennent tous en charge.

Un hôte dont le fetch ne permet pas le streaming fournit son propre transport. Le SDK l’appelle une fois par connexion et gère lui-même la reconnexion, la reprise Last-Event-ID, les délais progressifs et les signaux de vie :

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 ne dispose pas de fetch en streaming par défaut. L’application mobile Beeuni utilise ce point d’intégration avec react-native-sse. createHttpSessionSyncController reste disponible pour un hôte qui a uniquement besoin d’un historique limité et de la synchronisation de l’état.

Le contrôleur charge d’abord les 10 messages les plus récents. loadOlder() suit le curseur du serveur. loadHttpSessionHistory() suit tous les curseurs pour les exports explicites.

Types d’événements

Chaque événement possède un type et un objet properties contenant ses données, par exemple event.properties.sessionID.

type Quand il se déclenche
message.updated / message.removed Un message a été modifié ou supprimé.
message.part.updated / message.part.removed Une partie (texte, appel d’outil, fichier) a été enrichie ou supprimée.
session.status L’état d’occupation de la session a changé.
session.idle Le tour est terminé.
session.error Le tour a échoué. L’événement contient l’erreur.
question.asked L’agent a demandé une saisie.
question.replied / question.rejected La réponse à une question est arrivée.

Transformez les messages et les parties bruts en contenu affichable avec classifyTurn. Consultez la Référence du SDK.

Réessayer lors d’un démarrage à froid

ensureReady() interroge le point de terminaison /start de la session — chaque appel effectue une interrogation longue pouvant durer jusqu’à 30 s — jusqu’à ce que l’environnement d’exécution atteigne une étape terminale ready/failed/stopped ou que son délai (readyTimeoutMs, environ 180 s par défaut) expire. Sur une session déjà démarrée, la première interrogation renvoie immédiatement ready. Lors d’un démarrage à froid, les interrogations continuent tant que le bac à sable signale retriable: true : un démarrage lent prend donc simplement plus de temps au lieu de lever une exception. Une exception ApiError avec code: 'RUNTIME_UNAVAILABLE' if the runtime is still not ready n’est levée que lorsque le délai expire.

ensureReady() est idempotent : les appels simultanés pour la même session partagent une seule requête /start au lieu d’en envoyer plusieurs. La fonction utilitaire retryUntilReady ci-dessous est désormais facultative — ensureReady() réessaie déjà en interne — mais reste utile si vous souhaitez un délai total plus long que le readyTimeoutMs par défaut.

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));
    }
  }
}

Consultez les Classes d’erreurs pour connaître la structure complète de ApiError. Dans React, useSession réessaie /start pour vous : vous n’avez donc pas besoin de ce modèle.

Ce que vous indique /start

Chaque réponse /start décrit cet appel, et non l’historique cumulé de la ligne. Quatre champs portent ces informations.

Champ Signification
observed_at Une horloge unique pour toute la réponse.
action Ce que le serveur a fait : inspected, checked_provider, resumed, provisioned, restored, reconciled, awaited_wake, cooling_down.
observation Ce que le serveur a vérifié. known: false signifie non vérifié lors de cet appel — jamais « vérifié sans rien trouver ».
boot phase (provisioning / resuming / booting / ready / parked / failed), since et actively_starting.

boot.actively_starting répond à la question « une opération du fournisseur est-elle en cours pour cette session en ce moment ? ». Une charge utile starting avec actively_starting: false signifie que le serveur attend la fin d’un délai avant nouvelle tentative, et non qu’une machine démarre.

{
  "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 démarrage qui échoue est réessayé pour vous

Un démarrage qui échoue enregistre un délai d’attente, et non un verdict définitif. Le prochain /start après ce délai retente automatiquement le réveil. Le délai augmente avec les échecs consécutifs (2 min, 5 min, 10 min). Après cinq échecs consécutifs, /start répond stage: "failed" avec le nombre de tentatives dans failure.message ; ce verdict expire 30 minutes après le dernier échec, et POST …/restart l’efface immédiatement.

retriable est calculé à chaque appel. Un état pour lequel le serveur peut encore réessayer ne contient jamais retriable: false.

failure.evidence indique la vérification qui a produit le résultat négatif, quand elle a eu lieu et quand le serveur réessaiera. Chaque échec /start le contient.

Pièces jointes aux prompts

Téléversez les fichiers locaux lorsque les utilisateurs les ajoutent à un éditeur de message. Les téléversements privés appartiennent au projet et ne nécessitent pas de session en cours d’exécution.

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 résout une fois tous les téléversements prêts, dans l’ordre ids. Il est rejeté lorsqu’un téléversement échoue, est interrompu ou supprimé, ou lorsque signal est interrompu. submit(ids) transmet les entrées à cet envoi : elles quittent attachments, ne sont plus comptabilisées dans les limites et continuent à être téléversées après le démontage de l’éditeur. Appelez forget(ids) après la réussite du POST, ou reclaim(ids) pour les remettre dans l’éditeur après un échec d’envoi. La partie fichier résolue contient attachment_id, filename et mime. Elle ne contient ni URL ni octets du fichier. Le serveur associe et matérialise le téléversement. Chaque fichier parvient à l’ordinateur de la session, y compris les images et les PDF : avant l’exécution du prompt, le serveur l’écrit dans /workspace/uploads/.beeuni-inbox/ et le prompt le référence par son chemin. Aucun octet du fichier n’est transmis dans le prompt lui-même. Lorsque l’enregistrement de l’historique est activé, le serveur conserve aussi une copie pour la transcription. Un échec de copie n’empêche jamais un fichier d’atteindre l’ordinateur. Un fichier que le serveur ne peut pas écrire bloque le prompt, qui réessaie. Utilisez ces parties sur les routes de création de session de la plateforme, de démarrage d’une session déjà démarrée ou de boîte de réception des prompts. Ne transmettez pas directement les identifiants privés à sendParts de l’environnement d’exécution runtime. Les anciennes parties fichier de la plateforme avec url fonctionnent toujours.

Chaque élément possède un id stable, la référence file d’origine, receivedBytes et un état : pending, uploading, processing, ready, error ou aborted. Les octets envoyés déterminent la progression. Le contrôleur publie un instantané de progression uniquement lors d’un changement d’un point de pourcentage entier, au maximum dix fois par seconde et par téléversement. processing peut rester à 100 % jusqu’à ce que le serveur termine le téléversement. Les limites sont de 50 MiB par fichier, 100 MiB par message et 20 fichiers. Les fichiers ne doivent pas être vides. Deux fichiers sont téléversés simultanément par défaut.

retry(id) reprend le même identifiant. Après attachment_size_mismatch ou attachment_failed, le serveur ne conserve aucun identifiant utilisable : retry(id) téléverse donc de nouveau le File en tant que nouvelle pièce jointe. Un téléversement expiré ne peut pas être réessayé : retry(id) lève une exception, et l’erreur de l’élément contient le code attachment_expired. abort(id) annule les opérations inachevées. remove(id) supprime l’entrée, interrompt son téléversement et se résout immédiatement. Le DELETE côté serveur est effectué au mieux et n’est jamais rejeté ; un téléversement qu’il ne peut pas supprimer expire au bout de 24 heures. forget(ids) libère les entrées sans supprimer les objets côté serveur. Le hook interrompt les opérations listées lors du démontage ou d’un changement de projet et supprime ses téléversements au mieux : aucun envoi ne les détient, et les brouillons ne conservent aucun identifiant. Les téléversements transmis à un envoi continuent.

Les sélections restent uniquement en mémoire. Ne conservez jamais d’objets File, d’URL de blob, d’URL signées ou d’identifiants de téléversement dans un brouillon. Les téléversements abandonnés expirent au bout de 24 heures.

Les hôtes sans framework utilisent beeuni.project(projectId).attachments.createController() avec les mêmes méthodes. Abonnez-vous via subscribe(listener) et lisez getSnapshot(). Appelez dispose() lorsque l’éditeur se ferme. Les appelants directs peuvent utiliser attachments.upload(file, { signal, onProgress, onUpload, resume }) and attachments.delete(attachmentId). Conservez les métadonnées onUpload pour reprendre manuellement avec le même identifiant.

Le serveur sélectionne le transport dans le champ upload de l’identifiant :

  • kind: 'direct' est le mode par défaut. Le SDK envoie le fichier entier dans un seul PUT à upload.url avec upload.headers et sans en-tête Authorization. Avec XMLHttpRequest (navigateurs, React Native), la progression indique les octets envoyés. Avec fetch, la progression indique 0, puis la taille totale. Une URL expirée, ou que Storage refuse avec 400, 401 ou 403, est signée à nouveau une seule fois pour le même attachment_id. Un 409 de Storage signifie qu’une tentative précédente a stocké le fichier.
  • kind: 'chunked' envoie des PUT authentifiés et séquentiels de upload.chunk_size octets. Le SDK accepte toute valeur positive de chunk_size. Seul un déploiement dont la périphérie rejette les corps de requête volumineux sélectionne ce mode.

La finalisation vérifie la taille stockée et le SHA-256. Si la finalisation répond 409 attachment_not_uploaded, onUpload signale le handle direct avec received_bytes: 0, de sorte que retry(id) ou une reprise renvoie le fichier. L’initialisation, l’envoi et la finalisation réessaient après des délais d’attente dépassés, des erreurs réseau, des réponses 429 et 5xx avec un délai exponentiel à variation aléatoire. Le délai alloué est de 60 secondes à partir du premier échec : un envoi long qui échoue tardivement est donc tout de même réessayé. La finalisation réessaie aussi attachment_processing, avec un délai alloué de cinq minutes. L’initialisation ne réessaie jamais après une réponse 402 (un BillingError : le compte ne peut pas fonctionner) ou 429 attachment_budget_exceeded (40 envois inachevés ou 500 MiB d’envois non transmis pour l’utilisateur ; les envois inutilisés expirent sous 24 heures). Le serveur répond à une finalisation ou la refuse sous 105 secondes. Chaque requête de finalisation autorise 120 secondes. Les annulations par l’appelant ne sont jamais réessayées.

La référence d’une pièce jointe envoyée est libérée 1 heure après la remise de son prompt, et lors de la suppression de sa session ou de son projet. Le prochain passage de maintenance supprime alors le fichier, et son attachment_id ne peut plus être envoyé.

Fichiers

s.files lit et écrit dans le sandbox de la session : list, read, readBlob, status, findFiles, findText, upload, create, copy, remove, mkdir, rename. Chaque appel résout d’abord l’environnement d’exécution et cible toujours le sandbox propre à cette session. Consultez la référence du SDK pour la liste complète des méthodes.

Verbes de session

Le handle lit l’environnement d’exécution et lui répond pour vous. Chaque verbe provisionne d’abord la session et cible le sandbox propre à cette session, quel que soit le harnais qui l’exécute.

// 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, le client de compatibilité REST brut, est obsolète. Utilisez les verbes ci-dessus avec send, abort, rewind et stream.

Préchauffer une session de projet

Appelez ensureWarm() lorsqu’une page d’accueil de projet a besoin d’un environnement d’exécution prêt avant le premier 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() crée ou renvoie une session inutilisée pour l’utilisateur actuel. Il s’agit de la même création qu’exécute sessions.create(), avec les paramètres par défaut du projet : même contrôle de facturation, mêmes exigences de connecteurs. La seule différence est metadata.warm, qui masque la session dans sessions.list() jusqu’à l’arrivée de son premier prompt.

Considérez cela comme une opération spéculative. Un 409 WARM_SESSION_UNAVAILABLE signifie que le projet ne peut pas être préchauffé pour le moment — passez à sessions.create(), qui indique la véritable raison.

La session préchauffée utilise l’agent DEFAULT et le sandbox du projet. Si l’utilisateur en choisit un autre, abandonnez-la et appelez sessions.create() : une session préchauffée inutilisée est masquée et supprimée automatiquement.

Gestion des erreurs

Chaque appel via createBeeuni est rejeté avec une sous-classe typée de Error, jamais avec un objet simple. Interceptez l’erreur, vérifiez instanceof et traitez les cas selon .status ou .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;
  }
}

Classes d’erreur

Classe Hérite de Quand elle est levée Champs clés
ApiError Error Par défaut pour toute requête échouée : statut incorrect, erreur réseau, délai d’attente dépassé ou annulation status, code, detail, response, url, endpoint, timeout
AuthError ApiError getToken a renvoyé null. Beeuni n’a jamais envoyé la requête code vaut toujours 'NO_SESSION'
BillingError ApiError HTTP 402. La seule classe d’erreur de facturation status (402), code, detail.message
RequestTooLargeError ApiError HTTP 431. Généralement trop de fichiers dans une requête detail.suggestion
SessionNotReadyError Error Un accesseur de l’environnement d’exécution a été exécuté avant ensureReady() name vaut 'SessionNotReadyError'

ApiError.name vaut 'ApiError' par défaut. Deux cas remplacent cette valeur :

  • name: 'AbortError', code: 'ABORTED' — la requête a été annulée, par exemple lors d’une navigation. Ce n’est pas un échec. Ignorez-la.
  • code: 'TIMEOUT' — le délai d’attente propre à la requête a expiré. url, endpoint et timeout indiquent ce qui a dépassé le délai.

Pour tout autre échec, status contient le code de statut HTTP. code provient de error_code du backend ou, à défaut, du statut sous forme de chaîne. message est une propriété propre énumérable de ApiError : elle est donc conservée par JSON.stringify et par la décomposition d’objet.

Beeuni renvoie une fois une requête qui reçoit 401 avec un nouveau jeton obtenu via getToken(), lorsque ce jeton diffère de celui envoyé. Une requête dont le corps est un flux à usage unique n’est pas renvoyée.

Beeuni réessaie certaines requêtes avant que votre code ne voie une erreur. Si une requête GET ou HEAD renvoie 502, 503 ou 504, Beeuni la réessaie jusqu’à 2 fois, après un délai de 250 ms puis de 500 ms. Une erreur de transport transitoire sur un GET ou un HEAD — une erreur réseau, pas un code de statut — est réessayée de la même manière. Une nouvelle tentative réussie n’atteint jamais onError. Beeuni ne réessaie jamais les requêtes POST, PUT, PATCH ou DELETE, ni une réponse 500.

Beeuni lève AuthError côté client, avant d’envoyer une requête, lorsque getToken() renvoie null. AuthError hérite de ApiError, donc err instanceof ApiError correspond toujours. Vérifiez err instanceof AuthError ou err.code === 'NO_SESSION' pour traiter le cas « non connecté » séparément d’un échec du backend.

Beeuni lève BillingError pour chaque réponse HTTP 402 : crédits épuisés, limite du forfait dépassée ou autre contrôle de facturation. detail.message contient la raison fournie par le backend, et code contient le code machine du backend (par exemple app_account_unfunded). BillingError hérite de ApiError : vérifiez donc BillingError en premier lorsque vous gérez les deux.

Beeuni lève RequestTooLargeError pour HTTP 431. Cela signifie généralement que la requête contenait trop de fichiers. detail.suggestion contient une indication prête à être affichée à l’utilisateur.

Erreurs de disponibilité de la session

Deux erreurs signifient que le sandbox de la session n’est pas encore prêt. Gérez chacune différemment.

SessionNotReadyError lève une erreur de manière synchrone lorsque vous appelez un accesseur du runtime — session.previewUrl() ou session.proxyUrl() — avant que ce handle de session ait résolu son sandbox. Un handle de session ne résout que son propre sandbox ; il ne se rabat jamais sur celui d’une autre session.

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();
  }
}

Appelez d’abord await session.ensureReady(), ou appelez send(), qui prépare la session en interne. session.health() est le seul accesseur qui ne lève jamais cette erreur ; vous pouvez donc l’interroger périodiquement avant le démarrage de la session.

RUNTIME_UNAVAILABLE est la deuxième erreur — elle signifie que ensureReady() a lui-même dépassé le délai d’attente d’un démarrage à froid. Consultez Réessayer lors d’un démarrage à froid pour le schéma complet. Dans React, useSession réessaie pour vous et expose cette erreur via la valeur phase au lieu de la lever.

Fonctions utilitaires

Fonction utilitaire Signature Rôle
parseBillingError(error) (error) => Error Encapsule une réponse 402 dans un BillingError. Renvoie les autres erreurs telles quelles
isBillingError(error) (error) => boolean Renvoie error instanceof BillingError
formatBillingErrorForUI(error) (error) => BillingErrorUI | null Renvoie null pour les erreurs sans rapport avec la facturation. Sinon, renvoie { alertTitle, alertSubtitle } pour une fenêtre modale de mise à niveau
isRuntimeNotReadyResponse(error) (error) => boolean Vrai pour le 503 du démon du sandbox tant que le runtime de la session ne peut pas encore accepter de requête. La requête n’a pas été transmise ; il est donc sûr de réessayer. Même résultat sur runtime et pi
isSandboxNotReadyError(error) (error) => boolean Vrai pour le cas ci-dessus et pour un sandbox arrêté, en cours de démarrage ou en veille. Affichez-le comme « en cours de réveil » et continuez à l’interroger périodiquement
isRuntimeStartingError(error) (error) => boolean Vrai pour les deux cas ci-dessus et pour une URL de runtime qui n’est pas encore fixée. Une limite d’erreur réessaie dans ce cas au lieu d’afficher un plantage
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);
}

Dans @beeuni/sdk/react

@beeuni/sdk/react réexporte BillingError, RequestTooLargeError, parseBillingError, isBillingError et formatBillingErrorForUI. Il ne réexporte pas ApiError ni AuthError — importez-les depuis @beeuni/sdk.

useSession classe chaque échec de send, answerQuestion, answerPermission et rejectQuestion dans un objet sendError, ce qui vous évite d’écrire manuellement des vérifications instanceof :

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);
}

Consultez Hooks React pour le reste de useSession.

Cette page vous a-t-elle été utile ?