跳到内容
BeeuniBeeuni
中文
Esc
↑↓导航↵打开⌘J预览
本页内容

会话

运行会话、流式传输其事件,并处理它可能引发的错误。

会话是一次智能体运行,拥有独立的沙箱和 git 分支。 beeuni.session(projectId, sessionId) 返回用于执行会话所有操作的句柄:启动会话、发送提示、流式接收事件以及读取状态。本页介绍该句柄、就绪握手、流式传输,以及 SDK 调用可能抛出的类型化错误。

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

s 是用于执行会话所有操作的句柄。会话 ID、沙箱 ID 和分支名称的值相同。概念说明请参阅 会话。

会话生命周期

方法 封装 功能
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 读取你是否接收此会话的通知。需要 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 恢复项目默认值

标签和元数据

每个会话都带有 labels: string[] 和一个 metadata 对象。用它们对会话分类,并决定应用如何呈现会话。

  • 标签可自由定义:每个标签会去除首尾空白,长度为 1–64 个字符,每个会话最多 20 个,重复项会被移除。匹配为精确匹配,区分大小写。
  • 元数据是供你自定义键的自由格式 JSON 对象,每次写入最多 16,384 个 JSON 字符。服务器也会在其中存储自己的键(custom_name、source、…);指定服务器管理的键的请求会被拒绝,并返回 400。为你的键添加前缀(app_…),以避免冲突。
// 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 后,会话可以为一个提供商选择多个可用的账户密钥。通过 beeuni.accounts.secretResources.create(accountId, { project_id: projectId, ...input }) 创建项目范围的资源。 默认情况下,每个项目成员都可以使用它。通过 .setAccess(accountId, secretId, 'members', userIds) 限制使用权限。读取操作返回元数据和授权,绝不会返回密钥值。如果提供商在输出开始前发生 429,网关会尝试另一个已选择的密钥。

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

调用方需要显示重新加载进度时,请使用流式方法:

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。重新加载结果携带相同对象。

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 有三种状态。null 表示 Beeuni 无法判断。应检查 stale === true,切勿检查 !stale。旧版 API 会省略 release。请参阅 配置收敛。

s.participants() 返回 { participants, total, multi_user }。participants 列出当前可以打开会话的人,所有者排在首位,最多 20 人,每人均为 { user_id, name, email, avatar_url, is_viewer };total 是完整人数。 当有两人或更多人可以打开会话时,multi_user 为 true。在 React 中,useSessionParticipants(projectId, sessionId) 读取相同数据。每条消息的作者由 s.messageAuthors() 表示。

该句柄还提供另外三个读取方法:

  • s.previews() — 运行时公开的候选预览端口。
  • s.publicShares.list() / .create(input) / .revoke(shareId) — 公开分享链接。
  • s.reminders.list() / .create(input) / .update(reminderId, { enabled }) / .remove(reminderId) — 定时发送到此会话的提示。参见提醒。 .create({ transcript: true }) 创建对话的只读链接(每个会话只有一个有效链接;再次调用会返回同一链接)。使用 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 保留,包括文本、推理,以及每次工具调用的输入、输出、错误和元数据。沙箱停止或仍在唤醒时也能返回结果,无需启动沙箱。传入 limit(最多 500)获取最新窗口,将某个窗口的 next_cursor 作为 before 传入可读取其前一个窗口。窗口也受 1,000,000 个 JSON 字符的限制:它保留最新的消息,此时 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。

会话存储的文件

findSessionAttachments(messages) 按顺序列出对话记录引用的所有已存储文件,每个文件只列出一次:用户上传的文件、提示中的 <file> 引用,或保存的历史记录中保留的智能体展示文件副本。它既能读取实时对话记录,也能读取已保存的记录,且不会因记录格式错误而抛出异常。

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,字节本身携带类型信息。

每条消息由谁撰写

每条消息都有作者。服务端从发送提示的凭据中读取作者:项目成员,或另一会话的智能体。运行时对话记录不携带此信息。

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(其运行的智能体,如有名称)

哪个模型回答了某一轮

对话记录中的消息会标明该轮请求的模型。当项目的回退链 使用其他模型回答时,只有网关的请求记录会保存实际回答的模型及 Beeuni 的计费金额。

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) 读取相同数据。传入对话记录的消息数。只有新消息才会添加作者,因此消息数变化时,查询会重新获取数据。

就绪需要握手确认

发送提示前,调用 ensureReady()。它会在需要时配置沙箱,等待运行时启动,并返回解析得到的运行时。

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

冷启动时,ensureReady() 可能抛出 RUNTIME_UNAVAILABLE。参见冷启动时重试,了解其含义及重试方法。

s.send() 和 s.abort() 会代你调用 ensureReady()。

预置服务端授权的运行时会话固定值

服务端渲染的 React 宿主可以提供已为同一 Beeuni 会话持久化的运行时会话固定值:

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 消息响应会停止自动重试,但不会清除对话记录。显式协调可以重试读取,并在响应成功后恢复正常同步。

发送提示

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),这也是网页输入框和 CLI 使用的路径。它在提示存储完成时返回收件箱行(prompt_id、state、message_id),而不是在该轮结束时返回。从 s.stream() 或对话记录中读取回复。

对于 运行环境 REST 会话,句柄上的首次 send() 会读取 Beeuni 会话中持久化的模型和智能体。这可防止从快照继承的 运行环境 会话复用过时的快照默认值。

提示选项的优先级为:

  1. send() 调用。
  2. 句柄的 setModel() 或 setAgent() 值。
  3. 持久化的 Beeuni 会话默认值。

setModel 仅选择下一次本地 send 所请求的内容,不会传出句柄。要在服务端为正在运行的会话持久化新模型,请使用 changeModel:

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

此更改通过重启运行时生效,因此正在进行的轮次会结束。 如果正在运行的会话已立即应用更改,applied_live 为 true;如果将在下次启动时应用,则为 false。只有会话所有者或具有项目管理者权限的调用者可以更改模型;其他人会收到 403。

send() 解析运行时,然后向其发送提示。abort() 停止当前运行,但不删除会话。

会话作用域与费用

读取已存储的密钥范围限制和已落实的连接绑定。 secrets_allowlist: null 表示应用智能体的密钥授权:

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

connector_bindings 是解析后的映射,因此,覆盖了连接器的会话与继承项目默认设置的会话在这里看起来相同。在渲染作用域或将其发回之前,读取 connector_bindings_configured 以区分两者。

替换一个或两个作用域字段:

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

每个提供的字段都会完整替换其先前的值。省略字段可保持其不变。连接更改将在下一次工具调用时生效。 移除密钥会停止后续传递,但无法从模型上下文或现有进程中移除已披露的值。

两个维度都有明确的方式恢复默认设置。这与空值不同:

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,却发送了 {},则所有未指明的别名都会按默认拒绝原则被禁用。

读取统一费用记录:

const cost = await s.cost();

该记录汇总了已结算的 LLM 费用、已计费的沙箱计算费用、模型用量、token 总数、计算时长和账本条目。s.cost() 不会调用 ensureReady()。

运行时状态与预览

方法 返回值 用途
s.health(init?) { status, ok, health, body } 检查运行时是否存活
s.previewUrl(port, path?) string 获取智能体暴露端口的代理 URL
s.proxyUrl(url?) string | undefined 重写智能体输出的 localhost URL
const { ok, health } = await s.health();
const url = s.previewUrl(3000, '/docs');

s.health() 从不抛出异常。可随时调用,即使会话尚无运行时。s.previewUrl() 和 s.proxyUrl() 需要已解析的运行时——请先调用 s.ensureReady(),否则它们会抛出 SessionNotReadyError。参见会话就绪错误。

运行时支持的功能

health.capabilities 列出运行时提供的会话功能。使用 runtimeSupports(capabilities, capability) 读取某项功能,在返回 false 时隐藏对应控件。运行环境 会话提供全部十项功能;pi 会话仅提供 session.subagents,其他功能均返回 501 feature_not_supported(配置文档返回 404)。Harnesses 提供完整的 运行环境 和 pi 功能矩阵。

能力 功能
session.rewind 回退到某条消息,并恢复它
session.compact 按需总结对话
session.commands 项目斜杠命令
session.fork 在某条消息处分叉会话
session.subagents 子智能体的子会话
session.mcp 运行时自行连接的 MCP 服务器
session.todo 运行时的待办列表
session.shell 作为一个轮次运行的 shell 命令
session.attach 附加 harness 自带的终端客户端
session.config 运行时配置文档(/global/config);无论哪种情况,智能体和模型都来自项目
import { runtimeSupports } from '@beeuni/sdk';

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

在健康检查响应到达之前,以及运行时未列出 session.* 能力时,runtimeSupports 返回 true:在能力机制出现之前构建的守护进程运行的是 运行环境,它提供所有功能。在 React 中,使用 useRuntimeSupports。

start 响应也携带该列表:SessionStartResult.capabilities 通过 stage: 'ready' 设置。useSession 会记录它,因此在首次健康探测响应到达之前,能力门控就已准确。

状态用词

所有宿主使用同一套词汇,因此同一种状态在每个界面上都使用同一个词。

导出 用途
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。

流式传输

在脚本或服务器中使用 s.stream() 接收实时事件。在 React 应用中,改用 useSession——它会为你管理整个会话生命周期。

s.stream() 是兼容 运行环境 REST 的事件流。Beeuni API 从沙箱代理该事件流。没有单独的 WebSocket 端点。传输方式为 fetch,带有流式响应体,通过 ReadableStream 和 TextDecoderStream 读取。SDK 负责重连、退避和心跳检查。

流式接收会话事件:

  1. 先调用 ensureReady()。沙箱启动之前,运行时并不存在。
  2. 在发送消息之前打开事件流,以免错过早期事件。
  3. 发送消息。
  4. 看到 session.idle 时关闭事件流。
const session = beeuni.session(projectId, sessionId);
const { runtimeSessionId } = await session.ensureReady();

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

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

事件流按批次传递事件,批次间隔为 16 ms。同一批次中,针对同一个 part 字段的连续 message.part.delta 事件会合并为一个事件。其 properties.delta 是按顺序拼接的文本。其 id 是最后一个事件的 id。其 coalesced 数组列出被替代的事件。像处理单个增量一样,将 delta 追加到该 part 的字段。

流式传输需要 fetch,以及真正的 ReadableStream 响应体和 TextDecoderStream。浏览器、Node 18 及更高版本、Bun 和 Cloudflare Workers 均支持。

如果宿主的 fetch 无法进行流式传输,则需提供自己的传输实现。SDK 每次连接调用它一次,并自行管理重连、Last-Event-ID 续传、退避和心跳:

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() 会遍历所有游标,用于显式导出。

事件类型

每个事件都有一个 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 参考。

冷启动时重试

ensureReady() 轮询会话的 /start 端点——每次调用最多长轮询 30 秒——直到运行时进入终止阶段 ready/failed/stopped, 或超过截止时间(readyTimeoutMs,默认约 180 秒)。对于已预热的会话, 首次轮询会立即以 ready 完成。冷启动时,只要沙箱报告 retriable: true,它就会持续轮询,因此启动缓慢只会增加等待时间, 而不会抛出异常。只有在截止时间到期时,它才会抛出带有 code: 'RUNTIME_UNAVAILABLE' if the runtime is still not ready 的 ApiError。

ensureReady() 是幂等的,因此针对同一会话的并发调用会共享 一个 /start 请求,而不是发送多个请求。下面的 retryUntilReady 辅助函数 现在是可选的——ensureReady() 已在内部重试——但如果你需要比默认 readyTimeoutMs 更长的总等待时间,它仍然有用。

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 结构请参见错误类。在 React 中, useSession 会替你重试 /start,因此无需 采用此模式。

/start 告诉你什么

每个 /start 响应描述的都是本次调用,而非该行的累积 历史。这些信息由四个字段承载。

字段 含义
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 载荷表示 服务器正在等待重试冷却期结束,而不是计算机正在启动。

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

启动失败后会自动重试

启动失败会标记一个冷却期,而非永久判定。冷却期结束后的下一次 /start 会自行重新尝试唤醒。冷却期随连续失败次数 增加(2 分钟、5 分钟、10 分钟)。连续失败五次后, /start 会返回 stage: "failed",并在 failure.message 中包含尝试次数;该判定在最后一次失败 30 分钟后过期, POST …/restart 则会立即清除它。

每次调用都会推导 retriable。服务器仍可重新尝试的状态 绝不会携带 retriable: false。

failure.evidence 指明产生否定结果的检查、检查执行时间以及 服务器重试时间。每个 /start 失败都会携带它。

提示词附件

用户将本地文件添加到输入框时上传这些文件。私有上传归属于 项目,无需正在运行的会话。

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。 进度由已发送的字节数决定。控制器仅在进度变化达到整数百分比时发布进度快照, 每个上传每秒最多十次。processing 可能 一直保持在 100%,直到服务器完成上传。限额为每个 文件 50 MiB、每条消息 100 MiB,以及 20 个文件。文件不能为空。默认 同时上传两个文件。

retry(id) 使用同一句柄恢复上传。在 attachment_size_mismatch 或 attachment_failed 之后,服务器不再保留可用句柄,因此 retry(id) 会将 File 作为新附件重新上传。过期的上传无法重试:retry(id) 会抛出异常,条目错误会携带代码 attachment_expired。abort(id) 会取消 未完成的工作。 remove(id) 移除条目、中止其上传,并立即兑现。 服务器 DELETE 会尽力执行,且绝不会拒绝;无法删除的上传 会在 24 小时后过期。 forget(ids) 释放条目,但不删除服务器对象。 此 hook 会在卸载或项目变更时中止列表中的工作,并尽力删除其上传: 没有发送操作持有它们,草稿也不保留句柄。已移交的上传 会继续进行。

所选内容仅保存在内存中。切勿在草稿中持久化 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)。 保留 onUpload 元数据,以便手动使用同一 ID 恢复上传。

服务器通过句柄的 upload 字段选择传输方式:

  • kind: 'direct' 为默认值。SDK 会将整个文件作为一个 PUT 发送到 upload.url,并携带 upload.headers,不带 Authorization 标头。使用 XMLHttpRequest(浏览器、React Native)时,进度报告已发送的字节数。使用 fetch 时,进度先报告 0,然后报告完整大小。过期的 URL,或 Storage 以 400、401 或 403 拒绝的 URL,会针对相同的 attachment_id 重新签名一次。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,预算为五分钟。发起操作绝不会重试 402 (BillingError:账户无法运行)或 429 attachment_budget_exceeded(用户有 40 个未完成的上传,或有 500 MiB 未发送的上传;未使用的上传会在 24 小时内过期)。服务器会在 105 秒内响应或拒绝一次完成操作。每个完成请求允许 120 秒。调用方中止操作绝不会重试。

已发送附件的引用会在其提示发送后 1 小时释放,也会在其会话或项目被删除时释放。下一次维护扫描随后会删除文件,其 attachment_id 将无法再发送。

文件

s.files 读取和写入会话的沙盒:list、read、readBlob、 status、findFiles、findText、upload、create、copy、remove、 mkdir、rename。每次调用都会先解析运行时,并始终以该会话自己的沙盒为目标。请参阅 SDK reference 以获取完整的方法列表。

会话动词

句柄会代你读取运行时并作出响应。每个动词都会先配置会话,并以该会话自己的沙盒为目标,无论运行它的是哪种 harness。

// 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(原始 REST 兼容客户端)已弃用。请使用上述动词以及 send、abort、rewind 和 stream。

预热项目会话

当项目落地页需要在首次提示前准备好一个运行时时,请调用 ensureWarm()。

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() 会为当前用户创建或返回一个未使用的会话。它与创建 sessions.create() 运行的操作相同,并采用项目的默认设置:相同的计费门槛、相同的连接器要求。唯一的区别是 metadata.warm,它会在首次提示到达前将会话隐藏于 sessions.list()。

请将其视为推测性操作。409 WARM_SESSION_UNAVAILABLE 表示项目当前无法预热——请转而调用 sessions.create(),该操作会报告实际原因。

预热会话携带项目的 DEFAULT agent 和沙盒。如果用户选择了不同的选项,请放弃该会话并调用 sessions.create():未使用的预热会话会被隐藏并自行回收。

处理错误

通过 createBeeuni 进行的每次调用都会拒绝并抛出一个带类型的 Error 子类,绝不会是普通对象。捕获错误,检查 instanceof,然后根据 .status 或 .code 分支处理。

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

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

错误类

类 继承自 抛出时机 关键字段
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。通常表示单个请求中的文件过多 detail.suggestion
SessionNotReadyError Error 运行时访问器在 ensureReady() 之前运行 name 为 'SessionNotReadyError'

ApiError.name 默认为 'ApiError'。以下两种情况会覆盖该值:

  • name: 'AbortError'、code: 'ABORTED' —— 请求已取消,例如因导航而取消。这不是失败。请忽略它。
  • code: 'TIMEOUT' —— 请求自身的超时时间已到。url、endpoint 和 timeout 会显示超时的内容。

对于任何其他失败,status 保存 HTTP 状态码。code 来自后端的 error_code,如果没有则回退为字符串形式的状态码。message 是 ApiError 上可枚举的自有属性,因此会在 JSON.stringify 和对象展开后保留。

当 Beeuni 收到 401 时,如果该令牌不同于它发送的令牌,Beeuni 会使用从 getToken() 获取的新令牌再次发送请求。带有一次性流正文的请求不会重新发送。

Beeuni 会在代码看到错误之前重试某些请求。如果 GET 或 HEAD 请求返回 502、503 或 504,Beeuni 会最多重试 2 次,延迟分别为 250 毫秒和 500 毫秒。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',将“未登录”与后端故障区分开来。

对于每个 HTTP 402 响应,Beeuni 都会抛出 BillingError:积分不足、超出套餐限制或其他计费门槛。detail.message 保存后端提供的原因,code 保存后端的机器代码(例如 app_account_unfunded)。BillingError 继承自 ApiError,因此同时处理两者时请先检查 BillingError。

对于 HTTP 431,Beeuni 会抛出 RequestTooLargeError。这通常表示请求携带了过多文件。detail.suggestion 保存可直接向用户展示的提示。

会话就绪错误

有两个错误表示会话的沙盒尚未就绪。请分别处理它们。

在此会话句柄尚未解析出其沙箱之前,如果调用运行时访问器(session.previewUrl() 或 session.proxyUrl()),SessionNotReadyError 会同步抛出错误。会话句柄只会解析自己的沙箱,绝不会回退到其他会话的沙箱。

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 是第二种错误,表示 ensureReady() 本身在等待冷启动时超时。完整模式请参阅冷启动时重试。在 React 中,useSession 会自动为你重试,并通过 phase 值暴露此错误,而不是抛出它。

辅助函数

辅助函数 签名 作用
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。错误边界会据此重试,而不是显示崩溃
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);
}

在 @beeuni/sdk/react 中

@beeuni/sdk/react 重新导出 BillingError、RequestTooLargeError、parseBillingError、isBillingError 和 formatBillingErrorForUI。它不重新导出 ApiError 或 AuthError,请从 @beeuni/sdk 导入它们。

useSession 将每个 send、answerQuestion、answerPermission 和 rejectQuestion 失败归类到一个 sendError 对象中,因此无需手动编写 instanceof 检查:

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

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

有关 useSession 的其余内容,请参阅 React hooks。

这个页面有帮助吗?