会话
运行会话、流式传输其事件,并处理它可能引发的错误。
会话是一次智能体运行,拥有独立的沙箱和 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 会话中持久化的模型和智能体。这可防止从快照继承的 运行环境 会话复用过时的快照默认值。
提示选项的优先级为:
send()调用。- 句柄的
setModel()或setAgent()值。 - 持久化的 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 负责重连、退避和心跳检查。
流式接收会话事件:
- 先调用
ensureReady()。沙箱启动之前,运行时并不存在。 - 在发送消息之前打开事件流,以免错过早期事件。
- 发送消息。
- 看到
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。