# セッション

セッションを実行し、イベントをストリーミングして、発生しうるエラーを処理します。

Canonical page: https://beeuni.ai/docs/ja/sdk/sessions

セッションは、専用のサンドボックスと専用の git ブランチで行われる、エージェントの1回の実行です。
`beeuni.session(projectId, sessionId)` は、セッションのすべての操作に使うハンドルを返します。
開始、プロンプトの送信、イベントのストリーミング、ステータスの読み取りができます。
このページでは、ハンドル、準備完了のハンドシェイク、ストリーミング、SDK 呼び出しがスローしうる型付きエラーを説明します。

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

`s` は、セッションのすべての操作に使うハンドルです。セッション ID、サンドボックス
ID、ブランチ名は同じ値です。概念については
[セッション](/docs/ja/work/sessions)を参照してください。

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

## セッションのライフサイクル

| メソッド | ラップ対象 | 動作 |
|---|---|---|
| `s.get(opts?)` | `GET /projects/:pid/sessions/:sid` | セッションの詳細を読み取ります |
| `s.update(input)` | `PATCH …/sessions/:sid` | セッション名を変更し、ラベルを置き換え、またはメタデータのキーをマージします |
| `s.start(waitMs?)` | `POST …/sessions/:sid/start` | ランタイムをプロビジョニングして起動します |
| `s.restart()` | `POST …/sessions/:sid/restart` | ランタイムを再起動します。同じサンドボックスを維持します |
| `s.configState()` | `GET …/sessions/:sid/config` | 設定の最新状態（`stale`）と設定リリースの状態（`release`）を読み取ります |
| `s.reloadConfig(input?)` | `POST …/sessions/:sid/reload` | エージェント設定を再コンパイルし、検証後にランタイムを置き換えます |
| `s.reloadConfigStream(input, onEvent)` | `POST …/sessions/:sid/reload-stream` | 同じリロードを実行し、サーバーで確認された進行フェーズを出力します |
| `s.stop()` | `POST …/sessions/:sid/stop` | ランタイムを停止します。セッションは残ります |
| `s.delete()` | `DELETE …/sessions/:sid` | セッションを削除します |
| `s.setSharing(intent)` | `PUT …/sharing` | 共有と公開範囲を設定します |
| `s.participants()` | `GET …/sessions/:sid/participants` | セッションを開けるユーザーと、各プロンプトの送信者を読み取ります |
| `s.watch()` | `GET …/sessions/:sid/watch` | このセッションの[通知](/docs/ja/work/notifications)を受け取るかどうかを読み取ります。`notification_center` が必要です。オフの間は `403` `feature_disabled` |
| `s.setWatch(watching)` | `PUT …/sessions/:sid/watch` | 自分向けのセッション通知をフォロー（`true`）またはミュート（`false`）します。`notification_center` が必要です。オフの間は `403` `feature_disabled` |
| `s.presence({ tab_id, active, alerts? }, { keepalive? })` | `PUT …/sessions/:sid/presence` | 開いているタブを報告します。`alerts: true` はタブが独自に通知を表示することを示すため、スマートフォンへのプッシュ通知が抑制されます。`notification_center` がオフの場合、存在するタブがあれば抑制されます。`keepalive` により、ページが閉じた後もリクエストを継続できます。不在の報告ではデフォルトでオンです |
| `s.cost()` | `GET /usage/session-costs/:sid` | ランタイムを起動せずに、確定した LLM とコンピュートのコストを読み取ります |
| `s.scope()` | `GET …/sessions/:sid/scope` | 保存されたシークレットの範囲制限と、実体化された接続バインディングを読み取ります |
| `s.rescope(input)` | `PUT …/sessions/:sid/scope` | 次のプロンプトまたはツール呼び出しに向けて、指定されたスコープフィールドを置き換えます |
| `s.commit(input?)` | — | エージェントの作業をコミットします |
| `s.providerSecretPool.get(providerId)` | `GET …/provider-secret-pools/:providerId` | 選択されたアカウントシークレットの ID を読み取ります |
| `s.providerSecretPool.list()` | `GET …/provider-secret-pools` | 空のプールを含む設定済みのプールを一覧表示し、`can_edit` を報告します |
| `s.providerSecretPool.set(providerId, secretIds)` | `PUT …/provider-secret-pools/:providerId` | 選択を置き換えます。`null` はプロジェクトのデフォルトを復元します |

> `s.delete()` はセッションとそのランタイムを削除します。この操作は取り消せません。セッションを失わずに
> 一時停止するには、代わりに `s.stop()` を呼び出してください。

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

## ラベルとメタデータ

すべてのセッションには `labels: string[]` と `metadata` オブジェクトがあります。これらを使って
セッションを分類し、アプリでの表示方法を決めます。

- **ラベル**は自由形式です。各ラベルは前後の空白が除去され、1～64文字、1セッションあたり最大20個で、
  重複は除去されます。照合は完全一致で、大文字と小文字を区別します。
- **メタデータ**は独自のキーを格納する自由形式の JSON オブジェクトで、1回の書き込みにつき JSON で最大16,384文字です。
  サーバーも独自のキーをここに保存します
  （`custom_name`、`source`、…）。サーバー管理のキーを指定するリクエストは
  `400` で拒否されます。競合を避けるため、キーにプレフィックス（`app_…`）を付けてください。

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

React では、`labels` を `useProjectSessions(projectId, { labels })` に渡します。
ラベルの組み合わせごとに個別のキャッシュエントリが作成されます。エージェントは CLI
（`beeuni sessions new --label … --meta k=v`、`beeuni sessions update --label …`）
およびホスト型 MCP サーバー（`start_session` `labels` / `metadata`、
`list_sessions` `labels`）から同じフィールドを設定します。実行可能なバージョンは `packages/sdk/examples/12-session-labels.ts` です。

`pooled_provider_secrets` と `llm_gateway` が有効な場合、セッションは1つのプロバイダーに対して
利用可能な複数のアカウントシークレットを選択できます。`beeuni.accounts.secretResources.create(accountId, { project_id: projectId, ...input })` を通じて
プロジェクトスコープのリソースを作成します。
デフォルトでは、すべてのプロジェクトメンバーが使用できます。
`.setAccess(accountId, secretId, 'members', userIds)` で制限してください。読み取りではメタデータと権限が返され、
シークレットの値は決して返されません。出力開始前にプロバイダーが `429` となった場合、ゲートウェイは
選択された別のキーを試します。

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

呼び出し元がリロードの進行状況を表示する場合は、ストリーミングメソッドを使用してください。

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

フェーズは `checking-session`、`refreshing-workspace`、`compiling-config`、
`applying-config`、`confirming-config` です。`refresh_repo` が `false` の場合、サーバーは
`refreshing-workspace` を省略します。
`applying-config` フェーズには、デーモンによって検証されたランタイムの置き換えが含まれます。

API が設定リリースをサポートしている場合、`s.configState()` は `release` を返します。
その型は `SessionConfigRelease` です。リロードの結果にも同じオブジェクトが含まれます。

```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` は3状態です。`null` は Beeuni が判定できなかったことを意味します。`stale === true` をテストし、
`!stale` は決して使わないでください。古い API は `release` を省略します。
[設定の収束](/docs/ja/work/runtime#config-convergence)を参照してください。

`s.participants()` は `{ participants, total, multi_user }` を返します。`participants` は、
現在セッションを開けるユーザーを、所有者を先頭に最大20人まで一覧表示し、各項目は
`{ user_id, name, email, avatar_url, is_viewer }` です。`total` は総数です。
2人以上がセッションを開ける場合、`multi_user` は `true` です。React では、
`useSessionParticipants(projectId, sessionId)` が同じデータを読み取ります。各メッセージの作成者は
`s.messageAuthors()` です。

さらに3つの読み取りメソッドが、このハンドルを補完します。

- `s.previews()` — ランタイムが公開するプレビュー用ポートの候補。
- `s.publicShares.list()` / `.create(input)` / `.revoke(shareId)` — 公開共有リンク。
- `s.reminders.list()` / `.create(input)` / `.update(reminderId, { enabled })` / `.remove(reminderId)` — このセッションに送る予約済みプロンプト。[リマインダー](/docs/ja/connect/reminders)を参照してください。
  `.create({ transcript: true })` は会話への読み取り専用リンクを作成します（有効なリンクはセッションごとに1つで、2回目の呼び出しでは同じリンクを返します）。`getPublicSessionShareMessages(token)` で匿名で読み取れます。`findActiveTranscriptShare(shares)` は `.list()` から有効なリンクを選びます。
- `s.audit(limit?)` — セッション内のエージェント操作の監査証跡。
- `s.attachments.upload(file, options?)` — 起動前に非公開ファイルを保存します。最大50 MiB。プロンプトのファイルパート用に固定URLを返します。
- `s.attachments.read(attachmentId, signal?)` — サンドボックスを起動せずに、保存済みバイトを `Blob` として読み取ります。
- `s.transcript(options?)` — コンパクトなサーバー側のトランスクリプト（テキストとツール呼び出しのみで、ツールの入力や出力は含みません）。プロジェクトスコープのセッショントークンで利用できます。
- `s.transcriptSync(options?)` — 各ターンの終了時にサーバーが保存するトランスクリプト。実行環境のメッセージエンベロープ形式です。添付ファイルのバイトを除き、すべてのパートを1:1で保持します。テキスト、推論、各ツール呼び出しとその入力、出力、エラー、メタデータが含まれます。サンドボックスが停止中または起動途中でも、起動させずに応答します。最新のウィンドウを取得するには `limit`（最大500）を渡し、それより古いウィンドウを読み取るには、ウィンドウの `next_cursor` を `before` として渡します。ウィンドウはJSONが1,000,000文字に達しても打ち切られます。その際は最新のメッセージを保持し、`next_cursor` が保持された最古のメッセージを示します。サブエージェント自身の保存済みトランスクリプトを読み取るには、`child`（サブエージェントの `ses_…` ID、サブエージェントの行から開くセッション）を渡します。会話から派遣されたすべてのサブエージェントが保存されます。`source` は `mirror` です。`complete` がtrueになるのは、ウィンドウが保存済みメッセージをすべて含み、保存されたコピーがセッションの最初のメッセージまで到達している場合のみです。ランタイムを完全に読み取り、メッセージがなかったセッションは、`available: true`、`complete: true`、`total: 0` を返し、メッセージは返しません。保存履歴でツールの入力と出力が省かれていた2026-09-28より前に保存されたツール呼び出しは、タイトルまたはメタデータから確認できる入力とともに返され、コマンドの場合は出力も返されます。
- `s.voiceTranscript(options?)` — このセッションのライブ音声通話のトランスクリプト（発話ターンと `ask_beeuni`/`run_command` ワーカーのツール呼び出し）。セッションにライブ通話がない場合は、404ではなく空のリストを返します。

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

### セッションが保存したファイル

`findSessionAttachments(messages)` は、トランスクリプトが参照するすべての保存済みファイルを、順番に重複なく一覧表示します。ユーザーのアップロード、プロンプト内の `<file>` 参照、またはエージェントが表示したファイルの保存履歴内のコピーが対象です。ライブのトランスクリプトも保存済みのものも読み取り、不正な形式でも例外をスローしません。

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

各結果は `{ url, attachment_id, filename, mime, message_id, role }` です。
`role` はアップロードの場合は `user`、エージェントが表示したファイルの場合は `assistant` です。
後者では `mime` はnullで、バイト自体に型が含まれます。

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

## 各メッセージの作成者

すべてのメッセージには作成者がいます。サーバーはプロンプトを送信した認証情報から作成者を読み取ります。作成者はプロジェクトメンバー、または別のセッションのエージェントです。ランタイムのトランスクリプトには含まれません。

```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` はランタイムのメッセージIDをキーとします。`initial_author` は、生成されたセッションの最初のメッセージに対する親セッションです。セッションに最初のプロンプトがなかった場合は `null` です。各作成者は `SessionMessageAuthor` です。

| `kind` | フィールド |
| --- | --- |
| `'member'` | `user_id`, `name`, `email` (`string \| null`) |
| `'session'` | `session_id`, `name`（セッションのタイトル）, `agent`（実行するエージェント。名前がある場合） |

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

## ターンに応答したモデル

トランスクリプトのメッセージには、そのターンで要求したモデル名が記録されます。プロジェクトの[フォールバックチェーン](/docs/ja/project/models)が別のモデルで応答した場合、応答したモデルとBeeuniの請求額が記録されるのは、ゲートウェイのリクエスト記録のみです。

```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` は `null` です。`billed_cost` は、セッションのモデル呼び出しに対してBeeuniが差し引いた米ドル額です。`turns` は、各ターンを開始したプロンプトのランタイムメッセージIDをキーとします。`served_models` は、そのターンで応答したモデルをリクエスト数の多い順に一覧表示します。Reactでは、`useSessionModelUsage(projectId, sessionId)` が同じ記録を読み取ります。ターンが終了したら、その `refetch()` を呼び出してください。

Reactでは、`useSessionMessageAuthors(projectId, sessionId, messageCount)` が同じデータを読み取ります。トランスクリプトのメッセージ数を渡してください。作成者が追加されるのは新しいメッセージが届いたときだけなので、メッセージ数が変わるとクエリが再取得されます。

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

## 準備完了の確認はハンドシェイク

プロンプトを送信する前に、`ensureReady()` を呼び出してください。必要に応じてサンドボックスをプロビジョニングし、ランタイムの起動を待ち、解決されたランタイムを返します。

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

コールドブート時には、`ensureReady()` が `RUNTIME_UNAVAILABLE` をスローすることがあります。その意味と再試行方法については、[コールドブート時の再試行](#retry-on-a-cold-boot)を参照してください。

`s.send()` と `s.abort()` は、`ensureReady()` を代わりに呼び出します。

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

### サーバーが認可したランタイムセッションピンを初期値として設定する

サーバーレンダリングされるReactホストは、同じBeeuniセッションにすでに永続化されているランタイムセッションピンを渡せます。

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

この初期値は、`/start` の実行中にキャッシュ済みトランスクリプトの内容をハイドレートするだけです。ランタイムの識別情報を上書きしません。`/start` が返すピンが正となり、古い初期値を置き換えます。

`opencode_session_id` は `runtime_session_id` の非推奨の名称です。どちらも同じ値を持ちます。旧名称は上記のフォールバックとしてのみ読み取ってください。

信頼できないテナントセレクターからこの値を受け取らないでください。ホストでランタイムセッションを作成しないでください。Beeuniがルートセッションを作成して永続化します。ランタイムのクエリキャッシュとトランスクリプトコントローラーはサンドボックスのランタイムをスコープとするため、異なるサンドボックスでランタイムセッションIDが同じでも、キャッシュエントリは共有されません。

セッションを切り替えても、保留中のメッセージの再試行は元のサンドボックスURLを保持します。`404` または `410` のメッセージ応答は、トランスクリプトを消去せずに自動再試行を停止します。明示的な再照合では読み取りを再試行でき、応答が成功すると通常の同期を復元できます。

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

## プロンプトを送信する

```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()` は、セッションの永続的なプロンプト受信箱（`POST .../prompts`）にプロンプトを入れます。これはWebの入力欄とCLIが使う経路です。ターンの終了時ではなく、プロンプトの保存時に、受信箱の行（`prompt_id`、`state`、`message_id`）を返して解決します。返信は `s.stream()` またはトランスクリプトから読み取ってください。

実行環境 RESTセッションでは、ハンドルに対する最初の `send()` が、Beeuniセッションに永続化されたモデルとエージェントを読み取ります。これにより、スナップショットから継承された実行環境セッションが古いスナップショットのデフォルトを再利用するのを防ぎます。

プロンプトの選択の優先順位は次のとおりです。

1. `send()` の呼び出し。
2. ハンドルの `setModel()` または `setAgent()` の値。
3. 永続化されたBeeuniセッションのデフォルト。

`setModel` は、次のローカルな `send` が何を要求するかを選ぶだけで、ハンドルの外には出ません。実行中のセッションの新しいモデルをサーバー側で**永続化**するには、`changeModel` を使います。

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

変更はランタイムの再起動によって反映されるため、進行中のターンは終了します。`applied_live` は、実行中のセッションに即時適用された場合は `true`、次回起動時に適用される場合は `false` です。モデルを変更できるのはセッションの所有者、またはプロジェクトマネージャー権限を持つ呼び出し元のみです。それ以外には `403` が返されます。

`send()` はランタイムを解決してから、プロンプトを送信します。`abort()` はセッションを削除せずに現在の実行を停止します。

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

## セッションのスコープとコスト

保存されたシークレットの制限範囲と、具体化された接続バインディングを読み取ります。
`secrets_allowlist: null` は、エージェントのシークレット許可が適用されることを意味します。

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

`connector_bindings` は解決済みのマップなので、コネクターを上書きしたセッションでも、プロジェクトのデフォルトを継承するセッションでも同じように見えます。スコープを表示したり返送したりする前に、`connector_bindings_configured` を読み取って両者を区別してください。

スコープフィールドの一方または両方を置き換えます。

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

指定した各フィールドは、それまでの値全体を置き換えます。変更しないフィールドは省略してください。接続の変更は、次のツール呼び出しに適用されます。
シークレットを削除すると以後の配信は停止しますが、すでに開示された値をモデルのコンテキストや既存のプロセスから削除することはできません。

どちらの軸にも、明示的にデフォルトへ戻す方法があります。空の値とは異なります。

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

`secrets: []` と `connector_bindings: {}` は逆の指示で、プロジェクトのデフォルトも含めて、明示的に「プロジェクトのシークレットなし」「コネクターを一切使用しない」を指定します。`null` を意図して `{}` を送信したセッションでは、指定しなかったすべてのエイリアスへのアクセスが拒否されます。

統合コストレコードを読み取ります。

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

このレコードには、確定した LLM コスト、請求対象のサンドボックス計算コスト、モデル使用量、トークン合計、計算時間、台帳エントリがまとめられています。`s.cost()` は `ensureReady()` を呼び出しません。

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

## ランタイムの状態とプレビュー

| メソッド                      | 戻り値                        | 用途                                          |
| --------------------------- | ------------------------------ | -------------------------------------------- |
| `s.health(init?)`           | `{ status, ok, health, body }` | ランタイムが稼働しているか確認する           |
| `s.previewUrl(port, path?)` | `string`                       | エージェントが公開したポートのプロキシ URL を取得する |
| `s.proxyUrl(url?)`          | `string \| undefined`          | エージェントが出力した localhost URL を書き換える    |

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

`s.health()` は例外をスローしません。セッションにランタイムがまだない場合も含め、いつでも呼び出せます。`s.previewUrl()` と `s.proxyUrl()` には解決済みのランタイムが必要です。先に `s.ensureReady()` を呼び出してください。そうしないと `SessionNotReadyError` がスローされます。[セッション準備状態のエラー](#session-readiness-errors)を参照してください。

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

### ランタイムがサポートする機能

`health.capabilities` は、ランタイムが提供するセッション機能の一覧です。`runtimeSupports(capabilities, capability)` で機能を読み取り、`false` が返された場合は、そのコントロールを非表示にしてください。実行環境 セッションは全 10 機能を提供します。pi セッションは `session.subagents` のみを提供し、その他には `501 feature_not_supported`（設定ドキュメントには `404`）を返します。実行環境 と pi の完全な対応表は[ハーネス](/docs/ja/work/harnesses)にあります。

| ケイパビリティ | 機能 |
| --- | --- |
| `session.rewind` | メッセージまで巻き戻し、元に戻す |
| `session.compact` | 必要に応じて会話を要約する |
| `session.commands` | プロジェクトのスラッシュコマンド |
| `session.fork` | メッセージの時点でセッションを分岐する |
| `session.subagents` | サブエージェントの子セッション |
| `session.mcp` | ランタイム自身が接続する MCP サーバー |
| `session.todo` | ランタイムの ToDo リスト |
| `session.shell` | ターンとして実行するシェルコマンド |
| `session.attach` | ハーネス独自のターミナルクライアントを接続する |
| `session.config` | ランタイム設定ドキュメント（`/global/config`）。いずれの場合も、エージェントとモデルはプロジェクトから取得されます |

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

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

`runtimeSupports` は、ヘルス応答が届く前と、`session.*` ケイパビリティを列挙しないランタイムに対して、`true` を返します。ケイパビリティの導入前にビルドされたデーモンは 実行環境 を実行し、すべての機能を提供するためです。React では、[`useRuntimeSupports`](/docs/ja/sdk/react)を使用してください。

`start` の応答にも一覧が含まれ、`SessionStartResult.capabilities` は `stage: 'ready'` とともに設定されます。`useSession` がこれを記録するため、最初のヘルスプローブが応答する前から、ケイパビリティによる機能の出し分けが正しく行われます。

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

## ステータス用語

すべてのホストが同じ語彙を使うため、同じ状態はどの画面でも同じ言葉で表示されます。

| エクスポート                                     | 用途                                                                                          |
| ------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `sessionListStatus(session, reviewCount?)` | 一覧内のセッション：`needs-you`、`starting`、`running`、`done`、`stopped`、`failed`、`legacy` |
| `SESSION_LIST_STATUS[status]`              | そのステータスの `{ label, tone, description }`                                               |
| `sessionConnectionLabel(connection)`       | セッションのコンピューターの短いラベル、または `unknown` と `live` の場合は `null`                  |
| `SESSION_NOTICE`                           | コンピューターの準備ができていない間に入力欄に表示する文                                      |
| `turnRetryLabel(secondsLeft)`              | `Retrying in 5s`、`Retrying now`、またはカウントダウンが不明な場合は `Waiting to retry`           |

トーンは `actionable`、`live`、`progress`、`muted`、`danger` です。緑は `live` と `actionable` にのみ使用します。終了したセッションは `muted` です。新しい API が送信した、このビルドでは未知のステータスは `stopped` と表示され、`failed` にはなりません。

<a id="streaming"></a>

## ストリーミング

スクリプトやサーバーでライブイベントを受信するには、`s.stream()` を使用してください。React アプリでは、代わりに [`useSession`](/docs/ja/sdk/react)を使用してください。セッションのライフサイクル全体を管理します。

`s.stream()` は、実行環境 REST 互換のイベントストリームです。Beeuni API がサンドボックスからプロキシします。独立した WebSocket エンドポイントはありません。トランスポートはストリーミング応答ボディを持つ `fetch` で、`ReadableStream` と `TextDecoderStream` を通じて読み取ります。SDK が再接続、バックオフ、ハートビートチェックを処理します。

セッションをストリーミングする手順：

1. 最初に `ensureReady()` を呼び出します。サンドボックスが起動するまで、ランタイムは存在しません。
2. 初期のイベントを取りこぼさないよう、メッセージを送信する前にストリームを開きます。
3. メッセージを送信します。
4. `session.idle` を確認したら、ストリームを閉じます。

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

ストリームは、16 ms 間隔でイベントをバッチ配信します。バッチ内で同じパーツフィールドに対する `message.part.delta` イベントが連続すると、1 つのイベントとして届きます。その `properties.delta` は各イベントのテキストを順番に結合したものです。`id` は最後のイベントの ID です。`coalesced` 配列には、置き換えられたイベントが列挙されます。単一のデルタと同様に、`delta` をパーツのフィールドに追加してください。

ストリーミングには、実際の `ReadableStream` ボディと `TextDecoderStream` を備えた `fetch` が必要です。ブラウザー、Node 18 以降、Bun、Cloudflare Workers はすべてこれをサポートしています。

`fetch` がストリーミングできないホストは、独自のトランスポートを提供します。SDK は接続ごとにそれを 1 回呼び出し、再接続、`Last-Event-ID` による再開、バックオフ、ハートビートは引き続き自身で処理します。

```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 には、デフォルトでストリーミング対応の `fetch` がありません。Beeuni モバイルアプリは、`react-native-sse` を使ってこの拡張ポイントを利用します。範囲を限定した履歴とステータス同期だけが必要なホストでは、引き続き `createHttpSessionSyncController` を利用できます。

コントローラーは、最初に最新の 10 件のメッセージを読み込みます。`loadOlder()` はサーバーのカーソルをたどります。`loadHttpSessionHistory()` は、明示的なエクスポートのためにすべてのカーソルをたどります。

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

### イベントの種類

各イベントには、`type` と、そのデータを保持する `properties` オブジェクトがあります。例：`event.properties.sessionID`。

| `type`                                          | 発火するタイミング                                       |
| ----------------------------------------------- | --------------------------------------------------- |
| `message.updated` / `message.removed`           | メッセージが変更または削除されたとき。                   |
| `message.part.updated` / `message.part.removed` | パート（テキスト、ツール呼び出し、ファイル）が増加または削除されたとき。 |
| `session.status`                                | セッションのビジー状態が変わったとき。                   |
| `session.idle`                                  | ターンが完了したとき。                                  |
| `session.error`                                 | ターンが失敗したとき。イベントにエラーが含まれます。       |
| `question.asked`                                | エージェントが入力を求めたとき。                          |
| `question.replied` / `question.rejected`        | 質問への回答が届いたとき。                   |

`classifyTurn` を使って、生のメッセージとパートを描画可能な出力に変換します。
[SDK リファレンス](/docs/ja/sdk/reference)を参照してください。

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

## コールドブート時の再試行

`ensureReady()` はセッションの `/start` エンドポイントをポーリングします。各呼び出しは最大
30 秒間ロングポーリングし、ランタイムが終端ステージ `ready`/`failed`/`stopped` に達するか、
期限（`readyTimeoutMs`、デフォルトは約 180 秒）が過ぎるまで続けます。ウォームセッションでは、
最初のポーリングで即座に `ready` が返ります。コールドブートでは、サンドボックスが
`retriable: true` を報告している間ポーリングを続けるため、起動が遅くても例外をスローせず、
時間が長くかかるだけです。期限が切れた場合にのみ、`code:
'RUNTIME_UNAVAILABLE'` if the runtime is still not `ready` を伴う `ApiError` を
スローします。

`ensureReady()` は冪等なので、同じセッションへの同時呼び出しは複数のリクエストを送らず、
1 つの `/start` リクエストを共有します。以下の `retryUntilReady` ヘルパーは
現在は任意です。`ensureReady()` がすでに内部で再試行するためですが、デフォルトの
`readyTimeoutMs` より長い合計待機時間を確保したい場合には引き続き役立ちます。

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

`ApiError` の完全な構造は、[エラークラス](#error-classes)を参照してください。React では、
[`useSession`](/docs/ja/sdk/react) が `/start` を再試行するため、このパターンは
不要です。

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

## `/start` が伝える情報

`/start` の各応答が示すのは**その呼び出し**の内容であり、行に蓄積された
履歴ではありません。4 つのフィールドがその情報を伝えます。

| フィールド | 意味 |
| --- | --- |
| `observed_at` | 応答全体で共通の時刻。 |
| `action` | サーバーが行った処理：`inspected`、`checked_provider`、`resumed`、`provisioned`、`restored`、`reconciled`、`awaited_wake`、`cooling_down`。 |
| `observation` | サーバーが確認した内容。`known: false` は**この呼び出しでは確認していない**ことを意味し、「確認したが何も見つからなかった」という意味ではありません。 |
| `boot` | `phase`（`provisioning` / `resuming` / `booting` / `ready` / `parked` / `failed`）、`since`、`actively_starting`。 |

`boot.actively_starting` は「このセッションで今、プロバイダーの操作が実行中か？」に
答えます。`actively_starting: false` を伴う `starting` ペイロードは、
サーバーが再試行のクールダウン終了を待っていることを意味し、マシンが起動中という意味ではありません。

```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>

### 起動に失敗すると自動で再試行されます

起動に失敗すると、恒久的な判定ではなく**クールダウン**が記録されます。クールダウン後の
次の `/start` は、自動で再び起動を試みます。クールダウンは
連続失敗に応じて長くなります（2 分、5 分、10 分）。5 回連続で
失敗すると、`/start` は `stage: "failed"` を返し、試行回数を
`failure.message` に含めます。この判定は最後の失敗から 30 分後に失効し、
`POST …/restart` は即座に解除します。

`retriable` は呼び出しごとに導出されます。サーバーがまだ再試行できる状態には、
`retriable: false` が付くことはありません。

`failure.evidence` は、否定的な結果を出したチェック、その実行時刻、
サーバーの再試行時刻を示します。`/start` の失敗には必ずこれが含まれます。

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

## プロンプトの添付ファイル

ユーザーが入力欄にローカルファイルを追加したらアップロードします。非公開のアップロードは
プロジェクトに属し、実行中のセッションは不要です。

```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 })` はすべてのアップロードが準備できると、`ids` の順序で解決されます。
アップロードが失敗、中止、削除された場合、または `signal` が中止された場合は拒否されます。
`submit(ids)` はエントリをその送信に引き渡します。エントリは `attachments` から外れ、
制限の計算対象ではなくなり、入力欄がアンマウントされてもアップロードを続けます。
POST が成功したら `forget(ids)` を呼び出し、送信に失敗した場合は `reclaim(ids)` を呼び出して
入力欄に戻します。
解決されたファイルパートには `attachment_id`、`filename`、`mime` が含まれます。
URL やファイルのバイトデータは含まれません。サーバーがアップロードを紐付け、実体化します。
画像や PDF を含め、すべてのファイルがセッションのコンピューターに届きます。プロンプトの
実行前にサーバーが `/workspace/uploads/.beeuni-inbox/` に書き込み、
プロンプトはパスで参照します。プロンプト自体にファイルのバイトデータは含まれません。
履歴保存が有効なら、サーバーは会話記録用のコピーも保持します。コピーに失敗しても、
ファイルがコンピューターに届くことは妨げられません。サーバーがファイルを書き込めない場合は
プロンプトが保留され、再試行されます。
これらのパートは、プラットフォームのセッション作成、ウォームセッションの開始、またはプロンプト受信箱の
ルートで使用してください。非公開ハンドルを 実行環境 ランタイムの `sendParts` に直接渡さないでください。
`url` を持つ従来のプラットフォームのファイルパートも引き続き使用できます。

各項目には、安定した `id`、元の `file` への参照、`receivedBytes`、および
ステータス（`pending`、`uploading`、`processing`、`ready`、`error`、`aborted`）があります。
進捗は送信済みバイト数で決まります。コントローラーはパーセントの整数値が変わったときだけ、
アップロードごとに毎秒最大 10 回、進捗スナップショットを公開します。`processing` は
サーバーがアップロードを完了するまで 100% のままになることがあります。制限はファイルごとに 50 MiB、
メッセージごとに 100 MiB、ファイル数は 20 個です。空のファイルは使用できません。デフォルトでは
2 つのファイルを同時にアップロードします。

`retry(id)` は同じハンドルで再開します。`attachment_size_mismatch` または
`attachment_failed` の後はサーバーに使用可能なハンドルが残らないため、`retry(id)` は
File を新しい添付ファイルとして再アップロードします。期限切れのアップロードは再試行できません。`retry(id)` は
例外をスローし、項目のエラーにはコード `attachment_expired` が含まれます。`abort(id)` は
未完了の処理をキャンセルします。
`remove(id)` はエントリを削除し、アップロードを中止して、即座に解決されます。
サーバーの DELETE はベストエフォートで行われ、拒否されることはありません。削除できないアップロードは
24 時間後に失効します。
`forget(ids)` はサーバー上のオブジェクトを削除せずにエントリを解放します。
フックはアンマウント時またはプロジェクト変更時に、一覧内の処理を中止し、アップロードを
ベストエフォートで削除します。どの送信もそれらを保持せず、下書きにもハンドルは残りません。引き渡し済みのアップロードは
続行します。

選択内容はメモリ内にのみ存在します。File オブジェクト、blob URL、署名付き
URL、アップロードハンドルを下書きに永続化しないでください。放置されたアップロードは 24 時間後に失効します。

フレームワークを使わないホストでは、同じメソッドを持つ `beeuni.project(projectId).attachments.createController()` を
使用します。`subscribe(listener)` を通じて購読し、
`getSnapshot()` を読み取ります。入力欄を閉じるときは `dispose()` を呼び出してください。
直接呼び出す場合は `attachments.upload(file, { signal, onProgress, onUpload,
resume })` and `attachments.delete(attachmentId)` を使用できます。
同じ ID で手動再開するために、`onUpload` メタデータを保持してください。

サーバーはハンドルの `upload` フィールドで転送方式を選択します：

- デフォルトは `kind: 'direct'` です。SDK はファイル全体を 1 回の `PUT` で
  `upload.url` に送信します。`upload.headers` を使用し、Authorization ヘッダーは付けません。
  `XMLHttpRequest`（ブラウザー、React Native）では、進捗は送信済みバイト数を報告します。
  `fetch` では、進捗は 0、次にファイルの全サイズを報告します。URL の有効期限が切れた場合、または
  Storage が 400、401、403 で拒否した場合は、同じ
  `attachment_id` に対して 1 回だけ再署名します。Storage からの `409` は、以前の試行でファイルが保存されたことを意味します。
- `kind: 'chunked'` は、認証付きの `PUT` を `upload.chunk_size`
  バイトずつ順に送信します。SDK は正の `chunk_size` をすべて受け付けます。エッジが
  大きなリクエストボディを破棄するデプロイのみが、このモードを選択します。

完了処理では、保存されたサイズと SHA-256 を検証します。完了処理が `409
attachment_not_uploaded`, `onUpload` を返した場合、直接ハンドルを
`received_bytes: 0` とともに報告するため、`retry(id)` または再開によってファイルが再送信されます。
開始、アップロード、完了処理は、タイムアウト、ネットワークエラー、429、
5xx に対して、ジッター付き指数バックオフで再試行します。再試行の制限時間は最初の
失敗から 60 秒なので、長いアップロードが終盤で失敗しても再試行されます。完了処理では
`attachment_processing` も再試行し、制限時間は 5 分です。開始処理は 402
（`BillingError`：アカウントを実行できない）や 429 `attachment_budget_exceeded`（ユーザーあたり
未完了のアップロードが 40 件、または未送信のアップロードが 500 MiB。未使用のアップロードは
24 時間以内に期限切れになります）を再試行しません。サーバーは 1 回の完了処理に対して 105
秒以内に応答または拒否します。各完了リクエストの制限時間は 120 秒です。呼び出し元による中止は再試行しません。

送信済み添付ファイルの参照は、そのプロンプトが配信されてから 1 時間後、
およびそのセッションまたはプロジェクトが削除されたときに解放されます。その後、次のメンテナンス巡回で
ファイルが削除され、その `attachment_id` は送信できなくなります。

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

## ファイル

`s.files` はセッションのサンドボックスを読み書きします：`list`、`read`、`readBlob`、
`status`、`findFiles`、`findText`、`upload`、`create`、`copy`、`remove`、
`mkdir`、`rename`。すべての呼び出しは最初にランタイムを解決し、常に
このセッション自身のサンドボックスを対象にします。メソッドの全一覧は [SDK リファレンス](/docs/ja/sdk/reference) を
参照してください。

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

## セッションの操作メソッド

ハンドルがランタイムの読み取りと応答を代行します。各操作メソッドは最初に
セッションをプロビジョニングし、実行するハーネスに関係なく、このセッション自身のサンドボックスを対象にします。

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

生の REST 互換クライアントである `s.runtime` は非推奨です。上記の操作メソッドを
`send`、`abort`、`rewind`、`stream` とともに使用してください。

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

## プロジェクトセッションをウォームアップする

プロジェクトのランディングページで、最初のプロンプトの前にランタイムを 1 つ準備しておく必要がある場合は、
`ensureWarm()` を呼び出します。

```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()` は、現在のユーザー用に未使用のセッションを 1 つ作成するか、返します。
これは `sessions.create()` が実行するものと同じ作成処理で、プロジェクトのデフォルトを使用します。
課金条件もコネクター要件も同じです。唯一の
違いは `metadata.warm` で、最初のプロンプトが届くまで
`sessions.list()` からセッションを非表示にします。

これは先行準備として扱ってください。`409 WARM_SESSION_UNAVAILABLE` は、プロジェクトを
今はウォームアップできないことを意味します。その場合は `sessions.create()` に進むと、
実際の理由が報告されます。

ウォームセッションはプロジェクトの DEFAULT エージェントとサンドボックスを使用します。ユーザーが
別のものを選んだ場合は、そのセッションを破棄して `sessions.create()` を呼び出してください。未使用のウォーム
セッションは非表示になり、自動的に回収されます。

> `claimWarm()` は非推奨です。ウォームセッションは通常のセッションなので、
> 取得する必要はありません。そのセッションに移動してプロンプトを送信してください。この呼び出しは
> 古い形式に固定された利用側では引き続き動作しますが、次のメジャーバージョンで削除されます。

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

## エラー処理

`createBeeuni` 経由のすべての呼び出しは、型付きの `Error` サブクラスで拒否され、
プレーンオブジェクトは返しません。エラーをキャッチし、`instanceof` を確認して、
`.status` または `.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>

### エラークラス

| クラス                  | 継承元    | スローされる条件                                                                 | 主なフィールド                                                           |
| ---------------------- | ---------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `ApiError`             | `Error`    | リクエスト失敗時のデフォルト：異常ステータス、ネットワーク障害、タイムアウト、中止 | `status`、`code`、`detail`、`response`、`url`、`endpoint`、`timeout` |
| `AuthError`            | `ApiError` | `getToken` が `null` を返した。Beeuni はリクエストを送信していない                      | `code` は常に `'NO_SESSION'`                                      |
| `BillingError`         | `ApiError` | HTTP `402`。唯一の課金エラークラス                                       | `status`（`402`）、`code`、`detail.message`                           |
| `RequestTooLargeError` | `ApiError` | HTTP `431`。通常は 1 回のリクエストに含まれるファイルが多すぎる                              | `detail.suggestion`                                                  |
| `SessionNotReadyError` | `Error`    | `ensureReady()` より前にランタイムアクセサーが実行された                                  | `name` は `'SessionNotReadyError'`                                   |

`ApiError.name` はデフォルトで `'ApiError'` です。次の 2 つの場合は上書きされます。

- `name: 'AbortError'`、`code: 'ABORTED'` — 画面遷移などによってリクエストがキャンセルされました。これは失敗ではありません。無視してください。
- `code: 'TIMEOUT'` — リクエスト自体の制限時間が経過しました。`url`、`endpoint`、`timeout` が、何がタイムアウトしたかを示します。

その他の失敗では、`status` に HTTP ステータスコードが格納されます。`code` はバックエンドの `error_code` から取得され、なければステータスを文字列にしたものになります。`message` は `ApiError` の列挙可能な自身のプロパティなので、`JSON.stringify` やオブジェクトのスプレッドでも保持されます。

Beeuni は、`401` を受け取ったリクエストを、`getToken()` から取得した新しいトークンで再度送信します。ただし、そのトークンが送信済みのものと異なる場合に限ります。一度しか使用できないストリームボディを持つリクエストは再送信しません。

Beeuni は、コードにエラーが届く前に一部のリクエストを再試行します。`GET` または `HEAD` リクエストが `502`、`503`、`504` を返した場合、Beeuni は 250ms、次に 500ms の間隔で最大 2 回再試行します。`GET` または `HEAD` で一時的なトランスポート障害（ステータスコードではなくネットワークエラー）が発生した場合も、同様に再試行します。再試行が成功した場合、`onError` には到達しません。Beeuni は `POST`、`PUT`、`PATCH`、`DELETE` リクエストや、`500` レスポンスを再試行しません。

`getToken()` が `null` を返すと、Beeuni はリクエストを送信する前にクライアント側で `AuthError` をスローします。`AuthError` は `ApiError` を継承するため、`err instanceof ApiError` は引き続き一致します。「未サインイン」をバックエンド障害と別のケースとして扱うには、`err instanceof AuthError` または `err.code === 'NO_SESSION'` を確認してください。

Beeuni は、クレジット不足、プランの上限超過、その他の課金条件による、すべての HTTP `402` レスポンスに対して `BillingError` をスローします。`detail.message` にはバックエンドからの理由が、`code` にはバックエンドの機械可読コード（例：`app_account_unfunded`）が格納されます。`BillingError` は `ApiError` を継承するため、両方を処理する場合は先に `BillingError` を確認してください。

Beeuni は HTTP `431` に対して `RequestTooLargeError` をスローします。これは通常、リクエストに含まれるファイルが多すぎることを意味します。`detail.suggestion` には、そのままユーザーに表示できるヒントが格納されます。

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

### セッションの準備状態に関するエラー

セッションのサンドボックスがまだ準備できていないことを示すエラーは 2 つあります。それぞれ異なる方法で処理してください。

`SessionNotReadyError` は、このセッションハンドルが自身のサンドボックスを解決する前にランタイムアクセサー（`session.previewUrl()` または `session.proxyUrl()`）を呼び出すと、同期的にスローされます。セッションハンドルは自身のサンドボックスのみを解決し、別のセッションのサンドボックスにフォールバックすることはありません。

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

まず `await session.ensureReady()` を呼び出すか、内部でセッションを準備する `send()` を呼び出してください。`session.health()` はこのエラーをスローしない唯一のアクセサーなので、セッションが起動する前でもポーリングできます。

`RUNTIME_UNAVAILABLE` は2つ目のエラーで、`ensureReady()` 自体がコールドブートを待っている間にタイムアウトしたことを意味します。完全なパターンについては、[コールドブート時の再試行](#retry-on-a-cold-boot)を参照してください。React では、`useSession` がこの再試行を行い、スローする代わりに `phase` の値を通じてエラーを公開します。

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

### ヘルパー

| ヘルパー                           | シグネチャ                           | 機能                                                                                                  |
| -------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `parseBillingError(error)`       | `(error) => Error`                  | `402` レスポンスを `BillingError` にラップします。他のエラーはそのまま返します                                  |
| `isBillingError(error)`          | `(error) => boolean`                | `error instanceof BillingError` を返します                                                                       |
| `formatBillingErrorForUI(error)` | `(error) => BillingErrorUI \| null` | 課金関連以外のエラーでは `null` を返します。それ以外では、アップグレードモーダル用に `{ alertTitle, alertSubtitle }` を返します |
| `isRuntimeNotReadyResponse(error)` | `(error) => boolean` | セッションランタイムがまだリクエストを受け付けられない間、サンドボックスデーモンの `503` に対して true を返します。リクエストは転送されていないため、安全に再試行できます。実行環境 と pi で同じ結果になります |
| `isSandboxNotReadyError(error)` | `(error) => boolean` | 上記のケース、および停止中、起動中、または待機状態のサンドボックスに対して true を返します。「起動中」と表示し、ポーリングを続けてください |
| `isRuntimeStartingError(error)` | `(error) => boolean` | 上記の両方のケース、およびまだ固定されていないランタイム URL に対して true を返します。エラーバウンダリはクラッシュを表示する代わりに再試行します |

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

<a id="in-beeunisdkreact"></a>

### `@beeuni/sdk/react` での利用

`@beeuni/sdk/react` は `BillingError`、`RequestTooLargeError`、`parseBillingError`、`isBillingError`、`formatBillingErrorForUI` を再エクスポートします。`ApiError` と `AuthError` は再エクスポートしません。これらは `@beeuni/sdk` からインポートしてください。

`useSession` は、`send`、`answerQuestion`、`answerPermission`、`rejectQuestion` のすべての失敗を1つの `sendError` オブジェクトに分類するため、`instanceof` のチェックを手動で記述する必要はありません。

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

`useSession` のその他の内容については、[React フック](/docs/ja/sdk/react)を参照してください。
