---
title: "Sitzungen"
description: "Führe eine Sitzung aus, streame ihre Ereignisse und behandle die Fehler, die sie auslösen kann."
---

Eine Sitzung ist ein einzelner Agentenlauf in einer eigenen Sandbox auf einem eigenen Git-Branch.
`beeuni.session(projectId, sessionId)` gibt das Handle für alles zurück, was eine
Sitzung tut: sie starten, Prompts senden, Ereignisse streamen und den Status lesen. Diese
Seite behandelt das Handle, den Bereitschafts-Handshake, das Streaming und die typisierten
Fehler, die ein SDK-Aufruf auslösen kann.

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

`s` ist das Handle für alles, was eine Sitzung tut. Sitzungs-ID, Sandbox-
ID und Branch-Name haben denselben Wert. Das Konzept wird unter
[Sitzungen](/docs/de/work/sessions) erläutert.

<a id="session-lifecycle"></a>

## Lebenszyklus einer Sitzung

| Methode | Kapselt | Funktion |
|---|---|---|
| `s.get(opts?)` | `GET /projects/:pid/sessions/:sid` | Liest Sitzungsdetails |
| `s.update(input)` | `PATCH …/sessions/:sid` | Benennt die Sitzung um, ersetzt ihre Labels oder führt Metadatenschlüssel zusammen |
| `s.start(waitMs?)` | `POST …/sessions/:sid/start` | Stellt die Laufzeitumgebung bereit und startet sie |
| `s.restart()` | `POST …/sessions/:sid/restart` | Startet die Laufzeitumgebung neu; behält dieselbe Sandbox bei |
| `s.configState()` | `GET …/sessions/:sid/config` | Liest die Aktualität der Konfiguration (`stale`) und den Freigabestatus der Konfiguration (`release`) |
| `s.reloadConfig(input?)` | `POST …/sessions/:sid/reload` | Kompiliert die Agentenkonfiguration neu und ersetzt die Laufzeitumgebung nach der Validierung |
| `s.reloadConfigStream(input, onEvent)` | `POST …/sessions/:sid/reload-stream` | Führt denselben Neuladevorgang aus und gibt serverbestätigte Fortschrittsphasen aus |
| `s.stop()` | `POST …/sessions/:sid/stop` | Stoppt die Laufzeitumgebung; die Sitzung bleibt bestehen |
| `s.delete()` | `DELETE …/sessions/:sid` | Löscht die Sitzung |
| `s.setSharing(intent)` | `PUT …/sharing` | Legt Freigabe und Sichtbarkeit fest |
| `s.participants()` | `GET …/sessions/:sid/participants` | Liest, wer die Sitzung öffnen kann und wer jeden Prompt gesendet hat |
| `s.watch()` | `GET …/sessions/:sid/watch` | Liest, ob du die [Benachrichtigungen](/docs/de/work/notifications) dieser Sitzung erhältst. Benötigt `notification_center`; `403` `feature_disabled`, solange es deaktiviert ist |
| `s.setWatch(watching)` | `PUT …/sessions/:sid/watch` | Aktiviert (`true`) oder deaktiviert (`false`) die Benachrichtigungen der Sitzung für dich. Benötigt `notification_center`; `403` `feature_disabled`, solange es deaktiviert ist |
| `s.presence({ tab_id, active, alerts? }, { keepalive? })` | `PUT …/sessions/:sid/presence` | Meldet einen geöffneten Tab. `alerts: true` gibt an, dass der Tab seine eigenen Benachrichtigungen anzeigt, sodass die Push-Benachrichtigung auf dem Smartphone zurückgehalten wird. Wenn `notification_center` deaktiviert ist, hält jeder vorhandene Tab sie zurück. `keepalive` lässt die Anfrage über das Schließen einer Seite hinaus bestehen; bei einer Abwesenheitsmeldung standardmäßig aktiviert |
| `s.cost()` | `GET /usage/session-costs/:sid` | Liest endgültige LLM- und Rechenkosten, ohne die Laufzeitumgebung zu starten |
| `s.scope()` | `GET …/sessions/:sid/scope` | Liest die gespeicherte Einschränkung von Secrets und materialisierte Verbindungsbindungen |
| `s.rescope(input)` | `PUT …/sessions/:sid/scope` | Ersetzt die angegebenen Geltungsbereichsfelder für den nächsten Prompt oder Tool-Aufruf |
| `s.commit(input?)` | — | Committet die Arbeit des Agenten |
| `s.providerSecretPool.get(providerId)` | `GET …/provider-secret-pools/:providerId` | Liest ausgewählte Account-Secret-IDs |
| `s.providerSecretPool.list()` | `GET …/provider-secret-pools` | Listet konfigurierte Pools einschließlich leerer Pools auf und meldet `can_edit` |
| `s.providerSecretPool.set(providerId, secretIds)` | `PUT …/provider-secret-pools/:providerId` | Ersetzt die Auswahl; `null` stellt den Projektstandard wieder her |

:::warning
  `s.delete()` löscht die Sitzung und ihre Laufzeitumgebung. Dies kann nicht rückgängig gemacht werden. Um eine Sitzung
  zu pausieren, ohne sie zu verlieren, rufe stattdessen `s.stop()` auf.
:::

<a id="labels-and-metadata"></a>

## Labels und Metadaten

Jede Sitzung enthält `labels: string[]` und ein `metadata`-Objekt. Verwende sie, um
Sitzungen zu klassifizieren und festzulegen, wie deine App sie darstellt.

- **Labels** sind frei definierbar: Jedes wird von führenden und nachfolgenden Leerzeichen bereinigt und umfasst 1–64 Zeichen; pro
  Sitzung sind höchstens 20 erlaubt, Duplikate werden entfernt. Der Abgleich ist exakt und berücksichtigt Groß- und Kleinschreibung.
- **Metadaten** sind ein frei definierbares JSON-Objekt für deine eigenen Schlüssel mit höchstens 16.384
  Zeichen JSON pro Schreibvorgang. Der Server speichert dort auch seine eigenen Schlüssel
  (`custom_name`, `source`, …); eine Anfrage, die einen serververwalteten Schlüssel angibt, wird
  mit `400` abgelehnt. Verwende ein Präfix für deine Schlüssel (`app_…`), um Konflikte zu vermeiden.

```ts
// 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
}
```

Übergib in React `labels` an `useProjectSessions(projectId, { labels })`; jeder
Label-Satz erhält einen eigenen Cache-Eintrag. Agenten setzen dieselben Felder über die CLI
(`beeuni sessions new --label … --meta k=v`, `beeuni sessions update --label …`)
und über den gehosteten MCP-Server (`start_session` `labels` / `metadata`,
`list_sessions` `labels`). Eine ausführbare Version ist `packages/sdk/examples/12-session-labels.ts`.

Wenn `pooled_provider_secrets` und `llm_gateway` aktiviert sind, kann eine Sitzung
mehrere verfügbare Account-Secrets für einen Anbieter auswählen. Erstelle über
`beeuni.accounts.secretResources.create(accountId, { project_id: projectId, ...input })` eine projektbezogene Ressource.
Standardmäßig kann jedes Projektmitglied sie verwenden. Schränke sie mit
`.setAccess(accountId, secretId, 'members', userIds)` ein. Lesevorgänge geben Metadaten und Berechtigungen zurück, niemals
den Secret-Wert. Nach einem `429` des Anbieters, bevor die Ausgabe beginnt, versucht das Gateway
einen anderen ausgewählten Schlüssel.

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

Verwende die Streaming-Methode, wenn der Aufrufer den Fortschritt des Neuladens anzeigt:

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

Die Phasen sind `checking-session`, `refreshing-workspace`, `compiling-config`,
`applying-config` und `confirming-config`. Der Server lässt
`refreshing-workspace` aus, wenn `refresh_repo` gleich `false` ist. Die
Phase `applying-config` umfasst den vom Daemon validierten Austausch der Laufzeitumgebung.

`s.configState()` gibt `release` zurück, wenn die API Konfigurationsfreigaben unterstützt.
Der Typ ist `SessionConfigRelease`. Die Ergebnisse des Neuladens enthalten dasselbe Objekt.

```ts
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` hat drei Zustände. `null` bedeutet, dass Beeuni es nicht feststellen konnte. Prüfe `stale === true`,
niemals `!stale`. Eine ältere API lässt `release` aus. Siehe
[Konfigurationskonvergenz](/docs/de/work/runtime#config-convergence).

`s.participants()` gibt `{ participants, total, multi_user }` zurück. `participants`
listet die Personen auf, die die Sitzung aktuell öffnen können: den Eigentümer zuerst, höchstens 20, jeweils
`{ user_id, name, email, avatar_url, is_viewer }`; `total` ist die Gesamtzahl.
`multi_user` ist `true`, wenn zwei oder mehr Personen die Sitzung öffnen können. In React
liest `useSessionParticipants(projectId, sessionId)` dieselben Daten. Wer
jede Nachricht geschrieben hat, steht in `s.messageAuthors()`.

Drei weitere Lesemethoden vervollständigen das Handle:

- `s.previews()` — mögliche Vorschau-Ports, die die Runtime bereitstellt.
- `s.publicShares.list()` / `.create(input)` / `.revoke(shareId)` — öffentliche Freigabelinks.
- `s.reminders.list()` / `.create(input)` / `.update(reminderId, { enabled })` / `.remove(reminderId)` — geplante Prompts für diese Sitzung. Siehe [Erinnerungen](/docs/de/connect/reminders).
  `.create({ transcript: true })` erstellt einen schreibgeschützten Link zur Unterhaltung (ein aktiver Link pro Sitzung; ein zweiter Aufruf gibt denselben Link zurück). Mit `getPublicSessionShareMessages(token)` kannst du sie anonym lesen; `findActiveTranscriptShare(shares)` wählt den aktiven Link aus `.list()`.
- `s.audit(limit?)` — das Audit-Protokoll der Agentenaktionen dieser Sitzung.
- `s.attachments.upload(file, options?)` — speichert eine private Datei vor dem Start. Maximal 50 MiB. Gibt eine stabile URL für einen Dateiteil eines Prompts zurück.
- `s.attachments.read(attachmentId, signal?)` — liest gespeicherte Bytes als `Blob`, ohne die Sandbox zu starten.
- `s.transcript(options?)` — ein kompaktes serverseitiges Transkript (Text und Tool-Aufrufe, keine Tool-Eingaben oder -Ausgaben). Dies funktioniert mit einem auf das Projekt beschränkten Sitzungstoken.
- `s.transcriptSync(options?)` — das Transkript, das der Server am Ende jedes Turns als runtime-Nachrichten-Envelopes speichert. Jeder Teil bleibt 1:1 erhalten — Text, Reasoning und jeder Tool-Aufruf mit Eingabe, Ausgabe, Fehler und Metadaten — mit Ausnahme der Anhangsbytes. Es antwortet, während die Sandbox gestoppt ist oder noch hochfährt, ohne sie zu starten. Übergib `limit` (höchstens 500) für das neueste Fenster und den Wert `next_cursor` eines Fensters als `before`, um das davorliegende zu lesen. Ein Fenster endet auch bei 1.000.000 JSON-Zeichen: Es behält seine neuesten Nachrichten, und `next_cursor` benennt dann die älteste beibehaltene Nachricht. Übergib `child` (die `ses_…`-ID eines Subagenten, also die Sitzung, die eine Subagenten-Zeile öffnet), um das eigene gespeicherte Transkript dieses Subagenten zu lesen; jeder von der Unterhaltung gestartete Subagent wird gespeichert. `source` ist `mirror`; `complete` ist nur dann true, wenn das Fenster alle gespeicherten Nachrichten enthält und die gespeicherte Kopie bis zur ersten Nachricht der Sitzung reicht. Eine Sitzung, deren Runtime vollständig gelesen wurde und keine Nachrichten enthielt, antwortet mit `available: true`, `complete: true`, `total: 0` und ohne Nachrichten. Ein vor dem 2026-09-28 gespeicherter Tool-Aufruf, als der gespeicherte Verlauf Tool-Eingaben und -Ausgaben noch verwarf, wird mit der Eingabe ausgeliefert, die sein Titel oder seine Metadaten belegen, und ein Befehl zusätzlich mit seiner Ausgabe.
- `s.voiceTranscript(options?)` — das Live-Sprachanruf-Transkript dieser Sitzung (gesprochene Turns plus `ask_beeuni`/`run_command`-Tool-Aufrufe der Worker). Gibt eine leere Liste zurück, wenn die Sitzung keinen aktiven Anruf hat, keinen 404.

<a id="files-a-session-stored"></a>

### Von einer Sitzung gespeicherte Dateien

`findSessionAttachments(messages)` listet jede gespeicherte Datei auf, auf die ein Transkript
verweist, in der Reihenfolge und jeweils einmal: einen Benutzer-Upload, eine `<file>`-Referenz in einem
Prompt oder die Kopie einer vom Agenten gezeigten Datei, die der gespeicherte Verlauf aufbewahrt. Es liest ein
Live-Transkript ebenso wie ein gespeichertes und löst bei einem fehlerhaften Transkript niemals eine Ausnahme aus.

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

Jedes Ergebnis ist `{ url, attachment_id, filename, mime, message_id, role }`.
`role` ist `user` für einen Upload und `assistant` für eine vom Agenten gezeigte Datei;
`mime` ist bei Letzterer null, und die Bytes tragen ihren eigenen Typ.

<a id="who-wrote-each-message"></a>

## Wer die einzelnen Nachrichten geschrieben hat

Jede Nachricht hat einen Autor. Der Server liest ihn aus den Zugangsdaten, mit denen der
Prompt gesendet wurde: ein Projektmitglied oder der Agent einer anderen Sitzung. Das Runtime-Transkript
enthält ihn nicht.

```ts
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` ist nach Runtime-Nachrichten-ID indiziert. `initial_author` ist die übergeordnete Sitzung für
die erste Nachricht einer gestarteten Sitzung. Es ist `null`, wenn die Sitzung keinen ersten
Prompt hatte. Jeder Autor ist ein `SessionMessageAuthor`:

| `kind` | Felder |
| --- | --- |
| `'member'` | `user_id`, `name`, `email` (`string \| null`) |
| `'session'` | `session_id`, `name` (der Sitzungstitel), `agent` (der ausgeführte Agent, sofern benannt) |

<a id="which-model-answered-a-turn"></a>

## Welches Modell einen Turn beantwortet hat

Eine Transkript-Nachricht nennt das Modell, das ihr Turn angefordert hat. Wenn die
[Fallback-Kette](/docs/de/project/models) des Projekts mit einem anderen Modell antwortet, enthält nur der
Anfragedatensatz des Gateways das tatsächlich antwortende Modell und den von Beeuni abgerechneten Betrag.

```ts
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` ist vor der ersten Antwort `null`. `billed_cost` ist der USD-Betrag, den Beeuni für
die Modellaufrufe der Sitzung abgebucht hat. `turns` ist nach der Runtime-Nachrichten-ID des Prompts indiziert, der
jeden Turn gestartet hat. `served_models` listet die Modelle auf, die in diesem Turn geantwortet haben, mit den
meisten Anfragen zuerst. In React liest `useSessionModelUsage(projectId, sessionId)` denselben
Datensatz; rufe dessen `refetch()` auf, wenn ein Turn endet.

In React liest `useSessionMessageAuthors(projectId, sessionId, messageCount)` die
gleichen Daten. Übergib die Nachrichtenanzahl des Transkripts. Eine neue Nachricht ist das einzige Ereignis, das
einen Autor hinzufügt, daher ruft die Abfrage die Daten erneut ab, wenn sich die Anzahl ändert.

<a id="readiness-is-a-handshake"></a>

## Bereitschaft ist ein Handshake

Bevor du einen Prompt sendest, rufe `ensureReady()` auf. Es stellt bei Bedarf die Sandbox
bereit, wartet auf den Start der Runtime und gibt die aufgelöste Runtime zurück.

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

Bei einem Kaltstart kann `ensureReady()` `RUNTIME_UNAVAILABLE` auslösen. Siehe
[Bei einem Kaltstart erneut versuchen](#retry-on-a-cold-boot), um zu erfahren, was das bedeutet und wie du
den Versuch wiederholst.

`s.send()` und `s.abort()` rufen `ensureReady()` für dich auf.

<a id="seed-a-server-authorized-runtime-session-pin"></a>

### Einen serverseitig autorisierten Runtime-Sitzungs-Pin vorbelegen

Ein serverseitig gerenderter React-Host kann den bereits für
dieselbe Beeuni-Sitzung gespeicherten Runtime-Sitzungs-Pin bereitstellen:

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

Die Vorbelegung hydriert nur zwischengespeicherte Transkript-Inhalte, während `/start` läuft. Sie
überschreibt nicht die Runtime-Identität. Der von `/start` zurückgegebene Pin ist
maßgeblich und ersetzt eine veraltete Vorbelegung.

`opencode_session_id` ist der veraltete Name von `runtime_session_id`. Beide
enthalten denselben Wert; lies den alten Namen nur als den oben genannten Fallback.

Akzeptiere diesen Wert nicht aus einem nicht vertrauenswürdigen Mandantenselektor. Erstelle keine
Runtime-Sitzung im Host. Beeuni erstellt und speichert die Root-Sitzung.
Runtime-Abfrage-Caches und Transkript-Controller sind auf die Sandbox-Runtime
beschränkt, sodass gleiche Runtime-Sitzungs-IDs aus verschiedenen Sandboxes keine
Cache-Einträge teilen.

Ausstehende Wiederholungsversuche für Nachrichten behalten beim Wechseln der
Sitzung ihre ursprüngliche Sandbox-URL. Eine `404`- oder `410`-Nachrichtenantwort beendet automatische Wiederholungsversuche, ohne
das Transkript zu leeren. Ein expliziter Abgleich kann den Lesevorgang erneut versuchen und
nach einer erfolgreichen Antwort die normale Synchronisierung wiederherstellen.

<a id="send-a-prompt"></a>

## Einen Prompt senden

```ts
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()` legt den Prompt im dauerhaften Prompt-Posteingang der Sitzung ab
(`POST .../prompts`), dem Pfad, den der Web-Composer und die CLI verwenden. Es wird aufgelöst,
sobald der Prompt gespeichert ist, mit der Posteingangszeile (`prompt_id`, `state`,
`message_id`), nicht erst, wenn der Turn endet. Lies die Antwort aus `s.stream()` oder dem
Transkript.

Bei runtime-REST-Sitzungen liest das erste `send()` auf einem Handle das Modell und den
Agenten, die in der Beeuni-Sitzung gespeichert sind. Dadurch wird verhindert, dass eine aus einem Snapshot übernommene
runtime-Sitzung veraltete Snapshot-Standardwerte wiederverwendet.

Für die Prompt-Auswahl gilt folgende Priorität:

1. Der `send()`-Aufruf.
2. Der `setModel()`- oder `setAgent()`-Wert des Handles.
3. Der gespeicherte Standardwert der Beeuni-Sitzung.

`setModel` wählt nur aus, was das nächste lokale `send` anfordert — der Wert verlässt nie das
Handle. Um ein neues Modell für eine laufende Sitzung serverseitig zu **speichern**, verwende
`changeModel`:

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

Die Änderung wird durch einen Neustart der Runtime wirksam, daher endet ein laufender Turn.
`applied_live` ist `true`, wenn eine laufende Sitzung die Änderung sofort übernommen hat, und `false`, wenn sie
beim nächsten Start wirksam wird. Nur der Sitzungseigentümer oder ein Aufrufer mit Projektmanager-
Berechtigungen darf das Modell ändern; alle anderen erhalten `403`.

`send()` löst die Runtime auf und sendet dann einen Prompt an sie. `abort()` stoppt den aktuellen
Lauf, ohne die Sitzung zu löschen.

<a id="session-scope-and-cost"></a>

## Sitzungsumfang und Kosten

Lies die gespeicherte Einschränkung für Secrets und die materialisierten Verbindungsbindungen.
`secrets_allowlist: null` bedeutet, dass die Secret-Freigabe des Agenten gilt:

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

`connector_bindings` ist die AUFGELÖSTE Zuordnung und sieht daher bei einer Sitzung,
die ihre Konnektoren überschrieben hat, genauso aus wie bei einer, die die Projektstandards übernimmt. Lies
`connector_bindings_configured`, um sie zu unterscheiden, bevor du den Geltungsbereich darstellst oder
zurücksendest.

Ersetze eines oder beide Felder des Geltungsbereichs:

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

Jedes angegebene Feld ersetzt seinen gesamten vorherigen Wert. Lass ein Feld weg, um
es unverändert zu lassen. Verbindungsänderungen gelten ab dem nächsten Tool-Aufruf.
Das Entfernen eines Secrets stoppt dessen künftige Übermittlung, kann aber einen bereits offengelegten
Wert nicht aus dem Modellkontext oder einem bestehenden Prozess entfernen.

Beide Achsen haben einen expliziten Weg zurück zum Standard. Das ist nicht dasselbe wie ein
leerer Wert:

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

`secrets: []` und `connector_bindings: {}` sind die gegenteilige Anweisung: ein
explizites „keine Projekt-Secrets“ und „überhaupt keine Konnektoren“, einschließlich der
Projektstandards. Eine Sitzung, die `{}` sendet, obwohl sie `null` meint, verweigert sicherheitshalber den Zugriff auf jeden
Alias, den sie nicht genannt hat.

Lies den zusammengefassten Kostendatensatz:

```ts
const cost = await s.cost();
```

Der Datensatz kombiniert endgültige LLM-Kosten, abgerechnete Sandbox-Rechenkosten, Modellnutzung,
Token-Gesamtsummen, Rechendauer und Buchungseinträge. `s.cost()` ruft
`ensureReady()` nicht auf.

<a id="runtime-status-and-previews"></a>

## Laufzeitstatus und Vorschauen

| Methode                      | Rückgabe                        | Verwendung                                          |
| --------------------------- | ------------------------------ | -------------------------------------------- |
| `s.health(init?)`           | `{ status, ok, health, body }` | Prüfen, ob die Laufzeit aktiv ist           |
| `s.previewUrl(port, path?)` | `string`                       | Eine Proxy-URL für einen vom Agenten freigegebenen Port abrufen |
| `s.proxyUrl(url?)`          | `string \| undefined`          | Eine vom Agenten ausgegebene localhost-URL umschreiben    |

```ts
const { ok, health } = await s.health();
const url = s.previewUrl(3000, '/docs');
```

`s.health()` löst nie eine Ausnahme aus. Du kannst es jederzeit aufrufen, auch bevor die Sitzung eine
Laufzeit hat. `s.previewUrl()` und `s.proxyUrl()` benötigen eine aufgelöste Laufzeit — rufe
zuerst `s.ensureReady()` auf, sonst lösen sie `SessionNotReadyError` aus. Siehe
[Fehler bei der Sitzungsbereitschaft](#session-readiness-errors).

<a id="what-the-runtime-supports"></a>

### Was die Laufzeit unterstützt

`health.capabilities` listet die Sitzungsfunktionen auf, die die Laufzeit bereitstellt. Prüfe eine davon
mit `runtimeSupports(capabilities, capability)` und blende das Bedienelement aus, wenn
`false` zurückgegeben wird. Eine runtime-Sitzung stellt alle zehn bereit; eine pi-Sitzung stellt
nur `session.subagents` bereit und beantwortet die anderen mit `501 feature_not_supported`
(`404` für das Konfigurationsdokument). [Harnesses](/docs/de/work/harnesses) enthält die
vollständige Matrix für runtime und pi.

| Fähigkeit | Funktion |
| --- | --- |
| `session.rewind` | Zu einer Nachricht zurückkehren und sie wiederherstellen |
| `session.compact` | Die Unterhaltung bei Bedarf zusammenfassen |
| `session.commands` | Slash-Befehle des Projekts |
| `session.fork` | Eine Sitzung an einer Nachricht abzweigen |
| `session.subagents` | Untergeordnete Sitzungen von Subagenten |
| `session.mcp` | MCP-Server, mit denen sich die Laufzeit selbst verbindet |
| `session.todo` | Die Aufgabenliste der Laufzeit |
| `session.shell` | Ein als Turn ausgeführter Shell-Befehl |
| `session.attach` | Den eigenen Terminal-Client des Harness anbinden |
| `session.config` | Ein Laufzeit-Konfigurationsdokument (`/global/config`); Agenten und Modelle stammen in jedem Fall aus dem Projekt |

```ts
import { runtimeSupports } from '@beeuni/sdk';

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

`runtimeSupports` gibt `true` zurück, bevor eine Zustandsantwort eintrifft, sowie für eine
Laufzeit, die keine `session.*`-Fähigkeit auflistet: Ein Daemon, der vor der Einführung von Fähigkeiten erstellt wurde,
führt runtime aus, das alle Funktionen bereitstellt. Verwende in React
[`useRuntimeSupports`](/docs/de/sdk/react).

Die `start`-Antwort enthält ebenfalls die Liste: `SessionStartResult.capabilities` wird
mit `stage: 'ready'` gesetzt. `useSession` speichert sie, sodass eine Fähigkeitsprüfung
schon vor der Antwort der ersten Zustandsabfrage korrekt ist.

<a id="status-words"></a>

## Statusbegriffe

Jeder Host verwendet dasselbe Vokabular, sodass ein Zustand auf jedem Bildschirm denselben Begriff hat.

| Export                                     | Verwendung                                                                                          |
| ------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `sessionListStatus(session, reviewCount?)` | Eine Sitzung in einer Liste: `needs-you`, `starting`, `running`, `done`, `stopped`, `failed`, `legacy` |
| `SESSION_LIST_STATUS[status]`              | `{ label, tone, description }` für diesen Status                                               |
| `sessionConnectionLabel(connection)`       | Eine kurze Bezeichnung für den Computer der Sitzung oder `null` für `unknown` und `live`                  |
| `SESSION_NOTICE`                           | Der Satz im Nachrichteneditor, solange der Computer nicht bereit ist                                      |
| `turnRetryLabel(secondsLeft)`              | `Retrying in 5s`, `Retrying now` oder `Waiting to retry`, wenn kein Countdown bekannt ist           |

Die Farbstimmungen sind `actionable`, `live`, `progress`, `muted` und `danger`. Grün ist
nur für `live` und `actionable` vorgesehen; eine abgeschlossene Sitzung ist `muted`. Ein Status, den eine neuere
API sendet und dieser Build nicht kennt, wird als `stopped` angezeigt, niemals als `failed`.

## Streaming

Verwende `s.stream()`, um Live-Ereignisse in einem Skript oder auf einem Server zu empfangen. Verwende in einer React-
App stattdessen [`useSession`](/docs/de/sdk/react) — es verwaltet den gesamten
Sitzungslebenszyklus für dich.

`s.stream()` ist der Ereignisstream für die runtime-REST-Kompatibilität. Die Beeuni API
leitet ihn aus der Sandbox weiter. Es gibt keinen separaten WebSocket-Endpunkt. Der
Transport ist `fetch` mit einem Streaming-Antwortbody, der über
`ReadableStream` und `TextDecoderStream` gelesen wird. Das SDK übernimmt Wiederverbindungen,
Backoff und eine Heartbeat-Prüfung.

Streame eine Sitzung:

1. Rufe zuerst `ensureReady()` auf. Die Laufzeit existiert erst, wenn die Sandbox
   startet.
2. Öffne den Stream, bevor du eine Nachricht sendest, damit du keine frühen
   Ereignisse verpasst.
3. Sende die Nachricht.
4. Schließe den Stream, sobald du `session.idle` siehst.

```ts
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');
```

Der Stream liefert Ereignisse in Batches im Abstand von 16 ms. Aufeinanderfolgende
`message.part.delta`-Ereignisse für ein Part-Feld innerhalb eines Batches kommen als ein Ereignis an.
Sein `properties.delta` enthält ihren Text, in der ursprünglichen Reihenfolge zusammengefügt. Sein `id` ist die ID des letzten
Ereignisses. Sein `coalesced`-Array listet die ersetzten Ereignisse auf. Hänge `delta`
an das Feld des Parts an, wie bei einem einzelnen Delta.

Streaming benötigt `fetch` mit einem echten `ReadableStream`-Body und
`TextDecoderStream`. Browser, Node ab Version 18, Bun und Cloudflare Workers
unterstützen dies alle.

Ein Host, dessen `fetch` kein Streaming unterstützt, stellt seinen eigenen Transport bereit. Das SDK ruft ihn
einmal pro Verbindung auf und übernimmt weiterhin selbst Wiederverbindungen, die Fortsetzung mit `Last-Event-ID`, Backoff und
Heartbeat:

```ts
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 bietet standardmäßig kein Streaming-`fetch`. Die mobile Beeuni-App nutzt
diese Schnittstelle mit `react-native-sse`. `createHttpSessionSyncController` bleibt
für Hosts verfügbar, die nur einen begrenzten Verlauf und eine
Statussynchronisierung benötigen.

Der Controller lädt zuerst die neuesten 10 Nachrichten. `loadOlder()` folgt dem
Server-Cursor. `loadHttpSessionHistory()` folgt für explizite
Exporte jedem Cursor.

<a id="event-types"></a>

### Ereignistypen

Jedes Ereignis hat einen `type` und ein `properties`-Objekt, das seine Daten enthält, zum
Beispiel `event.properties.sessionID`.

| `type`                                          | Wann es ausgelöst wird                              |
| ----------------------------------------------- | --------------------------------------------------- |
| `message.updated` / `message.removed`           | Eine Nachricht wurde geändert oder gelöscht.        |
| `message.part.updated` / `message.part.removed` | Ein Teil (Text, Tool-Aufruf, Datei) wurde erweitert oder entfernt. |
| `session.status`                                | Der Beschäftigungsstatus der Sitzung hat sich geändert. |
| `session.idle`                                  | Der Turn wurde abgeschlossen.                       |
| `session.error`                                 | Der Turn ist fehlgeschlagen. Das Ereignis enthält den Fehler. |
| `question.asked`                                | Der Agent hat um Eingabe gebeten.                    |
| `question.replied` / `question.rejected`        | Die Antwort auf eine Frage ist eingegangen.          |

Wandle rohe Nachrichten und Teile mit `classifyTurn` in darstellbare Ausgabe um. Siehe
[SDK-Referenz](/docs/de/sdk/reference).

<a id="retry-on-a-cold-boot"></a>

## Wiederholungsversuche bei einem Kaltstart

`ensureReady()` fragt den Endpunkt `/start` der Sitzung ab — jeder Aufruf nutzt Long-Polling für bis
zu 30 s — bis die Laufzeit eine abschließende Phase `ready`/`failed`/`stopped` erreicht
oder die Frist (`readyTimeoutMs`, standardmäßig ca. 180 s) abläuft. Bei einer bereits gestarteten Sitzung
liefert die erste Abfrage sofort `ready`. Bei einem Kaltstart wird weiter abgefragt,
solange die Sandbox `retriable: true` meldet. Ein langsamer Start dauert also nur länger,
statt einen Fehler auszulösen. Erst wenn die Frist
abläuft, wird ein `ApiError` mit `code:
'RUNTIME_UNAVAILABLE'` if the runtime is still not `ready` ausgelöst.

`ensureReady()` ist idempotent, sodass gleichzeitige Aufrufe für dieselbe Sitzung
eine `/start`-Anfrage teilen, statt mehrere zu senden. Die Hilfsfunktion `retryUntilReady`
unten ist jetzt optional — `ensureReady()` führt bereits intern Wiederholungsversuche durch —, bleibt aber
nützlich, wenn du ein längeres Gesamtzeitbudget als den Standardwert `readyTimeoutMs` möchtest.

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

Siehe [Fehlerklassen](#error-classes) für die vollständige Struktur von `ApiError`. In React
wiederholt [`useSession`](/docs/de/sdk/react) `/start` für dich, sodass du
dieses Muster nicht brauchst.

<a id="what-start-tells-you"></a>

## Was `/start` dir mitteilt

Jede `/start`-Antwort beschreibt **diesen Aufruf**, nicht den gesammelten
Verlauf des Datensatzes. Vier Felder enthalten diese Informationen.

| Feld | Bedeutung |
| --- | --- |
| `observed_at` | Eine gemeinsame Zeitbasis für die gesamte Antwort. |
| `action` | Was der Server getan hat: `inspected`, `checked_provider`, `resumed`, `provisioned`, `restored`, `reconciled`, `awaited_wake`, `cooling_down`. |
| `observation` | Was der Server geprüft hat. `known: false` bedeutet **bei diesem Aufruf nicht geprüft** — niemals „geprüft und nichts gefunden“. |
| `boot` | `phase` (`provisioning` / `resuming` / `booting` / `ready` / `parked` / `failed`), `since` und `actively_starting`. |

`boot.actively_starting` beantwortet die Frage „Läuft gerade ein Anbietervorgang für diese
Sitzung?“. Eine `starting`-Payload mit `actively_starting: false` bedeutet,
dass der Server eine Wartezeit vor dem nächsten Versuch abwartet, nicht dass ein Computer startet.

```jsonc
{
  "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"
    }
  }
}
```

<a id="a-failed-start-is-retried-for-you"></a>

### Ein fehlgeschlagener Start wird automatisch wiederholt

Ein fehlgeschlagener Start setzt eine **Wartezeit**, kein dauerhaftes Urteil. Das nächste
`/start` nach der Wartezeit versucht selbstständig erneut, die Sitzung aufzuwecken. Die Wartezeit steigt
bei aufeinanderfolgenden Fehlern (2 min, 5 min, 10 min). Nach fünf aufeinanderfolgenden
Fehlern antwortet `/start` mit `stage: "failed"` und der Anzahl der Versuche in
`failure.message`; dieses Urteil läuft 30 Minuten nach dem letzten Fehler ab, und
`POST …/restart` hebt es sofort auf.

`retriable` wird bei jedem Aufruf abgeleitet. Ein Zustand, bei dem der Server noch einen erneuten Versuch unternehmen kann,
trägt niemals `retriable: false`.

`failure.evidence` nennt die Prüfung, die das negative Ergebnis geliefert hat, wann sie ausgeführt wurde und
wann der Server es erneut versucht. Jeder `/start`-Fehler enthält diese Angaben.

<a id="prompt-attachments"></a>

## Prompt-Anhänge

Lade lokale Dateien hoch, wenn Nutzer sie einem Eingabefeld hinzufügen. Private Uploads gehören zum
Projekt und erfordern keine laufende Sitzung.

```tsx
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 })` wird erfüllt, sobald alle Uploads bereit sind, in der Reihenfolge von `ids`.
Es wird abgelehnt, wenn ein Upload fehlschlägt, abgebrochen oder entfernt wird oder `signal` abbricht.
`submit(ids)` übergibt die Einträge an diesen Sendevorgang: Sie verlassen `attachments`, zählen
nicht mehr zu den Limits und werden weiter hochgeladen, nachdem das Eingabefeld ausgehängt wurde.
Rufe `forget(ids)` auf, nachdem der POST erfolgreich war, oder `reclaim(ids)`, um sie
nach einem fehlgeschlagenen Sendevorgang an das Eingabefeld zurückzugeben.
Der aufgelöste Dateiteil enthält `attachment_id`, `filename` und `mime`.
Er enthält weder eine URL noch Dateibytes. Der Server bindet den Upload und materialisiert ihn.
Jede Datei erreicht den Computer der Sitzung, einschließlich Bildern und PDFs: Bevor der
Prompt ausgeführt wird, schreibt der Server sie nach `/workspace/uploads/.beeuni-inbox/`, und der
Prompt verweist per Pfad darauf. Im Prompt selbst werden keine Dateibytes übertragen. Wenn
das Speichern des Verlaufs aktiviert ist, behält der Server außerdem eine Kopie für das Transkript. Eine fehlgeschlagene Kopie
verhindert nie, dass eine Datei den Computer erreicht. Kann der Server eine Datei nicht schreiben, wird der
Prompt zurückgehalten und erneut versucht.
Verwende diese Teile bei der Erstellung von Plattformsitzungen, beim Start bereits gestarteter Sitzungen oder auf Prompt-Inbox-
Routen. Übergib private Handles nicht direkt an `sendParts` der runtime-Laufzeit.
Ältere Plattform-Dateiteile mit `url` funktionieren weiterhin.

Jeder Eintrag hat ein stabiles `id`, die ursprüngliche `file`-Referenz, `receivedBytes` und
einen Status: `pending`, `uploading`, `processing`, `ready`, `error` oder `aborted`.
Die gesendeten Bytes bestimmen den Fortschritt. Der Controller veröffentlicht einen Fortschritts-Snapshot nur
bei einer Änderung um einen ganzen Prozentpunkt, höchstens zehn pro Sekunde und Upload. `processing` kann
bei 100 % bleiben, bis der Server den Upload abschließt. Die Limits betragen 50 MiB pro
Datei, 100 MiB pro Nachricht und 20 Dateien. Dateien dürfen nicht leer sein. Standardmäßig werden zwei Dateien
gleichzeitig hochgeladen.

`retry(id)` setzt denselben Handle fort. Nach `attachment_size_mismatch` oder
`attachment_failed` behält der Server keinen nutzbaren Handle, daher lädt `retry(id)` das
File erneut als neuen Anhang hoch. Ein abgelaufener Upload kann nicht erneut versucht werden: `retry(id)`
löst einen Fehler aus, und der Fehler des Eintrags enthält den Code `attachment_expired`. `abort(id)` bricht
unabgeschlossene Vorgänge ab.
`remove(id)` entfernt den Eintrag, bricht seinen Upload ab und wird sofort erfüllt. Das
DELETE auf dem Server erfolgt nach bestem Bemühen und wird nie abgelehnt; ein Upload, der nicht gelöscht werden kann,
läuft nach 24 Stunden ab.
`forget(ids)` gibt Einträge frei, ohne Serverobjekte zu löschen.
Der Hook bricht aufgelistete Vorgänge beim Aushängen oder Projektwechsel ab und löscht seine Uploads
nach bestem Bemühen: Kein Sendevorgang hält sie fest, und Entwürfe behalten keinen Handle. Übergebene Uploads
laufen weiter.

Die Auswahl bleibt nur im Arbeitsspeicher. Speichere niemals File-Objekte, Blob-URLs, signierte
URLs oder Upload-Handles in einem Entwurf. Verwaiste Uploads laufen nach 24 Stunden ab.

Hosts ohne Framework verwenden `beeuni.project(projectId).attachments.createController()`
mit denselben Methoden. Abonniere über `subscribe(listener)` und lies
`getSnapshot()`. Rufe `dispose()` auf, wenn das Eingabefeld geschlossen wird.
Direkte Aufrufer können `attachments.upload(file, { signal, onProgress, onUpload,
resume })` and `attachments.delete(attachmentId)` verwenden.
Bewahre die Metadaten von `onUpload` auf, um den Upload manuell mit derselben ID fortzusetzen.

Der Server wählt den Transport im Feld `upload` des Handles aus:

- `kind: 'direct'` ist der Standard. Das SDK sendet die gesamte Datei in einem `PUT` an
  `upload.url` mit `upload.headers` und ohne Authorization-Header. Mit
  `XMLHttpRequest` (Browser, React Native) meldet der Fortschritt die gesendeten Bytes. Mit
  `fetch` meldet der Fortschritt 0 und dann die volle Größe. Eine abgelaufene URL oder eine,
  die Storage mit 400, 401 oder 403 ablehnt, wird einmal für dasselbe
  `attachment_id` neu signiert. Ein `409` von Storage bedeutet, dass ein früherer Versuch die Datei gespeichert hat.
- `kind: 'chunked'` sendet nacheinander authentifizierte `PUT` mit jeweils `upload.chunk_size`
  Bytes. Das SDK akzeptiert jedes positive `chunk_size`. Nur ein Deployment, dessen Edge
  große Request-Bodys verwirft, wählt diesen Modus.

Der Abschluss überprüft die gespeicherte Größe und SHA-256. Wenn der Abschluss mit `409
attachment_not_uploaded`, `onUpload` antwortet, meldet es das direkte Handle mit
`received_bytes: 0`, sodass `retry(id)` oder eine Wiederaufnahme die Datei erneut sendet.
Initiierung, Upload und Abschluss wiederholen bei Timeouts, Netzwerkfehlern, 429 und
5xx den Versuch mit exponentiellem Backoff und Jitter. Das Zeitbudget beträgt 60 Sekunden ab dem ersten
Fehler, sodass auch ein langer Upload, der spät fehlschlägt, erneut versucht wird. Der Abschluss wiederholt auch bei
`attachment_processing` den Versuch, mit einem Zeitbudget von fünf Minuten. Die Initiierung wiederholt niemals bei 402
(ein `BillingError`: das Konto kann nicht ausgeführt werden) oder 429 `attachment_budget_exceeded` (40
unfertige Uploads oder 500 MiB nicht gesendete Uploads für den Benutzer; ungenutzte Uploads
laufen innerhalb von 24 Stunden ab). Der Server beantwortet einen Abschluss oder lehnt ihn innerhalb von 105
Sekunden ab. Jede Abschlussanfrage lässt 120 Sekunden zu. Bei Abbrüchen durch den Aufrufer wird niemals erneut versucht.

Die Referenz eines gesendeten Anhangs wird 1 Stunde nach Zustellung seines Prompts
sowie beim Löschen seiner Sitzung oder seines Projekts freigegeben. Der nächste Wartungsdurchlauf
entfernt dann die Datei, und ihr `attachment_id` kann nicht mehr gesendet werden.

<a id="files"></a>

## Dateien

`s.files` liest und schreibt in der Sandbox der Sitzung: `list`, `read`, `readBlob`,
`status`, `findFiles`, `findText`, `upload`, `create`, `copy`, `remove`,
`mkdir`, `rename`. Jeder Aufruf löst zuerst die Laufzeit auf und richtet sich immer an
die eigene Sandbox dieser Sitzung. Die vollständige Methodenliste findest du in der [SDK-Referenz](/docs/de/sdk/reference).

<a id="session-verbs"></a>

## Sitzungsverben

Das Handle liest die Laufzeit aus und antwortet ihr für dich. Jedes Verb stellt zuerst die
Sitzung bereit und richtet sich an die eigene Sandbox dieser Sitzung, unabhängig davon, welcher Harness sie ausführt.

```ts
// 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`, der rohe REST-Kompatibilitätsclient, ist veraltet. Verwende die Verben
oben mit `send`, `abort`, `rewind` und `stream`.

<a id="warm-a-project-session"></a>

## Eine Projektsitzung vorwärmen

Rufe `ensureWarm()` auf, wenn eine Projekt-Landingpage vor dem
 ersten Prompt eine einsatzbereite Laufzeit benötigt.

```ts
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()` erstellt eine ungenutzte Sitzung für den aktuellen Benutzer oder gibt eine zurück. Es
ist derselbe Erstellungsvorgang, den `sessions.create()` ausführt, mit den Standardeinstellungen des Projekts: dieselbe
Abrechnungsprüfung, dieselben Connector-Anforderungen. Der einzige
Unterschied ist `metadata.warm`, das die Sitzung vor
`sessions.list()` verbirgt, bis ihr erster Prompt eintrifft.

Betrachte dies als spekulativ. Ein `409 WARM_SESSION_UNAVAILABLE` bedeutet, dass das Projekt
momentan nicht vorgewärmt werden kann — fahre mit `sessions.create()` fort, das
den tatsächlichen Grund meldet.

Die vorgewärmte Sitzung verwendet den DEFAULT-Agenten und die Sandbox des Projekts. Wenn der Benutzer
andere auswählt, verwirf sie und rufe `sessions.create()` auf: Eine ungenutzte vorgewärmte
Sitzung ist verborgen und wird automatisch bereinigt.

:::warning
`claimWarm()` ist veraltet. Eine vorgewärmte Sitzung ist eine gewöhnliche Sitzung, daher gibt es
nichts zu beanspruchen — navigiere zu ihr und sende ihr einen Prompt. Der Aufruf funktioniert weiterhin für
Clients, die an das ältere Format gebunden sind, und wird in der nächsten Hauptversion entfernt.
:::

<a id="handling-errors"></a>

## Fehlerbehandlung

Jeder Aufruf über `createBeeuni` wird mit einer typisierten Unterklasse von `Error` abgelehnt,
niemals mit einem einfachen Objekt. Fange den Fehler ab, prüfe `instanceof` und verzweige anhand von
`.status` oder `.code`.

```ts
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;
  }
}
```

<a id="error-classes"></a>

### Fehlerklassen

| Klasse                  | Erweitert    | Wann sie ausgelöst wird                                                                 | Wichtige Felder                                                           |
| ---------------------- | ---------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `ApiError`             | `Error`    | Standard für jede fehlgeschlagene Anfrage: Fehlerstatus, Netzwerkfehler, Timeout oder Abbruch | `status`, `code`, `detail`, `response`, `url`, `endpoint`, `timeout` |
| `AuthError`            | `ApiError` | `getToken` gab `null` zurück. Beeuni hat die Anfrage nie gesendet                      | `code` ist immer `'NO_SESSION'`                                      |
| `BillingError`         | `ApiError` | HTTP `402`. Die einzige Abrechnungsfehlerklasse                                       | `status` (`402`), `code`, `detail.message`                           |
| `RequestTooLargeError` | `ApiError` | HTTP `431`. Meist zu viele Dateien in einer Anfrage                              | `detail.suggestion`                                                  |
| `SessionNotReadyError` | `Error`    | Ein Laufzeit-Accessor wurde vor `ensureReady()` ausgeführt                                  | `name` ist `'SessionNotReadyError'`                                   |

`ApiError.name` ist standardmäßig `'ApiError'`. Zwei Fälle überschreiben dies:

- `name: 'AbortError'`, `code: 'ABORTED'` — die Anfrage wurde abgebrochen, zum Beispiel durch Navigation. Das ist kein Fehler. Ignoriere ihn.
- `code: 'TIMEOUT'` — das eigene Timeout der Anfrage ist abgelaufen. `url`, `endpoint` und `timeout` zeigen, was das Timeout erreicht hat.

Bei jedem anderen Fehler enthält `status` den HTTP-Statuscode. `code` stammt aus dem `error_code` des Backends oder verwendet ersatzweise den Status als Zeichenfolge. `message` ist eine aufzählbare eigene Eigenschaft von `ApiError` und bleibt daher bei `JSON.stringify` und Object Spread erhalten.

Beeuni sendet eine Anfrage, die `401` erhält, einmal erneut mit einem frischen Token von `getToken()`, wenn sich dieses Token vom gesendeten unterscheidet. Eine Anfrage mit einem nur einmal lesbaren Stream-Body wird nicht erneut gesendet.

Beeuni wiederholt manche Anfragen, bevor dein Code einen Fehler sieht. Wenn eine `GET`- oder `HEAD`-Anfrage `502`, `503` oder `504` zurückgibt, wiederholt Beeuni sie bis zu 2 Mal, mit einer Verzögerung von erst 250 ms und dann 500 ms. Ein vorübergehender Transportfehler bei einer `GET`- oder `HEAD`-Anfrage — ein Netzwerkfehler, kein Statuscode — wird auf dieselbe Weise erneut versucht. Ein erfolgreicher Wiederholungsversuch erreicht `onError` niemals. Beeuni wiederholt niemals `POST`-, `PUT`-, `PATCH`- oder `DELETE`-Anfragen oder eine `500`-Antwort.

Beeuni löst `AuthError` auf dem Client aus, bevor es eine Anfrage sendet, wenn `getToken()` `null` zurückgibt. `AuthError` erweitert `ApiError`, daher trifft `err instanceof ApiError` weiterhin zu. Prüfe `err instanceof AuthError` oder `err.code === 'NO_SESSION'`, um „nicht angemeldet“ getrennt von einem Backend-Fehler zu behandeln.

Beeuni löst `BillingError` für jede HTTP-`402`-Antwort aus: Guthaben aufgebraucht, Tariflimit überschritten oder eine andere Abrechnungssperre. `detail.message` enthält den Grund aus dem Backend, und `code` enthält den maschinenlesbaren Code des Backends (zum Beispiel `app_account_unfunded`). `BillingError` erweitert `ApiError`, daher prüfe zuerst `BillingError`, wenn du beide behandelst.

Beeuni löst `RequestTooLargeError` für HTTP `431` aus. Das bedeutet meist, dass die Anfrage zu viele Dateien enthielt. `detail.suggestion` enthält einen direkt anzeigbaren Hinweis für den Benutzer.

<a id="session-readiness-errors"></a>

### Fehler bei der Sitzungsbereitschaft

Zwei Fehler bedeuten, dass die Sandbox der Sitzung noch nicht bereit ist. Behandle jeden davon unterschiedlich.

`SessionNotReadyError` löst synchron einen Fehler aus, wenn du einen Laufzeit-Accessor — `session.previewUrl()` oder `session.proxyUrl()` — aufrufst, bevor dieses Session-Handle seine Sandbox aufgelöst hat. Ein Session-Handle löst nur seine eigene Sandbox auf; es greift niemals auf die Sandbox einer anderen Session zurück.

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

Rufe zuerst `await session.ensureReady()` auf oder rufe `send()` auf, wodurch die Session intern vorbereitet wird. `session.health()` ist der einzige Accessor, der diesen Fehler niemals auslöst. Du kannst ihn daher abfragen, bevor die Session startet.

`RUNTIME_UNAVAILABLE` ist der zweite Fehler — er bedeutet, dass `ensureReady()` selbst beim Warten auf einen Kaltstart das Zeitlimit überschritten hat. Das vollständige Vorgehen findest du unter [Bei einem Kaltstart erneut versuchen](#retry-on-a-cold-boot). In React übernimmt `useSession` die Wiederholungsversuche für dich und stellt den Fehler über den Wert `phase` bereit, statt ihn auszulösen.

<a id="helpers"></a>

### Hilfsfunktionen

| Hilfsfunktion                    | Signatur                            | Funktion                                                                                                      |
| -------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `parseBillingError(error)`       | `(error) => Error`                  | Wandelt eine `402`-Antwort in ein `BillingError` um. Gibt andere Fehler unverändert zurück                                  |
| `isBillingError(error)`          | `(error) => boolean`                | Gibt `error instanceof BillingError` zurück                                                                       |
| `formatBillingErrorForUI(error)` | `(error) => BillingErrorUI \| null` | Gibt bei Fehlern ohne Abrechnungsbezug `null` zurück. Gibt andernfalls `{ alertTitle, alertSubtitle }` für ein Upgrade-Modal zurück |
| `isRuntimeNotReadyResponse(error)` | `(error) => boolean` | Wahr für `503` des Sandbox-Daemons, solange die Session-Laufzeit noch keine Anfrage annehmen kann. Die Anfrage wurde nicht weitergeleitet, daher ist ein erneuter Versuch sicher. Dasselbe Ergebnis bei runtime und pi |
| `isSandboxNotReadyError(error)` | `(error) => boolean` | Wahr für den obigen Fall und für eine Sandbox, die gestoppt ist, startet oder geparkt ist. Zeige sie als „wird aufgeweckt“ an und frage sie weiterhin ab |
| `isRuntimeStartingError(error)` | `(error) => boolean` | Wahr für beide obigen Fälle und für eine Laufzeit-URL, die noch nicht festgelegt ist. Eine Error Boundary versucht es in diesem Fall erneut, statt einen Absturz anzuzeigen |

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

### In `@beeuni/sdk/react`

`@beeuni/sdk/react` exportiert `BillingError`, `RequestTooLargeError`, `parseBillingError`, `isBillingError` und `formatBillingErrorForUI` erneut. `ApiError` und `AuthError` werden nicht erneut exportiert — importiere sie aus `@beeuni/sdk`.

`useSession` ordnet jeden Fehler von `send`, `answerQuestion`, `answerPermission` und `rejectQuestion` einem `sendError`-Objekt zu, sodass du keine `instanceof`-Prüfungen von Hand schreiben musst:

```ts
interface BeeuniSendError {
  kind: 'billing' | 'runtime-not-ready' | 'runtime-error';
  message: string;
  billing?: BillingError; // set when kind is 'billing'
  cause: unknown;
}
```

```tsx
const s = useSession(projectId, sessionId);

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

Weitere Informationen zu `useSession` findest du unter [React-Hooks](/docs/de/sdk/react).
